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.

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

{{#regionTypes}}

{{class}}

{{#keyword}}
keyword:
{{keyword}}
{{/keyword}} {{#extends}}
extends:
{{extends}}
{{/extends}}
{{{description}}}
{{#required_properties.length}}

Required Properties

    {{#required_properties}}
  • {{>prop}}
  • {{/required_properties}}
{{/required_properties.length}} {{#properties.length}}

Properties

    {{#properties}}
  • {{>prop}}
  • {{/properties}}
{{/properties.length}} {{#inherited_properties.length}}

Inherited Properties

    {{#inherited_properties}}
  • {{>prop}}
  • {{/inherited_properties}}
{{/inherited_properties.length}}
{{/regionTypes}}