Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 0 additions & 129 deletions src/block_expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,132 +191,3 @@ where
ctx.visited.remove(&block_handle);
result
}

#[cfg(test)]
mod tests {
use super::*;
use crate::entities::insert::Insert;
use crate::entities::{Point3D, Vec3D};

fn insert_at(x: f64, y: f64) -> Insert {
Insert {
owned_object_count: 0,
insertion_point: Point3D { x, y, z: 0.0 },
scale: Point3D {
x: 1.0,
y: 1.0,
z: 1.0,
},
rotation: 0.0,
extrusion: Vec3D {
x: 0.0,
y: 0.0,
z: 1.0,
},
has_attribs: false,
}
}

#[test]
fn expansion_context_tracks_depth_and_visited() {
let ctx = ExpansionContext::new();
assert_eq!(ctx.depth(), 0);
assert!(!ctx.is_visited(0x42));
assert_eq!(ctx.max_depth(), 16);
}

#[test]
fn with_max_depth_overrides_default() {
let ctx = ExpansionContext::new().with_max_depth(4);
assert_eq!(ctx.max_depth(), 4);
}

#[test]
fn depth_cap_rejects_overflow() {
let insert = insert_at(0.0, 0.0);
let mut ctx = ExpansionContext::new().with_max_depth(0);
let lookup = |_h: u64| Ok(Vec::new());
let r = expand_insert(&insert, 0x42, &mut ctx, &Transform3::identity(), &lookup);
assert!(matches!(r, Err(Error::Unsupported { .. })));
}

#[test]
fn cycle_detection_rejects_reentrant_block() {
let insert_a = insert_at(0.0, 0.0);
let insert_b = insert_a.clone();

let lookup = move |h: u64| -> Result<Vec<(DecodedEntity, Option<u64>)>> {
if h == 0xA {
Ok(vec![(
DecodedEntity::Insert(insert_b.clone()),
Some(0xA), // points back at self → 1-cycle
)])
} else {
Ok(Vec::new())
}
};

let mut ctx = ExpansionContext::new();
let r = expand_insert(&insert_a, 0xA, &mut ctx, &Transform3::identity(), &lookup);
assert!(matches!(r, Err(Error::Unsupported { .. })));
}

#[test]
fn empty_block_body_yields_empty_expansion() {
let insert = insert_at(10.0, 20.0);
let mut ctx = ExpansionContext::new();
let lookup = |_h: u64| Ok(Vec::new());
let expanded =
expand_insert(&insert, 0x1234, &mut ctx, &Transform3::identity(), &lookup).unwrap();
assert!(expanded.is_empty());
assert_eq!(ctx.depth, 0);
assert!(ctx.visited.is_empty());
}

#[test]
fn single_level_expansion_emits_block_body() {
// Block 0xB contains one LINE entity. Expanding an INSERT
// at (10, 20) that references block 0xB yields a single
// ExpandedEntity at depth 1 with the translated transform.
use crate::entities::line::Line;

let line = Line {
start: Point3D {
x: 0.0,
y: 0.0,
z: 0.0,
},
end: Point3D {
x: 5.0,
y: 0.0,
z: 0.0,
},
thickness: 0.0,
extrusion: Vec3D {
x: 0.0,
y: 0.0,
z: 1.0,
},
is_2d: true,
};
let line_decoded = DecodedEntity::Line(line);

let lookup = move |h: u64| -> Result<Vec<(DecodedEntity, Option<u64>)>> {
if h == 0xB {
Ok(vec![(line_decoded.clone(), None)])
} else {
Ok(Vec::new())
}
};

let insert = insert_at(10.0, 20.0);
let mut ctx = ExpansionContext::new();
let expanded =
expand_insert(&insert, 0xB, &mut ctx, &Transform3::identity(), &lookup).unwrap();
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0].depth, 1);
// Depth + visited restored on exit.
assert_eq!(ctx.depth, 0);
assert!(ctx.visited.is_empty());
}
}
66 changes: 0 additions & 66 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,69 +321,3 @@ pub fn aci_to_hex(index: u8) -> String {
let (r, g, b) = aci_to_rgb(index);
format!("#{r:02X}{g:02X}{b:02X}")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn named_primaries_match_expected() {
assert_eq!(aci_to_rgb(1), (255, 0, 0)); // red
assert_eq!(aci_to_rgb(2), (255, 255, 0)); // yellow
assert_eq!(aci_to_rgb(3), (0, 255, 0)); // green
assert_eq!(aci_to_rgb(4), (0, 255, 255)); // cyan
assert_eq!(aci_to_rgb(5), (0, 0, 255)); // blue
assert_eq!(aci_to_rgb(6), (255, 0, 255)); // magenta
assert_eq!(aci_to_rgb(7), (255, 255, 255)); // white/black
}

#[test]
fn grays_are_monotonic_lightness() {
// 250 (darkest gray) → 255 (white) should be monotonically increasing.
for i in 250..=254u8 {
let (r, _, _) = aci_to_rgb(i);
let (r_next, _, _) = aci_to_rgb(i + 1);
assert!(r_next >= r, "gray {i} → {} not monotonic", i + 1);
}
}

#[test]
fn hex_format_uppercase_six_digit() {
assert_eq!(aci_to_hex(1), "#FF0000");
assert_eq!(aci_to_hex(3), "#00FF00");
assert_eq!(aci_to_hex(5), "#0000FF");
assert_eq!(aci_to_hex(0), "#000000");
assert_eq!(aci_to_hex(255), "#FFFFFF");
}

#[test]
fn palette_length_is_exactly_256() {
assert_eq!(ACI_PALETTE.len(), 256);
}

#[test]
fn hue_grid_saturation_pattern() {
// The HSV grid has a predictable pattern: the "full saturation"
// row (index 10, 20, 30, ..., 240) is the most saturated hue
// step. The "faded" companion at index+1 should be lighter.
for hue_base in (10..=240).step_by(10) {
let (r1, g1, b1) = aci_to_rgb(hue_base);
let (r2, g2, b2) = aci_to_rgb(hue_base + 1);
let v1 = r1 as u32 + g1 as u32 + b1 as u32;
let v2 = r2 as u32 + g2 as u32 + b2 as u32;
assert!(
v2 >= v1,
"ACI {hue_base} ({v1}) darker-total than {} ({v2})",
hue_base + 1
);
}
}

#[test]
fn does_not_panic_on_any_u8() {
for i in 0u8..=255 {
let _ = aci_to_rgb(i);
let _ = aci_to_hex(i);
}
}
}
122 changes: 0 additions & 122 deletions src/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,125 +256,3 @@ impl Path {
self.segments.is_empty()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn line_bounds_covers_both_endpoints() {
let c = Curve::Line {
a: Point3D::new(0.0, 0.0, 0.0),
b: Point3D::new(10.0, -5.0, 3.0),
};
let b = c.bounds();
assert_eq!(b.min, Point3D::new(0.0, -5.0, 0.0));
assert_eq!(b.max, Point3D::new(10.0, 0.0, 3.0));
}

#[test]
fn circle_bounds_is_cube_around_center() {
let c = Curve::Circle {
center: Point3D::new(5.0, 5.0, 5.0),
radius: 2.0,
normal: Vec3D {
x: 0.0,
y: 0.0,
z: 1.0,
},
};
let b = c.bounds();
assert_eq!(b.min, Point3D::new(3.0, 3.0, 3.0));
assert_eq!(b.max, Point3D::new(7.0, 7.0, 7.0));
}

#[test]
fn polyline_bounds_covers_all_vertices() {
let c = Curve::Polyline {
vertices: vec![
PolylineVertex {
point: Point3D::new(-1.0, 0.0, 0.0),
bulge: 0.0,
},
PolylineVertex {
point: Point3D::new(1.0, 2.0, 0.0),
bulge: 0.0,
},
PolylineVertex {
point: Point3D::new(0.0, -1.0, 3.0),
bulge: 0.0,
},
],
closed: false,
};
let b = c.bounds();
assert_eq!(b.min, Point3D::new(-1.0, -1.0, 0.0));
assert_eq!(b.max, Point3D::new(1.0, 2.0, 3.0));
}

#[test]
fn path_from_polyline_builds_lines() {
let pts = [
Point3D::new(0.0, 0.0, 0.0),
Point3D::new(1.0, 0.0, 0.0),
Point3D::new(1.0, 1.0, 0.0),
];
let p = Path::from_polyline(&pts, false);
assert_eq!(p.segments.len(), 2);
assert!(!p.closed);
}

#[test]
fn path_from_polyline_closed_adds_final_segment() {
let pts = [
Point3D::new(0.0, 0.0, 0.0),
Point3D::new(1.0, 0.0, 0.0),
Point3D::new(1.0, 1.0, 0.0),
];
let p = Path::from_polyline(&pts, true);
assert_eq!(p.segments.len(), 3);
assert!(p.closed);
}

#[test]
fn empty_path_bounds_is_empty() {
let p = Path::empty();
assert!(p.is_empty());
assert!(p.bounds().is_empty());
}

#[test]
fn path_bounds_unions_segment_bounds() {
let pts = [
Point3D::new(0.0, 0.0, 0.0),
Point3D::new(10.0, 0.0, 0.0),
Point3D::new(10.0, 20.0, 0.0),
];
let p = Path::from_polyline(&pts, false);
let b = p.bounds();
assert_eq!(b.min, Point3D::new(0.0, 0.0, 0.0));
assert_eq!(b.max, Point3D::new(10.0, 20.0, 0.0));
}

#[test]
fn spline_bounds_covers_control_points_only() {
// NB: tight NURBS bounds could exclude control points, but
// our conservative impl uses the control polygon.
let s = Spline {
degree: 3,
control_points: vec![
Point3D::new(0.0, 0.0, 0.0),
Point3D::new(10.0, 0.0, 0.0),
Point3D::new(10.0, 10.0, 0.0),
Point3D::new(0.0, 10.0, 0.0),
],
weights: vec![],
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
closed: false,
};
let c = Curve::Spline(s);
let b = c.bounds();
assert_eq!(b.min, Point3D::new(0.0, 0.0, 0.0));
assert_eq!(b.max, Point3D::new(10.0, 10.0, 0.0));
}
}
Loading
Loading