CombScript
CombScript is a language for describing technical vector designs and a tool that exports these designs as SVG files. A primary goal of CombScript is to express designs naturally, so they are easier to adjust and customize.
CombScript was inspired by OpenSCAD, CSS, HTML, and Adobe Illustrator snapping shenanigans.
CombScript is an open source project under the MIT license, source code is available on Github.
Parametric
CombScript documents can use relative positioning and simple math to express flexible designs that can can be easily customized by tweaking parameters.
Expressive
Positions can be described in multiple ways allowing more natural expression. For example you can specify that a circle is 10 units from the right side of its container and vertically centered, rather than expressing its global coordinates.
Declarative
CombScript is more like HTML than JavaScript. A CombScript document describes a design, not the steps required to make it.
Unstyled
Shapes in CombScript represent only the path data and don't have their own style properties like fill-color, stroke-color, or stroke-width. CombScript is primarily designed for designs that describe paths for laser-cutters and plotters where such styles are not needed.
Boolean Operations
Complex shapes are created by combining simple shapes using boolean operations like intersection, union, and difference.
Export to SVG
Shape data can be exported as
.svg
files.
Basic Syntax
CombScript syntax is based on YAML, a common and human-readable data-file format. CombScript builds on YAML with predefined keywords to describe designs and support for mathematic expressions.
Here is an example of a simple CombScript document that draws an ellipse:
# basic CombScript example, draws a circle properties: name: hello_world children: - ellipse: properties: width: 200px height: 200px
This example begins by defining an optional document property, name
, which is used when exporting. Document properties are defined using the properties:
key followed by a set of indented key: value pairs.
Next, the example defines its children under the children:
key. The children are indented one level and each is prefixed with a -
to indicate a YAML array item. There are several types of children in CombScript including regions, rectangles, and ellipses.
This document has one child, an ellipse
. The ellipse has width
and height
properties which set its dimensions.
The very first line of the document is a comment. Comments start with #
and are ignored by CombScript. Use comments to leave helpful notes for human readers.
Regions
The basic building-blocks in CombScript are called regions. A region describes a rectangular bounds in the drawing and can contain child regions. Child regions are laid out in relation to the bounds of their parent.
There are several types of regions in CombScript. The basic region
type is primarily organizational. It is used to define a new part of the drawing and to group its children. The basic region
will not generate a vector shape in the exported SVG.
The rectangle
or ellipse
region types can do everything that a plain region
can do, and also draw a shape in the export.
Region Bounds
Every region has a set of bounds that describe a rectangular area in the drawing. These bounds can be set using the top
, left
, bottom
, and right
properties. These values are relative to the registration position of the region's parent.
A region's bounds can also be specified relative to the parent's bounds using margin_top
, margin_left
, margin_bottom
, and margin_right
properties.
You can specify the dimensions of a region's bounds with the width
and height
properties.
These properties can be mixed any way you wish, as long as they don't conflict.
children: - rectangle: properties: # height determined from top and bottom # horizontally centered by default top: -100px bottom: 100px width: 100px - rectangle: properties: # height and width explict # position constrained to right side margin_right: 10px width: 100px height: 100px
Children
Regions can have any number of child regions. A region's bounds, registration, and transform are used by when positioning its children. Using nesting, you can describe the positional relationships of your shapes making parts of your design parametric.
In the following example the position of the two ellipses is determined by the width of their parent.
children: - region: properties: width: 400px height: 200px children: - ellipse: properties: height: 100px width: 100px margin_left: 30px - ellipse: properties: height: 100px width: 100px margin_right: 30px
Registration
Regions can optionally set their registration position with the registration
property. The new registration position will be used by the region's children when laying out their bounds. It will also be used as the pivot point for rotations and scales applied to the region.
The default value of registration
is parent
which will keep the registration position set by the region's parent. Other values, such as 'top_left' and 'center' move it to a position determined by the region's bounds.
In this example, the outer region does not have a set registration
property and the rectangle is positioned relative to the document's registration position (the center).
children: - region: properties: width: 200px height: 200px children: - rectangle: properties: top: 0px left: 0px width: 75px height: 75px
In the following two examples the outer regions have set registration
values explicitly, and the rectangle is positioned accordingly.
children: - region: properties: width: 200px height: 200px registration: top_right children: - rectangle: properties: top: 0px left: 0px width: 75px height: 75px
children: - region: properties: width: 200px height: 200px registration: top_left children: - rectangle: properties: top: 0px left: 0px width: 75px height: 75px
Transformations
You can transform the coordinate system used by a region and its children using the rotation
, scale_x
, and scale_y
properties. A region's transforms are inherited by its children.
children: - rectangle: properties: width: 100px height: 100px rotation: 25deg scale_x: 3 children: - rectangle: properties: width: 50px height: 50px
Property Types
Different region properties have different required types. These types include dimensions, angles, numbers, and strings.
Dimensions
Dimensions are the most common property type. When specifying the value for a dimension property, you must include a linear unit such as inches
, millimeters
, or pixels
. Units can also be given with their common abbreviation: in
, mm
, px
. You don't have to use the same unit for every property, choose the unit that makes the most sense.
margin_top: 1cm width: .25in height: .25in
Angles
Angle property values must include an angular unit such as degrees
or radians
.
rotation: 25deg
Numbers
Some properties require a simple number value, and should not include a unit label.
scale_x: 2.5 sides: 4 rows: 2
Strings
Some properties require a string (text) value. Placing quotes around strings is generally optional, but you may need them if your text is a number or includes special characters.
registration: top_left boolean: add name: table_leg name: "10" name: "table/front"
Using Math
You can use mathematic expressions in dimension
, angle
, and number
property values. Combscript uses the mathjs library for evaluating expressions. Check out the mathjs expression syntax documentation for details.
Expressions
You can use expressions like 4 * .7
and (1 + 2) * 3in
. You can mix dimensions like 1in + 2mm
.
The result of the expression must have a legal unit for the property. You can't set a width
to 2in * 2in
because the result is 4 square inches, which is not a legal linear dimension.
children: - ellipse: properties: width: 50px + 1in height: 200px / 2
Functions
You can use common math functions like sin
, cos
, max
, and round
in expressions.
children: - rectangle: properties: width: 180px height: 180px - rectangle: properties: width: 30px height: 180px / sin(45deg) rotation: 45deg
Accessing Parent Bounds
Regions can reference their parent's bounds using special variables in expressions. CombScript provides the following variables: parent_height
, parent_width
, parent_left
, parent_right
, parent_top
, parent_bottom
.
children: - rectangle: properties: width: 150px height: 75px children: - rectangle: properties: width: 50px height: parent_width left: parent_right + 10px
User Constants
Constants are user defined values that can be used as property values and referenced in expressions.
constants: my_size: 100px children: - rectangle: properties: width: my_size height: my_size*2
In the previous example, the my_size
constant was declared on the document. If you declare a constant on a region, it will be available only in that region and its children.
constants: rect_size: 100px children: - rectangle: constants: extra_size: 50px properties: margin_left: 50px width: rect_size height: rect_size + extra_size - rectangle: properties: # this rectangle's properties cannot # reference extra_size margin_right: 50px width: rect_size height: rect_size
Booleans & Combining Shapes
Shapes can be combined using the boolean
property. The possible values are add
, subtract
, and intersect
.
The following examples show how each boolean operation behaves.
Add
children: - ellipse: properties: width: 60mm height: 60mm left: -15mm - ellipse: properties: width: 60mm height: 60mm right: 15mm boolean: add
Subtract
children: - ellipse: properties: width: 60mm height: 60mm left: -15mm - ellipse: properties: width: 60mm height: 60mm right: 15mm boolean: subtract
Intersect
children: - ellipse: properties: width: 60mm height: 60mm left: -15mm - ellipse: properties: width: 60mm height: 60mm right: 15mm boolean: intersect
Multiple Operations
It's possible to perform multiple boolean operations on the same target shape. Boolean operations are applied in order. In this example the large ellipse is subtracted from the rectangle, then the smaller ellipse is added.
children: - rectangle: properties: width: 150px height: 150px - ellipse: properties: width: 120px height: 120px right: -15px boolean: subtract - ellipse: properties: width: 80px height: 80px right: 0px boolean: add
The Boolean Target
When a region has a defined boolean:
property, its shape is added to, subtracted from, or interesected with the shape of another region, the target. The target is usually the region's parent. If the parent does not generate a shape, then the parent's first child becomes the target.
children: # rectangle regions create a shape # so they become the target of their children's # boolean operations - rectangle: properties: width: 5cm height: 5cm children: - rectangle: properties: width: 1cm height: 2cm margin_left: 0cm boolean: subtract
children: # plain regions don't create a shape... - region: properties: width: 5cm height: 5cm children: # ... so the first child becomes the target - rectangle: properties: margin_left: 5mm - rectangle: properties: width: 1cm height: 2cm margin_left: 0cm boolean: subtract
Boolean Pass
If a region sets boolean_pass: true
, then the boolean operations of its children will target its parent.
children: - ellipse: properties: width: 60mm height: 60mm children: - region: properties: width: 60mm height: 20mm margin_bottom: 0mm boolean_pass: true registration: center children: - ellipse: properties: width: 20mm height: 20mm margin_left: 0mm boolean: subtract - ellipse: properties: width: 20mm height: 20mm margin_right: 0mm boolean: add
Many to Many
When regions with children are combined by a boolean operation, the children are drawn first, and then the boolean operation is applied to each of the resulting shapes in turn.
For example, you can subtract many shapes from a single shape, subtract a single shape from many shapes, or even subtract many shapes from many shapes.
children: - region: children: - ellipse: properties: width: 30mm height: 30mm left: 3mm - ellipse: properties: width: 30mm height: 30mm right: -3mm - region: properties: # subtract all of this region's children # from each of the previous region's children boolean: subtract children: - ellipse: properties: width: 30mm height: 30mm top: 3mm - ellipse: properties: width: 30mm height: 30mm bottom: -3mm
Trapping
Boolean operations sometimes produce bad results due to a bug in how overlapping edges are handled. The trapping:
property helps to work around this bug by making it easy to slightly adjust the bounds of one of the rectangles to ensure they overlap enough for the boolean operation to work correctly.
Postivie trapping values expand the region bounds in all directions, negative values contract the bounds.
Be careful when using trapping as it does effect the dimensions of the shape output. Trapping values should be tiny (e.g. .01mm), so their effect on the output will be minimal.
children: - region: properties: margin_top: 10mm height: 20mm children: # these rectangles are adjacent but adding them # doesn't always work quite right - rectangle: properties: width: 60mm left: 0mm - rectangle: properties: width: 60mm right: 0mm boolean: add - region: properties: margin_bottom: 10mm height: 20mm children: - rectangle: properties: width: 60mm left: 0mm - rectangle: properties: width: 60mm right: 0mm boolean: add # use trapping to make this rectangle # slightly larger (and overlap slightly) # so the boolean add works as expected trapping: .01mm
Using Region Grids
CombScript provides a powerful region-type called a region_grid
. Region grids can be used to create patterns and to help with layout.
The rows
and columns
properties will set the number of rows and columns in the grid directly. You can set row_height
and column_width
instead to create as many rows or columns as needed to fill the region grid.
By default, each cell in the generated grid will contain clones of the specified children of the region grid.
children: - region_grid: properties: width: 450px height: 200px column_width: 50px rows: 4 registration: center children: - ellipse: properties: width: 30px height: 30px - rectangle: properties: width: 30px height: 30px
Set populate: alterante
on the region grid to distribute the children in an alternating fashion.
children: - region_grid: properties: width: 450px height: 200px column_width: 50px rows: 4 registration: center populate: alternate children: - ellipse: properties: width: 30px height: 30px - rectangle: properties: width: 30px height: 30px
YAML Repeated Nodes
YAML's repeated node feature can be used to to create named parameters and library shapes that can be used throughout your document. Repeated nodes are defined with &
followed by a name, and used with *
followed by the name. Repeated nodes are can be defined pretty much anywhere throughout the document, but it's a good practice to define them as an array under the library:
key.
It is common that you will reuse the same shape in multiple places in your document. Repeated nodes can represent entire regions, their properties, and even nested children. Each repeated node will be separately laid out in their parent's context.
library: - &ex rectangle: properties: margin_top: 10px margin_bottom: 10px width: 40px radius: 4px children: - region: properties: margin_left: 10px width: 150px height: 150px registration: center children: - *ex - region: properties: margin_right: 10px width: 100px height: 100px registration: center children: - *ex
Exporting
CombScript documents can be exported as .svg
files for use with other software.
You can specify names, groups, and tools to control how the shape data will appear in the exported .svg
.
Names
The name
property can be used to specify a name for a region.
This name is displayed in the UI breadcrumbs, and will be assigned to the shape when exported.
If the document properties includes a name, it will be used for the document filename.
Groups
Setting the group
property to true
on a region will cause the region's shapes to be grouped in the export. Groups are skipped in boolean operations.
Tools
Some laser cutters and other tools use stroke color to identify how a shape should be handled.
Setting the tool
property controls the stroke color for a region in the export.
Possible values are cut
(red), etch
(green), and guide
(blue). Tool colors cannot be customized at this time.
Credits
Authors
CombScript was created by Justin Bakse and Greg Schomburg, with contributions by Sara Page.
Libraries
CombScript builds on dozens of opensource web projects, big and small. Including: Paper.js, Math.js, Ace, JS-Yaml, jQuery, and Underscore
License
CombScript is distributed under the MIT License. Source is available at https://github.com/jbakse/comb_script.
Language Reference
{{class}}
-
{{#keyword}}
- keyword:
- {{keyword}} {{/keyword}} {{#extends}}
- extends:
- {{extends}} {{/extends}}
Required Properties
-
{{#required_properties}}
- {{>prop}} {{/required_properties}}
Properties
-
{{#properties}}
- {{>prop}} {{/properties}}
Inherited Properties
-
{{#inherited_properties}}
- {{>prop}} {{/inherited_properties}}
- type:
- {{type}}
- default:
- {{default}}{{^default}}undefined{{/default}} {{#required}}
- required:
- {{required}} {{/required}} {{#values.length}}
- values: {{#values}}
- {{.}} {{/values}} {{/values.length}}