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
@@ -848,6 +848,7 @@ impl OpenCADStudio {
|
|||||||
".{}.tmp.dwg",
|
".{}.tmp.dwg",
|
||||||
path.file_name().unwrap().to_string_lossy()
|
path.file_name().unwrap().to_string_lossy()
|
||||||
));
|
));
|
||||||
|
self.tabs[i].scene.update_fields(2, None);
|
||||||
let document = self.tabs[i].scene.document_for_save();
|
let document = self.tabs[i].scene.document_for_save();
|
||||||
if let Err(e) = crate::io::save(&document, &scratch) {
|
if let Err(e) = crate::io::save(&document, &scratch) {
|
||||||
return err(format!("save: {e}"));
|
return err(format!("save: {e}"));
|
||||||
|
|||||||
@@ -512,6 +512,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
// Rebuild tessellation caches without modifying the document.
|
// Rebuild tessellation caches without modifying the document.
|
||||||
"REGEN" | "REGENALL" => {
|
"REGEN" | "REGENALL" => {
|
||||||
|
// A regen is a field evaluation event (FIELDEVAL bit 16).
|
||||||
|
self.tabs[i].scene.update_fields(16, None);
|
||||||
self.tabs[i].scene.populate_meshes_from_document();
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref());
|
self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
|
|||||||
@@ -17,6 +17,80 @@ impl OpenCADStudio {
|
|||||||
self.open_field_dialog(FieldTarget::NewText);
|
self.open_field_dialog(FieldTarget::NewText);
|
||||||
Some(Task::none())
|
Some(Task::none())
|
||||||
}
|
}
|
||||||
|
// UPDATEFIELD — re-evaluate the fields of the selected objects
|
||||||
|
// (a block reference's attributes included), whatever FIELDEVAL.
|
||||||
|
"UPDATEFIELD" => {
|
||||||
|
let selected: Vec<codec::Handle> =
|
||||||
|
self.tabs[i].scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
||||||
|
if selected.is_empty() {
|
||||||
|
use crate::modules::draw::select::SelectObjectsCommand;
|
||||||
|
let cmd = SelectObjectsCommand::new("UPDATEFIELD");
|
||||||
|
self.command_line.push_info(&cmd.prompt());
|
||||||
|
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 {
|
||||||
|
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");
|
||||||
|
let found = self.tabs[i].scene.update_fields(32, Some(&hosts));
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
self.refresh_properties();
|
||||||
|
if found > 0 {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
self.command_line.push_output(crate::tf!("{} field(s) found.", found).as_ref());
|
||||||
|
self.command_line.push_output(crate::tf!("{} field(s) updated.", found).as_ref());
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
|
// FIELDEVAL — the events that update fields, kept in the drawing.
|
||||||
|
cmd if cmd == "FIELDEVAL" || cmd.starts_with("FIELDEVAL ") || cmd.starts_with("SETVAR FIELDEVAL") => {
|
||||||
|
let value = cmd
|
||||||
|
.trim_start_matches("SETVAR ")
|
||||||
|
.trim_start_matches("FIELDEVAL")
|
||||||
|
.trim();
|
||||||
|
let current = crate::entities::field::fieldeval(&self.tabs[i].scene.document);
|
||||||
|
let ask = |app: &mut Self| {
|
||||||
|
app.command_line
|
||||||
|
.push_output(&format!("Enter new value for FIELDEVAL <{current}>:"));
|
||||||
|
app.pending_setvar = Some("FIELDEVAL".into());
|
||||||
|
};
|
||||||
|
match value.parse::<i32>() {
|
||||||
|
_ if value.is_empty() => ask(self),
|
||||||
|
Ok(n) if (0..=31).contains(&n) => {
|
||||||
|
if n != current {
|
||||||
|
self.push_undo_snapshot(i, "FIELDEVAL");
|
||||||
|
crate::io::set_drawing_variable(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
"FIELDEVAL",
|
||||||
|
&n.to_string(),
|
||||||
|
);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.command_line.push_error("Requires an integer between 0 and 31.");
|
||||||
|
ask(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
cmd if cmd == "_FIELD_OBJECT" || cmd.starts_with("_FIELD_OBJECT ") => {
|
cmd if cmd == "_FIELD_OBJECT" || cmd.starts_with("_FIELD_OBJECT ") => {
|
||||||
let handle = cmd
|
let handle = cmd
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
|
|||||||
@@ -708,6 +708,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"PDFOSNAP",
|
"PDFOSNAP",
|
||||||
"UOSNAP",
|
"UOSNAP",
|
||||||
"FIELDDISPLAY",
|
"FIELDDISPLAY",
|
||||||
|
"FIELDEVAL",
|
||||||
|
"UPDATEFIELD",
|
||||||
"PDFIMPORTMODE",
|
"PDFIMPORTMODE",
|
||||||
"PDFIMPORTFILTER",
|
"PDFIMPORTFILTER",
|
||||||
"PDFIMPORTLAYERS",
|
"PDFIMPORTLAYERS",
|
||||||
|
|||||||
+10
-3
@@ -2163,6 +2163,14 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
||||||
|
// A save is a field evaluation event (FIELDEVAL bit 2).
|
||||||
|
self.tabs[i].scene.update_fields(2, None);
|
||||||
|
self.prepare_snapshot_save(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bring the drawing state the file stores up to date, without the save
|
||||||
|
/// event an autosave does not raise.
|
||||||
|
pub(in crate::app) fn prepare_snapshot_save(&mut self, i: usize) {
|
||||||
self.sync_view_state_for_save(i);
|
self.sync_view_state_for_save(i);
|
||||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||||
self.stamp_header_sysvars(i);
|
self.stamp_header_sysvars(i);
|
||||||
@@ -3297,7 +3305,7 @@ impl OpenCADStudio {
|
|||||||
if !self.tabs[i].dirty || self.active_save_jobs.contains_key(&self.tabs[i].id) {
|
if !self.tabs[i].dirty || self.active_save_jobs.contains_key(&self.tabs[i].id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.prepare_native_save(i);
|
self.prepare_snapshot_save(i);
|
||||||
let version = self.tabs[i].scene.document.version;
|
let version = self.tabs[i].scene.document.version;
|
||||||
let target = self.autosave_target(i);
|
let target = self.autosave_target(i);
|
||||||
tasks.push(self.queue_native_save(
|
tasks.push(self.queue_native_save(
|
||||||
@@ -3800,8 +3808,7 @@ impl OpenCADStudio {
|
|||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let hosts = crate::entities::field::stamp_plot_fields(&mut self.tabs[i].scene.document);
|
let hosts = crate::entities::field::stamp_plot_fields(&mut self.tabs[i].scene.document);
|
||||||
if !hosts.is_empty() {
|
if !hosts.is_empty() {
|
||||||
let changes: Vec<_> = hosts.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect();
|
self.tabs[i].scene.bump_text_hosts(&hosts);
|
||||||
self.tabs[i].scene.bump_entities(&changes);
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,21 @@ fn mtext_flag_str(f: MTextFlag) -> &'static str {
|
|||||||
/// Render text strokes for an attribute, honouring alignment, oblique angle,
|
/// Render text strokes for an attribute, honouring alignment, oblique angle,
|
||||||
/// width factor, generation flags (backward / upside-down), text-style
|
/// width factor, generation flags (backward / upside-down), text-style
|
||||||
/// resolution, and basic multiline splitting on `\n` / `\\P`.
|
/// resolution, and basic multiline splitting on `\n` / `\\P`.
|
||||||
fn build_attr_render(mut input: AttrTextInputs<'_>, document: &codec::CadDocument) -> RenderEntity {
|
/// Laid-out character cells of a multi-line (MTEXT) attribute: the layout's
|
||||||
|
/// glyph boxes, the point they are relative to and the layout rotation.
|
||||||
|
pub(crate) type AttrCells = (Vec<crate::entities::text_support::GlyphBox>, [f64; 2], f32);
|
||||||
|
|
||||||
|
fn build_attr_render(input: AttrTextInputs<'_>, document: &codec::CadDocument) -> RenderEntity {
|
||||||
|
build_attr_layout(input, document, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`build_attr_render`], also handing back a multi-line attribute's
|
||||||
|
/// character cells in `cells` (left `None` for a single-line one).
|
||||||
|
fn build_attr_layout(
|
||||||
|
mut input: AttrTextInputs<'_>,
|
||||||
|
document: &codec::CadDocument,
|
||||||
|
mut cells: Option<&mut Option<AttrCells>>,
|
||||||
|
) -> RenderEntity {
|
||||||
let normal = (input.normal.x, input.normal.y, input.normal.z);
|
let normal = (input.normal.x, input.normal.y, input.normal.z);
|
||||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||||
(
|
(
|
||||||
@@ -248,8 +262,11 @@ fn build_attr_render(mut input: AttrTextInputs<'_>, document: &codec::CadDocumen
|
|||||||
exact_line_spacing: false,
|
exact_line_spacing: false,
|
||||||
rectangle_height: 0.0,
|
rectangle_height: 0.0,
|
||||||
vertical_text: false,
|
vertical_text: false,
|
||||||
want_glyph_boxes: false,
|
want_glyph_boxes: cells.is_some(),
|
||||||
});
|
});
|
||||||
|
if let Some(out) = cells.as_deref_mut() {
|
||||||
|
*out = Some((layout.glyph_boxes.clone(), [anchor_pt.x, anchor_pt.y], rotation));
|
||||||
|
}
|
||||||
let _ = input.line_count;
|
let _ = input.line_count;
|
||||||
let _ = input.is_multiline;
|
let _ = input.is_multiline;
|
||||||
return RenderEntity {
|
return RenderEntity {
|
||||||
@@ -839,6 +856,35 @@ impl Transformable for AttributeDefinition {
|
|||||||
|
|
||||||
// ── AttributeEntity ───────────────────────────────────────────────────────────
|
// ── AttributeEntity ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The laid-out character cells of a multi-line (MTEXT) attribute, the
|
||||||
|
/// point they are relative to and the layout rotation; `None` for a
|
||||||
|
/// single-line attribute.
|
||||||
|
pub(crate) fn attribute_cells(a: &AttributeEntity, document: &codec::CadDocument) -> Option<AttrCells> {
|
||||||
|
let mut out = None;
|
||||||
|
build_attr_layout(attr_inputs(a), document, Some(&mut out));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr_inputs(a: &AttributeEntity) -> AttrTextInputs<'_> {
|
||||||
|
AttrTextInputs {
|
||||||
|
value: &a.value,
|
||||||
|
insertion_point: a.insertion_point,
|
||||||
|
alignment_point: a.alignment_point,
|
||||||
|
height: a.height,
|
||||||
|
rotation: a.rotation,
|
||||||
|
width_factor: a.width_factor,
|
||||||
|
oblique_angle: a.oblique_angle,
|
||||||
|
text_style: &a.text_style,
|
||||||
|
text_generation_flags: a.text_generation_flags,
|
||||||
|
horizontal_alignment: a.horizontal_alignment,
|
||||||
|
vertical_alignment: a.vertical_alignment,
|
||||||
|
normal: a.normal,
|
||||||
|
mtext_flag: a.mtext_flag,
|
||||||
|
is_multiline: a.is_multiline,
|
||||||
|
line_count: a.line_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderConvertible for AttributeEntity {
|
impl RenderConvertible for AttributeEntity {
|
||||||
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
||||||
Some(build_attr_render(
|
Some(build_attr_render(
|
||||||
|
|||||||
+31
-7
@@ -108,12 +108,36 @@ pub fn resolve(document: &CadDocument, host: Handle) -> Option<String> {
|
|||||||
codec::fields::resolve(document, host, &OcsFieldContext(Some(document)))
|
codec::fields::resolve(document, host, &OcsFieldContext(Some(document)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_handle(
|
/// FIELDEVAL of the drawing (kept in its variable dictionary; default 31):
|
||||||
document: &CadDocument,
|
/// the events that update fields — 1 open, 2 save, 4 plot, 8 eTransmit,
|
||||||
field: Handle,
|
/// 16 regen.
|
||||||
host: Handle,
|
pub fn fieldeval(document: &CadDocument) -> i32 {
|
||||||
) -> Option<String> {
|
crate::io::drawing_variable(document, "FIELDEVAL")
|
||||||
codec::fields::resolve_handle(document, field, host, &OcsFieldContext(Some(document)))
|
.and_then(|v| v.trim().parse().ok())
|
||||||
|
.unwrap_or(31)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the fields an event evaluates and store their values in the host
|
||||||
|
/// texts: `event` 1 open, 2 save or 16 regen (masked with FIELDEVAL), or 32
|
||||||
|
/// an explicit UPDATEFIELD. `hosts` limits it to those hosts. Returns the
|
||||||
|
/// hosts whose text changed and the number of fields the hosts hold.
|
||||||
|
pub fn update_fields(
|
||||||
|
document: &mut CadDocument,
|
||||||
|
event: i32,
|
||||||
|
hosts: Option<&[Handle]>,
|
||||||
|
) -> (Vec<Handle>, usize) {
|
||||||
|
let event = if event == 32 { event } else { event & fieldeval(document) };
|
||||||
|
if event == 0 || document.fields.is_empty() {
|
||||||
|
return (Vec::new(), 0);
|
||||||
|
}
|
||||||
|
let snapshot = document.clone();
|
||||||
|
document.update_fields(&OcsFieldContext(Some(&snapshot)), event, hosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 `%<…>%`
|
/// Evaluate one field code (`\AcVar Date \f "yyyy-MM-dd"`, or the `%<…>%`
|
||||||
@@ -477,7 +501,7 @@ pub fn stamp_plot_fields(document: &mut CadDocument) -> Vec<Handle> {
|
|||||||
.objects
|
.objects
|
||||||
.values()
|
.values()
|
||||||
.any(|object| matches!(object, codec::objects::ObjectType::Field(_)));
|
.any(|object| matches!(object, codec::objects::ObjectType::Field(_)));
|
||||||
if !has_fields {
|
if !has_fields || fieldeval(document) & 4 == 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let snapshot = document.clone();
|
let snapshot = document.clone();
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ fn drawing_dir_str(d: &DrawingDirection) -> &'static str {
|
|||||||
/// Per-visible-character world-space boxes for the MText editor's
|
/// Per-visible-character world-space boxes for the MText editor's
|
||||||
/// click-to-select preview. Uses the exact same layout opts as `to_render`
|
/// click-to-select preview. Uses the exact same layout opts as `to_render`
|
||||||
/// so the boxes line up with the rendered glyphs.
|
/// so the boxes line up with the rendered glyphs.
|
||||||
/// The MTEXT string to render: the live re-evaluated value when the entity
|
|
||||||
/// hosts a dynamic field, otherwise its stored (cached) value.
|
|
||||||
fn display_value(t: &MText, document: &codec::CadDocument) -> String {
|
|
||||||
crate::entities::field::resolve(document, t.common.handle).unwrap_or_else(|| t.value.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn glyph_boxes(t: &MText, document: &codec::CadDocument) -> Vec<GlyphBox> {
|
pub fn glyph_boxes(t: &MText, document: &codec::CadDocument) -> Vec<GlyphBox> {
|
||||||
let resolved_style = resolve_text_style(&t.style, document);
|
let resolved_style = resolve_text_style(&t.style, document);
|
||||||
@@ -115,7 +110,9 @@ pub fn glyph_boxes(t: &MText, document: &codec::CadDocument) -> Vec<GlyphBox> {
|
|||||||
} else {
|
} else {
|
||||||
t.rotation as f32
|
t.rotation as f32
|
||||||
};
|
};
|
||||||
let display = display_value(t, document);
|
// A field shows its stored value: fields update on their evaluation
|
||||||
|
// events (open, save, plot, regen, UPDATEFIELD), never on a redraw.
|
||||||
|
let display = t.value.clone();
|
||||||
let layout = layout_mtext(&MTextRenderOpts {
|
let layout = layout_mtext(&MTextRenderOpts {
|
||||||
value: &display,
|
value: &display,
|
||||||
insertion: [
|
insertion: [
|
||||||
@@ -171,7 +168,9 @@ fn to_render(t: &MText, document: &codec::CadDocument) -> RenderEntity {
|
|||||||
} else {
|
} else {
|
||||||
t.rotation as f32
|
t.rotation as f32
|
||||||
};
|
};
|
||||||
let display = display_value(t, document);
|
// A field shows its stored value: fields update on their evaluation
|
||||||
|
// events (open, save, plot, regen, UPDATEFIELD), never on a redraw.
|
||||||
|
let display = t.value.clone();
|
||||||
let layout = layout_mtext(&MTextRenderOpts {
|
let layout = layout_mtext(&MTextRenderOpts {
|
||||||
value: &display,
|
value: &display,
|
||||||
insertion: [
|
insertion: [
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,42 @@ pub fn text_local_bounds(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Each character's [left, right] pen cell along a TEXT line (DXF specials
|
||||||
|
/// already resolved), measured as [`text_local_bounds`] lays the line out:
|
||||||
|
/// the shaper's glyph cells for a TrueType font or a script that needs
|
||||||
|
/// shaping, else the stroke font's advances.
|
||||||
|
pub fn text_char_cells(font_name: &str, text: &str, height: f32, width_factor: f32) -> Vec<(f32, f32)> {
|
||||||
|
let face = Face::resolve(font_name);
|
||||||
|
let scale = height / 9.0;
|
||||||
|
let wf = width_factor.abs().clamp(0.01, 100.0);
|
||||||
|
let shaping_family = face.ttf_family().or_else(|| {
|
||||||
|
crate::scene::text::web_font::requires_shaping(text)
|
||||||
|
.then(|| crate::scene::text::web_font::primary_script().family())
|
||||||
|
});
|
||||||
|
if let Some(run) = shaping_family.and_then(|family| crate::scene::text::ttf_glyph::shape_run(family, text)) {
|
||||||
|
return crate::scene::text::ttf_glyph::char_cells(&run, text)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x0, x1)| (x0 * scale * wf, x1 * scale * wf))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
let mut cursor = 0.0_f32;
|
||||||
|
text.chars()
|
||||||
|
.map(|ch| {
|
||||||
|
let advance = if ch == ' ' {
|
||||||
|
face.word_spacing()
|
||||||
|
} else {
|
||||||
|
match face.glyph(ch) {
|
||||||
|
Some(glyph) => glyph.advance + face.spacing_after(ch),
|
||||||
|
None => 6.0 + face.letter_spacing(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cell = (cursor * scale * wf, (cursor + advance) * scale * wf);
|
||||||
|
cursor += advance;
|
||||||
|
cell
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand DXF `%%x` special-character sequences that appear in both TEXT and MTEXT values:
|
/// Expand DXF `%%x` special-character sequences that appear in both TEXT and MTEXT values:
|
||||||
/// - `%%d` / `%%D` → `°`
|
/// - `%%d` / `%%D` → `°`
|
||||||
/// - `%%p` / `%%P` → `±`
|
/// - `%%p` / `%%P` → `±`
|
||||||
@@ -488,6 +524,33 @@ fn font_stem(name: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse an MTEXT string into the layout's `Vec<MTextLine>`, using opencadcodec's
|
/// Parse an MTEXT string into the layout's `Vec<MTextLine>`, using opencadcodec's
|
||||||
/// structured `mtext_format::parse_mtext` — OCS keeps only the layout engine
|
/// structured `mtext_format::parse_mtext` — OCS keeps only the layout engine
|
||||||
/// (`layout_mtext` and callers read `MTextLine`/`RunState`), not a second MTEXT
|
/// (`layout_mtext` and callers read `MTextLine`/`RunState`), not a second MTEXT
|
||||||
@@ -971,6 +1034,34 @@ pub fn measure_word(
|
|||||||
w
|
w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-character cells of a word laid out by the shaper — the visual
|
||||||
|
/// [left, right] of each character in logical order, from the word start,
|
||||||
|
/// in drawing units — when the word is shaped (a TrueType font or a script
|
||||||
|
/// that needs shaping), as [`measure_word`] measures it. A glyph that
|
||||||
|
/// covers several characters (a ligature) shares its cell among them.
|
||||||
|
pub fn word_cells(
|
||||||
|
text: &str,
|
||||||
|
state: &RunState,
|
||||||
|
entity_h: f32,
|
||||||
|
base_wf: f32,
|
||||||
|
base_font: &str,
|
||||||
|
) -> Option<Vec<(f32, f32)>> {
|
||||||
|
let scale = run_scale(state, entity_h, base_wf);
|
||||||
|
let font_name = resolve_font(state, base_font);
|
||||||
|
let face = Face::resolve(&font_name);
|
||||||
|
let family = face.ttf_family().or_else(|| {
|
||||||
|
crate::scene::text::web_font::requires_shaping(text)
|
||||||
|
.then(|| crate::scene::text::web_font::primary_script().family())
|
||||||
|
})?;
|
||||||
|
let run = crate::scene::text::ttf_glyph::shape_run(family, text)?;
|
||||||
|
Some(
|
||||||
|
crate::scene::text::ttf_glyph::char_cells(&run, text)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x0, x1)| (x0 * scale, x1 * scale))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn measure_space(state: &RunState, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
|
pub fn measure_space(state: &RunState, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
|
||||||
let scale = run_scale(state, entity_h, base_wf);
|
let scale = run_scale(state, entity_h, base_wf);
|
||||||
let font_name = resolve_font(state, base_font);
|
let font_name = resolve_font(state, base_font);
|
||||||
@@ -1329,6 +1420,10 @@ pub struct GlyphBox {
|
|||||||
pub ymin: f32,
|
pub ymin: f32,
|
||||||
pub ymax: f32,
|
pub ymax: f32,
|
||||||
pub is_rtl: bool,
|
pub is_rtl: bool,
|
||||||
|
/// The same cell unrotated, relative to the insertion point
|
||||||
|
/// [left, bottom, right, top]: exact for rotated text, where the world
|
||||||
|
/// box above is only the bounds of two corners.
|
||||||
|
pub local: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Output of [`layout_mtext`]: stroke groups + the geometry the caller
|
/// Output of [`layout_mtext`]: stroke groups + the geometry the caller
|
||||||
@@ -2071,6 +2166,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
ymin: ay.min(by),
|
ymin: ay.min(by),
|
||||||
ymax: ay.max(by),
|
ymax: ay.max(by),
|
||||||
is_rtl: sub.is_rtl,
|
is_rtl: sub.is_rtl,
|
||||||
|
local: [line_lx + cursor_start, line_ly, line_lx + cursor_start, line_ly + caret_h],
|
||||||
});
|
});
|
||||||
vis += 1;
|
vis += 1;
|
||||||
}
|
}
|
||||||
@@ -2297,25 +2393,41 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
} else {
|
} else {
|
||||||
measure_word(text, &atom.state, entity_h, base_wf, &base_font_name)
|
measure_word(text, &atom.state, entity_h, base_wf, &base_font_name)
|
||||||
};
|
};
|
||||||
|
// The shaper's own glyph cells (visual positions,
|
||||||
|
// per character in logical order); evenly divided
|
||||||
|
// slots only when the word is not shaped.
|
||||||
let slot_w = word_w / count as f32;
|
let slot_w = word_w / count as f32;
|
||||||
for ci in 0..count {
|
let cells = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name)
|
||||||
let cx = cursor_x + ci as f32 * slot_w;
|
.unwrap_or_else(|| {
|
||||||
let (ax, ay) = to_world(line_base_x, line_base_y, cx, ly);
|
(0..count)
|
||||||
let (bx, by) = to_world(line_base_x, line_base_y, cx + slot_w, ly + run_h);
|
.map(|ci| {
|
||||||
|
let x = (count - 1 - ci) as f32 * slot_w;
|
||||||
|
(x, x + slot_w)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
for (ci, (x0, x1)) in cells.into_iter().enumerate() {
|
||||||
|
let (cx0, cx1) = (cursor_x + x0, cursor_x + x1);
|
||||||
|
let (ax, ay) = to_world(line_base_x, line_base_y, cx0, ly);
|
||||||
|
let (bx, by) = to_world(line_base_x, line_base_y, cx1, ly + run_h);
|
||||||
glyph_boxes.push(GlyphBox {
|
glyph_boxes.push(GlyphBox {
|
||||||
vis: atom.char_offset + count - 1 - ci,
|
vis: atom.char_offset + ci,
|
||||||
xmin: ax.min(bx),
|
xmin: ax.min(bx),
|
||||||
xmax: ax.max(bx),
|
xmax: ax.max(bx),
|
||||||
ymin: ay.min(by),
|
ymin: ay.min(by),
|
||||||
ymax: ay.max(by),
|
ymax: ay.max(by),
|
||||||
is_rtl: true,
|
is_rtl: true,
|
||||||
|
local: [line_lx + cx0, line_ly + ly, line_lx + cx1, line_ly + ly + run_h],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Per-character boxes, advancing exactly as
|
// Per-character boxes, advancing exactly as
|
||||||
// `measure_word` does so they track the glyphs.
|
// `measure_word` does so they track the glyphs.
|
||||||
|
// A shaped word (TrueType or a complex script)
|
||||||
|
// takes the shaper's cells, as it draws.
|
||||||
let scale = run_scale(&atom.state, entity_h, base_wf);
|
let scale = run_scale(&atom.state, entity_h, base_wf);
|
||||||
let face = Face::resolve(&font_name);
|
let face = Face::resolve(&font_name);
|
||||||
|
let shaped = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name);
|
||||||
let mut cx = cursor_x;
|
let mut cx = cursor_x;
|
||||||
for (ci, ch) in text.chars().enumerate() {
|
for (ci, ch) in text.chars().enumerate() {
|
||||||
let adv = match face.glyph(ch) {
|
let adv = match face.glyph(ch) {
|
||||||
@@ -2324,8 +2436,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
}
|
}
|
||||||
None => (6.0 + face.letter_spacing() * tracking) * scale,
|
None => (6.0 + face.letter_spacing() * tracking) * scale,
|
||||||
};
|
};
|
||||||
let (ax, ay) = to_world(line_base_x, line_base_y, cx, ly);
|
let (cx0, cx1) = match shaped.as_ref().and_then(|cells| cells.get(ci)) {
|
||||||
let (bx, by) = to_world(line_base_x, line_base_y, cx + adv, ly + run_h);
|
Some(&(x0, x1)) => (cursor_x + x0, cursor_x + x1),
|
||||||
|
None => (cx, cx + adv),
|
||||||
|
};
|
||||||
|
let (ax, ay) = to_world(line_base_x, line_base_y, cx0, ly);
|
||||||
|
let (bx, by) = to_world(line_base_x, line_base_y, cx1, ly + run_h);
|
||||||
glyph_boxes.push(GlyphBox {
|
glyph_boxes.push(GlyphBox {
|
||||||
vis: atom.char_offset + ci,
|
vis: atom.char_offset + ci,
|
||||||
xmin: ax.min(bx),
|
xmin: ax.min(bx),
|
||||||
@@ -2333,6 +2449,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
ymin: ay.min(by),
|
ymin: ay.min(by),
|
||||||
ymax: ay.max(by),
|
ymax: ay.max(by),
|
||||||
is_rtl: false,
|
is_rtl: false,
|
||||||
|
local: [line_lx + cx0, line_ly + ly, line_lx + cx1, line_ly + ly + run_h],
|
||||||
});
|
});
|
||||||
cx += adv;
|
cx += adv;
|
||||||
}
|
}
|
||||||
@@ -2460,6 +2577,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
ymin,
|
ymin,
|
||||||
ymax,
|
ymax,
|
||||||
is_rtl: false,
|
is_rtl: false,
|
||||||
|
local: [line_lx + x0, line_ly + valign_dy, line_lx + x1, line_ly + top],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2576,6 +2694,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
ymin: ay.min(by),
|
ymin: ay.min(by),
|
||||||
ymax: ay.max(by),
|
ymax: ay.max(by),
|
||||||
is_rtl: false,
|
is_rtl: false,
|
||||||
|
local: [line_lx + cursor_x, line_ly, line_lx + cursor_x + adv, line_ly + run_h],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cursor_x += adv;
|
cursor_x += adv;
|
||||||
|
|||||||
@@ -797,6 +797,8 @@ pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument,
|
|||||||
normalize_block_origins(&mut doc);
|
normalize_block_origins(&mut doc);
|
||||||
resolve_raster_image_paths(&mut doc, path.parent());
|
resolve_raster_image_paths(&mut doc, path.parent());
|
||||||
doc.source_path = Some(path.to_string_lossy().into_owned());
|
doc.source_path = Some(path.to_string_lossy().into_owned());
|
||||||
|
// Opening is a field evaluation event (FIELDEVAL bit 1).
|
||||||
|
crate::entities::field::update_fields(&mut doc, 1, None);
|
||||||
let dropped = purge_corrupt_entities(&mut doc);
|
let dropped = purge_corrupt_entities(&mut doc);
|
||||||
Ok((doc, dropped))
|
Ok((doc, dropped))
|
||||||
}
|
}
|
||||||
@@ -1050,6 +1052,8 @@ fn finalize_loaded_outcome(
|
|||||||
crate::entities::field::local_header_dates(doc);
|
crate::entities::field::local_header_dates(doc);
|
||||||
resolve_raster_image_paths(doc, path.parent());
|
resolve_raster_image_paths(doc, path.parent());
|
||||||
doc.source_path = Some(path.to_string_lossy().into_owned());
|
doc.source_path = Some(path.to_string_lossy().into_owned());
|
||||||
|
// Opening is a field evaluation event (FIELDEVAL bit 1).
|
||||||
|
crate::entities::field::update_fields(doc, 1, None);
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
fn oriented_mtext_corner_groups(
|
||||||
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
||||||
text: &codec::MText,
|
text: &codec::MText,
|
||||||
@@ -1431,15 +1555,16 @@ pub fn tessellate(
|
|||||||
// entity still has a hit-test target via snap_pts. With SDF on
|
// entity still has a hit-test target via snap_pts. With SDF on
|
||||||
// this is the normal path (strokes suppressed) and the wire
|
// this is the normal path (strokes suppressed) and the wire
|
||||||
// also carries the glyph quads built above.
|
// also carries the glyph quads built above.
|
||||||
if bins.is_empty() {
|
// FIELDDISPLAY: a field shows on a gray box behind its glyphs,
|
||||||
let mut wires: Vec<WireModel> = Vec::new();
|
// on screen only — for SDF glyphs and shaped stroke text alike.
|
||||||
// FIELDDISPLAY: a field shows on a gray box behind its
|
let mut field_background: Option<WireModel> = None;
|
||||||
// glyphs, on screen only.
|
if crate::entities::field::display()
|
||||||
if text_aabb != WireModel::UNBOUNDED_AABB
|
|
||||||
&& crate::entities::field::display()
|
|
||||||
&& crate::entities::field::hosts_field(document, entity)
|
&& crate::entities::field::hosts_field(document, entity)
|
||||||
{
|
{
|
||||||
|
let runs = field_run_corner_groups(document, entity, &stroke_groups, anno);
|
||||||
let corner_groups = match entity {
|
let corner_groups = match entity {
|
||||||
|
_ if runs.is_some() => runs.unwrap_or_default(),
|
||||||
|
_ if text_aabb == WireModel::UNBOUNDED_AABB => Vec::new(),
|
||||||
EntityType::MText(m) => {
|
EntityType::MText(m) => {
|
||||||
let rotation = stroke_groups
|
let rotation = stroke_groups
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1471,7 +1596,7 @@ pub fn tessellate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !ft.is_empty() {
|
if !ft.is_empty() {
|
||||||
wires.push(WireModel {
|
field_background = Some(WireModel {
|
||||||
display_visible: true,
|
display_visible: true,
|
||||||
plot_visible: false,
|
plot_visible: false,
|
||||||
name: name.clone(),
|
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.
|
// MTEXT background and frame follow the glyph bounds.
|
||||||
if text_aabb != WireModel::UNBOUNDED_AABB {
|
if text_aabb != WireModel::UNBOUNDED_AABB {
|
||||||
if let EntityType::MText(m) = entity {
|
if let EntityType::MText(m) = entity {
|
||||||
@@ -1722,6 +1850,7 @@ pub fn tessellate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut out: Vec<WireModel> = Vec::new();
|
let mut out: Vec<WireModel> = Vec::new();
|
||||||
|
out.extend(field_background);
|
||||||
let mut is_first = true;
|
let mut is_first = true;
|
||||||
for bin in bins {
|
for bin in bins {
|
||||||
let wire_color = match bin.color {
|
let wire_color = match bin.color {
|
||||||
|
|||||||
@@ -568,6 +568,9 @@ struct SceneDependencyIndex {
|
|||||||
text_geometry: DependencyTargets,
|
text_geometry: DependencyTargets,
|
||||||
annotation_geometry: DependencyTargets,
|
annotation_geometry: DependencyTargets,
|
||||||
signatures: HashMap<Handle, u64>,
|
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]> {
|
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);
|
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(
|
pub fn bump_entities_with_parametric_driven(
|
||||||
&mut self,
|
&mut self,
|
||||||
changes: &[(Handle, ChangeKind)],
|
changes: &[(Handle, ChangeKind)],
|
||||||
@@ -11727,6 +11788,7 @@ vis_index={:.1} visible_probe={:.1}",
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
index.block_roots = roots;
|
||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,30 @@ pub struct ShapedRun {
|
|||||||
pub glyphs: Vec<PlacedGlyph>,
|
pub glyphs: Vec<PlacedGlyph>,
|
||||||
/// Total pen advance of the run (9-unit).
|
/// Total pen advance of the run (9-unit).
|
||||||
pub advance: f32,
|
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>>>;
|
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);
|
buffer.shape_until_scroll(&mut font_system, false);
|
||||||
|
|
||||||
let mut glyphs = Vec::new();
|
let mut glyphs = Vec::new();
|
||||||
|
let mut cells = Vec::new();
|
||||||
let mut advance = 0.0_f32;
|
let mut advance = 0.0_f32;
|
||||||
for run in buffer.layout_runs() {
|
for run in buffer.layout_runs() {
|
||||||
advance = advance.max(run.line_w * px_to_9);
|
advance = advance.max(run.line_w * px_to_9);
|
||||||
for glyph in run.glyphs.iter() {
|
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
|
let face_index = font_system
|
||||||
.db_mut()
|
.db_mut()
|
||||||
.face(glyph.font_id)
|
.face(glyph.font_id)
|
||||||
@@ -923,7 +949,7 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
|||||||
if advance <= 0.0 && glyphs.is_empty() {
|
if advance <= 0.0 && glyphs.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} 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 px_to_9 = CAP_UNITS * upem_p / (SHAPE_FS * cap_p);
|
||||||
|
|
||||||
let mut glyphs: Vec<PlacedGlyph> = Vec::new();
|
let mut glyphs: Vec<PlacedGlyph> = Vec::new();
|
||||||
|
let mut cells = Vec::new();
|
||||||
let mut advance = 0.0_f32;
|
let mut advance = 0.0_f32;
|
||||||
for run in buf.layout_runs() {
|
for run in buf.layout_runs() {
|
||||||
advance = advance.max(run.line_w * px_to_9);
|
advance = advance.max(run.line_w * px_to_9);
|
||||||
for g in run.glyphs.iter() {
|
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 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 {
|
let Some(font) = fs.get_font(g.font_id, g.font_weight) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -1028,7 +1056,7 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
|||||||
if advance <= 0.0 && glyphs.is_empty() {
|
if advance <= 0.0 && glyphs.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(ShapedRun { glyphs, advance })
|
Some(ShapedRun { glyphs, advance, cells })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Reference in new issue
Block a user