mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
chore: resolve SonarCloud maintainability issues (#2025)
* chore: resolve SonarCloud maintainability issues * style: format with rustfmt * fix(engine): reference DocState instead of DrawingState * fix: restore cmd wildcard imports and remove unused Arc import * Unit tests for the extracted mleader alignment steps and spacemouse filter * test: fix assertions flagged in review and apply rustfmt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: initialise drift filter in struct literal for clippy Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Dylan Taylor <dylan@craftmaker.dev> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
Dylan Taylor
parent
e59195edb8
commit
63394e872d
@@ -107,6 +107,23 @@ impl Filter {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
fn update_drift(&mut self, complete: bool, now: Instant, bias: &mut [f64; 6]) {
|
||||
if !complete {
|
||||
return;
|
||||
}
|
||||
if self.raw.iter().zip(*bias).all(|(raw, b)| (*raw - b).abs() <= FULL_SCALE * DEADZONE) {
|
||||
let since = *self.idle_since.get_or_insert(now);
|
||||
if now.saturating_duration_since(since) >= Duration::from_secs(2) {
|
||||
for (b, raw) in bias.iter_mut().zip(self.raw) {
|
||||
*b += (raw - *b) * 0.01;
|
||||
}
|
||||
self.bias = Some(*bias);
|
||||
}
|
||||
} else {
|
||||
self.idle_since = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, values: [i16; 6], mask: u8, now: Instant) -> Motion {
|
||||
for (index, seen) in self.seen.iter_mut().enumerate() {
|
||||
if mask & (1 << index) != 0 {
|
||||
@@ -128,19 +145,7 @@ impl Filter {
|
||||
}
|
||||
let Some(mut bias) = self.bias else { return Motion::default() };
|
||||
// Follow small resting drift only after two seconds entirely inside the deadzone.
|
||||
if complete {
|
||||
if self.raw.iter().zip(bias).all(|(raw, bias)| (*raw - bias).abs() <= FULL_SCALE * DEADZONE) {
|
||||
let since = *self.idle_since.get_or_insert(now);
|
||||
if now.saturating_duration_since(since) >= Duration::from_secs(2) {
|
||||
for (bias, raw) in bias.iter_mut().zip(self.raw) {
|
||||
*bias += (raw - *bias) * 0.01;
|
||||
}
|
||||
self.bias = Some(bias);
|
||||
}
|
||||
} else {
|
||||
self.idle_since = None;
|
||||
}
|
||||
}
|
||||
self.update_drift(complete, now, &mut bias);
|
||||
let mut axes = [0.0; 6];
|
||||
for ((axis, raw), bias) in axes.iter_mut().zip(self.raw).zip(bias) {
|
||||
let value = ((raw - bias) / FULL_SCALE).clamp(-1.0, 1.0);
|
||||
@@ -156,3 +161,77 @@ impl Filter {
|
||||
Motion { pan: [tx, ty], zoom: (tz + rz).clamp(-1.0, 1.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_reports() {
|
||||
assert_eq!(decode(&[]), None);
|
||||
assert_eq!(decode(&[3]), None);
|
||||
assert_eq!(decode(&[3, 5]), Some(Report::Buttons(5)));
|
||||
let values = [1_i16, -2, 3, 4, -5, 6];
|
||||
let mut bytes = vec![1_u8];
|
||||
for v in values {
|
||||
bytes.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
assert_eq!(decode(&bytes), Some(Report::Axes { values, mask: 3 }));
|
||||
bytes[0] = 2;
|
||||
assert_eq!(decode(&bytes), Some(Report::Axes { values: [0, 0, 0, 1, -2, 3], mask: 2 }));
|
||||
assert_eq!(decode(&[2, 1, 2, 3]), None, "too short for a rotation report");
|
||||
}
|
||||
|
||||
/// Sixteen complete reports at rest, spread over more than CALIBRATION_TIME, all stamped
|
||||
/// on a made-up point in time big enough that they sit in the past of later updates.
|
||||
fn calibrated(t0: Instant) -> Filter {
|
||||
let mut f = Filter::default();
|
||||
for i in 0..17 {
|
||||
let at = t0 + Duration::from_millis(50 * i);
|
||||
assert_eq!(f.update([0; 6], 1, at), Motion::default());
|
||||
assert_eq!(f.update([0; 6], 2, at), Motion::default());
|
||||
}
|
||||
assert!(f.calibrated());
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axes_deflect_and_expire() {
|
||||
let t0 = Instant::now();
|
||||
let mut f = calibrated(t0);
|
||||
let at = t0 + Duration::from_secs(2);
|
||||
let ms = Duration::from_millis;
|
||||
let m = f.update([500, 500, 0, 0, 0, 0], 1, at);
|
||||
assert!((m.pan[0] - 1.0).abs() < 1e-9);
|
||||
assert!((m.pan[1] - 1.0).abs() < 1e-9);
|
||||
assert_eq!(m.zoom, 0.0, "no rotation input");
|
||||
// A fresh rotation report drives the zoom.
|
||||
let m = f.update([0, 0, 0, 0, 0, 500], 2, at + ms(100));
|
||||
assert!((m.zoom - 1.0).abs() < 1e-9);
|
||||
// The rotation axes went quiet for over 250 ms while translations still arrive.
|
||||
let m = f.update([500, 500, 0, 0, 0, 0], 1, at + ms(400));
|
||||
assert_eq!(m.zoom, 0.0);
|
||||
// A fresh rotation report puts the zoom back.
|
||||
let m = f.update([500, 500, 0, 0, 0, 500], 2, at + ms(500));
|
||||
assert!((m.zoom - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drift_tracks_rest_after_two_idle_seconds() {
|
||||
// Five axes units of drift sit inside the deadzone (FULL_SCALE * DEADZONE = 20).
|
||||
let mut f = Filter { raw: [5.0; 6], bias: Some([0.0; 6]), ..Filter::default() };
|
||||
let t0 = Instant::now();
|
||||
let mut b = [0.0; 6];
|
||||
f.update_drift(true, t0, &mut b); // starts the idle window: no shift yet
|
||||
assert_eq!(b, [0.0; 6]);
|
||||
f.update_drift(true, t0 + Duration::from_secs(3), &mut b);
|
||||
assert!((b[0] - 0.05).abs() < 1e-12, "bias follows resting drift");
|
||||
assert_eq!(f.bias, Some(b));
|
||||
// Motion outside the deadzone resets the idle window.
|
||||
f.raw = [100.0; 6];
|
||||
f.update_drift(true, t0 + Duration::from_secs(4), &mut b);
|
||||
assert_eq!(f.idle_since, None);
|
||||
f.update_drift(true, t0 + Duration::from_secs(5), &mut b);
|
||||
assert!((b[0] - 0.05).abs() < 1e-12, "no shift before two seconds at rest");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ struct Parser<'a> {
|
||||
|
||||
impl Parser<'_> {
|
||||
fn ws(&mut self) {
|
||||
while self.s.get(self.i).is_some_and(|c| c.is_ascii_whitespace()) {
|
||||
while self.s.get(self.i).is_some_and(u8::is_ascii_whitespace) {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ impl Default for ParametricSettings {
|
||||
angle_tolerance: 1.0,
|
||||
auto_types: ["Coincident", "Collinear", "Parallel", "Perpendicular", "Horizontal", "Vertical", "Tangent", "Concentric", "Equal"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
bars_visible: true,
|
||||
bar_exceptions: Vec::new(),
|
||||
|
||||
@@ -857,7 +857,7 @@ pub struct VportConfig {
|
||||
|
||||
impl VportConfig {
|
||||
/// The name of the configuration shown.
|
||||
pub const ACTIVE: &'static str = "*Active";
|
||||
pub const ACTIVE: &str = "*Active";
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.name.eq_ignore_ascii_case(Self::ACTIVE)
|
||||
}
|
||||
|
||||
@@ -109,9 +109,9 @@ impl Default for CellStyle {
|
||||
}
|
||||
|
||||
impl CellStyle {
|
||||
pub const TITLE: &'static str = "Title";
|
||||
pub const HEADER: &'static str = "Header";
|
||||
pub const DATA: &'static str = "Data";
|
||||
pub const TITLE: &str = "Title";
|
||||
pub const HEADER: &str = "Header";
|
||||
pub const DATA: &str = "Data";
|
||||
|
||||
/// Whether `name` is one of the three styles every table style has.
|
||||
pub fn is_builtin(name: &str) -> bool {
|
||||
@@ -190,7 +190,7 @@ impl TableStyle {
|
||||
|
||||
/// The names of its cell styles: Title, Header, Data, then the custom ones.
|
||||
pub fn cell_style_names(&self) -> Vec<String> {
|
||||
let mut out: Vec<String> = [CellStyle::TITLE, CellStyle::HEADER, CellStyle::DATA].iter().map(|s| s.to_string()).collect();
|
||||
let mut out: Vec<String> = [CellStyle::TITLE, CellStyle::HEADER, CellStyle::DATA].iter().map(ToString::to_string).collect();
|
||||
out.extend(self.cell_styles.iter().filter(|c| !CellStyle::is_builtin(&c.name)).map(|c| c.name.clone()));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ mod native {
|
||||
/// files of stopped apps that have no files left. Only an autosave file is deleted: a path
|
||||
/// whose name doesn't parse as one ([`parse_file_name`]) is left alone.
|
||||
pub fn discard(path: &str) {
|
||||
let ours = Path::new(path).file_name().and_then(|n| n.to_str()).is_some_and(|n| parse_file_name(n).is_some());
|
||||
let ours = Path::new(path).file_name().and_then(std::ffi::OsStr::to_str).is_some_and(|n| parse_file_name(n).is_some());
|
||||
if !ours {
|
||||
log::warn!("autosave: not deleting {path}: not an autosave file");
|
||||
return;
|
||||
|
||||
@@ -310,6 +310,67 @@ fn segment_dir(m: &MLeader) -> Option<Vec2> {
|
||||
(v.len() > 1e-12).then(|| v.normalized())
|
||||
}
|
||||
|
||||
fn align_parallel(items: &[(Handle, MLeader)], reference: Option<&MLeader>) -> Result<Vec<(Handle, Vec2)>> {
|
||||
let rdir = reference.and_then(segment_dir).ok_or_else(|| bad("mleaderalign", "the reference multileader has no leader"))?;
|
||||
let mut moves = Vec::new();
|
||||
for (h, m) in items {
|
||||
let Some(arrow) = m.leaders.first().and_then(|l| l.last()) else { continue };
|
||||
let len = (m.landing.xy() - arrow.xy()).len();
|
||||
moves.push((*h, arrow.xy() + rdir * len - m.landing.xy()));
|
||||
}
|
||||
Ok(moves)
|
||||
}
|
||||
|
||||
fn align_distribute(mut items: Vec<(Handle, MLeader)>, line: Option<(Vec2, Vec2)>) -> Result<Vec<(Handle, Vec2)>> {
|
||||
let (a, b) = line.ok_or_else(|| bad("mleaderalign", "`points` (first and last position) are required"))?;
|
||||
let d = b - a;
|
||||
if d.len() < 1e-12 {
|
||||
return Err(bad("mleaderalign", "the two points coincide"));
|
||||
}
|
||||
let u = d.normalized();
|
||||
items.sort_by(|x, y| (x.1.landing.xy() - a).dot(u).total_cmp(&(y.1.landing.xy() - a).dot(u)));
|
||||
let n = items.len();
|
||||
let mut moves = Vec::new();
|
||||
for (i, (h, m)) in items.iter().enumerate() {
|
||||
let f = if n > 1 { i as f64 / (n - 1) as f64 } else { 0.0 };
|
||||
moves.push((*h, a + d * f - m.landing.xy()));
|
||||
}
|
||||
Ok(moves)
|
||||
}
|
||||
|
||||
fn align_directional(
|
||||
items: &[(Handle, MLeader)],
|
||||
reference: Option<&MLeader>,
|
||||
line: Option<(Vec2, Vec2)>,
|
||||
spacing: Spacing,
|
||||
) -> Result<Vec<(Handle, Vec2)>> {
|
||||
let (a, b) = line.ok_or_else(|| bad("mleaderalign", "`direction` (two points or an angle) is required"))?;
|
||||
let d = b - a;
|
||||
if d.len() < 1e-12 {
|
||||
return Err(bad("mleaderalign", "the direction is zero"));
|
||||
}
|
||||
let u = d.normalized();
|
||||
let origin = reference.map(|r| r.landing.xy()).unwrap_or(a);
|
||||
let t_of = |m: &MLeader| (m.landing.xy() - origin).dot(u);
|
||||
let mut moves = Vec::new();
|
||||
if let Spacing::Value(sp) = spacing {
|
||||
let (mut pos, mut neg): (Vec<_>, Vec<_>) = items.iter().partition(|(_, m)| t_of(m) >= 0.0);
|
||||
pos.sort_by(|x, y| t_of(&x.1).total_cmp(&t_of(&y.1)));
|
||||
neg.sort_by(|x, y| t_of(&y.1).total_cmp(&t_of(&x.1)));
|
||||
for (k, (h, m)) in pos.iter().enumerate() {
|
||||
moves.push((*h, origin + u * (sp * (k + 1) as f64) - m.landing.xy()));
|
||||
}
|
||||
for (k, (h, m)) in neg.iter().enumerate() {
|
||||
moves.push((*h, origin - u * (sp * (k + 1) as f64) - m.landing.xy()));
|
||||
}
|
||||
} else {
|
||||
for (h, m) in items {
|
||||
moves.push((*h, origin + u * t_of(m) - m.landing.xy()));
|
||||
}
|
||||
}
|
||||
Ok(moves)
|
||||
}
|
||||
|
||||
fn align(s: &mut Session, hs: &[Handle], to: Option<Handle>, spacing: Spacing, line: Option<(Vec2, Vec2)>) -> Result<usize> {
|
||||
let mut items: Vec<(Handle, MLeader)> = Vec::new();
|
||||
for h in hs {
|
||||
@@ -331,56 +392,11 @@ fn align(s: &mut Session, hs: &[Handle], to: Option<Handle>, spacing: Spacing, l
|
||||
(_, Some(t)) => Some(get_mleader(s, t).ok_or_else(|| bad("mleaderalign", "`to` is not a multileader"))?),
|
||||
(_, None) => return Err(bad("mleaderalign", "`to` (the multileader to align to) is required")),
|
||||
};
|
||||
let mut moves: Vec<(Handle, Vec2)> = Vec::new();
|
||||
match spacing {
|
||||
Spacing::Parallel => {
|
||||
let rdir = reference.as_ref().and_then(segment_dir).ok_or_else(|| bad("mleaderalign", "the reference multileader has no leader"))?;
|
||||
for (h, m) in &items {
|
||||
let Some(arrow) = m.leaders.first().and_then(|l| l.last()) else { continue };
|
||||
let len = (m.landing.xy() - arrow.xy()).len();
|
||||
moves.push((*h, arrow.xy() + rdir * len - m.landing.xy()));
|
||||
}
|
||||
}
|
||||
Spacing::Distribute => {
|
||||
let (a, b) = line.ok_or_else(|| bad("mleaderalign", "`points` (first and last position) are required"))?;
|
||||
let d = b - a;
|
||||
if d.len() < 1e-12 {
|
||||
return Err(bad("mleaderalign", "the two points coincide"));
|
||||
}
|
||||
let u = d.normalized();
|
||||
items.sort_by(|x, y| (x.1.landing.xy() - a).dot(u).total_cmp(&(y.1.landing.xy() - a).dot(u)));
|
||||
let n = items.len();
|
||||
for (i, (h, m)) in items.iter().enumerate() {
|
||||
let f = if n > 1 { i as f64 / (n - 1) as f64 } else { 0.0 };
|
||||
moves.push((*h, a + d * f - m.landing.xy()));
|
||||
}
|
||||
}
|
||||
Spacing::Current | Spacing::Value(_) => {
|
||||
let (a, b) = line.ok_or_else(|| bad("mleaderalign", "`direction` (two points or an angle) is required"))?;
|
||||
let d = b - a;
|
||||
if d.len() < 1e-12 {
|
||||
return Err(bad("mleaderalign", "the direction is zero"));
|
||||
}
|
||||
let u = d.normalized();
|
||||
let origin = reference.as_ref().map(|r| r.landing.xy()).unwrap_or(a);
|
||||
let t_of = |m: &MLeader| (m.landing.xy() - origin).dot(u);
|
||||
if let Spacing::Value(sp) = spacing {
|
||||
let (mut pos, mut neg): (Vec<_>, Vec<_>) = items.iter().partition(|(_, m)| t_of(m) >= 0.0);
|
||||
pos.sort_by(|x, y| t_of(&x.1).total_cmp(&t_of(&y.1)));
|
||||
neg.sort_by(|x, y| t_of(&y.1).total_cmp(&t_of(&x.1)));
|
||||
for (k, (h, m)) in pos.iter().enumerate() {
|
||||
moves.push((*h, origin + u * (sp * (k + 1) as f64) - m.landing.xy()));
|
||||
}
|
||||
for (k, (h, m)) in neg.iter().enumerate() {
|
||||
moves.push((*h, origin - u * (sp * (k + 1) as f64) - m.landing.xy()));
|
||||
}
|
||||
} else {
|
||||
for (h, m) in &items {
|
||||
moves.push((*h, origin + u * t_of(m) - m.landing.xy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let moves = match spacing {
|
||||
Spacing::Parallel => align_parallel(&items, reference.as_ref())?,
|
||||
Spacing::Distribute => align_distribute(items, line)?,
|
||||
Spacing::Current | Spacing::Value(_) => align_directional(&items, reference.as_ref(), line, spacing)?,
|
||||
};
|
||||
move_content(s, &moves)?;
|
||||
Ok(moves.len())
|
||||
}
|
||||
|
||||
@@ -120,3 +120,88 @@ fn hostile_mleader_edit_params_never_panic() {
|
||||
}
|
||||
let _ = cadcraft_render::build(s.doc().unwrap(), &cadcraft_doc::Space::Model, &cadcraft_render::Options::default());
|
||||
}
|
||||
|
||||
mod units {
|
||||
use super::super::{Spacing, align_directional, align_distribute, align_parallel};
|
||||
use cadcraft_doc::{Handle, MLeader};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
|
||||
fn unit_ml(arrow: [f64; 2], land: [f64; 2]) -> MLeader {
|
||||
MLeader {
|
||||
leaders: vec![vec![Vec3::new(arrow[0], arrow[1], 0.0)]],
|
||||
landing: Vec3::new(land[0], land[1], 0.0),
|
||||
dogleg: 0.0,
|
||||
text: None,
|
||||
style: String::new(),
|
||||
arrow_size: 1.0,
|
||||
spline: false,
|
||||
block: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn no_leader(land: [f64; 2]) -> MLeader {
|
||||
MLeader {
|
||||
leaders: vec![],
|
||||
landing: Vec3::new(land[0], land[1], 0.0),
|
||||
dogleg: 0.0,
|
||||
text: None,
|
||||
style: String::new(),
|
||||
arrow_size: 1.0,
|
||||
spline: false,
|
||||
block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A five-unit-long leader whose direction differs from the reference's `+y`: moving the
|
||||
/// arrow onto `y` plus `5` (a five-unit parallel push) and back along the landing gives
|
||||
/// `(-3, 1)`.
|
||||
#[test]
|
||||
fn parallel_requires_a_reference_and_skips_leaders() {
|
||||
assert!(align_parallel(&[(Handle(1), unit_ml([0.0, 0.0], [3.0, 4.0]))], None).is_err());
|
||||
let d = align_parallel(
|
||||
&[(Handle(2), no_leader([1.0, 1.0])), (Handle(3), unit_ml([0.0, 0.0], [3.0, 4.0]))],
|
||||
Some(&unit_ml([0.0, 0.0], [0.0, 4.0])),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(d.len(), 1, "a multileader without a leader line is skipped");
|
||||
assert_eq!(d[0].0, Handle(3));
|
||||
assert!((d[0].1.x + 3.0).abs() < 1e-9);
|
||||
assert!((d[0].1.y - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distribute_orders_landings_along_the_line() {
|
||||
let mk = |h: u64, land: [f64; 2]| (Handle(h), unit_ml([0.0, 0.0], land));
|
||||
assert!(align_distribute(vec![mk(1, [2.0, 0.0])], None).is_err(), "needs two points");
|
||||
assert!(align_distribute(Vec::new(), Some((Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0)))).is_err(), "coinciding points give no direction");
|
||||
let items = vec![mk(1, [2.0, 0.0]), mk(2, [0.0, 0.0])];
|
||||
let d = align_distribute(items, Some((Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0)))).unwrap();
|
||||
assert_eq!(d[0].0, Handle(2), "sorted by projection onto the line");
|
||||
assert!((d[0].1.x - 0.0).abs() < 1e-9 && (d[0].1.y - 0.0).abs() < 1e-9, "first fraction is 0");
|
||||
assert_eq!(d[1].0, Handle(1));
|
||||
assert!((d[1].1.x - 2.0).abs() < 1e-9 && (d[1].1.y - 0.0).abs() < 1e-9, "last fraction is 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directional_projects_landings_and_supports_spacing() {
|
||||
let line = Some((Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0)));
|
||||
assert!(align_directional(&[], None, None, Spacing::Current).is_err(), "needs a direction");
|
||||
assert!(
|
||||
align_directional(&[], None, Some((Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0))), Spacing::Current).is_err(),
|
||||
"a zero direction is an error"
|
||||
);
|
||||
// Without a reference the origin is the first point: the landing is projected onto it.
|
||||
let d = align_directional(&[(Handle(7), unit_ml([0.0, 0.0], [1.0, 5.0]))], None, line, Spacing::Current).unwrap();
|
||||
assert_eq!(d.len(), 1);
|
||||
assert!((d[0].1.x - 0.0).abs() < 1e-9);
|
||||
assert!((d[0].1.y + 5.0).abs() < 1e-9);
|
||||
// With a reference as origin, given spacing counts outward on both sides of it.
|
||||
let items = vec![(Handle(1), unit_ml([0.0, 0.0], [4.0, 0.0])), (Handle(2), unit_ml([0.0, 0.0], [-4.0, 0.0]))];
|
||||
let d = align_directional(&items, Some(&unit_ml([0.0, 0.0], [0.0, 4.0])), line, Spacing::Value(2.0)).unwrap();
|
||||
assert_eq!(d.len(), 2);
|
||||
assert_eq!(d[0].0, Handle(1));
|
||||
assert!((d[0].1.x + 2.0).abs() < 1e-9 && (d[0].1.y - 4.0).abs() < 1e-9);
|
||||
assert_eq!(d[1].0, Handle(2));
|
||||
assert!((d[1].1.x - 2.0).abs() < 1e-9 && (d[1].1.y - 4.0).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ struct Parser<'a> {
|
||||
|
||||
impl Parser<'_> {
|
||||
fn peek(&mut self) -> Option<u8> {
|
||||
while self.s.get(self.i).is_some_and(|c| c.is_ascii_whitespace()) {
|
||||
while self.s.get(self.i).is_some_and(u8::is_ascii_whitespace) {
|
||||
self.i += 1;
|
||||
}
|
||||
self.s.get(self.i).copied()
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(crate) struct Guarded {
|
||||
}
|
||||
|
||||
pub(crate) fn message(p: &(dyn Any + Send)) -> String {
|
||||
p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
||||
p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
||||
}
|
||||
|
||||
impl Guarded {
|
||||
|
||||
@@ -829,7 +829,7 @@ impl Session {
|
||||
}
|
||||
/// The space commands act on (model space while working inside a layout viewport).
|
||||
pub fn space(&self) -> Space {
|
||||
self.state().map(|s| s.edit_space()).unwrap_or_default()
|
||||
self.state().map(DocState::edit_space).unwrap_or_default()
|
||||
}
|
||||
/// The tab being displayed: Model or a layout, regardless of MSPACE.
|
||||
pub fn layout_space(&self) -> Space {
|
||||
@@ -891,7 +891,7 @@ impl Session {
|
||||
|
||||
/// World units per pixel at the current view.
|
||||
pub fn pixel_size(&self) -> f64 {
|
||||
let v = self.state().map(|s| s.view()).unwrap_or_default();
|
||||
let v = self.state().map(DocState::view).unwrap_or_default();
|
||||
v.height / self.viewport_px.1.max(1.0)
|
||||
}
|
||||
|
||||
@@ -911,8 +911,11 @@ impl Session {
|
||||
let result = match result {
|
||||
Ok(r) => r,
|
||||
Err(p) => {
|
||||
let msg =
|
||||
p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
|
||||
let msg = p
|
||||
.downcast_ref::<&str>()
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| p.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "panic".into());
|
||||
// Restore the drawing as it was.
|
||||
if let Some((doc, sel, uid)) = &before
|
||||
&& let Some(st) = self.docs.iter_mut().find(|d| d.uid == *uid)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! Errors use AutoLISP's wording (`bad argument type: numberp: nil`, `too few arguments`,
|
||||
//! `divide by zero`), since routines and their `*error*` handlers match on it.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::ffi::OsStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::eval::{Ctx, Flow, Special};
|
||||
@@ -335,16 +337,16 @@ fn num_ne(a: &[Value]) -> R {
|
||||
eq_chain(a, false)
|
||||
}
|
||||
fn lt(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_lt())
|
||||
chain(a, Ordering::is_lt)
|
||||
}
|
||||
fn gt(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_gt())
|
||||
chain(a, Ordering::is_gt)
|
||||
}
|
||||
fn le(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_le())
|
||||
chain(a, Ordering::is_le)
|
||||
}
|
||||
fn ge(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_ge())
|
||||
chain(a, Ordering::is_ge)
|
||||
}
|
||||
|
||||
fn zerop(a: &[Value]) -> R {
|
||||
@@ -658,12 +660,12 @@ fn vl_list_to_string(a: &[Value]) -> R {
|
||||
fn vl_filename_base(a: &[Value]) -> R {
|
||||
arity(a, 1, 1)?;
|
||||
let s = string(&a[0])?;
|
||||
Ok(Value::str(std::path::Path::new(&*s.replace('\\', "/")).file_stem().and_then(|x| x.to_str()).unwrap_or("")))
|
||||
Ok(Value::str(std::path::Path::new(&*s.replace('\\', "/")).file_stem().and_then(OsStr::to_str).unwrap_or("")))
|
||||
}
|
||||
fn vl_filename_extension(a: &[Value]) -> R {
|
||||
arity(a, 1, 1)?;
|
||||
let s = string(&a[0])?;
|
||||
Ok(std::path::Path::new(&*s.replace('\\', "/")).extension().and_then(|x| x.to_str()).map(|e| Value::str(&format!(".{e}"))).unwrap_or_default())
|
||||
Ok(std::path::Path::new(&*s.replace('\\', "/")).extension().and_then(OsStr::to_str).map(|e| Value::str(&format!(".{e}"))).unwrap_or_default())
|
||||
}
|
||||
fn vl_filename_directory(a: &[Value]) -> R {
|
||||
arity(a, 1, 1)?;
|
||||
|
||||
@@ -624,7 +624,7 @@ impl Session {
|
||||
self.lisp.files.insert(file_key(name), text.to_string());
|
||||
let r = self.lisp_eval(text, false, blocking, Vec::new())?;
|
||||
if !matches!(r, Some(Err(_))) {
|
||||
let shown = std::path::Path::new(name).file_name().and_then(|n| n.to_str()).unwrap_or(name).to_string();
|
||||
let shown = std::path::Path::new(name).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(name).to_string();
|
||||
self.echo(format!("{shown} successfully loaded."));
|
||||
}
|
||||
Ok(r)
|
||||
@@ -632,7 +632,7 @@ impl Session {
|
||||
}
|
||||
|
||||
fn file_key(name: &str) -> String {
|
||||
let base = std::path::Path::new(&name.replace('\\', "/")).file_name().and_then(|n| n.to_str()).unwrap_or(name).to_ascii_lowercase();
|
||||
let base = std::path::Path::new(&name.replace('\\', "/")).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(name).to_ascii_lowercase();
|
||||
base.strip_suffix(".lsp").map(str::to_string).unwrap_or(base)
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ impl Prompt {
|
||||
self
|
||||
}
|
||||
pub fn kw(mut self, k: &[&str]) -> Self {
|
||||
self.keywords = k.iter().map(|s| s.to_string()).collect();
|
||||
self.keywords = k.iter().map(ToString::to_string).collect();
|
||||
self
|
||||
}
|
||||
pub fn base(mut self, p: Vec2) -> Self {
|
||||
|
||||
@@ -285,7 +285,7 @@ pub(crate) fn arrow_block_name(name: &str) -> Option<(String, Arrowhead)> {
|
||||
return None;
|
||||
}
|
||||
let kind = Arrowhead::parse(n);
|
||||
let norm: String = n.trim_start_matches('_').chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase();
|
||||
let norm: String = n.trim_start_matches('_').chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase();
|
||||
if kind == Arrowhead::ClosedFilled && !matches!(norm.as_str(), "closedfilled" | "filled") {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::*;
|
||||
use cadcraft_doc::{Circle, Common, Drawing, Ellipse, EntityKind, Layer, Line, LwPolyline, Space};
|
||||
use cadcraft_geom::{Mat3, PolyVertex, Spline, Vec2, Vec3};
|
||||
use quick_xml::Reader;
|
||||
use quick_xml::XmlVersion;
|
||||
@@ -527,7 +527,7 @@ impl Parser {
|
||||
} else if *rx > 0.0 && *ry > 0.0 {
|
||||
self.flush_run(&mut run, false)?;
|
||||
self.flush_ctrl(&mut ctrl)?;
|
||||
for p in flatten_arc(&m, cur_user, *end, *rx, *ry, *rot, *large, *sweep)? {
|
||||
for p in flatten_arc(&m, cur_user, *end, (*rx, *ry), *rot, *large, *sweep)? {
|
||||
if run.is_empty() {
|
||||
run.push(PolyVertex::new(cur));
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ fn pick_sweep(ccw: f64, large: bool, sweep: bool) -> f64 {
|
||||
|
||||
/// Flatten a general elliptical arc (any `rx`/`ry`/rotation) from `p0` to `p1` in user
|
||||
/// space, then map every sample through `m`.
|
||||
fn flatten_arc(m: &Mat3, p0: Vec2, p1: Vec2, rx: f64, ry: f64, rot_deg: f64, large: bool, sweep: bool) -> Result<Vec<Vec2>> {
|
||||
fn flatten_arc(m: &Mat3, p0: Vec2, p1: Vec2, (rx, ry): (f64, f64), rot_deg: f64, large: bool, sweep: bool) -> Result<Vec<Vec2>> {
|
||||
let (c, theta, delta) = elliptical_params(p0, p1, rx, ry, rot_deg.to_radians(), large, sweep)
|
||||
.ok_or_else(|| IoError::Format("SVG: elliptical arc cannot be converted".into()))?;
|
||||
let axes = Mat3::rotate(rot_deg.to_radians());
|
||||
|
||||
@@ -14,7 +14,7 @@ fn keys(v: &Value) -> Vec<String> {
|
||||
/// The `cmdline.state` fields plus `extra`.
|
||||
fn want(b: &mut Headless, extra: &[&str]) -> Vec<String> {
|
||||
let mut k = keys(&b.call("cmdline.state", json!({})).unwrap());
|
||||
k.extend(extra.iter().map(|s| s.to_string()));
|
||||
k.extend(extra.iter().map(ToString::to_string));
|
||||
k.sort();
|
||||
k
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ fn keys(v: &Value) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn want(extra: &[&str]) -> Vec<String> {
|
||||
let mut k: Vec<String> = STATE_KEYS.iter().chain(extra).map(|s| s.to_string()).collect();
|
||||
let mut k: Vec<String> = STATE_KEYS.iter().chain(extra).map(ToString::to_string).collect();
|
||||
k.sort();
|
||||
k
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ pub enum Arrowhead {
|
||||
impl Arrowhead {
|
||||
/// Parse a DIMBLK value ("" / "_DOT" / "Architectural tick" / "closed blank"…).
|
||||
pub fn parse(name: &str) -> Arrowhead {
|
||||
let n: String = name.trim().trim_start_matches('_').chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase();
|
||||
let n: String = name.trim().trim_start_matches('_').chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase();
|
||||
match n.as_str() {
|
||||
"" | "closedfilled" | "filled" => Arrowhead::ClosedFilled,
|
||||
"closedblank" | "blank" => Arrowhead::ClosedBlank,
|
||||
|
||||
@@ -195,7 +195,7 @@ fn toolbar(ui: &mut egui::Ui, st: &mut State, base_h: f64) {
|
||||
// Font
|
||||
let shown = cur.font.clone().unwrap_or_else(|| st.fmt.base_font.clone());
|
||||
egui::ComboBox::from_id_salt("mtext_font").width(120.0).selected_text(shown.clone()).show_ui(ui, |ui| {
|
||||
for fam in FAMILIES.iter().map(|s| s.to_string()).chain(cur.font.clone().filter(|f| !FAMILIES.contains(&f.as_str()))) {
|
||||
for fam in FAMILIES.iter().map(ToString::to_string).chain(cur.font.clone().filter(|f| !FAMILIES.contains(&f.as_str()))) {
|
||||
if ui.selectable_label(shown == fam, &fam).clicked() {
|
||||
change(st, |c| c.font = Some(fam.clone()));
|
||||
refocus = true;
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn qselect_dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
||||
ui.label(crate::tl!("Value:"));
|
||||
let choices: Vec<String> = match st.property.as_str() {
|
||||
"color" => {
|
||||
["ByLayer", "ByBlock", "Red", "Yellow", "Green", "Cyan", "Blue", "Magenta", "White"].iter().map(|s| s.to_string()).collect()
|
||||
["ByLayer", "ByBlock", "Red", "Yellow", "Green", "Cyan", "Blue", "Magenta", "White"].iter().map(ToString::to_string).collect()
|
||||
}
|
||||
"layer" => app.session.doc().map(|d| d.layers.iter().map(|l| l.name.clone()).collect()).unwrap_or_default(),
|
||||
"linetype" => app.session.doc().map(|d| d.linetypes.iter().map(|l| l.name.clone()).collect()).unwrap_or_default(),
|
||||
|
||||
@@ -45,12 +45,7 @@ fn visible_text(shape: &Shape, clip: Rect, result: &mut Vec<(String, Rect)>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_labels_never_overlap_at_standard_and_compact_sizes() {
|
||||
let mut overlaps = Vec::new();
|
||||
for (width, height) in [(1200.0, 760.0), (900.0, 560.0)] {
|
||||
for scale in [1.0, 2.0] {
|
||||
for message in [false, true] {
|
||||
fn check_status_labels_case(width: f32, height: f32, scale: f32, message: bool, overlaps: &mut Vec<String>) {
|
||||
let mut h = harness(width, height, scale, message);
|
||||
if let Ok(directory) = std::env::var("CRAFT_UI_VISUAL_DIR") {
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
@@ -73,6 +68,15 @@ fn status_labels_never_overlap_at_standard_and_compact_sizes() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_labels_never_overlap_at_standard_and_compact_sizes() {
|
||||
let mut overlaps = Vec::new();
|
||||
for (width, height) in [(1200.0, 760.0), (900.0, 560.0)] {
|
||||
for scale in [1.0, 2.0] {
|
||||
for message in [false, true] {
|
||||
check_status_labels_case(width, height, scale, message, &mut overlaps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fn keys(v: &Value) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn want(extra: &[&str]) -> Vec<String> {
|
||||
let mut k: Vec<String> = STATE_KEYS.iter().chain(extra).map(|s| s.to_string()).collect();
|
||||
let mut k: Vec<String> = STATE_KEYS.iter().chain(extra).map(ToString::to_string).collect();
|
||||
k.sort();
|
||||
k
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fn keys(v: &Value) -> Vec<String> {
|
||||
/// The `cmdline.state` fields plus `extra`.
|
||||
fn want(app: &mut CadApp, extra: &[&str]) -> Vec<String> {
|
||||
let mut k = keys(&call(app, "cmdline.state", json!({}))["result"]);
|
||||
k.extend(extra.iter().map(|s| s.to_string()));
|
||||
k.extend(extra.iter().map(ToString::to_string));
|
||||
k.sort();
|
||||
k
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ const ASSET_EXT: &[&str] = &[
|
||||
const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "examples/"];
|
||||
|
||||
pub fn is_asset(path: &str) -> bool {
|
||||
let ext = Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
let ext = Path::new(path).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
ASSET_DIRS.iter().any(|d| path.starts_with(d)) || ASSET_EXT.contains(&ext.as_str())
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|v| v.to_string());
|
||||
out.sort_by_key(ToString::to_string);
|
||||
out.dedup();
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user