Skip to content

nickglazzard1959/tinycam

Repository files navigation

Tinycam - A very minimal CAM package for basic CNC machines

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.

High level functions

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)
    
    kind may be: open, closed_on, closed_inside or closed_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)
    
    kind may be inside, outside or on. point_type controls the meaning of point and may be center, bottom_left, bottom_right, top_left or top_right.
  • Text: A line of text engraved in a specified font.
    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)
    
    The text is cut using vector font data derived from Hershey fonts. height is in millimeters, point is the text position (reference point), qualified by h_align (left, mid or right) and v_align (bottom, mid or top). The direction of the line of text may be rotated anti-clockwise from +X by rotate degrees about the reference point. spacing may be proportional or fixed.
  • 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_start is the angle, in degrees, anti-clockwise from +X at which the arc of text is to begin. theta_range is the angular extent of the arc. inner_radius is the radius if the arc at the bottom of the text (not including descenders). The other arguments are as per add_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 angle vangle to produce a cut of width millimeters. 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.

Using SVG paths exported from Inkscape

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 Export button.

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.

Fonts

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.

Parts library

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.

Using psannot functions

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!

Installation

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.

Pre-requisites

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.

Installing the Tinycam Python package

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.

Configuration

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:

Design Drawing Example

Usage

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 and t.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.

    Glue group drawing to workpiece

    Cutting to size with group drawing attached to workpiece

  • 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.

    Workpiece, spoil material and CNC 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 Z10 manually).

  • 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.

Sending G-code to the CNC machine

  • 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.

Examples

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.

    Circle lamp example 1

    Circle lamp example 2

  • 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.

    SVG paths example drawing

    SVG paths example result

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!

About

Very minimal CAM software for a 3 axis CNC router.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages