diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index e339e2950..2a3bdb869 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -1608,12 +1608,14 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "ANNOUPDATE"); let scale = self.tabs[i].scene.creation_annotation_scale_handle(); + let paper_to_model = self.tabs[i].scene.creation_annotation_multiplier(); let mut updated = 0usize; for handle in &handles { if crate::scene::annotative::update_entity_from_annotation_style( &mut self.tabs[i].scene.document, *handle, scale, + paper_to_model, ) { updated += 1; } diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index d00d1fe9f..3a5377e6d 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -2697,6 +2697,16 @@ impl OpenCADStudio { .header .current_text_style_name .clone(); + // An annotative style's default height is on paper; the editor + // and the entity work in the model height of this scale. + let height = if crate::scene::annotative::text_style_is_annotative( + &self.tabs[i].scene.document, + &style, + ) { + height * self.tabs[i].scene.creation_annotation_multiplier() + } else { + height + }; let new_cmd = MTextCommand::with_defaults(height, style); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index ccd1a5f36..2f67342d1 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -1721,7 +1721,7 @@ impl super::OpenCADStudio { annotative, ); if annotative { - if let Some(scale) = self.tabs[i].scene.current_annotation_scale_handle() { + if let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() { crate::scene::annotative::create_annotation_context( &mut self.tabs[i].scene.document, h, @@ -1755,7 +1755,7 @@ impl super::OpenCADStudio { self.push_undo_snapshot(i, "MTEXT"); let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt))); if annotative { - let scale = self.tabs[i].scene.current_annotation_scale_handle(); + let scale = self.tabs[i].scene.creation_annotation_scale_handle(); if let (Some(handle), Some(scale)) = (handle, scale) { crate::scene::annotative::create_annotation_context( &mut self.tabs[i].scene.document, @@ -1837,7 +1837,7 @@ impl super::OpenCADStudio { annotative, ); if annotative { - if let Some(scale) = self.tabs[i].scene.current_annotation_scale_handle() { + if let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() { crate::scene::annotative::create_annotation_context( &mut self.tabs[i].scene.document, h, @@ -1872,7 +1872,7 @@ impl super::OpenCADStudio { let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt))); self.tabs[i].dirty = true; if annotative { - let scale = self.tabs[i].scene.current_annotation_scale_handle(); + let scale = self.tabs[i].scene.creation_annotation_scale_handle(); if let (Some(handle), Some(scale)) = (handle, scale) { crate::scene::annotative::create_annotation_context( &mut self.tabs[i].scene.document, diff --git a/src/app/properties.rs b/src/app/properties.rs index 537844f50..8f15a3c9a 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1538,7 +1538,7 @@ impl OpenCADStudio { codec::EntityType::MText(t) => set_row_value( &mut sections, "annotative", - yes_no_choice(t.is_annotative), + yes_no_choice(t.is_annotative || is_anno), ), codec::EntityType::Dimension(codec::entities::Dimension::Arc(_)) => { set_row( @@ -1600,54 +1600,56 @@ impl OpenCADStudio { // text derives its paper height from the two endpoints, // while annotative text exposes that paper height and a // separate calculated model height. - if let codec::EntityType::Text(text) = entity { - let aligned = matches!( - text.horizontal_alignment, - codec::entities::TextHorizontalAlignment::Aligned - ); - let annotative = crate::scene::annotative::is_annotative(doc, entity); - let model_factor = if annotative { - annotation_scale_handle - .and_then(|handle| match doc.objects.get(&handle) { - Some(codec::objects::ObjectType::Scale(scale)) => Some( - scale.inverse_factor() - / self.tabs[i].scene.annotation_scale_unit_factor(), - ), - _ => None, - }) - .unwrap_or(self.tabs[i].scene.annotation_scale as f64) - } else { - 1.0 - }; - let paper_height = if aligned { - crate::entities::text::text_run_placement_at_scale(text, doc, model_factor as f32) - .height as f64 - } else { - text.height + if let Some(factor) = + self.tabs[i].scene.annotative_text_height_factor(entity.common().handle) + { + // Annotative text stores the model height of its default scale; + // the Height row edits the paper height at the displayed scale. + let (stored, model_factor, aligned) = match entity { + codec::EntityType::Text(text) => { + let aligned = matches!( + text.horizontal_alignment, + codec::entities::TextHorizontalAlignment::Aligned + ); + let stored = if aligned { + crate::entities::text::text_run_placement_at_scale(text, doc, 1.0).height + as f64 + } else { + text.height + }; + (stored, factor, aligned) + } + codec::EntityType::MText(text) => (text.height, factor, false), + _ => unreachable!("annotative_text_height_factor is text-only"), }; + let ratio = crate::scene::annotative::text_context_ratio( + doc, + entity.common().handle, + annotation_scale_handle, + ) + .unwrap_or(1.0); + let paper_height = stored / model_factor; for section in sections.iter_mut() { if let Some(row) = section.props.iter_mut().find(|row| row.field == "height") { - if annotative { - row.label = t!("Paper text height").into_owned(); - } - if aligned { - row.value = crate::scene::model::object::PropValue::ReadOnly( - crate::entities::common::format_length(paper_height), - ); - } + let editable = !aligned + && !matches!(row.value, crate::scene::model::object::PropValue::ReadOnly(_)); + *row = crate::entities::common::num_prop( + t!("Paper text height").as_ref(), + "height", + paper_height, + editable, + ); } } - if annotative { - insert_row_after( - &mut sections, - "height", - crate::entities::common::ro_prop( - t!("Model text height").as_ref(), - "model_text_height", - crate::entities::common::format_length(paper_height * model_factor), - ), - ); - } + insert_row_after( + &mut sections, + "height", + crate::entities::common::ro_prop( + t!("Model text height").as_ref(), + "model_text_height", + crate::entities::common::format_length(stored * ratio), + ), + ); } if !group_names.is_empty() { diff --git a/src/app/text_inline.rs b/src/app/text_inline.rs index 2e8adf729..89d6b77c7 100644 --- a/src/app/text_inline.rs +++ b/src/app/text_inline.rs @@ -276,22 +276,24 @@ impl super::OpenCADStudio { t.style = cur_style; } } - if command_creation { - let annotation_multiplier = if crate::scene::annotative::text_style_is_annotative( + // TEXT asks an annotative style for the paper height; the entity + // stores the model height of the scale it is created at. + let annotative = command_creation + && crate::scene::annotative::text_style_is_annotative( &self.tabs[i].scene.document, &t.style, - ) { - self.tabs[i].scene.creation_annotation_multiplier() - } else { - 1.0 - }; + ); + let paper_height = t.height; + if annotative { + t.height *= self.tabs[i].scene.creation_annotation_multiplier(); + } + if command_creation { let display_height = crate::entities::text::text_run_placement_at_scale( &t, &self.tabs[i].scene.document, - annotation_multiplier as f32, + 1.0, ) - .height as f64 - * annotation_multiplier; + .height as f64; if let Some(command) = self.tabs[i].suspended_cmd.as_mut() { command.on_editor_display_height(display_height); } @@ -311,9 +313,22 @@ impl super::OpenCADStudio { codec::entities::TextHorizontalAlignment::Aligned ) { - self.tabs[i].scene.document.header.text_height = t.height; + self.tabs[i].scene.document.header.text_height = paper_height; + } + let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t))); + if annotative { + let scale = self.tabs[i].scene.creation_annotation_scale_handle(); + if let (Some(handle), Some(scale)) = (handle, scale) { + crate::scene::annotative::create_annotation_context( + &mut self.tabs[i].scene.document, + handle, + scale, + ); + self.tabs[i] + .scene + .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + } } - let _ = self.commit_entity_handle(plane.place_entity(EntityType::Text(t))); self.tabs[i].dirty = true; } self.refresh_properties(); diff --git a/src/app/update/command.rs b/src/app/update/command.rs index bd8166206..4824068c8 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -4179,6 +4179,29 @@ impl OpenCADStudio { } } } + // Annotative text: the row holds the paper height. + "height" + if self.tabs[i] + .scene + .annotative_text_height_factor(handle) + .is_some() => + { + let factor = self.tabs[i] + .scene + .annotative_text_height_factor(handle) + .unwrap_or(1.0); + if let (Ok(paper), Some(entity)) = ( + val.trim().parse::(), + self.tabs[i].scene.document.get_entity_mut(handle), + ) { + crate::scene::view::dispatch::apply_geom_prop_in_working_plane( + entity, + "height", + &(paper * factor).to_string(), + plane, + ); + } + } "hyperlink" => { // Stored in the standard PE_URL XDATA // record; an empty value clears it. diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index c791a51b3..9d1decb69 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -722,18 +722,12 @@ pub fn effective_annotation_scale_for( if !context_annotative && (text_like || !style_annotative) { return 1.0; } - // Annotative TEXT/MTEXT store their paper text height as the base value. - // `fallback` is the absolute paper-to-model multiplier for the active - // annotation scale and already includes the drawing's INSUNITS conversion. - // - // Example for a metre drawing with 2 mm paper text: - // 1:1 -> 2 * 0.001 = 0.002 m - // 1:100 -> 2 * 0.100 = 0.200 m - // - // Using active/native here would only produce 1 / 100 and would lose the - // millimetre-to-metre conversion entirely. + // Annotative TEXT/MTEXT store the model height of their default (native) + // scale representation; another scale draws it at the ratio of the two. + // A scale the object has no representation for shows the stored one. if matches!(entity, EntityType::Text(_) | EntityType::MText(_)) { - return fallback; + return text_context_ratio(doc, entity.common().handle, scale_handle) + .map_or(1.0, |ratio| ratio as f32); } if matches!( entity, @@ -750,41 +744,39 @@ pub fn effective_annotation_scale_for( return fallback; } - let Some(coll_h) = annotation_scales_dict(doc, entity.common().handle) else { - return fallback; - }; - let Some(coll) = as_dict(doc, coll_h) else { - return fallback; - }; + text_context_ratio(doc, entity.common().handle, scale_handle) + .map_or(fallback, |ratio| ratio as f32) +} - let active = active_object_context_for_scale(doc, entity.common().handle, scale_handle); +/// How much larger the representation at `scale_handle` (the displayed scale +/// when `None`) draws than the object's default representation: the ratio of +/// their model-per-paper factors. `None` when either is missing. +pub fn text_context_ratio( + doc: &CadDocument, + handle: Handle, + scale_handle: Option, +) -> Option { + let coll = as_dict(doc, annotation_scales_dict(doc, handle)?)?; + let active = active_object_context_for_scale(doc, handle, scale_handle)?; let native = coll .entries .iter() .find_map(|(_, leaf_h)| match doc.objects.get(leaf_h) { Some(ObjectType::ObjectContextData(leaf)) if leaf.is_default => Some(leaf), _ => None, - }); - let (Some(active), Some(native)) = (active, native) else { - return fallback; - }; + })?; let (Some(ObjectType::Scale(active_scale)), Some(ObjectType::Scale(native_scale))) = ( doc.objects.get(&active.scale), doc.objects.get(&native.scale), ) else { - return fallback; + return None; }; - let native_factor = native_scale.inverse_factor(); if native_factor.abs() <= 1.0e-12 { - return fallback; + return None; } let relative = active_scale.inverse_factor() / native_factor; - if relative.is_finite() && relative > 0.0 { - relative as f32 - } else { - fallback - } + (relative.is_finite() && relative > 0.0).then_some(relative) } fn text_horizontal(value: i16) -> codec::entities::TextHorizontalAlignment { @@ -1864,6 +1856,7 @@ pub fn update_entity_from_annotation_style( doc: &mut CadDocument, handle: Handle, current_scale: Option, + paper_to_model: f64, ) -> bool { enum StyleUpdate { Text { annotative: bool, height: f64 }, @@ -1943,19 +1936,10 @@ pub fn update_entity_from_annotation_style( return false; }; + let mut style_height = None; let annotative = match update { StyleUpdate::Text { annotative, height } => { - if height > 0.0 { - if let Some(entity) = doc.get_entity_mut(handle) { - match entity { - EntityType::Text(text) => text.height = height, - EntityType::MText(text) => text.height = height, - EntityType::AttributeEntity(attribute) => attribute.height = height, - EntityType::AttributeDefinition(attribute) => attribute.height = height, - _ => {} - } - } - } + style_height = (height > 0.0).then_some(height); annotative } StyleUpdate::Dimension { annotative } => annotative, @@ -1972,6 +1956,27 @@ pub fn update_entity_from_annotation_style( create_annotation_context(doc, handle, scale); } } + if let Some(mut height) = style_height { + // An annotative style's height is on paper; the entity stores the + // model height of its default representation. + let text = matches!( + doc.get_entity(handle), + Some(EntityType::Text(_) | EntityType::MText(_)) + ); + if annotative && text { + let ratio = text_context_ratio(doc, handle, current_scale).unwrap_or(1.0); + height *= paper_to_model / ratio; + } + if let Some(entity) = doc.get_entity_mut(handle) { + match entity { + EntityType::Text(text) => text.height = height, + EntityType::MText(text) => text.height = height, + EntityType::AttributeEntity(attribute) => attribute.height = height, + EntityType::AttributeDefinition(attribute) => attribute.height = height, + _ => {} + } + } + } true } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 367d92d26..074b35015 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -6176,6 +6176,31 @@ impl Scene { added } + /// For an annotative TEXT/MTEXT, its stored height per unit of paper text + /// height at the displayed scale (the stored height is the model height + /// of its default representation). `None` for anything else. + pub(crate) fn annotative_text_height_factor(&self, handle: Handle) -> Option { + let entity = self.document.get_entity(handle)?; + if !matches!(entity, EntityType::Text(_) | EntityType::MText(_)) + || !crate::scene::annotative::is_annotative(&self.document, entity) + { + return None; + } + let scale = self.displayed_annotation_scale_handle(); + let model_per_paper = scale + .and_then(|scale| match self.document.objects.get(&scale) { + Some(ObjectType::Scale(scale)) => { + Some(scale.inverse_factor() / self.annotation_scale_unit_factor()) + } + _ => None, + }) + .unwrap_or(self.annotation_scale as f64); + let ratio = + crate::scene::annotative::text_context_ratio(&self.document, handle, scale) + .unwrap_or(1.0); + Some(model_per_paper / ratio) + } + pub(crate) fn displayed_annotation_scale_handle(&self) -> Option { if self.current_layout == "Model" { return self.scale_object_handle(&self.document.header.current_annotation_scale);