Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
kcl

KCL Modules

KCL allows splitting code up into multiple files. Each file is somewhat isolated from other files as a separate module.

When you define a function, you can use export before it to make it available to other modules.

// util.kcl
export fn increment(x) {
  return x + 1
}

Other files in the project can now import functions that have been exported. This makes them available to use in another file.

// main.kcl
import increment from "util.kcl"

answer = increment(41)

Imported files must be in the same project so that units are uniform across modules. This means that it must be in the same directory.

Import statements must be at the top-level of a file. It is not allowed to have an import statement inside a function or in the body of an if‑else.

Multiple functions can be exported in a file.

// util.kcl
export fn increment(x) {
  return x + 1
}

export fn decrement(x) {
  return x - 1
}

When importing, you can import multiple functions at once.

import increment, decrement from "util.kcl"

Imported symbols can be renamed for convenience or to avoid name collisions.

import increment as inc, decrement as dec from "util.kcl"

Functions vs clone

There are two common patterns for re‑using geometry:

  1. Wrap the construction in a function – flexible and fully parametric.
  2. Duplicate an existing object with clone – lightning‑fast, but an exact duplicate.

Parametric function example

fn cube(center) {
  return startSketchOn(XY)
    |> startProfileAt([center[0] - 10, center[1] - 10], %)
    |> line(endAbsolute = [center[0] + 10, center[1] - 10])
    |> line(endAbsolute = [center[0] + 10, center[1] + 10])
    |> line(endAbsolute = [center[0] - 10, center[1] + 10])
    |> close()
    |> extrude(length = 10)
}

myCube = cube([0, 0])

Pros

  • Any argument can be a parameter – size, position, appearance, etc.
  • Works great inside loops, arrays, or optimisation sweeps.

Cons

  • Every invocation rebuilds the entire feature tree.
  • Slower than a straight duplicate – each call is its own render job.

clone example

sketch001 = startSketchOn(-XZ)
  |> circle(center = [0, 0], radius = 10)
  |> extrude(length = 5) 
  |> appearance(color = "#ff0000", metalness = 90, roughness = 90)

sketch002 = clone(sketch001)  // ✓ instant copy

Pros

  • Roughly an O(1) operation – we just duplicate the underlying engine handle.
  • Perfect when you need ten identical bolts or two copies of the same imported STEP file.

Cons

  • Not parametric – the clone is exactly the same shape as the source.
  • If you need to tweak dimensions per‑instance, you’re back to a function.

Rule of thumb – Reach for clone when the geometry is already what you want. Reach for a function when you need customisation.


Module‑level parallelism

Under the hood, the Design Studio runs every module in parallel where it can. This means:

  • The top‑level code of foo.kcl, bar.kcl, and baz.kcl all start executing immediately and concurrently.
  • Imports that read foreign files (STEP/OBJ/…) overlap their I/O and background render.
  • CPU‑bound calculations in separate modules get their own worker threads.

Why modules beat one‑big‑file

If you shoe‑horn everything into main.kcl, each statement runs sequentially:

import "big.step" as gizmo  // blocks main while reading

gizmo |> translate(x=50)    // blocks again while waiting for render

Split gizmo into its own file and the read/render can overlap whatever else main.kcl is doing.

// gizmo.kcl                   (worker A)
import "big.step"

// main.kcl                    (worker B)
import "gizmo.kcl" as gizmo   // non‑blocking

// ... other setup ...

gizmo |> translate(x=50)      // only blocks here

Gotcha: defining but not calling functions

Defining a function inside a module is instantaneous – we just record the byte‑code. The heavy lifting happens when the function is called. So:

// util.kcl
export fn makeBolt(size) { /* … expensive CAD … */ }

If main.kcl waits until the very end to call makeBolt, none of that work was parallelised – you’ve pushed the cost back onto the serial tail of your script.

Better: call it early or move the invocation into another module.

// bolt_instance.kcl
import makeBolt from "util.kcl"
bolt = makeBolt(5)  // executed in parallel
bolt

Now main.kcl can import "bolt_instance.kcl" as bolt and get the result that was rendered while it was busy doing other things.


Whole module import

You can also import the whole module. This is useful if you want to use the result of a module as a variable, like a part.

import "tests/inputs/cube.kcl" as cube
cube
  |> translate(x=10)

This imports the whole module and makes it available as cube. You can then use it like any other object. The cube variable is now a reference to the result of the module. This means that if you change the module, the cube variable will change as well.

In cube.kcl, you cannot have multiple objects. It has to be a single part. If you have multiple objects, you will get an error. This is because the module is expected to return a single object that can be used as a variable.

You also cannot assign that object to a variable. This is because the module is expected to return a single object that can be used as a variable.

So for example, this is not allowed:

... a bunch of code to create cube and cube2 ...

myUnion = union([cube, cube2])

What you need to do instead is:

... a bunch of code to create cube and cube2 ...

union([cube, cube2])

That way the last line will return the union of the two objects.

Or what you could do instead is:

... a bunch of code to create cube and cube2 ...

myUnion = union([cube, cube2])
myUnion

This will return the union of the two objects, but it will not be assigned to a variable. This is because the module is expected to return a single object that can be used as a variable.


Multiple instances of the same import

Whether you are importing a file from another CAD system or a KCL file, that file represents object(s) in memory. If you import the same file multiple times, it will only be rendered once.

If you want to have multiple instances of the same object, you can use the clone function. This will render a new instance of the object in memory.

import cube from "tests/inputs/cube.kcl"

cube  
  |> translate(x=10)
clone(cube)
  |> translate(x=20)

In the sample above, the cube object is imported from a KCL file. The first instance is translated 10 units in the x direction. The second instance is cloned and translated 20 units in the x direction. The two instances are now separate objects in memory, and can be manipulated independently.

Here is an example with a file from another CAD system:

import "tests/inputs/cube.step" as cube

cube
  |> translate(x=10)
clone(cube)
  |> translate(x=20)

Importing files from other CAD systems

import can also be used to import files from other CAD systems. The format of the statement is the same as for KCL files. You can only import the whole file, not items from it. E.g.,

import "tests/inputs/cube.obj"

// Use `cube` just like a KCL object.
import "tests/inputs/cube.sldprt" as cube

// Use `cube` just like a KCL object.

For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by using an attribute. Likewise, you can also specify a coordinate system. E.g.,

@(unitLength = ft, coords = opengl)
import "tests/inputs/cube.obj"

When importing a GLTF file, the bin file will be imported as well.

Import paths are relative to the current project directory. Imports currently only work when using the native Design Studio, not in the browser.

Supported values

File formats: fbx, gltf/glb, obj+, ply+, sldprt, step/stp, stl+. (Those marked with a '+' support customising the length unit and coordinate system).

Length units: mm (the default), cm, m, inch, ft, yd.

Coordinate systems:

  • zoo (the default), forward: -Y, up: +Z, handedness: right
  • opengl, forward: +Z, up: +Y, handedness: right
  • vulkan, forward: +Z, up: -Y, handedness: left

Performance deep‑dive for foreign‑file imports

Parallelized foreign‑file imports now let you overlap file reads, initialization, and rendering. To maximize throughput, you need to understand the three distinct stages—reading, initializing (background render start), and invocation (blocking) —and structure your code to defer blocking operations until the end.

Foreign import execution stages

  1. Import (Read / Initialization) Stage

    import "tests/inputs/cube.step" as cube
    
    • Reads the file from disk and makes its API available.
    • Starts engine rendering but does not block your script.
    • This kick‑starts the render pipeline while you keep executing other code.
  2. Invocation (Blocking) Stage

    import "tests/inputs/cube.step" as cube
    
    cube
      |> translate(z=10) // ← blocks here only
    
    • Any method call (e.g., translate, scale, rotate) waits for the background render to finish before applying transformations.

Best practices

1. Defer blocking calls

import "tests/inputs/cube.step" as cube     // 1) Read / Background render starts


// --- perform other operations and calculations here ---


cube
  |> translate(z=10)                        // 2) Blocks only here

2. Split heavy work into separate modules

Place computationally expensive or IO‑heavy work into its own module so it can render in parallel while main.kcl continues.

Future improvements

Upcoming releases will auto‑analyse dependencies and only block when truly necessary. Until then, explicit deferral will give you the best performance.