Merge pull request #882 from ramox81/codex/fix-single-line-text
Complete single-line text behavior and properties
This commit is contained in:
commit
0e98c4857b
11 changed files with 759 additions and 110 deletions
|
|
@ -3748,6 +3748,22 @@ impl OpenCADStudio {
|
|||
&initial,
|
||||
height,
|
||||
super::text_inline::TextEntityField::Text,
|
||||
None,
|
||||
);
|
||||
}
|
||||
CmdResult::SuspendForTextInput { pos, entity } => {
|
||||
self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
let height = entity.height;
|
||||
self.open_text_inline(
|
||||
pos,
|
||||
None,
|
||||
"",
|
||||
height,
|
||||
super::text_inline::TextEntityField::Text,
|
||||
Some(entity),
|
||||
);
|
||||
}
|
||||
CmdResult::EditTextEntity { handle } => {
|
||||
|
|
|
|||
|
|
@ -1268,11 +1268,16 @@ impl OpenCADStudio {
|
|||
// ── Annotate commands ──────────────────────────────────────────
|
||||
"TEXT" => {
|
||||
use crate::modules::annotate::text::TextCommand;
|
||||
let height = crate::scene::creation_style::current_text_defaults(
|
||||
&self.tabs[i].scene.document,
|
||||
)
|
||||
.height;
|
||||
let new_cmd = TextCommand::with_height(height);
|
||||
let (defaults, styles, annotation_multiplier) = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
let annotation_multiplier = scene.creation_annotation_multiplier();
|
||||
let defaults =
|
||||
crate::scene::creation_style::current_text_defaults(&scene.document);
|
||||
let styles = scene.document.text_styles.iter().cloned().collect();
|
||||
(defaults, styles, annotation_multiplier)
|
||||
};
|
||||
let new_cmd =
|
||||
TextCommand::with_defaults(defaults, styles, annotation_multiplier);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1799,6 +1799,69 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
// Single-line text height rows depend on both the
|
||||
// justification and the active annotation scale. Aligned
|
||||
// text derives its paper height from the two endpoints,
|
||||
// while annotative text exposes that paper height and a
|
||||
// separate calculated model height.
|
||||
if let acadrust::EntityType::Text(text) = entity {
|
||||
let aligned = matches!(
|
||||
text.horizontal_alignment,
|
||||
acadrust::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(acadrust::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
|
||||
};
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !group_names.is_empty() {
|
||||
let label = group_names.join(", ");
|
||||
if let Some(general) = sections.first_mut() {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ pub struct TextInlineState {
|
|||
pub editing: Option<Handle>,
|
||||
/// Which entity slot this session writes to on commit.
|
||||
pub field: TextEntityField,
|
||||
/// Fully prepared entity supplied by the interactive TEXT command. Editing
|
||||
/// existing text and legacy direct-open paths leave this empty.
|
||||
pub creation: Option<Text>,
|
||||
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
||||
pub screen_anchor: iced::Point,
|
||||
}
|
||||
|
|
@ -169,7 +172,7 @@ impl super::OpenCADStudio {
|
|||
self.open_mtext_editor(pos, Some(target), &value, height);
|
||||
self.unfocus_widgets()
|
||||
} else {
|
||||
self.open_text_inline(pos, Some(target), &value, height, field);
|
||||
self.open_text_inline(pos, Some(target), &value, height, field, None);
|
||||
iced::widget::operation::focus(iced::widget::Id::new(super::view::TEXT_INLINE_ID))
|
||||
}
|
||||
}
|
||||
|
|
@ -183,6 +186,7 @@ impl super::OpenCADStudio {
|
|||
initial: &str,
|
||||
height: f64,
|
||||
field: TextEntityField,
|
||||
creation: Option<Text>,
|
||||
) {
|
||||
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
||||
return;
|
||||
|
|
@ -193,6 +197,7 @@ impl super::OpenCADStudio {
|
|||
height: if height > 0.0 { height } else { 0.25 },
|
||||
editing: handle,
|
||||
field,
|
||||
creation,
|
||||
screen_anchor: iced::Point::new(60.0, 90.0),
|
||||
};
|
||||
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
|
||||
|
|
@ -229,12 +234,18 @@ impl super::OpenCADStudio {
|
|||
} else {
|
||||
crate::command::WorkingPlane::default()
|
||||
};
|
||||
let command_creation = ed.creation.is_some();
|
||||
let mut t = if let Some(mut prepared) = ed.creation {
|
||||
prepared.value = ed.value.clone();
|
||||
prepared
|
||||
} else {
|
||||
let position = plane.to_local(ed.pos);
|
||||
let mut t = Text::with_value(
|
||||
Text::with_value(
|
||||
&ed.value,
|
||||
Vector3::new(position.x, position.y, position.z),
|
||||
)
|
||||
.with_height(ed.height);
|
||||
.with_height(ed.height)
|
||||
};
|
||||
// New text inherits the document's current text style (STYLE), not
|
||||
// the entity default. See #92.
|
||||
let cur_style = self.tabs[i]
|
||||
|
|
@ -244,24 +255,48 @@ impl super::OpenCADStudio {
|
|||
.current_text_style_name
|
||||
.clone();
|
||||
if !cur_style.is_empty() {
|
||||
if t.style.trim().is_empty() {
|
||||
t.style = cur_style;
|
||||
}
|
||||
let annotative = crate::scene::annotative::text_style_is_annotative(
|
||||
}
|
||||
if command_creation {
|
||||
let annotation_multiplier = if 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 display_height = crate::entities::text::text_run_placement_at_scale(
|
||||
&t,
|
||||
&self.tabs[i].scene.document,
|
||||
annotation_multiplier as f32,
|
||||
)
|
||||
.height as f64
|
||||
* annotation_multiplier;
|
||||
if let Some(command) = self.tabs[i].suspended_cmd.as_mut() {
|
||||
command.on_editor_display_height(display_height);
|
||||
}
|
||||
}
|
||||
self.push_undo_snapshot(i, "TEXT");
|
||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
||||
if annotative {
|
||||
let scale = self.tabs[i].scene.current_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.document.header.current_text_style_name = t.style.clone();
|
||||
let variable_height = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(&t.style))
|
||||
.is_none_or(|style| style.height <= 1.0e-9);
|
||||
if variable_height
|
||||
&& !matches!(
|
||||
t.horizontal_alignment,
|
||||
acadrust::entities::TextHorizontalAlignment::Aligned
|
||||
)
|
||||
{
|
||||
self.tabs[i].scene.document.header.text_height = t.height;
|
||||
}
|
||||
let _ = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.refresh_properties();
|
||||
|
|
|
|||
|
|
@ -1162,6 +1162,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
if matches!(
|
||||
item.action,
|
||||
GripMenuAction::Stretch
|
||||
| GripMenuAction::MoveWithText
|
||||
| GripMenuAction::MoveWithDimLine
|
||||
| GripMenuAction::MoveWithLeader
|
||||
| GripMenuAction::MoveIndependent
|
||||
|
|
@ -1212,7 +1213,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
{
|
||||
(crate::entities::multileader::MOVE_ALL_GRIP, true)
|
||||
} else {
|
||||
(popup.grip_id, if is_dimension { false } else { g.is_midpoint })
|
||||
(
|
||||
popup.grip_id,
|
||||
matches!(item.action, GripMenuAction::MoveWithText)
|
||||
|| (!is_dimension && g.is_midpoint),
|
||||
)
|
||||
};
|
||||
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||
popup.handle,
|
||||
|
|
|
|||
|
|
@ -1443,6 +1443,14 @@ pub enum CmdResult {
|
|||
initial: String,
|
||||
height: f64,
|
||||
},
|
||||
/// Suspend the active TEXT command while the in-place editor collects one
|
||||
/// independent line. The prepared entity carries the chosen style,
|
||||
/// justification, rotation and two-point geometry. When the editor closes,
|
||||
/// the command resumes so another line can be placed directly below it.
|
||||
SuspendForTextInput {
|
||||
pos: DVec3,
|
||||
entity: acadrust::entities::Text,
|
||||
},
|
||||
/// Apply new pattern/scale/angle to an existing hatch entity.
|
||||
HatcheditApply {
|
||||
handle: Handle,
|
||||
|
|
@ -1861,6 +1869,8 @@ pub trait CadCommand: Send {
|
|||
/// Resume the command with collected rich text.
|
||||
fn on_editor_text(&mut self, _value: String) {}
|
||||
|
||||
fn on_editor_display_height(&mut self, _height: f64) {}
|
||||
|
||||
/// Called when the user clicks and `needs_entity_pick()` is true.
|
||||
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
|
||||
fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,24 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
|
|||
(HA::Left, VA::Baseline)
|
||||
);
|
||||
if needs_alignment_point {
|
||||
if t.alignment_point.is_none() {
|
||||
if matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
|
||||
let point = t.alignment_point.unwrap_or(t.insertion_point);
|
||||
let dx = point.x - t.insertion_point.x;
|
||||
let dy = point.y - t.insertion_point.y;
|
||||
if dx.hypot(dy) <= 1.0e-9 {
|
||||
let span = t.height.max(1.0e-6)
|
||||
* t.width_factor.abs().max(0.01)
|
||||
* t.value.chars().count().max(1) as f64
|
||||
* 0.6;
|
||||
t.alignment_point = Some(acadrust::types::Vector3::new(
|
||||
t.insertion_point.x + t.rotation.cos() * span,
|
||||
t.insertion_point.y + t.rotation.sin() * span,
|
||||
t.insertion_point.z,
|
||||
));
|
||||
} else {
|
||||
t.alignment_point = Some(point);
|
||||
}
|
||||
} else if t.alignment_point.is_none() {
|
||||
t.alignment_point = Some(t.insertion_point);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -51,6 +68,21 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
|
|||
}
|
||||
}
|
||||
|
||||
fn two_point_span(t: &Text) -> Option<(f64, f64)> {
|
||||
if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
|
||||
return None;
|
||||
}
|
||||
let point = t.alignment_point?;
|
||||
let dx = point.x - t.insertion_point.x;
|
||||
let dy = point.y - t.insertion_point.y;
|
||||
let distance = dx.hypot(dy);
|
||||
(distance > 1.0e-9).then(|| (distance, dy.atan2(dx)))
|
||||
}
|
||||
|
||||
fn displayed_rotation(t: &Text) -> f64 {
|
||||
two_point_span(t).map_or(t.rotation, |(_, angle)| angle)
|
||||
}
|
||||
|
||||
/// Resolved placement of a TEXT run: the baseline-anchored run origin (WCS xy)
|
||||
/// plus every parameter needed to lay the glyphs out. Shared by `to_render` (the
|
||||
/// stroke path) and the SDF-quad text collector so both place text identically.
|
||||
|
|
@ -98,8 +130,12 @@ pub(crate) fn acad_text_encode(value: &str) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity {
|
||||
let p = text_run_placement(t, document);
|
||||
pub(crate) fn to_render_at_scale(
|
||||
t: &Text,
|
||||
document: &acadrust::CadDocument,
|
||||
annotation_scale: f32,
|
||||
) -> RenderEntity {
|
||||
let p = text_run_placement_at_scale(t, document, annotation_scale);
|
||||
let snap_pt = glam::DVec3::new(p.wcs_insertion[0], p.wcs_insertion[1], p.wcs_insertion[2]);
|
||||
// Parse `%%` codes via acadrust, re-encoded for the stroke tessellator.
|
||||
let value = acad_text_encode(&p.value);
|
||||
|
|
@ -140,7 +176,16 @@ fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity {
|
|||
|
||||
/// Compute a TEXT entity's run placement (origin + layout params). Extracted
|
||||
/// from `to_render` verbatim so the stroke and SDF-quad paths agree exactly.
|
||||
pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPlacement {
|
||||
pub fn text_run_placement_at_scale(
|
||||
t: &Text,
|
||||
document: &acadrust::CadDocument,
|
||||
annotation_scale: f32,
|
||||
) -> TextPlacement {
|
||||
let annotation_scale = if annotation_scale.is_finite() && annotation_scale > 1.0e-9 {
|
||||
annotation_scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let normal = (t.normal.x, t.normal.y, t.normal.z);
|
||||
let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(
|
||||
|
|
@ -152,10 +197,9 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
|||
);
|
||||
let resolved_style = resolve_text_style(&t.style, document);
|
||||
let font_name = resolved_style.font_name;
|
||||
// AutoCAD text geometry rule: the entity stores the FINAL width factor /
|
||||
// oblique angle, copied from the style at creation and persisting through
|
||||
// style edits. Use it as-is. Only fall back to the style when the entity
|
||||
// value is missing (the parser reports 0.0 for default-omitted fields).
|
||||
// The entity stores the final width factor and oblique angle copied from
|
||||
// its style at creation. Only fall back to the style when an omitted field
|
||||
// was read as zero.
|
||||
let base_wf = if t.width_factor.abs() > 1e-9 {
|
||||
(t.width_factor as f32).clamp(0.01, 100.0)
|
||||
} else {
|
||||
|
|
@ -168,17 +212,44 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
|||
// mirror, and XOR keeps a double mirror an involution.
|
||||
let eff_backward = resolved_style.is_backward ^ (t.generation_flags & 0x2 != 0);
|
||||
let eff_upside = resolved_style.is_upside_down ^ (t.generation_flags & 0x4 != 0);
|
||||
let width_factor = if eff_backward { -base_wf } else { base_wf };
|
||||
let rotation = if eff_upside {
|
||||
t.rotation as f32 + std::f32::consts::PI
|
||||
} else {
|
||||
t.rotation as f32
|
||||
};
|
||||
let mut width_factor = if eff_backward { -base_wf } else { base_wf };
|
||||
let oblique_angle = if t.oblique_angle.abs() > 1e-9 {
|
||||
t.oblique_angle as f32
|
||||
} else {
|
||||
resolved_style.oblique_angle
|
||||
};
|
||||
let value_for_bounds = resolve_dxf_special_chars(&t.value);
|
||||
let mut height = t.height.max(1.0e-9) as f32;
|
||||
let mut base_rotation = t.rotation as f32;
|
||||
|
||||
// Aligned and Fit are true two-point modes. Both derive their baseline
|
||||
// direction from the endpoints. Aligned scales height uniformly; Fit keeps
|
||||
// the height and changes only the horizontal factor.
|
||||
if let Some((span, angle)) = two_point_span(t) {
|
||||
base_rotation = angle as f32;
|
||||
if let Some(base_bounds) = text_local_bounds(
|
||||
&font_name,
|
||||
&value_for_bounds,
|
||||
height,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
) {
|
||||
if base_bounds.advance > 1.0e-6 {
|
||||
let scale =
|
||||
(span as f32 / annotation_scale / base_bounds.advance).max(1.0e-6);
|
||||
if matches!(t.horizontal_alignment, HA::Aligned) {
|
||||
height *= scale;
|
||||
} else {
|
||||
width_factor *= scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rotation = if eff_upside {
|
||||
base_rotation + std::f32::consts::PI
|
||||
} else {
|
||||
base_rotation
|
||||
};
|
||||
// Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of
|
||||
// precision when cast to f32, which snaps text baselines onto a coarse
|
||||
// grid and makes adjacent rows collide. Only the small local offsets
|
||||
|
|
@ -188,17 +259,16 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
|||
&t.vertical_alignment,
|
||||
&t.alignment_point,
|
||||
) {
|
||||
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x, a.y],
|
||||
(HA::Aligned | HA::Fit, _, _) => [t.insertion_point.x, t.insertion_point.y],
|
||||
(HA::Middle, _, Some(a)) => [a.x, a.y],
|
||||
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
|
||||
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y],
|
||||
_ => [t.insertion_point.x, t.insertion_point.y],
|
||||
};
|
||||
// Strip %%u/%%o for bounds (they add no width); resolve %%d/%%c/%%p for correct advance.
|
||||
let value_for_bounds = resolve_dxf_special_chars(&t.value);
|
||||
let bounds = text_local_bounds(
|
||||
&font_name,
|
||||
&value_for_bounds,
|
||||
t.height as f32,
|
||||
height,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
);
|
||||
|
|
@ -213,7 +283,8 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
|||
let ax = match t.horizontal_alignment {
|
||||
HA::Left => 0.0,
|
||||
HA::Center | HA::Middle => b.advance * 0.5 * sign,
|
||||
HA::Right | HA::Aligned | HA::Fit => b.advance * sign,
|
||||
HA::Right => b.advance * sign,
|
||||
HA::Aligned | HA::Fit => 0.0,
|
||||
};
|
||||
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
||||
let ay = match t.vertical_alignment {
|
||||
|
|
@ -236,7 +307,7 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
|||
];
|
||||
TextPlacement {
|
||||
origin,
|
||||
height: t.height as f32,
|
||||
height,
|
||||
rotation,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
|
|
@ -247,12 +318,19 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
|||
}
|
||||
|
||||
fn grips(t: &Text) -> Vec<GripDef> {
|
||||
let p = glam::DVec3::new(
|
||||
let insertion = glam::DVec3::new(
|
||||
t.insertion_point.x,
|
||||
t.insertion_point.y,
|
||||
t.insertion_point.z,
|
||||
);
|
||||
vec![square_grip(0, p)]
|
||||
let mut grips = vec![square_grip(0, insertion)];
|
||||
if let Some(point) = t.alignment_point {
|
||||
grips.push(square_grip(
|
||||
1,
|
||||
glam::DVec3::new(point.x, point.y, point.z),
|
||||
));
|
||||
}
|
||||
grips
|
||||
}
|
||||
|
||||
fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
|
|
@ -263,15 +341,25 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
|||
// both points are live.
|
||||
let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
|
||||
&& matches!(t.vertical_alignment, VA::Baseline);
|
||||
let pos_editable = is_plain_left || matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||
let align_editable = !is_plain_left;
|
||||
// The alignment point is meaningless (reset to the origin) for plain-Left text.
|
||||
let is_aligned = matches!(t.horizontal_alignment, HA::Aligned);
|
||||
let is_two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||
// The visible Properties palette exposes Text alignment as calculated
|
||||
// coordinates and Position as the editable placement point. For aligned
|
||||
// and fit text Position targets the first endpoint; for every other
|
||||
// non-left mode it targets the alignment anchor that actually places the
|
||||
// glyph run.
|
||||
let position_uses_alignment = !is_plain_left && !is_two_point;
|
||||
let ap = t.alignment_point.unwrap_or(t.insertion_point);
|
||||
let (ax, ay, az) = if align_editable {
|
||||
let (ax, ay, az) = if !is_plain_left {
|
||||
(ap.x, ap.y, ap.z)
|
||||
} else {
|
||||
(0.0, 0.0, 0.0)
|
||||
};
|
||||
let position = if position_uses_alignment {
|
||||
ap
|
||||
} else {
|
||||
t.insertion_point
|
||||
};
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Text").into_owned(),
|
||||
|
|
@ -331,22 +419,31 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
|||
t!("Height").as_ref(),
|
||||
"height",
|
||||
t.height,
|
||||
crate::entities::common::style_fixed_height(&t.style).is_none(),
|
||||
!is_aligned
|
||||
&& crate::entities::common::style_fixed_height(&t.style).is_none(),
|
||||
),
|
||||
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees()),
|
||||
if is_two_point {
|
||||
ro(
|
||||
t!("Rotation").as_ref(),
|
||||
"rotation",
|
||||
crate::entities::common::format_angle(displayed_rotation(t)),
|
||||
)
|
||||
} else {
|
||||
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees())
|
||||
},
|
||||
edit(t!("Width factor").as_ref(), "width_factor", t.width_factor),
|
||||
edit_angle(t!("Obliquing").as_ref(), "oblique_angle", t.oblique_angle.to_degrees()),
|
||||
num_row(t!("Text alignment X").as_ref(), "align_x", ax, align_editable),
|
||||
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, align_editable),
|
||||
num_row(t!("Text alignment Z").as_ref(), "align_z", az, align_editable),
|
||||
num_row(t!("Text alignment X").as_ref(), "align_x", ax, false),
|
||||
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, false),
|
||||
num_row(t!("Text alignment Z").as_ref(), "align_z", az, false),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
num_row(t!("Position X").as_ref(), "ins_x", t.insertion_point.x, pos_editable),
|
||||
num_row(t!("Position Y").as_ref(), "ins_y", t.insertion_point.y, pos_editable),
|
||||
num_row(t!("Position Z").as_ref(), "ins_z", t.insertion_point.z, pos_editable),
|
||||
num_row(t!("Position X").as_ref(), "ins_x", position.x, true),
|
||||
num_row(t!("Position Y").as_ref(), "ins_y", position.y, true),
|
||||
num_row(t!("Position Z").as_ref(), "ins_z", position.z, true),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
|
|
@ -439,37 +536,64 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
|
|||
return;
|
||||
};
|
||||
match field {
|
||||
"ins_x" => t.insertion_point.x = v,
|
||||
"ins_y" => t.insertion_point.y = v,
|
||||
"ins_z" => t.insertion_point.z = v,
|
||||
"align_x" | "align_y" | "align_z" => {
|
||||
let ins = t.insertion_point;
|
||||
let ap = t.alignment_point.get_or_insert(ins);
|
||||
"ins_x" | "ins_y" | "ins_z" => {
|
||||
let plain_left = matches!(t.horizontal_alignment, HA::Left)
|
||||
&& matches!(t.vertical_alignment, VA::Baseline);
|
||||
let two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||
let target = if !plain_left && !two_point {
|
||||
let insertion = t.insertion_point;
|
||||
t.alignment_point.get_or_insert(insertion)
|
||||
} else {
|
||||
&mut t.insertion_point
|
||||
};
|
||||
match field {
|
||||
"align_x" => ap.x = v,
|
||||
"align_y" => ap.y = v,
|
||||
_ => ap.z = v,
|
||||
"ins_x" => target.x = v,
|
||||
"ins_y" => target.y = v,
|
||||
_ => target.z = v,
|
||||
}
|
||||
}
|
||||
"height" if v > 0.0 => t.height = v,
|
||||
"rotation" => t.rotation = v.to_radians(),
|
||||
"align_x" | "align_y" | "align_z" => {
|
||||
// Calculated display rows are intentionally not writable.
|
||||
return;
|
||||
}
|
||||
"height"
|
||||
if v > 0.0 && !matches!(t.horizontal_alignment, HA::Aligned) =>
|
||||
{
|
||||
t.height = v
|
||||
}
|
||||
"rotation" if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) => {
|
||||
t.rotation = v.to_radians()
|
||||
}
|
||||
"width_factor" if v > 0.0 => t.width_factor = v,
|
||||
"oblique_angle" => t.oblique_angle = v.to_radians(),
|
||||
"oblique_angle" if (-85.0..=85.0).contains(&v) => {
|
||||
t.oblique_angle = v.to_radians()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(t: &mut Text, _grip_id: usize, apply: GripApply) {
|
||||
fn apply_grip(t: &mut Text, grip_id: usize, apply: GripApply) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
t.insertion_point.x = p.x as f64;
|
||||
t.insertion_point.y = p.y as f64;
|
||||
t.insertion_point.z = p.z as f64;
|
||||
let target = if grip_id == 1 {
|
||||
let insertion = t.insertion_point;
|
||||
t.alignment_point.get_or_insert(insertion)
|
||||
} else {
|
||||
&mut t.insertion_point
|
||||
};
|
||||
target.x = p.x;
|
||||
target.y = p.y;
|
||||
target.z = p.z;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
t.insertion_point.x += d.x as f64;
|
||||
t.insertion_point.y += d.y as f64;
|
||||
t.insertion_point.z += d.z as f64;
|
||||
t.insertion_point.x += d.x;
|
||||
t.insertion_point.y += d.y;
|
||||
t.insertion_point.z += d.z;
|
||||
if let Some(point) = t.alignment_point.as_mut() {
|
||||
point.x += d.x;
|
||||
point.y += d.y;
|
||||
point.z += d.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -495,7 +619,7 @@ fn apply_transform(t: &mut Text, tr: &EntityTransform) {
|
|||
|
||||
impl RenderConvertible for Text {
|
||||
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
||||
Some(to_render(self, document))
|
||||
Some(to_render_at_scale(self, document, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -551,7 +675,9 @@ impl Grippable for Text {
|
|||
value: f64,
|
||||
) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
if matches!(action, A::RotateText) {
|
||||
if matches!(action, A::RotateText)
|
||||
&& !matches!(self.horizontal_alignment, HA::Aligned | HA::Fit)
|
||||
{
|
||||
self.rotation = value.to_radians();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use acadrust::entities::{
|
||||
Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA,
|
||||
};
|
||||
use acadrust::tables::TextStyle;
|
||||
use acadrust::types::Vector3;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::creation_style::TextCreationDefaults;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/text.svg"));
|
||||
|
|
@ -15,57 +22,434 @@ pub fn tool() -> ToolDef {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step {
|
||||
InsertPoint,
|
||||
Start,
|
||||
Justification,
|
||||
Style,
|
||||
Height,
|
||||
Rotation,
|
||||
SecondPoint,
|
||||
}
|
||||
|
||||
pub struct TextCommand {
|
||||
step: Step,
|
||||
plane: WorkingPlane,
|
||||
first_point: Option<DVec3>,
|
||||
second_point: Option<DVec3>,
|
||||
horizontal: HA,
|
||||
vertical: VA,
|
||||
style_name: String,
|
||||
styles: Vec<TextStyle>,
|
||||
height: f64,
|
||||
rotation: f64,
|
||||
width_factor: f64,
|
||||
oblique_angle: f64,
|
||||
fixed_height: bool,
|
||||
annotative: bool,
|
||||
annotation_multiplier: f64,
|
||||
last_display_height: Option<f64>,
|
||||
last_entity: Option<Text>,
|
||||
}
|
||||
|
||||
impl TextCommand {
|
||||
pub fn with_height(height: f64) -> Self {
|
||||
Self {
|
||||
step: Step::InsertPoint,
|
||||
height,
|
||||
pub fn with_defaults(
|
||||
defaults: TextCreationDefaults,
|
||||
styles: Vec<TextStyle>,
|
||||
annotation_multiplier: f64,
|
||||
) -> Self {
|
||||
let current_height = defaults.height;
|
||||
let mut command = Self {
|
||||
step: Step::Start,
|
||||
plane: WorkingPlane::default(),
|
||||
first_point: None,
|
||||
second_point: None,
|
||||
horizontal: HA::Left,
|
||||
vertical: VA::Baseline,
|
||||
style_name: defaults.style_name,
|
||||
styles,
|
||||
height: defaults.height,
|
||||
rotation: 0.0,
|
||||
width_factor: defaults.width_factor,
|
||||
oblique_angle: defaults.oblique_angle,
|
||||
fixed_height: false,
|
||||
annotative: false,
|
||||
annotation_multiplier: annotation_multiplier.max(1.0e-9),
|
||||
last_display_height: None,
|
||||
last_entity: None,
|
||||
};
|
||||
let style = command.style_name.clone();
|
||||
command.select_style(&style);
|
||||
if !command.fixed_height {
|
||||
command.height = current_height;
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn select_style(&mut self, name: &str) -> bool {
|
||||
let Some(style) = self
|
||||
.styles
|
||||
.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(name))
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
self.style_name = style.name;
|
||||
self.fixed_height = style.height > 1.0e-9;
|
||||
if self.fixed_height {
|
||||
self.height = style.height;
|
||||
} else if style.last_height > 1.0e-9 {
|
||||
self.height = style.last_height;
|
||||
}
|
||||
self.width_factor = style.width_factor.max(0.01);
|
||||
self.oblique_angle = style.oblique_angle.clamp(
|
||||
-85.0_f64.to_radians(),
|
||||
85.0_f64.to_radians(),
|
||||
);
|
||||
self.annotative = style.annotative;
|
||||
true
|
||||
}
|
||||
|
||||
fn set_justification(&mut self, value: &str) -> bool {
|
||||
let normalized = value.trim().to_ascii_uppercase().replace([' ', '-'], "");
|
||||
let alignment = match normalized.as_str() {
|
||||
"L" | "LEFT" => (HA::Left, VA::Baseline),
|
||||
"C" | "CENTER" => (HA::Center, VA::Baseline),
|
||||
"R" | "RIGHT" => (HA::Right, VA::Baseline),
|
||||
"A" | "ALIGNED" | "ALIGN" => (HA::Aligned, VA::Baseline),
|
||||
"M" | "MIDDLE" => (HA::Middle, VA::Baseline),
|
||||
"F" | "FIT" => (HA::Fit, VA::Baseline),
|
||||
"TL" | "TOPLEFT" => (HA::Left, VA::Top),
|
||||
"TC" | "TOPCENTER" => (HA::Center, VA::Top),
|
||||
"TR" | "TOPRIGHT" => (HA::Right, VA::Top),
|
||||
"ML" | "MIDDLELEFT" => (HA::Left, VA::Middle),
|
||||
"MC" | "MIDDLECENTER" => (HA::Center, VA::Middle),
|
||||
"MR" | "MIDDLERIGHT" => (HA::Right, VA::Middle),
|
||||
"BL" | "BOTTOMLEFT" => (HA::Left, VA::Bottom),
|
||||
"BC" | "BOTTOMCENTER" => (HA::Center, VA::Bottom),
|
||||
"BR" | "BOTTOMRIGHT" => (HA::Right, VA::Bottom),
|
||||
_ => return false,
|
||||
};
|
||||
self.horizontal = alignment.0;
|
||||
self.vertical = alignment.1;
|
||||
true
|
||||
}
|
||||
|
||||
fn is_two_point(&self) -> bool {
|
||||
matches!(self.horizontal, HA::Aligned | HA::Fit)
|
||||
}
|
||||
|
||||
fn after_first_point(&mut self) -> CmdResult {
|
||||
if self.is_two_point() {
|
||||
self.step = Step::SecondPoint;
|
||||
} else if self.fixed_height {
|
||||
self.step = Step::Rotation;
|
||||
} else {
|
||||
self.step = Step::Height;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn after_second_point(&mut self) -> CmdResult {
|
||||
if matches!(self.horizontal, HA::Fit) && !self.fixed_height {
|
||||
self.step = Step::Height;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.open_editor()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_entity(&self) -> Option<Text> {
|
||||
let first = self.plane.to_local(self.first_point?);
|
||||
let mut text = Text::with_value("", Vector3::new(first.x, first.y, first.z))
|
||||
.with_height(self.height.max(1.0e-9));
|
||||
text.style = self.style_name.clone();
|
||||
text.width_factor = self.width_factor.max(0.01);
|
||||
text.oblique_angle = self.oblique_angle;
|
||||
text.rotation = self.rotation;
|
||||
text.horizontal_alignment = self.horizontal;
|
||||
text.vertical_alignment = self.vertical;
|
||||
text.alignment_point = if self.is_two_point() {
|
||||
let second = self.plane.to_local(self.second_point?);
|
||||
Some(Vector3::new(second.x, second.y, second.z))
|
||||
} else if matches!((self.horizontal, self.vertical), (HA::Left, VA::Baseline)) {
|
||||
None
|
||||
} else {
|
||||
Some(Vector3::new(first.x, first.y, first.z))
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn open_editor(&mut self) -> CmdResult {
|
||||
let Some(entity) = self.make_entity() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let pos = self.first_point.unwrap_or(DVec3::ZERO);
|
||||
self.last_display_height = None;
|
||||
self.last_entity = Some(entity.clone());
|
||||
CmdResult::SuspendForTextInput { pos, entity }
|
||||
}
|
||||
|
||||
fn open_next_line(&mut self) -> CmdResult {
|
||||
let Some(mut entity) = self.last_entity.take() else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let angle = if matches!(entity.horizontal_alignment, HA::Aligned | HA::Fit) {
|
||||
entity.alignment_point.map_or(entity.rotation, |point| {
|
||||
(point.y - entity.insertion_point.y)
|
||||
.atan2(point.x - entity.insertion_point.x)
|
||||
})
|
||||
} else {
|
||||
entity.rotation
|
||||
};
|
||||
let fallback_height = entity.height.max(1.0e-9)
|
||||
* if self.annotative {
|
||||
self.annotation_multiplier
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let spacing = self
|
||||
.last_display_height
|
||||
.take()
|
||||
.unwrap_or(fallback_height)
|
||||
.max(1.0e-9)
|
||||
* 1.666_666_666_7;
|
||||
let delta = Vector3::new(angle.sin() * spacing, -angle.cos() * spacing, 0.0);
|
||||
entity.insertion_point = entity.insertion_point + delta;
|
||||
if let Some(point) = entity.alignment_point.as_mut() {
|
||||
*point = *point + delta;
|
||||
}
|
||||
entity.value.clear();
|
||||
let local = DVec3::new(
|
||||
entity.insertion_point.x,
|
||||
entity.insertion_point.y,
|
||||
entity.insertion_point.z,
|
||||
);
|
||||
let pos = self.plane.to_world(local);
|
||||
self.last_entity = Some(entity.clone());
|
||||
CmdResult::SuspendForTextInput { pos, entity }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TextCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TEXT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::InsertPoint => t!("TEXT Specify insertion point:").into_owned(),
|
||||
match self.step {
|
||||
Step::Start => format!(
|
||||
"{}\n{}",
|
||||
crate::tf!(
|
||||
"TEXT Current style: {}, Height: {}, Annotative: {}",
|
||||
self.style_name,
|
||||
self.height,
|
||||
if self.annotative { "Yes" } else { "No" }
|
||||
),
|
||||
t!("TEXT Specify start point or [Justify/Style]:")
|
||||
),
|
||||
Step::Justification => t!(
|
||||
"TEXT Enter justification [Left/Center/Right/Aligned/Middle/Fit/TL/TC/TR/ML/MC/MR/BL/BC/BR]:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Style => crate::tf!("TEXT Enter style name <{}>:", self.style_name).into_owned(),
|
||||
Step::Height if self.annotative => {
|
||||
crate::tf!("TEXT Specify paper text height <{}>:", self.height).into_owned()
|
||||
}
|
||||
Step::Height => crate::tf!("TEXT Specify height <{}>:", self.height).into_owned(),
|
||||
Step::Rotation => crate::tf!(
|
||||
"TEXT Specify rotation angle <{}>:",
|
||||
self.rotation.to_degrees()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::SecondPoint => t!("TEXT Specify second endpoint:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
// Hand off to the in-place plain-text editor anchored at the click.
|
||||
CmdResult::OpenTextEditor {
|
||||
pos: pt,
|
||||
handle: None,
|
||||
initial: String::new(),
|
||||
height: self.height,
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Start => vec![
|
||||
CmdOption::new(t!("Justify").as_ref(), "J"),
|
||||
CmdOption::new(t!("Style").as_ref(), "ST"),
|
||||
],
|
||||
Step::Justification => [
|
||||
("Left", "L"), ("Center", "C"), ("Right", "R"),
|
||||
("Aligned", "A"), ("Middle", "M"), ("Fit", "F"),
|
||||
("TL", "TL"), ("TC", "TC"), ("TR", "TR"),
|
||||
("ML", "ML"), ("MC", "MC"), ("MR", "MR"),
|
||||
("BL", "BL"), ("BC", "BC"), ("BR", "BR"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(label, keyword)| CmdOption::new(label, keyword))
|
||||
.collect(),
|
||||
Step::Style => self
|
||||
.styles
|
||||
.iter()
|
||||
.map(|style| CmdOption::new(&style.name, &style.name))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
Step::Justification | Step::Style | Step::Height | Step::Rotation
|
||||
)
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::Start)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let token = text.trim();
|
||||
let upper = token.to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::Start => match upper.as_str() {
|
||||
"J" | "JUSTIFY" | "JUSTIFICATION" => self.step = Step::Justification,
|
||||
"S" | "ST" | "STYLE" => self.step = Step::Style,
|
||||
_ => return None,
|
||||
},
|
||||
Step::Justification => {
|
||||
if !self.set_justification(token) {
|
||||
return None;
|
||||
}
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Step::Style => {
|
||||
if !self.select_style(token) {
|
||||
return None;
|
||||
}
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Step::Height => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
if !value.is_finite() || value <= 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
self.height = value;
|
||||
if self.is_two_point() {
|
||||
return Some(self.open_editor());
|
||||
}
|
||||
self.step = Step::Rotation;
|
||||
}
|
||||
Step::Rotation => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
self.rotation = value.to_radians();
|
||||
return Some(self.open_editor());
|
||||
}
|
||||
Step::SecondPoint => return None,
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Start => {
|
||||
self.first_point = Some(point);
|
||||
self.second_point = None;
|
||||
self.after_first_point()
|
||||
}
|
||||
Step::Height => {
|
||||
let Some(first) = self.first_point else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let value = self
|
||||
.plane
|
||||
.vector_to_local(point - first)
|
||||
.truncate()
|
||||
.length();
|
||||
if value <= 1.0e-9 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.height = value;
|
||||
if self.is_two_point() {
|
||||
self.open_editor()
|
||||
} else {
|
||||
self.step = Step::Rotation;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
Step::Rotation => {
|
||||
let Some(first) = self.first_point else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let Some(angle) = self.plane.angle(first, point) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.rotation = angle;
|
||||
self.open_editor()
|
||||
}
|
||||
Step::SecondPoint => {
|
||||
let Some(first) = self.first_point else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if self
|
||||
.plane
|
||||
.vector_to_local(point - first)
|
||||
.truncate()
|
||||
.length()
|
||||
<= 1.0e-9
|
||||
{
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.second_point = Some(point);
|
||||
self.after_second_point()
|
||||
}
|
||||
Step::Justification | Step::Style => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Start => CmdResult::Cancel,
|
||||
Step::Justification | Step::Style => {
|
||||
self.step = Step::Start;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Height => {
|
||||
if self.is_two_point() {
|
||||
self.open_editor()
|
||||
} else {
|
||||
self.step = Step::Rotation;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
Step::Rotation => self.open_editor(),
|
||||
Step::SecondPoint => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_editor_closed(&mut self, committed: bool) -> CmdResult {
|
||||
if committed {
|
||||
self.open_next_line()
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn on_editor_display_height(&mut self, height: f64) {
|
||||
if height.is_finite() && height > 1.0e-9 {
|
||||
self.last_display_height = Some(height);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
|
||||
fn on_mouse_move(&mut self, _point: DVec3) -> Option<WireModel> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] }); // TextCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] });
|
||||
|
|
|
|||
4
src/scene/cache/properties.rs
vendored
4
src/scene/cache/properties.rs
vendored
|
|
@ -94,7 +94,9 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
|||
],
|
||||
};
|
||||
|
||||
if matches!(entity, EntityType::LwPolyline(polyline) if crate::entities::lwpolyline::is_rectangle(polyline)) {
|
||||
if matches!(entity, EntityType::Text(_))
|
||||
|| matches!(entity, EntityType::LwPolyline(polyline) if crate::entities::lwpolyline::is_rectangle(polyline))
|
||||
{
|
||||
section.props.retain(|prop| prop.field != "handle");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -633,7 +633,14 @@ pub fn tessellate(
|
|||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
|
||||
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
|
||||
.or_else(|| convert(entity, document));
|
||||
.or_else(|| match entity {
|
||||
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
|
||||
text,
|
||||
document,
|
||||
anno_scale,
|
||||
)),
|
||||
_ => convert(entity, document),
|
||||
});
|
||||
if let Some(te) = te {
|
||||
match te.object {
|
||||
// ── Text / MText: pre-tessellated glyph strokes ───────────────
|
||||
|
|
|
|||
|
|
@ -134,10 +134,6 @@ fn apply_text_defaults(doc: &CadDocument, entity: &mut EntityType) {
|
|||
if text.oblique_angle.abs() <= 1.0e-9 {
|
||||
text.oblique_angle = resolved.oblique_angle;
|
||||
}
|
||||
if text.generation_flags == 0 {
|
||||
text.generation_flags = (if resolved.flags.backward { 2 } else { 0 })
|
||||
| (if resolved.flags.upside_down { 4 } else { 0 });
|
||||
}
|
||||
}
|
||||
EntityType::MText(text) => {
|
||||
if resolved.height > 1.0e-9 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue