About

TPL or Tool Path Language is a programming language for creating machine tool paths for CNCs. It is based on JavaScript and is a powerful replacement for the venerable but horribly outdated GCode language. However, TPL can output GCode so it remains compatible with existing machine control software such as LinuxCNC.

TPL is part of the CAMotics project and can be used in conjunction with the CAMotics CNC simulator.

Downloads

TPL is part of the CAMotics project. See camotics.org downloads for information on downloading and installing the software.

An Example

This section aims to give you a quick and dirty introduction to TPL and some of its features via a few simple examples.

TPL code is JavaScript with a library of built-in functions which allow you to generate tool paths. It looks like this:

feed(400);     // Set the feed rate to 400 millimeters per minute
tool(1);       // Select tool 1

rapid({z: 5}); // Move to a safe height of 5mm
rapid({x: 1, y: 1});  // Go to start position
speed(2000);   // Spin at 2000 RPM in the clockwise direction

cut({z: -3});  // Cut down to depth
cut({x: 11});  // Cut to second corner
cut({y: 11});  // Cut to third corner
cut({x: 1});   // Cut to forth corner
cut({y: 1});   // Cut back to begining

rapid({z: 5}); // Move back to safe position
speed(0);      // Stop spinning

The CAMotics simulator shows the result:

The previous example initializes the machine then cuts a square. Note, you must always set the feed rate to something other than zero before making a cut. It's also a good idea to select a tool and set the spindle speed. Also note, TPL uses metric units by default. You can switch to imperial units by calling units(IMPERIAL).

Note, the bracket notation {} used above lets you specify specific arguments and leave others to their default values. The following function calls are all equivalent:

rapid(5, 10, 15);
rapid({x: 5, y: 10, z: 15});
rapid({z: 15, x: 5, y: 10});

The code in the square example may be more readable than typical GCode but TPL's real power comes from its ability to use more advanced language constructs such as functions. The above square example could be wrapped in a function like this:

function square(size, depth, safe) {
  cut({z: depth});  // Cut down to depth
  icut({x: size});  // Cut to second corner
  icut({y: size});  // Cut to third corner
  icut({x: -size}); // Cut to forth corner
  icut({y: -size}); // Cut back to begining
  rapid({z: safe}); // Move back to safe position
}

feed(400);          // Set the feed rate to 400 mm per minute
tool(1);            // Select tool 1

rapid({z: 5});      // Move to a safe height of 5mm
rapid({x: 1, y: 1});  // Go to start position
speed(2000);        // Spin at 2000 RPM in the clockwise direction

square(10, -3, 5);

speed(0);           // Stop spinning

Note the square() function uses icut() the incremental version of cut().

Given the square() function you can now cut repeated squares like this:

for (i = 1; i <= 10; i++) square(i * 5, -3, 5);

Here is the result of running the square function as in the for loop above.

To continue the example a bit further and show more of the power of TPL we can repeat and rotate the squares to create an interesting pattern using the following program:

function square(size, depth, safe) {
  cut({z: depth});  // Cut down to depth
  icut({x: size});  // Cut to second corner
  icut({y: size});  // Cut to third corner
  icut({x: -size}); // Cut to forth corner
  icut({y: -size}); // Cut back to begining
  rapid({z: safe}); // Move back to safe position
}

function squares(count, space, depth, safe) {
  for (var i = 1; i <= count; i++) {
    rapid({x: -space * i, y: -space * i});
    square(i * 2 * space, depth, safe);
  }
}

feed(400);           // Set the feed rate to 40 mm per second
tool(1);             // Select tool 1

rapid({z: 5});       // Move to a safe height of 5mm
rapid({x: 0, y: 0}); // Go to center position
speed(2000);         // Spin at 2000 RPM in the clockwise direction

for (var i = 0; i < 4; i++) {
  squares(10, 5, -3, 5);
  rotate(Math.PI / 8); // Rotate clockwise 15 degrees
}

speed(0); // Stop spinning

The simulator shows the final result:

Using TPL

After downloading and installing the latest CAMotics you can write a TPL program, or try out one of the examples. TPL programs usually have the file extension .tpl. You can execute a TPL program like this:

tplang input.tpl

This will print the resulting GCode to the screen. You can also save the output to a file like this:

tplang input.tpl > output.gc

You can then view the results in the CAMotics simulator like this:

camotics output.gc

You will also probably have to configure a tool table and define the workpeice (the material being cut) in order to get a correct simulation. See camotics.org for more information on using the simulator.

API

The TPL Application Programming Interface allows you to duplicate any GCode program easily in a much more readable way but you also have the full power of the JavaScript language at your finger tips so much more is possible. In fact you don't even need CAM software you can model the shapes you want to cut directly in TPL.

GCode

gcode(path)

Include an external gcode file. Note that the contents are processed, not included raw, so unsupported commands or phrasings may be lost.

gcode("plugin.gcode");          // Include "plugin.gcode"
gexec(code)

Includes a gcode string. Note that the contents are processed, not included raw, so unsupported commands or phrasings may be lost.

gexec("G0 X100 Y200");          // Add gcode command "G0 X100 Y200"

Tool Movement

rapid(x, y, z, a, b, c, u, v, w, incremental=false)

Issue a linear motion at the rapid feed rate from the current position to the new position defined by the provided axes arguments.

rapid(0, 0, 0);          // Rapid to x=0, y=0, z=0
rapid(10, 10);           // Rapid to x=10, y=10, z remains 0
rapid({z: 10, y: 10});   // Rapid to y=10, z=10, x remains 10
irapid(x, y, z, a, b, c, u, v, w, incremental=true)

The same as rapid() but incremental defaults to true.

cut(x, y, z, a, b, c, u, v, w, incremental=false)

The same as rapid() except moves are at the current feed rate set by a call to feed().

icut(x, y, z, a, b, c, u, v, w, incremental=true)

The same as cut() but incremental defaults to true.

arc(x = 0, y = 0, z = 0, angle = Math.PI * 2, plane = XY)

x, y and z specify the offset from the current position to the far center of the helical move in the selected plane. If the z value is zero then the move is a simple arc in the selected plane, otherwise it is a true helical move with an axis of length equal to the absolute value of the z value either above or below the selected plane through the current control point depending on the sign.

angle is the rotation of the arc in radians. A positive value indicates a clockwise rotation, negative a counter-clockwise rotation.

plane specifies to which plane the helical axis is perpendicular. Valid values are XY (the default), XZ or YZ.

probe(x, y, z, a, b, c, u, v, w, port=0, active=true, error=true)

Make a linear move in the direction indicated until the probe state changes to the target state. If active is true the probe motion will stop on contact. Otherwise it will stop on loss of contact.

probe() will return the coordinates of the position at the time the probe changed state. If the programmed point was reached before the state changed then the programmed point will be returned instead.

probe(z = -10);  // Probe towards z=-10
probe(z = 10, active = false);  // Probe away from current z

It is an error if:

  • The current point is the same as the programmed point.
  • The current feed rate is zero.
  • The probe is already in the target state.

dwell(seconds)

seconds indicate the time in seconds that all axes will remain unmoving. Fractions of a second may be used.

dwell(0.5);  // Dwell for half a second.

Machine State

feed(rate, mode = FEED_UNITS_PER_MIN)

rate indicates the units per minute in the XYZ Cartesian system of all subsequent non-rapid moves until the feed rate is changed.

mode may be one of the following:

  • FEED_UNITS_PER_MIN - The feed rate is in units per minute. The unit may be inches, millimeters or degrees depending on the current unit of length and which axes are moving.
  • FEED_INVERSE_TIME - Indicates that moves should be completed in one divided by rate minutes. For example, if rate is 2.0, moves should be completed in half a minute.
  • FEED_UNITS_PER_REV - Means that the controlled point should move a certain number of units per revolution.
Note, these names are case sensitive.

If no arguments are given, returns the current feed rate and mode.

feed(4);           // Move at 4 units per minute
rate = feed()[0];  // Get the current feed rate
speed(rate, surface, max)

rate indicates the revolutions per minute of the spindle.

If rate is positive the spindle will turn in the clockwise direction. If negative in the counterclockwise direction.

A rate of zero turns the spindle off.

If surface is specified it selects a constant surface speed of surface feet per minute if IMPERIAL units are selected or meters per minute if METRIC units are selected. If max is also specified it indicates the maximum spindle RPM.

If no arguments are given, returns the current spindle speed, surface speed and maximum RPM.

speed(1000);    // Spin clockwise at 1K RPM.
speed(-20000);  // Spin counterclockwise at 20K RPM.
tool(number)

Make tool number the current tool.

tool(1);  // Select tool 1

If no arguments are given, information about the currently selected tool is returned. E.g.:

{
   number: 1,
   shape: CONICAL,        // A tool type constant
   angle: Math.PI * 0.5,  // In radians, 90°
   length: 10,            // In current units
   diameter: 3.175,       // In current units
   snub_diameter: 1       // If shape == SNUBNOSE
}

For example, to get the current tool radius:

var tool_radius = tool().diameter / 2;

The tool type constants are as follows:

  • CYLINDRICAL
  • CONICAL
  • BALLNOSE
  • SPHEROID
  • SNUBNOSE
units(type)

type may be either METRIC which selects millimeters as the unit of measure or IMPERIAL which selects inches.

If no arguments are given the current type is returned.

units(METRIC);  // Select millimeters
pause(optional=false)

Pause the program until the user resumes. If optional is true then only pause if the stop switch is activated.

pause(true);
workpiece()

Get current simulation's workpiece offset and dimensions.

workpiece() = {
  dims: {x: 100, y: 100, z: 10},
  offset: {x: -50, y: -50, z: -10}
}

Matrix

pushMatrix(matrix)
popMatrix(matrix)
loadIdentity(matrix)
scale(x=1, y=1, z=1, matrix)
translate(x=0, y=0, z=0, matrix)
rotate(angle, x=0, y=0, z=1, matrix)
getXYZ()

Return an array of the current xyz position, i.e. [x, y, z].

getX()

Return the current x position.

getY()

Return the current y position.

getZ()

Return the current z position.

Math

JavaScript has built in math functions and constants which are members of the built-in Math object. The Math object is defined in the EMCAScript specification, section 15.8 or in a perhaps more readable format at W3 Schools, Math object reference.

var negOne = Math.cos(Math.PI);

Debugging

print(...)

Convert all arguments to strings and print them to the output stream.

Note that print() writes to the same output stream as the gcode functions so if you don't want to interfere with the gcode output you should wrap text in parentheses () so that it is treated as a gcode comment.

print() does not automatically add an end of line and does not interpret formatting strings.

print("Hello World!\n");
print("(A gcode comment)\n");
position()

Return a dict of the current positions, i.e. {x y z a b c u v w}.

print(position().x, position().y, position().z);
comment(...)

Convert all arguments to strings and output a GCode comment.

comment("A gcode comment");
message(...)

Convert all arguments to strings and output a GCode message.

In some contexts GCode messages are displayed to the user.

message("Hello World");
console.log(...)

Print to log info stream.

console.debug(...)

Print to log debug stream.

console.warn(...)

Print to log warning stream.

console.error(...)

Print to log error stream.

Module

Load code modules.

require(path)

Load the specified module. Modules are searched for in the directories specified in the TPL_PATH environment variable. A TPL module may be either a file ending in .tpl, .js or .json or a directory containing a package.json file.

.tpl or .js files are loaded as code whereas .json files are loaded as data. Code modules must assign values to the module.exports (alternatively exports) dictionary in order for them to be accessible. For example:

// A TPL module
module.exports = {
  data: [1, 2, 3],
 
  hello: function () {print("Hello World!\n");}
}

If the above code were in a file called hello.tpl, you might use the following to access it:

var m = require('hello.tpl');
 
m.hello();
 
for (var i = 0; i < m.data.length; i++)
  print(m.data[i], '\n');

package.json files are treated specially. The contents are interpreted as a dictionary of JSON data and the attribute main is read to get the relative path of the entry point for the module. The specified file is then loaded as a code or data module as above.

When a module is being loaded the following variables are defined:

module.name The module name.
module.id Same as module.name.
module.filename The full path to the module.
module.exports The exports dictionary. Used to export public data and functions. Can be assigned to a new value.
exports Same as module.exports but reassignment has no effect.
id Same as module.name.

If any modules have cyclic require() calls, module values may not be accessible at module load time. For example:

a.tpl:
var b = require('b.tpl');
print(b.data, '\n');
module.exports = {data: 'A'}
b.tpl:
var a = require('a.tpl');
print(a.data, '\n');
module.exports = {data: 'B'}
main.tpl:
var a = require('a.tpl');
var b = require('b.tpl');
print(a.data, '\n');
print(b.data, '\n');

Running main.tpl will produce the following output:

undefined
B
A
B

The first reference to a.data is undefined because module A is not yet fully loaded when accessed in module B. However, all data has been loaded by the time main.tpl accesses it.

2D CAM

Some basic CAM operations on 2D polygons.

offset(polys, delta, join, limit=1000, autoFix=true, scale=1000000)
polys A list of polygons. See format below.
delta The offset delta. Positive for outset. Negative for inset.
join Type of join. Can be JOIN_SQUARE, JOIN_ROUND or JOIN_MITER.
limit Default 1000. Miter limit for JOIN_MITER.
autoFix Default true. Automatically fix polygon orientation if necessary and remove duplicate vertices.
scale Default 10,000,000. Scale the polyon points to gain accuracy.

Use the Clipper library to compute polygon offsets.

The polys argument is an array of one or more polygons and is formatted either as an array of arrays of dictionaries:

[
  [{x: 0, y: 0}, {x: 1, y: 0}, . . ., {x: 0, y: 0}],
  . . .
]

or an array of arrays of 2D arrays:

[
  [[0, 0], [1, 0], . . ., [0, 0]],
  . . .
]

Note, the results of dxf.open() are not directly usable by offset() but other functions in the dxf module can convert raw DXF data to polygons.

DXF

DXF file format support.

Constants

POINT
LINE
ARC
POLYLINE
SPLINE
open(path)

Read a DXF file and return JSON data as a dictionary of layers. Each layer is a list of any of the following entities:

Points

{
  type: dxf.POINT,
  x: 0,
  y: 0,
  z: 0
}

Lines

{
  type: dxf.LINE,
  start: {x: 0, y: 0, z: 0},
  end: {x: 1, y: 1, z: 1}
}

Arcs

{
  type: dxf.ARC,
  center: {x: 0, y: 0, z: 0},
  radius: 1,
  startAngle: 0,
  endAngle: 2 * Math.PI,
  clockwise: true
}

Polylines

{
  type: dxf.POLYLINE,
  vertices: [
    {type: dxf.POINT, x: 0, y: 0, z: 0},
    . . .
  ]
}

Splines

{
  type: dxf.SPLINE,
  degree: 3,
  ctrlPts: [
    {type: dxf.POINT, x: 0, y: 0, z: 0},
    . . .
  ],
  knots: [0, . . .]
}

Example

var dxf = require('dxf');
var layers = dxf.open('test.dxf');
 
 

Bug Reports

See the GitHub issues page.

Links

OpenJSCAM a pure JS implementation of TPLang (in development).

Donations

See camotics.org/#donations.