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

Foundations

Welcome to Convex-λ, a functional geometry language designed for precision gemstone faceting. It combines declarative geometry definitions with functional programming concepts.

This chapter covers the fundamental building blocks of the language: syntax, comments, types, and variables.

Comments

Code should be documented. Convex-λ supports both line and block comments.

// This is a single line comment
// It ends at the end of the line

/*
   This is a block comment.
   It can span multiple lines.
   Useful for describing complex logic.
*/

x = 10 // Comments can also appear after code

Types

Convex-λ has a simple but powerful type system.

Numbers

All numbers are double-precision floating point. Integers are treated as floats.

val_a = 42
val_b = 3.14159
val_c = -0.001

Strings

Text is enclosed in double quotes.

name = "Brilliant Cut"
note = "Set girdle width"
assert(name == "Brilliant Cut", "String equality")

Booleans

True or false logic values.

is_visible = true
has_finished = false
assert(is_visible, "Boolean check")

Arrays

Ordered lists of values. Arrays can hold mixed types, but typically hold homogenous data (like a list of Points).

// Array of numbers
fib = [1, 1, 2, 3, 5, 8]
assert(fib[5] == 8, "Array access")

// Array of mixed types (valid but rare)
mixed = [1, "two", true]

Structs

Key-value pairs for grouping data. Keys are identifiers.

config = {
    speed: 100,
    mode: "fast"
}
assert(config.speed == 100, "Struct access")

Variables & Assignments

There are four ways to handle values in Convex-λ, each with a specific purpose.

1. Internal Assignment =

Used for intermediate calculations. These values are stored in memory but not sent to the 3D viewer or output log.

width = 10
height = 20
// Calculated but not shown
area = width * height
assert(area == 200, "Area calc")

2. Render Assignment :=

This is the primary way to output data. It assigns a value to a variable AND renders it.

  • If the value is Geometry (Plane, Point, Line), it is added to the "Stone" (the 3D model).
  • If the value is Data (Number, String), it is printed to the Output Log.
// This calculates a value AND prints it to the Output
total_area := 10 * 20

// This creates a plane AND adds it to the 3D model
// We will cover geometry constructors later.
p1 := Plane(Vector(0,0,1), 5)

3. Debug Assignment ?=

Used for "Ghost" rendering. It shows the geometry in the viewer (usually as a wireframe) without adding it to the official intersection list (the Stone). This is useful for visualizing construction lines or helper planes.

// Show a guide plane (ghost) but don't cut the stone
guide_plane ?= Plane(Vector(1,0,0), 0)

4. The Underscore Pattern _

Often you want to inspect a value without creating a named variable. We use the underscore _ combined with := to log values directly to the output.

// Log a string
_ := "Starting calculation..."

// Inspect a calculation immediately
_ := 10 + 20 * 4

Configuration

Global configuration statements set up the environment. These are usually placed at the top of your file.

Gear(96)        // Set index gear (default: 96)
RI(1.54)        // Set Refractive Index (default: 1.54)
Mode("deg")     // Set angle mode (default: "deg")
// Mode("index") is automatically set when Gear() is called.

Basic Arithmetic

Standard arithmetic operators apply to numbers.

a = 10 + 5   // 15
assert(a == 15, "+")

b = 10 - 5   // 5
assert(b == 5, "-")

c = 10 * 5   // 50
assert(c == 50, "*")

d = 10 / 5   // 2
assert(d == 2, "/")

e = 10 % 3   // 1 (Modulo)
assert(e == 1, "%")

f = 2 ^ 3    // 8 (Power)
assert(f == 8, "^")

The standard library provides common math functions.

import "std"

root = sqrt(16)   // 4
assert(root == 4, "sqrt")

val  = abs(-10)   // 10
assert(val == 10, "abs")

s    = sin(PI/2)  // 1
assert(abs(s - 1) < 0.0001, "sin")

c    = cos(0)     // 1
assert(c == 1, "cos")

In the next chapter, we will explore the powerful operator system that allows these math operations to work on Vectors, Points, and Arrays.

Operators

Convex-λ is built on a rich operator system that often behaves differently depending on the types of the operands (Polymorphism). This allows for concise and expressive geometric logic.

This chapter details every operator with extensive examples.

Mathematical Operators

Addition +

Used for arithmetic addition, vector translation, and array concatenation.

1. Number + Number Standard addition.

sum = 10 + 5  // 15
assert(sum == 15, "Add Num")

2. Vector + Vector Component-wise addition.

v1 = Vector(1, 0, 0)
v2 = Vector(0, 1, 0)
v3 = v1 + v2  // Vector(1, 1, 0)
assert(v3 == Vector(1, 1, 0), "Add Vec")

3. Point + Vector Translates a point by a vector.

p = Point(0, 0, 0)
v = Vector(0, 0, 5)
p_new = p + v  // Point(0, 0, 5)
assert(p_new == Point(0, 0, 5), "Point Translate")

4. Array + Array Concatenates two arrays.

arr1 = [1, 2]
arr2 = [3, 4]
arr3 = arr1 + arr2  // [1, 2, 3, 4]
assert(arr3.length == 4, "Array Concat")

5. Array + Scalar (Broadcast) Adds the scalar to every element of the array.

nums = [10, 20, 30]
result = nums + 5  // [15, 25, 35]
assert(result[0] == 15, "Array Broadcast +")

Subtraction -

Used for arithmetic subtraction, vector difference, and unary negation.

1. Number - Number

diff = 10 - 3  // 7
assert(diff == 7, "Sub Num")

2. Vector - Vector

v1 = Vector(1, 1, 1)
v2 = Vector(0, 0, 1)
v3 = v1 - v2  // Vector(1, 1, 0)
assert(v3 == Vector(1, 1, 0), "Sub Vec")

3. Unary Negation Negates numbers or vectors.

n = -10
v = -Vector(1, 0, 0)  // Vector(-1, 0, 0)
assert(v.x == -1, "Negate Vec")

4. Array Subtraction (Broadcast)

arr = [10, 20, 30]
res = arr - 5  // [5, 15, 25]
assert(res[0] == 5, "Array Broadcast -")

Multiplication *

Used for scaling, dot products, symmetry composition, and intersection.

1. Number * Number

prod = 6 * 7  // 42
assert(prod == 42, "Mul Num")

2. Vector * Number Scales the vector.

v = Vector(1, 0, 0)
scaled = v * 5  // Vector(5, 0, 0)
assert(scaled.x == 5, "Scale Vec")

3. Plane * Plane (Intersection) Returns the Line where two planes intersect.

pl1 = Plane(Vector(1,0,0), 0) // YZ plane
pl2 = Plane(Vector(0,1,0), 0) // XZ plane
line = pl1 * pl2  // Line along Z axis
assert(line.dir.z == 1 || line.dir.z == -1, "Plane Intersect")

4. Line * Plane (Intersection) Returns the Point where a line hits a plane.

line = Line(Point(0,0,10), Vector(0,0,-1))
plane = Plane(Vector(0,0,1), 0)
hit = line * plane  // Point(0,0,0)
assert(hit == Point(0,0,0), "Line Plane Hit")

5. Symmetry Group Composition Combines two symmetry operations.

// Rotate * Mirror composition
g = Rotate(Vector(0,0,1), 4) // * Mirror(...)

Division /

Used for arithmetic division and scaling.

1. Number / Number

quo = 10 / 2  // 5
assert(quo == 5, "Div Num")

2. Vector / Number Scales the vector by 1/N.

v = Vector(10, 0, 0)
scaled = v / 2  // Vector(5, 0, 0)
assert(scaled.x == 5, "Div Vec")

3. Point / Number Scales the point's coordinates (relative to origin).

p = Point(10, 10, 10)
scaled = p / 2  // Point(5, 5, 5)
assert(scaled.x == 5, "Div Point")

4. Array / Number (Broadcast)

arr = [10, 20, 30]
res = arr / 10  // [1, 2, 3]
assert(res[0] == 1, "Div Array")

5. Midpoint Calculation Combining + and / to find a midpoint.

p1 = Point(0, 0, 0)
p2 = Point(10, 10, 10)
mid = (p1 + p2) / 2  // Point(5, 5, 5)
assert(mid.x == 5, "Midpoint")

Modulo / Cross Product %

Overloaded for remainder and vector cross product.

1. Number % Number Euclidean remainder.

rem = 10 % 3  // 1
assert(rem == 1, "Mod")

2. Negative Modulo Handles negative numbers correctly (Euclidean).

val = -10 % 3  // 2 (not -1)
assert(val == 2, "Euclidean Mod")

3. Array Broadcast

nums = [10, 11, 12]
mods = nums % 2  // [0, 1, 0]
assert(mods[1] == 1, "Mod Array")

Power / Distance ^

Overloaded for exponentiation and distance calculation.

1. Number ^ Number Exponentiation.

sq = 5 ^ 2  // 25
assert(sq == 25, "Pow")

2. Vector ^ Vector (Distance) Euclidean distance between two vectors tips.

v1 = Vector(0, 0, 0)
v2 = Vector(3, 4, 0)
dist = v1 ^ v2  // 5
assert(dist == 5, "Vec Dist")

3. Point ^ Point (Distance) Distance between two points.

p1 = Point(0, 0, 0)
p2 = Point(1, 1, 1)
dist = p1 ^ p2  // sqrt(3) ~ 1.732
assert(abs(dist - 1.732) < 0.001, "Point Dist")

4. Array Broadcast

nums = [1, 2, 3]
squares = nums ^ 2  // [1, 4, 9]
assert(squares[2] == 9, "Pow Array")

Projection >>

Projects a point onto a plane.

1. Point >> Plane Projects the point onto the plane surface along the normal.

pt = Point(0, 0, 10)
pl = Plane(Vector(0,0,1), 0) // XY Plane
// Projects pt onto pl
proj = pt >> pl
assert(proj.z == 0, "Projection")

2. Array >> Plane Projects multiple points.

pts = [Point(0,0,1), Point(0,0,2)]
pl = Plane(Vector(0,0,1), 0)
flat = pts >> pl
assert(flat[0].z == 0, "Array Projection")

Logical Operators

Returns true or false.

Comparison

==, !=, <, >, <=, >=.

1. Number Comparison

check = 10 > 5  // true
assert(check, "GT")

2. Equality Check

is_equal = 5 == 5  // true
assert(is_equal, "EQ")

3. Filtering Logic Used inside filters.

x = 10
assert(x > 0, "Check")

4. Boolean Logic && (AND), || (OR), ! (NOT).

x = 5
valid = (x > 0) && (x < 10)
assert(valid, "Logic")

5. Ternary Operator ? : Conditional value selection.

x = 5
val = (x > 0) ? "Positive" : "Negative"
assert(val == "Positive", "Ternary")

Pipe Operators

Map / Apply |>

Passes the left operand as the first argument to the function on the right.

1. Function Application

x = 16 |> sqrt  // same as sqrt(16) -> 4
assert(x == 4, "Pipe Func")

2. Array Mapping Applies the function to every element.

nums = [1, 4, 9]
roots = nums |> sqrt  // [1, 2, 3]
assert(roots[1] == 2, "Pipe Map")

3. Symmetry Application Applies a symmetry group to geometry.

p = Point(1, 0, 0)
rot = Rotate(Vector(0,0,1), 4)
// Generates 4 points
sym_pts = p |> rot
assert(sym_pts.length == 4, "Symmetry Apply")

4. Chained Operations Reading left-to-right.

val = -16 |> abs |> sqrt
assert(val == 4, "Pipe Chain")

5. Lambda Application

nums = [1, 2, 3]
doubled = nums |> (x) => x * 2  // [2, 4, 6]
assert(doubled[2] == 6, "Pipe Lambda")

Filter ?|

Selects elements from an array that satisfy a condition.

1. Basic Filtering

nums = [1, 2, 3, 4, 5]
odds = nums ?| (x) => x % 2 != 0  // [1, 3, 5]
assert(odds.length == 3, "Filter Basic")

2. Geometric Filtering Find points above the equator.

pts = [Point(0,0,1), Point(0,0,-1)]
upper = pts ?| (p) => p.z > 0
assert(upper.length == 1, "Filter Geo")

3. Type Filtering (Advanced usage with type checks)

4. Range Filtering

nums = 1 .. 10
high = nums ?| (x) => x > 8  // [9, 10]
assert(high.length == 2, "Filter Range")

5. Combined with Map Filter then Map.

result = (1 .. 10) ?| ((x) => x > 5) |> (x) => x * 10
assert(result[0] == 60, "Filter Map")

Geometric Operators

Bind :

Binds a Normal Vector and a Point (or distance) to create a Plane.

1. Normal : Point Creates a plane with normal n passing through point p.

n = Vector(0, 0, 1)
p = Point(0, 0, 5)
pl = n : p  // Plane(0,0,1, dist=5)
assert(pl.distance == 5, "Bind Point")

2. Normal : Distance (Number) Usually done via constructor Plane(n, d), but : works if overloaded (check specific implementation). Typically used with Points.

3. Array : Point Creates multiple planes passing through one point.

normals = [Vector(1,0,0), Vector(0,1,0)]
p = Point(0,0,0)
planes = normals : p
assert(planes.length == 2, "Bind Array Normal")

4. Normal : Array Creates parallel planes.

n = Vector(0,0,1)
pts = [Point(0,0,0), Point(0,0,10)]
planes = n : pts
assert(planes.length == 2, "Bind Array Point")

5. Auto-Centering If the point is the origin (0,0,0), it creates a center-point cut.

pl = Vector(1,1,1) : Point(0,0,0)

Sweep / Auto-Note ->

Similar to Bind, but implies a "cutting" action and often attaches automatic documentation notes (like "Meet A", "Set Girdle").

1. Normal -> Distance Creates a plane at distance d.

pl = Vector(0,0,1) -> 5
assert(pl.distance == 5, "Sweep Dist")

2. Normal -> Point Creates a plane through point p.

pl = Vector(0,0,1) -> Point(0,0,5)
assert(pl.distance == 5, "Sweep Point")

3. Auto-Note: Set Girdle If the cut defines the girdle width, -> detects it.

girdle_pl = Vector(1,0,0) -> 10
// plane.notes is automatically set to "Set stone size" or "Set girdle width"

4. Auto-Note: Meet If the target point was derived from an intersection, -> generates a "Meet ..." note.

pl1 = Plane(Vector(1,0,0), 1)
pl2 = Plane(Vector(0,1,0), 1)
pl3 = Plane(Vector(0,0,1), 1)
meet_pt = pl1 * pl2 * pl3

cut = Vector(1,1,1) -> meet_pt
// plane.notes = "Meet [Plane names...]"

5. Chaining

pl1 = Plane(Vector(1,0,0), 1)
pl2 = Plane(Vector(0,1,0), 1)
pl3 = Plane(Vector(0,0,1), 1)

final_plane = Vector(0,0,1) -> (pl1 * pl2 * pl3)

Contact *>

Finds the "Contact Point" or "Tangent Point". It searches a set of geometry for the point most extreme in the direction of a vector.

*1. Vector > Stone Finds the vertex on the current stone that is furthest in direction v.

_ := Cube(2)
// Find the tip of the stone (highest Z)
tip = Vector(0,0,1) *> Stone
assert(tip.z == 1, "Contact Stone")

*2. Vector > Array Finds the point in the array that maximizes dot product with v.

pts = [Point(0,0,0), Point(10,10,10)]
v = Vector(1,1,1)
best = v *> pts  // Point(10,10,10)
assert(best.x == 10, "Contact Array")

3. Used for Meet-point Faceting "Cut until you touch this vertex".

_ := Cube(2)
target_v = Vector(0,0,1) *> Stone
new_cut = Vector(1,0,1) -> target_v

*4. Vector > Plane (Usually undefined unless bounded, behavior depends on implementation).

5. Finding Girdle Corners

_ := Cube(2)
corner = Vector(1,0,0) *> Stone

Range Operator ..

Creates an array of integers.

1. Basic Range

r = 1 .. 5  // [1, 2, 3, 4, 5]
assert(r.length == 5, "Range Len")

2. Loop Indexing Useful for creating sequences.

angles = 0 .. 3 |> (i) => i * 90
assert(angles[3] == 270, "Range Loop")

3. Creating Grids

x_vals = 0 .. 10
y_vals = 0 .. 10

4. With Filter

evens = (1 .. 10) ?| (x) => x % 2 == 0
assert(evens.length == 5, "Range Filter")

5. Negative Ranges

r = -3 .. 3  // [-3, -2, -1, 0, 1, 2, 3]
assert(r.length == 7, "Neg Range")

Functional Core

Convex-λ is a functional language. Functions are first-class citizens, meaning they can be assigned to variables, passed as arguments, and returned from other functions.

Lambda Expressions

Lambdas are anonymous functions defined using the => arrow syntax.

Basic Syntax

// Identity function
id = (x) => x
assert(id(5) == 5, "Identity")

// Squaring function
sq = (x) => x * x
assert(sq(4) == 16, "Square")

// Multiple arguments
add = (a, b) => a + b
assert(add(2,3) == 5, "Add")

Closures

Lambdas can "capture" variables from their surrounding scope. This is called a closure.

factor = 10
multiplier = (x) => x * factor

res = multiplier(5) // 50
assert(res == 50, "Closure")

Higher-Order Functions

These are functions that take other functions as input.

Map |>

Transforms every element in an array using a function.

nums = [1, 2, 3, 4]

// Square every number
squares = nums |> (x) => x * x
// Result: [1, 4, 9, 16]
assert(squares[3] == 16, "Map Square")

// Create Points from numbers
points = nums |> (z) => Point(0, 0, z)
// Result: [Point(0,0,1), Point(0,0,2), ...]
assert(points[0].z == 1, "Map Point")

Filter ?|

Selects elements that satisfy a boolean condition.

nums = 1..10

// Keep only evens
evens = nums ?| (n) => n % 2 == 0
// Result: [2, 4, 6, 8, 10]
assert(evens.length == 5, "Filter Even")

// Filter geometry
all_pts = [Point(0,0,1), Point(0,0,-1)]
upper_pts = all_pts ?| (p) => p.z > 0
assert(upper_pts.length == 1, "Filter Geo")

Fold

Reduces an array to a single value by iterating through it. Syntax: arr.fold(reducer_lambda, initial_value)

nums = [1, 2, 3, 4]

// Sum
sum = nums.fold((acc, x) => acc + x, 0) // 10
assert(sum == 10, "Fold Sum")

// Product
prod = nums.fold((acc, x) => acc * x, 1) // 24
assert(prod == 24, "Fold Prod")

// Find Max
max_val = nums.fold((acc, x) => max(acc, x), 0)
assert(max_val == 4, "Fold Max")

Nested Lambdas & Currying

You can return functions from functions.

// A function that returns a function
make_adder = (n) => (x) => x + n

add_10 = make_adder(10)
res = add_10(5) // 15
assert(res == 15, "Curry")

Complex Chains

Functional programming allows you to build complex logic by chaining simple operations.

Example: Geometry Generation pipeline

  1. Generate range of angles.
  2. Filter out specific sectors.
  3. Map to Vectors (Normals).
  4. Bind to Planes.
// 1. Generate angles 0..360 in steps of 15
angles = (0..23) |> (i) => i * 15

// 2. Filter: Keep only angles in the first quadrant (0-90)
quad1 = angles ?| (a) => a >= 0 && a <= 90

// 3. Map: Create Normal vectors at 45 deg elevation
normals = quad1 |> (az) => Normal(45, az)

// 4. Map: Create Planes at distance 10
planes = normals |> (n) => Plane(n, 10)

// Render all
_ := planes
assert(planes.length == 7, "Chain Result")

Example: Nested Processing

Finding the average height of points in a point cloud.

cloud = [Point(1,1,5), Point(2,2,10), Point(3,3,15)]

// Extract Z values
heights = cloud |> (p) => p.z

// Sum them up
total_h = heights.fold((acc, h) => acc + h, 0)

// Calculate average
avg_h = total_h / heights.length
assert(avg_h == 10, "Avg Height")

Geometric Types

Convex-λ provides first-class support for 3D geometry.

Vector

Represents a direction and magnitude in 3D space. Vectors are location-independent.

Constructor

Vector(x, y, z)

v = Vector(1.0, 0.0, 0.0)
up = Vector(0, 0, 1)
assert(up.z == 1, "Vector Z")

Properties

  • .x, .y, .z: Coordinates

Helpers

  • Normal(elevation, azimuth): Creates a unit vector from spherical coordinates.
    • elevation: 0 (Equator) to 90 (Pole).
    • azimuth: 0 to 360.
    • Note: If Mode("index") is active, elevation 0 is Pole, 90 is Equator.
// Standard Math Mode
n = Normal(90, 0) // Z axis (Pole)
assert(abs(n.z - 1) < 0.001, "Normal pole")

// Faceting Mode (after Gear())
Gear(96)
n_facet = Normal(0, 0) // Z axis (Pole)
assert(abs(n_facet.z - 1) < 0.001, "Facet Normal pole")

Point

Represents a specific location in 3D space.

Constructor

Point(x, y, z)

origin = Point(0, 0, 0)
p = Point(10, 5, -2)
assert(p.y == 5, "Point Y")

Properties

  • .x, .y, .z: Coordinates

Line

Represents an infinite line defined by an origin point and a direction vector.

Constructor

  1. Line(Point, Vector): Point and Direction.
  2. Line(Point, Point): Two points (direction is p2 - p1).
l1 = Line(Point(0,0,0), Vector(0,0,1)) // Z axis
l2 = Line(Point(0,0,0), Point(1,1,1))  // Diagonal

Usage

Lines are typically the result of intersections (Plane * Plane).

Plane

Represents an infinite plane. This is the primary "cutting" tool in faceting.

Constructor

  1. Plane(Normal, Distance): Normal vector and distance from origin.
  2. Plane(index, p1, p2): (Faceting mode only) Defines plane by gear index and two points.
// Plane facing X, at distance 5
p1 = Plane(Vector(1,0,0), 5)
assert(p1.distance == 5, "Plane Dist")

// Plane facing Z, passing through origin
p2 = Plane(Vector(0,0,1), 0)

Properties

  • .normal: The normal Vector.
  • .distance: The distance Number.

Symmetry Groups

Represents a geometric transformation or collection of transformations. Groups are applied using the pipe operator |>.

Constructor

  1. Rotate(axis, order): Creates a rotational symmetry group.
  2. Mirror(plane): Creates a reflection symmetry.
// 4-fold rotation around Z
rot = Rotate(Vector(0,0,1), 4)

// Mirror across XY plane
mir = Mirror(Plane(Vector(0,0,1), 0))

// Composition using *
group = rot * mir

Arrays of Geometry

Most operations work on arrays of geometry (broadcasting).

ring_pts = [Point(1,0,0), Point(0,1,0), Point(-1,0,0), Point(0,-1,0)]
assert(ring_pts.length == 4, "Array length")

Helpers

  • Hull(points_array): Creates the convex hull (array of planes) from a set of points.
pts = [Point(1,0,0), Point(-1,0,0), Point(0,1,0), Point(0,-1,0), Point(0,0,1), Point(0,0,-1)]
octahedron = Hull(pts)
assert(octahedron.length == 8, "Octahedron faces")

Solid Modeling

In Convex-λ, a "Solid" or "Stone" is defined as the intersection of half-spaces defined by planes. You "cut" a stone by adding planes.

5 Ways to Make a Cube

Here are five different ways to define a Cube of size 2 (extending from -1 to 1).

1. The Built-in Function

The simplest way.

c1 = Cube(2)
_ := c1
assert(c1.length == 6, "Builtin Cube")

2. Explicit Planes

Defining all 6 faces manually.

planes = [
    Plane(Vector( 1, 0, 0), 1), // Right
    Plane(Vector(-1, 0, 0), 1), // Left
    Plane(Vector( 0, 1, 0), 1), // Front
    Plane(Vector( 0,-1, 0), 1), // Back
    Plane(Vector( 0, 0, 1), 1), // Top
    Plane(Vector( 0, 0,-1), 1)  // Bottom
]
_ := planes
assert(planes.length == 6, "Explicit Planes")

3. Convex Hull of Points

Defining the 8 vertices and letting the Hull function calculate the planes.

// Helper to make code shorter
pm = [-1, 1]

// Generate all 8 points using functional mapping
points = pm |> (x) =>
    pm |> (y) =>
        pm |> (z) => Point(x, y, z)

// Note: This produces a nested array [[[Point...]]]
// but Hull flattens it automatically.
cube_hull = Hull(points)
_ := cube_hull
assert(cube_hull.length == 6, "Hull Cube")

4. Symmetry (Rotation)

Define one side face and rotate it 4 times, then add Top and Bottom.

// Side face (Right)
side = Plane(Vector(1,0,0), 1)

// Symmetry group: 4-fold rotation around Z
rotZ = Rotate(Vector(0,0,1), 4)

// Apply rotation
walls = side |> rotZ

// Add caps
caps = [
    Plane(Vector(0,0,1), 1),
    Plane(Vector(0,0,-1), 1)
]

full = walls + caps
_ := full
assert(full.length == 6, "Symm Cube")

5. Intersection of Slabs

A cube is the intersection of 3 perpendicular "slabs" (infinite strips).

// Function to make a slab (pair of parallel planes)
make_slab = (normal, width) => [
    Plane(normal, width/2),
    Plane(-normal, width/2)
]

slab_x = make_slab(Vector(1,0,0), 2)
slab_y = make_slab(Vector(0,1,0), 2)
slab_z = make_slab(Vector(0,0,1), 2)

slab_cube = slab_x + slab_y + slab_z
_ := slab_cube
assert(slab_cube.length == 6, "Slab Cube")

Platonic Solids

Tetrahedron

Using Hull of 4 points.

s = sqrt(2)
tetra_pts = [
    Point(1, 0, -1/s),
    Point(-1, 0, -1/s),
    Point(0, 1, 1/s),
    Point(0, -1, 1/s)
]
tetra = Hull(tetra_pts)
_ := tetra
assert(tetra.length == 4, "Tetrahedron")

Octahedron

Using the built-in or symmetry.

// Built-in
oct1 = Octahedron(2)
_ := oct1
assert(oct1.length == 8, "Octahedron Builtin")

// Or Intersecting Pyramids
pyramid_top = Plane(Vector(1,0,1), 1) |> Rotate(Vector(0,0,1), 4)
pyramid_bot = Plane(Vector(1,0,-1), 1) |> Rotate(Vector(0,0,1), 4)
oct2 = pyramid_top + pyramid_bot
_ := oct2
assert(oct2.length == 8, "Octahedron Symm")

Dodecahedron

Defined by 12 planes.

phi = (1 + sqrt(5)) / 2

// 1. A plane perpendicular to a vertex of a cube
p1 = Plane(Vector(0, 1, phi), 1)

// 2. Rotate it to cover all variations
// This requires complex icosahedral symmetry groups
// which we can simulate by permuting coordinates manually or using Hull.

Note: For complex solids like Dodecahedron and Icosahedron, calculating the Hull of their vertices is often easier.

Archimedean Solids

Truncated Cube

Start with a Cube, then cut off the corners.

// 1. Make the Cube
_ := Cube(2)

// 2. Cut corners
// A corner is at (1,1,1). The normal pointing at it is Vector(1,1,1).
corner_cut = Plane(Vector(1,1,1), 1.5)

// Apply 4-fold symmetry around Z, then Mirror Z
// Combine symmetry groups first
group = Rotate(Vector(0,0,1), 4) * Mirror(Plane(Vector(0,0,1), 0))
cuts = corner_cut |> group

_ := cuts
assert(cuts.length == 8, "Truncated Corners")

Faceting Setup

Welcome to Part 2: Faceting. While Part 1 focused on abstract geometry, Part 2 applies these concepts to the specific workflow of gemstone design.

Configuration

A faceting design always starts with the machine configuration.

Gear(96)    // Set the index gear (96, 80, 64, etc.)
RI(1.76)    // Set Refractive Index (e.g., Corundum)

Modes

Calling Gear(n) automatically switches the system to "index" mode.

  • Angle: 0 to 90 degrees (elevation).
    • 0° = Pole (Table)
    • 90° = Equator (Girdle)
  • Index: 0 to Gear Size (azimuth).

Note: This differs from standard math spherical coordinates used in Part 1.

The Faceting Coordinate System

When Gear is set, the Normal constructor adapts:

Normal(elevation_angle, gear_index)

// A facet on the table (top)
table = Normal(0, 0)

// A facet on the girdle at index 96 (or 0)
girdle_main = Normal(90, 96)

// A pavilion facet at 42 degrees, index 3
pavilion = Normal(42, 3)

Preforms

Before cutting facets, you usually establish a rough shape or "Preform".

Cylinder (Girdle)

A basic 16-sided cylinder preform.

// Define indices: 96 / 16 = 6
indices = (0..15) |> (i) => i * 6

// Create vertical planes (90 degrees) at distance 10
cylinder = indices |> (idx) => Normal(90, idx) -> 10

_ := cylinder

Cone (Pavilion)

Cutting a cone to establish a center point (CP).

// Cut cone at 42 degrees
cone = indices |> (idx) => Normal(42, idx) -> Point(0,0,0)

_ := cone

Symmetry helpers

In faceting, we often repeat cuts around the stone. While |> with Rotate works, standard loops and arrays are common.

// Standard 8-fold symmetry function
sym8 = (idx) => (0..7) |> (i) => (idx + i * (96/8)) % 96

// Generate all 8 indices for index 3
indices_3 = sym8(3)
// [3, 15, 27, 39, 51, 63, 75, 87]

Cutting & Tiering

This chapter covers the core of Meetpoint Faceting: placing facets precisely where they meet.

The Sweep Operator ->

In faceting, we rarely place planes at arbitrary coordinates. We "sweep" a plane from infinity until it hits a specific target.

Plane = Normal -> Target

1. Cutting to Centerpoint (CP)

The most common starting move. All pavilion mains usually meet at the culet (tip).

// Define symmetry indices (e.g., 96-index gear, 8-fold)
indices = [96, 12, 24, 36, 48, 60, 72, 84]

// Cut Mains at 42 degrees to Centerpoint
p1 := indices |> (i) => Normal(42, i) -> Point(0,0,0)

2. Cutting to Girdle Width

Establishing the size of the stone.

// Cut Girdle facets at 90 degrees, distance 5mm
girdle := indices |> (i) => Normal(90, i) -> 5

Note: The -> operator automatically detects this as a "Set stone size" operation and adds it to the facet notes.

3. Cutting to Meet (Meetpoint Faceting)

The heart of the process. We cut a new tier until it touches a vertex formed by previous tiers.

Example: Cutting Breaks

Assume we have p1 (Mains) and girdle tiers. We want to cut "Break" facets that meet where the Main and Girdle intersect.

  1. Find the Meetpoint. Use the * operator to intersect planes, or *> to find a vertex on the stone.

    // Find intersection of Main(96) and Girdle(96)
    // We access the specific planes from our arrays
    mp = p1[0] * girdle[0] * p1[1]
    // This defines a point where two mains and a girdle meet
    

    Alternatively, use Contact (*>):

    // "Look" in the direction of index 6 (between 96 and 12)
    // The stone's current corner is there.
    target_v = Normal(0, 6) *> Stone
    
  2. Cut to the Meetpoint.

    // Breaks at 45 degrees
    break_indices = [6, 18, 30, 42, 54, 66, 78, 90]
    
    p2 := break_indices |> (i) => Normal(45, i) -> target_v
    

    Note: -> automatically detects the meetpoint source and adds "Meet [Plane Names]" to the notes.

Tiering

A "Tier" is a group of facets (usually with same angle) cut in a symmetry pattern.

// Helper for 8-fold symmetry
sym8 = (start_idx) => (0..7) |> (k) => (start_idx + k*12) % 96

// Tier 1: Pavilion Mains
t1 := sym8(96) |> (i) => Normal(42, i) -> Point(0,0,0)

// Tier 2: Girdle
t2 := sym8(96) |> (i) => Normal(90, i) -> 5

// Tier 3: Breaks (meeting Mains & Girdle)
// Calculate one meetpoint
ref_mp = t1[0] * t2[0]
// Cut tier
t3 := sym8(6) |> (i) => Normal(45, i) -> ref_mp

Auto-Noting

The -> operator is smart. It analyzes the Target expression to generate cutting instructions.

  • -> Number: "Set stone size" or "Set girdle width" (if angle is 90).
  • -> Point(0,0,0): "Cut to centerpoint".
  • -> (P1 * P2): "Meet P1, P2".

This makes your code self-documenting for the final diagram generation.

Geometric Analysis

Part 2 concludes with advanced analysis. You can inspect the geometry you create to verify angles, measure distances, or optimize performance.

The Stone Variable

The Stone variable is a built-in structure that updates automatically whenever you use := with geometry.

// Setup
_ := Cube(2)

// Accessing Stone properties
_ := "Volume: " + Stone.volume
_ := "Area: " + Stone.area
_ := "Center: " + Stone.center

It also contains arrays of the raw geometry:

  • Stone.planes: All planes added.
  • Stone.vertices: Calculated convex hull vertices.

Filtering Facets

You can use the functional skills from Part 1 to inspect specific parts of the stone.

Example: Counting Facets by Type

// Setup
_ := Cube(2)

// Helper to classify facets
is_pavilion = (p) => p.normal.z < -0.01
is_crown    = (p) => p.normal.z > 0.01
is_girdle   = (p) => abs(p.normal.z) <= 0.01

// Filter the Stone.planes array
pav_count = (Stone.planes ?| is_pavilion).length
crown_count = (Stone.planes ?| is_crown).length

_ := "Pavilion Facets: " + pav_count
_ := "Crown Facets: " + crown_count

Ray Casting

You can "shoot" a laser through the stone to measure thickness or light paths.

Operator: Line * Stone -> Returns sorted array of intersection points.

Example: Measuring Center Thickness

// Setup
_ := Cube(2)

// 1. Define a Ray straight down through the center
ray = Line(Point(0,0,10), Vector(0,0,-1))

// 2. Cast against the Stone
hits := ray * Stone

// 3. Analyze
// hits[0] is entry (Table), hits[1] is exit (Culet)
thickness = hits[0] ^ hits[1]

_ := "Center Thickness: " + thickness

Custom Analysis Tools

You can build your own tools using Blocks and Lambdas.

Example: Bounding Box

// Setup
_ := Cube(2)

BoundingBox = (points) => {
    {
        min: Point(
            min(points |> (p) => p.x),
            min(points |> (p) => p.y),
            min(points |> (p) => p.z)
        ),
        max: Point(
            max(points |> (p) => p.x),
            max(points |> (p) => p.y),
            max(points |> (p) => p.z)
        )
    }
}

// Usage
box = BoundingBox(Stone.vertices)
_ := "Height: " + (box.max.z - box.min.z)

Simple Cut

A standard Round Brilliant style design.

// 1. Configuration
Gear(96)
RI(1.54)

// 2. Symmetry
sym = Rotate(Z, 8)

// 3. Pavilion (Bottom)
// Pavilion requires Planes (P), not just Vectors (n)
// Negative angle for Pavilion (Z < 0)
pavilion = Plane(Normal(-43.0, 4), 5) |> sym

// 4. Crown (Top)
// Positive angle for Crown (Z > 0)
crown = Plane(Normal(35.0, 4), 5) |> sym

// 5. Table
// Table is the top-most facet.
// Normal Z(0,0,1) points Up.
// Distance determines height. Must be > 0.
// Usually Table is higher than the Crown/Pavilion intersection height?
// Or we cut Crown/Pavilion first (infinite cone), then chop top with Table.
// If Crown distance is 5, tip is at Z ~ 5/cos(35) ~ 6.1.
// So Table should be at Z=6 or similar to create a facet.
table = Plane(Z, 6.0)

// 6. Assembly
main := pavilion + crown + table

Advanced Pavilion Design

This example demonstrates a multi-tier pavilion with color coding.

// 1. Configuration
Gear(96)
RI(1.54)

// Use standard Z-axis for 8-fold symmetry
sym = Rotate(Z, 8)
// Define a mirror plane across the Y axis (normal to Y)
// This creates 16-fold symmetry when combined with Rotate(Z,8)
mirror = Mirror(Plane(Y, 0))

// 2. Pavilion (Bottom)
// Pavilion angles are negative
P1 = Plane(Normal(-43.0, 0), 5) |> sym
P1 = P1 { color: "blue", notes: "Main Facets" }

// Tier 2: Star Facets
P2 = Plane(Normal(-41.0, 6), 5) |> sym
P2 = P2 { color: "cyan" }

// Tier 3: Lower Girdle
P3 = Plane(Normal(-44.0, 3), 5) |> sym * mirror
P3 = P3 { color: "navy" }

// 3. Crown (Top)
// Crown angles are positive
crown = Plane(Normal(35.0, 0), 5) |> sym

// 4. Table
table = Plane(Z, 6.0) // Flat top at height 6

// 5. Assembly
pavilion = P1 + P2 + P3
stone := pavilion + crown + table

Using Attributes

You can attach attributes to any geometry variable.

// Create P1 first (as above)
P1 = Plane(Normal(-43.0, 0), 5) |> sym

// Attach color
P1 = P1 { color: "blue" }

Gemstone Computations

You can write scripts to analyze geometry.

// 1. Setup Stone
sym = Rotate(Z, 8)
// Pavilion (Negative)
P1 = Plane(Normal(-42.0, 0), 5) |> sym
P2 = Plane(Normal(-41.0, 6), 5) |> sym
// Crown (Positive)
C1 = Plane(Normal(35.0, 0), 5) |> sym
// Table (Z+, Height 6)
Table = Plane(Z, 6.0)

Stone_Planes = P1 + P2 + C1 + Table
// Assign to "Stone" to render and compute hull
Stone := Stone_Planes

// 2. Extract Coordinates
// unique ensures we don't count duplicate vertices
Verts = Stone_Planes.unique.vertices

Xs = Verts |> (v) => v.x
Ys = Verts |> (v) => v.y
Zs = Verts |> (v) => v.z

// 3. Analysis Helpers
import "std"

// Custom fold to find max/min
get_max = (arr) => arr.fold((acc, x) => max(acc, x), -1000.0)
get_min = (arr) => arr.fold((acc, x) => min(acc, x), 1000.0)

min_x = get_min(Xs)
max_x = get_max(Xs)
// ... same for Y and Z
_ := [min_x, max_x]

Optical Checks

Calculate critical angles to check for windowing.

import "std"

// Calculate Critical Angle for RI 1.54
crit_angle = asin(1.0 / 1.54) * (180.0 / PI)

// Check if a pavilion angle works
is_windowing = 40.0 < crit_angle
_ := [crit_angle, is_windowing]

Analyzer

The Analyzer scores your gemstone design against professional-grade optical metrics. It runs 39 GPU-accelerated simulations across different viewing angles to measure brightness, contrast, and scintillation.

Before you start

  • Run your code first so the interpreter outputs valid geometry.
  • Clear all errors. The Analyzer stays disabled until the last run finishes without errors.
  • The studio requires WebGPU support in your browser.

Run an analysis

  1. Open a design and click Analyzer in the side panel.
  2. When the run finishes, the metrics and charts update immediately.

Understand the results

  • Metric cards show Average Brightness, Contrast Density, Contrast Intensity, Scintillation Score, Compactness, and Shannon Entropy. Each value is shown as a percentage relative to a reference brilliant.
    • Average Brightness is the mean return light over every valid pixel in the sweep.
    • Contrast Density measures the amount of contrast areas visible. It describes the spatial distribution of light and dark zones.
    • Contrast Intensity measures the strength of the contrast in areas where it exists.
    • Scintillation Score counts blink events between angles and normalises them by the number of pixels and tilt steps.
    • Compactness reports how much of the stone's bounding box is filled by the actual volume.
    • Shannon Entropy measures the information content or "richness" of the light distribution.
  • Per-angle sparklines plot the same metrics for each of the 39 tilt angles so you can see where the stone rises or falls off.

Head Shadow Toggle

The Head Shadow checkbox controls whether the simulation includes a realistic head shadow—the occlusion caused by the viewer's head blocking some overhead light.

  • Enabled (default): Simulates realistic viewing conditions.
  • Disabled: Removes the head shadow for brighter overall scores, useful for comparing theoretical maximum brightness.

Toggle this setting and the Analyzer immediately re-runs.

Show Angle Images

Click 📷 Show Angle Images to see the actual rendered frames from all 39 viewing angles. A modal opens with a grid of images labeled by their tilt angle (e.g., L45° for 45° left tilt, for upright view, T45° for 45° tilt).

This is invaluable for debugging:

  • See exactly where brightness drops or spikes
  • Identify problem areas like light leakage or dead zones
  • Verify that geometry is correct across viewing angles

Optimizer

The Optimizer explores your design's parameter space to find configurations that maximize optical performance. It leverages WebGPU acceleration to evaluate thousands of variations in real-time.

Configuring the optimizer in code

In Convex-λ, you configure the optimizer directly in your source code by assigning an optimizer variable with a struct:

optimizer = {
    algorithm: "de",
    maxIterations: 200,
    populationSize: 20,
    weights: {
        brightness: 0.4,
        contrast: 0.3,
        scintillation: 0.2,
        compactness: 0.1
    }
}

Configuration options

OptionTypeDescription
algorithmstring"de" (differential evolution), "ga" (genetic), "sa" (simulated annealing), "random"
maxIterationsnumberMaximum optimization iterations
populationSizenumberPopulation size for evolutionary algorithms
weightsstructWeights for each metric (brightness, contrast, scintillation, etc.)
crossoverRatenumberCrossover rate for DE/GA (0.0–1.0)
differentialWeightnumberDifferential weight for DE (typically 0.5–1.0)
mutationRatenumberMutation rate for GA
initialTempnumberStarting temperature for SA
finalTempnumberEnding temperature for SA
coolingRatenumberCooling rate for SA
seednumberRandom seed for reproducibility

Marking values as optimizable

Add hints after numbers to mark them as optimizable.

Syntax Note: The hint immediately follows the number. Do not enclose the hint in parentheses.

// Percentage range: +/- 10% of the value
angle = 45 +/- 10%
angle = 45 ± 10%

// Absolute range: between 3.2 and 3.6
depth = 3.4 3.2..3.6

// Plus/minus offset
width = 2.0 +/- 0.5
width = 2.0 ± 0.5

Hint formats

FormatMeaningExample
value +/- N%±N% of value45 +/- 10% → 40.5 to 49.5
value ± N%±N% of value45 ± 10% → 40.5 to 49.5
value min..maxExplicit range3.4 3.2..3.6
value +/- delta±delta2.0 +/- 0.5 → 1.5 to 2.5
value ± delta±delta2.0 ± 0.5 → 1.5 to 2.5

If no hints are present, the engine returns "No optimizable parameters found".

Launch and monitor a run

  1. Run your code and clear every error.
  2. Press Start Optimization. The button switches to "Optimization Running".
  3. The progress panel shows:
    • Current iteration and best score
    • Improvement vs. initial design
    • Live metrics (brightness, contrast, scintillation, entropy, compactness)
    • Convergence chart
    • Algorithm details (population, generation, evaluations)
    • Parameter table showing initial, current, and best values
  4. Stop & Keep Best stops and preserves the strongest candidate so far.

Compare and apply

  • The comparison panel shows side-by-side viewports labeled Original and Optimized.
  • Replace Editor with Optimized swaps the editor contents with the optimized parameters.
  • Save Optimized as New Version creates a new file while preserving the original.

Example

// Configure the optimizer
optimizer = {
    algorithm: "de",
    maxIterations: 150,
    populationSize: 15,
    weights: {
        brightness: 0.5,
        contrast: 0.3,
        scintillation: 0.2
    }
}

// Mark values as optimizable
pavilionAngle = 41.5 +/- 10%  // ±10%
crownAngle = 34.0 30..38      // range 30-38

// Basic setup
Gear(96)
Sym := Rotate(Z, 8)

// Geometry
Pavilion := Plane(Normal(pavilionAngle), 10) |> Sym
Crown := Plane(Normal(crownAngle), 10) |> Sym

Stone := Pavilion + Crown

Diagram Generation

Convex-λ automatically generates cutting instructions (diagrams) for every script execution. This diagram includes the Pavilion and Crown tiers, index settings, and angles.

Metadata (Info Struct)

You can customize the diagram's header information (Author, Title, Footnote, etc.) by defining a special info variable in your script.

You can use either = or := assignment.

info = {
    title: "My Brilliant Cut",
    author: "Jane Doe",
    date: "2024-03-20",
    footnote: "Best cut in RI 1.76"
}

Supported Fields

FieldDescription
titleThe main title displayed at the top of the diagram. Defaults to the script filename if omitted.
authorDisplayed below the title.
dateA date string displayed in the header.
footnoteSmall italic text displayed at the bottom of the diagram (useful for copyright, specific instructions, or material notes).

Tier Sorting & Splitting

The diagram generator automatically organizes facets into "Pavilion" and "Crown" sections based on their geometric position.

  • Crown: Facets facing generally "Up" (Z > 0).
  • Pavilion: Facets facing generally "Down" (Z <= 0).

Tiers are listed in the order they appear in your code (or the order they are constructed), preserving your specific cutting sequence.

Gear & Indexes

The diagram automatically detects the gear setting from your code using the Gear(N) directive (e.g., Gear(96)).

  • Index conversions are automatic.
  • The diagram displays the specific gear number used (e.g., "Gear: 96").

Refractive Index (RI)

The diagram also displays the Refractive Index (RI) used for the stone, helping you verify that the design matches the intended material. This is taken from your RI(n) setting.

Printing

Click the Print button in the diagram panel to open a clean, printer-friendly version of the cutting instructions, including:

  • Metadata headers
  • Formatted tables for Pavilion and Crown
  • Wireframe views (Top, Bottom, Side, Perspective)

Storage

Ultra IDE supports three storage backends for your Convex-λ projects. Each has different characteristics for file access, authentication, and synchronization.

At a Glance

Feature📁 Local Storage☁️ Google Drive📦 Dropbox
Files live...On your computer's diskIn your Google Drive cloudIn your Dropbox cloud
AuthenticationNone (browser permission)Google sign-inDropbox sign-in
SynchronizationInstant (direct disk access)On explicit saveOn explicit save
Offline access✅ Full❌ Requires internet❌ Requires internet
External editing✅ Use any editor❌ Use Ultra IDE only❌ Use Ultra IDE only

📁 Local Storage

When you use Local Storage, Ultra IDE works directly with files on your computer using your browser's File System Access API.

How it works

  1. Select a Working Folder on your disk (e.g., /Users/jon/Designs).
  2. Ultra IDE scans this folder for .cv files and displays them in the file tree.
  3. When you edit and save, changes are written directly to disk—just like a native application.
  4. If you edit files with an external editor (VS Code, Sublime), Ultra IDE sees those changes on next open.

Synchronization behavior

Instant and automatic. Because files live on your local disk:

  • Saving a file immediately updates the disk.
  • The working folder is persisted across browser restarts.
  • You can use Git, Dropbox desktop sync, or any other file-level tools alongside Ultra IDE.

Requirements

  • Browser: Chrome, Edge, or Brave (uses File System Access API).
  • Permission: You grant the browser one-time permission to read/write your working folder.

Note: Safari and Firefox do not support the File System Access API. Use Chrome-based browsers for local storage.


☁️ Google Drive

When you use Google Drive, Ultra IDE stores your project files in your Google Drive cloud storage.

How it works

  1. Sign in with your Google account when prompted.
  2. Select or create a project folder in your Drive (e.g., My Gems/Project1).
  3. Ultra IDE downloads the project files into memory.
  4. When you save, changes are uploaded back to Google Drive.

Synchronization behavior

Explicit saves only. Cloud storage works differently from local disk:

  • Files are not synced in real-time like Dropbox Sync or Google Drive desktop.
  • When you open a project, Ultra IDE downloads a snapshot of your files.
  • When you save, Ultra IDE uploads the changed files.
  • If you modify files through the Google Drive web interface while the IDE is open, those changes won't appear until you reopen the project.

What this means for you

  • Save frequently to push your work to the cloud.
  • Don't edit the same files in both Ultra IDE and Google Drive simultaneously.
  • Your files are safe in the cloud, accessible from any device with Ultra IDE.

📦 Dropbox

When you use Dropbox, Ultra IDE stores your project files in your Dropbox cloud storage.

How it works

  1. Sign in with your Dropbox account when prompted.
  2. Enter or select a project folder path (e.g., /Gemstones/MyDesign).
  3. Ultra IDE downloads the project files into memory.
  4. When you save, changes are uploaded back to Dropbox.

Synchronization behavior

Explicit saves only. Identical to Google Drive:

  • Files are downloaded once when you open the project.
  • Saves upload changes to Dropbox.
  • The Dropbox desktop app will sync those changes to your computer, but Ultra IDE doesn't monitor for external changes.

How It Works Behind the Scenes

Ultra IDE uses a multi-adapter architecture to support different storage backends:

┌─────────────────────────────────────────────┐
│                Ultra IDE                     │
│  (File Tree, Editor, Tabs, Run Code)         │
└───────────────────┬─────────────────────────┘
                    │
          ┌─────────▼─────────┐
          │  Integration Layer │
          │  (file-system-integration.ts)
          └─────────┬─────────┘
                    │
    ┌───────────────┼───────────────┐
    ▼               ▼               ▼
┌─────────┐   ┌─────────┐   ┌─────────┐
│ Local   │   │ Google  │   │ Dropbox │
│ Adapter │   │ Drive   │   │ Adapter │
│         │   │ Adapter │   │         │
└────┬────┘   └────┬────┘   └────┬────┘
     │             │             │
  [Disk]       [REST API]   [REST API]

All adapters implement the same IFileSystemService interface, so the IDE doesn't need to know which backend is active—it just calls readFile(), writeFile(), and listFiles().


Common Questions

"I saved in Ultra IDE but my Dropbox desktop app doesn't show the change!"

Dropbox does sync the file—check the timestamp. But if you have the file open in another editor, you may need to reload it there to see the update.

"I edited a file on Google Drive web—why doesn't Ultra IDE show the change?"

Ultra IDE downloaded your project when you opened it. External changes made after that point aren't automatically detected. Close and reopen the project to refresh.

"Is my code stored on ProFacet servers?"

No. Ultra IDE is a client-side application. Your files go directly between your browser and:

  • Your local disk (Local Storage), or
  • Google's servers (Google Drive), or
  • Dropbox's servers (Dropbox)

ProFacet never sees your design files.

"Which should I use?"

Your situationRecommended
I want files on my computer with Git📁 Local Storage
I work from multiple devices☁️ Google Drive or 📦 Dropbox
I share designs for others to view☁️ Google Drive (use Drive's sharing)
I need offline access📁 Local Storage
I'm on Safari/Firefox☁️ Cloud storage (local not supported)

⚠️ Collaboration warning: Cloud storage is not designed for simultaneous editing. If two people edit the same file at the same time, the last person to save will overwrite the other's changes. For real collaboration, use Local Storage with Git.

BNF Grammar

This is the complete formal grammar for Convex-λ in Backus-Naur Form.

Program Structure

<program>       ::= <statement>*

<statement>     ::= <import>
                  | <export>
                  | <assignment>
                  | <config>
                  | <expression>
                  | <comment>

Import and Export

<import>        ::= "import" <import_list> "from" <string>
<import_list>   ::= <identifier> ("," <identifier>)* ["as" <identifier>]

<export>        ::= "export" <identifier_list>
<identifier_list> ::= <identifier> ("," <identifier>)*

Assignments

<assignment>    ::= <identifier> "=" <expression>   # variable binding
                  | <identifier> ":=" <expression>  # renderable output

Configuration

<config>        ::= "Gear" "(" <number> ")"
                  | "Mode" "(" <string> ")"
                  | "RI" "(" <number> ")"

Expressions

<expression>    ::= <ternary>

<ternary>       ::= <logical_or> ("?" <expression> ":" <expression>)?

<logical_or>    ::= <logical_and> ("||" <logical_and>)*
<logical_and>   ::= <equality> ("&&" <equality>)*

<equality>      ::= <comparison> (("==" | "!=") <comparison>)*
<comparison>    ::= <term> (("<" | ">" | "<=" | ">=") <term>)*

<term>          ::= <factor> (("+" | "-") <factor>)*
<factor>        ::= <power> (("*" | "/" | "%") <power>)*

<power>         ::= <unary> ("^" <power>)?
<unary>         ::= ("!" | "-" | "?")? <pipe>

<pipe>          ::= <postfix> (("|>" | "?|") <postfix>)*
<postfix>       ::= <primary> <postfix_op>*

<postfix_op>    ::= "." <identifier>          # property access
                  | "(" <arg_list>? ")"        # function call
                  | "[" <expression> "]"       # index access
                  | ">>" <expression>          # index shorthand

Primary Expressions

<primary>       ::= <number>
                  | <string>
                  | <identifier>
                  | <array>
                  | <lambda>
                  | <fold>
                  | <constructor_call>
                  | "(" <expression> ")"

<array>         ::= "[" (<expression> ("," <expression>)*)? "]"

<lambda>        ::= <identifier> "=>" <expression>
                  | "(" <param_list>? ")" "=>" <expression>

<fold>          ::= "fold" "(" <expression> "," <expression> "," <expression> ")"

Constructors

<constructor_call> ::= "n" "(" <expression> "," <expression> ")"
                     | "p" "(" <expression> "," <expression> "," <expression> ")"
                     | "v" "(" <expression> "," <expression> "," <expression> ")"
                     | "P" "(" <expression> "," <expression> ")"
                     | "l" "(" <expression> "," <expression> ")"
                     | "Rot" "(" <expression> "," <expression> ")"
                     | "Mir" "(" <expression> ")"
                     | "Hull" "(" <expression> ")"

Literals

<number>        ::= <float> <opt_hint>?
<opt_hint>      ::= <range_hint> | <delta_hint>
<range_hint>    ::= <float> ".." <float>
<delta_hint>    ::= ("+/-" | "±") <float> "%"?

<string>        ::= '"' <char>* '"'

<identifier>    ::= <letter> (<letter> | <digit> | "_")*

<letter>        ::= "a".."z" | "A".."Z" | "_"
<digit>         ::= "0".."9"

Comments

<comment>       ::= "//" <any>* <newline>
                  | "/*" <any>* "*/"

Built-in Constants

<constant>      ::= "PI" | "PHI" | "RI" | "true" | "false"

Operators Summary

CategoryOperators
Arithmetic+, -, *, /, %, ^
Comparison==, !=, <, >, <=, >=
Logical&&, ||, !
Assignment=, :=
Pipe|>, `?
Geometric:, ->, *>
Access., >>, []
Lambda=>
Range..

Core API Reference

Standard Library Functions

Built-in functions available in the default scope or via import "std".

Math

  • sin(rad), cos(rad), tan(rad)
  • asin(v), acos(v), atan(v)
  • sqrt(n), abs(n), pow(n, exp)
  • min(a, b), max(a, b)
  • floor(n), ceil(n), round(n)
  • PI: Constant (3.14159...)

Collections

  • avg(array): Returns average of numbers.
  • print(args...): Prints to console.
  • Grid(start, step, count): Generates an arithmetic sequence.

Array Methods

  • array.fold(reducer, initial): Reduces array to single value.
  • array.map(lambda): Transforms each element.
  • array.filter(lambda): Selects elements.
  • array.count / array.length: Number of elements.
import "std"

sum := (1..10).fold((acc, x) => acc + x, 0)

Geometry Constructors

Primitives

  • Vector(x, y, z): Creates a Vector.
  • Point(x, y, z): Creates a Point.
  • Normal(elev, index): Creates a normal Vector from machine angles (elevation, index). Requires Gear().
  • Normal(elev, azim): Creates a normal Vector from spherical coordinates (elevation, azimuth).
  • Plane(normal, dist): Creates a Plane.
  • Line(origin, dir): Creates a Line.

Solids

  • Cube(size): Creates a Cube centered at origin.
  • Octahedron(size): Creates an Octahedron.
  • Hull(geometry...): computes the convex hull of the provided points/planes.

Symmetry

  • Rotate(axis, order): Creates a rotational symmetry group of order n around axis.
  • Mirror(plane): Creates a reflection symmetry group across plane.

Global Configuration

  • Gear(n): Sets the index gear (e.g., 96). Enables Index Mode.
  • RI(n): Sets the Refractive Index.
  • Mode(s): Sets the angle mode ("deg" or "index").