Graphics-Penplotter-GcodeXY

 view release on metacpan or  search on metacpan

lib/Graphics/Penplotter/GcodeXY/Anamorphic.pm  view on Meta::CPAN

    return () if $iw < 1e-12 || $ih < 1e-12;

    # Normalise to [-0.5, +0.5]: s = horizontal (right +), t = vertical (down +)
    my $s = ($u - $u0) / $iw - 0.5;
    my $t = ($v - $v0) / $ih - 0.5;

    # Map to viewing angles from the observer.
    # phi: azimuth of the viewing ray. Right side of image (s > 0) means the
    # observer looks more counterclockwise (toward their left).
    my $phi  = $cfg{phi_fwd} + $s * $cfg{ang_rad};

    # beta: elevation angle below horizontal.  Image bottom (t > 0 in SVG)
    # maps to looking further downward.
    my $beta = $cfg{beta0} + $t * $cfg{elev_rad};

    # Unit viewing direction from observer toward the mirror
    my $cos_b = cos($beta);
    my $dx    = $cos_b * cos($phi);
    my $dy    = $cos_b * sin($phi);
    my $dz    = -sin($beta);              # negative z = downward toward paper

    # Intersect the viewing ray with the cylinder surface
    my $t_cyl = _ana_ray_cylinder(
        $cfg{ex}, $cfg{ey}, $cfg{ez},
        $dx, $dy, $dz,
        $cfg{cx}, $cfg{cy}, $cfg{R},
    );
    return () unless defined $t_cyl;

    # Mirror surface point M
    my $mx = $cfg{ex} + $t_cyl * $dx;
    my $my = $cfg{ey} + $t_cyl * $dy;
    my $mz = $cfg{ez} + $t_cyl * $dz;

    # The cylinder hit must be at or above the paper (z >= 0)
    return () if $mz < -1e-9;

    # Outward unit normal at M (horizontal, pointing away from cylinder axis)
    my $nx = ($mx - $cfg{cx}) / $cfg{R};
    my $ny = ($my - $cfg{cy}) / $cfg{R};

    # Reflect the incident ray off the cylinder surface (nz = 0 for a vertical
    # cylinder, so the z component of the reflected ray is unchanged)
    my ($rx, $ry, $rz) = _ana_reflect($dx, $dy, $dz, $nx, $ny, 0.0);

    # The reflected ray must travel downward (rz < 0) to reach the paper
    return () if $rz >= -1e-9;

    # Intersect the reflected ray with the paper plane (z = 0)
    my $t_paper = -$mz / $rz;
    return () if $t_paper < 0;

    my $px = $mx + $t_paper * $rx;
    my $py = $my + $t_paper * $ry;
    return ($px, $py);
}

# ---------------------------------------------------------------------------
# _ana_resample_segment
#
# Given endpoints (x0,y0) and (x1,y1) in image space, return a list of
# [ x, y ] sample-point arrayrefs spaced at most $step apart.  The start
# point is NOT included (the caller holds it); the end point IS included.
# ---------------------------------------------------------------------------
sub _ana_resample_segment {
    my ($x0, $y0, $x1, $y1, $step) = @_;
    my $ddx  = $x1 - $x0;
    my $ddy  = $y1 - $y0;
    my $dist = sqrt($ddx*$ddx + $ddy*$ddy);
    if ($dist < 1e-12) { return ([$x1, $y1]) }
    my $n = ceil($dist / $step);
    $n = 1 if $n < 1;
    my @pts;
    for my $i (1 .. $n) {
        my $f = $i / $n;
        push @pts, [$x0 + $f*$ddx, $y0 + $f*$ddy];
    }
    return @pts;
}

# ===========================================================================
# PUBLIC ENTRY POINT
# ===========================================================================



# ---------------------------------------------------------------------------
# anamorphic
#
# Transform the current segment path through the cylindrical mirror model.
#
# Usage:
#     $g->anamorphic( $cx, $cy, $R, %opts );
#
# $cx, $cy  Centre of the cylindrical mirror in drawing coordinates.
# $R        Radius of the cylindrical mirror in the same units.
# %opts     Optional parameters (see POD / _ana_build_config).
#
# The method reads the existing psegments, uses their bounding box as the
# image extent, projects every sample point through the mirror model, writes
# the transformed segments back into psegments, then calls stroke() to flush.
# ---------------------------------------------------------------------------
sub anamorphic {
    my ($self, $cx, $cy, $R, %opts) = @_;

    $self->_croak('anamorphic: cylinder radius R must be > 0') unless $R > 0;

    my $step = delete $opts{step} // 1.0;

    # ------------------------------------------------------------------
    # Collect all drawable segment endpoints and compute the image bbox.
    # ------------------------------------------------------------------
    my @segs = @{ $self->{psegments} };

    my (@xs, @ys);
    for my $s (@segs) {
        my $k = $s->{key} // '';
        next unless $k eq 'm' || $k eq 'l';
        push @xs, $s->{sx}, $s->{dx};
        push @ys, $s->{sy}, $s->{dy};
    }
    $self->_croak('anamorphic: segment path is empty') unless @xs;

    my ($u0, $v0) = (min(@xs), min(@ys));
    my ($u1, $v1) = (max(@xs), max(@ys));

    $self->_croak('anamorphic: segment path has zero width or height')
        if $u1 - $u0 < 1e-12 || $v1 - $v0 < 1e-12;

    # ------------------------------------------------------------------
    # Build observer / image-mapping configuration.
    # ------------------------------------------------------------------
    my %cfg = _ana_build_config($cx, $cy, $R, %opts);

    # ------------------------------------------------------------------
    # Extract polylines from the segment path.
    # Each 'm' entry starts a new polyline; 'l' entries continue it,
    # resampled at most $step apart so the non-linear transform stays
    # accurate along longer segments.
    # ------------------------------------------------------------------
    my @polylines;
    my @cur;
    for my $s (@segs) {
        my $k = $s->{key} // '';
        if ($k eq 'm') {
            push @polylines, [@cur] if @cur > 1;
            @cur = ([$s->{dx}, $s->{dy}]);
        }
        elsif ($k eq 'l') {
            push @cur,
                _ana_resample_segment($s->{sx}, $s->{sy},
                                      $s->{dx}, $s->{dy}, $step);
        }
        # penup / pendown / comment entries are ignored
    }
    push @polylines, [@cur] if @cur > 1;

    $self->_croak('anamorphic: segment path contains no drawable segments')
        unless @polylines;

    # ------------------------------------------------------------------
    # Replace the segment path with the transformed version, then flush.
    # ------------------------------------------------------------------
    @{ $self->{psegments} } = ();

    for my $poly (@polylines) {
        my $pen_is_down = 0;
        my ($prev_px, $prev_py);

        for my $pt (@$poly) {
            my ($px, $py) = _ana_transform_point(



( run in 0.608 second using v1.01-cache-2.11-cpan-5735350b133 )