Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Using APL for Gemstone Design

⚠️ Experimental: APL support is experimental and under active development. The engine, syntax, and available functions may change without notice. If you encounter issues, please report them β€” and remember, this is a playground for the mathematically adventurous.

ProFacet supports three scripting languages for designing gemstones: FSL (the purpose-built spec language), Lua (a general-purpose scripting language), and APL β€” a mathematical notation turned programming language.

Why APL?

APL is not for everyone. It's a notation designed by mathematicians, for mathematicians. Where FSL and Lua offer convenience functions like mp(), cp(), and TierAP(), APL gives you raw linear algebra primitives and expects you to build everything yourself.

If that sounds appealing, read on. If you prefer a guided experience, stick with FSL or Lua.

What APL brings:

  • Concise mathematical notation β€” A complete gemstone design in 4 lines
  • Array-oriented thinking β€” Planes, vertices, and transforms are all matrices
  • No magic β€” Every computation is explicit; you see exactly what the math does
  • Live playground β€” Run APL directly in the handbook with instant 3D preview

Getting Started

In the interactive playground (found throughout this handbook), click the APL tab. The playground wraps your code with an auto-injected gemstone engine that provides the core functions. Your code runs between the header (which defines the engine) and the footer (which exports the stone to the 3D viewer).

Tutorial: Building Cuts Step by Step

Step 1: A Single Horizontal Cut

The simplest possible cut β€” a single horizontal table facet:

'Table' cut (0 0 1) at 0.09

That's it. Three numbers:

  • 0 β€” angle: 0Β° (horizontal)
  • 0 β€” index: gear position 0 (doesn't matter for horizontal, but required)
  • 1 β€” direction: up (crown side)

at 0.09 sets the depth. This cuts one flat plane off the top of the rough stone.

Step 2: A Single Angled Cut

Now tilt that facet and aim it downward:

'P1' cut (37.5 0 Β―1) at Β―1.0
  • Angle 37.5Β° β€” the facet tilts inward
  • Index 0 β€” at gear position 0
  • Direction Β―1 β€” pavilion (down)

Notice Β―1 β€” that's APL's high minus (negative sign), distinct from the subtraction operator -. This creates a single angled facet on the pavilion side.

Step 3: Adding Symmetry

One facet is lonely. Let's repeat it 8 times around the stone:

'P1' cut (37.5 (0 sym 8) Β―1) at Β―1.0

The only change: scalar 0 β†’ 0 sym 8. The sym function generates 8 evenly-spaced gear indices around the stone. With Gear ← 96 (the default), 0 sym 8 produces indices 0 12 24 36 48 60 72 84.

Step 4: Adding Mirrors

For a brilliant cut, each facet has a mirror reflection:

'P1' cut (37.5 (mir 3 sym 8) Β―1) at Β―1.0

mir adds reflected indices: mir 3 sym 8 produces 16 indices (8 original + 8 mirrored). Index 3 instead of 0 ensures the mirrors don't overlap β€” 0 mirrors onto itself.

Step 5: Offset Indices

Not all facets start at index 0. For example, star facets on the crown sit between the main facets:

'P1' cut (37.5 (mir 0 sym 8) Β―1) at Β―1.0
'G1' cut (90 (mir 0 sym 8) Β―1) at 1.0
'C1' cut (25 (mir 3 sym 8) 1) at 0.5

Notice 3 sym 8 β€” the 3 offsets the indices so these crown facets sit rotated relative to the pavilion mains. The girdle (G1 at 90Β°) gives the stone its outline.

Step 6: Meetpoints

Instead of specifying a fixed depth, you can cut to where existing facets meet:

⍝ Round Brilliant β€” 4 lines, zero variables
'P1' cut (37.5 (mir 3 sym 8) Β―1) at Β―1.0
'G1' cut (90 (mir 3 sym 8) Β―1) at 1.0
'C1' cut (25 (mir 3 sym 8) 1) at (zOff05 V on tier 'P1')
'Table' cut (0 (0 sym 1) 1) at 0.09

The crown (C1) cuts to a meetpoint instead of a fixed depth:

  • V on tier 'P1' β€” find vertices that touch the pavilion tier
  • zOff05 β€” add a small girdle offset (0.05) to those vertices
  • at detects the vertex matrix and auto-finds the meetpoint

The tier function looks up a tier's planes by name β€” so V on tier 'P1' finds vertices touching the pavilion mains without needing to save them to a variable first.

Reading the Cut Syntax

Each line follows this pattern:

'NAME' cut (ANGLE (INDICES) DIRECTION) TARGET

Assignment is optional β€” only save the result if you need it later (e.g., for V on).

Breaking down a pavilion cut:

'P1' cut (37.5 (mir 3 sym 8) Β―1) at Β―1.0
⍝     β”‚     β”‚              β”‚    β”‚  β”‚
⍝     β”‚     β”‚              β”‚    β”‚  └─ depth = 1.0 (pavilion depth)
⍝     β”‚     β”‚              β”‚    └──── at = target (depth, point, or vertices)
⍝     β”‚     β”‚              └───────── Β―1 = pavilion direction (down)
⍝     β”‚     └──────────────────────── mir 3 sym 8 = 16 indices (8-fold + mirrors)
⍝     └────────────────────────────── 37.5Β° tilt angle

A crown cut with meetpoint:

'C1' cut (25 (mir 3 sym 8) 1) at (zOff05 V on tier 'P1')
⍝        β”‚    β”‚              β”‚  β”‚
⍝        β”‚    β”‚              β”‚  └── zOff05 offsets the vertices, at auto-finds meetpoint
⍝        β”‚    β”‚              └───── 1 = crown direction (up)
⍝        β”‚    └──────────────────── 16 mirrored indices
⍝        └──────────────────────── 25Β° crown angle

Settings

The engine provides three variables you can set at the top of your code:

Gear ← 96         ⍝ Number of gear teeth (default 96)
Name ← 'My Stone'  ⍝ Name of the design
RI ← 1.54         ⍝ Refractive index (default 1.54, quartz)

Gear controls the angular resolution of the indexing system. The standard is 96 teeth. All indices run from 0 to Gear-1, and functions like sym and mir use Gear for modular arithmetic.

Cutting Notes

Use note to attach instructions to any tier. Notes appear in the printable cutting diagram.

'P1' note 'Cut pavilion mains first'
'G1' note 'Polish to 600 grit'
'C1' note 'Meet P1 and G1 at girdle'

Notes are separate from cut β€” add them anywhere after the tier has been created. Each tier can have one note; if you call note again on the same tier, both entries are included.

Tips for APL Users

  • βŽ•IO ← 0 is set automatically β€” all indexing is zero-based
  • The gear has Gear teeth (default 96) β€” indices run from 0 to Gear-1
  • Direction is Β―1 for pavilion, 1 for crown
  • V is a global variable holding all current vertices
  • Stone is a global NΓ—4 matrix of all planes
  • Tier names are the left argument β€” 'P1' cut ... names the tier directly; use tier 'P1' to reference it later

How It Works

Unlike FSL and Lua, the APL engine does not call into ProFacet's Rust/WASM engine. Instead, it implements the entire gemstone computation in pure APL β€” half-space intersection, vertex solving via Cramer's rule, and meetpoint projection. This means:

  1. No built-in geometry helpers β€” There is no mp() or cp() magic function. The engine provides lower-level primitives that you compose.
  2. Everything is a matrix β€” A stone is an NΓ—4 matrix where each row is [nx ny nz d] (a half-space plane). Vertices are an MΓ—3 matrix.
  3. You build the cuts β€” The cut function adds planes to the stone and recomputes vertices. You control the angle, symmetry, and depth directly.

The APL Gemstone Engine

The following code is automatically injected before your APL code runs. It's shown here in full so you can understand every function available to you.

Index Generation

⍝ Generate gear indices with symmetry
⍝ Usage: base_index sym fold_count
⍝ Example: 3 sym 8 β†’ 3 15 27 39 51 63 75 87
sym ← { Gear | ⍺ + (Gear÷⍡) Γ— ⍳⍡ }

⍝ Mirror indices (adds reflections)
⍝ Usage: mir base_indices
⍝ Example: mir 3 sym 8 β†’ 3 15 27 39 51 63 75 87 93 81 69 57 45 33 21 9
mir ← { βˆͺ Gear | ⍡ , Gear - ⍡ }

sym generates evenly-spaced indices around the gear. mir adds mirror reflections. Together, mir 3 sym 8 produces 16 indices for an 8-fold symmetric cut with mirrors.

Normal Generation

⍝ NGen: Generate 3D normal vectors from (angle, indices, direction)
⍝ Returns an NΓ—3 matrix of unit normals
βˆ‡ R ← NGen args;t;I;d;A;T
  (t I d) ← args
  A ← β—‹ 2 Γ— I Γ· Gear        ⍝ Gear index β†’ radians
  T ← β—‹ t Γ· 180              ⍝ Tilt angle β†’ radians
  R ← ⍉ (3 , β‰’I) ⍴ ((1β—‹T)Γ—2β—‹A) , ((1β—‹T)Γ—1β—‹A) , ((β‰’I)⍴dΓ—2β—‹T)
βˆ‡

NGen converts (angle, gear_indices, direction) into 3D unit normals. The direction d is Β―1 for pavilion (pointing down) or 1 for crown (pointing up).

Vertex Computation (Cramer's Rule)

⍝ 3Γ—3 determinant for batched matrices
det3v ← { +/ ⍡[;0;] Γ— (⍡[;1;1 2 0]×⍡[;2;2 0 1]) - ⍡[;1;2 0 1]×⍡[;2;1 2 0] }

⍝ Generate all 3-element combinations from 0..n-1
βˆ‡ R ← comb3 n;all
  all ← ⍉ (n n n) ⊀ ⍳ n*3
  R ← all ⌿⍨ (all[;0] < all[;1]) ∧ all[;1] < all[;2]
βˆ‡

⍝ Solve for all vertices of a stone (intersection of 3 half-spaces)
βˆ‡ R ← getVertices S; ...
  ⍝ For every combination of 3 planes, solve the 3Γ—3 linear system
  ⍝ using Cramer's rule. Keep only vertices that satisfy ALL planes.
  ...
βˆ‡

getVertices finds every point where exactly 3 planes intersect, then filters to keep only those inside the stone (satisfying all half-space constraints). This is the core of the slicer.

Meetpoint and Cut Functions

⍝ firstContact: find the vertex closest to a plane normal
firstContact ← { ⍡[ (0βŠƒβ’ ⍡ +.Γ— ⍺) ; ] }

⍝ V on T: filter vertices that lie on tier T
βˆ‡ R ← V on T
  ⍝ Returns vertices from V that touch the planes in tier T
βˆ‡

⍝ args mp V: compute meetpoint (closest vertex to the cutting direction)
βˆ‡ R ← args mp V
  R ← (NGen args)[0;] firstContact V
βˆ‡

⍝ args at target: compute plane distances from cut parameters
⍝ Handles three target types:
⍝   scalar  β†’ depth (e.g., at 1.0)
⍝   3-vec   β†’ meetpoint (e.g., at pt)
⍝   matrix  β†’ vertices (auto-finds meetpoint)
⍝   2-vec   β†’ angle solve between two points
βˆ‡ R ← args at rarg
  ⍝ Returns NΓ—4 matrix: [nx ny nz d] for each cutting plane
βˆ‡

⍝ CM operator: "Cut to Meetpoint" with transform
⍝ Usage: args (transform CM) V
βˆ‡ R ← args (LO CM) V;pt
  pt ← LO args mp V    ⍝ Find meetpoint, apply transform (e.g., zOff05)
  R ← args at pt        ⍝ Compute plane distances from that point
βˆ‡

The Cut Function

⍝ cut: Add named planes to the stone and recompute vertices
⍝ Left arg is the tier name, right arg is the plane matrix
βˆ‡ tier ← nm cut planes
  TierCount ← TierCount + 1
  TierNames ← TierNames , βŠ‚nm
  TierMap   ← TierMap , (β‰’planes) ⍴ TierCount
  Stone ← Stone βͺ planes               ⍝ Append new planes
  ⍝ Keep existing vertices that satisfy new constraints
  kept ← V ⌿⍨ ∧/ 1eΒ―5 β‰₯ (V +.Γ— ⍉ planes[;0 1 2]) -⍀1 planes[;3]
  ⍝ Solve for new vertices involving the new planes
  newV ← (old_count) solveNew Stone
  V ← urows (kept βͺ newV)              ⍝ Merge and deduplicate
  tier ← planes
βˆ‡

⍝ tier: look up a tier's planes by name
βˆ‡ R ← tier nm
  R ← (TierMap = TierNames ⍳ βŠ‚nm) ⌿ Stone
βˆ‡

cut is the main workhorse. The left argument names the tier (e.g., 'P1'), and the right argument is the NΓ—4 plane matrix. It appends half-space planes to Stone, filters existing vertices, solves for new intersections, and updates the global vertex set V.

tier looks up a previously cut tier by name β€” e.g., tier 'P1' returns the plane matrix for tier P1. This lets you reference tiers without storing them in variables: V on tier 'P1'.

Cutting Notes

⍝ note: attach a cutting instruction to a tier
⍝   'P1' note 'Cut pavilion mains first'
βˆ‡ nm note txt
  ⍝ Appends to TierNoteNames and TierNotes vectors
βˆ‡

note is separate from cut β€” call it any time after the tier exists. Notes are exported in the JSON output and appear in ProFacet's printable cutting diagram.

Utility Functions

⍝ zOff05: Add a 0.05 girdle offset to a meetpoint
βˆ‡ R ← zOff05 X
  R ← X + 0 0 0.05
βˆ‡

⍝ id: Identity (pass-through, used as default transform)
βˆ‡ R ← id X
  R ← X
βˆ‡

Initial State

⍝ The rough: a 2Γ—2Γ—2 cube defined by 6 half-spaces
Rough ← 6 4 ⍴ 0 0 1 2  0 0 Β―1 2  1 0 0 2  Β―1 0 0 2  0 1 0 2  0 Β―1 0 2
Stone ← Rough
V ← getVertices Stone
TierCount ← 0
TierMap ← 6 ⍴ 0
TierNames ← βŠ‚'Rough'
CutLog ← 0 3 ⍴ 0

Your code starts with a cube (Rough) and carves it down with cut calls.

CAM Outline Functions

The APL engine includes all ProFacet CAM (Centerpoint–Angle Method) outline functions. These generate girdle preforms with a single function call, just like FSL and Lua. All functions are prefixed with apl to avoid name collisions. Internally, they call cut directly.

Usage

⍝ Simple shapes β€” single argument
aplRound (43 8)        ⍝ Round with angle 43°, 8-fold symmetry
aplHexagon 43          ⍝ Hexagonal outline
aplOctagon 43          ⍝ Octagonal outline
aplShield 44           ⍝ Shield/kite shape

⍝ Truncated shapes β€” (angle truncation) or (truncation angle)
aplSquareTr (45 0.35)       ⍝ Truncated square
aplHexTr (42 0.3)           ⍝ Truncated hexagon
aplTriTr (0.35 44)          ⍝ Truncated triangle

⍝ Mirrored variants β€” add offset parameter
aplSquareTrMir (42 0.5 4)   ⍝ Mirrored truncated square
aplHexTrMir (42 0.3 2)      ⍝ Mirrored truncated hexagon
aplTriTrMir (0.25 42 4)     ⍝ Mirrored truncated triangle

⍝ Cushioned shapes
aplSquareCushTr (2 45 0.40)      ⍝ Cushioned truncated square
aplTriCushTr (0.30 44 2)         ⍝ Cushioned truncated triangle
aplPentCushTr (1 33 0.3)         ⍝ Cushioned truncated pentagon

⍝ Curved shapes
aplTriCurved (45 2 4)       ⍝ Curved trilliant
aplSquareCurved (45 2 4)    ⍝ Curved square

⍝ Rectangles
aplRect (1.6 45)                  ⍝ Rectangle (lwr angle)
aplRectTr (1.6 45 0.30 5)        ⍝ Truncated rectangle
aplRectDblTr (1.6 45 0.60 0.50)  ⍝ Double-truncated rectangle

⍝ Oval
aplOval (42 1.35 6)    ⍝ Oval (angle lwr segmentsPerQuad)

Full Reference

APL FunctionFSL/Lua EquivalentParameters
aplRoundRound(angle symmetry)
aplHexagonHexagonangle
aplOctagonOctagonangle
aplPentagonPentagonangle
aplHeptagonHeptagonangle
aplShieldShieldangle
aplHexTrHexTr(angle truncation)
aplHexTrMirHexTrMir(angle truncation offset)
aplSquareTrSquareTr(angle truncation)
aplSquareTrMirSquareTrMir(angle truncation offset)
aplTriTrTriTr(truncation angle)
aplTriTrMirTriTrMir(truncation angle offset)
aplRectRect(lwr angle)
aplRectTrRectTr(lwr angle truncation offset)
aplRectDblTrRectDblTr(lwr angle t1 t2)
aplSquareCushTrSquareCushTr(cushion angle truncation)
aplSquareCushTrMirSquareCushTrMir(cushion angle truncation offset)
aplTriCushTrTriCushTr(truncation angle cushion)
aplTriCushTrMirTriCushTrMir(truncation angle cushion offset)
aplTriCurvedTriCurved(angle c1 c2)
aplSquareCurvedSquareCurved(angle c1 c2)
aplPentCushTrPentCushTr(offset angle truncation)
aplPentTrPentTr(angle truncation)
aplPentTrMirPentTrMir(truncation angle offset)
aplPentCurvedPentCurved(angle c1 c2)
aplPentCushTrMirPentCushTrMir(offset angle truncation mir_offset)
aplHexCurvedHexCurved(angle c1 c2)
aplHexCushTrHexCushTr(offset angle truncation)
aplHexCushTrMirHexCushTrMir(offset angle truncation mir_offset)
aplHeptTrHeptTr(angle truncation)
aplHeptTrMirHeptTrMir(truncation angle offset)
aplHeptCurvedHeptCurved(angle c1 c2)
aplHeptCushTrHeptCushTr(offset angle truncation)
aplHeptCushTrMirHeptCushTrMir(offset angle truncation mir_offset)
aplOctTrOctTr(angle truncation)
aplOctTrMirOctTrMir(angle truncation offset)
aplOctCurvedOctCurved(angle c1 c2)
aplOctCushTrOctCushTr(offset angle truncation)
aplOctCushTrMirOctCushTrMir(offset angle truncation mir_offset)
aplOvalOval(angle lwr segmentsPerQuad)

Helper Functions

The CAM functions use these lower-level helpers which are also available for custom designs:

FunctionDescription
tierLook up tier planes by name: tier 'P1' β€” for use in V on tier 'P1'
edge(βŠ‚tier 'PF1') edge (βŠ‚tier 'G1') β€” find shared edge vertices
epe ep 0.5 β€” interpolate along edge at ratio

Comparison: FSL vs Lua vs APL

FeatureFSLLuaAPL
Syntax styleDeclarative specImperative scriptMathematical notation
Learning curveLowMediumHigh
Built-in geometryFullFullFull (via apl* functions) + raw primitives
Control flowFunctional onlyFull (loops, if/else)Dfns, guards, tradfns
Target audienceDesignersProgrammersMathematicians
EngineRust/WASMRust/WASMPure APL

Further Reading