NAME

Graphics::Penplotter::GcodeXY::Geometry3D - Role::Tiny role adding 3-D geometry to GcodeXY

VERSION

v0.4.0

SYNOPSIS

$g->gsave();                              # saves both 2-D and 3-D state
$g->initmatrix3();                        # reset 3-D CTM
$g->translate3(50, 50, 0);               # move 3-D origin
$g->rotate3(axis => [0,0,1], deg => 45); # spin around Z
$g->scale3(10);                           # uniform scale

my $m = $g->sphere(0, 0, 0, 1, 12, 24); # UV sphere mesh
my $s = $g->flatten_to_2d($m);           # project to 2-D edge list
$g->draw_polylines($s);                  # draw via host pen hooks

$g->grestore();
$g->output('myplot.gcode');

DESCRIPTION

This Role::Tiny role grafts a full 3-D geometry pipeline onto Graphics::Penplotter::GcodeXY. It is careful not to shadow any of the host class's own methods:

  • gsave/grestore are extended via after modifiers so the 3-D Current Transformation Matrix (CTM3) stack stays in sync with the host's 2-D stack without overriding anything.

  • All 3-D variants of 2-D methods that would otherwise conflict are renamed with a 3 suffix: translate3, scale3, rotate3, moveto3, line3, lineR3, initmatrix3, translateC3, currentpoint3.

  • All 3-D private state lives in $self->{_g3_*} hash slots.

The coordinate system is right-handed (+Z out of the screen) and the internal representation uses 4x4 homogeneous matrices in row-major order.

REQUIRED METHODS

The consuming class must provide:

penup  pendown  _genfastmove  _genslowmove  stroke  _croak  gsave  grestore

METHODS

CTM and transforms

initmatrix3()

Reset the 3-D CTM to identity.

translate3($tx, $ty [, $tz])

Pre-multiply the 3-D CTM by a translation.

translateC3()

Move the 3-D origin to the current 3-D position, then reset the position to (0,0,0).

scale3($sx [, $sy [, $sz]])

Pre-multiply by a scale matrix. If $sy/$sz are omitted they default to $sx (uniform scale).

rotate3(axis => [$ax,$ay,$az], deg => $angle)

Pre-multiply by a rotation around an arbitrary axis.

rotate3_euler($rx, $ry, $rz [, $order])

Pre-multiply by a sequence of axis-aligned rotations. $order is a three-character string such as 'XYZ' (default).

compose_matrix($aref, $bref)

Multiply two 4x4 matrices; returns a new matrix ref. Neither input is modified.

invert_matrix($mref)

Invert a 4x4 matrix (Gauss-Jordan with partial pivoting). Returns a matrix ref, or undef if the matrix is singular.

3-D current point

currentpoint3()

Return the current 3-D position as a list ($x, $y, $z).

currentpoint3($x, $y, $z)

Set the current 3-D position.

Point transformation

transform_point($pt_ref)

Transform a point (arrayref [$x,$y,$z]) through the current CTM3. Returns ($tx, $ty, $tz).

transform_points($pts_ref)

Transform an arrayref of points; returns an arrayref of [$tx,$ty,$tz].

3-D drawing primitives

moveto3($x, $y [, $z])

Lift the pen, fast-move to the projected 2-D position, lower pen.

movetoR3($dx, $dy [, $dz])

Relative moveto3 from the current 3-D position.

line3($x1,$y1,$z1 [, $x2,$y2,$z2])

Six-arg form: move to start, draw to end. Three-arg form: draw from the current position.

lineR3($dx, $dy [, $dz])

Relative line from the current 3-D position.

polygon3(x1,y1,z1, ...)

Move to the first triple, draw through the remaining triples.

polygon3C(x1,y1,z1, ...)

Like polygon3 but automatically closes back to the first point.

polygon3R(dx1,dy1,dz1, ...)

Like polygon3 but each triple is relative to the preceding point.

Wireframe solid drawing (draw directly, no mesh returned)

box3($x1,$y1,$z1, $x2,$y2,$z2)

Draw a wireframe axis-aligned box between two opposite corners.

cube($cx,$cy,$cz,$side)

Draw a wireframe cube centred at (cx,cy,cz).

axis_gizmo($cx,$cy,$cz [, $len [, $cone_r [, $cone_h]]])

Draw three labelled axis arrows (X, Y, Z) as wireframe lines with small arrow cones. $len is the total axis length (default 1). The cone radius and height default to 5% and 15% of $len respectively.

Mesh-returning solid primitives

All of the following return a mesh structure { verts => \@v, faces => \@f } which can be passed to flatten_to_2d, hidden_line_remove, mesh_to_obj, etc.

mesh($verts_ref, $faces_ref)

Low-level constructor. Build a mesh from existing arrays.

prism($cx,$cy,$cz, $w,$h,$d)

Axis-aligned rectangular prism (box) centred at (cx,cy,cz), with dimensions w (X), h (Y), d (Z). A cube is prism with w == h == d. Returns a closed 12-face triangulated mesh.

sphere($cx,$cy,$cz, $r [, $lat [, $lon]])

UV-sphere mesh. $lat and $lon control the tessellation density (defaults 12 and 24).

icosphere($cx,$cy,$cz, $r [, $subdivisions])

Icosphere mesh built by repeated midpoint subdivision of a regular icosahedron. $subdivisions defaults to 2 (320 faces). Produces a more uniform tessellation than sphere.

cylinder($base_ref, $top_ref, $r [, $seg])

Cylinder mesh. $base_ref and $top_ref are [$x,$y,$z] centre points. Side walls only; no end caps.

frustum($cx,$cy,$cz, $r_bot,$r_top,$height [, $seg])

General truncated cone (frustum) centred at (cx,cy,cz). Both end caps are included. When $r_top == 0 this is a cone; when $r_bot == $r_top it is a closed cylinder.

cone($cx,$cy,$cz, $r,$height [, $seg])

Convenience wrapper: frustum with r_top = 0.

capsule($cx,$cy,$cz, $r,$height [, $seg_r [, $seg_h]])

Cylinder with hemispherical end caps. $height is the length of the cylindrical body (not counting the caps). $seg_r is the number of radial segments (default 16); $seg_h is the number of latitudinal segments per hemisphere (default 8).

plane($cx,$cy,$cz, $w,$h [, $segs_w [, $segs_h]])

Flat rectangular mesh in the XY plane, centred at (cx,cy,cz). Dimensions $w x $h; subdivided into $segs_w x $segs_h quads. Useful for floors, billboards, and UI surfaces.

torus($cx,$cy,$cz, $R,$r [, $maj_seg [, $min_seg]])

Torus mesh in the XY plane. $R is the major radius (centre of tube to centre of torus); $r is the minor radius (tube radius). Defaults: 24 major segments, 12 minor segments.

disk($cx,$cy,$cz, $r [, $seg])

Flat circular disk mesh in the XY plane. Fan-triangulated from the centre. Vertex 0 is the centre; vertices 1..$seg are the rim.

pyramid($cx,$cy,$cz, $r,$height [, $sides])

Regular-polygon-base pyramid. (cx,cy,cz) is the base centre; $r is the base circumradius; $height is the height in +Z. $sides defaults to 4 (square pyramid). The base cap is included.

Quaternions

quat_from_axis_angle($axis_ref, $deg)

Return a unit quaternion [$w,$x,$y,$z].

quat_to_matrix($q)

Convert a quaternion to a 4x4 rotation matrix.

quat_slerp($q1, $q2, $t)

Spherical linear interpolation (0 <= t <= 1).

Mesh utilities

bbox3($mesh_or_pts)

Returns ([$minx,$miny,$minz], [$maxx,$maxy,$maxz]).

compute_normals($mesh)

Compute face and averaged vertex normals in-place; returns $mesh.

Visibility

backface_cull($mesh [, view_dir => \@dir])

Return an arrayref of the indices (into $mesh->{faces}) of faces whose outward normal has a negative dot product with the view direction, i.e. faces that are pointing toward the camera and therefore visible.

The view direction defaults, in order of preference, to:

  1. The fwd vector stored by the most recent set_camera() call, if one has been made.

  2. [0, 0, -1] (looking along the negative Z axis) if no camera has been set.

Pass view_dir => \@v to override both defaults with an explicit unit vector pointing from the scene toward the camera.

occlusion_clip($mesh [, res => N])

Z-buffer rasterisation; returns arrayref of [[p1,p2],...] edge segments.

hidden_line_remove($mesh [, %opts])

Back-face cull then occlusion clip; returns edge segments.

2-D output

flatten_to_2d($mesh_or_polylines)

Project mesh edges or pass-through polylines; returns [[$p1,$p2],...].

draw_polylines($segs_ref)

Emit segments via the host's pen hooks; calls stroke() at the end.

project_to_svg($obj [, %opts])

Return an SVG string of the projected edges.

Mesh I/O

mesh_to_obj($mesh [, $name])

Serialise to ASCII OBJ string.

mesh_from_obj($str)

Parse an ASCII OBJ string; returns a mesh.

mesh_to_stl($mesh [, $name])

Serialise to ASCII STL string.

mesh_from_stl($str)

Parse an ASCII STL string; returns a mesh (vertices are de-duplicated).

Camera

The three camera methods together provide a gluLookAt-style workflow for positioning the viewer in 3-D space. Typical usage:

$g->set_camera(
    eye    => [5, 5, 10],   # camera position in world space
    center => [0, 0,  0],   # point to look at
    up     => [0, 1,  0],   # world up hint
);
$g->camera_to_ctm();        # bake view matrix into the 3-D CTM

my $m = $g->sphere(0, 0, 0, 1);
my $v = $g->backface_cull($m);          # uses stored fwd automatically
$g->draw_polylines($g->flatten_to_2d(
    { verts => $m->{verts},
      faces => [ @{$m->{faces}}[@$v] ] }
));

Camera state is saved and restored by gsave() / grestore() alongside the 3-D CTM and current point.

set_camera(eye => \@e, center => \@c [, up => \@u])

Position the camera using a gluLookAt-style interface.

eye (required) is an arrayref [$ex,$ey,$ez] giving the camera position in world space.

center (required) is an arrayref [$cx,$cy,$cz] giving the point in world space the camera looks at. Must differ from eye; croaks with "same point" otherwise.

up (optional, default [0,1,0]) is an arrayref [$ux,$uy,$uz] giving a world-space hint for the upward direction. Must not be parallel to the view direction (center - eye); croaks with "parallel" if it is.

The method builds an orthonormal right-handed camera basis:

forward  =  normalise(center - eye)
right    =  normalise(forward x up_hint)
up       =  right x forward           # reorthogonalised

and assembles a standard 4x4 world-to-camera view matrix from those three basis vectors and the eye position. The result is stored internally and can be retrieved with get_camera().

After the call, backface_cull() picks up the stored fwd vector automatically unless an explicit view_dir is supplied.

get_camera()

Return the camera record set by the most recent set_camera() call, or undef if set_camera() has not yet been called (or if the record was cleared by grestore()).

The returned hashref contains:

eye

Arrayref [$ex,$ey,$ez] - the eye position as supplied.

center

Arrayref [$cx,$cy,$cz] - the look-at point as supplied.

up

Arrayref [$ux,$uy,$uz] - the reorthogonalised up vector (not necessarily the same as the hint passed in).

fwd

Arrayref [$fx,$fy,$fz] - unit forward vector pointing from eye toward center. Used automatically by backface_cull().

view

4x4 arrayref-of-arrayrefs - the world-to-camera view matrix in row-major order. The first three rows encode the camera basis (right, up, -forward); the translation is in the rightmost column.

camera_to_ctm()

Pre-multiply the view matrix stored by set_camera() into the 3-D CTM via the same _g3_premul4 path used by translate3, rotate3, etc.

After this call every subsequent transform_point(), moveto3(), line3(), flatten_to_2d(), etc. automatically includes the camera transform; no further action is needed to get correct projected coordinates.

Croaks with "no camera" if called before set_camera().

This method is additive: calling it more than once will compound the camera transform. If you need to reposition the camera, call initmatrix3() first (or use gsave() / grestore()).

set_perspective(fov => $deg [, aspect => $r, near => $n, far => $f])

Build and store a symmetric perspective projection matrix (equivalent to OpenGL's gluPerspective). Does not modify the CTM; call perspective_to_ctm() afterwards to apply it.

fov (optional, default 45)

Vertical field of view in degrees. Must be in (0, 180).

aspect (optional, default 1.0)

Viewport width / height ratio.

near (optional, default 0.1)

Distance to the near clipping plane. Must be > 0.

far (optional, default 100)

Distance to the far clipping plane. Must be > near.

The resulting 4x4 matrix (row-major, column-vector convention) has -1 in position [3][2], which causes transform_point() to compute tw = -z. The existing perspective divide (triggered whenever tw != 0 and tw != 1) then gives correctly foreshortened X and Y coordinates. Z is discarded by flatten_to_2d(), which only retains X and Y.

Projection state is saved and restored by gsave() / grestore().

set_frustum(left => $l, right => $r, bottom => $b, top => $t, near => $n, far => $f)

Build and store an asymmetric (off-axis) perspective projection matrix (equivalent to OpenGL's glFrustum). All six named arguments are required; near and far default to 0.1 and 100 respectively.

left, right, bottom, top are the X/Y extents of the view volume at the near plane. Setting left = -right and bottom = -top reproduces a symmetric frustum identical to set_perspective().

Use this method for off-centre viewports, stereo rendering, or anamorphic projections. As with set_perspective(), call perspective_to_ctm() afterwards to apply it.

perspective_to_ctm()

Pre-multiply the projection matrix stored by set_perspective() or set_frustum() into the 3-D CTM (CTM := P x CTM).

After this call every subsequent transform_point() call applies the full view + projection pipeline, and flatten_to_2d() yields perspective-correct 2-D coordinates.

Croaks if neither set_perspective() nor set_frustum() has been called.

Typical full workflow:

$g->initmatrix3();
$g->set_camera(eye => [5,5,10], center => [0,0,0]);
$g->camera_to_ctm();
$g->set_perspective(fov => 45, aspect => 1.0, near => 0.1, far => 100);
$g->perspective_to_ctm();
# All drawing calls now produce perspective-foreshortened output.
get_projection()

Return the 4x4 projection matrix stored by the most recent set_perspective() or set_frustum() call, or undef if none has been set. The matrix is an arrayref of four arrayrefs (row-major).

Numeric configuration

set_tolerance($eps), get_tolerance()

Set/get the floating-point equality tolerance (default 1e-9).

set_units($units)

Store a units tag (e.g. 'mm'); no automatic scaling is applied.

set_coordinate_convention(handedness => ..., euler_order => ...)

Store convention tags for downstream use.

IMPLEMENTATION NOTES

State storage

All 3-D state lives in $self->{_g3_*} to avoid collisions with the host:

_g3_CTM          4x4 arrayref-of-arrayrefs (current 3-D transform)
_g3_gstate       arrayref of save-state records
_g3_posx/y/z     3-D current point
_g3_camera       camera record (eye/center/up/fwd/view) set by set_camera()
_g3_tolerance    floating-point epsilon
_g3_units        units tag
_g3_handedness   coordinate convention
_g3_euler_order  default Euler axis order

Why method names have a 3 suffix

Role::Tiny does not override methods that already exist in the consuming class. Because Graphics::Penplotter::GcodeXY already defines translate, scale, rotate, moveto, line, lineR, gsave, grestore, and initmatrix, any role method with the same name would be silently discarded. The 3 suffix makes the 3-D variants unambiguous. gsave/grestore are augmented instead via after modifiers.

Mesh representation

All solid primitives that return a mesh use the structure:

{ verts => \@v, faces => \@f }

where @v is an array of [$x,$y,$z] position arrayrefs and @f is an array of [$i0,$i1,$i2] triangle index arrayrefs. Winding order is counter-clockwise when viewed from the outside (right-hand normal pointing outward).

AUTHOR

Albert Koelmans

LICENSE

Same terms as Perl itself.