Moonbit docs
// picogkshapes: Parametric shape construction library for PicoGK.
// MoonBit port of the Go picogkshapes package, which is itself a port of
// PicoPie's picogk.shapes parametric shape library.
//
// Provides high-level parametric shapes (Sphere, Box, Cylinder, Ring, Lens,
// Pipe, etc.) that build meshes from parametric surface sampling and
// rasterize them into voxel fields via the picogkffi FFI binding.
// --- Vec3 (float64) for parametric computations ---
///|
pub struct Vec3 {
x : Double
y : Double
z : Double
}
///|
pub fn Vec3::new(x : Double, y : Double, z : Double) -> Vec3 {
Vec3::{ x, y, z }
}
///|
pub fn vec3(x : Double, y : Double, z : Double) -> Vec3 {
Vec3::{ x, y, z }
}
///|
pub fn Vec3::add(self : Vec3, o : Vec3) -> Vec3 {
Vec3::{ x: self.x + o.x, y: self.y + o.y, z: self.z + o.z }
}
///|
pub fn Vec3::sub(self : Vec3, o : Vec3) -> Vec3 {
Vec3::{ x: self.x - o.x, y: self.y - o.y, z: self.z - o.z }
}
///|
pub fn Vec3::mul(self : Vec3, s : Double) -> Vec3 {
Vec3::{ x: self.x * s, y: self.y * s, z: self.z * s }
}
///|
pub fn Vec3::dot(self : Vec3, o : Vec3) -> Double {
self.x * o.x + self.y * o.y + self.z * o.z
}
///|
pub fn Vec3::cross(self : Vec3, o : Vec3) -> Vec3 {
Vec3::{
x: self.y * o.z - self.z * o.y,
y: self.z * o.x - self.x * o.z,
z: self.x * o.y - self.y * o.x,
}
}
///|
pub fn Vec3::len(self : Vec3) -> Double {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
///|
pub fn Vec3::normalized(self : Vec3) -> Vec3 {
let l = self.len()
if l < 0.000000000001 {
return Vec3::{ x: 0.0, y: 0.0, z: 0.0 }
}
Vec3::{ x: self.x / l, y: self.y / l, z: self.z / l }
}
///|
pub fn safe_normalized(v : Vec3, eps : Double) -> Vec3 {
let l = v.len()
if l < eps {
return Vec3::{ x: 0.0, y: 0.0, z: 0.0 }
}
Vec3::{ x: v.x / l, y: v.y / l, z: v.z / l }
}
///|
pub fn lerp(p1 : Vec3, p2 : Vec3, t : Double) -> Vec3 {
Vec3::{
x: p1.x * (1.0 - t) + p2.x * t,
y: p1.y * (1.0 - t) + p2.y * t,
z: p1.z * (1.0 - t) + p2.z * t,
}
}
///|
pub fn rotate_around_axis(
pt : Vec3,
axis : Vec3,
angle : Double,
origin : Vec3,
) -> Vec3 {
let pt = pt.sub(origin)
let axis = axis.normalized()
if axis.len() < 0.000000000001 {
return pt.add(origin)
}
let half = angle / 2.0
let s = @math.sin(half)
let qx = axis.x * s
let qy = axis.y * s
let qz = axis.z * s
let qw = @math.cos(half)
let qv = Vec3::{ x: qx, y: qy, z: qz }
let cross1 = qv.cross(pt).add(pt.mul(qw))
let cross2 = qv.cross(cross1)
pt.add(cross2.mul(2.0)).add(origin)
}
///|
pub fn orthogonal_dir(dir : Vec3) -> Vec3 {
let dir = dir.normalized()
if dir.x.abs() < 0.95 {
return Vec3::{ x: 1.0, y: 0.0, z: 0.0 }.sub(dir.mul(dir.x)).normalized()
}
Vec3::{ x: 0.0, y: 1.0, z: 0.0 }.sub(dir.mul(dir.y)).normalized()
}
// --- RGB colors ---
///|
pub struct RGB {
r : Double
g : Double
b : Double
}
///|
pub fn RGB::new(r : Double, g : Double, b : Double) -> RGB {
RGB::{ r, g, b }
}
///|
pub fn RGB::to_ffi(self : RGB) -> @picogkffi.ColorFloat {
@picogkffi.ColorFloat::new(self.r, self.g, self.b, 1.0)
}
///|
pub let palette : Map[String, RGB] = {
let m : Map[String, RGB] = {}
m["blue"] = RGB::{ r: 0.258824, g: 0.529412, b: 0.960784 }
m["frozen"] = RGB::{ r: 0.427451, g: 0.886275, b: 0.988235 }
m["pitaya"] = RGB::{ r: 0.980392, g: 0.164706, b: 0.533333 }
m["warning"] = RGB::{ r: 0.988235, g: 0.4, b: 0.031373 }
m["green"] = RGB::{ r: 0.0, g: 0.721569, b: 0.0 }
m["yellow"] = RGB::{ r: 0.988235, g: 0.847059, b: 0.031373 }
m["blueberry"] = RGB::{ r: 0.309804, g: 0.05098, b: 0.74902 }
m["lemongrass"] = RGB::{ r: 0.721569, g: 0.878431, b: 0.192157 }
m["orchid"] = RGB::{ r: 0.780392, g: 0.141176, b: 0.513725 }
m["ruby"] = RGB::{ r: 0.690196, g: 0.0, b: 0.172549 }
m["racing_green"] = RGB::{ r: 0.023529, g: 0.360784, b: 0.207843 }
m["crystal"] = RGB::{ r: 0.047059, g: 0.756863, b: 0.968627 }
m["billie"] = RGB::{ r: 0.007843, g: 0.968627, b: 0.043137 }
m["lavender"] = RGB::{ r: 0.788235, g: 0.4, b: 1.0 }
m["bubblegum"] = RGB::{ r: 1.0, g: 0.4, b: 0.807843 }
m["gray"] = RGB::{ r: 0.741176, g: 0.741176, b: 0.741176 }
m
}
// --- Modulations ---
//
// We provide both function-type helpers (line_const, surf_const, etc.) that
// return plain closures, and struct wrappers (LineModulation, SurfaceModulation)
// for cases where a value type is preferred.
///|
pub struct LineModulation {
fn_ : (Double) -> Double
}
///|
pub fn LineModulation::new(f : (Double) -> Double) -> LineModulation {
LineModulation::{ fn_: f }
}
///|
pub fn LineModulation::call(self : LineModulation, ratio : Double) -> Double {
(self.fn_)(ratio)
}
///|
pub fn LineModulation::const_val(v : Double) -> LineModulation {
LineModulation::{ fn_: fn(_ : Double) { v } }
}
///|
pub fn LineModulation::from_fn(f : (Double) -> Double) -> LineModulation {
LineModulation::{ fn_: f }
}
///|
pub fn LineModulation::mul(
self : LineModulation,
factor : Double,
) -> LineModulation {
LineModulation::{ fn_: fn(r : Double) { (self.fn_)(r) * factor } }
}
///|
pub fn LineModulation::add(
self : LineModulation,
other : LineModulation,
) -> LineModulation {
LineModulation::{ fn_: fn(r : Double) { (self.fn_)(r) + (other.fn_)(r) } }
}
///|
pub fn LineModulation::sub(
self : LineModulation,
other : LineModulation,
) -> LineModulation {
LineModulation::{ fn_: fn(r : Double) { (self.fn_)(r) - (other.fn_)(r) } }
}
///|
pub struct SurfaceModulation {
fn_ : (Double, Double) -> Double
}
///|
pub fn SurfaceModulation::new(
f : (Double, Double) -> Double,
) -> SurfaceModulation {
SurfaceModulation::{ fn_: f }
}
///|
pub fn SurfaceModulation::call(
self : SurfaceModulation,
phi : Double,
lr : Double,
) -> Double {
(self.fn_)(phi, lr)
}
///|
pub fn SurfaceModulation::const_val(v : Double) -> SurfaceModulation {
SurfaceModulation::{ fn_: fn(_ : Double, _ : Double) { v } }
}
///|
pub fn SurfaceModulation::from_fn(
f : (Double, Double) -> Double,
) -> SurfaceModulation {
SurfaceModulation::{ fn_: f }
}
///|
pub fn SurfaceModulation::from_line(
line : LineModulation,
line_arg : String,
) -> SurfaceModulation {
if line_arg == "first" {
SurfaceModulation::{ fn_: fn(phi : Double, _ : Double) { (line.fn_)(phi) } }
} else {
SurfaceModulation::{ fn_: fn(_ : Double, lr : Double) { (line.fn_)(lr) } }
}
}
///|
pub fn SurfaceModulation::mul(
self : SurfaceModulation,
factor : Double,
) -> SurfaceModulation {
SurfaceModulation::{
fn_: fn(p : Double, l : Double) { (self.fn_)(p, l) * factor },
}
}
///|
pub fn SurfaceModulation::add(
self : SurfaceModulation,
other : SurfaceModulation,
) -> SurfaceModulation {
SurfaceModulation::{
fn_: fn(p : Double, l : Double) { (self.fn_)(p, l) + (other.fn_)(p, l) },
}
}
///|
pub fn SurfaceModulation::sub(
self : SurfaceModulation,
other : SurfaceModulation,
) -> SurfaceModulation {
SurfaceModulation::{
fn_: fn(p : Double, l : Double) { (self.fn_)(p, l) - (other.fn_)(p, l) },
}
}
// --- Function-type modulation helpers (used by the gallery) ---
///|
pub fn line_const(v : Double) -> (Double) -> Double {
fn(_ : Double) { v }
}
///|
pub fn line_fn(f : (Double) -> Double) -> (Double) -> Double {
f
}
///|
pub fn surf_const(v : Double) -> (Double, Double) -> Double {
fn(_ : Double, _ : Double) { v }
}
///|
pub fn surf_fn(f : (Double, Double) -> Double) -> (Double, Double) -> Double {
f
}
///|
pub fn surf_from_line(
line : (Double) -> Double,
line_arg : String,
) -> (Double, Double) -> Double {
if line_arg == "first" {
fn(phi : Double, _ : Double) { line(phi) }
} else {
fn(_ : Double, lr : Double) { line(lr) }
}
}
// --- LocalFrame ---
///|
pub struct LocalFrame {
pos : Vec3
local_x : Vec3
local_y : Vec3
local_z : Vec3
}
///|
pub fn new_local_frame(position : Vec3, local_z : Vec3?) -> LocalFrame {
match local_z {
None =>
LocalFrame::{
pos: position,
local_x: vec3(1.0, 0.0, 0.0),
local_y: vec3(0.0, 1.0, 0.0),
local_z: vec3(0.0, 0.0, 1.0),
}
Some(z) =>
if z.len() < 0.000000000001 {
LocalFrame::{
pos: position,
local_x: vec3(1.0, 0.0, 0.0),
local_y: vec3(0.0, 1.0, 0.0),
local_z: vec3(0.0, 0.0, 1.0),
}
} else {
let zn = z.normalized()
let x = orthogonal_dir(zn)
let y = zn.cross(x).normalized()
LocalFrame::{ pos: position, local_x: x, local_y: y, local_z: zn }
}
}
}
///|
pub fn new_local_frame_xyz(
position : Vec3,
local_z : Vec3,
local_x : Vec3,
) -> LocalFrame {
let zn = local_z.normalized()
let xn = local_x.normalized()
let yn = zn.cross(xn).normalized()
LocalFrame::{ pos: position, local_x: xn, local_y: yn, local_z: zn }
}
///|
pub fn LocalFrame::point_to_world(self : LocalFrame, local_pt : Vec3) -> Vec3 {
self.pos
.add(self.local_x.mul(local_pt.x))
.add(self.local_y.mul(local_pt.y))
.add(self.local_z.mul(local_pt.z))
}
///|
pub fn LocalFrame::translated(self : LocalFrame, offset : Vec3) -> LocalFrame {
LocalFrame::{
pos: self.pos.add(offset),
local_x: self.local_x,
local_y: self.local_y,
local_z: self.local_z,
}
}
///|
pub fn LocalFrame::rotated(
self : LocalFrame,
angle : Double,
axis : Vec3,
) -> LocalFrame {
LocalFrame::{
pos: self.pos,
local_x: rotate_around_axis(self.local_x, axis, angle, vec3(0.0, 0.0, 0.0)),
local_y: rotate_around_axis(self.local_y, axis, angle, vec3(0.0, 0.0, 0.0)),
local_z: rotate_around_axis(self.local_z, axis, angle, vec3(0.0, 0.0, 0.0)),
}
}
// --- Frames (spine frames) ---
///|
pub struct Frames {
spine : Array[Vec3]
local_x : Array[Vec3]
local_y : Array[Vec3]
local_z : Array[Vec3]
}
///|
pub fn frames_aligned_to_x(points : Array[Vec3], target_x : Vec3) -> Frames {
let n = points.length()
let lx = Array::make(n, vec3(0.0, 0.0, 0.0))
let ly = Array::make(n, vec3(0.0, 0.0, 0.0))
let lz = Array::make(n, vec3(0.0, 0.0, 0.0))
for i in 0.. Vec3 {
let mut best_dot = -2.0
let mut best_dir = orthogonal_dir(local_z)
let mut deg = 0.0
while deg < 180.0 {
let angle = deg * 3.14159265358979 / 180.0
let dir = rotate_around_axis(
orthogonal_dir(local_z),
local_z,
angle,
vec3(0.0, 0.0, 0.0),
)
let d = dir.dot(target_x)
if d > best_dot {
best_dot = d
best_dir = dir
}
deg = deg + 0.01
}
if best_dir.dot(target_x) < 0.0 {
best_dir = best_dir.mul(-1.0)
}
best_dir.normalized()
}
///|
pub fn Frames::frame_at(self : Frames, lr : Double) -> LocalFrame {
let n = self.spine.length()
if n == 0 {
return new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
if lr <= 0.0 {
return new_local_frame_xyz(self.spine[0], self.local_z[0], self.local_x[0])
}
if lr >= 1.0 {
return new_local_frame_xyz(
self.spine[n - 1],
self.local_z[n - 1],
self.local_x[n - 1],
)
}
let t = lr * (n - 1).to_double()
let i = t.to_int()
let frac = t - i.to_double()
let idx = if i >= n - 1 { n - 2 } else { i }
let f = if i >= n - 1 { 1.0 } else { frac }
let pos = lerp(self.spine[idx], self.spine[idx + 1], f)
let z = lerp(self.local_z[idx], self.local_z[idx + 1], f).normalized()
let x = lerp(self.local_x[idx], self.local_x[idx + 1], f).normalized()
new_local_frame_xyz(pos, z, x)
}
// --- ControlPointSpline ---
///|
pub struct ControlPointSpline {
control_points : Array[Vec3]
degree : Int
knot : Array[Double]
}
///|
pub fn new_control_point_spline(
control_points : Array[Vec3],
degree : Int,
closed : Bool,
) -> ControlPointSpline {
let d = if degree < 1 { 2 } else { degree }
let mut pts = Array::make(control_points.length(), vec3(0.0, 0.0, 0.0))
for i in 0.. Vec3 {
let mut pt = vec3(0.0, 0.0, 0.0)
for i in 0.. Array[Vec3] {
let result = Array::make(n, vec3(0.0, 0.0, 0.0))
for i in 0.. Double {
let eps = 0.0000001
if degree == 0 {
if (knot[i] <= t && t < knot[i + 1]) ||
(
(t - knot[i + 1]).abs() < eps &&
(t - knot[knot.length() - 1]).abs() < eps
) {
return 1.0
}
return 0.0
}
let mut value = 0.0
if (knot[i + degree] - knot[i]).abs() > eps {
value = value +
(t - knot[i]) /
(knot[i + degree] - knot[i]) *
spline_basis(knot, t, i, degree - 1)
}
if (knot[i + degree + 1] - knot[i + 1]).abs() > eps {
value = value +
(knot[i + degree + 1] - t) /
(knot[i + degree + 1] - knot[i + 1]) *
spline_basis(knot, t, i + 1, degree - 1)
}
value
}
///|
fn spline_knot_vector(
n_control : Int,
degree : Int,
clamp : Bool,
) -> Array[Double] {
let n_knots = n_control + degree + 1
let valid_range = if n_control - degree < 1 { 1 } else { n_control - degree }
let d = 1.0 / valid_range.to_double()
let knot = Array::make(n_knots, 0.0)
for i in 0.. 1.0 {
knot[i] = 1.0
}
}
}
knot
}
// --- Mesh building utilities ---
///|
pub fn to_voxels(mesh : @picogkffi.Mesh) -> @picogkffi.Voxels {
@picogkffi.from_mesh(mesh)
}
///|
fn quad_grid_to_mesh(grid : Array[Array[Vec3]]) -> @picogkffi.Mesh {
let verts : Array[Double] = []
let tris : Array[Int] = []
let a = grid.length()
if a < 2 {
return @picogkffi.mesh_from_arrays([], [])
}
let b = grid[0].length()
if b < 2 {
return @picogkffi.mesh_from_arrays([], [])
}
for i in 0..<(a - 1) {
for j in 0..<(b - 1) {
let p0 = grid[i][j]
let p1 = grid[i + 1][j]
let p2 = grid[i + 1][j + 1]
let p3 = grid[i][j + 1]
let idx = verts.length() / 3
append_vert(verts, p0)
append_vert(verts, p1)
append_vert(verts, p2)
tris.push(idx)
tris.push(idx + 1)
tris.push(idx + 2)
let idx2 = verts.length() / 3
append_vert(verts, p0)
append_vert(verts, p2)
append_vert(verts, p3)
tris.push(idx2)
tris.push(idx2 + 1)
tris.push(idx2 + 2)
}
}
@picogkffi.mesh_from_arrays(verts, tris)
}
///|
fn append_vert(verts : Array[Double], v : Vec3) -> Unit {
verts.push(v.x)
verts.push(v.y)
verts.push(v.z)
}
///|
pub struct SurfaceMeshBuilder {
verts : Array[Double]
tris : Array[Int]
}
///|
pub fn new_surface_mesh_builder() -> SurfaceMeshBuilder {
SurfaceMeshBuilder::{ verts: [], tris: [] }
}
///|
pub fn SurfaceMeshBuilder::add(
self : SurfaceMeshBuilder,
grid : Array[Array[Vec3]],
flip : Bool,
) -> SurfaceMeshBuilder {
let a = grid.length()
if a < 2 {
return self
}
let b = grid[0].length()
if b < 2 {
return self
}
for i in 0..<(a - 1) {
for j in 0..<(b - 1) {
let p0 = grid[i][j]
let p1 = grid[i][j + 1]
let p2 = grid[i + 1][j + 1]
let p3 = grid[i + 1][j]
if flip {
let idx = self.verts.length() / 3
append_vert(self.verts, p0)
append_vert(self.verts, p2)
append_vert(self.verts, p1)
self.tris.push(idx)
self.tris.push(idx + 1)
self.tris.push(idx + 2)
let idx2 = self.verts.length() / 3
append_vert(self.verts, p0)
append_vert(self.verts, p3)
append_vert(self.verts, p2)
self.tris.push(idx2)
self.tris.push(idx2 + 1)
self.tris.push(idx2 + 2)
} else {
let idx = self.verts.length() / 3
append_vert(self.verts, p0)
append_vert(self.verts, p1)
append_vert(self.verts, p2)
self.tris.push(idx)
self.tris.push(idx + 1)
self.tris.push(idx + 2)
let idx2 = self.verts.length() / 3
append_vert(self.verts, p0)
append_vert(self.verts, p2)
append_vert(self.verts, p3)
self.tris.push(idx2)
self.tris.push(idx2 + 1)
self.tris.push(idx2 + 2)
}
}
}
self
}
///|
pub fn SurfaceMeshBuilder::build(self : SurfaceMeshBuilder) -> @picogkffi.Mesh {
@picogkffi.mesh_from_arrays(self.verts, self.tris)
}
///|
fn make_grid(a : Int, b : Int) -> Array[Array[Vec3]] {
let grid : Array[Array[Vec3]] = []
for _ in 0.. Array[Double] {
let result = Array::make(n, 0.0)
for i in 0.. Double
azim_steps : Int
polar_steps : Int
transform : ((Array[Vec3]) -> Array[Vec3])?
}
///|
pub fn new_sphere(
frame : LocalFrame?,
radius : (Double, Double) -> Double,
) -> Sphere {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Sphere::{
frame: f,
radius,
azim_steps: 360,
polar_steps: 180,
transform: None,
}
}
///|
pub fn Sphere::to_mesh(self : Sphere) -> @picogkffi.Mesh {
let a = self.azim_steps
let p = self.polar_steps + 1
let grid = make_grid(a, p)
let f = self.frame
for i in 0.. @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
///|
fn apply_grid_transform_sphere(
grid : Array[Array[Vec3]],
transform : ((Array[Vec3]) -> Array[Vec3])?,
) -> @picogkffi.Mesh {
let grid = match transform {
Some(t) => apply_transform(grid, t)
None => grid
}
quad_grid_to_mesh(grid)
}
///|
fn apply_transform(
grid : Array[Array[Vec3]],
t : (Array[Vec3]) -> Array[Vec3],
) -> Array[Array[Vec3]] {
let pts : Array[Vec3] = []
for row in grid {
for p in row {
pts.push(p)
}
}
let transformed = t(pts)
let result = make_grid(
grid.length(),
if grid.length() > 0 {
grid[0].length()
} else {
0
},
)
let mut idx = 0
for i in 0.. Double
depth : (Double) -> Double
frames : Frames?
w_steps : Int
d_steps : Int
l_steps : Int
transform : ((Array[Vec3]) -> Array[Vec3])?
}
///|
pub fn new_box(
frame : LocalFrame?,
length : Double,
width : (Double) -> Double,
depth : (Double) -> Double,
) -> Box {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Box::{
frame: f,
length,
width,
depth,
frames: None,
w_steps: 5,
d_steps: 5,
l_steps: 5,
transform: None,
}
}
///|
pub fn Box::to_mesh(self : Box) -> @picogkffi.Mesh {
let nw = self.w_steps
let nd = self.d_steps
let nl = self.l_steps
let w = Array::make(nw, 0.0)
for i in 0.. @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
///|
fn Box::box_spine(self : Box, lr : Double) -> (Vec3, Vec3, Vec3) {
match self.frames {
Some(fs) => {
let fr = fs.frame_at(lr)
(fr.pos, fr.local_x, fr.local_y)
}
None => {
let pos = self.frame.pos.add(self.frame.local_z.mul(self.length * lr))
(pos, self.frame.local_x, self.frame.local_y)
}
}
}
///|
fn Box::box_surface_grid(
self : Box,
w_r : Array[Double],
d_r : Array[Double],
l_r : Array[Double],
) -> Array[Array[Vec3]] {
let nr_w = w_r.length()
let nr_d = d_r.length()
let nr_l = l_r.length()
let mut r0 = 1
let mut r1 = 1
if nr_l > 1 && nr_w > 1 {
r0 = nr_l
r1 = nr_w
} else if nr_l > 1 && nr_d > 1 {
r0 = nr_l
r1 = nr_d
} else if nr_w > 1 && nr_d > 1 {
r0 = nr_w
r1 = nr_d
} else if nr_l > 1 {
r0 = nr_l
} else if nr_w > 1 {
r0 = nr_w
} else if nr_d > 1 {
r0 = nr_d
}
let grid = make_grid(r0, r1)
for i in 0.. Double
frames : Frames?
polar_steps : Int
radial_steps : Int
length_steps : Int
}
///|
pub fn new_cylinder(
frame : LocalFrame?,
length : Double,
radius : (Double, Double) -> Double,
) -> Cylinder {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Cylinder::{
frame: f,
length,
radius,
frames: None,
polar_steps: 360,
radial_steps: 5,
length_steps: 5,
}
}
///|
pub fn Cylinder::to_mesh(self : Cylinder) -> @picogkffi.Mesh {
let smb = new_surface_mesh_builder()
let nl = if self.length_steps < 2 { 2 } else { self.length_steps }
let l_ratios = arange(nl + 1)
let spine = fn(lr : Double) {
match self.frames {
Some(fs) => fs.frame_at(lr).pos
None => self.frame.pos.add(self.frame.local_z.mul(self.length * lr))
}
}
let (lx, ly) = match self.frames {
Some(fs) => {
let fr = fs.frame_at(0.0)
(fr.local_x, fr.local_y)
}
None => (self.frame.local_x, self.frame.local_y)
}
let mut grid = make_grid(self.polar_steps, self.radial_steps + 1)
for i in 0.. @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
// --- Ring (torus) ---
///|
pub struct Ring {
frame : LocalFrame
ring_radius : Double
radius : (Double, Double) -> Double
radial_steps : Int
polar_steps : Int
}
///|
pub fn new_ring(
frame : LocalFrame?,
ring_radius : Double,
radius : (Double, Double) -> Double,
) -> Ring {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Ring::{ frame: f, ring_radius, radius, radial_steps: 360, polar_steps: 360 }
}
///|
pub fn Ring::to_mesh(self : Ring) -> @picogkffi.Mesh {
let n_a = self.radial_steps + 1
let n_p = self.polar_steps
let grid = make_grid(n_a, n_p)
let f = self.frame
for i in 0.. @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
// --- Lens ---
///|
pub struct Lens {
frame : LocalFrame
height : Double
inner_radius : Double
outer_radius : Double
lower : (Double, Double) -> Double
upper : (Double, Double) -> Double
radial_steps : Int
polar_steps : Int
height_steps : Int
}
///|
pub fn new_lens(
frame : LocalFrame?,
height : Double,
inner_radius : Double,
outer_radius : Double,
) -> Lens {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Lens::{
frame: f,
height,
inner_radius,
outer_radius,
lower: surf_const(0.0),
upper: surf_const(height),
radial_steps: 5,
polar_steps: 360,
height_steps: 5,
}
}
///|
pub fn new_lens_with_mods(
frame : LocalFrame?,
height : Double,
inner_radius : Double,
outer_radius : Double,
lower : (Double, Double) -> Double,
upper : (Double, Double) -> Double,
) -> Lens {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Lens::{
frame: f,
height,
inner_radius,
outer_radius,
lower,
upper,
radial_steps: 500,
polar_steps: 360,
height_steps: 5,
}
}
///|
pub fn Lens::to_mesh(self : Lens) -> @picogkffi.Mesh {
let smb = new_surface_mesh_builder()
let p = arange(self.polar_steps)
let rr = arange(self.radial_steps)
let hr = arange(self.height_steps)
let smb = smb.add(self.lens_surface(1.0, p, rr), false)
let smb = smb.add(self.lens_surface(0.0, p, rr), true)
let smb = smb.add(self.lens_mantle_surface(hr, p, 0.0), false)
let smb = smb.add(self.lens_mantle_surface(hr, p, 1.0), true)
smb.build()
}
///|
pub fn Lens::to_voxels(self : Lens) -> @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
///|
fn Lens::lens_surface(
self : Lens,
h : Double,
phi_r : Array[Double],
rad_r : Array[Double],
) -> Array[Array[Vec3]] {
let nphi = phi_r.length()
let nrad = rad_r.length()
let grid = make_grid(nphi, nrad)
let f = self.frame
for i in 0.. Array[Array[Vec3]] {
let nh = hr.length()
let np = p.length()
let grid = make_grid(nh, np)
let f = self.frame
for i in 0.. Double
outer : (Double, Double) -> Double
frames : Frames?
polar_steps : Int
radial_steps : Int
length_steps : Int
transform : ((Array[Vec3]) -> Array[Vec3])?
mut phi_angle_func : (Double, Double) -> Double
}
///|
pub fn new_pipe(
frame : LocalFrame?,
length : Double,
inner_radius : (Double, Double) -> Double,
outer_radius : (Double, Double) -> Double,
) -> Pipe {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
// Check if radii are constant — if not, bump lengthSteps to 500
// (matching Go's isConstant check)
let inner_const = inner_radius(0.0, 0.0) == inner_radius(1.0, 1.0)
let outer_const = outer_radius(0.0, 0.0) == outer_radius(1.0, 1.0)
let l_steps = if inner_const && outer_const { 5 } else { 500 }
Pipe::{
frame: f,
length,
inner: inner_radius,
outer: outer_radius,
frames: None,
polar_steps: 360,
radial_steps: 5,
length_steps: l_steps,
transform: None,
phi_angle_func: fn(phi_ratio : Double, _ : Double) {
2.0 * 3.14159265358979 * phi_ratio
},
}
}
///|
pub fn new_pipe_with_transform(
frame : LocalFrame?,
length : Double,
inner_radius : (Double, Double) -> Double,
outer_radius : (Double, Double) -> Double,
trafo : (Array[Vec3]) -> Array[Vec3],
) -> Pipe {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Pipe::{
frame: f,
length,
inner: inner_radius,
outer: outer_radius,
frames: None,
polar_steps: 360,
radial_steps: 5,
length_steps: 500,
transform: Some(trafo),
phi_angle_func: fn(phi_ratio : Double, _ : Double) {
2.0 * 3.14159265358979 * phi_ratio
},
}
}
///|
pub fn new_pipe_with_frames(
frame : LocalFrame?,
length : Double,
inner_radius : (Double, Double) -> Double,
outer_radius : (Double, Double) -> Double,
fs : Frames,
) -> Pipe {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
Pipe::{
frame: f,
length,
inner: inner_radius,
outer: outer_radius,
frames: Some(fs),
polar_steps: 360,
radial_steps: 5,
length_steps: 500,
transform: None,
phi_angle_func: fn(phi_ratio : Double, _ : Double) {
2.0 * 3.14159265358979 * phi_ratio
},
}
}
///|
pub fn Pipe::to_mesh(self : Pipe) -> @picogkffi.Mesh {
let smb = new_surface_mesh_builder()
let pr = arange(self.polar_steps)
let rr = arange(self.radial_steps)
let lr = arange(self.length_steps)
let smb = smb.add(self.pipe_surface([lr[lr.length() - 1]], pr, rr), false)
let smb = smb.add(self.pipe_surface([lr[0]], pr, rr), true)
let smb = smb.add(self.pipe_surface(lr, pr, [0.0]), false)
let smb = smb.add(self.pipe_surface(lr, pr, [1.0]), true)
smb.build()
}
///|
pub fn Pipe::to_voxels(self : Pipe) -> @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
///|
fn Pipe::pipe_spine(self : Pipe, lr : Double) -> (Vec3, Vec3, Vec3) {
match self.frames {
Some(fs) => {
let fr = fs.frame_at(lr)
(fr.pos, fr.local_x, fr.local_y)
}
None => {
let pos = self.frame.pos.add(self.frame.local_z.mul(self.length * lr))
(pos, self.frame.local_x, self.frame.local_y)
}
}
}
///|
fn Pipe::pipe_surface(
self : Pipe,
lrs : Array[Double],
phi_rs : Array[Double],
rad_rs : Array[Double],
) -> Array[Array[Vec3]] {
let nl = lrs.length()
let np = phi_rs.length()
let nr = rad_rs.length()
let mut r0 = 1
let mut r1 = 1
let mut lr_axis = -1
let mut phi_axis = -1
let mut rad_axis = -1
if np > 1 && nl > 1 {
phi_axis = 0
lr_axis = 1
r0 = np
r1 = nl
} else if np > 1 && nr > 1 {
phi_axis = 0
rad_axis = 1
r0 = np
r1 = nr
} else if nl > 1 && nr > 1 {
lr_axis = 0
rad_axis = 1
r0 = nl
r1 = nr
} else if np > 1 {
phi_axis = 0
r0 = np
} else if nl > 1 {
lr_axis = 0
r0 = nl
} else if nr > 1 {
rad_axis = 0
r0 = nr
}
let grid = make_grid(r0, r1)
for i in 0.. apply_transform(grid, t)
None => grid
}
}
// --- PipeSegment ---
///|
pub struct PipeSegment {
pipe : Pipe
mid : (Double) -> Double
rng : (Double) -> Double
}
///|
pub fn new_pipe_segment(
frame : LocalFrame?,
length : Double,
inner_radius : (Double, Double) -> Double,
outer_radius : (Double, Double) -> Double,
start : (Double) -> Double,
end : (Double) -> Double,
method_name : String,
) -> PipeSegment {
let pipe = new_pipe(frame, length, inner_radius, outer_radius)
let (mid, rng) = if method_name == "start_end" {
(line_mid(start, end), line_sub(end, start))
} else {
(start, end)
}
let ps = PipeSegment::{ pipe, mid, rng }
ps.pipe.phi_angle_func = fn(phi_ratio : Double, lr : Double) {
mid(lr) + (phi_ratio - 0.5) * rng(lr)
}
ps
}
///|
pub fn new_pipe_segment_with_frames(
frame : LocalFrame?,
length : Double,
inner_radius : (Double, Double) -> Double,
outer_radius : (Double, Double) -> Double,
start : (Double) -> Double,
end : (Double) -> Double,
method_name : String,
fs : Frames,
) -> PipeSegment {
let pipe = new_pipe_with_frames(frame, length, inner_radius, outer_radius, fs)
let (mid, rng) = if method_name == "start_end" {
(line_mid(start, end), line_sub(end, start))
} else {
(start, end)
}
let ps = PipeSegment::{ pipe, mid, rng }
ps.pipe.phi_angle_func = fn(phi_ratio : Double, lr : Double) {
mid(lr) + (phi_ratio - 0.5) * rng(lr)
}
ps
}
///|
fn line_mid(
a : (Double) -> Double,
b : (Double) -> Double,
) -> (Double) -> Double {
fn(r : Double) { (a(r) + b(r)) * 0.5 }
}
///|
fn line_sub(
a : (Double) -> Double,
b : (Double) -> Double,
) -> (Double) -> Double {
fn(r : Double) { a(r) - b(r) }
}
///|
pub fn PipeSegment::to_mesh(self : PipeSegment) -> @picogkffi.Mesh {
let smb = new_surface_mesh_builder()
let pr = arange(self.pipe.polar_steps)
let rr = arange(self.pipe.radial_steps)
let lr = arange(self.pipe.length_steps)
let smb = smb.add(
self.pipe.pipe_surface([lr[lr.length() - 1]], pr, rr),
false,
)
let smb = smb.add(self.pipe.pipe_surface([lr[0]], pr, rr), true)
let smb = smb.add(self.pipe.pipe_surface(lr, pr, [0.0]), false)
let smb = smb.add(self.pipe.pipe_surface(lr, pr, [1.0]), true)
let smb = smb.add(self.pipe.pipe_surface(lr, [0.0], rr), false)
let smb = smb.add(self.pipe.pipe_surface(lr, [1.0], rr), true)
smb.build()
}
///|
pub fn PipeSegment::to_voxels(self : PipeSegment) -> @picogkffi.Voxels {
to_voxels(self.to_mesh())
}
// --- Implicit SDF shapes ---
///|
pub struct ImplicitGyroid {
unit_size : Double
thickness_ratio : Double
frequency : Double
}
///|
pub fn new_implicit_gyroid(
unit_size : Double,
thickness_ratio : Double,
) -> ImplicitGyroid {
ImplicitGyroid::{
unit_size,
thickness_ratio,
frequency: 2.0 * 3.14159265358979 / unit_size,
}
}
///|
pub struct ImplicitGenus {
gap : Double
}
///|
pub fn new_implicit_genus(gap : Double) -> ImplicitGenus {
ImplicitGenus::{ gap, }
}
///|
pub struct ImplicitSuperEllipsoid {
center : Vec3
ax : Double
ay : Double
az : Double
e1 : Double
e2 : Double
}
///|
pub fn new_implicit_super_ellipsoid(
center : Vec3,
ax : Double,
ay : Double,
az : Double,
e1 : Double,
e2 : Double,
) -> ImplicitSuperEllipsoid {
ImplicitSuperEllipsoid::{ center, ax, ay, az, e1, e2 }
}
// --- LatticePipe ---
///|
pub struct LatticePipe {
frame : LocalFrame
length : Double
radius : (Double) -> Double
frames : Frames?
l_steps : Int
}
///|
pub fn new_lattice_pipe(
frame : LocalFrame?,
length : Double,
radius : (Double) -> Double,
) -> LatticePipe {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
LatticePipe::{ frame: f, length, radius, frames: None, l_steps: 100 }
}
///|
pub fn new_lattice_pipe_with_frames(
frame : LocalFrame?,
length : Double,
radius : (Double) -> Double,
fs : Frames,
) -> LatticePipe {
let f = match frame {
Some(fr) => fr
None => new_local_frame(vec3(0.0, 0.0, 0.0), None)
}
LatticePipe::{ frame: f, length, radius, frames: Some(fs), l_steps: 100 }
}
///|
pub fn LatticePipe::to_voxels(self : LatticePipe) -> @picogkffi.Voxels {
let lat = @picogkffi.new_lattice()
let n = self.l_steps
for i in 1..<=n {
let lr0 = (i - 1).to_double() / n.to_double()
let lr1 = i.to_double() / n.to_double()
let p0 = match self.frames {
Some(fs) => fs.frame_at(lr0).pos
None => self.frame.pos.add(self.frame.local_z.mul(self.length * lr0))
}
let p1 = match self.frames {
Some(fs) => fs.frame_at(lr1).pos
None => self.frame.pos.add(self.frame.local_z.mul(self.length * lr1))
}
let r0 = (self.radius)(lr0)
let r1 = (self.radius)(lr1)
lat.add_beam(
@picogkffi.Vec3::new(p0.x, p0.y, p0.z),
@picogkffi.Vec3::new(p1.x, p1.y, p1.z),
r0,
r1,
true,
)
}
let vox = @picogkffi.from_lattice(lat)
lat.destroy()
vox
}
// --- LatticeManifold ---
///|
pub struct LatticeManifold {
frame : LocalFrame
length : Double
radius : (Double) -> Double
frames : Frames?
l_steps : Int
max_overhang_angle : Double
extend_both_sides : Bool
min_printable_radius : Double
}
///|
pub fn new_lattice_manifold(
frame : LocalFrame,
length : Double,
radius : Double,
max_overhang_angle : Double,
) -> LatticeManifold {
LatticeManifold::{
frame,
length,
radius: fn(_ : Double) { radius },
frames: None,
l_steps: 100,
max_overhang_angle,
extend_both_sides: false,
min_printable_radius: 0.1,
}
}
///|
pub fn LatticeManifold::set_extend_both_sides(
self : LatticeManifold,
b : Bool,
) -> LatticeManifold {
LatticeManifold::{
frame: self.frame,
length: self.length,
radius: self.radius,
frames: self.frames,
l_steps: self.l_steps,
max_overhang_angle: self.max_overhang_angle,
extend_both_sides: b,
min_printable_radius: self.min_printable_radius,
}
}
///|
fn LatticeManifold::spine_point(self : LatticeManifold, lr : Double) -> Vec3 {
match self.frames {
Some(fs) => fs.frame_at(lr).pos
None => self.frame.pos.add(self.frame.local_z.mul(self.length * lr))
}
}
///|
pub fn LatticeManifold::to_voxels(self : LatticeManifold) -> @picogkffi.Voxels {
let lat = @picogkffi.new_lattice()
let n = self.l_steps
let limit_angle = self.max_overhang_angle * 3.14159265358979 / 180.0
let half_alpha = 3.14159265358979 / 2.0 - limit_angle
let max_r = (self.radius)(0.0)
let r = max_r
let h = r * (1.0 - @math.cos(half_alpha))
let s = 2.0 * r * @math.sin(half_alpha)
let tip_length = @math.tan(half_alpha) * (0.5 * s - self.min_printable_radius)
let world_z = vec3(0.0, 0.0, 1.0)
for i in 0.. Array[Vec3] {
let ctrl = [
vec3(0.0, 0.0, 0.0),
vec3(0.0, 40.0, 0.0),
vec3(0.0, 50.0, 20.0),
vec3(0.0, 60.0, 60.0),
]
let spline = new_control_point_spline(ctrl, 2, false)
spline.points(500)
}
// --- SceneGroup (for gallery) ---
///|
pub struct SceneGroup {
voxels : @picogkffi.Voxels?
mesh : @picogkffi.Mesh?
color : RGB
}
///|
pub fn scene_group_voxels(vox : @picogkffi.Voxels, color : RGB) -> SceneGroup {
SceneGroup::{ voxels: Some(vox), mesh: None, color }
}
///|
pub fn scene_group_mesh(mesh : @picogkffi.Mesh, color : RGB) -> SceneGroup {
SceneGroup::{ voxels: None, mesh: Some(mesh), color }
}
// --- ColorScale3D and SplitByOverhangAngle (for mesh_painter) ---
///|
/// RainbowSpectrum returns blue→green→yellow→orange→red control colors.
pub fn rainbow_spectrum() -> Array[RGB] {
[
RGB::{ r: 0.0, g: 0.0, b: 1.0 },
RGB::{ r: 0.0, g: 1.0, b: 0.0 },
RGB::{ r: 1.0, g: 1.0, b: 0.0 },
RGB::{ r: 1.0, g: 130.0 / 255.0, b: 0.0 },
RGB::{ r: 1.0, g: 0.0, b: 0.0 },
]
}
///|
pub struct ColorScale3D {
rgb_arr : Array[RGB]
min_value : Double
max_value : Double
}
///|
pub fn new_color_scale_3d(
spectrum : Array[RGB],
min_value : Double,
max_value : Double,
) -> ColorScale3D {
let n = 500
let nc = spectrum.length()
let rgb = Array::make(n, RGB::{ r: 0.0, g: 0.0, b: 0.0 })
for i in 0..= nc - 1 { nc - 2 } else { idx.to_int() }
let frac = idx - lo.to_double()
let c1 = spectrum[lo]
let c2 = spectrum[lo + 1]
rgb[i] = RGB::{
r: c1.r + frac * (c2.r - c1.r),
g: c1.g + frac * (c2.g - c1.g),
b: c1.b + frac * (c2.b - c1.b),
}
}
ColorScale3D::{ rgb_arr: rgb, min_value, max_value }
}
///|
pub fn ColorScale3D::color(self : ColorScale3D, value : Double) -> RGB {
let v = if value < self.min_value {
self.min_value
} else if value > self.max_value {
self.max_value
} else {
value
}
let ratio = (v - self.min_value) / (self.max_value - self.min_value)
let idx = (ratio * (self.rgb_arr.length() - 1).to_double()).to_int()
let i = if idx < 0 {
0
} else if idx >= self.rgb_arr.length() {
self.rgb_arr.length() - 1
} else {
idx
}
let c = self.rgb_arr[i]
RGB::{
r: clamp_d(c.r, 0.0, 1.0),
g: clamp_d(c.g, 0.0, 1.0),
b: clamp_d(c.b, 0.0, 1.0),
}
}
///|
fn clamp_d(v : Double, lo : Double, hi : Double) -> Double {
if v < lo {
lo
} else if v > hi {
hi
} else {
v
}
}
///|
/// SplitByOverhangAngle splits a mesh into colored sub-meshes by triangle
/// overhang angle (degrees: 0 = vertical wall, 90 = horizontal).
pub fn split_by_overhang_angle(
mesh : @picogkffi.Mesh,
scale : ColorScale3D,
n_classes : Int,
) -> Array[SceneGroup] {
let verts = mesh.vertices()
let tris = mesh.triangles()
let nt = tris.length() / 3
// Compute per-triangle overhang angle
let angles = Array::make(nt, 0.0)
for i in 0.. 0.0 {
nx = nx / nlen
ny = ny / nlen
nz = nz / nlen
}
let dr = (nx * nx + ny * ny).sqrt()
let dz = nz.abs()
let mut angle = @math.atan2(dz, dr) * 180.0 / 3.14159265358979
if angle > 90.0 {
angle = 90.0
}
if angle < 0.0 {
angle = 0.0
}
angles[i] = angle
}
let lo = scale.min_value
let hi = scale.max_value
let groups : Array[SceneGroup] = []
for k in 0..= lo_k && (angles[i] < hi_k || k == n_classes - 1) {
let a = tris[i * 3] * 3
let b = tris[i * 3 + 1] * 3
let c = tris[i * 3 + 2] * 3
let idx = sub_verts.length() / 3
sub_verts.push(verts[a])
sub_verts.push(verts[a + 1])
sub_verts.push(verts[a + 2])
sub_verts.push(verts[b])
sub_verts.push(verts[b + 1])
sub_verts.push(verts[b + 2])
sub_verts.push(verts[c])
sub_verts.push(verts[c + 1])
sub_verts.push(verts[c + 2])
sub_tris.push(idx)
sub_tris.push(idx + 1)
sub_tris.push(idx + 2)
}
}
if sub_tris.length() > 0 {
let sub_mesh = @picogkffi.mesh_from_arrays(sub_verts, sub_tris)
let color = scale.color(lo_k)
groups.push(scene_group_mesh(sub_mesh, color))
}
}
groups
}