Tinycam is a very simple minded Python library to generate G-code for basic 3D CNC machines by writing a simple Python program describing what is to be cut.
The G-code has only been tested on a (very) low cost 3018 CNC machine ("no name" brand) running GRBL 0.9j with the Candle G-code sender running on Ubuntu Linux. There is no reason I know of why it wouldn't work on other CNC systems, though. Camotics can be used to preview the G-code output and has had no problems with Tinycam's output.
In addition to G-code, the library can produce PostScript and SVG drawings of what a program using the library will cut. These are the main ways of "previewing" what will be done, in fact.
Tinycam is mostly self-contained, but it does rely on a C++ library that converts concave polygons, perhaps with holes, to a collection of convex polygons. This library is due to Ivan Fratric et. al. and is to be found at: https://github.com/ivanfratric/polypartition (Copyright (c) 2011-2021 Ivan Fratric and contributors). An older version of this code is included here for convenience. It is needed for "pocketing" operations. Tinycam has not been built with newer versions of this code, so there may be issues it you choose to use those versions.
To easily "pre-view" what the output G-code will cut, the machine Tinycam is
installed on should have Ghostview installed. A frameless image viewer (pv-tinycam)
written in C++ and based around SDL 2 is included with Tinycam so that,
via Ghostscript, the output can be very easily displayed. The pv-tinycam program
relies on the LodePNG library (Copyright (c) 2005-2020 Lode Vandevenne),
available here: https://github.com/lvandeve/lodepng An older version of this
code is included for convenience (pv-tinycam has not been built with the
current version, so there may be issues if you choose to use that).
Tinycam has been used most for cutting front panels for electronics projects, and the beginning of a "parts library" for this application is included. It has only a hand full of simple parts in it at the moment.
Tinycam is intended primarily for "technical" applications where exact dimensions and positions need to be specified (and, perhaps, calculated). Being a Python library is very convenient for this. However, creating complicated paths programmatically can be difficult or inappropriate. To help with this, Tinycam can import SVG paths from Inkscape. This can be used for cutting text with specific fonts too.
A Tinycam instance is created with:
import tinycam.tinycam as tinycam
...
t = tinycam.tinycam()
A project must be defined first. This specifies the dimensions of the material to be cut, including its thickness:
# Define a project for a workpiece.
proj_name = t.workpiece('circle-lamp', wx=304.0, hy=162.0, dz=3.75)
print('Defined Project:',proj_name)
The objects to be cut are assigned to a tool group. There must be a separate tool group for each tool to be used in the project:
# Define a tool group: 3.175mm acrylic cutter.
td = 3.175
ac3175 = t.tool_group('ac3175', diameter=td, pass_depth=0.3, feed_rate=100.0)
print('Defined Tool Group:',ac3175)
t.set_tool_group(ac3175)
Note that feed rate and pass depth are specified for each tool group. The feed rate unit is millimeters per minute and the pass depth unit is millimeters.
The objects that can be cut (in the current tool group) are:
- Holes: Circular, any size larger than the tool diameter, through or blind,
perhaps an annulus if blind.
hole_name = t.add_hole(center_point, diameter, depth, end_diameter=None, name='auto', fill=True, psannot=None) - D-holes: Circular holes but with a flat top. Always through. Useful for some
electronic components.
d_hole_name = t.add_d_hole(center_point, diameter, flat_diameter, name='auto', fill=True, psannot=None) - Slots: Horizontal or vertical "lines" with radiussed ends, through or blind.
slot_name = t.add_slot(center_point, diameter, length, depth, orient='horizontal', name='auto', fill=True, psannot=None) - Paths: Polylines (open) or polygons (closed: can be cut on, inside or outside the boundary),
through or blind.
path_name = t.add_path(points_list, depth, kind='closed_on', name='auto', fill=True, psannot=None)kindmay be:open,closed_on,closed_insideorclosed_outside. - Pockets: Hollowed out areas with polygonal boundaries.
Can contain uncut internal "islands". Always blind.
pocket_name = t.add_pocket(points_list, depth, name='auto', fill=True, psannot=None) island_name = t.add_island_to_pocket(pocket_name, points_list) - Radrects: Rectangles with radiussed corners, through or blind.
radrect_name = t.add_radrect(point, width, height, radius, depth, kind='inside', name='auto', point_type='bottom_left', fill=True, psannot=None)kindmay beinside,outsideoron.point_typecontrols the meaning ofpointand may becenter,bottom_left,bottom_right,top_leftortop_right. - Text: A line of text engraved in a specified font.
The text is cut using vector font data derived from Hershey fonts.
text_name = t.add_text(self, string, font, height, point, depth, name='auto', spacing='proportional', h_align='left', v_align='bottom', rotate=0.0, psannot=None)heightis in millimeters,pointis the text position (reference point), qualified byh_align(left,midorright) andv_align(bottom,midortop). The direction of the line of text may be rotated anti-clockwise from +X byrotatedegrees about the reference point.spacingmay beproportionalorfixed. - Wrapped text: An arc of text (a line wrapped around a circle).
text_name = t.add_wrapped_text(self, string, font, height, point, depth, name='auto', spacing='proportional', theta_start=180.0, theta_range=350.0, inner_radius=10.0, psannot=None)theta_startis the angle, in degrees, anti-clockwise from +X at which the arc of text is to begin.theta_rangeis the angular extent of the arc.inner_radiusis the radius if the arc at the bottom of the text (not including descenders). The other arguments are as peradd_text().
There is also an experimental add_image() function to engrave an image, but it doesn't
work well enough to describe as yet.
In all the above function descriptions, psannot is a function that can draw text on the
PostScript (and PDF) output only as a descriptive aid. It has no effect on the G-code. How
this feature can be used is described below.
There are a couple of useful utility functions for depth parameters:
t.THRU()returns a depth that will (just) cut through the workpiece.t.VDEPTH(width, vangle)returns the depth needed for a V-bit with anglevangleto produce a cut ofwidthmillimeters. This is relevant to engraving.
For holes, the diameter of the hole can be adjusted so that it a disc of diameter is cut out intact (rather than a hole of that diameter being produced) using:
adjusted_diameter = t.OUTSIDE(diameter)
Coordinates (and lengths such as diameters) can be transformed using a built in geometric transform engine. An example might be:
def wire_slot(t, R):
"""
Cut a slot for a suspension wire.
"""
sp=np.asarray([[-2,-6],[2,-6],[2,6],[-2,6]])
t.transform.begin()
t.transform.add(T=center)
t.transform.add(R=R)
t.transform.add(T=(0,25))
t.add_path(points_list=sp,depth=3.5,kind='closed_inside')
t.transform.end()
...
center=(40,40)
# Suspension wire slots.
wire_slot(t,0)
wire_slot(t,120)
wire_slot(t,-120)
The full set of transform definition functions is:
t.transform.begin(): begin a transformation, pushing the current transformation.t.transform.identity(): set the current transformation to identity.t.transform.add(S=(1.0,1.0), R=0.0, T=(0.0,0.0), order='SRT'): add a transformation.t.transform.end(): end a transformation, popping the previous transformation to be current.
A drawing of the workpiece can be shown using:
t.view_ps(tool_paths=False, tool_group=None)
If tool_paths is True, an indication of where the tool will go is drawn. If
tool_group is given the name of a tool group, only that group will be drawn,
otherwise all groups will be drawn. This function will only work if Ghostscript
and pv-tinycam are installed and working.
Typing the q key will exit the display.
Other "hot keys" available in pv-tinycam are:
- mouse wheel down: zoom in.
- mouse wheel up: zoom out.
(space): reset zoom to 1.s: make display window smaller.l: make display window larger.1: restore original display window size.- left-mouse and drag: move the display window.
- right-mouse and drag: move the image inside the display window.
G-code files for each tool group will be output by the function:
ok = t.write_gcode(show_stats=True)
Each tool group will be output in its own file. The files are named:
<project_name>_<tool_group_name>.nc
The statistics shown include an estimate of the time needed to cut the paths.
PostScript (and PDF) drawings for each tool group can be output with:
ok = t.write_ps(show_stats=True, use_workpiece_size=False, psfilename=None,
tool_paths=False, tool_group=None)
If psfilename is supplied, all tool groups will be drawn to a single PostScript
(and PDF) file, otherwise, each tool group will be drawn in its own file, named according to:
<project_name>_<tool_group_name>.ps
<project_name>_<tool_group_name>.pdf
If use_workpiece_size=True, the page size will be set to the workpiece size, otherwise
it will not be specified in the output.
NOTE that a pair of files, one PS and one PDF, is output by write_ps(). This is to allow
easy printing of the files, especially on macOS, recent versions of which no longer deign to
process PS files in Preview. The two files have exactly the same contents.
A "planning sheet" can be written as PostScript and printed using:
t.write_planning_sheet()
The output file is tinycam_plan.ps.
The paths to be cut can also be drawn as SVG using:
t.write_svg()
It is possible to save a description of what is to be done to a workpiece as a .pickle
file with:
t.save_work()
The output file is named:
<project>.pickle
Such a file can be loaded with:
t.load_work()
In practice, this capability isn't very useful, though. The Python program used to define workpiece operations is really all that is needed.
After installation, Tinycam can be exercised a bit using the command:
tctests
This will create a project, add a workpiece and cut that with most of the operations described
above. The result will be pre-viewed with pv-tinycam. An example of engraving with one of the
available fonts will also be run and pre-viewed.
It is fairly easy to export SVG paths for various objects from Inkscape. If Inkscape is set up to display millimeter units, positions in Inkscape correspond to "real world" units.
It will usually be best to use a rectangle as a background. This could have the dimensions of the workpiece. A simple example of exporting the paths for a text object is a usable form is:
- Create a background rectangle.
- Create some text (e.g. "411").
- Select the text object. (Observe "Layer & Objects" panel).
- Convert to paths:
Path > Object to Path - This will create a single path for the whole text object. It is usually necessary to have a separate path for each letter.
- To get that use:
Path > Break Apart - Then Export (Observe Export panel).
- Use
Plain SVG - Enter an appropriate output file name.
- Press the
Exportbutton.
- Use
The SVG file can be loaded in Tinycam using:
svg_paths = t.parse_svg_file(svg_file_name, precision=0.5, set_origin=[0,0], y_flip=True)
The SVG paths are flattened to Tinycam polyline paths using the approximation precision
specified (in millimeters). If set_origin is not None, the bottom left of the bounding
box of the SVG paths will be translated to the specified coordinate. If a background
rectangle exists in the SVG file, the bottom left of this will go to the origin (or some
other specified point) in Tinycam coordinates. Inkspace has a coordinate system that has (0,0)
at the top left. Tinycam has (0,0) at the bottom left, as is appropriate for G-code. If
y_flip is True, the Y-axis will be "flipped" to compensate for this difference. Paths
from sources other than Inkscape may not need this (only Inkscape paths have been tested, but
other sources of SVG paths should work too). This function returns a dictionary of SVG paths
with the key being the path name used by Inkscape and the value being a dictionary with
entries for the path's flattened points and the path's bounding box.
It is straightforward to preview what has been read using:
t.view_svg_paths(svg_paths)
This will show the paths in pv-tinycam labelled with their path names. These names can
be used to select paths for use with add_path().
A single path can be fetched from an svg_paths dictionary using:
path = t.fetch_svg_path(svg_paths, path_name)
This is typically used with add_path() along these lines:
t.add_path(t.fetch_svg_path(svg_paths,'path3'), t.THRU(), kind='closed_outside', fill=False)
The bounding box for a single path can be returned with:
xmin, xmax, ymin, ymax = fetch_svg_path_bounds(svg_paths, path_name )
A complete example of using SVG paths with Tinycam is given in svgtest.py in the examples
sub-directory.
The fonts available are a subset of the venerable Hershey fonts, stored in a format that was created for the "DIMFILM resurrection" project which resulted in GPLOT2 (https://github.com/nickglazzard1959/gplot2 ).
The available font names are:
F.SIMPLE.DIM
F.SERIF.DIM
F.SERIF.GREEK.DIM
F.MATH.SMALL.DIM
F.MARKER.DIM
F.SANS.DIM
F.BASIC.DIM
F.SERIF.SMALL.DIM
F.SERIF.COMPLEX.DIM
F.SERIF.ITALIC.SMALL.DIM
F.SERIF.ITALIC.DIM
F.SERIF.COMPLEX.ITALIC.DIM
F.SIMPLE.SCRIPT.DIM
F.SANS.SCRIPT.DIM
F.GOTHIC.DIM
F.GOTHIC.GERMAN.DIM
F.GOTHIC.ITALIAN.DIM
F.SIMPLE.GREEK.DIM
F.SERIF.GREEK.SMALL.DIM
F.CYRILLIC.DIM
F.SYMBOLS.DIM
F.ASTRONOMICAL.DIM
F.ASTROLOGICAL.DIM
F.MUSICAL.DIM
To create a "font catalogue" in PostScript format, use the command:
tcfonts
after installing Tinycam. This will create a file called: test_fonts.ps which
will show every character in every font, as well as testing the font functions
quite thoroughly.
The file: tinycam_parts.py contains a few random part definitions for
parts I happen to have used in recent projects. The part definitions can
be used to cut out holes suitable for mounting the parts on a panel.
In addition to random components, functions for drilling ventilation meshes consisting of an hexagonal arrangement of holes are included:
vent_mesh(tc, diam, s, nh, nv, c)
where tc is a Tinycam instance, diam is the hole diameter in mm,
nh is the nunber of holes horizontally and nv is the number
vertically. The center of the mash is at c, an (x,y) tuple or
array. s is the distance between the edges of the holes in mm.
Alternatively,
vent_mesh_wh(tc, diam, s, w, h, c):
Instead of specifying the number of holes vertically and horizontally,
this takes a desired overall width, w, and height, h of the whole
mesh in millimeters.
These were added to Tinycam mainly to allow parts defined in the parts library to display the part name in the PostScript output. The function that defines a "part" in the parts library often defines a local function specifying how to draw the "user annotation". An example will best explain this, perhaps:
def push_button_la16j_11dz(tc, center):
"""
Hole for an LA16J-11DZ latching illuminated push button switch.
Rectangular switch front dimensions: 24 x 18 mm.
"""
def ua(xc, yc, diameter, rterm):
tc.ps.rect1((xc,yc), 24.0, 18.0, grey=0.2, thick=0.5, dash=True)
tc.ps.text((xc,yc), 'LA16J-11DZ')
tc.add_hole(center_point=center, diameter=16, depth=tc.THRU(), psannot=ua)
return (24.0, 18.0)
The user annotation function, ua(), can use any of the Tinycam PostScript drawing
functions, so this is quite flexible. The arguments passed to a ua() function depends
on the type of object being drawn, as follows:
- Path:
(path, path_item['name']) - Hole:
(xc, yc, diameter, rterm) - D-Hole:
(xc, yc, diameter, flat_diameter) - Slot:
(xc, yc, orient, diameter, length) - Pocket:
(outer_polygon) - Radrect:
(xc, yc, width, height, radius) - Text:
()
While flexible, this scheme is more complex than I would like it to be!
Tinycam consists of a Python package and two supporting items written in C/C++.
First, clone Tinycam from Github:
git clone https://github.com/nickglazzard1959/tinycam.git
Then cd tinycam.
Some non-Python libraries and tools must be present on the system before installation:
- A working C/C++ build environment.
- libjpeg development libraries.
- SDL 2 development libraries.
- Ghostscript
These are readily available via the Linux package manager for your Linux distribution or via Homebrew or MacPorts for macOS.
For example, using Homebrew for macOS:
xcode-select --install
brew install jpeg
brew install sdl2
brew install ghostscript
For Debian Linux:
sudo apt install libsdl2-dev
sudo apt install libjpeg-dev
sudo apt install ghostscript
Once the pre-requisites are installed, the C/C++ components can be built with:
./build.sh
Note that the pv-tinycam program and the shared library containing
polypartition (and some interface code for that) will be installed in:
/usr/local/lib
/usr/local/bin
These must exist and /usr/local/bin must be on the PATH of people
using Tinycam.
Use of a Python virtual environment (venv) is recommended. You will
need a working python3 interpreter to get started. The minimum supported
Python version is 3.9. A single venv may be shared between multiple tools,
but creation and use of a venv for Tinycam is shown for completeness. This
example venv is called genv.
- Create the venv:
python3 -m venv ~/genv
- Activate the newly created venv:
source ~/genv/bin/activate
- In the top level of the cloned Tinycam repo:
pip install .
That should complete the installation. The tctests command should run
the test program after installation.
The way Tinycam is intended to be used requires that the PostScript drawing of the workpiece be printed out and glued to the workpiece. This drawing is then used to set the X/Y zero point and to monitor the progress of the job.
This is only useful if the drawing is printed pretty much exactly to scale, so that millimeter markings when printed measure with a ruler as marked.
Unfortunately, there are many steps involved in printing a document and so there are many stages at which scaling can go awry. The safest way of dealing with this is to allow a PostScript specific scaling to be specified directly. Since the X and Y axes may need different scales (they do on my setup), two independent scales are needed.
Note that these scales ONLY apply to PostScript / PDF output. In the G-code case, 1mm specified had better correspond to 1mm moved or there is no hope!
The scales are specified in a configuration file called:
~/tinycamconfig.ini
Here is an example of its contents:
[printer_scales]
xscale = 1.03
yscale = 1.035
The scales must be determined by printing a design and measuring the spacing on the
millimeter grid printed as the background. If ~/tinycamconfig.ini does not exist, or
the section [printer_scales] is not present, the scales default to 1.0.
An example of a PostScript/PDF drawing of (one tool group of) a design is:
As noted, a design is created in the form of a Python program using the functions
Tinycam provides. The only "interactive" feedback available is to use t.view_ps()
in the program to display what will be cut. The overall "workflow" is:
-
Write a program that defines some significant part of the project.
-
Use
t.view_ps()in that program to see what things look like. -
Modify and add to the program until the whole project has been defined and looks correct.
-
Add
t.write_gcode()to write the G-code to be sent to the CNC to cut the workpiece andt.write_ps()to generate PostScript and PDF drawings of the design which can be printed. NOTE THAT a separate pair of G-code and PS/PDF files will be created for each tool group defined in the project, and hence for each tool that needs to be used. -
Optionally, preview the cutting process using a software tool such as
Camotics. -
Print the PostScript/PDF drawings of the design so it is exactly to scale.
-
Glue the first tool group drawing to the workpiece material to serve as a guide when setting up the CNC machine. A glue stick for gluing paper works for this.
-
Attach the workpiece to some spoil material (e.g. corrugated "twin wall" plastic sheet cut to size). Use thin double sided tape for this.
-
Attach the spoil material to the bed of the CNC machine, with the X and Y axes of the workpiece carefully aligned with those of the bed.
-
Transfer the G-code file for the first tool to be used to the device running the G-code sender that controls the CNC machine.
-
Insert the appropriate cutting tool in the CNC machine's collet.
-
Set up the CNC machine X,Y zero to match the appropriate point on the drawing attached to the workpiece. This is usually the origin where the X and Y axes cross.
-
Set the Z height zero using an appropriate method.
-
Raise the tool (e.g. by sending the G-code
G00 Z10manually). -
Start the spindle manually if necessary (the G-code contains a command to do this, but my CNC machine ignores it).
-
Send the G-code file for the first tool group.
- Wait for cutting to finish (taking care of the machine as necessary throughout).
- If there are more tools / tool groups to cut, get and send each in turn by repeating most of the steps above.
There are some random examples of Tinycam scripts in the examples
subdirectory. These are actual projects that I have created and cut
workpieces with the G-code that was output.
-
circle-lamp.py- Replacement for the innards of a T9 circular lamp for an illuminated magnifier. -
cutfonts.py- Very basic text engraving test. -
keyfobs.py- Circular key fobs with engraved numbers. -
lamp.py- Adapter to match a lamp shade to a lamp. -
panel1.py- Cut holes for a front panel. -
pcb-surface-pocket.py- Cut a 1mm deep pocket in something. -
psu19v.py- Cut holes for a simple front panel with a ventilation mesh. -
svgtest.py- Cut a house number sign using SVG paths exported from Inkscape.
To generate output for any of these, cd examples then (with the venv active):
python keyfobs.py
etc.
Note that I only really cut cast acrylic with my 3018. Anything else is a bit "challenging" for it ... :-) ... That said, it is actually really useful for my purposes and it is amazing for the price, so no complaints from me!








