forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into plugin/host-model-api
# Conflicts: # crates/ocs_plugin_api/src/process/v4.rs # src/app/update/mod.rs
This commit is contained in:
commit
8648225ac5
309 files changed
+81166
-4157
No files matched your search
+32
-14
@@ -3,8 +3,8 @@ use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro,
|
||||
square_grip,
|
||||
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, format_angle, format_area,
|
||||
format_length, parse_f64, ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::RenderConvertible;
|
||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||
@@ -197,24 +197,24 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
|
||||
vec![PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Start X").as_ref(), "start_x", format!("{sx:.4}")),
|
||||
ro(t!("Start Y").as_ref(), "start_y", format!("{sy:.4}")),
|
||||
ro(t!("Start Z").as_ref(), "start_z", format!("{sz:.4}")),
|
||||
ro(t!("Start X").as_ref(), "start_x", format_length(sx)),
|
||||
ro(t!("Start Y").as_ref(), "start_y", format_length(sy)),
|
||||
ro(t!("Start Z").as_ref(), "start_z", format_length(sz)),
|
||||
edit(t!("Center X").as_ref(), "center_x", cwx),
|
||||
edit(t!("Center Y").as_ref(), "center_y", cwy),
|
||||
edit(t!("Center Z").as_ref(), "center_z", cwz),
|
||||
ro(t!("End X").as_ref(), "end_x", format!("{ex:.4}")),
|
||||
ro(t!("End Y").as_ref(), "end_y", format!("{ey:.4}")),
|
||||
ro(t!("End Z").as_ref(), "end_z", format!("{ez:.4}")),
|
||||
ro(t!("End X").as_ref(), "end_x", format_length(ex)),
|
||||
ro(t!("End Y").as_ref(), "end_y", format_length(ey)),
|
||||
ro(t!("End Z").as_ref(), "end_z", format_length(ez)),
|
||||
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!("{total_angle:.2}")),
|
||||
ro(t!("Arc length").as_ref(), "arc_length", format!("{arc_length:.4}")),
|
||||
ro(t!("Area").as_ref(), "area", format!("{area:.4}")),
|
||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", arc.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", arc.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", arc.normal.z)),
|
||||
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),
|
||||
edit(t!("Normal Z").as_ref(), "normal_z", arc.normal.z),
|
||||
],
|
||||
}]
|
||||
}
|
||||
@@ -244,6 +244,24 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
|
||||
"radius" if v > 0.0 => arc.radius = v,
|
||||
"start_angle" => arc.start_angle = v.to_radians(),
|
||||
"end_angle" => arc.end_angle = v.to_radians(),
|
||||
"normal_x" | "normal_y" | "normal_z" => {
|
||||
let center = arc.center_wcs();
|
||||
let mut normal = cadkernel::space::Vec3::new(arc.normal.x, arc.normal.y, arc.normal.z);
|
||||
match field {
|
||||
"normal_x" => normal.x = v,
|
||||
"normal_y" => normal.y = v,
|
||||
"normal_z" => normal.z = v,
|
||||
_ => {}
|
||||
}
|
||||
if let Some(normal) = normal.normalize() {
|
||||
arc.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||
(center.x, center.y, center.z),
|
||||
(normal.x, normal.y, normal.z),
|
||||
);
|
||||
arc.center = acadrust::types::Vector3::new(x, y, z);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-4
@@ -3,7 +3,7 @@ use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
||||
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};
|
||||
@@ -169,9 +169,9 @@ fn properties(circle: &Circle) -> Vec<PropSection> {
|
||||
edit(t!("Diameter").as_ref(), "diameter", r * 2.0),
|
||||
edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r),
|
||||
edit(t!("Area").as_ref(), "area", PI * r * r),
|
||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", circle.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", circle.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", circle.normal.z)),
|
||||
edit(t!("Normal X").as_ref(), "normal_x", circle.normal.x),
|
||||
edit(t!("Normal Y").as_ref(), "normal_y", circle.normal.y),
|
||||
edit(t!("Normal Z").as_ref(), "normal_z", circle.normal.z),
|
||||
],
|
||||
}]
|
||||
}
|
||||
@@ -202,6 +202,28 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) {
|
||||
"diameter" if v > 0.0 => circle.radius = v / 2.0,
|
||||
"circumference" if v > 0.0 => circle.radius = v / (2.0 * PI),
|
||||
"area" if v > 0.0 => circle.radius = (v / PI).sqrt(),
|
||||
"normal_x" | "normal_y" | "normal_z" => {
|
||||
let center = circle.center_wcs();
|
||||
let mut normal = cadkernel::space::Vec3::new(
|
||||
circle.normal.x,
|
||||
circle.normal.y,
|
||||
circle.normal.z,
|
||||
);
|
||||
match field {
|
||||
"normal_x" => normal.x = v,
|
||||
"normal_y" => normal.y = v,
|
||||
"normal_z" => normal.z = v,
|
||||
_ => {}
|
||||
}
|
||||
if let Some(normal) = normal.normalize() {
|
||||
circle.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||
(center.x, center.y, center.z),
|
||||
(normal.x, normal.y, normal.z),
|
||||
);
|
||||
circle.center = acadrust::types::Vector3::new(x, y, z);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +380,19 @@ impl crate::entities::traits::MassPropsCalc for Circle {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn editing_normal_preserves_world_center() {
|
||||
let mut circle = Circle::from_coords(2.0, 3.0, 4.0, 5.0);
|
||||
let center = circle.center_wcs();
|
||||
|
||||
apply_geom_prop(&mut circle, "normal_x", "1");
|
||||
|
||||
assert!((circle.center_wcs() - center).length() < 1.0e-9);
|
||||
let normal_length =
|
||||
(circle.normal.x.powi(2) + circle.normal.y.powi(2) + circle.normal.z.powi(2)).sqrt();
|
||||
assert!((normal_length - 1.0).abs() < 1.0e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quadrant_grips_offer_interactive_radius() {
|
||||
use crate::entities::traits::Grippable;
|
||||
|
||||
@@ -283,6 +283,20 @@ fn surveyor(value_rad: f64, prec: usize) -> String {
|
||||
/// A leading `-` is a sign, but the `-` inside `5'-9"` is a separator — after
|
||||
/// feet there is nothing left to subtract from.
|
||||
pub fn parse_length(text: &str) -> Option<f64> {
|
||||
parse_length_unbounded(text).filter(|value| typed_value_in_range(*value))
|
||||
}
|
||||
|
||||
/// Largest magnitude a typed length or angle may have. Rust's float parser
|
||||
/// accepts `inf`, `nan` and overflowing exponents; geometry built from those,
|
||||
/// or from values whose differences overflow, breaks downstream tolerance and
|
||||
/// intersection math.
|
||||
pub const MAX_TYPED_MAGNITUDE: f64 = 1e15;
|
||||
|
||||
pub fn typed_value_in_range(value: f64) -> bool {
|
||||
value.is_finite() && value.abs() <= MAX_TYPED_MAGNITUDE
|
||||
}
|
||||
|
||||
fn parse_length_unbounded(text: &str) -> Option<f64> {
|
||||
let text = text.trim();
|
||||
let (sign, rest) = match text.strip_prefix('-') {
|
||||
Some(rest) => (-1.0, rest.trim_start()),
|
||||
@@ -339,6 +353,10 @@ fn parse_inches(text: &str) -> Option<f64> {
|
||||
/// number is read in whatever convention the drawing is set to, so what the
|
||||
/// readout shows can be typed straight back.
|
||||
pub fn parse_angle(text: &str) -> Option<f64> {
|
||||
parse_angle_unbounded(text).filter(|value| typed_value_in_range(*value))
|
||||
}
|
||||
|
||||
fn parse_angle_unbounded(text: &str) -> Option<f64> {
|
||||
let text = text.trim();
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
@@ -679,11 +697,16 @@ pub fn parse_f64(value: &str) -> Option<f64> {
|
||||
.ok()
|
||||
.or_else(|| parse_length(t))
|
||||
.or_else(|| parse_angle_deg(t))
|
||||
.filter(|value| typed_value_in_range(*value))
|
||||
}
|
||||
|
||||
/// Parse an angle string the panel displayed via AUNITS back to DEGREES:
|
||||
/// "30", "30°"/"30d", DMS "30°15'20.5\"", grads "33.33g", radians "0.52r".
|
||||
pub fn parse_angle_deg(value: &str) -> Option<f64> {
|
||||
parse_angle_deg_unbounded(value).filter(|value| typed_value_in_range(*value))
|
||||
}
|
||||
|
||||
fn parse_angle_deg_unbounded(value: &str) -> Option<f64> {
|
||||
let s = value.trim();
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
@@ -1161,6 +1184,24 @@ pub(crate) fn polyline_segment_fill(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod typed_range_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn non_finite_and_huge_typed_values_are_rejected() {
|
||||
for text in ["inf", "-inf", "nan", "1e400", "1e308"] {
|
||||
assert_eq!(parse_length(text), None, "length {text}");
|
||||
assert_eq!(parse_angle(text), None, "angle {text}");
|
||||
assert_eq!(parse_f64(text), None, "property {text}");
|
||||
assert_eq!(parse_angle_deg(text), None, "angle degrees {text}");
|
||||
}
|
||||
assert_eq!(parse_length("1e16"), None);
|
||||
assert_eq!(parse_length("1e15"), Some(1e15));
|
||||
assert_eq!(parse_length("-5'-6\""), Some(-66.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod length_format_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -235,6 +235,65 @@ pub fn set(doc: &mut CadDocument, handle: Handle, code: i16, value: Option<XData
|
||||
write_pairs(doc, handle, kept);
|
||||
}
|
||||
|
||||
/// Set — or, with `value: None`, clear — a single override on an entity that
|
||||
/// is **not yet in the document** (a command's pending commit), leaving the
|
||||
/// other overrides intact.
|
||||
///
|
||||
/// The document-backed [`set`] additionally registers the `ACAD` APPID; that
|
||||
/// APPID is present in every drawing OCS writes, and committing the entity
|
||||
/// goes through the normal add path, so an uncommitted write needs no
|
||||
/// document. Keeping the override on the entity means it is part of the single
|
||||
/// committed object and rides the ordinary undo/redo history.
|
||||
pub fn set_on_entity(entity: &mut EntityType, code: i16, value: Option<XDataValue>) {
|
||||
let common = entity.common_mut();
|
||||
let mut kept: Vec<(i16, XDataValue)> = pairs(&common.extended_data)
|
||||
.into_iter()
|
||||
.filter(|(c, _)| *c != code)
|
||||
.collect();
|
||||
if let Some(v) = value {
|
||||
kept.push((code, v));
|
||||
}
|
||||
|
||||
let legacy = common.extended_data.get_record("ACAD_DSTYLE").is_some()
|
||||
&& common.extended_data.get_record("ACAD").is_some_and(
|
||||
|rec| !matches!(rec.values.first(), Some(XDataValue::String(n)) if n == "DSTYLE"),
|
||||
);
|
||||
let app = if legacy { "ACAD_DSTYLE" } else { "ACAD" };
|
||||
|
||||
let values = if kept.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut vals = Vec::new();
|
||||
if !legacy {
|
||||
vals.push(XDataValue::String("DSTYLE".to_string()));
|
||||
}
|
||||
vals.push(XDataValue::ControlString("{".to_string()));
|
||||
for (code, value) in kept {
|
||||
vals.push(XDataValue::Integer16(code));
|
||||
vals.push(value);
|
||||
}
|
||||
vals.push(XDataValue::ControlString("}".to_string()));
|
||||
Some(vals)
|
||||
};
|
||||
|
||||
let mut rebuilt = ExtendedData::new();
|
||||
for record in common.extended_data.records() {
|
||||
if record.application_name != app && !(!legacy && record.application_name == "ACAD_DSTYLE")
|
||||
{
|
||||
rebuilt.add_record(record.clone());
|
||||
}
|
||||
}
|
||||
if let Some(vals) = values {
|
||||
let mut record = acadrust::xdata::ExtendedDataRecord::new(app);
|
||||
for v in vals {
|
||||
record.add_value(v);
|
||||
}
|
||||
rebuilt.add_record(record);
|
||||
}
|
||||
rebuilt.raw_dwg_eed = common.extended_data.raw_dwg_eed.clone();
|
||||
common.extended_data = rebuilt;
|
||||
}
|
||||
|
||||
pub fn property_real_code(field: &str) -> Option<i16> {
|
||||
Some(match field {
|
||||
"dim_arrow_size" => DIMASZ,
|
||||
|
||||
+1135
-156
File diff suppressed because it is too large.
Load diff
+52
-19
@@ -9,8 +9,8 @@ use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_prop as edit, oriented_triangle_grip, parse_f64, ro_prop as ro,
|
||||
square_grip,
|
||||
center_grip, edit_prop as edit, format_area, format_length, oriented_triangle_grip, parse_f64,
|
||||
ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::RenderConvertible;
|
||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||
@@ -163,33 +163,33 @@ fn properties(ell: &Ellipse) -> Vec<PropSection> {
|
||||
vec![PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Start X").as_ref(), "start_x", format!("{:.4}", start.x)),
|
||||
ro(t!("Start Y").as_ref(), "start_y", format!("{:.4}", start.y)),
|
||||
ro(t!("Start Z").as_ref(), "start_z", format!("{:.4}", start.z)),
|
||||
ro(t!("Start X").as_ref(), "start_x", format_length(start.x)),
|
||||
ro(t!("Start Y").as_ref(), "start_y", format_length(start.y)),
|
||||
ro(t!("Start Z").as_ref(), "start_z", format_length(start.z)),
|
||||
edit(t!("Center X").as_ref(), "center_x", ell.center.x),
|
||||
edit(t!("Center Y").as_ref(), "center_y", ell.center.y),
|
||||
edit(t!("Center Z").as_ref(), "center_z", ell.center.z),
|
||||
ro(t!("End X").as_ref(), "end_x", format!("{:.4}", end.x)),
|
||||
ro(t!("End Y").as_ref(), "end_y", format!("{:.4}", end.y)),
|
||||
ro(t!("End Z").as_ref(), "end_z", format!("{:.4}", end.z)),
|
||||
ro(t!("End X").as_ref(), "end_x", format_length(end.x)),
|
||||
ro(t!("End Y").as_ref(), "end_y", format_length(end.y)),
|
||||
ro(t!("End Z").as_ref(), "end_z", format_length(end.z)),
|
||||
edit(t!("Major radius").as_ref(), "major_r", r_major),
|
||||
edit(t!("Minor radius").as_ref(), "minor_r", r_minor),
|
||||
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!("{:.4}", major_vec.x)),
|
||||
ro(t!("Major axis vector Y").as_ref(), "major_y", format!("{:.4}", major_vec.y)),
|
||||
ro(t!("Major axis vector Z").as_ref(), "major_z", format!("{:.4}", major_vec.z)),
|
||||
ro(t!("Minor axis vector X").as_ref(), "minor_x", format!("{:.4}", minor_vec.x)),
|
||||
ro(t!("Minor axis vector Y").as_ref(), "minor_y", format!("{:.4}", minor_vec.y)),
|
||||
ro(t!("Minor axis vector Z").as_ref(), "minor_z", format!("{:.4}", minor_vec.z)),
|
||||
ro(t!("Area").as_ref(), "area", format!("{:.4}", props.area)),
|
||||
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!("{:.4}", props.perimeter)),
|
||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", ell.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", ell.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", ell.normal.z)),
|
||||
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),
|
||||
],
|
||||
}]
|
||||
}
|
||||
@@ -220,6 +220,26 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) {
|
||||
"ratio" if v > 0.0 => ell.minor_axis_ratio = v,
|
||||
"start_angle" => ell.start_parameter = v.to_radians(),
|
||||
"end_angle" => ell.end_parameter = v.to_radians(),
|
||||
"normal_x" | "normal_y" | "normal_z" => {
|
||||
let mut normal = [ell.normal.x, ell.normal.y, ell.normal.z];
|
||||
match field {
|
||||
"normal_x" => normal[0] = v,
|
||||
"normal_y" => normal[1] = v,
|
||||
"normal_z" => normal[2] = v,
|
||||
_ => {}
|
||||
}
|
||||
let Some(normal) = cadkernel::space::Vec3::from(normal).normalize() else {
|
||||
return;
|
||||
};
|
||||
let Some(major) = cadkernel::space::reorient_axis_to_plane(
|
||||
[ell.major_axis.x, ell.major_axis.y, ell.major_axis.z],
|
||||
normal.to_array(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
ell.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||
ell.major_axis = acadrust::types::Vector3::new(major[0], major[1], major[2]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -424,6 +444,19 @@ mod grip_tests {
|
||||
(v.major_axis.x * v.major_axis.x + v.major_axis.y * v.major_axis.y).sqrt()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_normal_reorients_major_axis_in_kernel() {
|
||||
let mut e = ell(10.0, 0.5);
|
||||
|
||||
apply_geom_prop(&mut e, "normal_x", "1");
|
||||
|
||||
let normal = DVec3::new(e.normal.x, e.normal.y, e.normal.z);
|
||||
let major = DVec3::new(e.major_axis.x, e.major_axis.y, e.major_axis.z);
|
||||
assert!((normal.length() - 1.0).abs() < 1.0e-12);
|
||||
assert!((major.length() - 10.0).abs() < 1.0e-9);
|
||||
assert!(normal.dot(major).abs() < 1.0e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minor_grip_below_major_updates_ratio_only() {
|
||||
let mut e = ell(10.0, 0.5);
|
||||
|
||||
@@ -428,8 +428,8 @@ 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", format!("{:.4}", area)),
|
||||
ro(t!("Cumulative area").as_ref(), "cumulative_area", format!("{:.4}", 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 {
|
||||
@@ -537,8 +537,8 @@ 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", format!("{:.4}", area)),
|
||||
ro(t!("Cumulative area").as_ref(), "cumulative_area", format!("{:.4}", 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)),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_prop as edit, oriented_triangle_grip, parse_f64, ro_prop as ro,
|
||||
square_grip,
|
||||
center_grip, edit_prop as edit, format_direction, format_length, oriented_triangle_grip,
|
||||
parse_f64, ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::RenderConvertible;
|
||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||
@@ -187,7 +187,7 @@ fn properties(line: &Line) -> Vec<PropSection> {
|
||||
"centerline_end_extension",
|
||||
association.end_extension,
|
||||
),
|
||||
ro(t!("Length").as_ref(), "length", format!("{:.4}", line.length())),
|
||||
ro(t!("Length").as_ref(), "length", format_length(line.length())),
|
||||
ro(
|
||||
"Associative",
|
||||
"centerline_associative",
|
||||
@@ -199,7 +199,7 @@ fn properties(line: &Line) -> Vec<PropSection> {
|
||||
let dx = line.end.x - line.start.x;
|
||||
let dy = line.end.y - line.start.y;
|
||||
let dz = line.end.z - line.start.z;
|
||||
let angle = dy.atan2(dx).to_degrees().rem_euclid(360.0);
|
||||
let angle = dy.atan2(dx);
|
||||
vec![PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
@@ -209,11 +209,11 @@ fn properties(line: &Line) -> Vec<PropSection> {
|
||||
edit(t!("End X").as_ref(), "end_x", line.end.x),
|
||||
edit(t!("End Y").as_ref(), "end_y", line.end.y),
|
||||
edit(t!("End Z").as_ref(), "end_z", line.end.z),
|
||||
ro(t!("Delta X").as_ref(), "delta_x", format!("{dx:.4}")),
|
||||
ro(t!("Delta Y").as_ref(), "delta_y", format!("{dy:.4}")),
|
||||
ro(t!("Delta Z").as_ref(), "delta_z", format!("{dz:.4}")),
|
||||
ro(t!("Length").as_ref(), "length", format!("{:.4}", line.length())),
|
||||
ro(t!("Angle").as_ref(), "angle", format!("{angle:.2}")),
|
||||
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!("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),
|
||||
edit(t!("Normal Z").as_ref(), "normal_z", line.normal.z),
|
||||
|
||||
@@ -143,6 +143,7 @@ pub fn dxf_name(e: &EntityType) -> &'static str {
|
||||
EntityType::MLine(_) => "MLINE",
|
||||
EntityType::RasterImage(_) => "RASTERIMAGE",
|
||||
EntityType::Wipeout(_) => "WIPEOUT",
|
||||
EntityType::Ole2Frame(_) => "OLE2FRAME",
|
||||
EntityType::Underlay(_) => "UNDERLAY",
|
||||
EntityType::AttributeDefinition(_) => "ATTDEF",
|
||||
EntityType::AttributeEntity(_) => "ATTRIB",
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip,
|
||||
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};
|
||||
@@ -705,8 +705,8 @@ impl PropertyEditable for Polyline2D {
|
||||
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),
|
||||
ro(t!("Area").as_ref(), "pl2_area", format!("{area:.4}")),
|
||||
ro(t!("Length").as_ref(), "pl2_length", format!("{length:.4}")),
|
||||
ro(t!("Area").as_ref(), "pl2_area", format_area(area)),
|
||||
ro(t!("Length").as_ref(), "pl2_length", format_length(length)),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
|
||||
@@ -301,9 +301,9 @@ 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", format!("{:.4}", rotation_deg)),
|
||||
ro(t!("Width").as_ref(), "ri_width", format!("{:.4}", self.width())),
|
||||
ro(t!("Height").as_ref(), "ri_height", format!("{:.4}", 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)),
|
||||
],
|
||||
},
|
||||
|
||||
+28
-1
@@ -80,7 +80,14 @@ fn boundary_edges(points: &[[f64; 3]]) -> Vec<Edge> {
|
||||
}
|
||||
|
||||
fn kernel_tolerance(points: &[[f64; 3]]) -> f64 {
|
||||
cadkernel::space::coplanarity_tolerance(points).max(1.0e-12)
|
||||
let tolerance = cadkernel::space::coplanarity_tolerance(points);
|
||||
// Corners whose extent overflows f64 give an infinite tolerance, which
|
||||
// `Tolerance::new` rejects by panicking.
|
||||
if tolerance.is_finite() {
|
||||
tolerance.max(1.0e-12)
|
||||
} else {
|
||||
1.0e-12
|
||||
}
|
||||
}
|
||||
|
||||
fn extrusion_body(
|
||||
@@ -366,3 +373,23 @@ impl Transformable for Solid {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod overflow_tests {
|
||||
use super::*;
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
/// Corners 1e308 apart overflow the coplanarity tolerance to infinity,
|
||||
/// which the kernel's `Tolerance::new` rejects by panicking.
|
||||
#[test]
|
||||
fn corners_with_overflowing_extent_do_not_panic() {
|
||||
let solid = Solid::new(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1e308, 0.0, 0.0),
|
||||
Vector3::new(0.0, 1e308, 0.0),
|
||||
Vector3::new(1e308, 1e308, 0.0),
|
||||
);
|
||||
let _ = wcs_bounds(&solid);
|
||||
let _ = solid_geometry(&solid);
|
||||
}
|
||||
}
|
||||
+245
-19
@@ -18,7 +18,13 @@ pub(crate) fn uses_fit_method(spline: &Spline) -> bool {
|
||||
}
|
||||
|
||||
pub(crate) fn shows_fit_points(spline: &Spline) -> bool {
|
||||
uses_fit_method(spline) && (!spline.cv_frame_visible || spline.flags.periodic)
|
||||
!spline.fit_points.is_empty() && (!spline.cv_frame_visible || spline.flags.periodic)
|
||||
}
|
||||
|
||||
pub(crate) fn shows_control_vertices(spline: &Spline) -> bool {
|
||||
!shows_fit_points(spline)
|
||||
&& !(uses_fit_method(spline) && spline.flags.periodic)
|
||||
&& control_vertices(spline).len() >= 2
|
||||
}
|
||||
|
||||
pub(crate) fn fit_nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
|
||||
@@ -67,13 +73,59 @@ pub(crate) fn nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
|
||||
Some(curve.with_periodicity(spline.flags.closed || spline.flags.periodic))
|
||||
}
|
||||
|
||||
/// NURBS knot locations on a control-point spline, evaluated on its own
|
||||
/// curve: distinct interior knot values (the clamped ends already snap as
|
||||
/// endpoints). Fit-method splines invent their knots at the fit nodes, which
|
||||
/// already snap as endpoints, so they contribute nothing — re-offering them
|
||||
/// would double every marker.
|
||||
pub(crate) fn spline_knot_points(spline: &Spline) -> Vec<glam::DVec3> {
|
||||
const MAX_KNOT_SNAPS: usize = 1024;
|
||||
if uses_fit_method(spline) {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(curve) = nurbs3(spline) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (from, to) = curve.domain();
|
||||
if !(from.is_finite() && to.is_finite()) || !(to > from) {
|
||||
return Vec::new();
|
||||
}
|
||||
let span = to - from;
|
||||
let mut knots: Vec<f64> = curve
|
||||
.knots()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|k| k.is_finite() && *k > from + span * 1e-9 && *k < to - span * 1e-9)
|
||||
.collect();
|
||||
knots.sort_by(f64::total_cmp);
|
||||
knots.dedup();
|
||||
if knots.len() > MAX_KNOT_SNAPS {
|
||||
let step = knots.len().div_ceil(MAX_KNOT_SNAPS);
|
||||
knots = knots.into_iter().step_by(step).collect();
|
||||
}
|
||||
knots
|
||||
.into_iter()
|
||||
.map(|k| glam::DVec3::from_array(curve.point_at_knot(k)))
|
||||
.filter(|p| p.is_finite())
|
||||
.collect()
|
||||
}
|
||||
|
||||
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| acadrust::types::Vector3::new(point[0], point[1], point[2])).collect();
|
||||
spline.weights = curve.weights().to_vec();
|
||||
spline.fit_points.clear();
|
||||
if !fit_points.is_empty() {
|
||||
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()) {
|
||||
*last = *point;
|
||||
}
|
||||
}
|
||||
spline.fit_points = fit_points;
|
||||
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
||||
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
||||
spline.flags.rational = spline.weights.windows(2)
|
||||
@@ -134,6 +186,11 @@ fn to_render(spl: &Spline) -> RenderEntity {
|
||||
crate::scene::model::wire_model::SnapHint::Endpoint,
|
||||
)
|
||||
}));
|
||||
snap_pts.extend(
|
||||
spline_knot_points(spl)
|
||||
.into_iter()
|
||||
.map(|p| (p, crate::scene::model::wire_model::SnapHint::Knot)),
|
||||
);
|
||||
let key_vertices = if planar.is_some() {
|
||||
Vec::new()
|
||||
} else {
|
||||
@@ -179,6 +236,11 @@ fn to_render(spl: &Spline) -> RenderEntity {
|
||||
)
|
||||
}));
|
||||
}
|
||||
points.extend(
|
||||
spline_knot_points(spl)
|
||||
.into_iter()
|
||||
.map(|p| (p, crate::scene::model::wire_model::SnapHint::Knot)),
|
||||
);
|
||||
(points, Vec::new())
|
||||
}
|
||||
// A spline through points in space is not a planar curve, so the
|
||||
@@ -568,7 +630,15 @@ fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> P
|
||||
|
||||
fn convert_to_control_method(spline: &mut Spline) -> bool {
|
||||
if !uses_fit_method(spline) {
|
||||
return true;
|
||||
let valid = spline.control_points.len() >= 2;
|
||||
if valid {
|
||||
spline.fit_points.clear();
|
||||
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
||||
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
||||
spline.dwg_flags1 &= !1;
|
||||
spline.dxf_flags &= !(32 | 1024);
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
let Some(planar) = crate::entities::curve::spline_curve(spline) else {
|
||||
return false;
|
||||
@@ -600,18 +670,74 @@ fn convert_to_control_method(spline: &mut Spline) -> bool {
|
||||
}
|
||||
|
||||
fn convert_to_fit_method(spline: &mut Spline) -> bool {
|
||||
uses_fit_method(spline)
|
||||
if uses_fit_method(spline) {
|
||||
return true;
|
||||
}
|
||||
if spline.fit_points.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
spline.control_points.clear();
|
||||
spline.knots.clear();
|
||||
spline.weights.clear();
|
||||
spline.flags.rational = false;
|
||||
spline.dwg_flags1 |= 1;
|
||||
spline.dxf_flags |= 32 | 1024;
|
||||
true
|
||||
}
|
||||
|
||||
fn derived_fit_points(spline: &Spline) -> Vec<acadrust::types::Vector3> {
|
||||
if spline.flags.closed || spline.flags.periodic {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(curve) = nurbs3(spline) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (start, end) = curve.domain();
|
||||
if !(start.is_finite() && end.is_finite() && end > start) {
|
||||
return Vec::new();
|
||||
}
|
||||
let tolerance = (end - start).abs().max(1.0) * 1.0e-12;
|
||||
let mut parameters = vec![start];
|
||||
for knot in curve.knots().iter().copied() {
|
||||
if knot > start + tolerance
|
||||
&& knot < end - tolerance
|
||||
&& parameters
|
||||
.last()
|
||||
.is_none_or(|previous| (knot - *previous).abs() > tolerance)
|
||||
{
|
||||
parameters.push(knot);
|
||||
}
|
||||
}
|
||||
parameters.push(end);
|
||||
parameters
|
||||
.into_iter()
|
||||
.map(|parameter| {
|
||||
let point = curve.point_at_knot(parameter);
|
||||
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn can_show_fit_points(spline: &Spline) -> bool {
|
||||
spline.fit_points.len() >= 2 || derived_fit_points(spline).len() >= 2
|
||||
}
|
||||
|
||||
fn prepare_fit_point_view(spline: &mut Spline) -> bool {
|
||||
if spline.fit_points.len() < 2 {
|
||||
spline.fit_points = derived_fit_points(spline);
|
||||
}
|
||||
spline.fit_points.len() >= 2
|
||||
}
|
||||
|
||||
fn grips(spline: &Spline) -> Vec<GripDef> {
|
||||
let fit_method = uses_fit_method(spline);
|
||||
let derived_control = control_vertices(spline);
|
||||
let show_control_vertices = (spline.cv_frame_visible || !fit_method)
|
||||
&& !(fit_method && spline.flags.periodic)
|
||||
let show_fit = shows_fit_points(spline);
|
||||
let show_control_vertices = !show_fit
|
||||
&& !(uses_fit_method(spline) && spline.flags.periodic)
|
||||
&& derived_control.len() >= 2;
|
||||
let source = if show_control_vertices {
|
||||
derived_control
|
||||
} else if fit_method {
|
||||
} else if show_fit {
|
||||
spline.fit_points.clone()
|
||||
} else {
|
||||
spline.control_points.clone()
|
||||
@@ -838,7 +964,7 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
||||
match field {
|
||||
"spline_method" => {
|
||||
if value == "Fit" {
|
||||
if convert_to_fit_method(spline) {
|
||||
if prepare_fit_point_view(spline) {
|
||||
spline.cv_frame_visible = false;
|
||||
}
|
||||
} else if value == "Control Vertices" {
|
||||
@@ -849,6 +975,12 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
||||
return;
|
||||
}
|
||||
"knot_param" => {
|
||||
if shows_fit_points(spline)
|
||||
&& !uses_fit_method(spline)
|
||||
&& !convert_to_fit_method(spline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
spline.knot_parameterization = match value {
|
||||
"Chord" => 0,
|
||||
"Square Root" => 1,
|
||||
@@ -883,9 +1015,30 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
||||
{
|
||||
return;
|
||||
}
|
||||
if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight")
|
||||
&& uses_fit_method(spline)
|
||||
&& !convert_to_control_method(spline)
|
||||
if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight") {
|
||||
if !shows_fit_points(spline) && !spline.fit_points.is_empty() {
|
||||
if !convert_to_control_method(spline) {
|
||||
return;
|
||||
}
|
||||
} else if uses_fit_method(spline) && !convert_to_control_method(spline) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if shows_fit_points(spline)
|
||||
&& !uses_fit_method(spline)
|
||||
&& matches!(
|
||||
field,
|
||||
"fit_pt_x"
|
||||
| "fit_pt_y"
|
||||
| "fit_pt_z"
|
||||
| "start_tan_x"
|
||||
| "start_tan_y"
|
||||
| "start_tan_z"
|
||||
| "end_tan_x"
|
||||
| "end_tan_y"
|
||||
| "end_tan_z"
|
||||
)
|
||||
&& !convert_to_fit_method(spline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -968,16 +1121,23 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
||||
return;
|
||||
}
|
||||
// First CV edit makes the displayed kernel controls authoritative.
|
||||
let fit_method = uses_fit_method(spline);
|
||||
if spline.cv_frame_visible && fit_method && !spline.flags.periodic {
|
||||
let editing_fit = shows_fit_points(spline);
|
||||
if editing_fit && !uses_fit_method(spline) {
|
||||
if !convert_to_fit_method(spline) {
|
||||
return;
|
||||
}
|
||||
} else if !editing_fit
|
||||
&& (uses_fit_method(spline) || !spline.fit_points.is_empty())
|
||||
&& !spline.flags.periodic
|
||||
{
|
||||
if !convert_to_control_method(spline) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let target = if (spline.cv_frame_visible && !spline.flags.periodic) || !fit_method {
|
||||
spline.control_points.get_mut(grip_id)
|
||||
} else {
|
||||
let target = if editing_fit {
|
||||
spline.fit_points.get_mut(grip_id)
|
||||
} else {
|
||||
spline.control_points.get_mut(grip_id)
|
||||
};
|
||||
if let Some(cp) = target {
|
||||
match apply {
|
||||
@@ -1041,9 +1201,10 @@ impl crate::entities::traits::Grippable for Spline {
|
||||
let fit_method = uses_fit_method(self);
|
||||
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![
|
||||
(can_show_control && fit_method).then_some(GripMenuItem {
|
||||
can_show_fit.then_some(GripMenuItem {
|
||||
label: "Fit",
|
||||
action: GripMenuAction::ShowFit,
|
||||
}),
|
||||
@@ -1084,7 +1245,7 @@ impl crate::entities::traits::Grippable for Spline {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
match action {
|
||||
A::ShowFit => {
|
||||
if convert_to_fit_method(self) {
|
||||
if prepare_fit_point_view(self) {
|
||||
self.cv_frame_visible = false;
|
||||
return;
|
||||
}
|
||||
@@ -1120,6 +1281,45 @@ impl crate::entities::traits::Transformable for Spline {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn knot_points_evaluate_distinct_interior_knots() {
|
||||
let mut spline = Spline::default();
|
||||
spline.degree = 3;
|
||||
spline.control_points = vec![
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(1.0, 2.0, 0.0),
|
||||
acadrust::types::Vector3::new(3.0, 3.0, 1.0),
|
||||
acadrust::types::Vector3::new(5.0, 2.0, 0.0),
|
||||
acadrust::types::Vector3::new(6.0, 0.0, 1.0),
|
||||
];
|
||||
spline.knots = vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0];
|
||||
let pts = spline_knot_points(&spline);
|
||||
assert_eq!(pts.len(), 1, "one interior knot value: {pts:?}");
|
||||
let curve = nurbs3(&spline).expect("control-point spline builds");
|
||||
let expected = curve.point_at_knot(0.5);
|
||||
let got = [pts[0].x, pts[0].y, pts[0].z];
|
||||
let err = ((got[0] - expected[0]).powi(2)
|
||||
+ (got[1] - expected[1]).powi(2)
|
||||
+ (got[2] - expected[2]).powi(2))
|
||||
.sqrt();
|
||||
assert!(err < 1e-9, "knot point off the curve: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_method_splines_emit_no_knot_snaps() {
|
||||
// Interpolated curves invent their own knots at the fit nodes, which
|
||||
// already snap as endpoints — re-offering them as knots would double
|
||||
// every marker.
|
||||
let mut spline = Spline::default();
|
||||
spline.degree = 3;
|
||||
spline.fit_points = vec![
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(4.0, 0.0, 1.0),
|
||||
acadrust::types::Vector3::new(4.0, 3.0, 2.0),
|
||||
];
|
||||
assert!(spline_knot_points(&spline).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_bounds_accept_closed_curves_with_unset_tangents() {
|
||||
let mut spline = Spline::default();
|
||||
@@ -1190,4 +1390,30 @@ mod tests {
|
||||
acadrust::types::Vector3::new(0.0, 2.5, 0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn showing_derived_fit_points_does_not_change_the_curve() {
|
||||
let mut spline = Spline::default();
|
||||
spline.degree = 3;
|
||||
spline.control_points = vec![
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(1.0, 3.0, 0.0),
|
||||
acadrust::types::Vector3::new(4.0, 3.0, 0.0),
|
||||
acadrust::types::Vector3::new(5.0, 0.0, 0.0),
|
||||
];
|
||||
spline.knots = cadkernel::space::clamped_uniform_knots(3, 4);
|
||||
let before = nurbs3(&spline).unwrap().point_at(0.37);
|
||||
|
||||
assert!(prepare_fit_point_view(&mut spline));
|
||||
spline.cv_frame_visible = false;
|
||||
|
||||
assert!(shows_fit_points(&spline));
|
||||
assert!(!uses_fit_method(&spline));
|
||||
let after = nurbs3(&spline).unwrap().point_at(0.37);
|
||||
assert!(
|
||||
cadkernel::space::Vec3::from(before)
|
||||
.distance(cadkernel::space::Vec3::from(after))
|
||||
< 1.0e-12
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,12 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT
|
||||
.map(std::path::Path::new)
|
||||
.and_then(|p| p.parent());
|
||||
crate::io::resolve_image_file(file, base)
|
||||
// Fonts fetched from the community repository live in
|
||||
// the per-user fonts folder — search it last.
|
||||
.or_else(|| {
|
||||
crate::io::font_repo::local_font_file(file)
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
if let Some(p) = shx_path {
|
||||
@@ -191,7 +197,7 @@ pub fn text_local_bounds(
|
||||
/// Expand DXF `%%x` special-character sequences that appear in both TEXT and MTEXT values:
|
||||
/// - `%%d` / `%%D` → `°`
|
||||
/// - `%%p` / `%%P` → `±`
|
||||
/// - `%%c` / `%%C` → `⌀`
|
||||
/// - `%%c` / `%%C` → `∅` (U+2205, the native diameter sign)
|
||||
/// - `%%u` / `%%U` → underline toggle (stripped — not renderable with stroke fonts)
|
||||
/// - `%%o` / `%%O` → overline toggle (stripped)
|
||||
/// - `%%%%` → `%`
|
||||
@@ -218,7 +224,7 @@ pub fn resolve_dxf_special_chars(s: &str) -> String {
|
||||
}
|
||||
Some('c') => {
|
||||
chars.next();
|
||||
out.push('⌀');
|
||||
out.push('∅');
|
||||
}
|
||||
Some('u') | Some('o') => {
|
||||
chars.next();
|
||||
@@ -792,9 +798,11 @@ pub fn reorder_line_atoms(atoms: Vec<LayoutAtom>, is_rtl: bool) -> Vec<LayoutAto
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let mut lvl = None;
|
||||
for i in r.clone() {
|
||||
// `bidi.levels` is indexed by byte, so walk characters rather than
|
||||
// bytes to avoid slicing inside a multi-byte character.
|
||||
for (offset, c) in line_text[r.clone()].char_indices() {
|
||||
let i = r.start + offset;
|
||||
if i < bidi.levels.len() {
|
||||
let c = line_text[i..].chars().next().unwrap_or(' ');
|
||||
match unicode_bidi::bidi_class(c) {
|
||||
unicode_bidi::BidiClass::L
|
||||
| unicode_bidi::BidiClass::R
|
||||
@@ -2960,6 +2968,12 @@ mod v_anchor_tests {
|
||||
reordered_b,
|
||||
vec!["2026 ", "is ", "here!", "שנה ", "שלום ", "مرحبا! ", "ہے۔ ", "اردو ", "یہ "]
|
||||
);
|
||||
|
||||
// Case 6: a word starting with multi-byte neutral punctuation must
|
||||
// not be sliced inside that character.
|
||||
let words6 = ["שלום", " ", "«x"];
|
||||
let reordered6 = reorder_atoms(&words6, false);
|
||||
assert_eq!(reordered6, vec!["שלום", " ", "«x"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -234,8 +234,8 @@ impl PropertyEditable for Underlay {
|
||||
edit(t!("Scale X").as_ref(), "ul_sx", self.x_scale),
|
||||
edit(t!("Scale Y").as_ref(), "ul_sy", self.y_scale),
|
||||
edit(t!("Scale Z").as_ref(), "ul_sz", self.z_scale),
|
||||
ro(t!("Width").as_ref(), "ul_width", format!("{:.4}", width)),
|
||||
ro(t!("Height").as_ref(), "ul_height", format!("{:.4}", height)),
|
||||
ro(t!("Width").as_ref(), "ul_width", crate::entities::common::format_length(width)),
|
||||
ro(t!("Height").as_ref(), "ul_height", crate::entities::common::format_length(height)),
|
||||
edit_angle(t!("Rotation").as_ref(), "ul_rot", self.rotation.to_degrees()),
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user