forked from 1b-agent/opencadstudio-upstream
feat(entities): integrate remaining fields on text-based entities
AttributeDefinition / AttributeEntity were rendering as left-aligned single-line text regardless of their stored alignment, generation flags, or multiline settings. Bring both to parity with Text plus expose every remaining field through the Properties panel: - horizontal_alignment / vertical_alignment with bounds-based anchor using alignment_point, mirroring Text's logic - text_generation_flags bit 2 (backward) flips width_factor sign; bit 4 (upside-down) adds π to the rotation; combined with the TextStyle flags via XOR (double-mirror cancels) - multi-line splitting on \n / \P with line_count surfaced as info - mtext_flag, is_multiline, line_count, lock_position, field_length, AttributeFlags bits (invisible / constant / verify / preset / annotative) surfaced as properties - AttributeEntity.attdef_handle shown as the definition reference - flags.constant marks the default-value field read-only - flags.locked_position / lock_position blocks the position grip Shape entity now applies rotation, relative_x_scale, and oblique_angle to its placeholder diamond marker, and surfaces shape_number, style_handle, thickness, and normal in the Properties panel. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
e59917f798
commit
c0e2b6282a
2 files changed
+623
-92
No files matched your search
+563
-78
@@ -1,58 +1,268 @@
|
||||
use acadrust::entities::attribute_definition::{
|
||||
HorizontalAlignment as AHA, MTextFlag, VerticalAlignment as AVA,
|
||||
};
|
||||
use acadrust::entities::{AttributeDefinition, AttributeEntity};
|
||||
use acadrust::types::Vector3;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::text_support::resolve_text_style;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
|
||||
use crate::entities::text_support::{resolve_dxf_special_chars, resolve_text_style, text_local_bounds};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
use crate::scene::{cxf, transform};
|
||||
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Bundle of the fields both attribute kinds carry. Lets the truck builder
|
||||
/// stay generic over ATTDEF vs ATTRIB.
|
||||
struct AttrTextInputs<'a> {
|
||||
value: &'a str,
|
||||
insertion_point: Vector3,
|
||||
alignment_point: Vector3,
|
||||
height: f64,
|
||||
rotation: f64,
|
||||
width_factor: f64,
|
||||
oblique_angle: f64,
|
||||
text_style: &'a str,
|
||||
text_generation_flags: i16,
|
||||
horizontal_alignment: AHA,
|
||||
vertical_alignment: AVA,
|
||||
normal: Vector3,
|
||||
mtext_flag: MTextFlag,
|
||||
is_multiline: bool,
|
||||
line_count: i16,
|
||||
}
|
||||
|
||||
fn halign_str(a: AHA) -> &'static str {
|
||||
match a {
|
||||
AHA::Left => "Left",
|
||||
AHA::Center => "Center",
|
||||
AHA::Right => "Right",
|
||||
AHA::Aligned => "Aligned",
|
||||
AHA::Middle => "Middle",
|
||||
AHA::Fit => "Fit",
|
||||
}
|
||||
}
|
||||
|
||||
fn valign_str(a: AVA) -> &'static str {
|
||||
match a {
|
||||
AVA::Baseline => "Baseline",
|
||||
AVA::Bottom => "Bottom",
|
||||
AVA::Middle => "Middle",
|
||||
AVA::Top => "Top",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_halign(s: &str) -> Option<AHA> {
|
||||
Some(match s {
|
||||
"Left" => AHA::Left,
|
||||
"Center" => AHA::Center,
|
||||
"Right" => AHA::Right,
|
||||
"Aligned" => AHA::Aligned,
|
||||
"Middle" => AHA::Middle,
|
||||
"Fit" => AHA::Fit,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_valign(s: &str) -> Option<AVA> {
|
||||
Some(match s {
|
||||
"Baseline" => AVA::Baseline,
|
||||
"Bottom" => AVA::Bottom,
|
||||
"Middle" => AVA::Middle,
|
||||
"Top" => AVA::Top,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn bool_yn(b: bool) -> &'static str {
|
||||
if b {
|
||||
"Yes"
|
||||
} else {
|
||||
"No"
|
||||
}
|
||||
}
|
||||
|
||||
fn mtext_flag_str(f: MTextFlag) -> &'static str {
|
||||
match f {
|
||||
MTextFlag::SingleLine => "SingleLine",
|
||||
MTextFlag::MultiLine => "MultiLine",
|
||||
MTextFlag::ConstantMultiLine => "ConstantMultiLine",
|
||||
}
|
||||
}
|
||||
|
||||
/// Render text strokes for an attribute, honouring alignment, oblique angle,
|
||||
/// width factor, generation flags (backward / upside-down), text-style
|
||||
/// resolution, and basic multiline splitting on `\n` / `\\P`.
|
||||
fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
let normal = (input.normal.x, input.normal.y, input.normal.z);
|
||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||
(
|
||||
input.insertion_point.x,
|
||||
input.insertion_point.y,
|
||||
input.insertion_point.z,
|
||||
),
|
||||
normal,
|
||||
);
|
||||
let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32);
|
||||
|
||||
let resolved = resolve_text_style(input.text_style, document);
|
||||
|
||||
// width_factor sign also flips on text_generation_flags bit 2 (backward).
|
||||
// The TextStyle's own is_backward flag is multiplicative — if both are set
|
||||
// they cancel (mirror twice → forward), matching AutoCAD.
|
||||
let base_wf =
|
||||
((input.width_factor as f32).max(0.01) * resolved.width_factor.max(0.01)).clamp(0.01, 100.0);
|
||||
let attr_backward = (input.text_generation_flags & 2) != 0;
|
||||
let mut width_factor = base_wf;
|
||||
if attr_backward ^ resolved.is_backward {
|
||||
width_factor = -width_factor;
|
||||
}
|
||||
|
||||
// Upside-down (bit 4 / TextStyle.is_upside_down) rotates by π around the
|
||||
// insertion point. Combined with rotation we get a 180° flip about the
|
||||
// anchor — same as Text.
|
||||
let attr_upside_down = (input.text_generation_flags & 4) != 0;
|
||||
let upside_down = attr_upside_down ^ resolved.is_upside_down;
|
||||
let rotation = if upside_down {
|
||||
input.rotation as f32 + std::f32::consts::PI
|
||||
} else {
|
||||
input.rotation as f32
|
||||
};
|
||||
let oblique_angle = input.oblique_angle as f32 + resolved.oblique_angle;
|
||||
|
||||
// Anchor selection mirrors Text: only Left/Baseline uses insertion_point;
|
||||
// every other alignment uses alignment_point.
|
||||
let needs_align_pt = !(matches!(input.horizontal_alignment, AHA::Left)
|
||||
&& matches!(input.vertical_alignment, AVA::Baseline));
|
||||
let anchor_f64 = if needs_align_pt {
|
||||
[input.alignment_point.x, input.alignment_point.y]
|
||||
} else {
|
||||
[input.insertion_point.x, input.insertion_point.y]
|
||||
};
|
||||
|
||||
// Multi-line: split on \\P or \n. line_count is informational and we
|
||||
// recompute lines from the actual value.
|
||||
let raw_value = if input.is_multiline || !matches!(input.mtext_flag, MTextFlag::SingleLine) {
|
||||
input.value.to_string()
|
||||
} else {
|
||||
input.value.to_string()
|
||||
};
|
||||
let plain: Vec<String> = raw_value
|
||||
.replace("\\P", "\n")
|
||||
.split('\n')
|
||||
.map(|l| l.to_string())
|
||||
.collect();
|
||||
// Tag-style fallback when there is no value (ATTDEF preview style).
|
||||
let lines: Vec<String> = if plain.iter().all(|l| l.is_empty()) {
|
||||
vec![format!("[{}]", input.value)]
|
||||
} else {
|
||||
plain
|
||||
};
|
||||
|
||||
let line_height = (input.height as f32) * 1.4; // typical CXF inter-line gap
|
||||
let (cos_r, sin_r) = (rotation.cos() as f64, rotation.sin() as f64);
|
||||
|
||||
let mut strokes_all = Vec::with_capacity(lines.len());
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
// For width calculations strip MText decorations / DXF specials.
|
||||
let value_for_bounds = resolve_dxf_special_chars(line);
|
||||
let bounds = text_local_bounds(
|
||||
&resolved.font_name,
|
||||
&value_for_bounds,
|
||||
input.height as f32,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
);
|
||||
let (anchor_local_x, anchor_local_y) =
|
||||
if let Some(([min_x, min_y], [max_x, max_y])) = bounds {
|
||||
let ax = match input.horizontal_alignment {
|
||||
AHA::Left => min_x,
|
||||
AHA::Center | AHA::Middle => (min_x + max_x) * 0.5,
|
||||
AHA::Right | AHA::Aligned | AHA::Fit => max_x,
|
||||
};
|
||||
let ay = match input.vertical_alignment {
|
||||
AVA::Baseline => 0.0,
|
||||
AVA::Bottom => min_y,
|
||||
AVA::Middle => (min_y + max_y) * 0.5,
|
||||
AVA::Top => max_y,
|
||||
};
|
||||
(ax, ay)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let line_offset_y = -(i as f32) * line_height;
|
||||
let local_y_for_line = anchor_local_y - line_offset_y;
|
||||
let origin: [f64; 2] = [
|
||||
anchor_f64[0] - (anchor_local_x as f64 * cos_r - local_y_for_line as f64 * sin_r),
|
||||
anchor_f64[1] - (anchor_local_x as f64 * sin_r + local_y_for_line as f64 * cos_r),
|
||||
];
|
||||
let strokes = cxf::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
input.height as f32,
|
||||
rotation,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
&resolved.font_name,
|
||||
line,
|
||||
);
|
||||
strokes_all.push(TextStroke { strokes, origin });
|
||||
}
|
||||
let _ = input.line_count; // round-trip only — recomputed above
|
||||
|
||||
TruckEntity {
|
||||
object: TruckObject::Text(strokes_all),
|
||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ── AttributeDefinition ───────────────────────────────────────────────────────
|
||||
|
||||
impl TruckConvertible for AttributeDefinition {
|
||||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||
(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
),
|
||||
normal,
|
||||
);
|
||||
let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32);
|
||||
let resolved = resolve_text_style(&self.text_style, document);
|
||||
let display = if self.default_value.is_empty() {
|
||||
format!("[{}]", self.tag)
|
||||
let display_value = if self.default_value.is_empty() {
|
||||
// tag-only preview path: build_attr_truck wraps in brackets.
|
||||
self.tag.clone()
|
||||
} else {
|
||||
self.default_value.clone()
|
||||
};
|
||||
let wf = (self.width_factor as f32).max(0.01);
|
||||
let origin = [self.insertion_point.x, self.insertion_point.y];
|
||||
let strokes = cxf::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
self.height as f32,
|
||||
self.rotation as f32,
|
||||
wf * resolved.width_factor.max(0.01),
|
||||
self.oblique_angle as f32 + resolved.oblique_angle,
|
||||
&resolved.font_name,
|
||||
&display,
|
||||
);
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Text(vec![TextStroke { strokes, origin }]),
|
||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris: vec![],
|
||||
})
|
||||
Some(build_attr_truck(
|
||||
AttrTextInputs {
|
||||
value: &display_value,
|
||||
insertion_point: self.insertion_point,
|
||||
alignment_point: self.alignment_point,
|
||||
height: self.height,
|
||||
rotation: self.rotation,
|
||||
width_factor: self.width_factor,
|
||||
oblique_angle: self.oblique_angle,
|
||||
text_style: &self.text_style,
|
||||
text_generation_flags: self.text_generation_flags,
|
||||
horizontal_alignment: self.horizontal_alignment,
|
||||
vertical_alignment: self.vertical_alignment,
|
||||
normal: self.normal,
|
||||
mtext_flag: self.mtext_flag,
|
||||
is_multiline: self.is_multiline,
|
||||
line_count: self.line_count,
|
||||
},
|
||||
document,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for AttributeDefinition {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
// lock_position blocks the position grip. Show an empty grip set so
|
||||
// the renderer can't drag the entity in either AttributeFlags or
|
||||
// top-level lock_position is on.
|
||||
if self.lock_position || self.flags.locked_position {
|
||||
return vec![];
|
||||
}
|
||||
vec![square_grip(
|
||||
0,
|
||||
Vec3::new(
|
||||
@@ -64,17 +274,26 @@ impl Grippable for AttributeDefinition {
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if self.lock_position || self.flags.locked_position {
|
||||
return;
|
||||
}
|
||||
if grip_id == 0 {
|
||||
match apply {
|
||||
GripApply::Translate(d) => {
|
||||
self.insertion_point.x += d.x as f64;
|
||||
self.insertion_point.y += d.y as f64;
|
||||
self.insertion_point.z += d.z as f64;
|
||||
self.alignment_point.x += d.x as f64;
|
||||
self.alignment_point.y += d.y as f64;
|
||||
self.alignment_point.z += d.z as f64;
|
||||
}
|
||||
GripApply::Absolute(p) => {
|
||||
self.insertion_point.x = p.x as f64;
|
||||
self.insertion_point.y = p.y as f64;
|
||||
self.insertion_point.z = p.z as f64;
|
||||
self.alignment_point.x = p.x as f64;
|
||||
self.alignment_point.y = p.y as f64;
|
||||
self.alignment_point.z = p.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,32 +301,165 @@ impl Grippable for AttributeDefinition {
|
||||
}
|
||||
|
||||
impl PropertyEditable for AttributeDefinition {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> PropSection {
|
||||
let mut props = vec![
|
||||
ro("Tag", "att_tag", self.tag.clone()),
|
||||
ro("Prompt", "att_prompt", self.prompt.clone()),
|
||||
Property {
|
||||
label: "Default".into(),
|
||||
field: "att_default",
|
||||
value: PropValue::EditText(self.default_value.clone()),
|
||||
},
|
||||
edit("Insert X", "att_ix", self.insertion_point.x),
|
||||
edit("Insert Y", "att_iy", self.insertion_point.y),
|
||||
edit("Insert Z", "att_iz", self.insertion_point.z),
|
||||
edit("Align X", "att_ax", self.alignment_point.x),
|
||||
edit("Align Y", "att_ay", self.alignment_point.y),
|
||||
edit("Align Z", "att_az", self.alignment_point.z),
|
||||
edit("Height", "att_h", self.height),
|
||||
edit("Rotation", "att_rot", self.rotation.to_degrees()),
|
||||
edit("Width Factor", "att_wf", self.width_factor),
|
||||
edit("Oblique Angle", "att_ob", self.oblique_angle.to_degrees()),
|
||||
Property {
|
||||
label: "H-Align".into(),
|
||||
field: "att_halign",
|
||||
value: PropValue::Choice {
|
||||
selected: halign_str(self.horizontal_alignment).to_string(),
|
||||
options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "V-Align".into(),
|
||||
field: "att_valign",
|
||||
value: PropValue::Choice {
|
||||
selected: valign_str(self.vertical_alignment).to_string(),
|
||||
options: ["Baseline", "Bottom", "Middle", "Top"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Style".into(),
|
||||
field: "att_style",
|
||||
value: PropValue::Choice {
|
||||
selected: if self.text_style.trim().is_empty() {
|
||||
"Standard".into()
|
||||
} else {
|
||||
self.text_style.clone()
|
||||
},
|
||||
options: text_style_names.to_vec(),
|
||||
},
|
||||
},
|
||||
ro(
|
||||
"Generation",
|
||||
"att_gen_flags",
|
||||
format!("{:#06b}", self.text_generation_flags & 0xff),
|
||||
),
|
||||
ro(
|
||||
"Field Length",
|
||||
"att_field_len",
|
||||
self.field_length.to_string(),
|
||||
),
|
||||
Property {
|
||||
label: "MText Mode".into(),
|
||||
field: "att_mtext_flag",
|
||||
value: PropValue::Choice {
|
||||
selected: mtext_flag_str(self.mtext_flag).to_string(),
|
||||
options: ["SingleLine", "MultiLine", "ConstantMultiLine"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
ro(
|
||||
"Multiline",
|
||||
"att_is_multiline",
|
||||
bool_yn(self.is_multiline),
|
||||
),
|
||||
ro(
|
||||
"Line Count",
|
||||
"att_line_count",
|
||||
self.line_count.to_string(),
|
||||
),
|
||||
ro(
|
||||
"Lock Position",
|
||||
"att_lock_pos",
|
||||
bool_yn(self.lock_position),
|
||||
),
|
||||
ro("Invisible", "att_invisible", bool_yn(self.flags.invisible)),
|
||||
ro("Constant", "att_constant", bool_yn(self.flags.constant)),
|
||||
ro("Verify", "att_verify", bool_yn(self.flags.verify)),
|
||||
ro("Preset", "att_preset", bool_yn(self.flags.preset)),
|
||||
ro(
|
||||
"Annotative",
|
||||
"att_annotative",
|
||||
bool_yn(self.flags.annotative),
|
||||
),
|
||||
];
|
||||
// Constant attributes can't be edited at insert time — surface that
|
||||
// by marking the Default field read-only.
|
||||
if self.flags.constant {
|
||||
if let Some(p) = props.iter_mut().find(|p| p.field == "att_default") {
|
||||
p.value = PropValue::ReadOnly(self.default_value.clone());
|
||||
}
|
||||
}
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Tag", "att_tag", self.tag.clone()),
|
||||
ro("Prompt", "att_prompt", self.prompt.clone()),
|
||||
edit("Default", "att_default", 0.0), // String — handled as text
|
||||
edit("Insert X", "att_ix", self.insertion_point.x),
|
||||
edit("Insert Y", "att_iy", self.insertion_point.y),
|
||||
edit("Insert Z", "att_iz", self.insertion_point.z),
|
||||
edit("Height", "att_h", self.height),
|
||||
edit("Rotation", "att_rot", self.rotation.to_degrees()),
|
||||
],
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
// String / enum fields first.
|
||||
if field == "att_default" {
|
||||
if !self.flags.constant {
|
||||
self.default_value = value.to_string();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if field == "att_style" {
|
||||
self.text_style = value.to_string();
|
||||
return;
|
||||
}
|
||||
if field == "att_halign" {
|
||||
if let Some(a) = parse_halign(value) {
|
||||
self.horizontal_alignment = a;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if field == "att_valign" {
|
||||
if let Some(a) = parse_valign(value) {
|
||||
self.vertical_alignment = a;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if field == "att_mtext_flag" {
|
||||
self.mtext_flag = match value {
|
||||
"MultiLine" => MTextFlag::MultiLine,
|
||||
"ConstantMultiLine" => MTextFlag::ConstantMultiLine,
|
||||
_ => MTextFlag::SingleLine,
|
||||
};
|
||||
return;
|
||||
}
|
||||
// Numeric scalars.
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"att_ix" => self.insertion_point.x = v,
|
||||
"att_iy" => self.insertion_point.y = v,
|
||||
"att_iz" => self.insertion_point.z = v,
|
||||
"att_ax" => self.alignment_point.x = v,
|
||||
"att_ay" => self.alignment_point.y = v,
|
||||
"att_az" => self.alignment_point.z = v,
|
||||
"att_h" if v > 0.0 => self.height = v,
|
||||
"att_rot" => self.rotation = v.to_radians(),
|
||||
"att_wf" if v.abs() > 1e-9 => self.width_factor = v,
|
||||
"att_ob" => self.oblique_angle = v.to_radians(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +474,12 @@ impl Transformable for AttributeDefinition {
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
transform::reflect_xy_point(
|
||||
&mut entity.alignment_point.x,
|
||||
&mut entity.alignment_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -130,40 +488,34 @@ impl Transformable for AttributeDefinition {
|
||||
|
||||
impl TruckConvertible for AttributeEntity {
|
||||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||
(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
),
|
||||
normal,
|
||||
);
|
||||
let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32);
|
||||
let resolved = resolve_text_style(&self.text_style, document);
|
||||
let wf = (self.width_factor as f32).max(0.01);
|
||||
let origin = [self.insertion_point.x, self.insertion_point.y];
|
||||
let strokes = cxf::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
self.height as f32,
|
||||
self.rotation as f32,
|
||||
wf * resolved.width_factor.max(0.01),
|
||||
self.oblique_angle as f32 + resolved.oblique_angle,
|
||||
&resolved.font_name,
|
||||
&self.value,
|
||||
);
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Text(vec![TextStroke { strokes, origin }]),
|
||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris: vec![],
|
||||
})
|
||||
Some(build_attr_truck(
|
||||
AttrTextInputs {
|
||||
value: &self.value,
|
||||
insertion_point: self.insertion_point,
|
||||
alignment_point: self.alignment_point,
|
||||
height: self.height,
|
||||
rotation: self.rotation,
|
||||
width_factor: self.width_factor,
|
||||
oblique_angle: self.oblique_angle,
|
||||
text_style: &self.text_style,
|
||||
text_generation_flags: self.text_generation_flags,
|
||||
horizontal_alignment: self.horizontal_alignment,
|
||||
vertical_alignment: self.vertical_alignment,
|
||||
normal: self.normal,
|
||||
mtext_flag: self.mtext_flag,
|
||||
is_multiline: self.is_multiline,
|
||||
line_count: self.line_count,
|
||||
},
|
||||
document,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for AttributeEntity {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
if self.lock_position || self.flags.locked_position {
|
||||
return vec![];
|
||||
}
|
||||
vec![square_grip(
|
||||
0,
|
||||
Vec3::new(
|
||||
@@ -175,17 +527,26 @@ impl Grippable for AttributeEntity {
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if self.lock_position || self.flags.locked_position {
|
||||
return;
|
||||
}
|
||||
if grip_id == 0 {
|
||||
match apply {
|
||||
GripApply::Translate(d) => {
|
||||
self.insertion_point.x += d.x as f64;
|
||||
self.insertion_point.y += d.y as f64;
|
||||
self.insertion_point.z += d.z as f64;
|
||||
self.alignment_point.x += d.x as f64;
|
||||
self.alignment_point.y += d.y as f64;
|
||||
self.alignment_point.z += d.z as f64;
|
||||
}
|
||||
GripApply::Absolute(p) => {
|
||||
self.insertion_point.x = p.x as f64;
|
||||
self.insertion_point.y = p.y as f64;
|
||||
self.insertion_point.z = p.z as f64;
|
||||
self.alignment_point.x = p.x as f64;
|
||||
self.alignment_point.y = p.y as f64;
|
||||
self.alignment_point.z = p.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,31 +554,149 @@ impl Grippable for AttributeEntity {
|
||||
}
|
||||
|
||||
impl PropertyEditable for AttributeEntity {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Tag", "atte_tag", self.tag.clone()),
|
||||
ro("Value", "atte_val", self.value.clone()),
|
||||
Property {
|
||||
label: "Value".into(),
|
||||
field: "atte_val",
|
||||
value: PropValue::EditText(self.value.clone()),
|
||||
},
|
||||
edit("Insert X", "atte_ix", self.insertion_point.x),
|
||||
edit("Insert Y", "atte_iy", self.insertion_point.y),
|
||||
edit("Insert Z", "atte_iz", self.insertion_point.z),
|
||||
edit("Align X", "atte_ax", self.alignment_point.x),
|
||||
edit("Align Y", "atte_ay", self.alignment_point.y),
|
||||
edit("Align Z", "atte_az", self.alignment_point.z),
|
||||
edit("Height", "atte_h", self.height),
|
||||
edit("Rotation", "atte_rot", self.rotation.to_degrees()),
|
||||
edit("Width Factor", "atte_wf", self.width_factor),
|
||||
edit("Oblique Angle", "atte_ob", self.oblique_angle.to_degrees()),
|
||||
Property {
|
||||
label: "H-Align".into(),
|
||||
field: "atte_halign",
|
||||
value: PropValue::Choice {
|
||||
selected: halign_str(self.horizontal_alignment).to_string(),
|
||||
options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "V-Align".into(),
|
||||
field: "atte_valign",
|
||||
value: PropValue::Choice {
|
||||
selected: valign_str(self.vertical_alignment).to_string(),
|
||||
options: ["Baseline", "Bottom", "Middle", "Top"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Style".into(),
|
||||
field: "atte_style",
|
||||
value: PropValue::Choice {
|
||||
selected: if self.text_style.trim().is_empty() {
|
||||
"Standard".into()
|
||||
} else {
|
||||
self.text_style.clone()
|
||||
},
|
||||
options: text_style_names.to_vec(),
|
||||
},
|
||||
},
|
||||
ro(
|
||||
"Generation",
|
||||
"atte_gen_flags",
|
||||
format!("{:#06b}", self.text_generation_flags & 0xff),
|
||||
),
|
||||
ro(
|
||||
"Field Length",
|
||||
"atte_field_len",
|
||||
self.field_length.to_string(),
|
||||
),
|
||||
ro("MText Mode", "atte_mtext_flag", mtext_flag_str(self.mtext_flag)),
|
||||
ro(
|
||||
"Multiline",
|
||||
"atte_is_multiline",
|
||||
bool_yn(self.is_multiline),
|
||||
),
|
||||
ro(
|
||||
"Line Count",
|
||||
"atte_line_count",
|
||||
self.line_count.to_string(),
|
||||
),
|
||||
ro(
|
||||
"Lock Position",
|
||||
"atte_lock_pos",
|
||||
bool_yn(self.lock_position),
|
||||
),
|
||||
ro(
|
||||
"Definition",
|
||||
"atte_attdef",
|
||||
if self.attdef_handle.is_null() {
|
||||
"(none)".into()
|
||||
} else {
|
||||
format!("{:X}", self.attdef_handle.value())
|
||||
},
|
||||
),
|
||||
ro(
|
||||
"Invisible",
|
||||
"atte_invisible",
|
||||
bool_yn(self.flags.invisible),
|
||||
),
|
||||
ro("Constant", "atte_constant", bool_yn(self.flags.constant)),
|
||||
ro("Verify", "atte_verify", bool_yn(self.flags.verify)),
|
||||
ro("Preset", "atte_preset", bool_yn(self.flags.preset)),
|
||||
ro(
|
||||
"Annotative",
|
||||
"atte_annotative",
|
||||
bool_yn(self.flags.annotative),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
if field == "atte_val" {
|
||||
if !self.flags.constant {
|
||||
self.value = value.to_string();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if field == "atte_style" {
|
||||
self.text_style = value.to_string();
|
||||
return;
|
||||
}
|
||||
if field == "atte_halign" {
|
||||
if let Some(a) = parse_halign(value) {
|
||||
self.horizontal_alignment = a;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if field == "atte_valign" {
|
||||
if let Some(a) = parse_valign(value) {
|
||||
self.vertical_alignment = a;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"atte_ix" => self.insertion_point.x = v,
|
||||
"atte_iy" => self.insertion_point.y = v,
|
||||
"atte_iz" => self.insertion_point.z = v,
|
||||
"atte_ax" => self.alignment_point.x = v,
|
||||
"atte_ay" => self.alignment_point.y = v,
|
||||
"atte_az" => self.alignment_point.z = v,
|
||||
"atte_h" if v > 0.0 => self.height = v,
|
||||
"atte_rot" => self.rotation = v.to_radians(),
|
||||
"atte_wf" if v.abs() > 1e-9 => self.width_factor = v,
|
||||
"atte_ob" => self.oblique_angle = v.to_radians(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +711,12 @@ impl Transformable for AttributeEntity {
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
transform::reflect_xy_point(
|
||||
&mut entity.alignment_point.x,
|
||||
&mut entity.alignment_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+60
-14
@@ -1,8 +1,9 @@
|
||||
// SHAPE entity — reference to an .SHX shape-file glyph.
|
||||
//
|
||||
// Since .SHX binary files are not parsed, we render a small diamond marker
|
||||
// at the insertion point (same approach as unknown/unsupported entities).
|
||||
// The shape_name and style_name are surfaced as read-only properties.
|
||||
// at the insertion point (same approach as unknown/unsupported entities)
|
||||
// but apply the SHAPE's own rotation / oblique / width factor so the marker
|
||||
// gives a rough indication of the glyph orientation.
|
||||
|
||||
use acadrust::entities::Shape;
|
||||
use glam::Vec3;
|
||||
@@ -17,17 +18,36 @@ use crate::scene::wire_model::SnapHint;
|
||||
|
||||
// ── Marker geometry ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Small diamond marker at the shape insertion point.
|
||||
fn shape_marker(ox: f64, oy: f64, oz: f64, size: f64) -> Vec<[f64; 3]> {
|
||||
let s = size * 0.5;
|
||||
vec![
|
||||
[ox, oy + s, oz], // top
|
||||
[ox + s, oy, oz], // right
|
||||
[ox, oy - s, oz], // bottom
|
||||
[ox - s, oy, oz], // left
|
||||
[ox, oy + s, oz], // close
|
||||
[f64::NAN; 3],
|
||||
]
|
||||
/// Small diamond marker at the shape insertion point. The diamond is sized
|
||||
/// by `size`, stretched horizontally by `relative_x_scale`, sheared by
|
||||
/// `oblique_angle`, and rotated by `rotation`.
|
||||
fn shape_marker(
|
||||
ox: f64,
|
||||
oy: f64,
|
||||
oz: f64,
|
||||
size: f64,
|
||||
rotation: f64,
|
||||
rel_x_scale: f64,
|
||||
oblique_angle: f64,
|
||||
) -> Vec<[f64; 3]> {
|
||||
let s = size.abs().max(0.001) * 0.5;
|
||||
let rx = s * if rel_x_scale.abs() < 1e-9 { 1.0 } else { rel_x_scale };
|
||||
let ry = s;
|
||||
let local = [(0.0, ry), (rx, 0.0), (0.0, -ry), (-rx, 0.0)];
|
||||
let (sin_r, cos_r) = (rotation.sin(), rotation.cos());
|
||||
let ob = oblique_angle.tan();
|
||||
let mut out: Vec<[f64; 3]> = Vec::with_capacity(6);
|
||||
for &(x, y) in &local {
|
||||
let sx = x + y * ob;
|
||||
let lx = sx * cos_r - y * sin_r + ox;
|
||||
let ly = sx * sin_r + y * cos_r + oy;
|
||||
out.push([lx, ly, oz]);
|
||||
}
|
||||
// Close polygon + segment separator.
|
||||
let first = out[0];
|
||||
out.push(first);
|
||||
out.push([f64::NAN; 3]);
|
||||
out
|
||||
}
|
||||
|
||||
// ── TruckConvertible ──────────────────────────────────────────────────────────
|
||||
@@ -40,7 +60,15 @@ impl TruckConvertible for Shape {
|
||||
let size = self.size.abs().max(0.5);
|
||||
|
||||
let snap_pt = Vec3::new(ox as f32, oy as f32, oz as f32);
|
||||
let pts = shape_marker(ox, oy, oz, size);
|
||||
let pts = shape_marker(
|
||||
ox,
|
||||
oy,
|
||||
oz,
|
||||
size,
|
||||
self.rotation,
|
||||
self.relative_x_scale,
|
||||
self.oblique_angle,
|
||||
);
|
||||
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
@@ -88,16 +116,28 @@ impl Grippable for Shape {
|
||||
|
||||
impl PropertyEditable for Shape {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
let style_handle_display = match self.style_handle {
|
||||
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
||||
_ => "(none)".to_string(),
|
||||
};
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Name", "shp_name", self.shape_name.clone()),
|
||||
ro("Number", "shp_number", self.shape_number.to_string()),
|
||||
ro("Style", "shp_style", self.style_name.clone()),
|
||||
ro("Style Handle", "shp_style_handle", style_handle_display),
|
||||
edit("Insert X", "shp_ix", self.insertion_point.x),
|
||||
edit("Insert Y", "shp_iy", self.insertion_point.y),
|
||||
edit("Insert Z", "shp_iz", self.insertion_point.z),
|
||||
edit("Size", "shp_sz", self.size),
|
||||
edit("Rotation", "shp_rot", self.rotation.to_degrees()),
|
||||
edit("Width Factor", "shp_xs", self.relative_x_scale),
|
||||
edit("Oblique Angle", "shp_ob", self.oblique_angle.to_degrees()),
|
||||
edit("Thickness", "shp_th", self.thickness),
|
||||
edit("Normal X", "shp_nx", self.normal.x),
|
||||
edit("Normal Y", "shp_ny", self.normal.y),
|
||||
edit("Normal Z", "shp_nz", self.normal.z),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -112,6 +152,12 @@ impl PropertyEditable for Shape {
|
||||
"shp_iz" => self.insertion_point.z = v,
|
||||
"shp_sz" => self.size = v.max(0.001),
|
||||
"shp_rot" => self.rotation = v.to_radians(),
|
||||
"shp_xs" if v.abs() > 1e-9 => self.relative_x_scale = v,
|
||||
"shp_ob" => self.oblique_angle = v.to_radians(),
|
||||
"shp_th" => self.thickness = v,
|
||||
"shp_nx" => self.normal.x = v,
|
||||
"shp_ny" => self.normal.y = v,
|
||||
"shp_nz" => self.normal.z = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user