style: apply cargo fmt across workspace

This commit is contained in:
Karim Jerbi committed 2026-10-11 03:14:29 +01:00
1 parent 85710e5454
commit 9cf9ff2c5c
531 files changed
+57740 -33906

No files matched your search

+20 -18
View File
@@ -1,5 +1,5 @@
use codec::entities::Arc;
use crate::t;
use codec::entities::Arc;
use crate::command::EntityTransform;
use crate::entities::common::{
@@ -105,11 +105,7 @@ fn control_points(arc: &Arc) -> [glam::DVec3; 3] {
]
}
pub(crate) fn refit_grips(
arc: &mut Arc,
original: &Arc,
edits: &[(usize, glam::DVec3)],
) -> bool {
pub(crate) fn refit_grips(arc: &mut Arc, original: &Arc, edits: &[(usize, glam::DVec3)]) -> bool {
let mut points = control_points(original);
let mut changed = false;
for &(grip_id, point) in edits {
@@ -209,8 +205,16 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
edit(t!("Radius").as_ref(), "radius", arc.radius),
edit_angle(t!("Start angle").as_ref(), "start_angle", sa.to_degrees()),
edit_angle(t!("End angle").as_ref(), "end_angle", ea.to_degrees()),
ro(t!("Total angle").as_ref(), "total_angle", format_angle(total_angle.to_radians())),
ro(t!("Arc length").as_ref(), "arc_length", format_length(arc_length)),
ro(
t!("Total angle").as_ref(),
"total_angle",
format_angle(total_angle.to_radians()),
),
ro(
t!("Arc length").as_ref(),
"arc_length",
format_length(arc_length),
),
ro(t!("Area").as_ref(), "area", format_area(area)),
edit(t!("Normal X").as_ref(), "normal_x", arc.normal.x),
edit(t!("Normal Y").as_ref(), "normal_y", arc.normal.y),
@@ -357,7 +361,11 @@ impl crate::entities::traits::Grippable for Arc {
],
}
}
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
_grip_id: usize,
_action: crate::scene::model::object::GripMenuAction,
) {
// Radius / Arc Length / Lengthen all need a follow-up prompt;
// the actual edit happens in `apply_grip_menu_value`.
}
@@ -382,8 +390,8 @@ impl crate::entities::traits::Grippable for Arc {
action: crate::scene::model::object::GripMenuAction,
point: glam::DVec3,
) -> Option<f64> {
use kernel::geom2d::{Circle as KernelCircle, Curve as KernelCurve, Vec2};
use crate::scene::model::object::GripMenuAction as A;
use kernel::geom2d::{Circle as KernelCircle, Curve as KernelCurve, Vec2};
if self.radius <= 1.0e-9 {
return None;
}
@@ -532,17 +540,11 @@ mod tests {
let tangent = glam::DVec3::from_array(curve.tangent_at(0.5)).normalize();
let wanted_length = curve.length() + 1.25;
let arc_length = arc
.grip_menu_point_value(
3,
GripMenuAction::ArcLength,
middle + tangent * 1.25,
)
.grip_menu_point_value(3, GripMenuAction::ArcLength, middle + tangent * 1.25)
.expect("arc length value");
assert!((arc_length - wanted_length).abs() < 1.0e-9);
arc.apply_grip_menu_value(3, GripMenuAction::ArcLength, arc_length);
assert!(
(crate::entities::curve::arc_curve(&arc).length() - wanted_length).abs() < 1.0e-9
);
assert!((crate::entities::curve::arc_curve(&arc).length() - wanted_length).abs() < 1.0e-9);
}
}
+171 -52
View File
@@ -5,13 +5,15 @@ use codec::entities::{AttributeDefinition, AttributeEntity};
use codec::types::Vector3;
use crate::command::EntityTransform;
use crate::entities::common::{edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
use crate::entities::common::{
edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
};
use crate::entities::text_support::{
layout_mtext, resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{GlyphRun, RenderEntity, RenderObject, TextStroke};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
use crate::scene::text::lff;
@@ -265,7 +267,11 @@ fn build_attr_layout(
want_glyph_boxes: cells.is_some(),
});
if let Some(out) = cells.as_deref_mut() {
*out = Some((layout.glyph_boxes.clone(), [anchor_pt.x, anchor_pt.y], rotation));
*out = Some((
layout.glyph_boxes.clone(),
[anchor_pt.x, anchor_pt.y],
rotation,
));
}
let _ = input.line_count;
let _ = input.is_multiline;
@@ -313,25 +319,24 @@ fn build_attr_layout(
width_factor,
oblique_angle,
);
let (anchor_local_x, anchor_local_y) =
if let Some(b) = bounds {
// Horizontal anchor uses the pen advance box so leading /
// trailing spaces keep their width.
let ax = match input.horizontal_alignment {
AHA::Left => 0.0,
AHA::Center | AHA::Middle => b.advance * 0.5,
AHA::Right | AHA::Aligned | AHA::Fit => b.advance,
};
let ay = match input.vertical_alignment {
AVA::Baseline => 0.0,
AVA::Bottom => b.ink_min[1],
AVA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5,
AVA::Top => b.ink_max[1],
};
(ax, ay)
} else {
(0.0, 0.0)
let (anchor_local_x, anchor_local_y) = if let Some(b) = bounds {
// Horizontal anchor uses the pen advance box so leading /
// trailing spaces keep their width.
let ax = match input.horizontal_alignment {
AHA::Left => 0.0,
AHA::Center | AHA::Middle => b.advance * 0.5,
AHA::Right | AHA::Aligned | AHA::Fit => b.advance,
};
let ay = match input.vertical_alignment {
AVA::Baseline => 0.0,
AVA::Bottom => b.ink_min[1],
AVA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5,
AVA::Top => b.ink_max[1],
};
(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] = [
@@ -448,10 +453,16 @@ impl Grippable for AttributeDefinition {
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
let current = if grip_id == 1 { self.alignment_point } else { self.insertion_point };
let current = if grip_id == 1 {
self.alignment_point
} else {
self.insertion_point
};
let d = match apply {
GripApply::Translate(d) => glam::DVec3::new(d.x as f64, d.y as f64, d.z as f64),
GripApply::Absolute(p) => glam::DVec3::new(p.x as f64, p.y as f64, p.z as f64) - dv(current),
GripApply::Absolute(p) => {
glam::DVec3::new(p.x as f64, p.y as f64, p.z as f64) - dv(current)
}
};
// A two-point definition stretches the moved endpoint; any other
// grip moves the whole text.
@@ -582,7 +593,11 @@ fn is_yes(value: &str) -> bool {
}
fn row(label: &str, field: &'static str, value: PropValue) -> Property {
Property { label: label.into(), field, value }
Property {
label: label.into(),
field,
value,
}
}
fn set_bit(flags: i16, bit: i16, on: bool) -> i16 {
@@ -647,14 +662,30 @@ impl PropertyEditable for AttributeDefinition {
}
};
let text_props = vec![
row(t!("Tag").as_ref(), "att_tag", PropValue::PlainText(self.tag.clone())),
row(t!("Annotative").as_ref(), "att_annotative", yes_no(self.flags.annotative)),
row(
t!("Tag").as_ref(),
"att_tag",
PropValue::PlainText(self.tag.clone()),
),
row(
t!("Annotative").as_ref(),
"att_annotative",
yes_no(self.flags.annotative),
),
if self.flags.constant {
ro(t!("Prompt").as_ref(), "att_prompt", self.prompt.clone())
} else {
row(t!("Prompt").as_ref(), "att_prompt", PropValue::PlainText(self.prompt.clone()))
row(
t!("Prompt").as_ref(),
"att_prompt",
PropValue::PlainText(self.prompt.clone()),
)
},
row(t!("Value").as_ref(), "att_default", PropValue::PlainText(self.default_value.clone())),
row(
t!("Value").as_ref(),
"att_default",
PropValue::PlainText(self.default_value.clone()),
),
row(
t!("Style").as_ref(),
"att_style",
@@ -668,7 +699,11 @@ impl PropertyEditable for AttributeDefinition {
},
),
if self.is_multiline {
ro(t!("Justify").as_ref(), "att_justify", t!(justify_label(self)).into_owned())
ro(
t!("Justify").as_ref(),
"att_justify",
t!(justify_label(self)).into_owned(),
)
} else {
row(
t!("Justify").as_ref(),
@@ -686,15 +721,29 @@ impl PropertyEditable for AttributeDefinition {
self.height,
crate::entities::common::style_fixed_height(&self.text_style).is_none(),
),
edit_angle(t!("Rotation").as_ref(), "att_rot", self.rotation.to_degrees()),
edit_angle(
t!("Rotation").as_ref(),
"att_rot",
self.rotation.to_degrees(),
),
edit(t!("Width factor").as_ref(), "att_wf", self.width_factor),
edit_angle(t!("Obliquing").as_ref(), "att_ob", self.oblique_angle.to_degrees()),
ro(t!("Direction").as_ref(), "att_direction", t!("By style").into_owned()),
edit_angle(
t!("Obliquing").as_ref(),
"att_ob",
self.oblique_angle.to_degrees(),
),
ro(
t!("Direction").as_ref(),
"att_direction",
t!("By style").into_owned(),
),
if self.is_multiline {
edit(
t!("Boundary width").as_ref(),
"att_bwidth",
self.embedded_mtext.as_ref().map_or(0.0, |m| m.rectangle_width),
self.embedded_mtext
.as_ref()
.map_or(0.0, |m| m.rectangle_width),
)
} else {
ro(t!("Boundary width").as_ref(), "att_bwidth", String::new())
@@ -704,17 +753,48 @@ impl PropertyEditable for AttributeDefinition {
alignment(t!("Text alignment Z"), "att_az", self.alignment_point.z),
];
vec![
PropSection { title: t!("Text").into_owned(), props: text_props },
PropSection {
title: t!("Text").into_owned(),
props: text_props,
},
PropSection {
title: t!("Misc").into_owned(),
props: vec![
row(t!("Upside down").as_ref(), "att_upside_down", yes_no(self.text_generation_flags & 0x4 != 0)),
row(t!("Backward").as_ref(), "att_backward", yes_no(self.text_generation_flags & 0x2 != 0)),
row(t!("Invisible").as_ref(), "att_invisible", yes_no(self.flags.invisible)),
row(t!("Constant").as_ref(), "att_constant", yes_no(self.flags.constant)),
row(t!("Verify").as_ref(), "att_verify", yes_no(self.flags.verify)),
row(t!("Preset").as_ref(), "att_preset", yes_no(self.flags.preset)),
row(t!("Multiple lines").as_ref(), "att_multiline", yes_no(self.is_multiline)),
row(
t!("Upside down").as_ref(),
"att_upside_down",
yes_no(self.text_generation_flags & 0x4 != 0),
),
row(
t!("Backward").as_ref(),
"att_backward",
yes_no(self.text_generation_flags & 0x2 != 0),
),
row(
t!("Invisible").as_ref(),
"att_invisible",
yes_no(self.flags.invisible),
),
row(
t!("Constant").as_ref(),
"att_constant",
yes_no(self.flags.constant),
),
row(
t!("Verify").as_ref(),
"att_verify",
yes_no(self.flags.verify),
),
row(
t!("Preset").as_ref(),
"att_preset",
yes_no(self.flags.preset),
),
row(
t!("Multiple lines").as_ref(),
"att_multiline",
yes_no(self.is_multiline),
),
row(
t!("Lock position").as_ref(),
"att_lock_pos",
@@ -859,7 +939,10 @@ impl Transformable for AttributeDefinition {
/// The laid-out character cells of a multi-line (MTEXT) attribute, the
/// point they are relative to and the layout rotation; `None` for a
/// single-line attribute.
pub(crate) fn attribute_cells(a: &AttributeEntity, document: &codec::CadDocument) -> Option<AttrCells> {
pub(crate) fn attribute_cells(
a: &AttributeEntity,
document: &codec::CadDocument,
) -> Option<AttrCells> {
let mut out = None;
build_attr_layout(attr_inputs(a), document, Some(&mut out));
out
@@ -1015,12 +1098,32 @@ impl PropertyEditable for AttributeEntity {
self.height,
crate::entities::common::style_fixed_height(&self.text_style).is_none(),
),
edit_angle(t!("Rotation").as_ref(), "atte_rot", self.rotation.to_degrees()),
edit_angle(
t!("Rotation").as_ref(),
"atte_rot",
self.rotation.to_degrees(),
),
edit(t!("Width factor").as_ref(), "atte_wf", self.width_factor),
edit_angle(t!("Obliquing").as_ref(), "atte_ob", self.oblique_angle.to_degrees()),
edit(t!("Text alignment X").as_ref(), "atte_ax", self.alignment_point.x),
edit(t!("Text alignment Y").as_ref(), "atte_ay", self.alignment_point.y),
edit(t!("Text alignment Z").as_ref(), "atte_az", self.alignment_point.z),
edit_angle(
t!("Obliquing").as_ref(),
"atte_ob",
self.oblique_angle.to_degrees(),
),
edit(
t!("Text alignment X").as_ref(),
"atte_ax",
self.alignment_point.x,
),
edit(
t!("Text alignment Y").as_ref(),
"atte_ay",
self.alignment_point.y,
),
edit(
t!("Text alignment Z").as_ref(),
"atte_az",
self.alignment_point.z,
),
ro(
t!("Boundary width").as_ref(),
"atte_field_len",
@@ -1049,15 +1152,31 @@ impl PropertyEditable for AttributeEntity {
"atte_backward",
bool_yn(self.text_generation_flags & 0x2 != 0),
),
ro(t!("Invisible").as_ref(), "atte_invisible", bool_yn(self.flags.invisible)),
ro(
t!("Invisible").as_ref(),
"atte_invisible",
bool_yn(self.flags.invisible),
),
ro(
t!("Multiple lines").as_ref(),
"atte_mtext_flag",
mtext_flag_str(self.mtext_flag),
),
ro(t!("Constant").as_ref(), "atte_constant", bool_yn(self.flags.constant)),
ro(t!("Verify").as_ref(), "atte_verify", bool_yn(self.flags.verify)),
ro(t!("Preset").as_ref(), "atte_preset", bool_yn(self.flags.preset)),
ro(
t!("Constant").as_ref(),
"atte_constant",
bool_yn(self.flags.constant),
),
ro(
t!("Verify").as_ref(),
"atte_verify",
bool_yn(self.flags.verify),
),
ro(
t!("Preset").as_ref(),
"atte_preset",
bool_yn(self.flags.preset),
),
ro(
t!("Lock position").as_ref(),
"atte_lock_pos",
+22 -19
View File
@@ -1,10 +1,8 @@
use codec::entities::Circle;
use crate::t;
use codec::entities::Circle;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, parse_f64, square_grip,
};
use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, square_grip};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
@@ -192,8 +190,7 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) {
"center_z" => z = v,
_ => {}
}
let (ox, oy, oz) =
crate::scene::view::transform::wcs_point_to_ocs((x, y, z), normal);
let (ox, oy, oz) = crate::scene::view::transform::wcs_point_to_ocs((x, y, z), normal);
circle.center.x = ox;
circle.center.y = oy;
circle.center.z = oz;
@@ -204,11 +201,8 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) {
"area" if v > 0.0 => circle.radius = (v / PI).sqrt(),
"normal_x" | "normal_y" | "normal_z" => {
let center = circle.center_wcs();
let mut normal = kernel::space::Vec3::new(
circle.normal.x,
circle.normal.y,
circle.normal.z,
);
let mut normal =
kernel::space::Vec3::new(circle.normal.x, circle.normal.y, circle.normal.z);
match field {
"normal_x" => normal.x = v,
"normal_y" => normal.y = v,
@@ -287,10 +281,7 @@ impl crate::entities::traits::Grippable for Circle {
apply_grip(self, grip_id, apply);
}
fn grip_menu(
&self,
grip_id: usize,
) -> Vec<crate::scene::model::object::GripMenuItem> {
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
let mut items = vec![GripMenuItem {
label: "Stretch",
@@ -436,9 +427,18 @@ mod tests {
let seg_0_0001 = circle_segments(r, Some(0.0001));
assert!(seg_0_1 >= 48, "seg_0_1 was {seg_0_1}");
assert!(seg_0_01 > seg_0_1, "seg_0_01 ({seg_0_01}) <= seg_0_1 ({seg_0_1})");
assert!(seg_0_001 > seg_0_01, "seg_0_001 ({seg_0_001}) <= seg_0_01 ({seg_0_01})");
assert!(seg_0_0001 > seg_0_001, "seg_0_0001 ({seg_0_0001}) <= seg_0_001 ({seg_0_001})");
assert!(
seg_0_01 > seg_0_1,
"seg_0_01 ({seg_0_01}) <= seg_0_1 ({seg_0_1})"
);
assert!(
seg_0_001 > seg_0_01,
"seg_0_001 ({seg_0_001}) <= seg_0_01 ({seg_0_01})"
);
assert!(
seg_0_0001 > seg_0_001,
"seg_0_0001 ({seg_0_0001}) <= seg_0_001 ({seg_0_001})"
);
// Extreme zoom in hits maximum cap of 4096.
assert_eq!(circle_segments(r, Some(1e-9)), 4096);
@@ -476,7 +476,10 @@ mod tests {
};
assert_eq!(far_count, 49); // 48 segments + 1 closing vertex
assert!(close_count > far_count, "close_count was {close_count} vs far_count {far_count}");
assert!(
close_count > far_count,
"close_count was {close_count} vs far_count {far_count}"
);
}
#[test]
+17 -24
View File
@@ -89,7 +89,8 @@ pub fn set_fixed_text_heights(document: &codec::CadDocument) {
let mut map = cell.borrow_mut();
map.clear();
for style in document.text_styles.iter() {
let font = crate::entities::text_support::resolve_text_style(&style.name, document).font_name;
let font =
crate::entities::text_support::resolve_text_style(&style.name, document).font_name;
map.insert(style.name.to_ascii_lowercase(), font);
}
});
@@ -653,8 +654,8 @@ pub fn lineweight_options() -> Vec<String> {
"Default".to_string(),
];
for value in [
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120,
140, 158, 200, 211,
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158,
200, 211,
] {
options.push(format!("{:.2} mm", value as f64 / 100.0));
}
@@ -694,11 +695,7 @@ pub fn num_prop(label: &str, field: &'static str, value: f64, editable: bool) ->
/// A ◀ / ▶ index navigator row (e.g. a polyline's Current Vertex). `display` is
/// the label shown between the arrows (e.g. "2 / 7").
pub fn stepper_prop(
label: &str,
field: &'static str,
display: impl Into<String>,
) -> Property {
pub fn stepper_prop(label: &str, field: &'static str, display: impl Into<String>) -> Property {
Property {
label: label.into(),
field,
@@ -751,9 +748,7 @@ fn parse_angle_deg_unbounded(value: &str) -> Option<f64> {
let i = rest.find(['°', 'd', 'D'])?;
let d: f64 = rest[..i].trim().parse().ok()?;
let mut total = d;
let tail = rest[i..]
.trim_start_matches(['°', 'd', 'D'])
.trim();
let tail = rest[i..].trim_start_matches(['°', 'd', 'D']).trim();
if !tail.is_empty() {
let (mpart, spart) = match tail.find('\'') {
Some(j) => (&tail[..j], &tail[j + 1..]),
@@ -1103,10 +1098,7 @@ pub(crate) fn extrude_wide_band_outline(
point_segments.extend_from_slice(&base_segments);
let mut emitted = HashSet::new();
for ((&point, &station), &segment) in base_points
.iter()
.zip(&base_stations)
.zip(&base_segments)
for ((&point, &station), &segment) in base_points.iter().zip(&base_stations).zip(&base_segments)
{
if !point[0].is_finite() {
continue;
@@ -1286,8 +1278,9 @@ mod length_format_tests {
for lunits in [3, 4, 5] {
for value in [0.995f64, 5.995, 11.999, 23.999, 66.5, 9.25] {
let shown = with_units(lunits, if lunits == 3 { 4 } else { 4 }, value);
let read = parse_length(&shown)
.unwrap_or_else(|| panic!("lunits {lunits} wrote {shown:?}, which does not read back"));
let read = parse_length(&shown).unwrap_or_else(|| {
panic!("lunits {lunits} wrote {shown:?}, which does not read back")
});
assert!(
(read - value).abs() < 0.02,
"lunits {lunits}: {value} wrote {shown:?}, read back as {read}"
@@ -1322,11 +1315,7 @@ mod length_format_tests {
"format_angle produced {} chars",
angle.len()
);
assert!(
area.len() < 64,
"format_area produced {} chars",
area.len()
);
assert!(area.len() < 64, "format_area produced {} chars", area.len());
}
}
@@ -1360,7 +1349,9 @@ mod angle_format_tests {
assert_eq!(shown(4, 0, 180.0), "W");
assert_ne!(shown(4, 8, 0.0000000005), "E");
for prec in [0, 2] {
for deg in [0.99999f64, 45.9999, 89.99999, 179.99999, 269.99999, 359.99999] {
for deg in [
0.99999f64, 45.9999, 89.99999, 179.99999, 269.99999, 359.99999,
] {
let text = shown(4, prec, deg);
assert!(
!text.contains("90d"),
@@ -1377,7 +1368,9 @@ mod angle_format_tests {
for deg in [0.99999f64, 45.5, 45.9999, 89.99999, 200.25, 359.99999] {
let text = shown(aunits, prec, deg);
let read = parse_angle(&text).unwrap_or_else(|| {
panic!("aunits {aunits} prec {prec} wrote {text:?}, which does not read back")
panic!(
"aunits {aunits} prec {prec} wrote {text:?}, which does not read back"
)
});
let delta = (read.to_degrees().rem_euclid(360.0) - deg.rem_euclid(360.0))
.rem_euclid(360.0);
+38 -18
View File
@@ -89,9 +89,11 @@ const PLANARITY_TOLERANCE: f64 = 1e-9;
pub fn entity_curve(entity: &EntityType) -> Option<PlanarCurve> {
match entity {
EntityType::Line(line) => straight_curve(line.start, line.end, Straight::Segment),
EntityType::Ray(ray) => {
straight_curve(ray.base_point, ray.base_point + ray.direction, Straight::Ray)
}
EntityType::Ray(ray) => straight_curve(
ray.base_point,
ray.base_point + ray.direction,
Straight::Ray,
),
EntityType::XLine(line) => straight_curve(
line.base_point,
line.base_point + line.direction,
@@ -121,13 +123,18 @@ pub fn entity_spatial_measurement(entity: &EntityType) -> Option<kernel::space::
SmoothSurfaceType::None => return None,
};
let controls: Vec<_> = crate::entities::polyline::polyline3d_controls(polyline)
.into_iter().map(|vertex| xyz(vertex.position)).collect();
let curve = NurbsCurve3::from_control_polygon(degree, &controls, polyline.is_closed())?;
.into_iter()
.map(|vertex| xyz(vertex.position))
.collect();
let curve =
NurbsCurve3::from_control_polygon(degree, &controls, polyline.is_closed())?;
return ArcLengthCurve3::from_nurbs(curve);
}
let points: Vec<_> = polyline.vertices.iter().map(|vertex| {
[vertex.position.x, vertex.position.y, vertex.position.z]
}).collect();
let points: Vec<_> = polyline
.vertices
.iter()
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
.collect();
return ArcLengthCurve3::from_polyline(&points, polyline.is_closed());
}
EntityType::Spline(spline) => {
@@ -135,10 +142,18 @@ pub fn entity_spatial_measurement(entity: &EntityType) -> Option<kernel::space::
crate::entities::spline::fit_nurbs3(spline)?
} else {
let controls: Vec<_> = spline.control_points.iter().copied().map(xyz).collect();
let weights = if spline.weights.is_empty() { vec![1.0; controls.len()] }
else { spline.weights.clone() };
NurbsCurve3::new_strict(spline.degree as usize, controls, spline.knots.clone(), weights)?
.with_periodicity(spline.flags.periodic)
let weights = if spline.weights.is_empty() {
vec![1.0; controls.len()]
} else {
spline.weights.clone()
};
NurbsCurve3::new_strict(
spline.degree as usize,
controls,
spline.knots.clone(),
weights,
)?
.with_periodicity(spline.flags.periodic)
}
}
_ => return None,
@@ -351,8 +366,7 @@ pub fn spline_curve(spline: &SplineEnt) -> Option<PlanarCurve> {
for tangent in [spline.begin_tangent, spline.end_tangent] {
let tangent = Vec3::from(xyz(tangent));
if tangent.length_squared() > 1e-18
&& tangent.dot(plane_normal).abs()
> PLANARITY_TOLERANCE * tangent.length().max(1.0)
&& tangent.dot(plane_normal).abs() > PLANARITY_TOLERANCE * tangent.length().max(1.0)
{
return None;
}
@@ -431,8 +445,7 @@ pub fn lwpolyline_world_xy(polyline: &LwPolylineEnt) -> Option<LwPolylineEnt> {
if normal.x.abs() > 1e-12 || normal.y.abs() > 1e-12 || normal.z.abs() <= 1e-12 {
return None;
}
let Curve::Polyline(curve) = entity_curve_xy(&EntityType::LwPolyline(polyline.clone()))?
else {
let Curve::Polyline(curve) = entity_curve_xy(&EntityType::LwPolyline(polyline.clone()))? else {
return None;
};
if curve.vertices.len() != polyline.vertices.len() {
@@ -827,7 +840,11 @@ mod tests {
polyline.normal = v3(0.0, 0.0, 1.0);
let mut bend = LwVertex::from_coords(0.0, 0.0);
bend.bulge = 0.9999999999999999;
polyline.vertices = vec![LwVertex::from_coords(-20.0, 0.0), bend, LwVertex::from_coords(20.0, 0.0)];
polyline.vertices = vec![
LwVertex::from_coords(-20.0, 0.0),
bend,
LwVertex::from_coords(20.0, 0.0),
];
let snaps = arc_segment_snaps(&EntityType::LwPolyline(polyline));
let at = |hint: SnapHint, x: f64, y: f64| {
snaps.iter().any(|(p, h)| {
@@ -890,7 +907,10 @@ mod tests {
assert_eq!(solid.euler_characteristic(), 2);
let document = codec::entities::acis::types::SatDocument::new();
let (bodies, loss) = kernel::acis::lift(&document);
assert!(bodies.is_empty() && loss.is_empty(), "an empty document lifts to nothing");
assert!(
bodies.is_empty() && loss.is_empty(),
"an empty document lifts to nothing"
);
}
#[test]
+24 -35
View File
@@ -374,7 +374,9 @@ fn inherited_int(doc: &CadDocument, handle: Handle, code: i16) -> i16 {
return value;
}
let Some(style) = doc.dim_styles.iter().find(|style| {
style.name.eq_ignore_ascii_case(&dimension.base().style_name)
style
.name
.eq_ignore_ascii_case(&dimension.base().style_name)
|| (dimension.base().style_name.trim().is_empty()
&& style.name.eq_ignore_ascii_case("Standard"))
}) else {
@@ -410,7 +412,9 @@ fn inherited_real(doc: &CadDocument, handle: Handle, code: i16) -> f64 {
return value;
}
let Some(style) = doc.dim_styles.iter().find(|style| {
style.name.eq_ignore_ascii_case(&dimension.base().style_name)
style
.name
.eq_ignore_ascii_case(&dimension.base().style_name)
|| (dimension.base().style_name.trim().is_empty()
&& style.name.eq_ignore_ascii_case("Standard"))
}) else {
@@ -433,7 +437,9 @@ fn inherited_string(doc: &CadDocument, handle: Handle, code: i16) -> String {
return value;
}
let Some(style) = doc.dim_styles.iter().find(|style| {
style.name.eq_ignore_ascii_case(&dimension.base().style_name)
style
.name
.eq_ignore_ascii_case(&dimension.base().style_name)
|| (dimension.base().style_name.trim().is_empty()
&& style.name.eq_ignore_ascii_case("Standard"))
}) else {
@@ -452,12 +458,7 @@ fn split_template(value: &str) -> (&str, &str) {
/// Apply a dimension override row. A changed override lays automatic text
/// out afresh, so e.g. a new DIMTAD moves it off the stored position.
pub fn set_property(
doc: &mut CadDocument,
handle: Handle,
field: &str,
value: &str,
) -> bool {
pub fn set_property(doc: &mut CadDocument, handle: Handle, field: &str, value: &str) -> bool {
let applied = apply_property(doc, handle, field, value);
if applied {
if let Some(EntityType::Dimension(dimension)) = doc.get_entity_mut(handle) {
@@ -467,12 +468,7 @@ pub fn set_property(
applied
}
fn apply_property(
doc: &mut CadDocument,
handle: Handle,
field: &str,
value: &str,
) -> bool {
fn apply_property(doc: &mut CadDocument, handle: Handle, field: &str, value: &str) -> bool {
let trimmed = value.trim();
if field == "dim_center_type" {
let current = inherited_real(doc, handle, DIMCEN);
@@ -548,7 +544,12 @@ fn apply_property(
let Some(visible) = yes(trimmed) else {
return false;
};
set(doc, handle, code, Some(XDataValue::Integer16((visible == 0) as i16)));
set(
doc,
handle,
code,
Some(XDataValue::Integer16((visible == 0) as i16)),
);
return true;
}
let bit_field = match field {
@@ -660,12 +661,7 @@ fn apply_property(
"deviation" => {
if (upper - lower).abs() <= 1e-12 {
let distinct_lower = if upper.abs() > 1e-12 { 0.0 } else { 0.0001 };
set(
doc,
handle,
DIMTM,
Some(XDataValue::Real(distinct_lower)),
);
set(doc, handle, DIMTM, Some(XDataValue::Real(distinct_lower)));
}
(1, 0, false)
}
@@ -688,8 +684,7 @@ fn apply_property(
if trimmed.is_empty() {
set(doc, handle, code, None);
} else if let Ok(number) = trimmed.parse::<f64>() {
let number = if field == "dim_text_offset"
&& inherited_real(doc, handle, DIMGAP) < 0.0
let number = if field == "dim_text_offset" && inherited_real(doc, handle, DIMGAP) < 0.0
{
-number.abs()
} else {
@@ -712,9 +707,7 @@ fn apply_property(
}
if let Some(code) = property_int_code(field) {
let parsed = match field {
"dim_line_lineweight" | "dim_ext_line_lineweight" => {
parse_lineweight_label(trimmed)
}
"dim_line_lineweight" | "dim_ext_line_lineweight" => parse_lineweight_label(trimmed),
"dim_precision"
| "dim_alt_precision"
| "dim_tolerance_precision"
@@ -724,14 +717,10 @@ fn apply_property(
"dim_text_outside_align" | "dim_text_inside_align" | "dim_line_inside" => {
yes(trimmed).map(|value| i16::from(value == 0))
}
"dim_ext_line_fixed"
| "dim_text_inside"
| "dim_line_forced"
| "dim_alt_enabled" => yes(trimmed),
"dim_decimal_separator" => trimmed
.chars()
.next()
.map(|character| character as i16),
"dim_ext_line_fixed" | "dim_text_inside" | "dim_line_forced" | "dim_alt_enabled" => {
yes(trimmed)
}
"dim_decimal_separator" => trimmed.chars().next().map(|character| character as i16),
"dim_units" => match trimmed.to_ascii_lowercase().as_str() {
"scientific" => Some(1),
"decimal" => Some(2),
+628 -431
View File
File diff suppressed because it is too large. Load diff
+53 -11
View File
@@ -192,16 +192,52 @@ fn properties(ell: &Ellipse) -> Vec<PropSection> {
edit(t!("Radius ratio").as_ref(), "ratio", ell.minor_axis_ratio),
edit(t!("Start angle").as_ref(), "start_angle", start_angle),
edit(t!("End angle").as_ref(), "end_angle", end_angle),
ro(t!("Major axis vector X").as_ref(), "major_x", format_length(major_vec.x)),
ro(t!("Major axis vector Y").as_ref(), "major_y", format_length(major_vec.y)),
ro(t!("Major axis vector Z").as_ref(), "major_z", format_length(major_vec.z)),
ro(t!("Minor axis vector X").as_ref(), "minor_x", format_length(minor_vec.x)),
ro(t!("Minor axis vector Y").as_ref(), "minor_y", format_length(minor_vec.y)),
ro(t!("Minor axis vector Z").as_ref(), "minor_z", format_length(minor_vec.z)),
ro(
t!("Major axis vector X").as_ref(),
"major_x",
format_length(major_vec.x),
),
ro(
t!("Major axis vector Y").as_ref(),
"major_y",
format_length(major_vec.y),
),
ro(
t!("Major axis vector Z").as_ref(),
"major_z",
format_length(major_vec.z),
),
ro(
t!("Minor axis vector X").as_ref(),
"minor_x",
format_length(minor_vec.x),
),
ro(
t!("Minor axis vector Y").as_ref(),
"minor_y",
format_length(minor_vec.y),
),
ro(
t!("Minor axis vector Z").as_ref(),
"minor_z",
format_length(minor_vec.z),
),
ro(t!("Area").as_ref(), "area", format_area(props.area)),
ro(t!("Start parameter").as_ref(), "start_param", format!("{:.4}", ell.start_parameter)),
ro(t!("End parameter").as_ref(), "end_param", format!("{:.4}", ell.end_parameter)),
ro(t!("Length").as_ref(), "length", format_length(props.perimeter)),
ro(
t!("Start parameter").as_ref(),
"start_param",
format!("{:.4}", ell.start_parameter),
),
ro(
t!("End parameter").as_ref(),
"end_param",
format!("{:.4}", ell.end_parameter),
),
ro(
t!("Length").as_ref(),
"length",
format_length(props.perimeter),
),
edit(t!("Normal X").as_ref(), "normal_x", ell.normal.x),
edit(t!("Normal Y").as_ref(), "normal_y", ell.normal.y),
edit(t!("Normal Z").as_ref(), "normal_z", ell.normal.z),
@@ -479,8 +515,14 @@ mod grip_tests {
// major=10 (+X), minor=5 (+Y). Drag the minor grip to (0,11), past major.
let mut e = ell(10.0, 0.5);
apply_grip(&mut e, 2, GripApply::Absolute(DVec3::new(0.0, 11.0, 0.0)));
assert!((xy_len(&e) - 11.0).abs() < 1e-9, "major follows the drag to 11");
assert!(e.major_axis.x.abs() < 1e-9, "major points +Y after the swap");
assert!(
(xy_len(&e) - 11.0).abs() < 1e-9,
"major follows the drag to 11"
);
assert!(
e.major_axis.x.abs() < 1e-9,
"major points +Y after the swap"
);
assert!(
(xy_len(&e) * e.minor_axis_ratio - 10.0).abs() < 1e-9,
"minor holds the old major length (10)"
+500 -307
View File
File diff suppressed because it is too large. Load diff
+117 -25
View File
@@ -29,7 +29,10 @@ impl FieldContext for OcsFieldContext<'_> {
let secs = t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs_f64();
Some(secs / 86_400.0 + 2_440_587.5 + utc_offset_days())
};
return Some((julian(meta.created().ok()?)?, julian(meta.modified().ok()?)?));
return Some((
julian(meta.created().ok()?)?,
julian(meta.modified().ok()?)?,
));
}
#[cfg(target_arch = "wasm32")]
None
@@ -94,7 +97,8 @@ impl FieldContext for OcsFieldContext<'_> {
/// The sheet sets open in the Sheet Set Manager, for `\AcSm` fields. The
/// manager replaces the list whenever a set opens, closes or changes.
static SHEET_SETS: std::sync::RwLock<Vec<codec::sheet_set::SheetSetDatabase>> = std::sync::RwLock::new(Vec::new());
static SHEET_SETS: std::sync::RwLock<Vec<codec::sheet_set::SheetSetDatabase>> =
std::sync::RwLock::new(Vec::new());
pub fn set_sheet_sets(sets: Vec<codec::sheet_set::SheetSetDatabase>) {
if let Ok(mut guard) = SHEET_SETS.write() {
@@ -126,7 +130,11 @@ pub fn update_fields(
event: i32,
hosts: Option<&[Handle]>,
) -> (Vec<Handle>, usize) {
let event = if event == 32 { event } else { event & fieldeval(document) };
let event = if event == 32 {
event
} else {
event & fieldeval(document)
};
if event == 0 || document.fields.is_empty() {
return (Vec::new(), 0);
}
@@ -168,17 +176,55 @@ pub fn evaluate(
objects: &[Handle],
host: Option<Handle>,
) -> Option<String> {
codec::fields::evaluate_code(document, code, objects, host, &OcsFieldContext(Some(document)))
codec::fields::evaluate_code(
document,
code,
objects,
host,
&OcsFieldContext(Some(document)),
)
}
/// The system variables the Field dialog lists, in its order (lower case).
pub const SYSVARS: &[&str] = &[
"aunits", "auprec", "celtscale", "chamfera", "chamferb", "chamferc", "chamferd",
"clayer", "dimscale", "dimtxt", "dwgname", "dwgprefix", "elevation", "filletrad",
"insbase", "insunits", "limmax", "limmin", "ltscale", "lunits", "luprec",
"measurement", "mirrtext", "pdmode", "pdsize", "textsize", "textstyle", "thickness",
"useri1", "useri2", "useri3", "useri4", "useri5", "userr1", "userr2", "userr3",
"userr4", "userr5",
"aunits",
"auprec",
"celtscale",
"chamfera",
"chamferb",
"chamferc",
"chamferd",
"clayer",
"dimscale",
"dimtxt",
"dwgname",
"dwgprefix",
"elevation",
"filletrad",
"insbase",
"insunits",
"limmax",
"limmin",
"ltscale",
"lunits",
"luprec",
"measurement",
"mirrtext",
"pdmode",
"pdsize",
"textsize",
"textstyle",
"thickness",
"useri1",
"useri2",
"useri3",
"useri4",
"useri5",
"userr1",
"userr2",
"userr3",
"userr4",
"userr5",
];
/// Real system variables show six decimals, as the reference shows them
@@ -206,7 +252,13 @@ pub fn sysvar(document: &CadDocument, name: &str) -> Option<String> {
"clayer" => h.current_layer_name.clone(),
"dimscale" => num(h.dim_scale),
"dimtxt" => num(h.dim_text_height),
"dwgname" => if file.is_empty() { "Drawing1.dwg".to_string() } else { file.to_string() },
"dwgname" => {
if file.is_empty() {
"Drawing1.dwg".to_string()
} else {
file.to_string()
}
}
"dwgprefix" => folder.to_string(),
"elevation" => num(h.elevation),
"filletrad" => num(h.fillet_radius),
@@ -215,8 +267,16 @@ pub fn sysvar(document: &CadDocument, name: &str) -> Option<String> {
format!("{},{},{}", num(p.x), num(p.y), num(p.z))
}
"insunits" => h.insertion_units.to_string(),
"limmax" => format!("{},{}", num(h.model_space_limits_max.x), num(h.model_space_limits_max.y)),
"limmin" => format!("{},{}", num(h.model_space_limits_min.x), num(h.model_space_limits_min.y)),
"limmax" => format!(
"{},{}",
num(h.model_space_limits_max.x),
num(h.model_space_limits_max.y)
),
"limmin" => format!(
"{},{}",
num(h.model_space_limits_min.x),
num(h.model_space_limits_min.y)
),
"ltscale" => num(h.linetype_scale),
"lunits" => h.linear_unit_format.to_string(),
"luprec" => h.linear_unit_precision.to_string(),
@@ -287,7 +347,14 @@ fn os_date_locale() -> codec::fields::DateLocale {
let info = |kind: u32| -> Option<String> {
let mut buffer = [0u16; 128];
// A null locale name is the user's default locale.
let len = unsafe { GetLocaleInfoEx(std::ptr::null(), kind, buffer.as_mut_ptr(), buffer.len() as i32) };
let len = unsafe {
GetLocaleInfoEx(
std::ptr::null(),
kind,
buffer.as_mut_ptr(),
buffer.len() as i32,
)
};
(len > 1).then(|| String::from_utf16_lossy(&buffer[..len as usize - 1]))
};
let mut locale = codec::fields::DateLocale::default();
@@ -380,8 +447,14 @@ pub fn utc_offset_days() -> f64 {
use windows_sys::Win32::Storage::FileSystem::FileTimeToLocalFileTime;
// 100 ns ticks since 1601-01-01.
let ticks = (epoch_secs() as u64 + 11_644_473_600) * 10_000_000;
let utc = FILETIME { dwLowDateTime: ticks as u32, dwHighDateTime: (ticks >> 32) as u32 };
let mut local = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 };
let utc = FILETIME {
dwLowDateTime: ticks as u32,
dwHighDateTime: (ticks >> 32) as u32,
};
let mut local = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
if unsafe { FileTimeToLocalFileTime(&utc, &mut local) } != 0 {
let local = ((local.dwHighDateTime as u64) << 32) | local.dwLowDateTime as u64;
return (local as i64 - ticks as i64) as f64 / 864_000_000_000.0;
@@ -397,10 +470,14 @@ pub fn utc_offset_days() -> f64 {
pub fn local_header_dates(document: &mut CadDocument) {
let h = &mut document.header;
let offset = utc_offset_days();
if h.universal_create_date_julian != 0.0 && h.create_date_julian == h.universal_create_date_julian {
if h.universal_create_date_julian != 0.0
&& h.create_date_julian == h.universal_create_date_julian
{
h.create_date_julian = h.universal_create_date_julian + offset;
}
if h.universal_update_date_julian != 0.0 && h.update_date_julian == h.universal_update_date_julian {
if h.universal_update_date_julian != 0.0
&& h.update_date_julian == h.universal_update_date_julian
{
h.update_date_julian = h.universal_update_date_julian + offset;
}
}
@@ -427,7 +504,8 @@ fn display_name() -> Option<String> {
let mut buffer = [0u16; 256];
let mut len = buffer.len() as u32;
if unsafe { GetUserNameExW(NameDisplay, buffer.as_mut_ptr(), &mut len) } && len > 0 {
return Some(String::from_utf16_lossy(&buffer[..len as usize])).filter(|n| !n.trim().is_empty());
return Some(String::from_utf16_lossy(&buffer[..len as usize]))
.filter(|n| !n.trim().is_empty());
}
None
}
@@ -454,7 +532,8 @@ pub fn refresh_sheet_set_fields(document: &mut CadDocument) -> Vec<Handle> {
.filter(|f| f.evaluator.starts_with("AcSm"))
.filter(|f| {
let host = field_host(document, f.handle).unwrap_or(Handle::NULL);
codec::fields::resolve_handle(document, f.handle, host, &context).is_none_or(|v| v == "####")
codec::fields::resolve_handle(document, f.handle, host, &context)
.is_none_or(|v| v == "####")
})
.map(|f| (f.handle, f.evaluator.clone()))
.collect();
@@ -509,19 +588,32 @@ pub fn long_date_time_now() -> String {
}
/// [`attach_attribute_fields`] with every field code passed through `map`.
pub fn attach_attribute_fields_mapped(document: &mut CadDocument, insert: Handle, map: &dyn Fn(&str) -> String) -> Vec<Handle> {
pub fn attach_attribute_fields_mapped(
document: &mut CadDocument,
insert: Handle,
map: &dyn Fn(&str) -> String,
) -> Vec<Handle> {
let context = OcsFieldContext(None);
document.attach_attribute_fields_mapped(insert, &context, map)
}
/// Whether any attribute definition of `block` hosts a field.
pub fn block_has_attribute_fields(document: &CadDocument, block: &str) -> bool {
let Some(record) = document.block_records.iter().find(|r| r.name.eq_ignore_ascii_case(block)) else {
let Some(record) = document
.block_records
.iter()
.find(|r| r.name.eq_ignore_ascii_case(block))
else {
return false;
};
record.entity_handles.iter().filter_map(|h| document.get_entity(*h)).any(|e| {
matches!(e, codec::entities::EntityType::AttributeDefinition(_)) && hosts_field(document, e)
})
record
.entity_handles
.iter()
.filter_map(|h| document.get_entity(*h))
.any(|e| {
matches!(e, codec::entities::EntityType::AttributeDefinition(_))
&& hosts_field(document, e)
})
}
/// Give plot-time fields (PlotDate) the plot's time; returns the hosts whose
+66 -61
View File
@@ -1,17 +1,20 @@
use crate::t;
use codec::entities::{BoundaryEdge, Hatch};
use glam::Vec3;
use kernel::geom2d::{
Arc as KernelArc, Curve as KernelCurve, Ellipse as KernelEllipse,
EllipseArc as KernelEllipseArc, Line as KernelLine, NurbsCurve as KernelNurbs,
Parameterization, Polyline as KernelPolyline, PolylineVertex as KernelVertex,
};
use glam::Vec3;
use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{center_grip, circle_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro};
use crate::entities::common::{
center_grip, circle_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64,
ro_prop as ro,
};
use crate::entities::traits::{FallbackTess, Grippable, PropertyEditable, Transformable};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::convert::tess_util::FallbackGeometry;
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
/// The area enclosed by the hatch boundary paths.
@@ -122,18 +125,14 @@ pub(crate) fn edge_curve(edge: &BoundaryEdge) -> Option<KernelCurve> {
let weights = s
.rational
.then(|| s.control_points.iter().map(|p| p.z).collect::<Vec<f64>>());
let curve = KernelNurbs::new(
s.degree.max(1) as usize,
control,
s.knots.clone(),
weights,
)
.or_else(|| {
// A fit-point boundary spline, interpolated the same way a
// SPLINE entity's is.
let fit: Vec<[f64; 2]> = s.fit_points.iter().map(|p| [p.x, p.y]).collect();
KernelNurbs::interpolate(&fit, None, None, Parameterization::Chord)
})?;
let curve =
KernelNurbs::new(s.degree.max(1) as usize, control, s.knots.clone(), weights)
.or_else(|| {
// A fit-point boundary spline, interpolated the same way a
// SPLINE entity's is.
let fit: Vec<[f64; 2]> = s.fit_points.iter().map(|p| [p.x, p.y]).collect();
KernelNurbs::interpolate(&fit, None, None, Parameterization::Chord)
})?;
KernelCurve::Nurbs(curve)
}
})
@@ -185,10 +184,7 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> {
/// Scale a catalog pattern about its intrinsic coordinate origin.
/// Pattern lines loaded from DXF/DWG are rendered prebaked, so their metadata
/// scale is not applied again by the renderer.
pub(crate) fn scale_pattern_geometry(
pattern: &mut codec::entities::HatchPattern,
factor: f64,
) {
pub(crate) fn scale_pattern_geometry(pattern: &mut codec::entities::HatchPattern, factor: f64) {
let (origin_x, origin_y) = (0.0, 0.0);
for line in pattern.lines.iter_mut() {
line.base_point.x = origin_x + (line.base_point.x - origin_x) * factor;
@@ -203,18 +199,12 @@ pub(crate) fn scale_pattern_geometry(
/// Rotate a catalog pattern about its intrinsic coordinate origin.
/// This keeps the line angle, base point and world-space offset in sync.
pub(crate) fn rotate_pattern_geometry(
pattern: &mut codec::entities::HatchPattern,
angle: f64,
) {
pub(crate) fn rotate_pattern_geometry(pattern: &mut codec::entities::HatchPattern, angle: f64) {
let (sin, cos) = angle.sin_cos();
let (origin_x, origin_y) = (0.0, 0.0);
for line in pattern.lines.iter_mut() {
line.angle += angle;
let (x, y) = (
line.base_point.x - origin_x,
line.base_point.y - origin_y,
);
let (x, y) = (line.base_point.x - origin_x, line.base_point.y - origin_y);
line.base_point.x = origin_x + x * cos - y * sin;
line.base_point.y = origin_y + x * sin + y * cos;
let (x, y) = (line.offset.x, line.offset.y);
@@ -315,11 +305,7 @@ pub fn clear_background_color(h: &mut Hatch) {
}
fn associative_property(h: &Hatch) -> Property {
if h
.paths
.iter()
.any(|path| !path.boundary_handles.is_empty())
{
if h.paths.iter().any(|path| !path.boundary_handles.is_empty()) {
Property {
label: t!("Associative").into_owned(),
field: "associative",
@@ -362,11 +348,18 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
if g.enabled {
// ── Gradient fill ──────────────────────────────────────────────────
let grad_type = if g.is_single_color { "One color" } else { "Two color" };
let (kind, inverted) =
crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
let grad_type = if g.is_single_color {
"One color"
} else {
"Two color"
};
let (kind, inverted) = crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
let mut pattern_props = vec![
ro(t!("Type").as_ref(), "fill_kind", t!("Gradient").into_owned()),
ro(
t!("Type").as_ref(),
"fill_kind",
t!("Gradient").into_owned(),
),
Property {
label: t!("Color mode").into_owned(),
field: "fill_type",
@@ -428,8 +421,16 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
title: t!("Geometry").into_owned(),
props: vec![
edit(t!("Elevation").as_ref(), "elevation", h.elevation),
ro(t!("Area").as_ref(), "area", crate::entities::common::format_area(area)),
ro(t!("Cumulative area").as_ref(), "cumulative_area", crate::entities::common::format_area(area)),
ro(
t!("Area").as_ref(),
"area",
crate::entities::common::format_area(area),
),
ro(
t!("Cumulative area").as_ref(),
"cumulative_area",
crate::entities::common::format_area(area),
),
],
},
PropSection {
@@ -450,7 +451,6 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
];
}
// ── Hatch (pattern / solid) ────────────────────────────────────────────
// Show only controls used by the selected fill type.
let type_row = if h.is_solid {
@@ -537,8 +537,16 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
title: t!("Geometry").into_owned(),
props: vec![
edit(t!("Elevation").as_ref(), "elevation", h.elevation),
ro(t!("Area").as_ref(), "area", crate::entities::common::format_area(area)),
ro(t!("Cumulative area").as_ref(), "cumulative_area", crate::entities::common::format_area(area)),
ro(
t!("Area").as_ref(),
"area",
crate::entities::common::format_area(area),
),
ro(
t!("Cumulative area").as_ref(),
"cumulative_area",
crate::entities::common::format_area(area),
),
],
},
];
@@ -588,11 +596,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
// update from boundary edits. Only enable it when a real
// relationship is available; disabling retains the handles so the
// user can turn it back on later.
if !requested
|| h.paths
.iter()
.any(|path| !path.boundary_handles.is_empty())
{
if !requested || h.paths.iter().any(|path| !path.boundary_handles.is_empty()) {
h.is_associative = requested;
}
return;
@@ -614,9 +618,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
h.pattern = codec::entities::HatchPattern::new("_USER");
}
HatchPatternType::Predefined => {
if let Some(entry) =
crate::scene::model::hatch_patterns::find("ANSI31")
{
if let Some(entry) = crate::scene::model::hatch_patterns::find("ANSI31") {
let mut pattern =
crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
scale_pattern_geometry(&mut pattern, h.pattern_scale);
@@ -769,8 +771,7 @@ impl Grippable for Hatch {
// pattern exists). Dragging it moves the pattern tiling origin; boundary
// grips follow from the next id.
if let Some(l0) = self.pattern.lines.first() {
let (gx, gy) =
boundary_centroid(self).unwrap_or((l0.base_point.x, l0.base_point.y));
let (gx, gy) = boundary_centroid(self).unwrap_or((l0.base_point.x, l0.base_point.y));
out.push(circle_grip(id, glam::DVec3::new(gx, gy, elev)));
id += 1;
} else if self.is_associative {
@@ -982,12 +983,19 @@ impl Grippable for Hatch {
]
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
if grip_id == 0 && matches!(action, A::OriginPoint) {
if let (Some((gx, gy)), Some((_ox, _oy))) = (
boundary_centroid(self),
self.pattern.lines.first().map(|line| (line.base_point.x, line.base_point.y)),
self.pattern
.lines
.first()
.map(|line| (line.base_point.x, line.base_point.y)),
) {
self.set_pattern_origin(codec::types::Vector2::new(gx, gy));
}
@@ -1054,8 +1062,7 @@ impl FallbackTess for Hatch {
let Some(curve) = edge_curve(edge) else {
continue;
};
let local = curve
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE);
let local = curve.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE);
if local.len() < 2 {
continue;
}
@@ -1067,11 +1074,9 @@ impl FallbackTess for Hatch {
.map(|point| to_wcs(point[0], point[1]))
.collect();
match edge {
BoundaryEdge::Polyline(poly) => key_verts.extend(
poly.vertices
.iter()
.map(|point| to_wcs(point.x, point.y)),
),
BoundaryEdge::Polyline(poly) => {
key_verts.extend(poly.vertices.iter().map(|point| to_wcs(point.x, point.y)))
}
_ => key_verts.extend([world[0], *world.last().unwrap()]),
}
match edge {
+82 -32
View File
@@ -1,7 +1,7 @@
use codec::entities::{Helix, HelixConstraint, Spline};
use codec::types::Vector3;
use kernel::space::{HelixCurve, HelixDirection, NurbsCurve3, Vec3};
use glam::DVec3;
use kernel::space::{HelixCurve, HelixDirection, NurbsCurve3, Vec3};
use crate::command::EntityTransform;
use crate::entities::common::{
@@ -57,8 +57,13 @@ fn axis_frame(helix: &Helix) -> Option<(Vec3, Vec3, Vec3)> {
.map(|curve| Vec3::from(curve.tangent_at(0.0)))
.and_then(Vec3::normalize)
.or_else(|| perpendicular(axis))?;
let (axis, radial, _) = HelixCurve::frame_from_points(base.to_array(),point(helix.axis_vector).to_array(),point(helix.start_point).to_array(),tangent.to_array())?;
Some((base,Vec3::from(axis),Vec3::from(radial)))
let (axis, radial, _) = HelixCurve::frame_from_points(
base.to_array(),
point(helix.axis_vector).to_array(),
point(helix.start_point).to_array(),
tangent.to_array(),
)?;
Some((base, Vec3::from(axis), Vec3::from(radial)))
}
fn base_radius(helix: &Helix) -> Option<f64> {
@@ -72,7 +77,9 @@ fn radius_from_axis(helix: &Helix, value: Vec3) -> Option<f64> {
}
fn top_radius(helix: &Helix) -> f64 {
if helix.turns == 0.0 { return helix.radius; }
if helix.turns == 0.0 {
return helix.radius;
}
spline_curve(&helix.spline)
.map(|curve| Vec3::from(curve.point_at(1.0)))
.and_then(|endpoint| radius_from_axis(helix, endpoint))
@@ -150,7 +157,9 @@ fn rebuild_with_radial(helix: &mut Helix, top_radius: f64, radial: Option<Vec3>)
let Some(mut curve) = kernel_curve(helix, top_radius) else {
return false;
};
if let Some(radial) = radial { curve.start_direction = radial.to_array(); }
if let Some(radial) = radial {
curve.start_direction = radial.to_array();
}
let Some(nurbs) = curve.nurbs() else {
return false;
};
@@ -209,7 +218,10 @@ fn properties(helix: &Helix) -> Vec<PropSection> {
let height = helix.turns * helix.turn_height;
let top_radius = top_radius(helix);
let curve = kernel_curve(helix, top_radius);
let turn_slope = curve.as_ref().and_then(HelixCurve::turn_slope).unwrap_or(0.0);
let turn_slope = curve
.as_ref()
.and_then(HelixCurve::turn_slope)
.unwrap_or(0.0);
let total_length = curve.as_ref().and_then(HelixCurve::length).unwrap_or(0.0);
let constrain = match helix.constraint {
HelixConstraint::TurnHeight => t!("Turn Height").into_owned(),
@@ -234,15 +246,32 @@ fn properties(helix: &Helix) -> Vec<PropSection> {
edit(t!("Position X").as_ref(), "position_x", base.x),
edit(t!("Position Y").as_ref(), "position_y", base.y),
edit(t!("Position Z").as_ref(), "position_z", base.z),
choice(t!("Constrain").as_ref(), "constrain", &constrain, &constraint_options),
choice(
t!("Constrain").as_ref(),
"constrain",
&constrain,
&constraint_options,
),
edit(t!("Height").as_ref(), "height", height),
edit(t!("Turns").as_ref(), "turns", helix.turns),
edit(t!("Turn height").as_ref(), "turn_height", helix.turn_height),
edit(t!("Base radius").as_ref(), "base_radius", base_radius(helix).unwrap_or(0.0)),
edit(
t!("Base radius").as_ref(),
"base_radius",
base_radius(helix).unwrap_or(0.0),
),
edit(t!("Top radius").as_ref(), "top_radius", top_radius),
choice(t!("Twist").as_ref(), "twist", &twist, &twist_options),
ro(t!("Turn slope").as_ref(), "turn_slope", format_angle(turn_slope)),
ro(t!("Total length").as_ref(), "total_length", format_length(total_length)),
ro(
t!("Turn slope").as_ref(),
"turn_slope",
format_angle(turn_slope),
),
ro(
t!("Total length").as_ref(),
"total_length",
format_length(total_length),
),
],
}]
}
@@ -297,9 +326,12 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
if helix.turns == 0.0 && number != 0.0 {
return;
}
if number < 0.0 { helix.axis_vector = vector(-point(helix.axis_vector)); }
if number < 0.0 {
helix.axis_vector = vector(-point(helix.axis_vector));
}
let number = number.abs();
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON
{
let turns = number / helix.turn_height;
if turns < 0.0 {
return;
@@ -310,8 +342,9 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
}
}
"turns" if number >= 0.0 => {
if number == 0.0 { helix.turns = 0.0; }
else if helix.constraint == HelixConstraint::TurnHeight {
if number == 0.0 {
helix.turns = 0.0;
} else if helix.constraint == HelixConstraint::TurnHeight {
helix.turns = number;
} else {
helix.turns = number;
@@ -319,8 +352,9 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
}
}
"turn_height" if number >= 0.0 => {
if number == 0.0 { helix.turn_height = 0.0; }
else if helix.constraint == HelixConstraint::Turns {
if number == 0.0 {
helix.turn_height = 0.0;
} else if helix.constraint == HelixConstraint::Turns {
helix.turn_height = number;
} else {
let turns = old_height / number;
@@ -336,7 +370,9 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
return;
};
helix.start_point = vector(base + start_direction * number);
if !rebuild_with_radial(helix, top, Some(start_direction)) { *helix = original; }
if !rebuild_with_radial(helix, top, Some(start_direction)) {
*helix = original;
}
return;
}
"top_radius" if number >= 0.0 => {
@@ -355,7 +391,9 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
let original = helix.clone();
let top = top_radius(helix);
let Some(base_radius) = base_radius(helix) else { return; };
let Some(base_radius) = base_radius(helix) else {
return;
};
let Some((base, axis, start_direction)) = axis_frame(helix) else {
return;
};
@@ -400,7 +438,8 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
return;
}
let target_axis = axis_vector / height;
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON
{
helix.axis_vector = vector(target_axis * helix.turn_height.signum());
helix.turns = height / helix.turn_height.abs();
} else {
@@ -408,8 +447,8 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
helix.turn_height = height / helix.turns.max(EPSILON);
}
let new_axis = point(helix.axis_vector);
let start_direction = projected_direction(start_direction, new_axis)
.or_else(|| perpendicular(new_axis));
let start_direction =
projected_direction(start_direction, new_axis).or_else(|| perpendicular(new_axis));
let Some(start_direction) = start_direction else {
*helix = original;
return;
@@ -427,7 +466,8 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
if height.abs() <= EPSILON {
return;
}
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON
{
let turns = height / helix.turn_height;
if turns <= EPSILON {
return;
@@ -453,18 +493,23 @@ fn apply_transform(helix: &mut Helix, transform: &EntityTransform) {
EntityTransform::Translate(delta) => {
codec::Entity::translate(helix, Vector3::new(delta.x, delta.y, delta.z));
}
EntityTransform::Rotate { center, axis, angle_rad } => {
EntityTransform::Rotate {
center,
axis,
angle_rad,
} => {
crate::scene::view::transform::apply_standard_transform(
helix,
*center,
*axis,
*angle_rad,
helix, *center, *axis, *angle_rad,
);
}
EntityTransform::Scale { center, factor } => {
crate::scene::view::transform::apply_standard_scale(helix, *center, *factor);
}
EntityTransform::Mirror { p1, p2, working_normal } => {
EntityTransform::Mirror {
p1,
p2,
working_normal,
} => {
let reflection = crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
@@ -590,15 +635,20 @@ mod tests {
assert_eq!(once.handedness, helix.handedness);
let original_curve = spline_curve(&helix.spline).unwrap();
let once_curve = spline_curve(&once.spline).unwrap();
assert!(Vec3::from(once_curve.point_at(0.0))
.distance(Vec3::from(original_curve.point_at(1.0))) < 1.0e-9);
assert!(
Vec3::from(once_curve.point_at(0.0)).distance(Vec3::from(original_curve.point_at(1.0)))
< 1.0e-9
);
let twice = reversed(&once).expect("second reversal");
assert!((twice.radius - helix.radius).abs() < 1.0e-12);
let twice_curve = spline_curve(&twice.spline).unwrap();
for parameter in [0.0, 0.2, 0.5, 0.8, 1.0] {
assert!(Vec3::from(twice_curve.point_at(parameter))
.distance(Vec3::from(original_curve.point_at(parameter))) < 1.0e-8);
assert!(
Vec3::from(twice_curve.point_at(parameter))
.distance(Vec3::from(original_curve.point_at(parameter)))
< 1.0e-8
);
}
}
}
+67 -18
View File
@@ -48,9 +48,15 @@ pub(crate) fn unscale_attribute_grip(
if (block_scale - 1.0).abs() <= 1.0e-6 {
return apply;
}
let origin = glam::DVec3::new(insert.insert_point.x, insert.insert_point.y, insert.insert_point.z);
let origin = glam::DVec3::new(
insert.insert_point.x,
insert.insert_point.y,
insert.insert_point.z,
);
match apply {
GripApply::Absolute(target) => GripApply::Absolute(origin + (target - origin) / block_scale),
GripApply::Absolute(target) => {
GripApply::Absolute(origin + (target - origin) / block_scale)
}
GripApply::Translate(delta) => GripApply::Translate(delta / block_scale),
}
}
@@ -261,7 +267,11 @@ fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) {
fn apply_grip(ins: &mut Insert, grip_id: usize, apply: GripApply) {
// An attribute grip moves that attribute alone; the block stays put.
if let Some(index) = grip_id.checked_sub(ATTRIBUTE_GRIP_BASE) {
if let Some(att) = ins.attributes.get_mut(index).filter(|a| attribute_is_movable(a)) {
if let Some(att) = ins
.attributes
.get_mut(index)
.filter(|a| attribute_is_movable(a))
{
Grippable::apply_grip(att, 0, apply);
}
return;
@@ -318,9 +328,7 @@ fn apply_transform(ins: &mut Insert, t: &EntityTransform) {
crate::impl_entity_basics!(Insert);
impl crate::entities::traits::FallbackTess for Insert {
fn fallback_geometry(
&self,
) -> crate::scene::convert::tess_util::FallbackGeometry {
fn fallback_geometry(&self) -> crate::scene::convert::tess_util::FallbackGeometry {
let (ipx, ipy, ipz) = (
self.insert_point.x,
self.insert_point.y,
@@ -487,7 +495,10 @@ mod tests {
/// handle to pick an attribute up by and nowhere to drop it.
#[test]
fn each_attribute_offers_its_own_grip() {
let ins = block_with(vec![attribute("TAG_A", 3.0, 4.0), attribute("TAG_B", 6.0, 8.0)]);
let ins = block_with(vec![
attribute("TAG_A", 3.0, 4.0),
attribute("TAG_B", 6.0, 8.0),
]);
let g = grips(&ins);
assert_eq!(g.len(), 3, "insertion point plus one grip per attribute");
@@ -500,15 +511,31 @@ mod tests {
/// block and its other attributes stay where they were.
#[test]
fn attribute_grip_moves_only_that_attribute() {
let mut ins = block_with(vec![attribute("TAG_A", 3.0, 4.0), attribute("TAG_B", 6.0, 8.0)]);
let mut ins = block_with(vec![
attribute("TAG_A", 3.0, 4.0),
attribute("TAG_B", 6.0, 8.0),
]);
let moved = grips(&ins)[1].id;
apply_grip(&mut ins, moved, GripApply::Absolute(glam::DVec3::new(30.0, 40.0, 0.0)));
apply_grip(
&mut ins,
moved,
GripApply::Absolute(glam::DVec3::new(30.0, 40.0, 0.0)),
);
assert_eq!(ins.insert_point, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(ins.attributes[0].insertion_point, Vector3::new(30.0, 40.0, 0.0));
assert_eq!(ins.attributes[0].alignment_point, Vector3::new(30.0, 40.0, 0.0));
assert_eq!(ins.attributes[1].insertion_point, Vector3::new(6.0, 8.0, 0.0));
assert_eq!(
ins.attributes[0].insertion_point,
Vector3::new(30.0, 40.0, 0.0)
);
assert_eq!(
ins.attributes[0].alignment_point,
Vector3::new(30.0, 40.0, 0.0)
);
assert_eq!(
ins.attributes[1].insertion_point,
Vector3::new(6.0, 8.0, 0.0)
);
}
/// The insertion grip keeps carrying the attributes along with the block,
@@ -518,10 +545,17 @@ mod tests {
fn insertion_grip_still_carries_the_attributes() {
let mut ins = block_with(vec![attribute("TAG_A", 3.0, 4.0)]);
apply_grip(&mut ins, 0, GripApply::Translate(glam::DVec3::new(10.0, 0.0, 0.0)));
apply_grip(
&mut ins,
0,
GripApply::Translate(glam::DVec3::new(10.0, 0.0, 0.0)),
);
assert_eq!(ins.insert_point, Vector3::new(10.0, 0.0, 0.0));
assert_eq!(ins.attributes[0].insertion_point, Vector3::new(13.0, 4.0, 0.0));
assert_eq!(
ins.attributes[0].insertion_point,
Vector3::new(13.0, 4.0, 0.0)
);
}
/// A constant attribute belongs to the block definition and an invisible
@@ -535,15 +569,30 @@ mod tests {
invisible.flags.invisible = true;
let mut locked = attribute("TAG_LOCKED", 4.0, 4.0);
locked.lock_position = true;
let mut ins = block_with(vec![constant, invisible, locked, attribute("TAG_OK", 5.0, 5.0)]);
let mut ins = block_with(vec![
constant,
invisible,
locked,
attribute("TAG_OK", 5.0, 5.0),
]);
let g = grips(&ins);
assert_eq!(g.len(), 2, "only the movable attribute is gripped: {g:?}");
assert_eq!(g[1].world, glam::DVec3::new(5.0, 5.0, 0.0));
apply_grip(&mut ins, g[1].id, GripApply::Absolute(glam::DVec3::new(50.0, 50.0, 0.0)));
assert_eq!(ins.attributes[3].insertion_point, Vector3::new(50.0, 50.0, 0.0));
assert_eq!(ins.attributes[0].insertion_point, Vector3::new(1.0, 1.0, 0.0));
apply_grip(
&mut ins,
g[1].id,
GripApply::Absolute(glam::DVec3::new(50.0, 50.0, 0.0)),
);
assert_eq!(
ins.attributes[3].insertion_point,
Vector3::new(50.0, 50.0, 0.0)
);
assert_eq!(
ins.attributes[0].insertion_point,
Vector3::new(1.0, 1.0, 0.0)
);
}
#[test]
+34 -25
View File
@@ -174,11 +174,9 @@ fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
p.y as f64 - old_elbow.y,
p.z as f64 - old_elbow.z,
),
GripApply::Translate(d) => codec::types::Vector3::new(
d.x as f64,
d.y as f64,
d.z as f64,
),
GripApply::Translate(d) => {
codec::types::Vector3::new(d.x as f64, d.y as f64, d.z as f64)
}
};
leader.vertices[n - 2].x = old_elbow.x + delta.x;
@@ -228,9 +226,7 @@ fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
}
} else if let GripApply::Translate(d) = apply {
leader.translate(codec::types::Vector3::new(
d.x as f64,
d.y as f64,
d.z as f64,
d.x as f64, d.y as f64, d.z as f64,
));
}
}
@@ -273,7 +269,11 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
};
// Geometry exposes one editable vertex at a time.
let mut geometry = vec![stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label)];
let mut geometry = vec![stepper(
t!("Current Vertex").as_ref(),
"current_vertex",
vertex_label,
)];
if let Some(v) = leader.vertices.get(vi) {
geometry.push(edit(t!("Vertex X").as_ref(), "vertex_x", v.x));
geometry.push(edit(t!("Vertex Y").as_ref(), "vertex_y", v.y));
@@ -317,7 +317,11 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
ro(t!("Text offset").as_ref(), "text_offset", String::new()),
ro(t!("Text pos vert").as_ref(), "text_pos_vert", String::new()),
];
let fit = vec![ro(t!("Dim scale overall").as_ref(), "dim_scale_overall", String::new())];
let fit = vec![ro(
t!("Dim scale overall").as_ref(),
"dim_scale_overall",
String::new(),
)];
vec![
PropSection {
@@ -559,7 +563,11 @@ impl crate::entities::traits::Grippable for Leader {
}]
}
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
let n = self.vertices.len();
match action {
@@ -627,8 +635,10 @@ impl LeaderTess for Leader {
line_weight_px: f32,
anno_scale: f32,
) -> crate::scene::model::wire_model::WireModel {
use crate::scene::convert::tessellate::{append_arrow, arrow_from_block, ArrowKind, DimGeom};
use crate::entities::dim_override as dov;
use crate::scene::convert::tessellate::{
append_arrow, arrow_from_block, ArrowKind, DimGeom,
};
use crate::scene::model::wire_model::WireModel;
let xd = &self.common.extended_data;
// Dim-line colour: a per-object ACAD_DSTYLE override (code 176, an ACI
@@ -649,9 +659,11 @@ impl LeaderTess for Leader {
.map(|s| s.dimclrd)
});
match dim_clr {
Some(idx) if idx != 0 && idx != 256 => crate::scene::convert::tess_util::aci_to_rgba(
&codec::types::Color::from_index(idx),
),
Some(idx) if idx != 0 && idx != 256 => {
crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(
idx,
))
}
_ => entity_color,
}
};
@@ -664,9 +676,8 @@ impl LeaderTess for Leader {
_ => line_weight_px,
};
let name = handle.value().to_string();
let p3 = |v: &codec::types::Vector3| -> [f32; 3] {
[(v.x) as f32, (v.y) as f32, (v.z) as f32]
};
let p3 =
|v: &codec::types::Vector3| -> [f32; 3] { [(v.x) as f32, (v.y) as f32, (v.z) as f32] };
let verts = &self.vertices;
@@ -686,9 +697,9 @@ impl LeaderTess for Leader {
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name,
points: vec![],
points_low: Vec::new(),
@@ -810,8 +821,7 @@ impl LeaderTess for Leader {
.unwrap_or(anno_scale as f64);
// Hook length: DIMASZ, like the arrowhead, when a style resolves;
// the legacy text-height heuristic otherwise.
let mut land_len = match dov::real(xd, dov::DIMASZ)
.or_else(|| style.map(|s| s.dimasz))
let mut land_len = match dov::real(xd, dov::DIMASZ).or_else(|| style.map(|s| s.dimasz))
{
Some(a) => a * dim_scale,
None => self.text_height * 1.5 * anno_scale as f64,
@@ -851,8 +861,7 @@ impl LeaderTess for Leader {
// Split absolute f64 into the render/eye double-single so the GPU keeps
// full precision at UTM coordinates (empty low bufs = absolute f32).
let (points, points_low) = crate::scene::convert::tessellate::points_to_ds(points);
let (fill_tris, fill_tris_low) =
crate::scene::convert::tessellate::points_to_ds(fill_tris);
let (fill_tris, fill_tris_low) = crate::scene::convert::tessellate::points_to_ds(fill_tris);
WireModel {
bg_adapt: None,
+33 -10
View File
@@ -9,15 +9,15 @@
//! zoom) via [`relative_render`], falling back to a small fixed world size when
//! no world-per-pixel factor is available (e.g. snapshot tessellation).
use crate::t;
use codec::entities::Light;
use codec::EntityType;
use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_angle_prop, edit_prop as edit, ro_prop, square_grip,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
@@ -298,7 +298,11 @@ impl PropertyEditable for Light {
value: self.plot_glyph,
},
},
ro_prop(t!("Color").as_ref(), "li_color", format!("{:?}", self.light_color)),
ro_prop(
t!("Color").as_ref(),
"li_color",
format!("{:?}", self.light_color),
),
edit(t!("Intensity").as_ref(), "li_intensity", self.intensity),
Property {
label: t!("Attenuation").into_owned(),
@@ -325,13 +329,23 @@ impl PropertyEditable for Light {
value: self.use_attenuation_limits,
},
},
edit(t!("Start Limit").as_ref(), "li_start", self.attenuation_start_limit),
edit(t!("End Limit").as_ref(), "li_end", self.attenuation_end_limit),
edit_angle_prop(t!("Hotspot Angle").as_ref(),
edit(
t!("Start Limit").as_ref(),
"li_start",
self.attenuation_start_limit,
),
edit(
t!("End Limit").as_ref(),
"li_end",
self.attenuation_end_limit,
),
edit_angle_prop(
t!("Hotspot Angle").as_ref(),
"li_hotspot",
self.hotspot_angle.to_degrees(),
),
edit_angle_prop(t!("Falloff Angle").as_ref(),
edit_angle_prop(
t!("Falloff Angle").as_ref(),
"li_falloff",
self.falloff_angle.to_degrees(),
),
@@ -348,9 +362,18 @@ impl PropertyEditable for Light {
value: self.cast_shadows,
},
},
ro_prop(t!("Type").as_ref(), "li_shadow_type", self.shadow_type.to_string()),
ro_prop(t!("Map Size").as_ref(), "li_shadow_size", self.shadow_map_size.to_string()),
ro_prop(t!("Softness").as_ref(),
ro_prop(
t!("Type").as_ref(),
"li_shadow_type",
self.shadow_type.to_string(),
),
ro_prop(
t!("Map Size").as_ref(),
"li_shadow_size",
self.shadow_map_size.to_string(),
),
ro_prop(
t!("Softness").as_ref(),
"li_shadow_softness",
self.shadow_map_softness.to_string(),
),
+69 -16
View File
@@ -1,5 +1,5 @@
use codec::{entities::Line, Entity};
use crate::t;
use codec::{entities::Line, Entity};
use crate::command::EntityTransform;
use crate::entities::common::{
@@ -28,8 +28,16 @@ fn to_render(line: &Line) -> RenderEntity {
key_vertices.push([point.x, point.y, point.z]);
}
tangent_geoms.push(TangentGeom::Line {
p1: [segment[0].x as f32, segment[0].y as f32, segment[0].z as f32],
p2: [segment[1].x as f32, segment[1].y as f32, segment[1].z as f32],
p1: [
segment[0].x as f32,
segment[0].y as f32,
segment[0].z as f32,
],
p2: [
segment[1].x as f32,
segment[1].y as f32,
segment[1].z as f32,
],
});
}
let center = crate::scene::centermark::dvec(association.center);
@@ -116,7 +124,11 @@ fn grips(line: &Line) -> Vec<GripDef> {
+ association.length_adjustments[index]
+ association.overshoots[index])
.max(extension_start);
result.push(oriented_triangle_grip(index + 5, center + *direction * distance, *direction));
result.push(oriented_triangle_grip(
index + 5,
center + *direction * distance,
*direction,
));
}
return result;
}
@@ -152,11 +164,20 @@ fn properties(line: &Line) -> Vec<PropSection> {
label: "Show extension".to_owned(),
field: "centermark_show_extension",
value: PropValue::Choice {
selected: if association.show_extensions { "Yes" } else { "No" }.to_owned(),
selected: if association.show_extensions {
"Yes"
} else {
"No"
}
.to_owned(),
options: vec!["Yes".to_owned(), "No".to_owned()],
},
},
edit("Cross size", "centermark_cross_size", association.cross_size),
edit(
"Cross size",
"centermark_cross_size",
association.cross_size,
),
edit("Cross gap", "centermark_cross_gap", association.cross_gap),
edit(
"Extension length",
@@ -187,7 +208,11 @@ fn properties(line: &Line) -> Vec<PropSection> {
"centerline_end_extension",
association.end_extension,
),
ro(t!("Length").as_ref(), "length", format_length(line.length())),
ro(
t!("Length").as_ref(),
"length",
format_length(line.length()),
),
ro(
"Associative",
"centerline_associative",
@@ -212,7 +237,11 @@ fn properties(line: &Line) -> Vec<PropSection> {
ro(t!("Delta X").as_ref(), "delta_x", format_length(dx)),
ro(t!("Delta Y").as_ref(), "delta_y", format_length(dy)),
ro(t!("Delta Z").as_ref(), "delta_z", format_length(dz)),
ro(t!("Length").as_ref(), "length", format_length(line.length())),
ro(
t!("Length").as_ref(),
"length",
format_length(line.length()),
),
ro(t!("Angle").as_ref(), "angle", format_direction(angle)),
edit(t!("Normal X").as_ref(), "normal_x", line.normal.x),
edit(t!("Normal Y").as_ref(), "normal_y", line.normal.y),
@@ -233,8 +262,12 @@ fn apply_geom_prop(line: &mut Line, field: &str, value: &str) {
);
}
"centermark_cross_size" | "centermark_cross_gap" | "centermark_extension_length" => {
let Some(number) = parse_f64(value) else { return; };
if !number.is_finite() || number < 0.0 { return; }
let Some(number) = parse_f64(value) else {
return;
};
if !number.is_finite() || number < 0.0 {
return;
}
match field {
"centermark_cross_size" => association.cross_size = number,
"centermark_cross_gap" => association.cross_gap = number,
@@ -413,17 +446,29 @@ fn apply_plain_transform(line: &mut Line, t: &EntityTransform) {
EntityTransform::Translate(d) => {
line.translate(codec::types::Vector3::new(d.x, d.y, d.z));
}
EntityTransform::Rotate { center, axis, angle_rad } => {
crate::scene::view::transform::apply_standard_transform(line, *center, *axis, *angle_rad);
EntityTransform::Rotate {
center,
axis,
angle_rad,
} => {
crate::scene::view::transform::apply_standard_transform(
line, *center, *axis, *angle_rad,
);
}
EntityTransform::Scale { center, factor } => {
crate::scene::view::transform::apply_standard_scale(line, *center, *factor);
}
EntityTransform::Mirror { p1, p2, working_normal } => {
EntityTransform::Mirror {
p1,
p2,
working_normal,
} => {
codec::Entity::apply_transform(
line,
&crate::scene::view::transform::reflection_about_working_line(
*p1, *p2, *working_normal,
*p1,
*p2,
*working_normal,
),
);
}
@@ -479,7 +524,11 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) {
association.cross_size *= scale;
association.cross_gap *= scale;
association.extension_length *= scale;
for value in association.length_adjustments.iter_mut().chain(association.overshoots.iter_mut()) {
for value in association
.length_adjustments
.iter_mut()
.chain(association.overshoots.iter_mut())
{
*value *= scale;
}
association.associated = false;
@@ -536,7 +585,11 @@ impl crate::entities::traits::Grippable for Line {
]
}
}
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
_grip_id: usize,
_action: crate::scene::model::object::GripMenuAction,
) {
// Lengthen needs a follow-up distance — handled by
// `apply_grip_menu_value`.
}
+136 -108
View File
@@ -86,8 +86,7 @@ fn bulge_from_midpoint(p0: [f64; 2], p1: [f64; 2], mid: [f64; 2]) -> Option<f64>
let a0 = (p0[1] - cy).atan2(p0[0] - cx);
let a1 = (p1[1] - cy).atan2(p1[0] - cx);
// Arc direction = orientation of the p0 → mid → p1 turn.
let cross =
(mid[0] - p0[0]) * (p1[1] - mid[1]) - (mid[1] - p0[1]) * (p1[0] - mid[0]);
let cross = (mid[0] - p0[0]) * (p1[1] - mid[1]) - (mid[1] - p0[1]) * (p1[0] - mid[0]);
if cross == 0.0 {
return None;
}
@@ -133,8 +132,7 @@ fn split_bulges_from_point(
let a0 = (p0[1] - cy).atan2(p0[0] - cx);
let am = (mid[1] - cy).atan2(mid[0] - cx);
let a1 = (p1[1] - cy).atan2(p1[0] - cx);
let cross =
(mid[0] - p0[0]) * (p1[1] - mid[1]) - (mid[1] - p0[1]) * (p1[0] - mid[0]);
let cross = (mid[0] - p0[0]) * (p1[1] - mid[1]) - (mid[1] - p0[1]) * (p1[0] - mid[0]);
let bulge = |from: f64, to: f64| {
if cross > 0.0 {
((to - from).rem_euclid(TAU) / 4.0).tan()
@@ -170,12 +168,8 @@ pub(crate) fn refit_added_arc_vertex(
let p0 = polyline.vertices[prev].location;
let mid = polyline.vertices[vertex_id].location;
let p1 = polyline.vertices[next].location;
let (first, second) = split_bulges_from_point(
[p0.x, p0.y],
[mid.x, mid.y],
[p1.x, p1.y],
original_bulge,
);
let (first, second) =
split_bulges_from_point([p0.x, p0.y], [mid.x, mid.y], [p1.x, p1.y], original_bulge);
polyline.vertices[prev].bulge = first.clamp(-1e6, 1e6);
polyline.vertices[vertex_id].bulge = second.clamp(-1e6, 1e6);
}
@@ -195,12 +189,8 @@ pub(crate) fn refit_added_arc_vertex(
let p0 = &polyline.vertices[prev].location;
let mid = &polyline.vertices[vertex_id].location;
let p1 = &polyline.vertices[next].location;
let (first, second) = split_bulges_from_point(
[p0.x, p0.y],
[mid.x, mid.y],
[p1.x, p1.y],
original_bulge,
);
let (first, second) =
split_bulges_from_point([p0.x, p0.y], [mid.x, mid.y], [p1.x, p1.y], original_bulge);
polyline.vertices[prev].bulge = first.clamp(-1e6, 1e6);
polyline.vertices[vertex_id].bulge = second.clamp(-1e6, 1e6);
}
@@ -377,7 +367,9 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
} else if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
{
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(crate::entities::common::bulge_arc_to_tangent(
&arc, &to_wcs, normal,
));
for s in arc
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
.into_iter()
@@ -429,14 +421,13 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
[v1.location.x, v1.location.y],
v0.bulge,
) {
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(crate::entities::common::bulge_arc_to_tangent(
&arc, &to_wcs, normal,
));
}
}
let (pts, widths) = crate::entities::common::tapered_band_points(
&band_verts,
pline.is_closed,
&to_wcs,
);
let (pts, widths) =
crate::entities::common::tapered_band_points(&band_verts, pline.is_closed, &to_wcs);
let (fill_origin, fills) = wide_fills(pline);
return RenderEntity {
pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills),
@@ -488,7 +479,9 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
}
let (wx1, wy1, wz1) = to_wcs(ox1, oy1);
kv.push([wx1, wy1, wz1]);
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(crate::entities::common::bulge_arc_to_tangent(
&arc, &to_wcs, normal,
));
}
if i + 1 < seg_count {
pts.push([f64::NAN; 3]);
@@ -536,9 +529,7 @@ fn band_verts(pline: &LwPolyline) -> Vec<([f64; 2], f64, f64, f64)> {
.collect()
}
fn tapered_band_verts(
band: &[([f64; 2], f64, f64, f64)],
) -> Option<&[([f64; 2], f64, f64, f64)]> {
fn tapered_band_verts(band: &[([f64; 2], f64, f64, f64)]) -> Option<&[([f64; 2], f64, f64, f64)]> {
let w0 = band.first().map_or(0.0, |v| v.2);
let varies = band
.iter()
@@ -579,7 +570,9 @@ fn centerline_metadata(
start.bulge,
) {
let normal = (pline.normal.x, pline.normal.y, pline.normal.z);
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
tangents.push(crate::entities::common::bulge_arc_to_tangent(
&arc, to_wcs, normal,
));
}
if index == 0 {
key_vertices.push([p0.0, p0.1, p0.2]);
@@ -591,10 +584,7 @@ fn centerline_metadata(
/// Split at vertex `idx`: a closed polyline re-opens there (one piece); an
/// open one splits into two (interior vertices only). `None` when invalid.
pub(crate) fn break_at_vertex(
p: &codec::LwPolyline,
idx: usize,
) -> Option<Vec<codec::EntityType>> {
pub(crate) fn break_at_vertex(p: &codec::LwPolyline, idx: usize) -> Option<Vec<codec::EntityType>> {
use codec::EntityType;
let n = p.vertices.len();
if n < 3 || idx >= n {
@@ -718,8 +708,7 @@ fn revision_cloud_arc_length(pline: &LwPolyline) -> Option<f64> {
return None;
}
let total = straight_guide(pline).length();
(total.is_finite() && total > 0.0)
.then_some(total / pline.vertices.len() as f64)
(total.is_finite() && total > 0.0).then_some(total / pline.vertices.len() as f64)
}
fn cloud_anchors(curve: &Curve) -> Vec<f64> {
@@ -778,25 +767,20 @@ fn cloud_points(curve: &Curve, requested: f64) -> Option<Vec<[f64; 2]>> {
let longest = spans
.iter()
.enumerate()
.max_by(|(_, left), (_, right)| {
(left.1 - left.0).total_cmp(&(right.1 - right.0))
})?
.max_by(|(_, left), (_, right)| (left.1 - left.0).total_cmp(&(right.1 - right.0)))?
.0;
counts[longest] += 3 - count_total;
}
let mut points = Vec::new();
for ((start, end), count) in spans.into_iter().zip(counts) {
for step in 0..count {
points.push(curve.point_at_distance(
start + (end - start) * step as f64 / count as f64,
));
points
.push(curve.point_at_distance(start + (end - start) * step as f64 / count as f64));
}
}
let tolerance = requested.max(total) * 1.0e-12;
points.dedup_by(|right, left| Vec2::from(*left).distance((*right).into()) <= tolerance);
if points.len() >= 2
&& Vec2::from(points[0]).distance((*points.last()?).into()) <= tolerance
{
if points.len() >= 2 && Vec2::from(points[0]).distance((*points.last()?).into()) <= tolerance {
points.pop();
}
(points.len() >= 3).then_some(points)
@@ -956,13 +940,25 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
},
};
let mut geometry_props = vec![
stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label),
stepper(
t!("Current Vertex").as_ref(),
"current_vertex",
vertex_label,
),
edit(t!("Vertex X").as_ref(), "vertex_x", vx),
edit(t!("Vertex Y").as_ref(), "vertex_y", vy),
edit_scalar(t!("Bulge").as_ref(), "bulge", v.map_or(0.0, |vertex| vertex.bulge)),
edit_scalar(
t!("Bulge").as_ref(),
"bulge",
v.map_or(0.0, |vertex| vertex.bulge),
),
];
if !is_rectangle(pline) {
geometry_props.push(edit(t!("Start segment width").as_ref(), "start_width", start_w));
geometry_props.push(edit(
t!("Start segment width").as_ref(),
"start_width",
start_w,
));
geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w));
}
geometry_props.extend([
@@ -1176,10 +1172,7 @@ fn kernel_polyline(polyline: &LwPolyline) -> Polyline {
}
}
fn edit_polyline_geometry(
polyline: &mut LwPolyline,
edit: impl FnOnce(&mut Polyline) -> bool,
) {
fn edit_polyline_geometry(polyline: &mut LwPolyline, edit: impl FnOnce(&mut Polyline) -> bool) {
let mut geometry = kernel_polyline(polyline);
if edit(&mut geometry) {
for (target, source) in polyline.vertices.iter_mut().zip(geometry.vertices) {
@@ -1198,13 +1191,9 @@ fn move_segment_parallel(polyline: &mut LwPolyline, segment: usize, offset: f64)
#[cfg(test)]
fn resize_arc_concentrically(polyline: &mut LwPolyline, segment: usize, offset: f64) {
edit_polyline_geometry(polyline, |geometry| {
geometry
.segment_arc(segment)
.is_some_and(|arc| kernel::geom2d::resize_polyline_arc(
geometry,
segment,
arc.radius + offset,
))
geometry.segment_arc(segment).is_some_and(|arc| {
kernel::geom2d::resize_polyline_arc(geometry, segment, arc.radius + offset)
})
});
}
@@ -1216,7 +1205,12 @@ fn resize_arc_segment_radius(polyline: &mut LwPolyline, segment: usize, radius:
fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(pline, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(&mut v.location.x, &mut v.location.y, p1, p2);
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
// Bulge encodes which side the arc bows to; a reflection
// reverses it or every curved segment flips to the wrong side.
v.bulge = -v.bulge;
@@ -1230,7 +1224,9 @@ impl RenderConvertible for LwPolyline {
// Arc segments offer their centres and quadrants.
if self.vertices.iter().any(|v| v.bulge != 0.0) {
let entity = codec::EntityType::LwPolyline(self.clone());
render.snap_pts.extend(crate::entities::curve::arc_segment_snaps(&entity));
render
.snap_pts
.extend(crate::entities::curve::arc_segment_snaps(&entity));
}
Some(render)
}
@@ -1352,7 +1348,11 @@ impl crate::entities::traits::Grippable for LwPolyline {
}
if action == A::Radius && grip_id >= n {
let seg = grip_id - n;
let count = if self.is_closed { n } else { n.saturating_sub(1) };
let count = if self.is_closed {
n
} else {
n.saturating_sub(1)
};
return (seg < count && self.vertices[seg].bulge.abs() >= 1.0e-9)
.then_some("New radius");
}
@@ -1430,7 +1430,11 @@ impl crate::entities::traits::Grippable for LwPolyline {
}
if action == A::MoveParallel {
if let Some(seg) = grip_id.checked_sub(self.vertices.len()) {
if self.vertices.get(seg).is_some_and(|vertex| vertex.bulge.abs() < 1.0e-9) {
if self
.vertices
.get(seg)
.is_some_and(|vertex| vertex.bulge.abs() < 1.0e-9)
{
move_segment_parallel(self, seg, value);
}
}
@@ -1464,12 +1468,15 @@ impl crate::entities::traits::Grippable for LwPolyline {
if signed_distance.abs() <= Tolerance::default().linear() {
return;
}
let direction = codec::types::Vector2::new(axis[0], axis[1])
* signed_distance.signum();
let direction = codec::types::Vector2::new(axis[0], axis[1]) * signed_distance.signum();
self.vertices[i0].location = self.vertices[o0].location + direction * value;
self.vertices[i1].location = self.vertices[o1].location + direction * value;
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
let n = self.vertices.len();
match action {
@@ -1589,7 +1596,9 @@ pub(crate) fn wide_fills(pl: &codec::entities::LwPolyline) -> ([f64; 2], Vec<Vec
return ([0.0; 2], vec![]);
}
let is_wide = pl.constant_width > 1e-9
|| verts.iter().any(|v| v.start_width > 1e-9 || v.end_width > 1e-9);
|| verts
.iter()
.any(|v| v.start_width > 1e-9 || v.end_width > 1e-9);
if !is_wide {
return ([0.0; 2], Vec::new());
}
@@ -1639,14 +1648,11 @@ impl crate::entities::traits::MassPropsCalc for codec::entities::LwPolyline {
.expect("an LwPolyline with at least two vertices has a planar curve");
let area = curve.curve.enclosed_area().abs();
let perimeter = curve.length();
let center = curve
.curve
.enclosed_centroid()
.unwrap_or_else(|| {
let x = p.vertices.iter().map(|v| v.location.x).sum::<f64>() / n as f64;
let y = p.vertices.iter().map(|v| v.location.y).sum::<f64>() / n as f64;
[x, y]
});
let center = curve.curve.enclosed_centroid().unwrap_or_else(|| {
let x = p.vertices.iter().map(|v| v.location.x).sum::<f64>() / n as f64;
let y = p.vertices.iter().map(|v| v.location.y).sum::<f64>() / n as f64;
[x, y]
});
let [cx, cy, _] = curve.plane.point_at(center);
crate::entities::traits::MassProps {
area,
@@ -1670,7 +1676,8 @@ mod tests {
let mut pl = LwPolyline::default();
pl.constant_width = constant_width;
for i in 0..count {
pl.vertices.push(LwVertex::new(Vector2::new(i as f64 * 10.0, 0.0)));
pl.vertices
.push(LwVertex::new(Vector2::new(i as f64 * 10.0, 0.0)));
}
pl
}
@@ -1706,7 +1713,9 @@ mod tests {
let menu = pl.grip_menu(2);
assert_eq!(menu[0].action, GripMenuAction::Radius);
assert_eq!(menu[1].action, GripMenuAction::Stretch);
assert!(!menu.iter().any(|item| item.action == GripMenuAction::MoveParallel));
assert!(!menu
.iter()
.any(|item| item.action == GripMenuAction::MoveParallel));
}
#[test]
@@ -1764,10 +1773,7 @@ mod tests {
#[test]
fn move_parallel_reconnects_internal_edge_to_adjacent_lines() {
let mut pl = polyline(
&[(0.0, 0.0), (2.0, 2.0), (8.0, 2.0), (10.0, 0.0)],
false,
);
let mut pl = polyline(&[(0.0, 0.0), (2.0, 2.0), (8.0, 2.0), (10.0, 0.0)], false);
move_segment_parallel(&mut pl, 1, 2.0);
assert_eq!(pl.vertices[0].location, Vector2::new(0.0, 0.0));
assert_eq!(pl.vertices[1].location, Vector2::new(4.0, 4.0));
@@ -1780,8 +1786,11 @@ mod tests {
let mut pl = polyline(&[(-5.0, 0.0), (0.0, 0.0), (5.0, 5.0)], false);
pl.vertices[1].bulge = -(std::f64::consts::FRAC_PI_2 * 0.25).tan();
let before = crate::entities::common::BulgeArc::from_bulge(
[0.0, 0.0], [5.0, 5.0], pl.vertices[1].bulge,
).unwrap();
[0.0, 0.0],
[5.0, 5.0],
pl.vertices[1].bulge,
)
.unwrap();
move_segment_parallel(&mut pl, 0, 1.0);
@@ -1789,7 +1798,8 @@ mod tests {
[pl.vertices[1].location.x, pl.vertices[1].location.y],
[pl.vertices[2].location.x, pl.vertices[2].location.y],
pl.vertices[1].bulge,
).unwrap();
)
.unwrap();
assert!(Vec2::from(after.center).distance(Vec2::from(before.center)) < 1.0e-9);
assert!((after.radius - before.radius).abs() < 1.0e-9);
assert_eq!(pl.vertices[2].location, Vector2::new(5.0, 5.0));
@@ -1833,7 +1843,8 @@ mod tests {
[pl.vertices[0].location.x, pl.vertices[0].location.y],
[pl.vertices[1].location.x, pl.vertices[1].location.y],
pl.vertices[0].bulge,
).unwrap();
)
.unwrap();
assert!((moved.radius - 7.0).abs() < 1.0e-9);
assert_eq!(pl.vertices[2].location, pl.vertices[1].location);
}
@@ -1850,8 +1861,11 @@ mod tests {
pl.vertices[0].bulge = ccw_bulge(Vec2::ZERO, Vec2::new(-5.0, 0.0), shared);
pl.vertices[1].bulge = ccw_bulge(Vec2::new(6.0, 0.0), shared, Vec2::new(11.0, 0.0));
let before = crate::entities::common::BulgeArc::from_bulge(
[shared.x, shared.y], [11.0, 0.0], pl.vertices[1].bulge,
).unwrap();
[shared.x, shared.y],
[11.0, 0.0],
pl.vertices[1].bulge,
)
.unwrap();
resize_arc_concentrically(&mut pl, 0, 1.0);
@@ -1859,7 +1873,8 @@ mod tests {
[pl.vertices[1].location.x, pl.vertices[1].location.y],
[pl.vertices[2].location.x, pl.vertices[2].location.y],
pl.vertices[1].bulge,
).unwrap();
)
.unwrap();
assert!(Vec2::from(after.center).distance(Vec2::from(before.center)) < 1.0e-9);
assert!((after.radius - before.radius).abs() < 1.0e-9);
assert_eq!(pl.vertices[2].location, Vector2::new(11.0, 0.0));
@@ -1868,12 +1883,13 @@ mod tests {
#[test]
fn radius_menu_resizes_a_corner_as_a_tangent_fillet() {
let mut pl = polyline(
&[(0.0, 0.0), (8.0, 0.0), (10.0, 2.0), (10.0, 10.0)], false,
);
let mut pl = polyline(&[(0.0, 0.0), (8.0, 0.0), (10.0, 2.0), (10.0, 10.0)], false);
pl.vertices[1].bulge = (std::f64::consts::FRAC_PI_2 / 4.0).tan();
let grip = pl.vertices.len() + 1;
assert!(pl.grip_menu(grip).iter().any(|item| item.action == GripMenuAction::Radius));
assert!(pl
.grip_menu(grip)
.iter()
.any(|item| item.action == GripMenuAction::Radius));
pl.apply_grip_menu_value(grip, GripMenuAction::Radius, 4.0);
assert_eq!(pl.vertices[0].location, Vector2::new(0.0, 0.0));
assert!((pl.vertices[1].location.x - 6.0).abs() < 1.0e-9);
@@ -1885,28 +1901,31 @@ mod tests {
[pl.vertices[1].location.x, pl.vertices[1].location.y],
[pl.vertices[2].location.x, pl.vertices[2].location.y],
pl.vertices[1].bulge,
).unwrap();
)
.unwrap();
assert!((arc.radius - 4.0).abs() < 1.0e-9);
assert!(arc.sweep > 0.0);
}
#[test]
fn radius_cursor_tracks_the_fillet_midpoint_ray() {
let mut pl = polyline(
&[(0.0, 0.0), (8.0, 0.0), (10.0, 2.0), (10.0, 10.0)], false,
);
let mut pl = polyline(&[(0.0, 0.0), (8.0, 0.0), (10.0, 2.0), (10.0, 10.0)], false);
pl.vertices[1].bulge = (std::f64::consts::FRAC_PI_2 / 4.0).tan();
let arc = crate::entities::common::BulgeArc::from_bulge(
[pl.vertices[1].location.x, pl.vertices[1].location.y],
[pl.vertices[2].location.x, pl.vertices[2].location.y],
pl.vertices[1].bulge,
).unwrap();
)
.unwrap();
let midpoint = Vec2::from(arc.sample(0.5));
let cursor = Vec2::new(10.0, 0.0) + (midpoint - Vec2::new(10.0, 0.0)) * 2.0;
let value = pl.grip_menu_point_value(
pl.vertices.len() + 1, GripMenuAction::Radius,
glam::DVec3::new(cursor.x, cursor.y, 0.0),
).unwrap();
let value = pl
.grip_menu_point_value(
pl.vertices.len() + 1,
GripMenuAction::Radius,
glam::DVec3::new(cursor.x, cursor.y, 0.0),
)
.unwrap();
assert!((value - 4.0).abs() < 1.0e-9);
}
@@ -1919,7 +1938,8 @@ mod tests {
[pl.vertices[0].location.x, pl.vertices[0].location.y],
[pl.vertices[1].location.x, pl.vertices[1].location.y],
pl.vertices[0].bulge,
).unwrap();
)
.unwrap();
assert!((arc.radius - 8.0).abs() < 1.0e-9);
assert!(Vec2::from(arc.center).distance(Vec2::new(5.0, 0.0)) < 1.0e-9);
assert!(arc.sweep > 0.0);
@@ -1938,19 +1958,21 @@ mod tests {
#[test]
fn radius_on_a_non_tangent_internal_arc_keeps_center_and_side_angles() {
let mut pl = polyline(
&[(0.0, 0.0), (8.0, 0.0), (10.0, 2.0), (10.0, 10.0)], false,
);
let mut pl = polyline(&[(0.0, 0.0), (8.0, 0.0), (10.0, 2.0), (10.0, 10.0)], false);
pl.vertices[1].bulge = 1.0;
let before = crate::entities::common::BulgeArc::from_bulge(
[8.0, 0.0], [10.0, 2.0], pl.vertices[1].bulge,
).unwrap();
[8.0, 0.0],
[10.0, 2.0],
pl.vertices[1].bulge,
)
.unwrap();
resize_arc_segment_radius(&mut pl, 1, 4.0);
let after = crate::entities::common::BulgeArc::from_bulge(
[pl.vertices[1].location.x, pl.vertices[1].location.y],
[pl.vertices[2].location.x, pl.vertices[2].location.y],
pl.vertices[1].bulge,
).unwrap();
)
.unwrap();
assert!(Vec2::from(after.center).distance(Vec2::from(before.center)) < 1.0e-9);
assert!((after.radius - 4.0).abs() < 1.0e-9);
let incoming = Vec2::new(
@@ -1999,7 +2021,10 @@ mod tests {
let props = pl.geometry_properties(&[]);
let geom_props = &props[0].props;
let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap();
let gw = geom_props
.iter()
.find(|p| p.field == "global_width")
.unwrap();
match &gw.value {
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(5.0)),
_ => panic!("expected EditText"),
@@ -2016,7 +2041,10 @@ mod tests {
let props = pl.geometry_properties(&[]);
let geom_props = &props[0].props;
let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap();
let gw = geom_props
.iter()
.find(|p| p.field == "global_width")
.unwrap();
match &gw.value {
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
_ => panic!("expected EditText with VARIES_LABEL"),
+2 -2
View File
@@ -551,7 +551,7 @@ pub fn match_properties_kernel(
#[cfg(test)]
mod match_layer_kernel_tests {
use super::{MatchLayerError, match_layer_kernel};
use super::{match_layer_kernel, MatchLayerError};
fn line_on(layer: &str) -> codec::EntityType {
let mut line = codec::entities::Line::new();
@@ -606,7 +606,7 @@ mod match_layer_kernel_tests {
#[cfg(test)]
mod match_properties_kernel_tests {
use super::{MatchOpts, match_properties_kernel};
use super::{match_properties_kernel, MatchOpts};
use codec::EntityType;
fn line_src() -> EntityType {
+176 -105
View File
@@ -1,12 +1,12 @@
use codec::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, PolyfaceMesh};
use glam::Vec3;
use kernel::geom2d::{triangulate, Tolerance};
use kernel::space::{polygon, NurbsSurface3, Plane, Vec3 as KernelVec3};
use glam::Vec3;
use crate::command::EntityTransform;
use crate::entities::common::{
edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64, ro_prop as ro,
square_grip, stepper_prop as stepper,
edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64, ro_prop as ro, square_grip,
stepper_prop as stepper,
};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
@@ -83,8 +83,7 @@ fn face3d_fill(corners: [[f64; 3]; 4]) -> Vec<[f64; 3]> {
let mut ring = Vec::with_capacity(4);
for corner in corners {
if ring.last().is_none_or(|previous| {
KernelVec3::from(*previous).distance(KernelVec3::from(corner))
> tolerance.linear()
KernelVec3::from(*previous).distance(KernelVec3::from(corner)) > tolerance.linear()
}) {
ring.push(corner);
}
@@ -200,8 +199,7 @@ impl PropertyEditable for Face3D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let was_triangle = self.is_triangle();
let vertex_count = if was_triangle { 3 } else { 4 };
let current = crate::scene::view::dispatch::prop_current_vertex()
.min(vertex_count - 1);
let current = crate::scene::view::dispatch::prop_current_vertex().min(vertex_count - 1);
let corner = match current {
0 => &self.first_corner,
1 => &self.second_corner,
@@ -263,8 +261,7 @@ impl PropertyEditable for Face3D {
};
let was_triangle = self.is_triangle();
let vertex_count = if was_triangle { 3 } else { 4 };
let current = crate::scene::view::dispatch::prop_current_vertex()
.min(vertex_count - 1);
let current = crate::scene::view::dispatch::prop_current_vertex().min(vertex_count - 1);
let corner = match current {
0 => &mut self.first_corner,
1 => &mut self.second_corner,
@@ -285,16 +282,25 @@ impl PropertyEditable for Face3D {
impl Transformable for Face3D {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for corner in [
&mut entity.first_corner,
&mut entity.second_corner,
&mut entity.third_corner,
&mut entity.fourth_corner,
] {
crate::scene::view::transform::reflect_xy_point(&mut corner.x, &mut corner.y, p1, p2);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for corner in [
&mut entity.first_corner,
&mut entity.second_corner,
&mut entity.third_corner,
&mut entity.fourth_corner,
] {
crate::scene::view::transform::reflect_xy_point(
&mut corner.x,
&mut corner.y,
p1,
p2,
);
}
},
);
}
}
@@ -381,8 +387,16 @@ impl RenderConvertible for PolygonMesh {
lines.push(b);
};
let m_segments = if self.is_closed_m() { m } else { m.saturating_sub(1) };
let n_segments = if self.is_closed_n() { n } else { n.saturating_sub(1) };
let m_segments = if self.is_closed_m() {
m
} else {
m.saturating_sub(1)
};
let n_segments = if self.is_closed_n() {
n
} else {
n.saturating_sub(1)
};
for row in 0..m {
for column in 0..n_segments {
add_segment(row * n + column, row * n + (column + 1) % n);
@@ -457,8 +471,8 @@ impl PropertyEditable for PolygonMesh {
SurfaceSmoothType::Bezier => "Bezier",
};
let vertex_count = self.vertices.len();
let current = crate::scene::view::dispatch::prop_current_vertex()
.min(vertex_count.saturating_sub(1));
let current =
crate::scene::view::dispatch::prop_current_vertex().min(vertex_count.saturating_sub(1));
let vertex = self.vertices.get(current);
let current_vertex = if vertex_count == 0 { 0 } else { current + 1 };
vec![
@@ -470,15 +484,18 @@ impl PropertyEditable for PolygonMesh {
"pm_current_vertex",
current_vertex as f64,
),
edit(t!("Vertex X").as_ref(),
edit(
t!("Vertex X").as_ref(),
"pm_vx",
vertex.map_or(0.0, |vertex| vertex.location.x),
),
edit(t!("Vertex Y").as_ref(),
edit(
t!("Vertex Y").as_ref(),
"pm_vy",
vertex.map_or(0.0, |vertex| vertex.location.y),
),
edit(t!("Vertex Z").as_ref(),
edit(
t!("Vertex Z").as_ref(),
"pm_vz",
vertex.map_or(0.0, |vertex| vertex.location.z),
),
@@ -508,8 +525,16 @@ impl PropertyEditable for PolygonMesh {
"pm_smooth_n",
self.n_smooth_density as f64,
),
ro(t!("M vertex count").as_ref(), "pm_m", self.m_vertex_count.to_string()),
ro(t!("N vertex count").as_ref(), "pm_n", self.n_vertex_count.to_string()),
ro(
t!("M vertex count").as_ref(),
"pm_m",
self.m_vertex_count.to_string(),
),
ro(
t!("N vertex count").as_ref(),
"pm_n",
self.n_vertex_count.to_string(),
),
],
},
PropSection {
@@ -537,13 +562,9 @@ impl PropertyEditable for PolygonMesh {
let bool_value = |current: bool| {
if value == "toggle" {
Some(!current)
} else if value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("yes")
{
} else if value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("yes") {
Some(true)
} else if value.eq_ignore_ascii_case("false")
|| value.eq_ignore_ascii_case("no")
{
} else if value.eq_ignore_ascii_case("false") || value.eq_ignore_ascii_case("no") {
Some(false)
} else {
None
@@ -610,16 +631,20 @@ impl PropertyEditable for PolygonMesh {
impl Transformable for PolygonMesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
},
);
}
}
@@ -688,17 +713,26 @@ impl PropertyEditable for PolyfaceMesh {
title: t!("Geometry").into_owned(),
props: vec![
ro(t!("Vertex").as_ref(), "pfm_vertex", String::new()),
ro(t!("Vertex X").as_ref(),
ro(
t!("Vertex X").as_ref(),
"pfm_vx",
first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
first
.map(|v| format!("{:.4}", v.location.x))
.unwrap_or_default(),
),
ro(t!("Vertex Y").as_ref(),
ro(
t!("Vertex Y").as_ref(),
"pfm_vy",
first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(),
first
.map(|v| format!("{:.4}", v.location.y))
.unwrap_or_default(),
),
ro(t!("Vertex Z").as_ref(),
ro(
t!("Vertex Z").as_ref(),
"pfm_vz",
first.map(|v| format!("{:.4}", v.location.z)).unwrap_or_default(),
first
.map(|v| format!("{:.4}", v.location.z))
.unwrap_or_default(),
),
// Polyface meshes store an explicit vertex/face list rather
// than an M×N grid, so the grid-only rows are not applicable.
@@ -708,8 +742,16 @@ impl PropertyEditable for PolyfaceMesh {
ro(t!("N closed").as_ref(), "pfm_closed_n", String::new()),
ro(t!("M density").as_ref(), "pfm_density_m", String::new()),
ro(t!("N density").as_ref(), "pfm_density_n", String::new()),
ro(t!("Vertex count").as_ref(), "pfm_v", self.vertices.len().to_string()),
ro(t!("Face count").as_ref(), "pfm_f", self.faces.len().to_string()),
ro(
t!("Vertex count").as_ref(),
"pfm_v",
self.vertices.len().to_string(),
),
ro(
t!("Face count").as_ref(),
"pfm_f",
self.faces.len().to_string(),
),
],
},
PropSection {
@@ -724,16 +766,20 @@ impl PropertyEditable for PolyfaceMesh {
impl Transformable for PolyfaceMesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
},
);
}
}
@@ -751,7 +797,11 @@ struct RefinedMesh {
}
fn edge_key(a: usize, b: usize) -> (usize, usize) {
if a < b { (a, b) } else { (b, a) }
if a < b {
(a, b)
} else {
(b, a)
}
}
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
@@ -859,21 +909,25 @@ fn subdivide_catmull_clark(mesh: &RefinedMesh, blend_crease: bool) -> RefinedMes
continue;
}
let face_average =
mean_points(incident_faces.iter().filter_map(|&index| face_points.get(index)));
let face_average = mean_points(
incident_faces
.iter()
.filter_map(|&index| face_points.get(index)),
);
let edge_midpoints: Vec<[f64; 3]> = incident_edges
.iter()
.filter_map(|&(a, b)| {
let other = if a == vertex_index { b } else { a };
mesh.vertices
.get(other)
.map(|&p| mul3(add3(point, p), 0.5))
mesh.vertices.get(other).map(|&p| mul3(add3(point, p), 0.5))
})
.collect();
let edge_average = mean_points(edge_midpoints.iter());
let n = incident_faces.len() as f64;
let smooth = mul3(
add3(add3(face_average, mul3(edge_average, 2.0)), mul3(point, n - 3.0)),
add3(
add3(face_average, mul3(edge_average, 2.0)),
mul3(point, n - 3.0),
),
1.0 / n,
);
@@ -896,10 +950,9 @@ fn subdivide_catmull_clark(mesh: &RefinedMesh, blend_crease: bool) -> RefinedMes
sharp_neighbours.sort_by(|a, b| b.0.total_cmp(&a.0));
let crease_point = match sharp_neighbours.as_slice() {
[(_, first), (_, second), ..] => mul3(
add3(add3(mul3(point, 6.0), *first), *second),
1.0 / 8.0,
),
[(_, first), (_, second), ..] => {
mul3(add3(add3(mul3(point, 6.0), *first), *second), 1.0 / 8.0)
}
_ => smooth,
};
let corner_point = if sharp_neighbours.len() >= 3 {
@@ -907,10 +960,7 @@ fn subdivide_catmull_clark(mesh: &RefinedMesh, blend_crease: bool) -> RefinedMes
} else {
crease_point
};
let sharpness = sharp_neighbours
.get(1)
.map(|item| item.0)
.unwrap_or(0.0);
let sharpness = sharp_neighbours.get(1).map(|item| item.0).unwrap_or(0.0);
let amount = if blend_crease {
sharpness.clamp(0.0, 1.0)
} else if sharpness > 0.0 {
@@ -1061,10 +1111,7 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option<kernel::bre
}
}
fn face_triangle_indices(
vertices: &[[f64; 3]],
faces: &[Vec<usize>],
) -> (Vec<u32>, Vec<usize>) {
fn face_triangle_indices(vertices: &[[f64; 3]], faces: &[Vec<usize>]) -> (Vec<u32>, Vec<usize>) {
let mut indices = Vec::new();
let mut triangle_faces = Vec::new();
for (face_index, face) in faces.iter().enumerate() {
@@ -1079,7 +1126,8 @@ fn face_triangle_indices(
let mut mapped = [0u32; 3];
let mut valid = true;
for corner in 0..3 {
let Some(local) = polygon.iter().position(|point| *point == triangle[corner]) else {
let Some(local) = polygon.iter().position(|point| *point == triangle[corner])
else {
valid = false;
break;
};
@@ -1261,13 +1309,7 @@ pub(crate) fn tessellate_shaded_mesh(
let vertices: Vec<[f64; 3]> = mesh
.vertices
.iter()
.map(|vertex| {
[
vertex.location.x,
vertex.location.y,
vertex.location.z,
]
})
.map(|vertex| [vertex.location.x, vertex.location.y, vertex.location.z])
.collect();
let mut faces = Vec::new();
let mut face_colors = Vec::new();
@@ -1285,11 +1327,18 @@ pub(crate) fn tessellate_shaded_mesh(
}
for corner in 0..indices.len() {
if raw[corner] > 0 {
edges.insert(edge_key(indices[corner], indices[(corner + 1) % indices.len()]));
edges.insert(edge_key(
indices[corner],
indices[(corner + 1) % indices.len()],
));
}
}
faces.push(indices);
face_colors.push(face.color.as_ref().map(crate::scene::convert::tess_util::aci_to_rgba));
face_colors.push(
face.color
.as_ref()
.map(crate::scene::convert::tess_util::aci_to_rgba),
);
}
make_mesh_lod_set(
mesh.common.handle.value().to_string(),
@@ -1377,8 +1426,7 @@ impl PropertyEditable for Mesh {
*edge_use.entry(key).or_insert(0) += 1;
}
}
let watertight =
!self.faces.is_empty() && edge_use.values().all(|&c| c == 2);
let watertight = !self.faces.is_empty() && edge_use.values().all(|&c| c == 2);
vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
@@ -1395,10 +1443,23 @@ impl PropertyEditable for Mesh {
value: self.blend_crease,
},
},
ro(t!("Number of Faces").as_ref(), "msh_f", self.faces.len().to_string()),
ro(t!("Number of Vertices").as_ref(), "msh_v", self.vertices.len().to_string()),
ro(t!("Number of Edges").as_ref(), "msh_e", self.edges.len().to_string()),
ro(t!("Creased Edges").as_ref(),
ro(
t!("Number of Faces").as_ref(),
"msh_f",
self.faces.len().to_string(),
),
ro(
t!("Number of Vertices").as_ref(),
"msh_v",
self.vertices.len().to_string(),
),
ro(
t!("Number of Edges").as_ref(),
"msh_e",
self.edges.len().to_string(),
),
ro(
t!("Creased Edges").as_ref(),
"msh_creased",
self.edges
.iter()
@@ -1406,12 +1467,18 @@ impl PropertyEditable for Mesh {
.count()
.to_string(),
),
ro(t!("Override Option").as_ref(),
ro(
t!("Override Option").as_ref(),
"msh_override",
self.override_option.to_string(),
),
ro(t!("Number of Grips").as_ref(), "msh_grips", self.vertices.len().to_string()),
ro(t!("Watertight").as_ref(),
ro(
t!("Number of Grips").as_ref(),
"msh_grips",
self.vertices.len().to_string(),
),
ro(
t!("Watertight").as_ref(),
"msh_watertight",
if watertight { "Yes" } else { "No" },
),
@@ -1441,10 +1508,14 @@ impl PropertyEditable for Mesh {
impl Transformable for Mesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
}
},
);
}
}
+67 -112
View File
@@ -43,7 +43,8 @@ pub(crate) fn rebuild_mline_geometry(mline: &mut MLine) -> bool {
.map(kernel::space::Vec3::to_array)
})
.or_else(|| {
mline.is_closed()
mline
.is_closed()
.then(|| {
(kernel::space::Vec3::from(points[0])
- kernel::space::Vec3::from(points[count - 1]))
@@ -112,9 +113,7 @@ pub(crate) fn rebuild_mline_geometry(mline: &mut MLine) -> bool {
second_offset.to_array(),
after.to_array(),
)
.and_then(|(at, _)| {
(first_offset + before * at - flat[index]).normalize()
})
.and_then(|(at, _)| (first_offset + before * at - flat[index]).normalize())
.unwrap_or_else(|| after.perpendicular())
}
(Some(direction), None) | (None, Some(direction)) => direction.perpendicular(),
@@ -144,8 +143,7 @@ fn adjusted_mline_endpoint(
mline.vertices[vertex].direction.y,
mline.vertices[vertex].direction.z,
)
.normalize()
else {
.normalize() else {
return point;
};
let Some(normal) =
@@ -252,10 +250,7 @@ pub(crate) fn store_mline_drawn_ranges(
);
if ranges.is_empty() {
parameters.extend([0.0, 0.0]);
} else if ranges.len() == 1
&& ranges[0].0 <= 1.0e-9
&& ranges[0].1 >= length - 1.0e-9
{
} else if ranges.len() == 1 && ranges[0].0 <= 1.0e-9 && ranges[0].1 >= length - 1.0e-9 {
parameters.push(0.0);
} else {
parameters.push(ranges[0].0);
@@ -355,11 +350,8 @@ fn mline_element_endpoints(
let point = |index: usize| {
let vertex = mline.vertices.get(index)?;
let offset = vertex.segments.get(element)?.parameters.first().copied()?;
let position = kernel::space::Vec3::new(
vertex.position.x,
vertex.position.y,
vertex.position.z,
);
let position =
kernel::space::Vec3::new(vertex.position.x, vertex.position.y, vertex.position.z);
let miter = kernel::space::Vec3::new(vertex.miter.x, vertex.miter.y, vertex.miter.z);
Some(position + miter * offset)
};
@@ -481,9 +473,7 @@ pub fn resolved_mline_style<'a>(
})
.or_else(|| {
document.objects.values().find_map(|object| match object {
ObjectType::MLineStyle(style)
if style.name.eq_ignore_ascii_case(&m.style_name) =>
{
ObjectType::MLineStyle(style) if style.name.eq_ignore_ascii_case(&m.style_name) => {
Some(style)
}
_ => None,
@@ -495,17 +485,11 @@ pub fn mline_lines(m: &MLine, document: &codec::CadDocument) -> Vec<MLineLine> {
mline_lines_resolved(m, resolved_mline_style(m, document))
}
pub fn mline_lines_with_style(
m: &MLine,
style: &codec::objects::MLineStyle,
) -> Vec<MLineLine> {
pub fn mline_lines_with_style(m: &MLine, style: &codec::objects::MLineStyle) -> Vec<MLineLine> {
mline_lines_resolved(m, Some(style))
}
fn mline_lines_resolved(
m: &MLine,
style: Option<&codec::objects::MLineStyle>,
) -> Vec<MLineLine> {
fn mline_lines_resolved(m: &MLine, style: Option<&codec::objects::MLineStyle>) -> Vec<MLineLine> {
use codec::entities::{MLineFlags, MLineJustification};
use codec::types::Color;
@@ -595,8 +579,7 @@ fn mline_lines_resolved(
continue;
};
let at = |t: f64| (a + direction * t).to_array();
let parameters: &[f64] = m
.vertices[vi]
let parameters: &[f64] = m.vertices[vi]
.segments
.get(ei)
.map(|segment| segment.parameters.as_slice())
@@ -766,24 +749,25 @@ pub fn mline_fill_triangles_with_style(
if !style.flags.fill_on || m.vertices.len() < 2 || style.elements.len() < 2 {
return Vec::new();
}
let (low_index, high_index) = style
.elements
.iter()
.enumerate()
.fold((0, 0), |(low, high), (index, element)| {
let offset = element.offset * m.scale_factor;
let low = if offset < style.elements[low].offset * m.scale_factor {
index
} else {
low
};
let high = if offset > style.elements[high].offset * m.scale_factor {
index
} else {
high
};
(low, high)
});
let (low_index, high_index) =
style
.elements
.iter()
.enumerate()
.fold((0, 0), |(low, high), (index, element)| {
let offset = element.offset * m.scale_factor;
let low = if offset < style.elements[low].offset * m.scale_factor {
index
} else {
low
};
let high = if offset > style.elements[high].offset * m.scale_factor {
index
} else {
high
};
(low, high)
});
let minimum = style
.elements
.iter()
@@ -807,11 +791,8 @@ pub fn mline_fill_triangles_with_style(
.and_then(|segment| segment.parameters.first())
.copied()
.unwrap_or((style.elements[element].offset + shift) * m.scale_factor);
let point = (kernel::space::Vec3::new(
item.position.x,
item.position.y,
item.position.z,
) + kernel::space::Vec3::new(item.miter.x, item.miter.y, item.miter.z) * distance)
let point = (kernel::space::Vec3::new(item.position.x, item.position.y, item.position.z)
+ kernel::space::Vec3::new(item.miter.x, item.miter.y, item.miter.z) * distance)
.to_array();
if endpoint {
adjusted_mline_endpoint(m, style, vertex, point)
@@ -830,17 +811,10 @@ pub fn mline_fill_triangles_with_style(
let next = (vertex + 1) % m.vertices.len();
let start_endpoint = !closed && vertex == 0;
let end_endpoint = !closed && next + 1 == m.vertices.len();
let low_start = kernel::space::Vec3::from(offset_point(
vertex,
low_index,
start_endpoint,
));
let low_start = kernel::space::Vec3::from(offset_point(vertex, low_index, start_endpoint));
let low_end = kernel::space::Vec3::from(offset_point(next, low_index, end_endpoint));
let high_start = kernel::space::Vec3::from(offset_point(
vertex,
high_index,
start_endpoint,
));
let high_start =
kernel::space::Vec3::from(offset_point(vertex, high_index, start_endpoint));
let high_end = kernel::space::Vec3::from(offset_point(next, high_index, end_endpoint));
let low_length = low_start.distance(low_end);
let high_length = high_start.distance(high_end);
@@ -982,9 +956,7 @@ impl Grippable for MLine {
};
for (segment, moved_start) in candidates {
if segment >= segment_count
|| affected
.iter()
.any(|(existing, _, _)| *existing == segment)
|| affected.iter().any(|(existing, _, _)| *existing == segment)
{
continue;
}
@@ -1020,7 +992,11 @@ impl Grippable for MLine {
]
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
let n = self.vertices.len();
match action {
@@ -1039,11 +1015,8 @@ impl Grippable for MLine {
new_v.position.x = (v0.position.x + v1.position.x) * 0.5;
new_v.position.y = (v0.position.y + v1.position.y) * 0.5;
new_v.position.z = (v0.position.z + v1.position.z) * 0.5;
let point = kernel::space::Vec3::new(
new_v.position.x,
new_v.position.y,
new_v.position.z,
);
let point =
kernel::space::Vec3::new(new_v.position.x, new_v.position.y, new_v.position.z);
let source: Option<Vec<_>> = (0..self.style_element_count)
.map(|element| {
let endpoints = mline_element_endpoints(self, grip_id, element)?;
@@ -1086,22 +1059,14 @@ impl Grippable for MLine {
let second_cuts =
remap_mline_ranges(&cuts, split, source_length, second_length);
let first = &mut self.vertices[grip_id].segments[element];
store_mline_drawn_ranges(
&mut first.parameters,
first_length,
&first_drawn,
);
store_mline_drawn_ranges(&mut first.parameters, first_length, &first_drawn);
store_mline_cut_ranges(
&mut first.area_fill_parameters,
first_length,
&first_cuts,
);
let second = &mut self.vertices[insert].segments[element];
store_mline_drawn_ranges(
&mut second.parameters,
second_length,
&second_drawn,
);
store_mline_drawn_ranges(&mut second.parameters, second_length, &second_drawn);
store_mline_cut_ranges(
&mut second.area_fill_parameters,
second_length,
@@ -1109,9 +1074,7 @@ impl Grippable for MLine {
);
}
}
A::RemoveVertex
if grip_id < n && n > 2 && (!self.is_closed() || n > 3) =>
{
A::RemoveVertex if grip_id < n && n > 2 && (!self.is_closed() || n > 3) => {
let merge = self.is_closed() || (grip_id > 0 && grip_id + 1 < n);
let previous = if grip_id == 0 { n - 1 } else { grip_id - 1 };
let source = merge.then(|| {
@@ -1126,10 +1089,7 @@ impl Grippable for MLine {
};
let mut peripheral = Vec::new();
let candidates = if self.is_closed() {
vec![
((previous + n - 1) % n, false),
((grip_id + 1) % n, true),
]
vec![((previous + n - 1) % n, false), ((grip_id + 1) % n, true)]
} else {
let mut candidates = Vec::new();
if grip_id >= 2 {
@@ -1186,18 +1146,13 @@ impl Grippable for MLine {
drawn.extend(
mline_drawn_ranges(&second.parameters, second_length)
.into_iter()
.map(|range| {
(range.0 + first_length, range.1 + first_length)
}),
.map(|range| (range.0 + first_length, range.1 + first_length)),
);
let mut cuts =
mline_cut_ranges(&first.area_fill_parameters, first_length);
let mut cuts = mline_cut_ranges(&first.area_fill_parameters, first_length);
cuts.extend(
mline_cut_ranges(&second.area_fill_parameters, second_length)
.into_iter()
.map(|range| {
(range.0 + first_length, range.1 + first_length)
}),
.map(|range| (range.0 + first_length, range.1 + first_length)),
);
let drawn = merge_mline_ranges(remap_mline_ranges(
&drawn,
@@ -1212,11 +1167,7 @@ impl Grippable for MLine {
target_length,
));
let target = &mut self.vertices[previous].segments[element];
store_mline_drawn_ranges(
&mut target.parameters,
target_length,
&drawn,
);
store_mline_drawn_ranges(&mut target.parameters, target_length, &drawn);
store_mline_cut_ranges(
&mut target.area_fill_parameters,
target_length,
@@ -1377,21 +1328,25 @@ fn restore_mline_offsets(mline: &mut MLine, offsets: &[Vec<Option<f64>>]) {
impl Transformable for MLine {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.position.x,
&mut v.position.y,
p1,
p2,
);
}
crate::scene::view::transform::reflect_xy_point(
&mut v.position.x,
&mut v.position.y,
&mut entity.start_point.x,
&mut entity.start_point.y,
p1,
p2,
);
}
crate::scene::view::transform::reflect_xy_point(
&mut entity.start_point.x,
&mut entity.start_point.y,
p1,
p2,
);
});
},
);
}
}
+1 -1
View File
@@ -37,5 +37,5 @@ pub mod text_support;
pub mod tolerance;
pub mod traits;
pub mod underlay;
pub mod viewport;
pub mod view_symbol;
pub mod viewport;
+38 -17
View File
@@ -2,13 +2,14 @@ use codec::entities::{AttachmentPoint, DrawingDirection, MText};
use crate::command::EntityTransform;
use crate::entities::common::{
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro,
square_grip, triangle_grip,
};
use crate::entities::text_support::{
clamp_mtext_column_count, layout_mtext, resolve_text_style, GlyphBox, MTextColumns,
MTextRenderOpts, MTextVAnchor,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
@@ -100,7 +101,9 @@ fn display_text(t: &MText, document: &codec::CadDocument) -> String {
if gap == 0 || !after.chars().next().is_some_and(is_rtl_char) {
continue;
}
let word = after.find(|c: char| c == ' ' || c == '\\').unwrap_or(after.len());
let word = after
.find(|c: char| c == ' ' || c == '\\')
.unwrap_or(after.len());
let moved = format!("{}{}{}", &after[..word], &rest[..gap], &after[word..]);
out.replace_range(span.end.., &moved);
}
@@ -261,7 +264,11 @@ pub(crate) fn explode_mtext(t: &MText, document: &codec::CadDocument) -> Vec<cod
text.common.handle = codec::Handle::NULL;
if let Some([r, g, b]) = stroke.color {
let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
text.common.color = codec::types::Color::Rgb { r: byte(r), g: byte(g), b: byte(b) };
text.common.color = codec::types::Color::Rgb {
r: byte(r),
g: byte(g),
b: byte(b),
};
}
text.style = t.style.clone();
text.height = run.height as f64;
@@ -321,8 +328,7 @@ fn grips(t: &MText) -> Vec<GripDef> {
let columns_active =
t.column_data.column_type != 0 && column_count > 1 && t.column_data.width > 0.0;
let block_width = if columns_active {
t.column_data.width * column_count as f64
+ t.column_data.gutter * (column_count - 1) as f64
t.column_data.width * column_count as f64 + t.column_data.gutter * (column_count - 1) as f64
} else {
t.rectangle_width.max(0.0)
};
@@ -338,9 +344,9 @@ fn grips(t: &MText) -> Vec<GripDef> {
let down = glam::DVec3::new(sin, -cos, 0.0);
let (_, vertical) = attach_anchors(t);
let factor = match vertical {
MTextVAnchor::Top
| MTextVAnchor::MiddleOfTopLine
| MTextVAnchor::BottomOfTopLine => 1.0,
MTextVAnchor::Top | MTextVAnchor::MiddleOfTopLine | MTextVAnchor::BottomOfTopLine => {
1.0
}
MTextVAnchor::Middle => 0.5,
MTextVAnchor::Bottom | MTextVAnchor::MiddleOfBottomLine => -1.0,
};
@@ -439,8 +445,16 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
crate::entities::common::style_fixed_height(&t.style).is_none(),
),
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees()),
edit(t!("Line space factor").as_ref(), "line_spacing", t.line_spacing_factor),
edit(t!("Line space distance").as_ref(), "line_space_distance", line_space_distance),
edit(
t!("Line space factor").as_ref(),
"line_spacing",
t.line_spacing_factor,
),
edit(
t!("Line space distance").as_ref(),
"line_space_distance",
line_space_distance,
),
Property {
label: t!("Line space style").into_owned(),
field: "line_space_style",
@@ -473,7 +487,12 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
.collect(),
},
},
num_row(t!("Defined width").as_ref(), "rect_w", t.rectangle_width, false),
num_row(
t!("Defined width").as_ref(),
"rect_w",
t.rectangle_width,
false,
),
num_row(
t!("Defined height").as_ref(),
"rect_h",
@@ -652,8 +671,7 @@ fn apply_grip(t: &mut MText, grip_id: usize, apply: GripApply) {
t.column_data.column_count = column_count;
let gaps = (column_count - 1) as f64;
t.column_data.width =
((width - t.column_data.gutter * gaps) / column_count as f64)
.max(0.01);
((width - t.column_data.gutter * gaps) / column_count as f64).max(0.01);
} else {
t.rectangle_width = width;
}
@@ -698,8 +716,7 @@ fn apply_grip(t: &mut MText, grip_id: usize, apply: GripApply) {
p.y as f64 - t.insertion_point.y,
0.0,
);
t.column_data.gutter =
(delta.dot(dir) / k - t.column_data.width).max(0.0);
t.column_data.gutter = (delta.dot(dir) / k - t.column_data.width).max(0.0);
}
_ => {}
}
@@ -762,7 +779,11 @@ impl Grippable for MText {
}
}
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
_grip_id: usize,
_action: crate::scene::model::object::GripMenuAction,
) {
// Rotate needs a follow-up angle handled by
// `apply_grip_menu_value`; Move-with-Text is the default drag.
}
+191 -261
View File
@@ -57,7 +57,8 @@ pub(crate) fn active_vertical_attachment(ml: &MultiLeader) -> TextAttachmentType
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row,
ro_prop as ro, square_grip, triangle_grip,
};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
@@ -189,13 +190,11 @@ fn to_render(ml: &MultiLeader, document: &codec::CadDocument) -> Option<RenderEn
codec::entities::multileader::TextAttachmentPointType::Center => 0.5,
codec::entities::multileader::TextAttachmentPointType::Right => 1.0,
},
v_anchor: mleader_v_anchor(
if text_sign >= 0.0 {
ctx.text_left_attachment
} else {
ctx.text_right_attachment
},
),
v_anchor: mleader_v_anchor(if text_sign >= 0.0 {
ctx.text_left_attachment
} else {
ctx.text_right_attachment
}),
line_spacing_factor: ctx.line_spacing_factor as f32,
exact_line_spacing: matches!(
ctx.line_spacing_style,
@@ -250,14 +249,12 @@ fn to_render(ml: &MultiLeader, document: &codec::CadDocument) -> Option<RenderEn
first = false;
// Build the full control-point list: line.points + landing point
let mut ctrl: Vec<[f64; 3]> =
line.points.iter().map(|p| p3(p)).collect();
let mut ctrl: Vec<[f64; 3]> = line.points.iter().map(|p| p3(p)).collect();
let last_f = *ctrl.last().unwrap_or(&elbow_pt);
let dist = ((last_f[0] - elbow_pt[0]).powi(2)
+ (last_f[1] - elbow_pt[1]).powi(2))
.sqrt();
let dist =
((last_f[0] - elbow_pt[0]).powi(2) + (last_f[1] - elbow_pt[1]).powi(2)).sqrt();
if dist > 1e-9 {
ctrl.push(elbow_pt);
@@ -454,20 +451,11 @@ fn text_box_geom(ml: &MultiLeader) -> ([f64; 2], [f64; 3]) {
};
(
[dir[0] * k, dir[1] * k],
[
tl.x + dir[0] * k * extent,
tl.y + dir[1] * k * extent,
tl.z,
],
[tl.x + dir[0] * k * extent, tl.y + dir[1] * k * extent, tl.z],
)
}
fn mleader_landing_geom(
ml: &MultiLeader,
) -> Option<(DVec3, DVec3, f64, f64, f64)> {
if ml.content_type != LeaderContentType::MText
|| !ml.enable_landing
|| !ml.enable_dogleg
{
fn mleader_landing_geom(ml: &MultiLeader) -> Option<(DVec3, DVec3, f64, f64, f64)> {
if ml.content_type != LeaderContentType::MText || !ml.enable_landing || !ml.enable_dogleg {
return None;
}
@@ -546,10 +534,7 @@ fn grips(ml: &MultiLeader) -> Vec<GripDef> {
for root in &ml.context.leader_roots {
for line in &root.lines {
for p in &line.points {
result.push(square_grip(
id,
DVec3::new(p.x, p.y, p.z),
));
result.push(square_grip(id, DVec3::new(p.x, p.y, p.z)));
id += 1;
}
@@ -557,35 +542,21 @@ fn grips(ml: &MultiLeader) -> Vec<GripDef> {
}
if ml.content_type == LeaderContentType::MText {
if let Some((elbow, _, _, _, _)) = mleader_landing_geom(ml)
{
result.push(square_grip(
id,
elbow,
));
if let Some((elbow, _, _, _, _)) = mleader_landing_geom(ml) {
result.push(square_grip(id, elbow));
id += 1;
}
let tl = &ml.context.text_location;
result.push(center_grip(
id,
DVec3::new(tl.x, tl.y, tl.z),
));
result.push(center_grip(id, DVec3::new(tl.x, tl.y, tl.z)));
id += 1;
let (_, far) = text_box_geom(ml);
result.push(triangle_grip(
id,
DVec3::new(
far[0],
far[1],
far[2],
),
));
result.push(triangle_grip(id, DVec3::new(far[0], far[1], far[2])));
}
result
@@ -595,26 +566,15 @@ fn grips(ml: &MultiLeader) -> Vec<GripDef> {
/// text grip's "Move with Leader" action so the leader follows the text.
pub(crate) const MOVE_ALL_GRIP: usize = usize::MAX;
fn apply_grip(
ml: &mut MultiLeader,
grip_id: usize,
apply: GripApply,
) {
fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
if grip_id == MOVE_ALL_GRIP {
let (dx, dy, dz) = match apply {
GripApply::Translate(d) => (
d.x as f64,
d.y as f64,
d.z as f64,
),
GripApply::Translate(d) => (d.x as f64, d.y as f64, d.z as f64),
GripApply::Absolute(a) => (
a.x as f64
- ml.context.text_location.x,
a.y as f64
- ml.context.text_location.y,
a.z as f64
- ml.context.text_location.z,
a.x as f64 - ml.context.text_location.x,
a.y as f64 - ml.context.text_location.y,
a.z as f64 - ml.context.text_location.z,
),
};
@@ -728,18 +688,13 @@ fn apply_grip(
);
let target = match apply {
GripApply::Absolute(a) => DVec3::new(a.x as f64, a.y as f64, a.z as f64),
GripApply::Translate(d) => {
old_end + DVec3::new(d.x as f64, d.y as f64, d.z as f64)
}
GripApply::Translate(d) => old_end + DVec3::new(d.x as f64, d.y as f64, d.z as f64),
};
if let Some((elbow, axis, sign, _, gap)) = landing {
let requested_total = (target - elbow).dot(axis) * sign;
let new_length = (requested_total - gap).max(1.0e-6);
set_mleader_text_location(
ml,
elbow + axis * (sign * (new_length + gap)),
);
set_mleader_text_location(ml, elbow + axis * (sign * (new_length + gap)));
ml.dogleg_length = new_length;
for root in &mut ml.context.leader_roots {
root.landing_distance = new_length;
@@ -756,15 +711,9 @@ fn apply_grip(
let (dir, far) = text_box_geom(ml);
let (nx, ny) = match apply {
GripApply::Absolute(a) => (
a.x as f64,
a.y as f64,
),
GripApply::Absolute(a) => (a.x as f64, a.y as f64),
GripApply::Translate(d) => (
far[0] + d.x as f64,
far[1] + d.y as f64,
),
GripApply::Translate(d) => (far[0] + d.x as f64, far[1] + d.y as f64),
};
let tl = &ml.context.text_location;
@@ -890,7 +839,11 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
!ml.enable_annotation_scale,
),
// Style name is resolved from style_handle by the panel builder (needs doc).
ro(t!("Multileader style").as_ref(), "mleader_style", "Standard"),
ro(
t!("Multileader style").as_ref(),
"mleader_style",
"Standard",
),
bool_toggle(
t!("Annotative").as_ref(),
"enable_annotation_scale",
@@ -903,46 +856,66 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
// Landing rows are folded in here: the standalone "Leader Structure" group
// is a style-dialog tab, not a palette group.
let mut leader_props = vec![
choice(
t!("Leader type").as_ref(),
"path_type",
leader_type_str(&ml.path_type),
&["Straight", "Spline", "None"],
),
Property {
label: t!("Leader color").into_owned(),
field: "line_color",
value: PropValue::ColorChoice(ml.line_color),
},
// Linetype name resolved from line_type_handle by the panel builder.
ro(t!("Leader linetype").as_ref(), "line_type_handle", "ByBlock"),
Property {
label: t!("Leader lineweight").into_owned(),
choice(
t!("Leader type").as_ref(),
"path_type",
leader_type_str(&ml.path_type),
&["Straight", "Spline", "None"],
),
Property {
label: t!("Leader color").into_owned(),
field: "line_color",
value: PropValue::ColorChoice(ml.line_color),
},
// Linetype name resolved from line_type_handle by the panel builder.
ro(
t!("Leader linetype").as_ref(),
"line_type_handle",
"ByBlock",
),
Property {
label: t!("Leader lineweight").into_owned(),
field: "line_weight",
value: PropValue::FieldLwChoice {
field: "line_weight",
value: PropValue::FieldLwChoice {
field: "line_weight",
value: ml.line_weight,
},
value: ml.line_weight,
},
// Arrowhead block name resolved by the panel builder (default "Closed filled").
ro(t!("Arrowhead").as_ref(), "arrowhead_handle", "Closed filled"),
edit(t!("Arrowhead Size").as_ref(), "arrowhead_size", ml.arrowhead_size),
bool_toggle(t!("Horizontal Landing").as_ref(), "enable_dogleg", ml.enable_dogleg),
num_row(
t!("Landing distance").as_ref(),
"landing_distance",
if ml.enable_dogleg { ml.dogleg_length } else { 0.0 },
ml.enable_dogleg,
),
];
},
// Arrowhead block name resolved by the panel builder (default "Closed filled").
ro(
t!("Arrowhead").as_ref(),
"arrowhead_handle",
"Closed filled",
),
edit(
t!("Arrowhead Size").as_ref(),
"arrowhead_size",
ml.arrowhead_size,
),
bool_toggle(
t!("Horizontal Landing").as_ref(),
"enable_dogleg",
ml.enable_dogleg,
),
num_row(
t!("Landing distance").as_ref(),
"landing_distance",
if ml.enable_dogleg {
ml.dogleg_length
} else {
0.0
},
ml.enable_dogleg,
),
];
// The extension is an MText-only landing option. It is absent for block
// content and whenever the horizontal landing itself is disabled.
if ml.content_type == LeaderContentType::MText && ml.enable_dogleg {
leader_props.push(bool_toggle(
t!("Leader extension").as_ref(),
"extend_leader_to_text",
ml.extend_leader_to_text,
));
t!("Leader extension").as_ref(),
"extend_leader_to_text",
ml.extend_leader_to_text,
));
}
let leaders = PropSection {
title: t!("Leaders").into_owned(),
@@ -974,8 +947,16 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
),
edit(t!("Width").as_ref(), "text_width", ctx.text_width),
edit(t!("Height").as_ref(), "text_height", ml.text_height),
edit_angle(t!("Rotation").as_ref(), "text_rotation", ctx.text_rotation.to_degrees()),
edit(t!("Line space factor").as_ref(), "line_spacing", ctx.line_spacing_factor),
edit_angle(
t!("Rotation").as_ref(),
"text_rotation",
ctx.text_rotation.to_degrees(),
),
edit(
t!("Line space factor").as_ref(),
"line_spacing",
ctx.line_spacing_factor,
),
edit(
t!("Line space distance").as_ref(),
"line_space_distance",
@@ -1038,7 +1019,8 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
),
],
};
text.props.splice(attachment_insert..attachment_insert, attachment_rows);
text.props
.splice(attachment_insert..attachment_insert, attachment_rows);
// ── Block ────────────────────────────────────────────────────────────
let block = PropSection {
@@ -1130,9 +1112,8 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
for root in &mut ml.context.leader_roots {
for line in &mut root.lines {
line.path_type = ml.path_type;
line.override_flags.insert(
codec::entities::LeaderLinePropertyOverrideFlags::PATH_TYPE,
);
line.override_flags
.insert(codec::entities::LeaderLinePropertyOverrideFlags::PATH_TYPE);
}
}
ml.property_override_flags
@@ -1554,14 +1535,8 @@ impl crate::entities::traits::Grippable for MultiLeader {
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
apply_grip(self, grip_id, apply);
}
fn grip_menu(
&self,
grip_id: usize,
) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{
GripMenuAction,
GripMenuItem,
};
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
let n_vertices: usize = self
.context
@@ -1571,42 +1546,27 @@ impl crate::entities::traits::Grippable for MultiLeader {
.map(|line| line.points.len())
.sum();
if self.content_type
== LeaderContentType::MText
{
let has_elbow =
mleader_landing_geom(self).is_some();
if self.content_type == LeaderContentType::MText {
let has_elbow = mleader_landing_geom(self).is_some();
let elbow_id =
n_vertices;
let elbow_id = n_vertices;
let landing_id =
n_vertices
+ usize::from(has_elbow);
let landing_id = n_vertices + usize::from(has_elbow);
let width_id =
landing_id + 1;
let width_id = landing_id + 1;
if has_elbow
&& grip_id == elbow_id
{
return vec![
GripMenuItem {
label: "Stretch",
action:
GripMenuAction::Stretch,
},
];
if has_elbow && grip_id == elbow_id {
return vec![GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
}];
}
if grip_id == landing_id {
return vec![
GripMenuItem {
label: "Stretch",
action:
GripMenuAction::Stretch,
},
];
return vec![GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
}];
}
if grip_id == width_id {
@@ -1617,22 +1577,23 @@ impl crate::entities::traits::Grippable for MultiLeader {
vec![
GripMenuItem {
label: "Stretch",
action:
GripMenuAction::Stretch,
action: GripMenuAction::Stretch,
},
GripMenuItem {
label: "Add Leader",
action:
GripMenuAction::AddLeader,
action: GripMenuAction::AddLeader,
},
GripMenuItem {
label: "Remove Leader",
action:
GripMenuAction::RemoveLeader,
action: GripMenuAction::RemoveLeader,
},
]
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
// Locate the (root, line) and vertex position owning this grip id.
let mut idx = 0usize;
@@ -1732,8 +1693,7 @@ pub(crate) fn block_content_insert(
let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64;
let local_x = (anchor_x - record.base_point.x) * ml.block_scale.x;
let local_y = (anchor_y - record.base_point.y) * ml.block_scale.y;
let rotated =
Transform::rotation(ml.block_rotation).apply_vector([local_x, local_y]);
let rotated = Transform::rotation(ml.block_rotation).apply_vector([local_x, local_y]);
insertion.x -= rotated[0];
insertion.y -= rotated[1];
}
@@ -1820,9 +1780,11 @@ impl MultiLeaderTess for MultiLeader {
.find(|lt| lt.handle == h)
.map(|lt| lt.name.clone());
match name {
Some(n) => {
crate::scene::view::render::resolve_pattern(&document.line_types, &n, lt_scale)
}
Some(n) => crate::scene::view::render::resolve_pattern(
&document.line_types,
&n,
lt_scale,
),
None => (0.0, [0.0; 8]),
}
}
@@ -1831,14 +1793,12 @@ impl MultiLeaderTess for MultiLeader {
let name = handle.value().to_string();
let nan = [f32::NAN; 3];
let p3 = |v: &codec::types::Vector3| -> [f32; 3] {
[(v.x) as f32, (v.y) as f32, (v.z) as f32]
};
let p3 =
|v: &codec::types::Vector3| -> [f32; 3] { [(v.x) as f32, (v.y) as f32, (v.z) as f32] };
// ── Scaling ──────────────────────────────────────────────────────────────
// Used only when a context omits an already-resolved content size.
let annotative =
crate::scene::annotative::mleader_is_annotative(document, ml);
let annotative = crate::scene::annotative::mleader_is_annotative(document, ml);
// Context sizes are resolved at their stored annotation scale.
let base_scale = if ml.scale_factor.abs() > 1.0e-12 {
@@ -1849,28 +1809,23 @@ impl MultiLeaderTess for MultiLeader {
let stored_context_scale = ml.context.scale_factor as f32;
let context_scale_correction =
if annotative && stored_context_scale.abs() > 1.0e-12 {
let correction =
(base_scale * anno_scale) / stored_context_scale;
let context_scale_correction = if annotative && stored_context_scale.abs() > 1.0e-12 {
let correction = (base_scale * anno_scale) / stored_context_scale;
if correction.is_finite() && correction > 0.0 {
correction
} else {
1.0
}
if correction.is_finite() && correction > 0.0 {
correction
} else {
1.0
};
}
} else {
1.0
};
let fallback_content_scale =
base_scale * if annotative { anno_scale } else { 1.0 };
let fallback_content_scale = base_scale * if annotative { anno_scale } else { 1.0 };
// The active context stores the resolved world-space arrow size.
// Reapplying the entity scale here makes context-sized arrows grow twice.
let arrow_size =
ml.context.arrowhead_size as f32
* context_scale_correction;
let arrow_size = ml.context.arrowhead_size as f32 * context_scale_correction;
let draw_arrow = arrow_size > 0.0;
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
// arrowhead_handle resolves through the block records to a named arrow
@@ -1898,18 +1853,14 @@ impl MultiLeaderTess for MultiLeader {
let mut text_loc_w = ml.context.text_location;
// Scale the annotation-side offset around the fixed elbow.
if annotative
&& (context_scale_correction - 1.0).abs() > 1.0e-6
{
if annotative && (context_scale_correction - 1.0).abs() > 1.0e-6 {
if let Some(root) = ml.context.leader_roots.first() {
let anchor = root.connection_point;
let k = context_scale_correction as f64;
text_loc_w = codec::types::Vector3::new(
anchor.x
+ (text_loc_w.x - anchor.x) * k,
anchor.y
+ (text_loc_w.y - anchor.y) * k,
anchor.x + (text_loc_w.x - anchor.x) * k,
anchor.y + (text_loc_w.y - anchor.y) * k,
text_loc_w.z,
);
}
@@ -2025,9 +1976,7 @@ impl MultiLeaderTess for MultiLeader {
// stray line up the side of the text.
// Landing distance belongs to the selected leader-root context
// and is already resolved in world units.
let d =
root.landing_distance
* context_scale_correction as f64;
let d = root.landing_distance * context_scale_correction as f64;
// The dogleg runs along the leader root's stored direction —
// for a rotated leader that is the angled baseline, not world
// X. Roots without a usable direction keep the legacy
@@ -2039,17 +1988,12 @@ impl MultiLeaderTess for MultiLeader {
(x / l, y / l)
})
.unwrap_or((text_sign_w, 0.0));
let landing_end = [
(cp.x + ux * d) as f32,
(cp.y + uy * d) as f32,
cp_f[2],
];
let landing_end = [(cp.x + ux * d) as f32, (cp.y + uy * d) as f32, cp_f[2]];
points.push(nan);
points.push(cp_f);
points.push(landing_end);
if ml.extend_leader_to_text {
let gap = ml.context.landing_gap.max(0.0)
* context_scale_correction as f64;
let gap = ml.context.landing_gap.max(0.0) * context_scale_correction as f64;
points.push([
(text_loc_w.x - ux * gap) as f32,
(text_loc_w.y - uy * gap) as f32,
@@ -2102,17 +2046,16 @@ impl MultiLeaderTess for MultiLeader {
// error at UTM scale) — not a crash. Follow-up: double-single-split
// via points_to_ds to match emit_wire's paired fill path.
fill_tris_low: Vec::new(),
..Default::default()
});
});
let host = codec::EntityType::MultiLeader(Box::new(ml.clone()));
for block_use in crate::scene::render_graph::entity_render_block_uses(document, &host, 1.0)
.into_iter()
.filter(|block_use| {
block_use.active
&& block_use.role
== crate::scene::render_graph::BlockRole::MultiLeaderContent
&& block_use.role == crate::scene::render_graph::BlockRole::MultiLeaderContent
})
{
wires.extend(
@@ -2152,11 +2095,9 @@ impl MultiLeaderTess for MultiLeader {
// Only the fallback path (file omits the context value) needs
// scale_factor + annotation scale applied.
let height = if ctx.text_height > 0.0 {
ctx.text_height as f32
* context_scale_correction
ctx.text_height as f32 * context_scale_correction
} else {
ml.text_height as f32
* fallback_content_scale
ml.text_height as f32 * fallback_content_scale
};
let ins = &text_loc_w;
@@ -2217,9 +2158,7 @@ impl MultiLeaderTess for MultiLeader {
// The stored text attachment point is the insertion's horizontal
// anchor within the text block (Left/Center/Right) — honour it
// instead of guessing from the leader side.
use codec::entities::multileader::{
TextAttachmentDirectionType,
};
use codec::entities::multileader::TextAttachmentDirectionType;
// The context's attachment point exists in every DWG version;
// the entity-level copy only exists from R2010 on.
let h_anchor: f32 = match ctx.text_attachment_point {
@@ -2247,9 +2186,7 @@ impl MultiLeaderTess for MultiLeader {
value: &ctx.text_string,
insertion: [local_ins_x as f64, local_ins_y as f64, z as f64],
height,
rect_w:
ctx.text_width as f32
* context_scale_correction,
rect_w: ctx.text_width as f32 * context_scale_correction,
rotation: rot,
style: &style,
attach_h_anchor: h_anchor,
@@ -2301,10 +2238,9 @@ impl MultiLeaderTess for MultiLeader {
Vec::new();
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
for ts in &layout.strokes {
let is_shaped = ts
.run
.as_ref()
.is_some_and(|r| crate::scene::text::web_font::requires_shaping(&r.text));
let is_shaped = ts.run.as_ref().is_some_and(|r| {
crate::scene::text::web_font::requires_shaping(&r.text)
});
if ts.run.is_none() || is_shaped {
for stroke in &ts.strokes {
if stroke.len() < 2 {
@@ -2331,18 +2267,17 @@ impl MultiLeaderTess for MultiLeader {
continue;
}
let run = ts.run.as_ref().unwrap();
let (quads, pen_adv) =
crate::scene::text::glyph_quads::layout_glyph_quads(
&mut atlas,
run.height,
run.rotation,
run.width_factor,
run.oblique,
run.tracking,
&run.font,
run.bold,
&run.text,
);
let (quads, pen_adv) = crate::scene::text::glyph_quads::layout_glyph_quads(
&mut atlas,
run.height,
run.rotation,
run.width_factor,
run.oblique,
run.tracking,
&run.font,
run.bold,
&run.text,
);
// Inline `\C` / `\c` colour wins over the entity text
// colour, matching the top-level Text arm.
let gcolor = ts
@@ -2379,8 +2314,7 @@ impl MultiLeaderTess for MultiLeader {
}
if !sdf_verts.is_empty() || !deco_pts.is_empty() || !deco_fill.is_empty() {
// Pick box from the glyph quads (f64 accumulate → f32).
let (mut nx, mut ny, mut xx, mut xy) =
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
let (mut nx, mut ny, mut xx, mut xy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for v in &sdf_verts {
let x = v.pos[0] as f64 + v.pos_low[0] as f64;
let y = v.pos[1] as f64 + v.pos_low[1] as f64;
@@ -2450,9 +2384,7 @@ impl MultiLeaderTess for MultiLeader {
use codec::entities::multileader::TextAttachmentType as TA;
let ul_lines: Option<Vec<usize>> = match vertical_attach {
TA::BottomOfTopLineUnderlineTopLine => Some(vec![0]),
TA::BottomOfTopLineUnderlineAll => {
Some((0..layout.line_count).collect())
}
TA::BottomOfTopLineUnderlineAll => Some((0..layout.line_count).collect()),
TA::BottomLine | TA::BottomOfTopLineUnderlineBottomLine => {
Some(vec![layout.line_count.saturating_sub(1)])
}
@@ -2460,8 +2392,7 @@ impl MultiLeaderTess for MultiLeader {
};
if let Some(lines) = ul_lines {
let mut pts: Vec<[f32; 3]> = Vec::new();
let (x0, x1) =
(-max_line_w * h_anchor, max_line_w * (1.0 - h_anchor));
let (x0, x1) = (-max_line_w * h_anchor, max_line_w * (1.0 - h_anchor));
for li in lines {
let y = v_offset - li as f32 * line_h - height * 0.15;
let (ax, ay) = (
@@ -2512,17 +2443,16 @@ impl MultiLeaderTess for MultiLeader {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
..Default::default()
});
});
}
}
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
if ml.text_frame || ctx.background_fill_enabled {
// Border offset is a scale factor around the laid-out text box.
let pad = height
* ((ctx.background_scale_factor.max(1.0) as f32 - 1.0) * 0.5);
let pad = height * ((ctx.background_scale_factor.max(1.0) as f32 - 1.0) * 0.5);
// Box the glyphs that were actually laid out (valid for
// vertical flow too); the metric-derived box is only the
// no-glyph fallback.
@@ -2560,9 +2490,9 @@ impl MultiLeaderTess for MultiLeader {
} else {
color_or_inherit(&ctx.background_fill_color, entity_color)
};
fill_color[3] *=
(1.0 - ctx.background_transparency.clamp(0, 90) as f32 / 100.0)
.clamp(0.1, 1.0);
fill_color[3] *= (1.0
- ctx.background_transparency.clamp(0, 90) as f32 / 100.0)
.clamp(0.1, 1.0);
let fill_tris: Vec<[f32; 3]> = vec![
wcs_corners[0],
wcs_corners[1],
@@ -2586,9 +2516,9 @@ impl MultiLeaderTess for MultiLeader {
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: name.clone(),
points: vec![],
points_low: Vec::new(),
@@ -2605,9 +2535,9 @@ impl MultiLeaderTess for MultiLeader {
plinegen: true,
fill_tris,
fill_tris_low: Vec::new(),
..Default::default()
});
..Default::default()
});
}
// Text frame — closed rectangle, matches text color.
@@ -2634,9 +2564,9 @@ impl MultiLeaderTess for MultiLeader {
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name,
points: frame_points,
points_low: Vec::new(),
@@ -2653,9 +2583,9 @@ impl MultiLeaderTess for MultiLeader {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
..Default::default()
});
..Default::default()
});
}
}
}
+18 -4
View File
@@ -14,9 +14,17 @@ pub fn ui_name(e: &EntityType) -> &'static str {
match e {
EntityType::Point(_) => "Point",
EntityType::Line(line)
if codec::entities::CenterMarkAssociation::read(&line.common.extended_data).is_some() => "Center Mark",
if codec::entities::CenterMarkAssociation::read(&line.common.extended_data)
.is_some() =>
{
"Center Mark"
}
EntityType::Line(line)
if codec::entities::CenterLineAssociation::read(&line.common.extended_data).is_some() => "Center Line",
if codec::entities::CenterLineAssociation::read(&line.common.extended_data)
.is_some() =>
{
"Center Line"
}
EntityType::Line(_) => "Line",
EntityType::Circle(_) => "Circle",
EntityType::Arc(_) => "Arc",
@@ -70,7 +78,10 @@ pub fn ui_name(e: &EntityType) -> &'static str {
if matches!(
extended.data,
codec::entities::ExtendedEntityData::SectionObject(_)
) => "Section Plane",
) =>
{
"Section Plane"
}
EntityType::Extended(_) => "Extended Entity",
EntityType::Seqend(_) => "Seqend",
EntityType::Unknown(_) => "Unknown",
@@ -170,7 +181,10 @@ pub fn dxf_name(e: &EntityType) -> &'static str {
if matches!(
extended.data,
codec::entities::ExtendedEntityData::SectionObject(_)
) => "SECTIONOBJECT",
) =>
{
"SECTIONOBJECT"
}
_ => "ENTITY",
}
}
+14 -40
View File
@@ -6,8 +6,8 @@
//! entity geometry continues through the normal render pipeline.
use codec::objects::{
AssociativeData, BlockEvalValue, DynamicBlockData, ObjectType,
SolidHistoryNodeBase, SolidHistoryOperation,
AssociativeData, BlockEvalValue, DynamicBlockData, ObjectType, SolidHistoryNodeBase,
SolidHistoryOperation,
};
use codec::{CadDocument, EntityType, Handle};
@@ -107,11 +107,7 @@ pub fn render_settings(cache: &ObjectDataCache) -> &[Handle] {
&cache.render_settings
}
pub fn update_light_entity(
cache: &mut ObjectDataCache,
handle: Handle,
exists: bool,
) {
pub fn update_light_entity(cache: &mut ObjectDataCache, handle: Handle, exists: bool) {
let lights = std::sync::Arc::make_mut(&mut cache.light_entities);
if exists {
if !lights.contains(&handle) {
@@ -361,8 +357,7 @@ fn solid_history_sections(
let Some(history_handle) = entity_history_handle(entity) else {
return Vec::new();
};
let Some(ObjectType::DynamicBlock(history_object)) =
document.objects.get(&history_handle)
let Some(ObjectType::DynamicBlock(history_object)) = document.objects.get(&history_handle)
else {
return vec![PropSection {
title: "Solid History".to_string(),
@@ -771,8 +766,7 @@ fn dynamic_block_sections(
.filter(|object| {
!matches!(
object.data,
DynamicBlockData::SolidHistory(_)
| DynamicBlockData::SolidHistoryNode(_)
DynamicBlockData::SolidHistory(_) | DynamicBlockData::SolidHistoryNode(_)
)
})
.map(|object| {
@@ -1028,9 +1022,7 @@ where
result
}
fn class_object_section(
data: &codec::objects::ClassObjectData,
) -> Option<PropSection> {
fn class_object_section(data: &codec::objects::ClassObjectData) -> Option<PropSection> {
use codec::objects::ClassObjectData;
let section = match data {
@@ -2058,9 +2050,7 @@ fn class_object_section(
Some(section)
}
fn semantic_property_text(
value: &codec::objects::SemanticPropertyValue,
) -> String {
fn semantic_property_text(value: &codec::objects::SemanticPropertyValue) -> String {
use codec::objects::SemanticPropertyValue;
match value {
SemanticPropertyValue::Text(value) => preview_text(value, 512),
@@ -2075,9 +2065,7 @@ fn semantic_property_text(
}
}
fn auxiliary_object_property(
object: &ObjectType,
) -> Option<Property> {
fn auxiliary_object_property(object: &ObjectType) -> Option<Property> {
use codec::objects::DataObjectData;
let (label, value) = match object {
@@ -2275,9 +2263,7 @@ fn build_document_sections(document: &CadDocument) -> Vec<PropSection> {
"Design Point",
format!(
"{:.8}, {:.8}, {:.8}",
value.design_point.x,
value.design_point.y,
value.design_point.z
value.design_point.x, value.design_point.y, value.design_point.z
),
),
ro(
@@ -2300,9 +2286,7 @@ fn build_document_sections(document: &CadDocument) -> Vec<PropSection> {
"Up",
format!(
"{:.8}, {:.8}, {:.8}",
value.up_direction.x,
value.up_direction.y,
value.up_direction.z
value.up_direction.x, value.up_direction.y, value.up_direction.z
),
),
ro(
@@ -2380,11 +2364,7 @@ fn build_document_sections(document: &CadDocument) -> Vec<PropSection> {
"Entries",
bounded_join(
list.lights.iter().map(|light| {
format!(
"{}={}",
light.name,
handle_text(light.handle)
)
format!("{}={}", light.name, handle_text(light.handle))
}),
64,
),
@@ -2404,9 +2384,7 @@ fn build_document_sections(document: &CadDocument) -> Vec<PropSection> {
"Date / Time",
format!(
"Julian {}; {} ms; DST {}",
sun.julian_day,
sun.milliseconds,
sun.is_daylight_savings_on
sun.julian_day, sun.milliseconds, sun.is_daylight_savings_on
),
),
ro(
@@ -2651,8 +2629,7 @@ fn build_document_sections(document: &CadDocument) -> Vec<PropSection> {
"Display",
format!(
"predefined first {}; high-level info {}",
global.predefined_presets_first,
global.high_level_info
global.predefined_presets_first, global.high_level_info
),
),
],
@@ -2669,10 +2646,7 @@ fn build_document_sections(document: &CadDocument) -> Vec<PropSection> {
}
if !document.dgn_ls_definitions.is_empty() {
let mut definitions = document
.dgn_ls_definitions
.values()
.collect::<Vec<_>>();
let mut definitions = document.dgn_ls_definitions.values().collect::<Vec<_>>();
definitions.sort_by_key(|definition| definition.handle.value());
let omitted = definitions.len().saturating_sub(128);
let mut props = definitions
+11 -7
View File
@@ -3,13 +3,13 @@ use codec::EntityType;
use kernel::geom2d::{Circle, Curve, Line};
use kernel::space::{curve::bezier_points, PlanarCurve, Plane, Vec3};
use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, parse_f64, square_grip};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::{PointMarker, SnapHint};
use crate::t;
/// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE
/// is handled by [`relative_render`].
@@ -138,8 +138,8 @@ fn point_plane(pt: &Point) -> Plane {
} else {
Vec3::Z.cross(normal)
};
let base = Plane::orthonormal(origin, x_seed.to_array(), normal.to_array())
.unwrap_or(Plane::XY);
let base =
Plane::orthonormal(origin, x_seed.to_array(), normal.to_array()).unwrap_or(Plane::XY);
let (sin, cos) = pt.x_axis_angle.sin_cos();
Plane::from_axes(
origin,
@@ -203,15 +203,16 @@ fn point_glyph(pt: &Point, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> {
let nan = [f64::NAN; 3];
let mut paths: Vec<Vec<[f64; 3]>> = curves
.iter()
.map(|curve| PlanarCurve::new(plane, curve.clone()).tessellate(64.0 / std::f64::consts::TAU))
.map(|curve| {
PlanarCurve::new(plane, curve.clone()).tessellate(64.0 / std::f64::consts::TAU)
})
.collect();
if pt.thickness.abs() > 1.0e-10 && !curves.is_empty() {
let normal = Vec3::from(plane.normal().unwrap_or([0.0, 0.0, 1.0]));
let top_origin = (Vec3::from(plane.origin) + normal * pt.thickness).to_array();
let top_plane = Plane::from_axes(top_origin, plane.x_axis, plane.y_axis);
paths.extend(curves.iter().map(|curve| {
PlanarCurve::new(top_plane, curve.clone())
.tessellate(64.0 / std::f64::consts::TAU)
PlanarCurve::new(top_plane, curve.clone()).tessellate(64.0 / std::f64::consts::TAU)
}));
paths.push(bezier_points(&[plane.origin, top_origin], 1));
}
@@ -346,7 +347,10 @@ mod tests {
doc.header.point_display_size = -5.0;
let drawn = relative_render(&EntityType::Point(point_on("0")), &doc, Some(0.01));
assert!(drawn.is_some(), "a user point still takes the viewport-aware path");
assert!(
drawn.is_some(),
"a user point still takes the viewport-aware path"
);
let defpoint = relative_render(
&EntityType::Point(point_on("xref|Defpoints")),
+121 -90
View File
@@ -1,12 +1,12 @@
use codec::entities::{Polyline, Polyline2D, Polyline3D};
use crate::t;
use codec::entities::{Polyline, Polyline2D, Polyline3D};
use crate::command::EntityTransform;
use crate::entities::common::{
edit_prop as edit, format_area, format_length, parse_f64, ro_prop as ro, square_grip,
stepper_prop as stepper, VARIES_LABEL,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::TangentGeom;
@@ -92,7 +92,11 @@ impl Grippable for Polyline {
]
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
let n = self.vertices.len();
match action {
@@ -127,7 +131,11 @@ impl PropertyEditable for Polyline {
vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
ro(t!("Vertices").as_ref(), "vertices", self.vertices.len().to_string()),
ro(
t!("Vertices").as_ref(),
"vertices",
self.vertices.len().to_string(),
),
Property {
label: t!("Closed").into_owned(),
field: "pl_closed",
@@ -154,16 +162,20 @@ impl PropertyEditable for Polyline {
impl Transformable for Polyline {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
},
);
}
}
@@ -176,14 +188,10 @@ impl Transformable for Polyline {
/// spokes across the fitted curve (#408). `None` = no fit data, draw the
/// stored vertices as-is. A fit-flagged polyline whose fit points are missing
/// falls back to the stored vertices rather than drawing nothing.
pub fn drawn_vertices2d(
pl: &Polyline2D,
) -> Option<Vec<codec::entities::Vertex2D>> {
pub fn drawn_vertices2d(pl: &Polyline2D) -> Option<Vec<codec::entities::Vertex2D>> {
use codec::entities::polyline::VertexFlags;
let has_fit = pl.vertices.iter().any(|v| {
v.flags.bits()
& (VertexFlags::SPLINE_VERTEX.bits() | VertexFlags::EXTRA_VERTEX.bits())
!= 0
v.flags.bits() & (VertexFlags::SPLINE_VERTEX.bits() | VertexFlags::EXTRA_VERTEX.bits()) != 0
});
if !has_fit {
return None;
@@ -227,9 +235,8 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
};
if !fill_mode {
let continuous = pl.flags.bits()
& codec::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits()
!= 0;
let continuous =
pl.flags.bits() & codec::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits() != 0;
let mut boundary = crate::entities::common::wide_band_outline(
&band_verts_2d(pl),
pl.is_closed(),
@@ -296,7 +303,9 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
} else if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
{
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(crate::entities::common::bulge_arc_to_tangent(
&arc, &to_wcs, normal,
));
for s in arc
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
.into_iter()
@@ -374,7 +383,9 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
[v1.location.x, v1.location.y],
bulge,
) {
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tangents.push(crate::entities::common::bulge_arc_to_tangent(
&arc, &to_wcs, normal,
));
}
if i == 0 {
@@ -389,11 +400,8 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
// uniform-width one keeps the constant-band Contour.
let object = match tapered_band_verts_2d(&band_verts) {
Some(band_verts) => {
let (pts, widths) = crate::entities::common::tapered_band_points(
band_verts,
pl.is_closed(),
&to_wcs,
);
let (pts, widths) =
crate::entities::common::tapered_band_points(band_verts, pl.is_closed(), &to_wcs);
RenderObject::TaperedLines(pts, widths)
}
None => RenderObject::Lines(
@@ -413,9 +421,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
}
/// Effective segment widths for a Polyline2D band.
fn band_verts_2d(
pl: &codec::entities::Polyline2D,
) -> Vec<([f64; 2], f64, f64, f64)> {
fn band_verts_2d(pl: &codec::entities::Polyline2D) -> Vec<([f64; 2], f64, f64, f64)> {
let default_start = pl.start_width;
let default_end = pl.end_width;
let filtered = drawn_vertices2d(pl);
@@ -481,7 +487,9 @@ fn centerline_metadata_2d(
[end.location.x, end.location.y],
start.bulge,
) {
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
tangents.push(crate::entities::common::bulge_arc_to_tangent(
&arc, to_wcs, normal,
));
}
if index == 0 {
key_vertices.push([p0.0, p0.1, p0.2]);
@@ -497,7 +505,9 @@ impl RenderConvertible for Polyline2D {
// Arc segments offer their centres and quadrants.
if self.vertices.iter().any(|v| v.bulge != 0.0) {
let entity = codec::EntityType::Polyline2D(self.clone());
render.snap_pts.extend(crate::entities::curve::arc_segment_snaps(&entity));
render
.snap_pts
.extend(crate::entities::curve::arc_segment_snaps(&entity));
}
Some(render)
}
@@ -546,7 +556,11 @@ impl Grippable for Polyline2D {
]
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
let n = self.vertices.len();
let elev = self.elevation;
@@ -629,8 +643,7 @@ pub(crate) fn polyline2d_vertex_segment_widths(
pub(crate) fn polyline2d_global_width(pline: &Polyline2D) -> Option<f64> {
let filtered = drawn_vertices2d(pline);
let verts: &[codec::entities::Vertex2D] =
filtered.as_deref().unwrap_or(&pline.vertices);
let verts: &[codec::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pline.vertices);
let count = verts.len();
let seg_count = if pline.is_closed() {
count
@@ -665,7 +678,11 @@ impl PropertyEditable for Polyline2D {
let n = self.vertices.len();
let mut area = 0.0;
let mut length = 0.0;
let seg_count = if self.is_closed() { n } else { n.saturating_sub(1) };
let seg_count = if self.is_closed() {
n
} else {
n.saturating_sub(1)
};
for i in 0..seg_count {
let a = &self.vertices[i].location;
let b = &self.vertices[(i + 1) % n].location;
@@ -704,10 +721,18 @@ impl PropertyEditable for Polyline2D {
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
stepper(t!("Current Vertex").as_ref(), "pl2_current_vertex", vertex_label),
stepper(
t!("Current Vertex").as_ref(),
"pl2_current_vertex",
vertex_label,
),
edit(t!("Vertex X").as_ref(), "pl2_vertex_x", vertex_x),
edit(t!("Vertex Y").as_ref(), "pl2_vertex_y", vertex_y),
edit(t!("Start segment width").as_ref(), "pl2_seg_start_w", seg_start_w),
edit(
t!("Start segment width").as_ref(),
"pl2_seg_start_w",
seg_start_w,
),
edit(t!("End segment width").as_ref(), "pl2_seg_end_w", seg_end_w),
global_width_prop,
edit(t!("Elevation").as_ref(), "pl2_elevation", self.elevation),
@@ -849,19 +874,23 @@ impl PropertyEditable for Polyline2D {
impl Transformable for Polyline2D {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
// Bulge encodes which side the arc bows to; a reflection
// reverses it or every curved segment flips to the wrong side.
v.bulge = -v.bulge;
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
// Bulge encodes which side the arc bows to; a reflection
// reverses it or every curved segment flips to the wrong side.
v.bulge = -v.bulge;
}
},
);
}
}
@@ -891,9 +920,7 @@ fn polyline3d_control_indices(pl: &Polyline3D) -> Vec<usize> {
.vertices
.iter()
.enumerate()
.filter_map(|(index, vertex)| {
(vertex.flags & POLY3D_SPLINE_POINT == 0).then_some(index)
})
.filter_map(|(index, vertex)| (vertex.flags & POLY3D_SPLINE_POINT == 0).then_some(index))
.collect();
if controls.is_empty() {
(0..pl.vertices.len()).collect()
@@ -937,8 +964,7 @@ fn polyline3d_length(pl: &Polyline3D) -> f64 {
.map(|pair| Vec3::from(pair[0]).distance(Vec3::from(pair[1])))
.sum::<f64>();
if pl.is_closed() && points.len() >= 2 {
length += Vec3::from(points[0])
.distance(Vec3::from(points[points.len() - 1]));
length += Vec3::from(points[0]).distance(Vec3::from(points[points.len() - 1]));
}
length
}
@@ -1015,9 +1041,7 @@ fn rebuild_polyline3d_fit(pl: &mut Polyline3D) -> bool {
.map(|vertex| vertex.layer.clone())
.unwrap_or_else(|| "0".to_string());
let curve_vertices = samples.into_iter().map(|point| {
let mut vertex = codec::entities::Vertex3DPolyline::from_xyz(
point[0], point[1], point[2],
);
let mut vertex = codec::entities::Vertex3DPolyline::from_xyz(point[0], point[1], point[2]);
vertex.layer = vertex_layer.clone();
vertex.flags = POLY3D_VERTEX | POLY3D_SPLINE_POINT;
vertex
@@ -1109,9 +1133,7 @@ impl Grippable for Polyline3D {
v.position.z = p.z as f64;
}
}
if self.smooth_type
!= codec::entities::polyline3d::SmoothSurfaceType::None
{
if self.smooth_type != codec::entities::polyline3d::SmoothSurfaceType::None {
let _ = rebuild_polyline3d_fit(self);
}
}
@@ -1146,7 +1168,11 @@ impl Grippable for Polyline3D {
items
}
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
use codec::entities::polyline3d::SmoothSurfaceType as SST;
@@ -1159,10 +1185,8 @@ impl Grippable for Polyline3D {
if n >= 2 {
let previous = &controls[n - 2].position;
let last = &controls[n - 1].position;
let next = Vec3::new(previous.x, previous.y, previous.z).lerp(
Vec3::new(last.x, last.y, last.z),
2.0,
);
let next = Vec3::new(previous.x, previous.y, previous.z)
.lerp(Vec3::new(last.x, last.y, last.z), 2.0);
new_v.position.x = next.x;
new_v.position.y = next.y;
new_v.position.z = next.z;
@@ -1173,10 +1197,8 @@ impl Grippable for Polyline3D {
let i1 = (grip_id + 1) % n;
let v0 = &controls[grip_id];
let v1 = &controls[i1];
let midpoint = Vec3::new(v0.position.x, v0.position.y, v0.position.z).lerp(
Vec3::new(v1.position.x, v1.position.y, v1.position.z),
0.5,
);
let midpoint = Vec3::new(v0.position.x, v0.position.y, v0.position.z)
.lerp(Vec3::new(v1.position.x, v1.position.y, v1.position.z), 0.5);
let mut new_v = v0.clone();
new_v.position.x = midpoint.x;
new_v.position.y = midpoint.y;
@@ -1302,9 +1324,7 @@ impl PropertyEditable for Polyline3D {
} else {
self.open();
}
if self.smooth_type
!= codec::entities::polyline3d::SmoothSurfaceType::None
{
if self.smooth_type != codec::entities::polyline3d::SmoothSurfaceType::None {
let _ = rebuild_polyline3d_fit(self);
}
}
@@ -1362,16 +1382,20 @@ impl PropertyEditable for Polyline3D {
impl Transformable for Polyline3D {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.position.x,
&mut v.position.y,
p1,
p2,
);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.position.x,
&mut v.position.y,
p1,
p2,
);
}
},
);
}
}
/// Generate solid-fill boundary polygons for each wide segment of a Polyline2D.
@@ -1437,7 +1461,8 @@ mod tests {
pl.start_width = start_w;
pl.end_width = end_w;
for i in 0..count {
pl.vertices.push(Vertex2D::new(Vector3::new(i as f64 * 10.0, 0.0, 0.0)));
pl.vertices
.push(Vertex2D::new(Vector3::new(i as f64 * 10.0, 0.0, 0.0)));
}
pl
}
@@ -1449,7 +1474,10 @@ mod tests {
let props = pl.geometry_properties(&[]);
let geom_props = &props[0].props;
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
let gw = geom_props
.iter()
.find(|p| p.field == "pl2_start_w")
.unwrap();
match &gw.value {
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(4.0)),
_ => panic!("expected EditText"),
@@ -1466,7 +1494,10 @@ mod tests {
let props = pl.geometry_properties(&[]);
let geom_props = &props[0].props;
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
let gw = geom_props
.iter()
.find(|p| p.field == "pl2_start_w")
.unwrap();
match &gw.value {
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
_ => panic!("expected EditText with VARIES_LABEL"),
+117 -73
View File
@@ -1,5 +1,5 @@
use codec::entities::{RasterImage, Wipeout};
use crate::t;
use codec::entities::{RasterImage, Wipeout};
use crate::command::EntityTransform;
use crate::entities::common::{
@@ -7,8 +7,8 @@ use crate::entities::common::{
ro_prop as ro, square_grip,
};
use crate::entities::text_support::{resolve_text_style, text_local_bounds};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{GlyphRun, RenderEntity, RenderObject, TextStroke};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
use crate::scene::text::lff;
@@ -103,7 +103,10 @@ pub(crate) fn image_world_to_clip(img: &RasterImage, p: [f64; 3]) -> codec::type
// Solve d = a·u + b·v in the image plane (x/y components).
let det = u.x * v.y - u.y * v.x;
let (a, b) = if det.abs() > 1e-300 {
((d[0] * v.y - d[1] * v.x) / det, (u.x * d[1] - u.y * d[0]) / det)
(
(d[0] * v.y - d[1] * v.x) / det,
(u.x * d[1] - u.y * d[0]) / det,
)
} else {
(0.0, 0.0)
};
@@ -152,7 +155,8 @@ impl RenderConvertible for RasterImage {
// triangulation in `ImageModel` so outline and pixels align.
let pts = match image_clip_polygon(self) {
Some(poly) => {
let mut ring: Vec<[f64; 3]> = poly.iter().map(|p| px_to_world(p[0], p[1])).collect();
let mut ring: Vec<[f64; 3]> =
poly.iter().map(|p| px_to_world(p[0], p[1])).collect();
ring.push(ring[0]);
if self.clip_boundary.clip_mode == codec::entities::ClipMode::Inside {
let mut frame = image_wire(corners, false);
@@ -321,7 +325,9 @@ impl PropertyEditable for RasterImage {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let rotation_deg = self.u_vector.y.atan2(self.u_vector.x).to_degrees();
let scale = self.u_vector.length();
let show_image = self.flags.contains(codec::entities::ImageDisplayFlags::SHOW_IMAGE);
let show_image = self
.flags
.contains(codec::entities::ImageDisplayFlags::SHOW_IMAGE);
let show_clipped = self
.flags
.contains(codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY);
@@ -334,9 +340,21 @@ impl PropertyEditable for RasterImage {
edit(t!("Position X").as_ref(), "ri_ox", self.insertion_point.x),
edit(t!("Position Y").as_ref(), "ri_oy", self.insertion_point.y),
edit(t!("Position Z").as_ref(), "ri_oz", self.insertion_point.z),
ro(t!("Rotation").as_ref(), "ri_rotation", crate::entities::common::format_angle(rotation_deg.to_radians())),
ro(t!("Width").as_ref(), "ri_width", crate::entities::common::format_length(self.width())),
ro(t!("Height").as_ref(), "ri_height", crate::entities::common::format_length(self.height())),
ro(
t!("Rotation").as_ref(),
"ri_rotation",
crate::entities::common::format_angle(rotation_deg.to_radians()),
),
ro(
t!("Width").as_ref(),
"ri_width",
crate::entities::common::format_length(self.width()),
),
ro(
t!("Height").as_ref(),
"ri_height",
crate::entities::common::format_length(self.height()),
),
ro(t!("Scale").as_ref(), "ri_scale", format!("{:.4}", scale)),
],
},
@@ -344,7 +362,11 @@ impl PropertyEditable for RasterImage {
title: t!("Misc").into_owned(),
props: vec![
ro(t!("Name").as_ref(), "ri_name", self.file_name().to_string()),
edit(t!("Brightness").as_ref(), "ri_bright", self.brightness as f64),
edit(
t!("Brightness").as_ref(),
"ri_bright",
self.brightness as f64,
),
edit(t!("Contrast").as_ref(), "ri_contrast", self.contrast as f64),
edit(t!("Fade").as_ref(), "ri_fade", self.fade as f64),
ro(t!("Transparency").as_ref(), "ri_transparency", transparency),
@@ -381,7 +403,9 @@ impl PropertyEditable for RasterImage {
match field {
"ri_show_image" => {
let on = if value == "toggle" {
!self.flags.contains(codec::entities::ImageDisplayFlags::SHOW_IMAGE)
!self
.flags
.contains(codec::entities::ImageDisplayFlags::SHOW_IMAGE)
} else {
value == "true"
};
@@ -435,21 +459,25 @@ impl PropertyEditable for RasterImage {
impl Transformable for RasterImage {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2);
reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2);
reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2);
}
},
);
}
}
@@ -477,16 +505,8 @@ fn wipeout_plane(wipeout: &Wipeout) -> kernel::space::Plane {
wipeout.insertion_point.y,
wipeout.insertion_point.z,
],
[
wipeout.u_vector.x,
wipeout.u_vector.y,
wipeout.u_vector.z,
],
[
wipeout.v_vector.x,
wipeout.v_vector.y,
wipeout.v_vector.z,
],
[wipeout.u_vector.x, wipeout.u_vector.y, wipeout.u_vector.z],
[wipeout.v_vector.x, wipeout.v_vector.y, wipeout.v_vector.z],
)
}
@@ -602,18 +622,15 @@ impl Grippable for Wipeout {
GripApply::Translate(delta) => glam::DVec3::from(current) + delta,
GripApply::Absolute(point) => point,
};
let u_hat = glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z)
.normalize_or_zero();
let v_hat = glam::DVec3::new(self.v_vector.x, self.v_vector.y, self.v_vector.z)
.normalize_or_zero();
let u_hat =
glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z).normalize_or_zero();
let v_hat =
glam::DVec3::new(self.v_vector.x, self.v_vector.y, self.v_vector.z).normalize_or_zero();
if u_hat == glam::DVec3::ZERO || v_hat == glam::DVec3::ZERO {
return;
}
let resize_plane = kernel::space::Plane::from_axes(
[0.0; 3],
u_hat.to_array(),
v_hat.to_array(),
);
let resize_plane =
kernel::space::Plane::from_axes([0.0; 3], u_hat.to_array(), v_hat.to_array());
let (width, height, insertion) = match grip_id {
0 => {
let fixed = glam::DVec3::from(corners[2]);
@@ -656,11 +673,8 @@ impl Grippable for Wipeout {
_ => return,
};
if width.abs() > 1e-9 && height.abs() > 1e-9 {
self.insertion_point = codec::types::Vector3::new(
insertion.x,
insertion.y,
insertion.z,
);
self.insertion_point =
codec::types::Vector3::new(insertion.x, insertion.y, insertion.z);
let sx = self.size.x.abs().max(1e-9);
let sy = self.size.y.abs().max(1e-9);
self.u_vector = codec::types::Vector3::new(
@@ -679,7 +693,9 @@ impl Grippable for Wipeout {
impl PropertyEditable for Wipeout {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let show_image = self.flags.contains(codec::entities::WipeoutDisplayFlags::SHOW_IMAGE);
let show_image = self
.flags
.contains(codec::entities::WipeoutDisplayFlags::SHOW_IMAGE);
let show_clipped = self
.flags
.contains(codec::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY);
@@ -693,8 +709,16 @@ impl PropertyEditable for Wipeout {
PropSection {
title: t!("Image Adjust").into_owned(),
props: vec![
ro(t!("Brightness").as_ref(), "wo_brightness", self.brightness.to_string()),
ro(t!("Contrast").as_ref(), "wo_contrast", self.contrast.to_string()),
ro(
t!("Brightness").as_ref(),
"wo_brightness",
self.brightness.to_string(),
),
ro(
t!("Contrast").as_ref(),
"wo_contrast",
self.contrast.to_string(),
),
ro(t!("Fade").as_ref(), "wo_fade", self.fade.to_string()),
],
},
@@ -713,9 +737,21 @@ impl PropertyEditable for Wipeout {
PropSection {
title: t!("Misc").into_owned(),
props: vec![
ro(t!("Show image").as_ref(), "wo_show_image", if show_image { t!("Yes") } else { t!("No") }),
ro(t!("Show clipped").as_ref(), "wo_show_clipped", if show_clipped { t!("Yes") } else { t!("No") }),
ro(t!("Background transparency").as_ref(), "wo_bg_transparency", if bg_transparency { t!("Yes") } else { t!("No") }),
ro(
t!("Show image").as_ref(),
"wo_show_image",
if show_image { t!("Yes") } else { t!("No") },
),
ro(
t!("Show clipped").as_ref(),
"wo_show_clipped",
if show_clipped { t!("Yes") } else { t!("No") },
),
ro(
t!("Background transparency").as_ref(),
"wo_bg_transparency",
if bg_transparency { t!("Yes") } else { t!("No") },
),
],
},
]
@@ -723,7 +759,8 @@ impl PropertyEditable for Wipeout {
fn apply_geom_prop(&mut self, field: &str, value: &str) {
if field == "wo_rotation" {
let Some(target_degrees) = parse_angle_deg(value).filter(|value| value.is_finite()) else {
let Some(target_degrees) = parse_angle_deg(value).filter(|value| value.is_finite())
else {
return;
};
let u = glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z);
@@ -784,21 +821,25 @@ impl PropertyEditable for Wipeout {
impl Transformable for Wipeout {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2);
reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2);
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2);
reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2);
}
},
);
}
}
@@ -825,7 +866,10 @@ mod wipeout_property_tests {
.atan2(wipeout.u_vector.x)
.to_degrees()
.rem_euclid(360.0);
assert!((angle - target).abs() < 1e-9, "target={target} actual={angle}");
assert!(
(angle - target).abs() < 1e-9,
"target={target} actual={angle}"
);
assert!((wipeout.u_vector.length() - lengths.0).abs() < 1e-12);
assert!((wipeout.v_vector.length() - lengths.1).abs() < 1e-12);
}
+61 -43
View File
@@ -1,12 +1,10 @@
use codec::entities::{Ray, XLine};
use crate::t;
use codec::entities::{Ray, XLine};
use crate::command::EntityTransform;
use crate::entities::common::{
edit_prop as edit, format_length, ro_prop as ro, square_grip,
};
use crate::entities::common::{edit_prop as edit, format_length, ro_prop as ro, square_grip};
use crate::entities::curve::{point_along, unit_direction};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
@@ -103,9 +101,21 @@ impl PropertyEditable for Ray {
edit(t!("Start X").as_ref(), "ray_bx", self.base_point.x),
edit(t!("Start Y").as_ref(), "ray_by", self.base_point.y),
edit(t!("Start Z").as_ref(), "ray_bz", self.base_point.z),
edit(t!("Direction vector X").as_ref(), "ray_dx", self.direction.x),
edit(t!("Direction vector Y").as_ref(), "ray_dy", self.direction.y),
edit(t!("Direction vector Z").as_ref(), "ray_dz", self.direction.z),
edit(
t!("Direction vector X").as_ref(),
"ray_dx",
self.direction.x,
),
edit(
t!("Direction vector Y").as_ref(),
"ray_dy",
self.direction.y,
),
edit(
t!("Direction vector Z").as_ref(),
"ray_dz",
self.direction.z,
),
],
}]
}
@@ -134,24 +144,28 @@ impl PropertyEditable for Ray {
impl Transformable for Ray {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.base_point.x,
&mut entity.base_point.y,
p1,
p2,
);
// Mirror the direction: negate the component perpendicular to mirror axis.
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
let d = &mut entity.direction;
let dot = d.x * ax + d.y * ay;
d.x = 2.0 * dot * ax / len2 - d.x;
d.y = 2.0 * dot * ay / len2 - d.y;
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.base_point.x,
&mut entity.base_point.y,
p1,
p2,
);
// Mirror the direction: negate the component perpendicular to mirror axis.
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
let d = &mut entity.direction;
let dot = d.x * ax + d.y * ay;
d.x = 2.0 * dot * ax / len2 - d.x;
d.y = 2.0 * dot * ay / len2 - d.y;
}
},
);
}
}
@@ -292,22 +306,26 @@ impl PropertyEditable for XLine {
impl Transformable for XLine {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.base_point.x,
&mut entity.base_point.y,
p1,
p2,
);
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
let d = &mut entity.direction;
let dot = d.x * ax + d.y * ay;
d.x = 2.0 * dot * ax / len2 - d.x;
d.y = 2.0 * dot * ay / len2 - d.y;
}
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.base_point.x,
&mut entity.base_point.y,
p1,
p2,
);
let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay;
if len2 > 1e-12 {
let d = &mut entity.direction;
let dot = d.x * ax + d.y * ay;
d.x = 2.0 * dot * ax / len2 - d.x;
d.y = 2.0 * dot * ay / len2 - d.y;
}
},
);
}
}
+27 -9
View File
@@ -5,16 +5,18 @@
// but apply the SHAPE's own rotation / oblique / width factor so the marker
// gives a rough indication of the glyph orientation.
use codec::entities::Shape;
use crate::t;
use codec::entities::Shape;
use crate::command::EntityTransform;
use crate::entities::common::{edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::common::{
edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip,
};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue};
use crate::scene::view::transform;
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
use crate::scene::view::transform;
// ── Marker geometry ───────────────────────────────────────────────────────────
@@ -258,7 +260,11 @@ impl PropertyEditable for Shape {
title: t!("Geometry").into_owned(),
props: vec![
ro(t!("Name").as_ref(), "shp_name", self.shape_name.clone()),
ro(t!("Number").as_ref(), "shp_number", self.shape_number.to_string()),
ro(
t!("Number").as_ref(),
"shp_number",
self.shape_number.to_string(),
),
Property {
label: t!("Style").into_owned(),
field: "shp_style",
@@ -267,14 +273,26 @@ impl PropertyEditable for Shape {
options: text_style_names.to_vec(),
},
},
ro(t!("Style Handle").as_ref(), "shp_style_handle", style_handle_display),
ro(
t!("Style Handle").as_ref(),
"shp_style_handle",
style_handle_display,
),
edit(t!("Insert X").as_ref(), "shp_ix", self.insertion_point.x),
edit(t!("Insert Y").as_ref(), "shp_iy", self.insertion_point.y),
edit(t!("Insert Z").as_ref(), "shp_iz", self.insertion_point.z),
edit(t!("Size").as_ref(), "shp_sz", self.size),
edit_angle(t!("Rotation").as_ref(), "shp_rot", self.rotation.to_degrees()),
edit_angle(
t!("Rotation").as_ref(),
"shp_rot",
self.rotation.to_degrees(),
),
edit(t!("Width Factor").as_ref(), "shp_xs", self.relative_x_scale),
edit_angle(t!("Oblique Angle").as_ref(), "shp_ob", self.oblique_angle.to_degrees()),
edit_angle(
t!("Oblique Angle").as_ref(),
"shp_ob",
self.oblique_angle.to_degrees(),
),
edit(t!("Normal X").as_ref(), "shp_nx", self.normal.x),
edit(t!("Normal Y").as_ref(), "shp_ny", self.normal.y),
edit(t!("Normal Z").as_ref(), "shp_nz", self.normal.z),
+17 -32
View File
@@ -1,11 +1,11 @@
// SOLID geometry comes from the kernel; entity coordinates stay in WCS here.
use codec::entities::Solid;
use crate::t;
use codec::entities::Solid;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::SnapHint;
@@ -44,10 +44,7 @@ pub(crate) fn wcs_corners(solid: &Solid) -> [[f64; 3]; 4] {
fn set_wcs_corner(solid: &mut Solid, index: usize, point: glam::DVec3) {
let n = normal_tuple(solid);
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
(point.x, point.y, point.z),
n,
);
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs((point.x, point.y, point.z), n);
let corner = match index {
0 => &mut solid.first_corner,
1 => &mut solid.second_corner,
@@ -107,15 +104,8 @@ fn extrusion_body(
.skip(1)
.map(|point| glam::DVec3::from_array(*point) - origin)
.find(|axis| axis.length() > tolerance)?;
let plane = kernel::space::Plane::orthonormal(
base[0],
axis.to_array(),
normal.to_array(),
)?;
if base
.iter()
.any(|point| !plane.contains(*point, tolerance))
{
let plane = kernel::space::Plane::orthonormal(base[0], axis.to_array(), normal.to_array())?;
if base.iter().any(|point| !plane.contains(*point, tolerance)) {
return None;
}
let projected = base
@@ -130,20 +120,14 @@ fn extrusion_body(
})
})
.collect::<Vec<_>>();
kernel::brep::extrude(
plane,
&profile,
(normal * solid.thickness).to_array(),
)
kernel::brep::extrude(plane, &profile, (normal * solid.thickness).to_array())
}
fn solid_geometry(solid: &Solid) -> Option<SolidGeometry> {
let base = boundary(solid);
let tolerance = kernel_tolerance(&base);
let fill_tris = kernel::space::polygon::triangulate(
&base,
kernel::geom2d::Tolerance::new(tolerance),
);
let fill_tris =
kernel::space::polygon::triangulate(&base, kernel::geom2d::Tolerance::new(tolerance));
if solid.thickness.abs() <= 1.0e-10 {
return Some(SolidGeometry {
edges: boundary_edges(&base),
@@ -191,11 +175,9 @@ fn solid_geometry(solid: &Solid) -> Option<SolidGeometry> {
pub(crate) fn wcs_bounds(solid: &Solid) -> Option<kernel::brep::Aabb> {
let base = boundary(solid);
let tolerance = kernel_tolerance(&base);
let profile_valid = !kernel::space::polygon::triangulate(
&base,
kernel::geom2d::Tolerance::new(tolerance),
)
.is_empty();
let profile_valid =
!kernel::space::polygon::triangulate(&base, kernel::geom2d::Tolerance::new(tolerance))
.is_empty();
extrusion_body(solid, &base, tolerance, profile_valid)
.and_then(|body| kernel::brep::body_bounds(&body))
.or_else(|| kernel::brep::Aabb::around(base))
@@ -221,9 +203,12 @@ impl RenderConvertible for Solid {
snap.push((dvec3(point), SnapHint::Node));
snap.push((dvec3(point), SnapHint::Endpoint));
}
snap.extend(geometry.edges.iter().map(|(start, end)| {
((dvec3(*start) + dvec3(*end)) * 0.5, SnapHint::Midpoint)
}));
snap.extend(
geometry
.edges
.iter()
.map(|(start, end)| ((dvec3(*start) + dvec3(*end)) * 0.5, SnapHint::Midpoint)),
);
let pick_tris = geometry
.extruded
.then(|| geometry.fill_tris.clone())
+179 -103
View File
@@ -2,16 +2,16 @@
//
// Shared grips and properties for modeler entities.
use codec::entities::{Body, Region, Solid3D, Surface};
use codec::xdata::{ExtendedDataRecord, XDataValue};
use kernel::space::polygon;
use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, format_area, format_length, parse_f64, ro_prop as ro,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::t;
use codec::entities::{Body, Region, Solid3D, Surface};
use codec::xdata::{ExtendedDataRecord, XDataValue};
use kernel::space::polygon;
/// Shared transform for the ACIS volume entities. Translate / rotate / scale
/// delegate to opencadcodec (which composes the move into the solid's ACIS
@@ -22,10 +22,14 @@ macro_rules! impl_acis_transformable {
($ty:ty) => {
impl Transformable for $ty {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |e, p1, p2| {
let m = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
codec::Entity::apply_transform(e, &m);
});
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|e, p1, p2| {
let m = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
codec::Entity::apply_transform(e, &m);
},
);
}
}
};
@@ -42,14 +46,15 @@ fn dvec3(v: &codec::types::Vector3) -> glam::DVec3 {
}
fn translate_acis_entity<T: codec::Entity>(entity: &mut T, d: glam::DVec3) {
codec::Entity::translate(
entity,
codec::types::Vector3::new(d.x, d.y, d.z),
);
codec::Entity::translate(entity, codec::types::Vector3::new(d.x, d.y, d.z));
}
fn yes_no(value: bool) -> &'static str {
if value { "Yes" } else { "No" }
if value {
"Yes"
} else {
"No"
}
}
pub(crate) const SURFACE_PROPERTIES_APP: &str = "OCS_SURFACE_PROPERTIES";
@@ -282,20 +287,32 @@ fn region_area_perimeter(region: &Region) -> (f64, f64) {
let exact = (|| {
let (plane, loops, true) = crate::scene::model::presspull_model::profile_geometry(
&EntityType::Region(region.clone()),
)? else { return None; };
)?
else {
return None;
};
let unit = kernel::space::Plane::orthonormal(plane.origin, plane.x_axis, plane.normal()?)?;
let transform = kernel::geom2d::Transform {
origin: unit.project(plane.origin)?.into(),
x_axis: unit.project_vector(plane.x_axis)?.into(),
y_axis: unit.project_vector(plane.y_axis)?.into(),
};
let curves = loops.iter().flatten().map(|curve| curve.transformed(&transform))
let curves = loops
.iter()
.flatten()
.map(|curve| curve.transformed(&transform))
.collect::<Option<Vec<_>>>()?;
let area = curves.iter().map(|curve| curve.chain_area()).sum::<f64>().abs();
let area = curves
.iter()
.map(|curve| curve.chain_area())
.sum::<f64>()
.abs();
let perimeter = curves.iter().map(|curve| curve.length()).sum::<f64>();
(area.is_finite() && perimeter.is_finite()).then_some((area, perimeter))
})();
if let Some(measurements) = exact { return measurements; }
if let Some(measurements) = exact {
return measurements;
}
let wires = &region.wires;
let mut area_vector = [0.0f64; 3];
let mut perimeter = 0.0;
@@ -333,8 +350,12 @@ impl Grippable for Solid3D {
impl PropertyEditable for Solid3D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let mut sections =
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
let mut sections = acis_sections(
&self.acis_data,
&self.wires,
&self.silhouettes,
self.history_handle,
);
sections[0]
.props
.insert(0, ro(t!("UID").as_ref(), "s3d_uid", self.uid.clone()));
@@ -413,8 +434,12 @@ impl Grippable for Body {
impl PropertyEditable for Body {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let mut sections =
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
let mut sections = acis_sections(
&self.acis_data,
&self.wires,
&self.silhouettes,
self.history_handle,
);
sections[0]
.props
.insert(0, ro(t!("UID").as_ref(), "bdy_uid", self.uid.clone()));
@@ -498,16 +523,23 @@ fn sweep_options_text(options: &codec::entities::SurfaceSweepOptions) -> String
fn surface_construction_section(surface: &Surface) -> PropSection {
use codec::entities::SurfaceData;
let mut props = vec![
ro(t!("Kind").as_ref(), "srf_kind", format!("{:?}", surface.kind)),
ro(t!("Modeler Format").as_ref(),
ro(
t!("Kind").as_ref(),
"srf_kind",
format!("{:?}", surface.kind),
),
ro(
t!("Modeler Format").as_ref(),
"srf_modeler_version",
surface.modeler_format_version.to_string(),
),
edit(t!("U Isolines").as_ref(),
edit(
t!("U Isolines").as_ref(),
"srf_u_isolines",
surface.u_isolines as f64,
),
edit(t!("V Isolines").as_ref(),
edit(
t!("V Isolines").as_ref(),
"srf_v_isolines",
surface.v_isolines as f64,
),
@@ -515,7 +547,8 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
match &surface.surface_data {
SurfaceData::Generic => {}
SurfaceData::Plane { class_version } => {
props.push(ro(t!("Class Version").as_ref(),
props.push(ro(
t!("Class Version").as_ref(),
"srf_class_version",
class_version.to_string(),
));
@@ -527,21 +560,33 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
sweep_transform,
} => {
props.extend([
ro(t!("Sweep Entity").as_ref(),
ro(
t!("Sweep Entity").as_ref(),
"srf_sweep_entity",
embedded_name(sweep_entity.as_ref()),
),
ro(t!("Sweep Vector").as_ref(), "srf_sweep_vector", vector_text(sweep_vector)),
ro(t!("Sweep Options").as_ref(), "srf_sweep_options", sweep_options_text(options)),
ro(t!("Sweep Transform").as_ref(),
ro(
t!("Sweep Vector").as_ref(),
"srf_sweep_vector",
vector_text(sweep_vector),
),
ro(
t!("Sweep Options").as_ref(),
"srf_sweep_options",
sweep_options_text(options),
),
ro(
t!("Sweep Transform").as_ref(),
"srf_sweep_transform",
matrix_text(sweep_transform),
),
ro(t!("Sweep Entity Transform").as_ref(),
ro(
t!("Sweep Entity Transform").as_ref(),
"srf_sweep_entity_transform",
matrix_text(&options.sweep_entity_transform),
),
ro(t!("Path Entity Transform").as_ref(),
ro(
t!("Path Entity Transform").as_ref(),
"srf_path_entity_transform",
matrix_text(&options.path_entity_transform),
),
@@ -636,28 +681,41 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
close_to_axis,
} => {
props.extend([
ro(t!("Revolve Entity").as_ref(),
ro(
t!("Revolve Entity").as_ref(),
"srf_revolve_entity",
embedded_name(revolve_entity.as_ref()),
),
ro(t!("Class / Entity").as_ref(),
ro(
t!("Class / Entity").as_ref(),
"srf_revolve_ids",
format!("{class_version} / {entity_id}"),
),
ro(t!("Axis Point").as_ref(), "srf_axis_point", vector_text(axis_point)),
ro(t!("Axis Vector").as_ref(), "srf_axis_vector", vector_text(axis_vector)),
ro(t!("Angles").as_ref(),
ro(
t!("Axis Point").as_ref(),
"srf_axis_point",
vector_text(axis_point),
),
ro(
t!("Axis Vector").as_ref(),
"srf_axis_vector",
vector_text(axis_vector),
),
ro(
t!("Angles").as_ref(),
"srf_revolve_angles",
format!(
"start {:.6}; revolve {:.6}; draft {:.6}; twist {:.6}",
start_angle, revolve_angle, draft_angle, twist_angle
),
),
ro(t!("Draft Distances").as_ref(),
ro(
t!("Draft Distances").as_ref(),
"srf_revolve_draft_distances",
format!("{draft_start_distance:.6}→{draft_end_distance:.6}"),
),
ro(t!("Revolve Flags").as_ref(),
ro(
t!("Revolve Flags").as_ref(),
"srf_revolve_flags",
format!(
"solid {}; close-to-axis {}",
@@ -665,7 +723,8 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
yes_no(*close_to_axis)
),
),
ro(t!("Entity Transform").as_ref(),
ro(
t!("Entity Transform").as_ref(),
"srf_revolve_transform",
matrix_text(entity_transform),
),
@@ -680,11 +739,13 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
options,
} => {
props.extend([
ro(t!("Class Version").as_ref(),
ro(
t!("Class Version").as_ref(),
"srf_class_version",
class_version.to_string(),
),
ro(t!("Sweep / Path").as_ref(),
ro(
t!("Sweep / Path").as_ref(),
"srf_swept_entities",
format!(
"{} / {}",
@@ -692,20 +753,28 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
embedded_name(path_entity.as_ref())
),
),
ro(t!("Sweep Options").as_ref(), "srf_sweep_options", sweep_options_text(options)),
ro(t!("Sweep Transform").as_ref(),
ro(
t!("Sweep Options").as_ref(),
"srf_sweep_options",
sweep_options_text(options),
),
ro(
t!("Sweep Transform").as_ref(),
"srf_sweep_transform",
matrix_text(sweep_transform),
),
ro(t!("Path Transform").as_ref(),
ro(
t!("Path Transform").as_ref(),
"srf_path_transform",
matrix_text(path_transform),
),
ro(t!("Sweep Entity Transform").as_ref(),
ro(
t!("Sweep Entity Transform").as_ref(),
"srf_sweep_entity_transform",
matrix_text(&options.sweep_entity_transform),
),
ro(t!("Path Entity Transform").as_ref(),
ro(
t!("Path Entity Transform").as_ref(),
"srf_path_entity_transform",
matrix_text(&options.path_entity_transform),
),
@@ -720,18 +789,36 @@ fn surface_construction_section(surface: &Surface) -> PropSection {
v_vector2,
} => {
props.extend([
ro(t!("NURB Version").as_ref(),
ro(
t!("NURB Version").as_ref(),
"srf_nurb_version",
short_170.to_string(),
),
ro(t!("CV Hull").as_ref(),
ro(
t!("CV Hull").as_ref(),
"srf_nurb_cv_hull",
yes_no(*cv_hull_display),
),
ro(t!("U Vector 1").as_ref(), "srf_nurb_u1", vector_text(u_vector1)),
ro(t!("V Vector 1").as_ref(), "srf_nurb_v1", vector_text(v_vector1)),
ro(t!("U Vector 2").as_ref(), "srf_nurb_u2", vector_text(u_vector2)),
ro(t!("V Vector 2").as_ref(), "srf_nurb_v2", vector_text(v_vector2)),
ro(
t!("U Vector 1").as_ref(),
"srf_nurb_u1",
vector_text(u_vector1),
),
ro(
t!("V Vector 1").as_ref(),
"srf_nurb_v1",
vector_text(v_vector1),
),
ro(
t!("U Vector 2").as_ref(),
"srf_nurb_u2",
vector_text(u_vector2),
),
ro(
t!("V Vector 2").as_ref(),
"srf_nurb_v2",
vector_text(v_vector2),
),
]);
}
}
@@ -759,18 +846,19 @@ impl Grippable for Surface {
return;
}
if let GripApply::Translate(delta) = apply {
translate_surface(
self,
codec::types::Vector3::new(delta.x, delta.y, delta.z),
);
translate_surface(self, codec::types::Vector3::new(delta.x, delta.y, delta.z));
}
}
}
impl PropertyEditable for Surface {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let mut sections =
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
let mut sections = acis_sections(
&self.acis_data,
&self.wires,
&self.silhouettes,
self.history_handle,
);
sections.push(surface_construction_section(self));
sections.push(position_section("srf", &self.point_of_reference));
sections
@@ -808,21 +896,15 @@ impl PropertyEditable for Surface {
return;
};
let delta = match field {
"srf_px" => codec::types::Vector3::new(
value - self.point_of_reference.x,
0.0,
0.0,
),
"srf_py" => codec::types::Vector3::new(
0.0,
value - self.point_of_reference.y,
0.0,
),
"srf_pz" => codec::types::Vector3::new(
0.0,
0.0,
value - self.point_of_reference.z,
),
"srf_px" => {
codec::types::Vector3::new(value - self.point_of_reference.x, 0.0, 0.0)
}
"srf_py" => {
codec::types::Vector3::new(0.0, value - self.point_of_reference.y, 0.0)
}
"srf_pz" => {
codec::types::Vector3::new(0.0, 0.0, value - self.point_of_reference.z)
}
_ => return,
};
if delta != codec::types::Vector3::ZERO {
@@ -837,8 +919,8 @@ impl PropertyEditable for Surface {
//
// These entity types share ACIS data and a point of reference.
use crate::scene::model::mesh_model::MeshLodSet;
use crate::scene::convert::solid3d_tess;
use crate::scene::model::mesh_model::MeshLodSet;
use codec::{types::Vector3, EntityType};
const DISPLAY_DEFLECTION_COEFFICIENT: f64 = 2.5e-4;
@@ -863,9 +945,7 @@ pub fn display_deflection(
} else {
1.0
};
Some(
(span * DISPLAY_DEFLECTION_COEFFICIENT / resolution.max(1.0).sqrt()).max(1e-9),
)
Some((span * DISPLAY_DEFLECTION_COEFFICIENT / resolution.max(1.0).sqrt()).max(1e-9))
}
/// `point_of_reference` of an ACIS-backed volume entity, if applicable.
@@ -889,27 +969,15 @@ pub fn tessellate_volume(
isolines: usize,
) -> Option<MeshLodSet> {
match e {
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(
s,
color,
facet_res,
chordal_deflection,
isolines,
),
EntityType::Region(r) => solid3d_tess::tessellate_region(
r,
color,
facet_res,
chordal_deflection,
isolines,
),
EntityType::Body(b) => solid3d_tess::tessellate_body(
b,
color,
facet_res,
chordal_deflection,
isolines,
),
EntityType::Solid3D(s) => {
solid3d_tess::tessellate_solid3d(s, color, facet_res, chordal_deflection, isolines)
}
EntityType::Region(r) => {
solid3d_tess::tessellate_region(r, color, facet_res, chordal_deflection, isolines)
}
EntityType::Body(b) => {
solid3d_tess::tessellate_body(b, color, facet_res, chordal_deflection, isolines)
}
EntityType::Surface(s) => solid3d_tess::tessellate_surface(
s,
color,
@@ -931,8 +999,13 @@ mod tests {
use kernel::geom2d::{Curve, Line};
fn ring(points: &[[f64; 2]]) -> Vec<Curve> {
points.iter().copied().zip(points.iter().copied().cycle().skip(1))
.take(points.len()).map(|(start, end)| Curve::Line(Line { start, end })).collect()
points
.iter()
.copied()
.zip(points.iter().copied().cycle().skip(1))
.take(points.len())
.map(|(start, end)| Curve::Line(Line { start, end }))
.collect()
}
#[test]
@@ -945,8 +1018,11 @@ mod tests {
crate::scene::model::presspull_model::region_from_loops(
&loops,
crate::command::WorkingPlane::default(),
).unwrap()
else { unreachable!() };
)
.unwrap()
else {
unreachable!()
};
region.wires.clear();
let (area, perimeter) = region_area_perimeter(&region);
+63 -54
View File
@@ -4,8 +4,8 @@ use kernel::space::{NurbsCurve3, Parameterization};
use crate::command::EntityTransform;
use crate::entities::common::{
dropdown_grip, edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64,
ro_prop as ro, round_grip, square_grip,
dropdown_grip, edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64, ro_prop as ro,
round_grip, square_grip,
};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
@@ -31,8 +31,11 @@ pub(crate) fn fit_nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
if !uses_fit_method(spline) {
return None;
}
let mut points: Vec<_> = spline.fit_points.iter()
.map(|point| [point.x, point.y, point.z]).collect();
let mut points: Vec<_> = spline
.fit_points
.iter()
.map(|point| [point.x, point.y, point.z])
.collect();
let parameterization = match spline.knot_parameterization {
1 => Parameterization::Centripetal,
2 => Parameterization::Uniform,
@@ -42,15 +45,25 @@ pub(crate) fn fit_nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
return NurbsCurve3::interpolate_periodic(&points, parameterization);
}
if spline.flags.closed && points.first() != points.last() {
if let Some(first) = points.first().copied() { points.push(first); }
if let Some(first) = points.first().copied() {
points.push(first);
}
}
let tangent = |value: codec::types::Vector3| {
let value = [value.x, value.y, value.z];
(value.iter().map(|component| component * component).sum::<f64>() > 1e-18)
(value
.iter()
.map(|component| component * component)
.sum::<f64>()
> 1e-18)
.then_some(value)
};
NurbsCurve3::interpolate_fit(&points, tangent(spline.begin_tangent),
tangent(spline.end_tangent), parameterization)
NurbsCurve3::interpolate_fit(
&points,
tangent(spline.begin_tangent),
tangent(spline.end_tangent),
parameterization,
)
}
pub(crate) fn nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
@@ -65,7 +78,11 @@ pub(crate) fn nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
};
NurbsCurve3::new_strict(
degree,
spline.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(),
spline
.control_points
.iter()
.map(|point| [point.x, point.y, point.z])
.collect(),
spline.knots.clone(),
weights,
)?
@@ -114,11 +131,15 @@ pub(crate) fn replace_with_nurbs(spline: &mut Spline, curve: &NurbsCurve3) {
let mut fit_points = spline.fit_points.clone();
spline.degree = curve.degree() as i32;
spline.knots = curve.knots().to_vec();
spline.control_points = curve.control_points().iter()
.map(|point| codec::types::Vector3::new(point[0], point[1], point[2])).collect();
spline.control_points = curve
.control_points()
.iter()
.map(|point| codec::types::Vector3::new(point[0], point[1], point[2]))
.collect();
spline.weights = curve.weights().to_vec();
if !fit_points.is_empty() {
if let (Some(first), Some(point)) = (fit_points.first_mut(), spline.control_points.first()) {
if let (Some(first), Some(point)) = (fit_points.first_mut(), spline.control_points.first())
{
*first = *point;
}
if let (Some(last), Some(point)) = (fit_points.last_mut(), spline.control_points.last()) {
@@ -128,7 +149,9 @@ pub(crate) fn replace_with_nurbs(spline: &mut Spline, curve: &NurbsCurve3) {
spline.fit_points = fit_points;
spline.begin_tangent = codec::types::Vector3::ZERO;
spline.end_tangent = codec::types::Vector3::ZERO;
spline.flags.rational = spline.weights.windows(2)
spline.flags.rational = spline
.weights
.windows(2)
.any(|pair| (pair[0] - pair[1]).abs() > 1e-12);
spline.flags.closed = false;
spline.flags.periodic = false;
@@ -165,9 +188,7 @@ fn to_render(spl: &Spline) -> RenderEntity {
// so the kernel has nothing to say about it and the solve
// here remains the only description of its shape.
None if spl.flags.periodic => periodic_fit_spline_polyline(spl),
None if spl.flags.closed => {
catmull_rom_polyline(&spl.fit_points, true)
}
None if spl.flags.closed => catmull_rom_polyline(&spl.fit_points, true),
None => fit_spline_polyline(spl),
};
let snap = planar
@@ -325,9 +346,7 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
.collect()
});
match NurbsCurve3::new(degree, controls, spl.knots.clone(), weights) {
Some(curve) => {
curve.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
}
Some(curve) => curve.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE),
None => spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect(),
}
}
@@ -600,12 +619,7 @@ pub(crate) fn control_vertex_count(spline: &Spline) -> usize {
control_vertices(spline).len()
}
fn choice_prop(
label: &str,
field: &'static str,
selected: &str,
options: &[&str],
) -> Property {
fn choice_prop(label: &str, field: &'static str, selected: &str, options: &[&str]) -> Property {
Property {
label: label.into(),
field,
@@ -768,8 +782,8 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
let show_fit = shows_fit_points(spline);
let method = if show_fit { "Fit" } else { "Control Vertices" };
let closed = spline.flags.closed || spline.flags.periodic;
let can_show_control = control_vertices(spline).len() >= 2
&& !(fit_method && spline.flags.periodic);
let can_show_control =
control_vertices(spline).len() >= 2 && !(fit_method && spline.flags.periodic);
let yes_no = |b: bool| if b { "Yes" } else { "No" };
let knot_param = match spline.knot_parameterization {
0 => "Chord",
@@ -865,7 +879,11 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
choice_prop(
t!("CV frame").as_ref(),
"cv_frame",
if spline.cv_frame_visible { "Show" } else { "Hide" },
if spline.cv_frame_visible {
"Show"
} else {
"Hide"
},
&["Hide", "Show"],
)
} else {
@@ -1095,22 +1113,18 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
spline.fit_tolerance = v;
}
}
"start_tan_x" | "start_tan_y" | "start_tan_z" => {
match field {
"start_tan_x" => spline.begin_tangent.x = v,
"start_tan_y" => spline.begin_tangent.y = v,
"start_tan_z" => spline.begin_tangent.z = v,
_ => {}
}
}
"end_tan_x" | "end_tan_y" | "end_tan_z" => {
match field {
"end_tan_x" => spline.end_tangent.x = v,
"end_tan_y" => spline.end_tangent.y = v,
"end_tan_z" => spline.end_tangent.z = v,
_ => {}
}
}
"start_tan_x" | "start_tan_y" | "start_tan_z" => match field {
"start_tan_x" => spline.begin_tangent.x = v,
"start_tan_y" => spline.begin_tangent.y = v,
"start_tan_z" => spline.begin_tangent.z = v,
_ => {}
},
"end_tan_x" | "end_tan_y" | "end_tan_z" => match field {
"end_tan_x" => spline.end_tangent.x = v,
"end_tan_y" => spline.end_tangent.y = v,
"end_tan_z" => spline.end_tangent.z = v,
_ => {}
},
_ => {}
}
spline.flags.planar = crate::entities::curve::spline_is_planar(spline);
@@ -1162,11 +1176,8 @@ fn apply_transform(spline: &mut Spline, t: &EntityTransform) {
working_normal,
} = t
{
let transform = crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
*working_normal,
);
let transform =
crate::scene::view::transform::reflection_about_working_line(*p1, *p2, *working_normal);
codec::Entity::apply_transform(spline, &transform);
spline.flags.planar = crate::entities::curve::spline_is_planar(spline);
return;
@@ -1199,8 +1210,8 @@ impl crate::entities::traits::Grippable for Spline {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
if grip_id == SPLINE_MODE_GRIP_ID {
let fit_method = uses_fit_method(self);
let can_show_control = control_vertices(self).len() >= 2
&& !(fit_method && self.flags.periodic);
let can_show_control =
control_vertices(self).len() >= 2 && !(fit_method && self.flags.periodic);
let can_show_fit = can_show_fit_points(self);
return if !shows_fit_points(self) {
vec![
@@ -1411,9 +1422,7 @@ mod tests {
assert!(!uses_fit_method(&spline));
let after = nurbs3(&spline).unwrap().point_at(0.37);
assert!(
kernel::space::Vec3::from(before)
.distance(kernel::space::Vec3::from(after))
< 1.0e-12
kernel::space::Vec3::from(before).distance(kernel::space::Vec3::from(after)) < 1.0e-12
);
}
}
+802 -791
View File
File diff suppressed because it is too large. Load diff
+24 -29
View File
@@ -2,16 +2,17 @@ use codec::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment
use crate::command::EntityTransform;
use crate::entities::common::{
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, parse_f64, ro_prop as ro, square_grip,
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, 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, RenderConvertible};
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
use crate::scene::text::lff;
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{GlyphRun, RenderEntity, RenderObject, TextStroke};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
use crate::scene::text::lff;
use crate::t;
/// Combined single-line-text justification (horizontal × vertical) shown as one
@@ -236,8 +237,7 @@ pub fn text_run_placement_at_scale(
oblique_angle,
) {
if base_bounds.advance > 1.0e-6 {
let scale =
(span as f32 / annotation_scale / base_bounds.advance).max(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 {
@@ -332,10 +332,7 @@ fn grips(t: &Text) -> Vec<GripDef> {
);
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.push(square_grip(1, glam::DVec3::new(point.x, point.y, point.z)));
}
grips
}
@@ -346,8 +343,8 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
// other justification anchors on the alignment point (and recomputes the
// insertion point), except Aligned/Fit which are true two-point spans where
// both points are live.
let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
&& matches!(t.vertical_alignment, VA::Baseline);
let is_plain_left =
matches!(t.horizontal_alignment, HA::Left) && matches!(t.vertical_alignment, VA::Baseline);
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
@@ -393,11 +390,8 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
label: t!("Justify").into_owned(),
field: "justify",
value: PropValue::Choice {
selected: text_justify_str(
&t.horizontal_alignment,
&t.vertical_alignment,
)
.to_string(),
selected: text_justify_str(&t.horizontal_alignment, &t.vertical_alignment)
.to_string(),
options: [
"Left",
"Center",
@@ -426,8 +420,7 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
t!("Height").as_ref(),
"height",
t.height,
!is_aligned
&& crate::entities::common::style_fixed_height(&t.style).is_none(),
!is_aligned && crate::entities::common::style_fixed_height(&t.style).is_none(),
),
if is_two_point {
ro(
@@ -439,7 +432,11 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
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()),
edit_angle(
t!("Obliquing").as_ref(),
"oblique_angle",
t.oblique_angle.to_degrees(),
),
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),
@@ -563,18 +560,12 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
// Calculated display rows are intentionally not writable.
return;
}
"height"
if v > 0.0 && !matches!(t.horizontal_alignment, HA::Aligned) =>
{
t.height = v
}
"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" if (-85.0..=85.0).contains(&v) => {
t.oblique_angle = v.to_radians()
}
"oblique_angle" if (-85.0..=85.0).contains(&v) => t.oblique_angle = v.to_radians(),
_ => {}
}
}
@@ -657,7 +648,11 @@ impl Grippable for Text {
]
}
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
fn apply_grip_menu(
&mut self,
_grip_id: usize,
_action: crate::scene::model::object::GripMenuAction,
) {
// Move-with-Text falls through to Stretch (single grip moves
// the whole text); Rotate needs a follow-up angle handled by
// `apply_grip_menu_value`.
+154 -91
View File
@@ -241,7 +241,12 @@ pub fn text_local_bounds(
/// already resolved), measured as [`text_local_bounds`] lays the line out:
/// the shaper's glyph cells for a TrueType font or a script that needs
/// shaping, else the stroke font's advances.
pub fn text_char_cells(font_name: &str, text: &str, height: f32, width_factor: f32) -> Vec<(f32, f32)> {
pub fn text_char_cells(
font_name: &str,
text: &str,
height: f32,
width_factor: f32,
) -> Vec<(f32, f32)> {
let face = Face::resolve(font_name);
let scale = height / 9.0;
let wf = width_factor.abs().clamp(0.01, 100.0);
@@ -249,7 +254,9 @@ pub fn text_char_cells(font_name: &str, text: &str, height: f32, width_factor: f
crate::scene::text::web_font::requires_shaping(text)
.then(|| crate::scene::text::web_font::primary_script().family())
});
if let Some(run) = shaping_family.and_then(|family| crate::scene::text::ttf_glyph::shape_run(family, text)) {
if let Some(run) =
shaping_family.and_then(|family| crate::scene::text::ttf_glyph::shape_run(family, text))
{
return crate::scene::text::ttf_glyph::char_cells(&run, text)
.into_iter()
.map(|(x0, x1)| (x0 * scale * wf, x1 * scale * wf))
@@ -511,7 +518,6 @@ impl MTextLine {
}
}
/// Font-name stem: drop any path and extension, so a `\F` font path like
/// `C:\\fonts\\arial.ttf` and a plain `\f` name `arial` resolve to the same font.
fn font_stem(name: &str) -> String {
@@ -540,7 +546,11 @@ pub fn mtext_visible_count(s: &str) -> usize {
.map(|run| match &run.kind {
MTextRunKind::Glyphs(text) => text.chars().count(),
MTextRunKind::Tab => 1,
MTextRunKind::Stack { numerator, denominator, .. } => {
MTextRunKind::Stack {
numerator,
denominator,
..
} => {
numerator.chars().count()
+ denominator.chars().count()
+ usize::from(!denominator.is_empty())
@@ -708,9 +718,7 @@ pub fn adapt_mtext_paragraphs(
// button applies, so imported italic text isn't shown upright.
oblique_rad: match p.oblique_angle {
Some(q) => (q as f32).to_radians(),
None if p.font.as_ref().is_some_and(|f| f.italic) => {
15.0_f32.to_radians()
}
None if p.font.as_ref().is_some_and(|f| f.italic) => 15.0_f32.to_radians(),
None => 0.0,
},
tracking: p.tracking.map(|t| t as f32).unwrap_or(1.0),
@@ -875,7 +883,9 @@ fn is_rtl_word(w: &str) -> bool {
&& w.chars().all(|c| {
matches!(
unicode_bidi::bidi_class(c),
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL | unicode_bidi::BidiClass::NSM
unicode_bidi::BidiClass::R
| unicode_bidi::BidiClass::AL
| unicode_bidi::BidiClass::NSM
)
})
}
@@ -922,7 +932,8 @@ pub fn reorder_rtl_runs(atoms: Vec<LayoutAtom>) -> Vec<LayoutAtom> {
continue;
}
let mut end = i + 1;
while end < atoms.len() && (rtl(&atoms[end]) || matches!(atoms[end].kind, AtomKind::Space)) {
while end < atoms.len() && (rtl(&atoms[end]) || matches!(atoms[end].kind, AtomKind::Space))
{
end += 1;
}
atoms[i..end].reverse();
@@ -936,7 +947,12 @@ pub fn run_scale(state: &RunState, entity_h: f32, base_wf: f32) -> f32 {
}
pub fn resolve_font<'a>(state: &'a RunState, base: &'a str) -> std::borrow::Cow<'a, str> {
let Some(font) = state.font.as_deref().map(str::trim).filter(|f| !f.is_empty()) else {
let Some(font) = state
.font
.as_deref()
.map(str::trim)
.filter(|f| !f.is_empty())
else {
return std::borrow::Cow::Borrowed(base);
};
if lff::is_builtin(font) {
@@ -1081,7 +1097,12 @@ pub fn word_cells(
}
None => {
for ch in run.chars().filter(|c| *c != LRM) {
let w = stroke_run_width(&face, ch.encode_utf8(&mut [0u8; 4]), state.tracking, scale);
let w = stroke_run_width(
&face,
ch.encode_utf8(&mut [0u8; 4]),
state.tracking,
scale,
);
cells.push((x, x + w));
x += w;
}
@@ -1102,10 +1123,9 @@ fn trailing_letter_gap(atom: &LayoutAtom, entity_h: f32, base_wf: f32, base_font
let Some((text, false)) = word_runs(text, &face).last().copied() else {
return 0.0;
};
text.chars()
.rev()
.find(|c| *c != LRM)
.map_or(0.0, |c| face.spacing_after(c) * atom.state.tracking * run_scale(&atom.state, entity_h, base_wf))
text.chars().rev().find(|c| *c != LRM).map_or(0.0, |c| {
face.spacing_after(c) * atom.state.tracking * run_scale(&atom.state, entity_h, base_wf)
})
}
pub fn measure_space(state: &RunState, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
@@ -1221,8 +1241,8 @@ pub fn wrap_paragraph(
.find(|ts| ts.position > local + 1e-4)
.map(|ts| !matches!(ts.kind, TabKind::Left))
.unwrap_or(false);
let new_w = next_tab_position(cur_w + start_x, tab_stops, indent_left, entity_h)
- start_x;
let new_w =
next_tab_position(cur_w + start_x, tab_stops, indent_left, entity_h) - start_x;
let max_w = line_max_w(subline_idx);
if new_w > max_w && !cur.is_empty() {
sublines.push(std::mem::take(&mut cur));
@@ -1354,9 +1374,9 @@ pub fn flow_grip_axis(
};
let k = if vertical {
match v_anchor {
MTextVAnchor::Top
| MTextVAnchor::MiddleOfTopLine
| MTextVAnchor::BottomOfTopLine => 1.0,
MTextVAnchor::Top | MTextVAnchor::MiddleOfTopLine | MTextVAnchor::BottomOfTopLine => {
1.0
}
MTextVAnchor::Middle => 0.5,
MTextVAnchor::Bottom | MTextVAnchor::MiddleOfBottomLine => -1.0,
}
@@ -1456,8 +1476,7 @@ impl MTextColumns {
}
fn total_width(&self) -> f32 {
self.width * self.count as f32
+ self.gutter.max(0.0) * self.count.saturating_sub(1) as f32
self.width * self.count as f32 + self.gutter.max(0.0) * self.count.saturating_sub(1) as f32
}
}
@@ -1523,7 +1542,11 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
let base_font_name = opts.style.font_name.clone();
let base_font = Face::resolve(&base_font_name);
let base_wf_abs = opts.style.width_factor.max(0.01);
let base_wf = if opts.style.is_backward { -base_wf_abs } else { base_wf_abs };
let base_wf = if opts.style.is_backward {
-base_wf_abs
} else {
base_wf_abs
};
let base_oblique = opts.style.oblique_angle;
let entity_h = opts.height;
let rect_w = opts.rect_w;
@@ -1613,7 +1636,9 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
if let Some(prev) = word.chars().last() {
if cjk_break_between(prev, ch) {
atoms.push(LayoutAtom {
kind: AtomKind::Word(display_word(std::mem::take(&mut word))),
kind: AtomKind::Word(display_word(std::mem::take(
&mut word,
))),
state: run.state.clone(),
char_offset: word_start,
});
@@ -1838,8 +1863,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
mh * m * (5.0 / 3.0) * base_font.line_spacing()
}
_ if opts.exact_line_spacing => entity_gap,
_ => (mh * ls_factor * (5.0 / 3.0) * base_font.line_spacing())
.max(entity_gap),
_ => (mh * ls_factor * (5.0 / 3.0) * base_font.line_spacing()).max(entity_gap),
}
})
.collect();
@@ -2188,7 +2212,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
ymin: ay.min(by),
ymax: ay.max(by),
is_rtl: false,
local: [line_lx + cursor_start, line_ly, line_lx + cursor_start, line_ly + caret_h],
local: [
line_lx + cursor_start,
line_ly,
line_lx + cursor_start,
line_ly + caret_h,
],
});
vis += 1;
}
@@ -2202,8 +2231,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
.get(i + 1)
.is_none_or(|n| n.is_first_in_paragraph || n.column != sub.column);
let (spread_letters, gap) = {
let justify =
matches!(sub.align, Some(ParagraphAlign::Justify)) && !is_last_in_para;
let justify = matches!(sub.align, Some(ParagraphAlign::Justify)) && !is_last_in_para;
let distribute = matches!(sub.align, Some(ParagraphAlign::Distribute));
let slack = (content_right - content_left).max(0.0) - line_w;
if rect_w > 0.0 && (justify || distribute) && slack > 1e-6 {
@@ -2233,13 +2261,16 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
}
};
let mut cursor_x = if opts.vertical_text { 0.0 } else { cursor_start };
let mut cursor_x = if opts.vertical_text {
0.0
} else {
cursor_start
};
for (ai, atom) in sub.atoms.iter().enumerate() {
match &atom.kind {
AtomKind::Word(text) => {
let run_h = atom.state.height_mul * entity_h;
let signed_wf =
base_wf.signum() * atom.state.width_mul * base_wf.abs();
let signed_wf = base_wf.signum() * atom.state.width_mul * base_wf.abs();
let oblique = base_oblique + atom.state.oblique_rad;
let font_name = resolve_font(&atom.state, &base_font_name);
if opts.vertical_text {
@@ -2249,8 +2280,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
// own single-glyph run so the SDF path draws it at its
// cell origin; 1.4×height is the classic vertical cell
// advance.
let color =
atom.state.color.as_ref().and_then(resolve_inline_color);
let color = atom.state.color.as_ref().and_then(resolve_inline_color);
let cell = run_h * 1.4;
// Word wrap, vertically: a word that would overrun the
// column limit moves WHOLE to the next column (the
@@ -2431,15 +2461,16 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
// per character in logical order); evenly divided
// slots only when the word is not shaped.
let slot_w = word_w / count as f32;
let cells = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name)
.unwrap_or_else(|| {
(0..count)
.map(|ci| {
let x = (count - 1 - ci) as f32 * slot_w;
(x, x + slot_w)
})
.collect()
});
let cells =
word_cells(text, &atom.state, entity_h, base_wf, &base_font_name)
.unwrap_or_else(|| {
(0..count)
.map(|ci| {
let x = (count - 1 - ci) as f32 * slot_w;
(x, x + slot_w)
})
.collect()
});
for (ci, (x0, x1)) in cells.into_iter().enumerate() {
let (cx0, cx1) = (cursor_x + x0, cursor_x + x1);
let (ax, ay) = to_world(line_base_x, line_base_y, cx0, ly);
@@ -2451,7 +2482,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
ymin: ay.min(by),
ymax: ay.max(by),
is_rtl: true,
local: [line_lx + cx0, line_ly + ly, line_lx + cx1, line_ly + ly + run_h],
local: [
line_lx + cx0,
line_ly + ly,
line_lx + cx1,
line_ly + ly + run_h,
],
});
}
} else {
@@ -2461,7 +2497,8 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
// takes the shaper's cells, as it draws.
let scale = run_scale(&atom.state, entity_h, base_wf);
let face = Face::resolve(&font_name);
let shaped = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name);
let shaped =
word_cells(text, &atom.state, entity_h, base_wf, &base_font_name);
let mut cx = cursor_x;
for (ci, ch) in text.chars().filter(|c| *c != LRM).enumerate() {
let adv = match face.glyph(ch) {
@@ -2470,10 +2507,11 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
}
None => (6.0 + face.letter_spacing() * tracking) * scale,
};
let (cx0, cx1) = match shaped.as_ref().and_then(|cells| cells.get(ci)) {
Some(&(x0, x1)) => (cursor_x + x0, cursor_x + x1),
None => (cx, cx + adv),
};
let (cx0, cx1) =
match shaped.as_ref().and_then(|cells| cells.get(ci)) {
Some(&(x0, x1)) => (cursor_x + x0, cursor_x + x1),
None => (cx, cx + adv),
};
let (ax, ay) = to_world(line_base_x, line_base_y, cx0, ly);
let (bx, by) = to_world(line_base_x, line_base_y, cx1, ly + run_h);
glyph_boxes.push(GlyphBox {
@@ -2483,7 +2521,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
ymin: ay.min(by),
ymax: ay.max(by),
is_rtl: false,
local: [line_lx + cx0, line_ly + ly, line_lx + cx1, line_ly + ly + run_h],
local: [
line_lx + cx0,
line_ly + ly,
line_lx + cx1,
line_ly + ly + run_h,
],
});
cx += adv;
}
@@ -2588,18 +2631,14 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
to_world(line_base_x, line_base_y, x0, top),
to_world(line_base_x, line_base_y, x1, top),
];
let xmin = corners
.iter()
.map(|p| p.0)
.fold(f32::INFINITY, f32::min);
let xmin =
corners.iter().map(|p| p.0).fold(f32::INFINITY, f32::min);
let xmax = corners
.iter()
.map(|p| p.0)
.fold(f32::NEG_INFINITY, f32::max);
let ymin = corners
.iter()
.map(|p| p.1)
.fold(f32::INFINITY, f32::min);
let ymin =
corners.iter().map(|p| p.1).fold(f32::INFINITY, f32::min);
let ymax = corners
.iter()
.map(|p| p.1)
@@ -2611,7 +2650,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
ymin,
ymax,
is_rtl: false,
local: [line_lx + x0, line_ly + valign_dy, line_lx + x1, line_ly + top],
local: [
line_lx + x0,
line_ly + valign_dy,
line_lx + x1,
line_ly + top,
],
});
}
}
@@ -2667,8 +2711,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
continue;
}
// Justify / distribute widen every word gap by `gap`.
let adv =
measure_space(&atom.state, entity_h, base_wf, &base_font_name) + gap;
let adv = measure_space(&atom.state, entity_h, base_wf, &base_font_name) + gap;
// A space inside a decorated span carries the underline /
// overline / strike state but draws no glyph, so without this
// the rule breaks at every inter-word gap. Emit the decoration
@@ -2676,8 +2719,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
// exactly `measure_space`, so the rule meets the neighbours'.
if atom.state.underline || atom.state.overline || atom.state.strike {
let run_h = atom.state.height_mul * entity_h;
let signed_wf =
base_wf.signum() * atom.state.width_mul * base_wf.abs();
let signed_wf = base_wf.signum() * atom.state.width_mul * base_wf.abs();
let oblique = base_oblique + atom.state.oblique_rad;
let font_name = resolve_font(&atom.state, &base_font_name);
let tracking = atom.state.tracking;
@@ -2686,8 +2728,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
2 => line_max_h - run_h,
_ => 0.0,
};
let color =
atom.state.color.as_ref().and_then(resolve_inline_color);
let color = atom.state.color.as_ref().and_then(resolve_inline_color);
let lx = cursor_x;
let ly = valign_dy;
let origin: [f64; 2] = [
@@ -2719,8 +2760,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
if opts.want_glyph_boxes {
let run_h = atom.state.height_mul * entity_h;
let (ax, ay) = to_world(line_base_x, line_base_y, cursor_x, 0.0);
let (bx, by) =
to_world(line_base_x, line_base_y, cursor_x + adv, run_h);
let (bx, by) = to_world(line_base_x, line_base_y, cursor_x + adv, run_h);
glyph_boxes.push(GlyphBox {
vis: atom.char_offset,
xmin: ax.min(bx),
@@ -2728,7 +2768,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
ymin: ay.min(by),
ymax: ay.max(by),
is_rtl: false,
local: [line_lx + cursor_x, line_ly, line_lx + cursor_x + adv, line_ly + run_h],
local: [
line_lx + cursor_x,
line_ly,
line_lx + cursor_x + adv,
line_ly + run_h,
],
});
}
cursor_x += adv;
@@ -2782,12 +2827,8 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
);
}
AtomKind::Stack { .. } => {
field_w += atom_width(
next,
entity_h,
base_wf,
&base_font_name,
);
field_w +=
atom_width(next, entity_h, base_wf, &base_font_name);
}
_ => break,
}
@@ -2806,12 +2847,8 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
cursor_x = sx - offset;
}
_ => {
cursor_x = next_tab_position(
cursor_x,
&sub.tab_stops,
tab_base,
entity_h,
);
cursor_x =
next_tab_position(cursor_x, &sub.tab_stops, tab_base, entity_h);
}
}
}
@@ -3029,7 +3066,11 @@ mod adapter_tests {
assert!(up.oblique_rad.abs() < 1e-4, "got {}", up.oblique_rad);
// An explicit `\Q` wins over the italic flag (no double slant).
let (_, q) = first_run(&adapt_mtext_paragraphs("\\Q30;\\fArial|i1;x", 2.5, true));
assert!((q.oblique_rad - 30f32.to_radians()).abs() < 1e-4, "got {}", q.oblique_rad);
assert!(
(q.oblique_rad - 30f32.to_radians()).abs() < 1e-4,
"got {}",
q.oblique_rad
);
}
#[test]
@@ -3067,10 +3108,8 @@ mod adapter_tests {
let lines = adapt_mtext_paragraphs("a\\Pb\\Pc", 2.5, true);
assert_eq!(lines.len(), 3);
}
}
#[cfg(test)]
mod v_anchor_tests {
use super::*;
@@ -3181,14 +3220,19 @@ mod v_anchor_tests {
reorder(&["אבג", " ", "דה", " ", "abc"]),
vec![" ", "דה", " ", "אבג", "abc"]
);
assert_eq!(reorder(&["Hello", " ", "world"]), vec!["Hello", " ", "world"]);
assert_eq!(
reorder(&["Hello", " ", "world"]),
vec!["Hello", " ", "world"]
);
// A word starting with multi-byte punctuation ends the run whole.
assert_eq!(reorder(&["שלום", " ", "«x"]), vec![" ", "שלום", "«x"]);
}
#[test]
fn cjk_paragraph_wraps_between_ideographs() {
use crate::entities::text_support::{layout_mtext, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle};
use crate::entities::text_support::{
layout_mtext, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
};
// Breaks are allowed between ideographs but not before a closing mark
// or after an opening one.
assert!(cjk_break_between('工', '程'));
@@ -3239,7 +3283,9 @@ mod v_anchor_tests {
#[test]
fn test_mtext_arabic_bidi_layout() {
use crate::entities::text_support::{layout_mtext, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle};
use crate::entities::text_support::{
layout_mtext, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
};
let style = ResolvedTextStyle {
font_name: "Standard".to_string(),
@@ -3274,7 +3320,11 @@ mod v_anchor_tests {
// Stroke origins should increase monotonically in X (left to right visual order)
let origins_x: Vec<f64> = layout.strokes.iter().map(|s| s.origin[0]).collect();
for w in origins_x.windows(2) {
assert!(w[0] < w[1], "Each subsequent visual word must sit further to the right: {:?}", origins_x);
assert!(
w[0] < w[1],
"Each subsequent visual word must sit further to the right: {:?}",
origins_x
);
}
// For RTL text with rect_w == 0.0, the last visual word (which is "بسم", the first spoken word)
@@ -3284,7 +3334,10 @@ mod v_anchor_tests {
// Glyph boxes should have is_rtl = true for Arabic words
let rtl_boxes: Vec<_> = layout.glyph_boxes.iter().filter(|b| b.is_rtl).collect();
assert!(!rtl_boxes.is_empty(), "Arabic characters must produce is_rtl = true glyph boxes");
assert!(
!rtl_boxes.is_empty(),
"Arabic characters must produce is_rtl = true glyph boxes"
);
// Test mixed multilingual string in wrapped box (rect_w = 200.0)
let opts_mixed = MTextRenderOpts {
@@ -3307,9 +3360,16 @@ mod v_anchor_tests {
assert!(!layout_mixed.strokes.is_empty());
// A paragraph starting with right-to-left text still follows the
// attachment: left-anchored in a 200-wide box, it starts at the left.
let min_origin = layout_mixed.strokes.iter().map(|s| s.origin[0]).fold(f64::INFINITY, f64::min);
let min_origin = layout_mixed
.strokes
.iter()
.map(|s| s.origin[0])
.fold(f64::INFINITY, f64::min);
// (The space after the turned run leads, as the reference shows it.)
assert!(min_origin < 5.0, "a left-anchored paragraph starts at the left margin (got min_origin={min_origin})");
assert!(
min_origin < 5.0,
"a left-anchored paragraph starts at the left margin (got min_origin={min_origin})"
);
// Test single Arabic word "ميل"
let opts_mayl = MTextRenderOpts {
@@ -3331,6 +3391,9 @@ mod v_anchor_tests {
let layout_mayl = layout_mtext(&opts_mayl);
assert_eq!(layout_mayl.strokes.len(), 1);
let s = &layout_mayl.strokes[0];
assert!(!s.strokes.is_empty() || !s.fill_tris.is_empty(), "Shaped glyphs must produce vector geometry");
assert!(
!s.strokes.is_empty() || !s.fill_tris.is_empty(),
"Shaped glyphs must produce vector geometry"
);
}
}
+49 -79
View File
@@ -2,7 +2,7 @@ use codec::entities::Tolerance;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{
GlyphRun, RenderEntity, RenderObject, TextPlane, TextStroke,
};
@@ -179,20 +179,8 @@ pub(crate) fn resolve_dim_style<'a>(
///
/// This is where the pen LANDS, so it carries the font's letter spacing past
/// the final glyph — which is what puts the gap between one run and the next.
fn run_advance(
text: &str,
font: &str,
height: f32,
width_factor: f32,
oblique: f32,
) -> f32 {
crate::entities::text_support::text_local_bounds(
font,
text,
height,
width_factor,
oblique,
)
fn run_advance(text: &str, font: &str, height: f32, width_factor: f32, oblique: f32) -> f32 {
crate::entities::text_support::text_local_bounds(font, text, height, width_factor, oblique)
.map(|b| b.advance)
.unwrap_or(0.0)
}
@@ -202,9 +190,7 @@ fn run_advance(
/// Glyph geometry is authored against a 9-unit cap height, so a font's spacing
/// scales with the character height like everything else.
fn letter_spacing(font: &str, height: f32, width_factor: f32) -> f32 {
crate::scene::text::font_face::Face::resolve(font).letter_spacing()
* height
/ 9.0
crate::scene::text::font_face::Face::resolve(font).letter_spacing() * height / 9.0
* width_factor
}
@@ -301,14 +287,12 @@ fn tessellate_tolerance(
.unwrap_or(tol.text_height) as f32;
let h = if h > 1e-6 { h } else { 2.5_f32 };
let text_style_name = crate::entities::dim_override::handle(
xd,
crate::entities::dim_override::DIMTXSTY,
)
.and_then(|handle| doc.text_styles.iter().find(|entry| entry.handle == handle))
.map(|entry| entry.name.as_str())
.or_else(|| style.map(|style| style.dimtxsty.as_str()))
.unwrap_or("Standard");
let text_style_name =
crate::entities::dim_override::handle(xd, crate::entities::dim_override::DIMTXSTY)
.and_then(|handle| doc.text_styles.iter().find(|entry| entry.handle == handle))
.map(|entry| entry.name.as_str())
.or_else(|| style.map(|style| style.dimtxsty.as_str()))
.unwrap_or("Standard");
let text_style = crate::entities::text_support::resolve_text_style(text_style_name, doc);
// A compartment is a PROPORTION of the character height: it spans -h..+h
@@ -333,9 +317,8 @@ fn tessellate_tolerance(
// (`len()`) would make each one two or three cells wide.
// A compartment's runs come from different fonts, so each is measured in its
// own before they are summed.
let cell_width = |cell: &Cell| -> f32 {
(content_width(cell, h, &text_style) + 2.0 * pad).max(min_cell_w)
};
let cell_width =
|cell: &Cell| -> f32 { (content_width(cell, h, &text_style) + 2.0 * pad).max(min_cell_w) };
let row_widths: Vec<Vec<f32>> = rows
.iter()
.map(|row| row.iter().map(|c| cell_width(c)).collect())
@@ -362,13 +345,7 @@ fn tessellate_tolerance(
let rw: f32 = widths.iter().sum();
let y0 = row_bottom(ri);
let y1 = y0 + cell_h;
box_out.push(vec![
[0.0, y0],
[rw, y0],
[rw, y1],
[0.0, y1],
[0.0, y0],
]);
box_out.push(vec![[0.0, y0], [rw, y0], [rw, y1], [0.0, y1], [0.0, y0]]);
}
// ── Compartment dividers, within each row ─────────────────────────────
@@ -408,16 +385,15 @@ fn tessellate_tolerance(
text_style.oblique_angle,
)
};
let (local_strokes, _) =
lff::tessellate_text_ex(
[0.0, 0.0],
h,
0.0,
width_factor,
oblique,
&font,
&text,
);
let (local_strokes, _) = lff::tessellate_text_ex(
[0.0, 0.0],
h,
0.0,
width_factor,
oblique,
&font,
&text,
);
let strokes: Vec<Vec<[f32; 2]>> = local_strokes
.into_iter()
.filter(|pl: &Vec<[f32; 2]>| !pl.is_empty())
@@ -442,16 +418,13 @@ fn tessellate_tolerance(
(box_out, cells)
}
pub(crate) fn preview_strokes(
tol: &Tolerance,
doc: &codec::CadDocument,
) -> Vec<Vec<[f32; 2]>> {
pub(crate) fn preview_strokes(tol: &Tolerance, doc: &codec::CadDocument) -> Vec<Vec<[f32; 2]>> {
tessellate_tolerance(tol, doc).0
}
fn frame_basis(tol: &Tolerance) -> (glam::DVec3, glam::DVec3) {
let normal = glam::DVec3::new(tol.normal.x, tol.normal.y, tol.normal.z)
.normalize_or(glam::DVec3::Z);
let normal =
glam::DVec3::new(tol.normal.x, tol.normal.y, tol.normal.z).normalize_or(glam::DVec3::Z);
let direction = glam::DVec3::new(tol.direction.x, tol.direction.y, tol.direction.z);
let fallback = crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z)).0;
let fallback = glam::DVec3::new(fallback.0, fallback.1, fallback.2);
@@ -491,38 +464,33 @@ impl RenderConvertible for Tolerance {
let style = resolve_dim_style(self, document);
let xd = &self.common.extended_data;
let explicit_color = |index: Option<i16>| {
index.filter(|value| !matches!(value, 0 | 256)).map(|value| {
let [red, green, blue, _] = crate::scene::convert::tess_util::aci_to_rgba(
&codec::types::Color::from_index(value),
);
[red, green, blue]
})
index
.filter(|value| !matches!(value, 0 | 256))
.map(|value| {
let [red, green, blue, _] = crate::scene::convert::tess_util::aci_to_rgba(
&codec::types::Color::from_index(value),
);
[red, green, blue]
})
};
let color_value = |color: &codec::types::Color| {
let [red, green, blue, _] =
crate::scene::convert::tess_util::aci_to_rgba(color);
let [red, green, blue, _] = crate::scene::convert::tess_util::aci_to_rgba(color);
[red, green, blue]
};
let frame_color = if let Some(index) = crate::entities::dim_override::int(
xd,
crate::entities::dim_override::DIMCLRD,
) {
explicit_color(Some(index))
} else if let Some(color) =
style.and_then(|entry| entry.dimclrd_true_color.as_ref())
let frame_color = if let Some(index) =
crate::entities::dim_override::int(xd, crate::entities::dim_override::DIMCLRD)
{
explicit_color(Some(index))
} else if let Some(color) = style.and_then(|entry| entry.dimclrd_true_color.as_ref()) {
Some(color_value(color))
} else {
explicit_color(style.map(|entry| entry.dimclrd))
};
let text_color = if let Some(index) = crate::entities::dim_override::int(
xd,
crate::entities::dim_override::DIMCLRT,
) {
explicit_color(Some(index))
} else if let Some(color) =
style.and_then(|entry| entry.dimclrt_true_color.as_ref())
let text_color = if let Some(index) =
crate::entities::dim_override::int(xd, crate::entities::dim_override::DIMCLRT)
{
explicit_color(Some(index))
} else if let Some(color) = style.and_then(|entry| entry.dimclrt_true_color.as_ref()) {
Some(color_value(color))
} else {
explicit_color(style.map(|entry| entry.dimclrt))
@@ -547,10 +515,8 @@ impl RenderConvertible for Tolerance {
groups.push(TextStroke {
strokes: cell.strokes,
origin: [
ins.x + x_axis.x * cell.origin[0] as f64
+ y_axis.x * cell.origin[1] as f64,
ins.y + x_axis.y * cell.origin[0] as f64
+ y_axis.y * cell.origin[1] as f64,
ins.x + x_axis.x * cell.origin[0] as f64 + y_axis.x * cell.origin[1] as f64,
ins.y + x_axis.y * cell.origin[0] as f64 + y_axis.y * cell.origin[1] as f64,
],
color: text_color,
fill_tris: vec![],
@@ -638,7 +604,11 @@ impl PropertyEditable for Tolerance {
options: text_style_names.to_vec(),
},
},
edit(t!("Text height").as_ref(), "tol_text_height", self.text_height),
edit(
t!("Text height").as_ref(),
"tol_text_height",
self.text_height,
),
],
},
PropSection {
+12 -30
View File
@@ -2,10 +2,8 @@ use codec::{CadDocument, EntityType};
use crate::command::EntityTransform;
use crate::scene::convert::acad_to_render::RenderEntity;
use crate::scene::model::object::{
GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection,
};
use crate::scene::convert::tess_util::FallbackGeometry;
use crate::scene::model::object::{GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection};
pub trait RenderConvertible {
fn to_render(&self, document: &CadDocument) -> Option<RenderEntity>;
@@ -28,7 +26,10 @@ pub trait Grippable {
/// per entity to add Add Vertex / Convert to Arc / Reverse Arrows /
/// etc.
fn grip_menu(&self, _grip_id: usize) -> Vec<GripMenuItem> {
vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }]
vec![GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
}]
}
/// React to a popup-menu commit. Default no-op — `Stretch` is the
/// "do nothing extra" path, the normal drag still happens on click.
@@ -39,12 +40,7 @@ pub trait Grippable {
/// follow-up (Lengthen / Radius / Arc Length / Rotate Text / …).
/// The app prompts the user for a value on the command line and
/// calls this with the parsed `f64`.
fn apply_grip_menu_value(
&mut self,
_grip_id: usize,
_action: GripMenuAction,
_value: f64,
) {}
fn apply_grip_menu_value(&mut self, _grip_id: usize, _action: GripMenuAction, _value: f64) {}
/// If the popup action this entity defines for `grip_id` needs a
/// numeric prompt, return the prompt string and the action so the
/// app can stash a pending-value state and route the next typed
@@ -169,18 +165,14 @@ pub fn entity_type_name(et: &EntityType) -> &str {
match et {
EntityType::Point(_) => "Point",
EntityType::Line(line)
if codec::entities::CenterMarkAssociation::read(
&line.common.extended_data,
)
.is_some() =>
if codec::entities::CenterMarkAssociation::read(&line.common.extended_data)
.is_some() =>
{
"CenterMark"
}
EntityType::Line(line)
if codec::entities::CenterLineAssociation::read(
&line.common.extended_data,
)
.is_some() =>
if codec::entities::CenterLineAssociation::read(&line.common.extended_data)
.is_some() =>
{
"CenterLine"
}
@@ -258,12 +250,7 @@ pub trait EntityTypeOps {
action: GripMenuAction,
point: glam::DVec3,
) -> Option<f64>;
fn apply_grip_menu_value(
&mut self,
grip_id: usize,
action: GripMenuAction,
value: f64,
);
fn apply_grip_menu_value(&mut self, grip_id: usize, action: GripMenuAction, value: f64);
fn apply_transform(&mut self, t: &EntityTransform);
fn mass_props(&self) -> Option<MassProps>;
fn text_content(&self) -> Option<String>;
@@ -522,12 +509,7 @@ impl EntityTypeOps for EntityType {
)
}
fn apply_grip_menu_value(
&mut self,
grip_id: usize,
action: GripMenuAction,
value: f64,
) {
fn apply_grip_menu_value(&mut self, grip_id: usize, action: GripMenuAction, value: f64) {
dispatch!(self,
|e| Grippable::apply_grip_menu_value(e, grip_id, action, value),
[
+54 -19
View File
@@ -7,13 +7,13 @@
// a PDF underlay. Rows that need the definition (name, page, path,
// width, height) are filled in by the Properties panel.
use codec::entities::{Underlay, UnderlayDisplayFlags};
use crate::t;
use codec::entities::{Underlay, UnderlayDisplayFlags};
use glam::DVec3;
use crate::command::EntityTransform;
use crate::entities::common::{center_grip, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
@@ -50,7 +50,9 @@ pub(crate) fn definition<'a>(
/// Page number of a PDF definition ("1" when unset), or the sheet (DWF) or
/// model (DGN) name (empty: the file's first).
pub(crate) fn page_of(def: &codec::entities::UnderlayDefinition) -> &str {
if def.page_name.trim().is_empty() && matches!(def.underlay_type, codec::entities::UnderlayType::Pdf) {
if def.page_name.trim().is_empty()
&& matches!(def.underlay_type, codec::entities::UnderlayType::Pdf)
{
"1"
} else {
def.page_name.trim()
@@ -162,7 +164,10 @@ pub(crate) fn world_bounds(
document: &codec::CadDocument,
) -> Option<([f64; 3], [f64; 3])> {
let outline: Vec<[f64; 3]> = if is_clipped(u) && !u.clip_inverted {
clip_polygon_local(u).into_iter().map(|p| local_to_world(u, p)).collect()
clip_polygon_local(u)
.into_iter()
.map(|p| local_to_world(u, p))
.collect()
} else {
page_quad(u, document)?.to_vec()
};
@@ -182,7 +187,11 @@ pub(crate) fn world_bounds(
pub(crate) fn plain_number(value: f64) -> String {
let text = format!("{value:.4}");
let text = text.trim_end_matches('0').trim_end_matches('.').to_string();
if text == "-0" { "0".to_string() } else { text }
if text == "-0" {
"0".to_string()
} else {
text
}
}
fn number_row(label: &str, field: &'static str, value: f64) -> Property {
@@ -254,7 +263,11 @@ impl RenderConvertible for Underlay {
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
let origin = v3(&self.insertion_point);
let insertion_snap = (
DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z),
DVec3::new(
self.insertion_point.x,
self.insertion_point.y,
self.insertion_point.z,
),
SnapHint::Insertion,
);
let quad = page_quad(self, document);
@@ -305,7 +318,9 @@ impl RenderConvertible for Underlay {
quad.map(|q| q.to_vec()).unwrap_or_default()
};
let mut snap_pts = vec![insertion_snap];
snap_pts.extend(crate::scene::model::pdf_vector::underlay_snap_points(self, document));
snap_pts.extend(crate::scene::model::pdf_vector::underlay_snap_points(
self, document,
));
Some(RenderEntity {
pick_tris: vec![],
object: RenderObject::Lines(pts),
@@ -394,7 +409,9 @@ impl PropertyEditable for Underlay {
let show = self.flags.contains(UnderlayDisplayFlags::ON);
let clipping = self.flags.contains(UnderlayDisplayFlags::CLIPPING);
let monochrome = self.flags.contains(UnderlayDisplayFlags::MONOCHROME);
let adjust_bg = self.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND);
let adjust_bg = self
.flags
.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND);
vec![
PropSection {
@@ -403,7 +420,11 @@ impl PropertyEditable for Underlay {
number_row(t!("Contrast").as_ref(), "ul_contrast", self.contrast as f64),
number_row(t!("Fade").as_ref(), "ul_fade", self.fade as f64),
yes_no(t!("Monochrome").as_ref(), "ul_mono", monochrome),
yes_no(t!("Adjust colors for background").as_ref(), "ul_adjust_bg", adjust_bg),
yes_no(
t!("Adjust colors for background").as_ref(),
"ul_adjust_bg",
adjust_bg,
),
],
},
PropSection {
@@ -412,7 +433,11 @@ impl PropertyEditable for Underlay {
number_row(t!("Position X").as_ref(), "ul_ix", self.insertion_point.x),
number_row(t!("Position Y").as_ref(), "ul_iy", self.insertion_point.y),
number_row(t!("Position Z").as_ref(), "ul_iz", self.insertion_point.z),
number_row(t!("Rotation").as_ref(), "ul_rot", self.rotation.to_degrees()),
number_row(
t!("Rotation").as_ref(),
"ul_rot",
self.rotation.to_degrees(),
),
// Filled from the page size by the Properties panel.
number_row(t!("Width").as_ref(), "ul_width", 0.0),
number_row(t!("Height").as_ref(), "ul_height", 0.0),
@@ -453,9 +478,11 @@ impl PropertyEditable for Underlay {
"ul_adjust_bg" => {
let on = parse_flag(
value,
self.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
self.flags
.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
);
self.flags.set(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND, on);
self.flags
.set(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND, on);
return;
}
"ul_on" => {
@@ -500,8 +527,15 @@ impl Transformable for Underlay {
self.insertion_point.y += d.y as f64;
self.insertion_point.z += d.z as f64;
}
EntityTransform::Mirror { p1, p2, working_normal } => {
if !working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
EntityTransform::Mirror {
p1,
p2,
working_normal,
} => {
if !working_normal
.normalize_or(DVec3::Z)
.abs_diff_eq(DVec3::Z, 1e-10)
{
codec::Entity::apply_transform(
self,
&crate::scene::view::transform::reflection_about_working_line(
@@ -536,13 +570,14 @@ impl Transformable for Underlay {
self.y_scale *= f;
self.z_scale *= f;
}
EntityTransform::Rotate { center, axis, angle_rad } => {
EntityTransform::Rotate {
center,
axis,
angle_rad,
} => {
if !axis.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
crate::scene::view::transform::apply_standard_transform(
self,
*center,
*axis,
*angle_rad,
self, *center, *axis, *angle_rad,
);
return;
}
+53 -25
View File
@@ -1,5 +1,5 @@
use codec::entities::{SectionSymbol, ViewBorder};
use crate::t;
use codec::entities::{SectionSymbol, ViewBorder};
use crate::command::EntityTransform;
use crate::entities::common::{edit_angle_prop, edit_prop, parse_f64, ro_prop, square_grip};
@@ -23,9 +23,9 @@ fn apply_entity_transform<T: codec::Entity>(entity: &mut T, transform: &EntityTr
entity,
transform,
|entity, first, second| {
entity.apply_transform(
&crate::scene::view::transform::reflection_about_xy_line(first, second),
);
entity.apply_transform(&crate::scene::view::transform::reflection_about_xy_line(
first, second,
));
},
);
}
@@ -55,8 +55,7 @@ impl Grippable for SectionSymbol {
point.point.z += delta.z;
}
GripApply::Absolute(position) => {
point.point =
codec::types::Vector3::new(position.x, position.y, position.z);
point.point = codec::types::Vector3::new(position.x, position.y, position.z);
}
}
self.sync_display_fields();
@@ -86,43 +85,63 @@ impl PropertyEditable for SectionSymbol {
PropSection {
title: t!("Section Symbol").into_owned(),
props: vec![
edit_prop(t!("Symbol Scale").as_ref(), "section_scale", self.symbol_scale),
ro_prop(t!("View Symbol Version").as_ref(),
edit_prop(
t!("Symbol Scale").as_ref(),
"section_scale",
self.symbol_scale,
),
ro_prop(
t!("View Symbol Version").as_ref(),
"section_view_version",
self.view_symbol_version.to_string(),
),
ro_prop(t!("Version").as_ref(), "section_version", self.version.to_string()),
ro_prop(t!("Style").as_ref(),
ro_prop(
t!("Version").as_ref(),
"section_version",
self.version.to_string(),
),
ro_prop(
t!("Style").as_ref(),
"section_style",
handle_text(self.style_handle),
),
ro_prop(t!("View Representation").as_ref(),
ro_prop(
t!("View Representation").as_ref(),
"section_view_rep",
handle_text(self.view_rep_handle),
),
ro_prop(t!("View Symbol Flags").as_ref(),
ro_prop(
t!("View Symbol Flags").as_ref(),
"section_view_flags",
self.raw_view_symbol_70.to_string(),
),
ro_prop(t!("Point Count").as_ref(),
ro_prop(
t!("Point Count").as_ref(),
"section_point_count",
self.points.len().to_string(),
),
ro_prop(t!("Stored Point Counts").as_ref(),
ro_prop(
t!("Stored Point Counts").as_ref(),
"section_raw_counts",
format!(
"{}, {}, flags {}",
self.raw_point_count_90,
self.raw_point_record_count,
self.raw_flags_90
self.raw_point_count_90, self.raw_point_record_count, self.raw_flags_90
),
),
ro_prop(t!("Identifier").as_ref(), "section_label", self.label.clone()),
ro_prop(
t!("Identifier").as_ref(),
"section_label",
self.label.clone(),
),
],
},
PropSection {
title: t!("Section Points").into_owned(),
props: vec![ro_prop(t!("Records").as_ref(), "section_points", point_data)],
props: vec![ro_prop(
t!("Records").as_ref(),
"section_points",
point_data,
)],
},
]
}
@@ -196,25 +215,34 @@ impl PropertyEditable for ViewBorder {
props: vec![
edit_prop(t!("Center X").as_ref(), "view_border_x", self.center[0]),
edit_prop(t!("Center Y").as_ref(), "view_border_y", self.center[1]),
ro_prop(t!("Width").as_ref(),
ro_prop(
t!("Width").as_ref(),
"view_border_width",
format!("{:.6}", self.max[0] - self.min[0]),
),
ro_prop(t!("Height").as_ref(),
ro_prop(
t!("Height").as_ref(),
"view_border_height",
format!("{:.6}", self.max[1] - self.min[1]),
),
edit_prop(t!("Scale").as_ref(), "view_border_scale", self.scale),
edit_angle_prop(t!("Rotation").as_ref(),
edit_angle_prop(
t!("Rotation").as_ref(),
"view_border_rotation",
self.rotation_angle.to_degrees(),
),
ro_prop(t!("Version").as_ref(), "view_border_version", self.version.to_string()),
ro_prop(t!("Active Viewport").as_ref(),
ro_prop(
t!("Version").as_ref(),
"view_border_version",
self.version.to_string(),
),
ro_prop(
t!("Active Viewport").as_ref(),
"view_border_viewport",
handle_text(self.active_viewport),
),
ro_prop(t!("Scale Object").as_ref(),
ro_prop(
t!("Scale Object").as_ref(),
"view_border_scale_handle",
handle_text(self.scale_handle),
),
+6 -5
View File
@@ -24,7 +24,6 @@ const STANDARD_SCALES: &[(&str, f64)] = &[
("10:1", 10.0),
];
fn scale_label(scale: f64) -> String {
for (label, val) in STANDARD_SCALES {
if (scale - val).abs() < val * 0.01 {
@@ -195,7 +194,11 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
"vp_on" => {
// turn_on/turn_off keep the explicit-off bit in step, so a
// viewport the file marked off can be turned back on.
let on = if value == "toggle" { !vp.is_on() } else { value == "true" };
let on = if value == "toggle" {
!vp.is_on()
} else {
value == "true"
};
if on {
vp.turn_on();
} else {
@@ -439,9 +442,7 @@ fn apply_transform(vp: &mut Viewport, t: &EntityTransform) {
crate::impl_entity_basics!(Viewport);
impl crate::entities::traits::FallbackTess for Viewport {
fn fallback_geometry(
&self,
) -> crate::scene::convert::tess_util::FallbackGeometry {
fn fallback_geometry(&self) -> crate::scene::convert::tess_util::FallbackGeometry {
// A clipped viewport uses its linked boundary entity as its visible
// frame. Drawing the viewport's rectangular extents as well leaves an
// incorrect box around polygonal/Object MVIEW results.