Graphics-Penplotter-GcodeXY
view release on metacpan or search on metacpan
);
my $cam_before = $g->get_camera();
$g->gsave();
# Replace the camera inside the gsave block
$g->set_camera(
eye => [5, 5, 5],
center => [0, 0, 0],
up => [0, 1, 0],
);
my $cam_inner = $g->get_camera();
ok abs($cam_inner->{eye}[0] - 5) < 1e-9,
'camera gsave: inner set_camera takes effect';
$g->grestore();
my $cam_after = $g->get_camera();
ok defined $cam_after, 'camera grestore: camera restored (not undef)';
ok abs($cam_after->{eye}[2] - 5) < 1e-9,
'camera grestore: eye Z restored to original value';
ok abs($cam_after->{eye}[0]) < 1e-9,
'camera grestore: eye X restored to 0';
}
# ==========================================================================
# SECTION: gsave / grestore with no camera set (undef round-trip)
# ==========================================================================
{
my $fresh = MockPlotter->new;
ok !defined $fresh->get_camera(),
'camera grestore undef: starts with no camera';
$fresh->gsave();
$fresh->set_camera(
eye => [1, 2, 3],
center => [0, 0, 0],
up => [0, 1, 0],
);
ok defined $fresh->get_camera(),
'camera grestore undef: camera set inside gsave block';
$fresh->grestore();
ok !defined $fresh->get_camera(),
'camera grestore undef: grestore restores undef camera';
}
# ==========================================================================
# SECTION: occlusion_clip -- diagonal suppression, aliasing, occluder support
# ==========================================================================
#
# Tests target three distinct fixes in occlusion_clip / hidden_line_remove:
#
# 1. Coplanar diagonal suppression
# Each quad face of prism() is tessellated into 2 triangles. The shared
# edge (the "diagonal") must be suppressed so cube faces appear as
# rectangles, not as two triangles.
#
# 2. Shared-reference aliasing fix
# Before the fix, multiple segment endpoints held the *same* [$x,$y]
# arrayref. An in-place viewport transform (e.g. $pt->[0] *= SCALE)
# then compounded the scale factor once per alias, producing astronomical
# coordinates. After the fix every endpoint is an independent copy.
#
# 3. Occluder support
# hidden_line_remove accepts occluders => \@meshes. Those meshes
# populate the z-buffer in a first pass so that target triangles behind
# them fail the depth test and are omitted.
{
# Helper: set up a plotter with a straight-on perspective camera.
# Eye at (0,0,-100), looking at origin, FOV=45.
my sub make_straight ($fov=45) {
my $p = MockPlotter->new;
$p->initmatrix3();
$p->set_camera(eye=>[0,0,-100], center=>[0,0,0], up=>[0,1,0]);
$p->camera_to_ctm();
$p->set_perspective(fov=>$fov, aspect=>1, near=>1, far=>500);
$p->perspective_to_ctm();
return $p;
}
# Helper: corner camera -- eye at (100,100,-100), same target and FOV.
my sub make_corner () {
my $p = MockPlotter->new;
$p->initmatrix3();
$p->set_camera(eye=>[100,100,-100], center=>[0,0,0], up=>[0,1,0]);
$p->camera_to_ctm();
$p->set_perspective(fov=>45, aspect=>1, near=>1, far=>500);
$p->perspective_to_ctm();
return $p;
}
# --- 1. Diagonal suppression ---
# Straight-on camera: only the front (-Z) face is visible after backface
# cull. That face has 4 edges. Without diagonal suppression the two
# triangles comprising that face would generate 6 edges (including the
# internal diagonal), of which 2 would be duplicated -> still 4 unique
# edges, but WITH the diagonal = 5. After suppression: exactly 4.
{
my $p = make_straight();
my $m = $p->prism(0,0,0, 20,20,20);
my $segs = $p->hidden_line_remove($m);
is scalar @$segs, 4,
'diagonal suppression: straight-on view, 1 face visible -> 4 edges';
}
# Corner camera: +X, +Y, -Z faces all visible (3 faces).
# Each adjacent pair shares one cube edge, so 3x4 - 3 = 9 unique edges.
# Without diagonal suppression each face would emit 5 edges -> 15 - 3 = 12
# (minus shared genuine edges), an incorrect higher count.
{
my $p = make_corner();
my $m = $p->prism(0,0,0, 20,20,20);
my $segs = $p->hidden_line_remove($m);
is scalar @$segs, 9,
'diagonal suppression: corner view, 3 faces visible -> 9 unique edges';
}
# --- 2. Aliasing fix: no two segment endpoints share the same arrayref ---
{
my $p = make_corner();
my $m = $p->prism(0,0,0, 20,20,20);
my $segs = $p->hidden_line_remove($m);
my %seen_ref;
my $aliases = 0;
for my $seg (@$segs) {
for my $pt (@$seg) {
$aliases++ if $seen_ref{"$pt"}++;
}
}
is $aliases, 0,
'aliasing fix: no segment endpoints share the same arrayref';
}
# Confirm coordinates are finite and in a sane range for NDC space.
{
my $p = make_straight();
my $m = $p->prism(0,0,0, 20,20,20);
my $segs = $p->hidden_line_remove($m);
my $ok = 1;
for my $seg (@$segs) {
for my $pt (@$seg) {
$ok = 0 if !defined $pt->[0] || abs($pt->[0]) > 100
|| !defined $pt->[1] || abs($pt->[1]) > 100;
}
}
ok $ok, 'aliasing fix: all NDC coordinates are finite and < 100';
}
# --- 3. Occluder support ---
# Fully occluded: front cube 20x20x20 at z=0, back cube 10x10x10 at z=30.
# Straight-on camera: back cube projects entirely inside front cube's
# footprint. Without an occluder, back cube returns 4 segments (its
# single visible face). With the front cube as an occluder, the back
# cube's triangles all lose the z-test -> 0 segments.
{
my $p = make_straight();
my $front = $p->prism(0,0,0, 20,20,20);
my $back = $p->prism(0,0,30, 10,10,10);
my $segs_solo = $p->hidden_line_remove($back);
ok scalar @$segs_solo > 0,
'occluder: back cube is visible when processed alone';
my $segs_occluded = $p->hidden_line_remove($back, occluders => [$front]);
is scalar @$segs_occluded, 0,
'occluder: back cube fully occluded by front cube -> 0 segments';
}
# Same-size full occlusion: front 20x20x20 at z=0, back 20x20x20 at z=30.
{
my $p = make_straight();
my $front = $p->prism(0,0,0, 20,20,20);
my $back = $p->prism(0,0,30, 20,20,20);
my $segs = $p->hidden_line_remove($back, occluders => [$front]);
is scalar @$segs, 0,
'occluder: same-size back cube fully occluded -> 0 segments';
}
# Non-occluded: occluder that does NOT overlap back cube's footprint
# must leave the back cube's segment count unchanged.
{
my $p = make_straight();
my $far_left = $p->prism(-50,0,0, 20,20,20); # way off to the side
my $back = $p->prism( 0,0,30, 10,10,10);
my $segs_solo = $p->hidden_line_remove($back);
my $segs_occ = $p->hidden_line_remove($back, occluders => [$far_left]);
is scalar @$segs_occ, scalar @$segs_solo,
'occluder: non-overlapping occluder does not remove any segments';
}
( run in 0.860 second using v1.01-cache-2.11-cpan-524268b4103 )