forked from 1b-agent/opencadstudio-upstream
style: apply cargo fmt across workspace
This commit is contained in:
1 parent
85710e5454
commit
9cf9ff2c5c
531 files changed
+57740
-33906
No files matched your search
+20
-18
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+53
-11
@@ -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
File diff suppressed because it is too large.
Load diff
+117
-25
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = ®ion.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(®ion);
|
||||
|
||||
+63
-54
@@ -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
File diff suppressed because it is too large.
Load diff
+24
-29
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user