Graphics-Penplotter-GcodeXY

 view release on metacpan or  search on metacpan

t/15-3d.t  view on Meta::CPAN

    );
    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 )