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,
|
&initial,
|
||||||
height,
|
height,
|
||||||
super::text_inline::TextEntityField::Text,
|
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 } => {
|
CmdResult::EditTextEntity { handle } => {
|
||||||
|
|
|
||||||
|
|
@ -1268,11 +1268,16 @@ impl OpenCADStudio {
|
||||||
// ── Annotate commands ──────────────────────────────────────────
|
// ── Annotate commands ──────────────────────────────────────────
|
||||||
"TEXT" => {
|
"TEXT" => {
|
||||||
use crate::modules::annotate::text::TextCommand;
|
use crate::modules::annotate::text::TextCommand;
|
||||||
let height = crate::scene::creation_style::current_text_defaults(
|
let (defaults, styles, annotation_multiplier) = {
|
||||||
&self.tabs[i].scene.document,
|
let scene = &self.tabs[i].scene;
|
||||||
)
|
let annotation_multiplier = scene.creation_annotation_multiplier();
|
||||||
.height;
|
let defaults =
|
||||||
let new_cmd = TextCommand::with_height(height);
|
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.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
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() {
|
if !group_names.is_empty() {
|
||||||
let label = group_names.join(", ");
|
let label = group_names.join(", ");
|
||||||
if let Some(general) = sections.first_mut() {
|
if let Some(general) = sections.first_mut() {
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,9 @@ pub struct TextInlineState {
|
||||||
pub editing: Option<Handle>,
|
pub editing: Option<Handle>,
|
||||||
/// Which entity slot this session writes to on commit.
|
/// Which entity slot this session writes to on commit.
|
||||||
pub field: TextEntityField,
|
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).
|
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
||||||
pub screen_anchor: iced::Point,
|
pub screen_anchor: iced::Point,
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +172,7 @@ impl super::OpenCADStudio {
|
||||||
self.open_mtext_editor(pos, Some(target), &value, height);
|
self.open_mtext_editor(pos, Some(target), &value, height);
|
||||||
self.unfocus_widgets()
|
self.unfocus_widgets()
|
||||||
} else {
|
} 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))
|
iced::widget::operation::focus(iced::widget::Id::new(super::view::TEXT_INLINE_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +186,7 @@ impl super::OpenCADStudio {
|
||||||
initial: &str,
|
initial: &str,
|
||||||
height: f64,
|
height: f64,
|
||||||
field: TextEntityField,
|
field: TextEntityField,
|
||||||
|
creation: Option<Text>,
|
||||||
) {
|
) {
|
||||||
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -193,6 +197,7 @@ impl super::OpenCADStudio {
|
||||||
height: if height > 0.0 { height } else { 0.25 },
|
height: if height > 0.0 { height } else { 0.25 },
|
||||||
editing: handle,
|
editing: handle,
|
||||||
field,
|
field,
|
||||||
|
creation,
|
||||||
screen_anchor: iced::Point::new(60.0, 90.0),
|
screen_anchor: iced::Point::new(60.0, 90.0),
|
||||||
};
|
};
|
||||||
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
|
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
|
||||||
|
|
@ -229,12 +234,18 @@ impl super::OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
crate::command::WorkingPlane::default()
|
crate::command::WorkingPlane::default()
|
||||||
};
|
};
|
||||||
let position = plane.to_local(ed.pos);
|
let command_creation = ed.creation.is_some();
|
||||||
let mut t = Text::with_value(
|
let mut t = if let Some(mut prepared) = ed.creation {
|
||||||
&ed.value,
|
prepared.value = ed.value.clone();
|
||||||
Vector3::new(position.x, position.y, position.z),
|
prepared
|
||||||
)
|
} else {
|
||||||
.with_height(ed.height);
|
let position = plane.to_local(ed.pos);
|
||||||
|
Text::with_value(
|
||||||
|
&ed.value,
|
||||||
|
Vector3::new(position.x, position.y, position.z),
|
||||||
|
)
|
||||||
|
.with_height(ed.height)
|
||||||
|
};
|
||||||
// New text inherits the document's current text style (STYLE), not
|
// New text inherits the document's current text style (STYLE), not
|
||||||
// the entity default. See #92.
|
// the entity default. See #92.
|
||||||
let cur_style = self.tabs[i]
|
let cur_style = self.tabs[i]
|
||||||
|
|
@ -244,24 +255,48 @@ impl super::OpenCADStudio {
|
||||||
.current_text_style_name
|
.current_text_style_name
|
||||||
.clone();
|
.clone();
|
||||||
if !cur_style.is_empty() {
|
if !cur_style.is_empty() {
|
||||||
t.style = cur_style;
|
if t.style.trim().is_empty() {
|
||||||
}
|
t.style = cur_style;
|
||||||
let annotative = crate::scene::annotative::text_style_is_annotative(
|
|
||||||
&self.tabs[i].scene.document,
|
|
||||||
&t.style,
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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");
|
||||||
|
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.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
|
|
||||||
|
|
@ -1162,6 +1162,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
if matches!(
|
if matches!(
|
||||||
item.action,
|
item.action,
|
||||||
GripMenuAction::Stretch
|
GripMenuAction::Stretch
|
||||||
|
| GripMenuAction::MoveWithText
|
||||||
| GripMenuAction::MoveWithDimLine
|
| GripMenuAction::MoveWithDimLine
|
||||||
| GripMenuAction::MoveWithLeader
|
| GripMenuAction::MoveWithLeader
|
||||||
| GripMenuAction::MoveIndependent
|
| 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)
|
(crate::entities::multileader::MOVE_ALL_GRIP, true)
|
||||||
} else {
|
} 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(
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||||
popup.handle,
|
popup.handle,
|
||||||
|
|
|
||||||
|
|
@ -1443,6 +1443,14 @@ pub enum CmdResult {
|
||||||
initial: String,
|
initial: String,
|
||||||
height: f64,
|
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.
|
/// Apply new pattern/scale/angle to an existing hatch entity.
|
||||||
HatcheditApply {
|
HatcheditApply {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
|
|
@ -1861,6 +1869,8 @@ pub trait CadCommand: Send {
|
||||||
/// Resume the command with collected rich text.
|
/// Resume the command with collected rich text.
|
||||||
fn on_editor_text(&mut self, _value: String) {}
|
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.
|
/// Called when the user clicks and `needs_entity_pick()` is true.
|
||||||
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
|
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
|
||||||
fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
|
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)
|
(HA::Left, VA::Baseline)
|
||||||
);
|
);
|
||||||
if needs_alignment_point {
|
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);
|
t.alignment_point = Some(t.insertion_point);
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
/// 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
|
/// 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.
|
/// 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
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity {
|
pub(crate) fn to_render_at_scale(
|
||||||
let p = text_run_placement(t, document);
|
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]);
|
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.
|
// Parse `%%` codes via acadrust, re-encoded for the stroke tessellator.
|
||||||
let value = acad_text_encode(&p.value);
|
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
|
/// Compute a TEXT entity's run placement (origin + layout params). Extracted
|
||||||
/// from `to_render` verbatim so the stroke and SDF-quad paths agree exactly.
|
/// 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 normal = (t.normal.x, t.normal.y, t.normal.z);
|
||||||
let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs(
|
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 resolved_style = resolve_text_style(&t.style, document);
|
||||||
let font_name = resolved_style.font_name;
|
let font_name = resolved_style.font_name;
|
||||||
// AutoCAD text geometry rule: the entity stores the FINAL width factor /
|
// The entity stores the final width factor and oblique angle copied from
|
||||||
// oblique angle, copied from the style at creation and persisting through
|
// its style at creation. Only fall back to the style when an omitted field
|
||||||
// style edits. Use it as-is. Only fall back to the style when the entity
|
// was read as zero.
|
||||||
// value is missing (the parser reports 0.0 for default-omitted fields).
|
|
||||||
let base_wf = if t.width_factor.abs() > 1e-9 {
|
let base_wf = if t.width_factor.abs() > 1e-9 {
|
||||||
(t.width_factor as f32).clamp(0.01, 100.0)
|
(t.width_factor as f32).clamp(0.01, 100.0)
|
||||||
} else {
|
} 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.
|
// mirror, and XOR keeps a double mirror an involution.
|
||||||
let eff_backward = resolved_style.is_backward ^ (t.generation_flags & 0x2 != 0);
|
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 eff_upside = resolved_style.is_upside_down ^ (t.generation_flags & 0x4 != 0);
|
||||||
let width_factor = if eff_backward { -base_wf } else { base_wf };
|
let mut 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 oblique_angle = if t.oblique_angle.abs() > 1e-9 {
|
let oblique_angle = if t.oblique_angle.abs() > 1e-9 {
|
||||||
t.oblique_angle as f32
|
t.oblique_angle as f32
|
||||||
} else {
|
} else {
|
||||||
resolved_style.oblique_angle
|
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
|
// Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of
|
||||||
// precision when cast to f32, which snaps text baselines onto a coarse
|
// precision when cast to f32, which snaps text baselines onto a coarse
|
||||||
// grid and makes adjacent rows collide. Only the small local offsets
|
// 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.vertical_alignment,
|
||||||
&t.alignment_point,
|
&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],
|
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
|
||||||
(_, VA::Bottom | VA::Middle | VA::Top, 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],
|
_ => [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(
|
let bounds = text_local_bounds(
|
||||||
&font_name,
|
&font_name,
|
||||||
&value_for_bounds,
|
&value_for_bounds,
|
||||||
t.height as f32,
|
height,
|
||||||
width_factor,
|
width_factor,
|
||||||
oblique_angle,
|
oblique_angle,
|
||||||
);
|
);
|
||||||
|
|
@ -213,7 +283,8 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||||
let ax = match t.horizontal_alignment {
|
let ax = match t.horizontal_alignment {
|
||||||
HA::Left => 0.0,
|
HA::Left => 0.0,
|
||||||
HA::Center | HA::Middle => b.advance * 0.5 * sign,
|
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).
|
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
||||||
let ay = match t.vertical_alignment {
|
let ay = match t.vertical_alignment {
|
||||||
|
|
@ -236,7 +307,7 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||||
];
|
];
|
||||||
TextPlacement {
|
TextPlacement {
|
||||||
origin,
|
origin,
|
||||||
height: t.height as f32,
|
height,
|
||||||
rotation,
|
rotation,
|
||||||
width_factor,
|
width_factor,
|
||||||
oblique_angle,
|
oblique_angle,
|
||||||
|
|
@ -247,12 +318,19 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grips(t: &Text) -> Vec<GripDef> {
|
fn grips(t: &Text) -> Vec<GripDef> {
|
||||||
let p = glam::DVec3::new(
|
let insertion = glam::DVec3::new(
|
||||||
t.insertion_point.x,
|
t.insertion_point.x,
|
||||||
t.insertion_point.y,
|
t.insertion_point.y,
|
||||||
t.insertion_point.z,
|
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> {
|
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.
|
// both points are live.
|
||||||
let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
|
let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
|
||||||
&& matches!(t.vertical_alignment, VA::Baseline);
|
&& matches!(t.vertical_alignment, VA::Baseline);
|
||||||
let pos_editable = is_plain_left || matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
let is_aligned = matches!(t.horizontal_alignment, HA::Aligned);
|
||||||
let align_editable = !is_plain_left;
|
let is_two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||||
// The alignment point is meaningless (reset to the origin) for plain-Left text.
|
// 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 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)
|
(ap.x, ap.y, ap.z)
|
||||||
} else {
|
} else {
|
||||||
(0.0, 0.0, 0.0)
|
(0.0, 0.0, 0.0)
|
||||||
};
|
};
|
||||||
|
let position = if position_uses_alignment {
|
||||||
|
ap
|
||||||
|
} else {
|
||||||
|
t.insertion_point
|
||||||
|
};
|
||||||
vec![
|
vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Text").into_owned(),
|
title: t!("Text").into_owned(),
|
||||||
|
|
@ -331,22 +419,31 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
t!("Height").as_ref(),
|
t!("Height").as_ref(),
|
||||||
"height",
|
"height",
|
||||||
t.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(t!("Width factor").as_ref(), "width_factor", t.width_factor),
|
||||||
edit_angle(t!("Obliquing").as_ref(), "oblique_angle", t.oblique_angle.to_degrees()),
|
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 X").as_ref(), "align_x", ax, false),
|
||||||
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, align_editable),
|
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, false),
|
||||||
num_row(t!("Text alignment Z").as_ref(), "align_z", az, align_editable),
|
num_row(t!("Text alignment Z").as_ref(), "align_z", az, false),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
num_row(t!("Position X").as_ref(), "ins_x", t.insertion_point.x, pos_editable),
|
num_row(t!("Position X").as_ref(), "ins_x", position.x, true),
|
||||||
num_row(t!("Position Y").as_ref(), "ins_y", t.insertion_point.y, pos_editable),
|
num_row(t!("Position Y").as_ref(), "ins_y", position.y, true),
|
||||||
num_row(t!("Position Z").as_ref(), "ins_z", t.insertion_point.z, pos_editable),
|
num_row(t!("Position Z").as_ref(), "ins_z", position.z, true),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
|
|
@ -439,37 +536,64 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match field {
|
match field {
|
||||||
"ins_x" => t.insertion_point.x = v,
|
"ins_x" | "ins_y" | "ins_z" => {
|
||||||
"ins_y" => t.insertion_point.y = v,
|
let plain_left = matches!(t.horizontal_alignment, HA::Left)
|
||||||
"ins_z" => t.insertion_point.z = v,
|
&& matches!(t.vertical_alignment, VA::Baseline);
|
||||||
"align_x" | "align_y" | "align_z" => {
|
let two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||||
let ins = t.insertion_point;
|
let target = if !plain_left && !two_point {
|
||||||
let ap = t.alignment_point.get_or_insert(ins);
|
let insertion = t.insertion_point;
|
||||||
|
t.alignment_point.get_or_insert(insertion)
|
||||||
|
} else {
|
||||||
|
&mut t.insertion_point
|
||||||
|
};
|
||||||
match field {
|
match field {
|
||||||
"align_x" => ap.x = v,
|
"ins_x" => target.x = v,
|
||||||
"align_y" => ap.y = v,
|
"ins_y" => target.y = v,
|
||||||
_ => ap.z = v,
|
_ => target.z = v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"height" if v > 0.0 => t.height = v,
|
"align_x" | "align_y" | "align_z" => {
|
||||||
"rotation" => t.rotation = v.to_radians(),
|
// 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,
|
"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 {
|
match apply {
|
||||||
GripApply::Absolute(p) => {
|
GripApply::Absolute(p) => {
|
||||||
t.insertion_point.x = p.x as f64;
|
let target = if grip_id == 1 {
|
||||||
t.insertion_point.y = p.y as f64;
|
let insertion = t.insertion_point;
|
||||||
t.insertion_point.z = p.z as f64;
|
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) => {
|
GripApply::Translate(d) => {
|
||||||
t.insertion_point.x += d.x as f64;
|
t.insertion_point.x += d.x;
|
||||||
t.insertion_point.y += d.y as f64;
|
t.insertion_point.y += d.y;
|
||||||
t.insertion_point.z += d.z as f64;
|
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 {
|
impl RenderConvertible for Text {
|
||||||
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
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,
|
value: f64,
|
||||||
) {
|
) {
|
||||||
use crate::scene::model::object::GripMenuAction as A;
|
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();
|
self.rotation = value.to_radians();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use acadrust::entities::{
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA,
|
||||||
use crate::scene::model::wire_model::WireModel;
|
};
|
||||||
|
use acadrust::tables::TextStyle;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
use glam::DVec3;
|
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;
|
use crate::t;
|
||||||
|
|
||||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/text.svg"));
|
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 {
|
enum Step {
|
||||||
InsertPoint,
|
Start,
|
||||||
|
Justification,
|
||||||
|
Style,
|
||||||
|
Height,
|
||||||
|
Rotation,
|
||||||
|
SecondPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextCommand {
|
pub struct TextCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
plane: WorkingPlane,
|
||||||
|
first_point: Option<DVec3>,
|
||||||
|
second_point: Option<DVec3>,
|
||||||
|
horizontal: HA,
|
||||||
|
vertical: VA,
|
||||||
|
style_name: String,
|
||||||
|
styles: Vec<TextStyle>,
|
||||||
height: f64,
|
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 {
|
impl TextCommand {
|
||||||
pub fn with_height(height: f64) -> Self {
|
pub fn with_defaults(
|
||||||
Self {
|
defaults: TextCreationDefaults,
|
||||||
step: Step::InsertPoint,
|
styles: Vec<TextStyle>,
|
||||||
height,
|
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 {
|
impl CadCommand for TextCommand {
|
||||||
|
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||||
|
self.plane = plane;
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"TEXT"
|
"TEXT"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match self.step {
|
||||||
Step::InsertPoint => t!("TEXT Specify insertion point:").into_owned(),
|
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 {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
// Hand off to the in-place plain-text editor anchored at the click.
|
match self.step {
|
||||||
CmdResult::OpenTextEditor {
|
Step::Start => vec![
|
||||||
pos: pt,
|
CmdOption::new(t!("Justify").as_ref(), "J"),
|
||||||
handle: None,
|
CmdOption::new(t!("Style").as_ref(), "ST"),
|
||||||
initial: String::new(),
|
],
|
||||||
height: self.height,
|
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 {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, _point: DVec3) -> Option<WireModel> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] });
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] }); // TextCommand
|
|
||||||
|
|
|
||||||
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");
|
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.
|
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||||
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
|
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(|| 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 {
|
if let Some(te) = te {
|
||||||
match te.object {
|
match te.object {
|
||||||
// ── Text / MText: pre-tessellated glyph strokes ───────────────
|
// ── 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 {
|
if text.oblique_angle.abs() <= 1.0e-9 {
|
||||||
text.oblique_angle = resolved.oblique_angle;
|
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) => {
|
EntityType::MText(text) => {
|
||||||
if resolved.height > 1.0e-9 {
|
if resolved.height > 1.0e-9 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue