Intial ICED commit
This commit is contained in:
commit
97653580e5
|
|
@ -0,0 +1,5 @@
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"crates/cord-parse",
|
||||||
|
"crates/cord-sdf",
|
||||||
|
"crates/cord-shader",
|
||||||
|
"crates/cord-cordic",
|
||||||
|
"crates/cord-format",
|
||||||
|
"crates/cord-render",
|
||||||
|
"crates/cord-decompile",
|
||||||
|
"crates/cord-trig",
|
||||||
|
"crates/cord-riesz",
|
||||||
|
"crates/cord-expr",
|
||||||
|
"crates/cord-gui",
|
||||||
|
"crates/cordial",
|
||||||
|
"crates/cord-sparse",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "cord"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "3D geometry system: source → trig IR → CORDIC binary"
|
||||||
|
license = "Unlicense"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["sdf", "csg", "cordic", "geometry", "3d"]
|
||||||
|
categories = ["graphics", "mathematics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-parse = { path = "crates/cord-parse" }
|
||||||
|
cord-sdf = { path = "crates/cord-sdf" }
|
||||||
|
cord-shader = { path = "crates/cord-shader" }
|
||||||
|
cord-cordic = { path = "crates/cord-cordic" }
|
||||||
|
cord-format = { path = "crates/cord-format" }
|
||||||
|
cord-render = { path = "crates/cord-render" }
|
||||||
|
cord-decompile = { path = "crates/cord-decompile" }
|
||||||
|
cord-trig = { path = "crates/cord-trig" }
|
||||||
|
cord-riesz = { path = "crates/cord-riesz" }
|
||||||
|
cord-expr = { path = "crates/cord-expr" }
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
anyhow = "1"
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
This is free to use, without conditions.
|
||||||
|
|
||||||
|
There is no licence here on purpose. Individuals, students, hobbyists — take what
|
||||||
|
you need, make it yours, don't think twice. You'd flatter me.
|
||||||
|
|
||||||
|
The absence of a licence is deliberate. A licence is a legal surface. Words can be
|
||||||
|
reinterpreted, and corporations employ lawyers whose job is exactly that. Silence is
|
||||||
|
harder to exploit than language. If a company wants to use this, the lack of explicit
|
||||||
|
permission makes it just inconvenient enough to matter.
|
||||||
|
|
||||||
|
This won't change the world. But it shifts the balance, even slightly, away from the
|
||||||
|
system that co-opts open work for closed profit. That's enough for me.
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="Cord"
|
||||||
|
DIR="build"
|
||||||
|
BUNDLE="${DIR}/${APP_NAME}.app"
|
||||||
|
BIN_NAME="cord-gui"
|
||||||
|
|
||||||
|
echo "building ${BIN_NAME}..."
|
||||||
|
cargo build --release -p cord-gui
|
||||||
|
|
||||||
|
echo "creating ${BUNDLE}..."
|
||||||
|
rm -rf "${DIR}"
|
||||||
|
mkdir -p "${DIR}"
|
||||||
|
mkdir -p "${BUNDLE}/Contents/MacOS"
|
||||||
|
mkdir -p "${BUNDLE}/Contents/Resources"
|
||||||
|
|
||||||
|
cp "target/release/${BIN_NAME}" "${BUNDLE}/Contents/MacOS/${BIN_NAME}"
|
||||||
|
cp "crates/cord-gui/Info.plist" "${BUNDLE}/Contents/Info.plist"
|
||||||
|
|
||||||
|
# Generate icon from SVG
|
||||||
|
ICON_SVG="static/vectors/cord.svg"
|
||||||
|
if [ -f "${ICON_SVG}" ] && command -v rsvg-convert &>/dev/null; then
|
||||||
|
ICONSET="build/icon.iconset"
|
||||||
|
rm -rf "${ICONSET}"
|
||||||
|
mkdir -p "${ICONSET}"
|
||||||
|
|
||||||
|
# iconutil requires exactly these sizes:
|
||||||
|
# icon_NxN.png (1x)
|
||||||
|
# icon_NxN@2x.png (retina — rendered at 2N)
|
||||||
|
for sz in 16 32 128 256 512; do
|
||||||
|
rsvg-convert -w ${sz} -h ${sz} "${ICON_SVG}" -o "${ICONSET}/icon_${sz}x${sz}.png"
|
||||||
|
dbl=$((sz * 2))
|
||||||
|
rsvg-convert -w ${dbl} -h ${dbl} "${ICON_SVG}" -o "${ICONSET}/icon_${sz}x${sz}@2x.png"
|
||||||
|
done
|
||||||
|
|
||||||
|
iconutil -c icns "${ICONSET}" -o "${BUNDLE}/Contents/Resources/AppIcon.icns"
|
||||||
|
rm -rf "${ICONSET}"
|
||||||
|
echo "icon generated"
|
||||||
|
else
|
||||||
|
echo "no icon svg or rsvg-convert not found, skipping icon"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "done: ${BUNDLE}"
|
||||||
|
echo "to register file types, run: open ${BUNDLE}"
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-cordic"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "CORDIC compiler and evaluator — pure shift-and-add arithmetic for trig IR"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["cordic", "fixed-point", "arithmetic", "sdf"]
|
||||||
|
categories = ["mathematics", "no-std"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
use cord_trig::{TrigGraph, TrigOp};
|
||||||
|
use crate::ops::*;
|
||||||
|
|
||||||
|
/// A compiled CORDIC program ready for binary serialization or execution.
|
||||||
|
///
|
||||||
|
/// Each instruction produces its result into a slot matching its index.
|
||||||
|
/// The instruction list parallels the TrigGraph node list — compilation
|
||||||
|
/// is a direct 1:1 mapping with constants folded to fixed-point.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CORDICProgram {
|
||||||
|
pub word_bits: u8,
|
||||||
|
pub instructions: Vec<CORDICInstr>,
|
||||||
|
pub output: u32,
|
||||||
|
pub atan_table: Vec<i64>,
|
||||||
|
pub gain: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CompileConfig {
|
||||||
|
pub word_bits: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CompileConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { word_bits: 32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CORDICProgram {
|
||||||
|
/// Compile a TrigGraph into a CORDIC program.
|
||||||
|
///
|
||||||
|
/// Each TrigOp node becomes one CORDICInstr. Constants are converted
|
||||||
|
/// from f64 to fixed-point at compile time. Everything else is a
|
||||||
|
/// direct structural mapping.
|
||||||
|
pub fn compile(graph: &TrigGraph, config: &CompileConfig) -> Self {
|
||||||
|
let frac_bits = config.word_bits - 1;
|
||||||
|
let to_fixed = |val: f64| -> i64 {
|
||||||
|
(val * (1i64 << frac_bits) as f64).round() as i64
|
||||||
|
};
|
||||||
|
|
||||||
|
let instructions: Vec<CORDICInstr> = graph.nodes.iter().map(|op| {
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX => CORDICInstr::InputX,
|
||||||
|
TrigOp::InputY => CORDICInstr::InputY,
|
||||||
|
TrigOp::InputZ => CORDICInstr::InputZ,
|
||||||
|
TrigOp::Const(v) => CORDICInstr::LoadImm(to_fixed(*v)),
|
||||||
|
TrigOp::Add(a, b) => CORDICInstr::Add(*a, *b),
|
||||||
|
TrigOp::Sub(a, b) => CORDICInstr::Sub(*a, *b),
|
||||||
|
TrigOp::Mul(a, b) => CORDICInstr::Mul(*a, *b),
|
||||||
|
TrigOp::Div(a, b) => CORDICInstr::Div(*a, *b),
|
||||||
|
TrigOp::Neg(a) => CORDICInstr::Neg(*a),
|
||||||
|
TrigOp::Abs(a) => CORDICInstr::Abs(*a),
|
||||||
|
TrigOp::Sin(a) => CORDICInstr::Sin(*a),
|
||||||
|
TrigOp::Cos(a) => CORDICInstr::Cos(*a),
|
||||||
|
TrigOp::Tan(a) => CORDICInstr::Tan(*a),
|
||||||
|
TrigOp::Asin(a) => CORDICInstr::Asin(*a),
|
||||||
|
TrigOp::Acos(a) => CORDICInstr::Acos(*a),
|
||||||
|
TrigOp::Atan(a) => CORDICInstr::Atan(*a),
|
||||||
|
TrigOp::Sinh(a) => CORDICInstr::Sinh(*a),
|
||||||
|
TrigOp::Cosh(a) => CORDICInstr::Cosh(*a),
|
||||||
|
TrigOp::Tanh(a) => CORDICInstr::Tanh(*a),
|
||||||
|
TrigOp::Asinh(a) => CORDICInstr::Asinh(*a),
|
||||||
|
TrigOp::Acosh(a) => CORDICInstr::Acosh(*a),
|
||||||
|
TrigOp::Atanh(a) => CORDICInstr::Atanh(*a),
|
||||||
|
TrigOp::Sqrt(a) => CORDICInstr::Sqrt(*a),
|
||||||
|
TrigOp::Exp(a) => CORDICInstr::Exp(*a),
|
||||||
|
TrigOp::Ln(a) => CORDICInstr::Ln(*a),
|
||||||
|
TrigOp::Hypot(a, b) => CORDICInstr::Hypot(*a, *b),
|
||||||
|
TrigOp::Atan2(a, b) => CORDICInstr::Atan2(*a, *b),
|
||||||
|
TrigOp::Min(a, b) => CORDICInstr::Min(*a, *b),
|
||||||
|
TrigOp::Max(a, b) => CORDICInstr::Max(*a, *b),
|
||||||
|
TrigOp::Clamp { val, lo, hi } => CORDICInstr::Clamp {
|
||||||
|
val: *val,
|
||||||
|
lo: *lo,
|
||||||
|
hi: *hi,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
CORDICProgram {
|
||||||
|
word_bits: config.word_bits,
|
||||||
|
instructions,
|
||||||
|
output: graph.output,
|
||||||
|
atan_table: atan_table(config.word_bits),
|
||||||
|
gain: cordic_gain(config.word_bits, frac_bits),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize to binary representation.
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
// Header: magic + word_bits
|
||||||
|
buf.extend_from_slice(b"CORD");
|
||||||
|
buf.push(self.word_bits);
|
||||||
|
|
||||||
|
// Atan table
|
||||||
|
let table_len = self.atan_table.len() as u16;
|
||||||
|
buf.extend_from_slice(&table_len.to_le_bytes());
|
||||||
|
for &val in &self.atan_table {
|
||||||
|
buf.extend_from_slice(&val.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gain
|
||||||
|
buf.extend_from_slice(&self.gain.to_le_bytes());
|
||||||
|
|
||||||
|
// Output slot
|
||||||
|
buf.extend_from_slice(&self.output.to_le_bytes());
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
let instr_len = self.instructions.len() as u32;
|
||||||
|
buf.extend_from_slice(&instr_len.to_le_bytes());
|
||||||
|
for instr in &self.instructions {
|
||||||
|
encode_instruction(&mut buf, instr);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize from binary.
|
||||||
|
pub fn from_bytes(data: &[u8]) -> Option<Self> {
|
||||||
|
if data.len() < 5 || &data[0..4] != b"CORD" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let word_bits = data[4];
|
||||||
|
let mut pos = 5;
|
||||||
|
|
||||||
|
// Atan table
|
||||||
|
let table_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
|
||||||
|
pos += 2;
|
||||||
|
let mut atan_table = Vec::with_capacity(table_len);
|
||||||
|
for _ in 0..table_len {
|
||||||
|
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
|
||||||
|
atan_table.push(val);
|
||||||
|
pos += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gain
|
||||||
|
let gain = i64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
|
||||||
|
pos += 8;
|
||||||
|
|
||||||
|
// Output slot
|
||||||
|
let output = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
let instr_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
|
||||||
|
pos += 4;
|
||||||
|
let mut instructions = Vec::with_capacity(instr_len);
|
||||||
|
for _ in 0..instr_len {
|
||||||
|
let (instr, consumed) = decode_instruction(&data[pos..])?;
|
||||||
|
instructions.push(instr);
|
||||||
|
pos += consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(CORDICProgram { word_bits, instructions, output, atan_table, gain })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
use cord_trig::{TrigGraph, TrigOp};
|
||||||
|
|
||||||
|
/// CORDIC evaluator: evaluates a TrigGraph using only integer
|
||||||
|
/// shifts, adds, and comparisons. No floating point trig.
|
||||||
|
///
|
||||||
|
/// Proof that the entire pipeline compiles down to
|
||||||
|
/// binary arithmetic — shift, add, compare, repeat.
|
||||||
|
pub struct CORDICEvaluator {
|
||||||
|
word_bits: u8,
|
||||||
|
frac_bits: u8,
|
||||||
|
atan_table: Vec<i64>,
|
||||||
|
gain: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CORDICEvaluator {
|
||||||
|
pub fn new(word_bits: u8) -> Self {
|
||||||
|
let frac_bits = word_bits - 1;
|
||||||
|
let iterations = word_bits;
|
||||||
|
|
||||||
|
// Precompute atan(2^-i) as fixed-point
|
||||||
|
let atan_table: Vec<i64> = (0..iterations)
|
||||||
|
.map(|i| {
|
||||||
|
let angle = (2.0f64).powi(-(i as i32)).atan();
|
||||||
|
(angle * (1i64 << frac_bits) as f64).round() as i64
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// CORDIC gain K = product of 1/sqrt(1 + 2^{-2i})
|
||||||
|
let mut k = 1.0f64;
|
||||||
|
for i in 0..iterations {
|
||||||
|
k *= 1.0 / (1.0 + (2.0f64).powi(-2 * i as i32)).sqrt();
|
||||||
|
}
|
||||||
|
let gain = (k * (1i64 << frac_bits) as f64).round() as i64;
|
||||||
|
|
||||||
|
CORDICEvaluator { word_bits, frac_bits, atan_table, gain }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert f64 to fixed-point.
|
||||||
|
fn to_fixed(&self, val: f64) -> i64 {
|
||||||
|
(val * (1i64 << self.frac_bits) as f64).round() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert fixed-point back to f64.
|
||||||
|
fn to_float(&self, val: i64) -> f64 {
|
||||||
|
val as f64 / (1i64 << self.frac_bits) as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixed-point multiply: (a * b) >> frac_bits
|
||||||
|
fn fixed_mul(&self, a: i64, b: i64) -> i64 {
|
||||||
|
((a as i128 * b as i128) >> self.frac_bits) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixed-point square root via Newton's method.
|
||||||
|
fn fixed_sqrt(&self, val: i64) -> i64 {
|
||||||
|
if val <= 0 { return 0; }
|
||||||
|
// Initial guess: convert to float, sqrt, convert back
|
||||||
|
let mut x = self.to_fixed(self.to_float(val).sqrt());
|
||||||
|
if x <= 0 { x = 1; }
|
||||||
|
// Two Newton iterations for refinement
|
||||||
|
for _ in 0..2 {
|
||||||
|
let div = self.fixed_div(val, x);
|
||||||
|
x = (x + div) >> 1;
|
||||||
|
}
|
||||||
|
x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixed-point divide: (a << frac_bits) / b
|
||||||
|
fn fixed_div(&self, a: i64, b: i64) -> i64 {
|
||||||
|
if b == 0 {
|
||||||
|
return if a >= 0 { i64::MAX } else { i64::MIN };
|
||||||
|
}
|
||||||
|
(((a as i128) << self.frac_bits) / b as i128) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CORDIC rotation mode: given angle z, compute (cos(z), sin(z)).
|
||||||
|
/// Input z is fixed-point radians.
|
||||||
|
/// Returns (x, y) = (cos(z), sin(z)) in fixed-point.
|
||||||
|
///
|
||||||
|
/// Algorithm:
|
||||||
|
/// Start with x = K (gain), y = 0, z = angle
|
||||||
|
/// For each iteration i:
|
||||||
|
/// if z >= 0: rotate positive (d = +1)
|
||||||
|
/// else: rotate negative (d = -1)
|
||||||
|
/// x_new = x - d * (y >> i)
|
||||||
|
/// y_new = y + d * (x >> i)
|
||||||
|
/// z_new = z - d * atan(2^-i)
|
||||||
|
fn cordic_rotation(&self, angle: i64) -> (i64, i64) {
|
||||||
|
let mut x = self.gain;
|
||||||
|
let mut y: i64 = 0;
|
||||||
|
let mut z = angle;
|
||||||
|
|
||||||
|
for i in 0..self.word_bits as usize {
|
||||||
|
let d = if z >= 0 { 1i64 } else { -1 };
|
||||||
|
let x_new = x - d * (y >> i);
|
||||||
|
let y_new = y + d * (x >> i);
|
||||||
|
z -= d * self.atan_table[i];
|
||||||
|
x = x_new;
|
||||||
|
y = y_new;
|
||||||
|
}
|
||||||
|
|
||||||
|
(x, y) // (cos, sin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CORDIC vectoring mode: given (x, y), compute magnitude and angle.
|
||||||
|
/// Returns (magnitude, angle) in fixed-point.
|
||||||
|
///
|
||||||
|
/// Algorithm:
|
||||||
|
/// Start with x, y, z = 0
|
||||||
|
/// For each iteration i:
|
||||||
|
/// if y < 0: rotate positive (d = +1)
|
||||||
|
/// else: rotate negative (d = -1)
|
||||||
|
/// x_new = x - d * (y >> i)
|
||||||
|
/// y_new = y + d * (x >> i)
|
||||||
|
/// z_new = z - d * atan(2^-i)
|
||||||
|
/// Result: x ≈ sqrt(x₀² + y₀²) / K, z ≈ atan2(y₀, x₀)
|
||||||
|
fn cordic_vectoring(&self, x_in: i64, y_in: i64) -> (i64, i64) {
|
||||||
|
let mut x = x_in;
|
||||||
|
let mut y = y_in;
|
||||||
|
let mut z: i64 = 0;
|
||||||
|
|
||||||
|
// Handle negative x by reflecting into right half-plane
|
||||||
|
let negate_x = x < 0;
|
||||||
|
if negate_x {
|
||||||
|
x = -x;
|
||||||
|
y = -y;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..self.word_bits as usize {
|
||||||
|
let d = if y < 0 { 1i64 } else { -1 };
|
||||||
|
let x_new = x - d * (y >> i);
|
||||||
|
let y_new = y + d * (x >> i);
|
||||||
|
z -= d * self.atan_table[i];
|
||||||
|
x = x_new;
|
||||||
|
y = y_new;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vectoring output: x_final = (1/K) * sqrt(x0^2 + y0^2).
|
||||||
|
// self.gain stores K (~0.6073). Multiply to recover true magnitude.
|
||||||
|
let magnitude = self.fixed_mul(x, self.gain);
|
||||||
|
|
||||||
|
if negate_x {
|
||||||
|
let pi = self.to_fixed(std::f64::consts::PI);
|
||||||
|
let angle = if z >= 0 { pi - z } else { -pi - z };
|
||||||
|
(magnitude, angle)
|
||||||
|
} else {
|
||||||
|
(magnitude, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate the entire trig graph using only CORDIC operations.
|
||||||
|
/// Returns the output as f64 (converted from fixed-point at the end).
|
||||||
|
pub fn evaluate(&self, graph: &TrigGraph, x: f64, y: f64, z: f64) -> f64 {
|
||||||
|
let mut vals = vec![0i64; graph.nodes.len()];
|
||||||
|
|
||||||
|
for (i, op) in graph.nodes.iter().enumerate() {
|
||||||
|
vals[i] = match op {
|
||||||
|
TrigOp::InputX => self.to_fixed(x),
|
||||||
|
TrigOp::InputY => self.to_fixed(y),
|
||||||
|
TrigOp::InputZ => self.to_fixed(z),
|
||||||
|
TrigOp::Const(c) => self.to_fixed(*c),
|
||||||
|
|
||||||
|
TrigOp::Add(a, b) => vals[*a as usize] + vals[*b as usize],
|
||||||
|
TrigOp::Sub(a, b) => vals[*a as usize] - vals[*b as usize],
|
||||||
|
TrigOp::Mul(a, b) => self.fixed_mul(vals[*a as usize], vals[*b as usize]),
|
||||||
|
TrigOp::Div(a, b) => self.fixed_div(vals[*a as usize], vals[*b as usize]),
|
||||||
|
TrigOp::Neg(a) => -vals[*a as usize],
|
||||||
|
TrigOp::Abs(a) => vals[*a as usize].abs(),
|
||||||
|
|
||||||
|
TrigOp::Sin(a) => {
|
||||||
|
let (_, sin) = self.cordic_rotation(vals[*a as usize]);
|
||||||
|
sin
|
||||||
|
}
|
||||||
|
TrigOp::Cos(a) => {
|
||||||
|
let (cos, _) = self.cordic_rotation(vals[*a as usize]);
|
||||||
|
cos
|
||||||
|
}
|
||||||
|
TrigOp::Tan(a) => {
|
||||||
|
let (cos, sin) = self.cordic_rotation(vals[*a as usize]);
|
||||||
|
self.fixed_div(sin, cos)
|
||||||
|
}
|
||||||
|
TrigOp::Asin(a) => {
|
||||||
|
// asin(x) = atan2(x, sqrt(1-x²))
|
||||||
|
let x = vals[*a as usize];
|
||||||
|
let one = self.to_fixed(1.0);
|
||||||
|
let x2 = self.fixed_mul(x, x);
|
||||||
|
let rem = one - x2;
|
||||||
|
let sqrt_rem = self.fixed_sqrt(rem);
|
||||||
|
let (_, angle) = self.cordic_vectoring(sqrt_rem, x);
|
||||||
|
angle
|
||||||
|
}
|
||||||
|
TrigOp::Acos(a) => {
|
||||||
|
// acos(x) = atan2(sqrt(1-x²), x)
|
||||||
|
let x = vals[*a as usize];
|
||||||
|
let one = self.to_fixed(1.0);
|
||||||
|
let x2 = self.fixed_mul(x, x);
|
||||||
|
let rem = one - x2;
|
||||||
|
let sqrt_rem = self.fixed_sqrt(rem);
|
||||||
|
let (_, angle) = self.cordic_vectoring(x, sqrt_rem);
|
||||||
|
angle
|
||||||
|
}
|
||||||
|
TrigOp::Atan(a) => {
|
||||||
|
let one = self.to_fixed(1.0);
|
||||||
|
let (_, angle) = self.cordic_vectoring(one, vals[*a as usize]);
|
||||||
|
angle
|
||||||
|
}
|
||||||
|
TrigOp::Sinh(a) => self.to_fixed(self.to_float(vals[*a as usize]).sinh()),
|
||||||
|
TrigOp::Cosh(a) => self.to_fixed(self.to_float(vals[*a as usize]).cosh()),
|
||||||
|
TrigOp::Tanh(a) => self.to_fixed(self.to_float(vals[*a as usize]).tanh()),
|
||||||
|
TrigOp::Asinh(a) => self.to_fixed(self.to_float(vals[*a as usize]).asinh()),
|
||||||
|
TrigOp::Acosh(a) => self.to_fixed(self.to_float(vals[*a as usize]).acosh()),
|
||||||
|
TrigOp::Atanh(a) => self.to_fixed(self.to_float(vals[*a as usize]).atanh()),
|
||||||
|
TrigOp::Sqrt(a) => self.fixed_sqrt(vals[*a as usize]),
|
||||||
|
TrigOp::Exp(a) => self.to_fixed(self.to_float(vals[*a as usize]).exp()),
|
||||||
|
TrigOp::Ln(a) => self.to_fixed(self.to_float(vals[*a as usize]).ln()),
|
||||||
|
|
||||||
|
TrigOp::Hypot(a, b) => {
|
||||||
|
let (mag, _) = self.cordic_vectoring(vals[*a as usize], vals[*b as usize]);
|
||||||
|
mag
|
||||||
|
}
|
||||||
|
TrigOp::Atan2(a, b) => {
|
||||||
|
let (_, angle) = self.cordic_vectoring(vals[*b as usize], vals[*a as usize]);
|
||||||
|
angle
|
||||||
|
}
|
||||||
|
|
||||||
|
TrigOp::Min(a, b) => vals[*a as usize].min(vals[*b as usize]),
|
||||||
|
TrigOp::Max(a, b) => vals[*a as usize].max(vals[*b as usize]),
|
||||||
|
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
vals[*val as usize].clamp(vals[*lo as usize], vals[*hi as usize])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.to_float(vals[graph.output as usize])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cord_trig::ir::TrigOp;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sin_cos() {
|
||||||
|
let eval = CORDICEvaluator::new(32);
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let angle = g.push(TrigOp::Const(std::f64::consts::FRAC_PI_4));
|
||||||
|
let sin_node = g.push(TrigOp::Sin(angle));
|
||||||
|
g.set_output(sin_node);
|
||||||
|
|
||||||
|
let result = eval.evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
let expected = std::f64::consts::FRAC_PI_4.sin();
|
||||||
|
assert!((result - expected).abs() < 0.001,
|
||||||
|
"sin(π/4): CORDIC={result}, expected={expected}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hypot() {
|
||||||
|
let eval = CORDICEvaluator::new(32);
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let a = g.push(TrigOp::Const(3.0));
|
||||||
|
let b = g.push(TrigOp::Const(4.0));
|
||||||
|
let h = g.push(TrigOp::Hypot(a, b));
|
||||||
|
g.set_output(h);
|
||||||
|
|
||||||
|
let result = eval.evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
assert!((result - 5.0).abs() < 0.01,
|
||||||
|
"hypot(3,4): CORDIC={result}, expected=5.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sphere_sdf() {
|
||||||
|
let eval = CORDICEvaluator::new(32);
|
||||||
|
let f64_eval = cord_trig::eval::evaluate;
|
||||||
|
|
||||||
|
let mut builder = cord_trig::lower::SdfBuilder::new();
|
||||||
|
let p = builder.root_point();
|
||||||
|
let dist = builder.sphere(p, 5.0);
|
||||||
|
let graph = builder.finish(dist);
|
||||||
|
|
||||||
|
// Point on surface
|
||||||
|
let cordic_val = eval.evaluate(&graph, 5.0, 0.0, 0.0);
|
||||||
|
let float_val = f64_eval(&graph, 5.0, 0.0, 0.0);
|
||||||
|
assert!((cordic_val - float_val).abs() < 0.01,
|
||||||
|
"sphere surface: CORDIC={cordic_val}, f64={float_val}");
|
||||||
|
|
||||||
|
// Point inside
|
||||||
|
let cordic_val = eval.evaluate(&graph, 2.0, 1.0, 1.0);
|
||||||
|
let float_val = f64_eval(&graph, 2.0, 1.0, 1.0);
|
||||||
|
assert!((cordic_val - float_val).abs() < 0.1,
|
||||||
|
"sphere inside: CORDIC={cordic_val}, f64={float_val}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//! CORDIC compiler and evaluator for TrigGraph IR.
|
||||||
|
//!
|
||||||
|
//! Compiles a [`cord_trig::TrigGraph`] into a sequence of CORDIC instructions
|
||||||
|
//! that evaluate using only shifts, adds, and a precomputed angle table.
|
||||||
|
//! Zero floating-point operations in the evaluation path.
|
||||||
|
//!
|
||||||
|
//! Supports configurable word widths (8–64 bit). At 32 bits, error vs f64
|
||||||
|
//! reference is typically zero at the precision boundary.
|
||||||
|
|
||||||
|
pub mod compiler;
|
||||||
|
pub mod ops;
|
||||||
|
pub mod eval;
|
||||||
|
|
||||||
|
pub use compiler::CORDICProgram;
|
||||||
|
pub use eval::CORDICEvaluator;
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
/// A compiled CORDIC instruction operating on indexed slots.
|
||||||
|
///
|
||||||
|
/// Slot i holds the result of instruction i. All operand references
|
||||||
|
/// point to earlier slots (j < i), matching the TrigGraph's topological order.
|
||||||
|
///
|
||||||
|
/// CORDIC mapping:
|
||||||
|
/// Sin, Cos → rotation mode (angle → sin/cos via shift-and-add)
|
||||||
|
/// Hypot → vectoring mode (magnitude via shift-and-add)
|
||||||
|
/// Atan2 → vectoring mode (angle via shift-and-add)
|
||||||
|
/// Mul → fixed-point multiply (shift-and-add)
|
||||||
|
/// Add, Sub, Neg, Abs, Min, Max, Clamp → direct binary ops
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CORDICInstr {
|
||||||
|
InputX,
|
||||||
|
InputY,
|
||||||
|
InputZ,
|
||||||
|
LoadImm(i64),
|
||||||
|
|
||||||
|
Add(u32, u32),
|
||||||
|
Sub(u32, u32),
|
||||||
|
Mul(u32, u32),
|
||||||
|
Div(u32, u32),
|
||||||
|
Neg(u32),
|
||||||
|
Abs(u32),
|
||||||
|
|
||||||
|
Sin(u32),
|
||||||
|
Cos(u32),
|
||||||
|
Tan(u32),
|
||||||
|
|
||||||
|
Asin(u32),
|
||||||
|
Acos(u32),
|
||||||
|
Atan(u32),
|
||||||
|
|
||||||
|
Sinh(u32),
|
||||||
|
Cosh(u32),
|
||||||
|
Tanh(u32),
|
||||||
|
|
||||||
|
Asinh(u32),
|
||||||
|
Acosh(u32),
|
||||||
|
Atanh(u32),
|
||||||
|
|
||||||
|
Sqrt(u32),
|
||||||
|
Exp(u32),
|
||||||
|
Ln(u32),
|
||||||
|
|
||||||
|
Hypot(u32, u32),
|
||||||
|
Atan2(u32, u32),
|
||||||
|
|
||||||
|
Min(u32, u32),
|
||||||
|
Max(u32, u32),
|
||||||
|
Clamp { val: u32, lo: u32, hi: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Precomputed arctan table for CORDIC iterations.
|
||||||
|
/// atan(2^-i) in fixed-point with the given number of fractional bits.
|
||||||
|
pub fn atan_table(word_bits: u8) -> Vec<i64> {
|
||||||
|
let frac_bits = word_bits - 1;
|
||||||
|
(0..word_bits)
|
||||||
|
.map(|i| {
|
||||||
|
let angle = (2.0f64).powi(-(i as i32)).atan();
|
||||||
|
(angle * (1i64 << frac_bits) as f64).round() as i64
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CORDIC gain constant K_n = prod(1/sqrt(1 + 2^{-2i})) for n iterations.
|
||||||
|
pub fn cordic_gain(iterations: u8, frac_bits: u8) -> i64 {
|
||||||
|
let mut k = 1.0f64;
|
||||||
|
for i in 0..iterations {
|
||||||
|
k *= 1.0 / (1.0 + (2.0f64).powi(-2 * i as i32)).sqrt();
|
||||||
|
}
|
||||||
|
(k * (1i64 << frac_bits) as f64).round() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary encoding
|
||||||
|
|
||||||
|
const OP_INPUT_X: u8 = 0x00;
|
||||||
|
const OP_INPUT_Y: u8 = 0x01;
|
||||||
|
const OP_INPUT_Z: u8 = 0x02;
|
||||||
|
const OP_LOAD_IMM: u8 = 0x03;
|
||||||
|
const OP_ADD: u8 = 0x04;
|
||||||
|
const OP_SUB: u8 = 0x05;
|
||||||
|
const OP_MUL: u8 = 0x06;
|
||||||
|
const OP_DIV: u8 = 0x10;
|
||||||
|
const OP_NEG: u8 = 0x07;
|
||||||
|
const OP_ABS: u8 = 0x08;
|
||||||
|
const OP_SIN: u8 = 0x09;
|
||||||
|
const OP_COS: u8 = 0x0A;
|
||||||
|
const OP_HYPOT: u8 = 0x0B;
|
||||||
|
const OP_ATAN2: u8 = 0x0C;
|
||||||
|
const OP_MIN: u8 = 0x0D;
|
||||||
|
const OP_MAX: u8 = 0x0E;
|
||||||
|
const OP_CLAMP: u8 = 0x0F;
|
||||||
|
const OP_TAN: u8 = 0x11;
|
||||||
|
const OP_ASIN: u8 = 0x12;
|
||||||
|
const OP_ACOS: u8 = 0x13;
|
||||||
|
const OP_ATAN: u8 = 0x14;
|
||||||
|
const OP_SINH: u8 = 0x15;
|
||||||
|
const OP_COSH: u8 = 0x16;
|
||||||
|
const OP_TANH: u8 = 0x17;
|
||||||
|
const OP_ASINH: u8 = 0x18;
|
||||||
|
const OP_ACOSH: u8 = 0x19;
|
||||||
|
const OP_ATANH: u8 = 0x1A;
|
||||||
|
const OP_SQRT: u8 = 0x1B;
|
||||||
|
const OP_EXP: u8 = 0x1C;
|
||||||
|
const OP_LN: u8 = 0x1D;
|
||||||
|
|
||||||
|
pub fn encode_instruction(buf: &mut Vec<u8>, instr: &CORDICInstr) {
|
||||||
|
match instr {
|
||||||
|
CORDICInstr::InputX => buf.push(OP_INPUT_X),
|
||||||
|
CORDICInstr::InputY => buf.push(OP_INPUT_Y),
|
||||||
|
CORDICInstr::InputZ => buf.push(OP_INPUT_Z),
|
||||||
|
CORDICInstr::LoadImm(v) => {
|
||||||
|
buf.push(OP_LOAD_IMM);
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Add(a, b) => {
|
||||||
|
buf.push(OP_ADD);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Sub(a, b) => {
|
||||||
|
buf.push(OP_SUB);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Mul(a, b) => {
|
||||||
|
buf.push(OP_MUL);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Div(a, b) => {
|
||||||
|
buf.push(OP_DIV);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Neg(a) => {
|
||||||
|
buf.push(OP_NEG);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Abs(a) => {
|
||||||
|
buf.push(OP_ABS);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Sin(a) => {
|
||||||
|
buf.push(OP_SIN);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Cos(a) => {
|
||||||
|
buf.push(OP_COS);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Tan(a) => { buf.push(OP_TAN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Asin(a) => { buf.push(OP_ASIN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Acos(a) => { buf.push(OP_ACOS); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Atan(a) => { buf.push(OP_ATAN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Sinh(a) => { buf.push(OP_SINH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Cosh(a) => { buf.push(OP_COSH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Tanh(a) => { buf.push(OP_TANH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Asinh(a) => { buf.push(OP_ASINH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Acosh(a) => { buf.push(OP_ACOSH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Atanh(a) => { buf.push(OP_ATANH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Sqrt(a) => { buf.push(OP_SQRT); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Exp(a) => { buf.push(OP_EXP); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Ln(a) => { buf.push(OP_LN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
CORDICInstr::Hypot(a, b) => {
|
||||||
|
buf.push(OP_HYPOT);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Atan2(a, b) => {
|
||||||
|
buf.push(OP_ATAN2);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Min(a, b) => {
|
||||||
|
buf.push(OP_MIN);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Max(a, b) => {
|
||||||
|
buf.push(OP_MAX);
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
CORDICInstr::Clamp { val, lo, hi } => {
|
||||||
|
buf.push(OP_CLAMP);
|
||||||
|
buf.extend_from_slice(&val.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&lo.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&hi.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], pos: usize) -> Option<u32> {
|
||||||
|
Some(u32::from_le_bytes(data.get(pos..pos + 4)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_i64(data: &[u8], pos: usize) -> Option<i64> {
|
||||||
|
Some(i64::from_le_bytes(data.get(pos..pos + 8)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_instruction(data: &[u8]) -> Option<(CORDICInstr, usize)> {
|
||||||
|
let op = *data.first()?;
|
||||||
|
match op {
|
||||||
|
OP_INPUT_X => Some((CORDICInstr::InputX, 1)),
|
||||||
|
OP_INPUT_Y => Some((CORDICInstr::InputY, 1)),
|
||||||
|
OP_INPUT_Z => Some((CORDICInstr::InputZ, 1)),
|
||||||
|
OP_LOAD_IMM => {
|
||||||
|
let v = read_i64(data, 1)?;
|
||||||
|
Some((CORDICInstr::LoadImm(v), 9))
|
||||||
|
}
|
||||||
|
OP_ADD => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Add(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_SUB => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Sub(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_MUL => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Mul(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_DIV => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Div(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_NEG => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
Some((CORDICInstr::Neg(a), 5))
|
||||||
|
}
|
||||||
|
OP_ABS => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
Some((CORDICInstr::Abs(a), 5))
|
||||||
|
}
|
||||||
|
OP_SIN => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
Some((CORDICInstr::Sin(a), 5))
|
||||||
|
}
|
||||||
|
OP_COS => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
Some((CORDICInstr::Cos(a), 5))
|
||||||
|
}
|
||||||
|
OP_TAN => { let a = read_u32(data, 1)?; Some((CORDICInstr::Tan(a), 5)) }
|
||||||
|
OP_ASIN => { let a = read_u32(data, 1)?; Some((CORDICInstr::Asin(a), 5)) }
|
||||||
|
OP_ACOS => { let a = read_u32(data, 1)?; Some((CORDICInstr::Acos(a), 5)) }
|
||||||
|
OP_ATAN => { let a = read_u32(data, 1)?; Some((CORDICInstr::Atan(a), 5)) }
|
||||||
|
OP_SINH => { let a = read_u32(data, 1)?; Some((CORDICInstr::Sinh(a), 5)) }
|
||||||
|
OP_COSH => { let a = read_u32(data, 1)?; Some((CORDICInstr::Cosh(a), 5)) }
|
||||||
|
OP_TANH => { let a = read_u32(data, 1)?; Some((CORDICInstr::Tanh(a), 5)) }
|
||||||
|
OP_ASINH => { let a = read_u32(data, 1)?; Some((CORDICInstr::Asinh(a), 5)) }
|
||||||
|
OP_ACOSH => { let a = read_u32(data, 1)?; Some((CORDICInstr::Acosh(a), 5)) }
|
||||||
|
OP_ATANH => { let a = read_u32(data, 1)?; Some((CORDICInstr::Atanh(a), 5)) }
|
||||||
|
OP_SQRT => { let a = read_u32(data, 1)?; Some((CORDICInstr::Sqrt(a), 5)) }
|
||||||
|
OP_EXP => { let a = read_u32(data, 1)?; Some((CORDICInstr::Exp(a), 5)) }
|
||||||
|
OP_LN => { let a = read_u32(data, 1)?; Some((CORDICInstr::Ln(a), 5)) }
|
||||||
|
OP_HYPOT => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Hypot(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_ATAN2 => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Atan2(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_MIN => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Min(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_MAX => {
|
||||||
|
let a = read_u32(data, 1)?;
|
||||||
|
let b = read_u32(data, 5)?;
|
||||||
|
Some((CORDICInstr::Max(a, b), 9))
|
||||||
|
}
|
||||||
|
OP_CLAMP => {
|
||||||
|
let val = read_u32(data, 1)?;
|
||||||
|
let lo = read_u32(data, 5)?;
|
||||||
|
let hi = read_u32(data, 9)?;
|
||||||
|
Some((CORDICInstr::Clamp { val, lo, hi }, 13))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-decompile"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Mesh decompiler — STL/OBJ/3MF to SDF tree via RANSAC and monogenic classification"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["decompile", "mesh", "sdf", "stl", "reverse-engineering"]
|
||||||
|
categories = ["graphics", "mathematics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-sdf = { path = "../cord-sdf" }
|
||||||
|
cord-riesz = { path = "../cord-riesz" }
|
||||||
|
anyhow = "1"
|
||||||
|
threemf = "0.8"
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
use crate::mesh::{AABB, Triangle, TriangleMesh, Vec3};
|
||||||
|
|
||||||
|
/// BVH over a triangle mesh for fast nearest-point queries.
|
||||||
|
pub struct BVH {
|
||||||
|
nodes: Vec<BVHNode>,
|
||||||
|
tri_indices: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BVHNode {
|
||||||
|
Leaf {
|
||||||
|
bounds: AABB,
|
||||||
|
first: usize,
|
||||||
|
count: usize,
|
||||||
|
},
|
||||||
|
Internal {
|
||||||
|
bounds: AABB,
|
||||||
|
left: usize,
|
||||||
|
right: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BVH {
|
||||||
|
pub fn build(mesh: &TriangleMesh) -> Self {
|
||||||
|
let n = mesh.triangles.len();
|
||||||
|
let mut tri_indices: Vec<usize> = (0..n).collect();
|
||||||
|
let mut centroids: Vec<Vec3> = mesh.triangles.iter().map(|t| t.centroid()).collect();
|
||||||
|
let mut nodes = Vec::with_capacity(2 * n);
|
||||||
|
|
||||||
|
build_recursive(
|
||||||
|
&mesh.triangles,
|
||||||
|
&mut tri_indices,
|
||||||
|
&mut centroids,
|
||||||
|
&mut nodes,
|
||||||
|
0,
|
||||||
|
n,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { nodes, tri_indices }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the signed distance from point p to the mesh.
|
||||||
|
/// Sign is determined by the normal of the nearest triangle.
|
||||||
|
pub fn signed_distance(&self, mesh: &TriangleMesh, p: Vec3) -> f64 {
|
||||||
|
let mut best_dist_sq = f64::INFINITY;
|
||||||
|
let mut best_sign = 1.0f64;
|
||||||
|
self.query_nearest(mesh, p, 0, &mut best_dist_sq, &mut best_sign);
|
||||||
|
best_dist_sq.sqrt() * best_sign
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the nearest triangle index and unsigned distance.
|
||||||
|
pub fn nearest_triangle(&self, mesh: &TriangleMesh, p: Vec3) -> (usize, f64) {
|
||||||
|
let mut best_dist_sq = f64::INFINITY;
|
||||||
|
let mut best_idx = 0;
|
||||||
|
self.query_nearest_idx(mesh, p, 0, &mut best_dist_sq, &mut best_idx);
|
||||||
|
(best_idx, best_dist_sq.sqrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_nearest(
|
||||||
|
&self,
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
p: Vec3,
|
||||||
|
node_idx: usize,
|
||||||
|
best_dist_sq: &mut f64,
|
||||||
|
best_sign: &mut f64,
|
||||||
|
) {
|
||||||
|
match &self.nodes[node_idx] {
|
||||||
|
BVHNode::Leaf { bounds, first, count } => {
|
||||||
|
if bounds.distance_to_point(p).powi(2) > *best_dist_sq {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for i in *first..(*first + *count) {
|
||||||
|
let tri_idx = self.tri_indices[i];
|
||||||
|
let tri = &mesh.triangles[tri_idx];
|
||||||
|
let (closest, dist) = tri.closest_point(p);
|
||||||
|
let dist_sq = dist * dist;
|
||||||
|
if dist_sq < *best_dist_sq {
|
||||||
|
*best_dist_sq = dist_sq;
|
||||||
|
let to_point = p - closest;
|
||||||
|
let normal = tri.normal();
|
||||||
|
*best_sign = if to_point.dot(normal) >= 0.0 { 1.0 } else { -1.0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BVHNode::Internal { bounds, left, right } => {
|
||||||
|
if bounds.distance_to_point(p).powi(2) > *best_dist_sq {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let left_bounds = node_bounds(&self.nodes[*left]);
|
||||||
|
let right_bounds = node_bounds(&self.nodes[*right]);
|
||||||
|
let dl = left_bounds.distance_to_point(p);
|
||||||
|
let dr = right_bounds.distance_to_point(p);
|
||||||
|
|
||||||
|
if dl < dr {
|
||||||
|
self.query_nearest(mesh, p, *left, best_dist_sq, best_sign);
|
||||||
|
self.query_nearest(mesh, p, *right, best_dist_sq, best_sign);
|
||||||
|
} else {
|
||||||
|
self.query_nearest(mesh, p, *right, best_dist_sq, best_sign);
|
||||||
|
self.query_nearest(mesh, p, *left, best_dist_sq, best_sign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_nearest_idx(
|
||||||
|
&self,
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
p: Vec3,
|
||||||
|
node_idx: usize,
|
||||||
|
best_dist_sq: &mut f64,
|
||||||
|
best_idx: &mut usize,
|
||||||
|
) {
|
||||||
|
match &self.nodes[node_idx] {
|
||||||
|
BVHNode::Leaf { bounds, first, count } => {
|
||||||
|
if bounds.distance_to_point(p).powi(2) > *best_dist_sq {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for i in *first..(*first + *count) {
|
||||||
|
let tri_idx = self.tri_indices[i];
|
||||||
|
let tri = &mesh.triangles[tri_idx];
|
||||||
|
let (_, dist) = tri.closest_point(p);
|
||||||
|
let dist_sq = dist * dist;
|
||||||
|
if dist_sq < *best_dist_sq {
|
||||||
|
*best_dist_sq = dist_sq;
|
||||||
|
*best_idx = tri_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BVHNode::Internal { bounds, left, right } => {
|
||||||
|
if bounds.distance_to_point(p).powi(2) > *best_dist_sq {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let left_bounds = node_bounds(&self.nodes[*left]);
|
||||||
|
let right_bounds = node_bounds(&self.nodes[*right]);
|
||||||
|
let dl = left_bounds.distance_to_point(p);
|
||||||
|
let dr = right_bounds.distance_to_point(p);
|
||||||
|
|
||||||
|
if dl < dr {
|
||||||
|
self.query_nearest_idx(mesh, p, *left, best_dist_sq, best_idx);
|
||||||
|
self.query_nearest_idx(mesh, p, *right, best_dist_sq, best_idx);
|
||||||
|
} else {
|
||||||
|
self.query_nearest_idx(mesh, p, *right, best_dist_sq, best_idx);
|
||||||
|
self.query_nearest_idx(mesh, p, *left, best_dist_sq, best_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count triangles whose centroid falls within a given AABB.
|
||||||
|
pub fn count_in_region(&self, mesh: &TriangleMesh, region: &AABB) -> usize {
|
||||||
|
self.count_recursive(mesh, region, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_recursive(&self, mesh: &TriangleMesh, region: &AABB, node_idx: usize) -> usize {
|
||||||
|
match &self.nodes[node_idx] {
|
||||||
|
BVHNode::Leaf { bounds, first, count } => {
|
||||||
|
if !aabb_overlaps(bounds, region) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let mut n = 0;
|
||||||
|
for i in *first..(*first + *count) {
|
||||||
|
let c = mesh.triangles[self.tri_indices[i]].centroid();
|
||||||
|
if point_in_aabb(c, region) {
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n
|
||||||
|
}
|
||||||
|
BVHNode::Internal { bounds, left, right } => {
|
||||||
|
if !aabb_overlaps(bounds, region) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.count_recursive(mesh, region, *left)
|
||||||
|
+ self.count_recursive(mesh, region, *right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_bounds(node: &BVHNode) -> &AABB {
|
||||||
|
match node {
|
||||||
|
BVHNode::Leaf { bounds, .. } | BVHNode::Internal { bounds, .. } => bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aabb_overlaps(a: &AABB, b: &AABB) -> bool {
|
||||||
|
a.min.x <= b.max.x && a.max.x >= b.min.x
|
||||||
|
&& a.min.y <= b.max.y && a.max.y >= b.min.y
|
||||||
|
&& a.min.z <= b.max.z && a.max.z >= b.min.z
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_in_aabb(p: Vec3, b: &AABB) -> bool {
|
||||||
|
p.x >= b.min.x && p.x <= b.max.x
|
||||||
|
&& p.y >= b.min.y && p.y <= b.max.y
|
||||||
|
&& p.z >= b.min.z && p.z <= b.max.z
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_LEAF_SIZE: usize = 8;
|
||||||
|
|
||||||
|
fn build_recursive(
|
||||||
|
triangles: &[Triangle],
|
||||||
|
indices: &mut [usize],
|
||||||
|
centroids: &mut [Vec3],
|
||||||
|
nodes: &mut Vec<BVHNode>,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
) -> usize {
|
||||||
|
let count = end - start;
|
||||||
|
|
||||||
|
// Compute bounds
|
||||||
|
let mut bounds = AABB::empty();
|
||||||
|
for &idx in &indices[start..end] {
|
||||||
|
bounds = bounds.union(&AABB::from_triangle(&triangles[idx]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= MAX_LEAF_SIZE {
|
||||||
|
let node_idx = nodes.len();
|
||||||
|
nodes.push(BVHNode::Leaf { bounds, first: start, count });
|
||||||
|
return node_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split along longest axis at centroid median
|
||||||
|
let axis = bounds.longest_axis();
|
||||||
|
let mid = start + count / 2;
|
||||||
|
|
||||||
|
// Partial sort: partition around the median centroid on the chosen axis
|
||||||
|
let get_axis = |v: Vec3| match axis {
|
||||||
|
0 => v.x,
|
||||||
|
1 => v.y,
|
||||||
|
_ => v.z,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple partition: sort the range by centroid along axis
|
||||||
|
let index_slice = &mut indices[start..end];
|
||||||
|
index_slice.sort_unstable_by(|&a, &b| {
|
||||||
|
let ca = get_axis(centroids[a]);
|
||||||
|
let cb = get_axis(centroids[b]);
|
||||||
|
ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
let node_idx = nodes.len();
|
||||||
|
nodes.push(BVHNode::Leaf { bounds: AABB::empty(), first: 0, count: 0 }); // placeholder
|
||||||
|
|
||||||
|
let left = build_recursive(triangles, indices, centroids, nodes, start, mid);
|
||||||
|
let right = build_recursive(triangles, indices, centroids, nodes, mid, end);
|
||||||
|
|
||||||
|
nodes[node_idx] = BVHNode::Internal { bounds, left, right };
|
||||||
|
node_idx
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::bvh::BVH;
|
||||||
|
use crate::mesh::TriangleMesh;
|
||||||
|
use super::oracle::BvhOracle;
|
||||||
|
use super::scheduler::CrawlerScheduler;
|
||||||
|
use super::{CrawlerConfig, SurfaceHit};
|
||||||
|
|
||||||
|
/// Run the crawler decompiler on CPU.
|
||||||
|
/// Uses the BVH for SDF queries. All crawlers run sequentially per step,
|
||||||
|
/// but SDF queries within each step can be parallelized.
|
||||||
|
pub fn run_cpu(
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
bvh: &BVH,
|
||||||
|
config: CrawlerConfig,
|
||||||
|
) -> Vec<SurfaceHit> {
|
||||||
|
let oracle = BvhOracle { mesh, bvh };
|
||||||
|
let mut scheduler = CrawlerScheduler::new(mesh.bounds, config);
|
||||||
|
scheduler.run(&oracle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run with the voxel oracle (faster per-query but lower precision).
|
||||||
|
/// Useful as intermediate step before GPU path.
|
||||||
|
pub fn run_cpu_voxel(
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
bvh: &BVH,
|
||||||
|
config: CrawlerConfig,
|
||||||
|
voxel_resolution: usize,
|
||||||
|
) -> Vec<SurfaceHit> {
|
||||||
|
let oracle = super::oracle::VoxelOracle::from_mesh(mesh, bvh, voxel_resolution);
|
||||||
|
let mut scheduler = CrawlerScheduler::new(mesh.bounds, config);
|
||||||
|
scheduler.run(&oracle)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
pub mod oracle;
|
||||||
|
pub mod state;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub mod cpu;
|
||||||
|
|
||||||
|
use crate::mesh::Vec3;
|
||||||
|
|
||||||
|
/// Surface sample collected by a crawler.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SurfaceHit {
|
||||||
|
pub position: Vec3,
|
||||||
|
pub normal: Vec3,
|
||||||
|
pub face_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the crawler-based decompiler.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CrawlerConfig {
|
||||||
|
/// Number of initial probes deployed around the bounding sphere.
|
||||||
|
pub initial_probes: usize,
|
||||||
|
/// Number of crawlers deployed per contact point in patrol phase.
|
||||||
|
pub crawlers_per_contact: usize,
|
||||||
|
/// Step size as fraction of bounding sphere radius.
|
||||||
|
pub step_fraction: f64,
|
||||||
|
/// Distance threshold for surface detection.
|
||||||
|
pub surface_epsilon: f64,
|
||||||
|
/// Angle tolerance (radians) for face boundary detection.
|
||||||
|
pub edge_angle_threshold: f64,
|
||||||
|
/// Maximum steps before a crawler gives up.
|
||||||
|
pub max_steps: u32,
|
||||||
|
/// Scan row spacing as fraction of bounding radius.
|
||||||
|
pub scan_spacing: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CrawlerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
initial_probes: 128,
|
||||||
|
crawlers_per_contact: 4,
|
||||||
|
step_fraction: 0.005,
|
||||||
|
surface_epsilon: 1e-4,
|
||||||
|
edge_angle_threshold: 0.3,
|
||||||
|
max_steps: 10000,
|
||||||
|
scan_spacing: 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
use crate::mesh::Vec3;
|
||||||
|
use crate::bvh::BVH;
|
||||||
|
use crate::mesh::TriangleMesh;
|
||||||
|
|
||||||
|
/// An SDF oracle provides signed distance and gradient queries.
|
||||||
|
/// CPU implementation uses BVH; GPU would sample a 3D texture.
|
||||||
|
pub trait SdfOracle: Send + Sync {
|
||||||
|
fn sdf(&self, p: Vec3) -> f64;
|
||||||
|
|
||||||
|
fn gradient(&self, p: Vec3) -> Vec3 {
|
||||||
|
let e = 1e-4;
|
||||||
|
let dx = self.sdf(Vec3::new(p.x + e, p.y, p.z))
|
||||||
|
- self.sdf(Vec3::new(p.x - e, p.y, p.z));
|
||||||
|
let dy = self.sdf(Vec3::new(p.x, p.y + e, p.z))
|
||||||
|
- self.sdf(Vec3::new(p.x, p.y - e, p.z));
|
||||||
|
let dz = self.sdf(Vec3::new(p.x, p.y, p.z + e))
|
||||||
|
- self.sdf(Vec3::new(p.x, p.y, p.z - e));
|
||||||
|
let inv = 1.0 / (2.0 * e);
|
||||||
|
Vec3::new(dx * inv, dy * inv, dz * inv).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_to_surface(&self, p: Vec3, max_iter: u32) -> Option<(Vec3, Vec3)> {
|
||||||
|
let mut pos = p;
|
||||||
|
for _ in 0..max_iter {
|
||||||
|
let d = self.sdf(pos);
|
||||||
|
if d.abs() < 1e-5 {
|
||||||
|
let n = self.gradient(pos);
|
||||||
|
return Some((pos, n));
|
||||||
|
}
|
||||||
|
let g = self.gradient(pos);
|
||||||
|
pos = pos - g * d;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BVH-backed oracle for CPU path.
|
||||||
|
pub struct BvhOracle<'a> {
|
||||||
|
pub mesh: &'a TriangleMesh,
|
||||||
|
pub bvh: &'a BVH,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SdfOracle for BvhOracle<'a> {
|
||||||
|
fn sdf(&self, p: Vec3) -> f64 {
|
||||||
|
self.bvh.signed_distance(self.mesh, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Voxelized SDF for GPU-friendly queries (also usable on CPU).
|
||||||
|
/// Stores a regular grid of signed distances.
|
||||||
|
pub struct VoxelOracle {
|
||||||
|
pub data: Vec<f32>,
|
||||||
|
pub resolution: usize,
|
||||||
|
pub origin: Vec3,
|
||||||
|
pub cell_size: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VoxelOracle {
|
||||||
|
pub fn from_mesh(mesh: &TriangleMesh, bvh: &BVH, resolution: usize) -> Self {
|
||||||
|
let pad = mesh.bounds.diagonal() * 0.1;
|
||||||
|
let origin = Vec3::new(
|
||||||
|
mesh.bounds.min.x - pad,
|
||||||
|
mesh.bounds.min.y - pad,
|
||||||
|
mesh.bounds.min.z - pad,
|
||||||
|
);
|
||||||
|
let extent = mesh.bounds.diagonal() + 2.0 * pad;
|
||||||
|
let cell_size = extent / resolution as f64;
|
||||||
|
|
||||||
|
let n3 = resolution * resolution * resolution;
|
||||||
|
let mut data = vec![0.0f32; n3];
|
||||||
|
|
||||||
|
for iz in 0..resolution {
|
||||||
|
for iy in 0..resolution {
|
||||||
|
for ix in 0..resolution {
|
||||||
|
let p = Vec3::new(
|
||||||
|
origin.x + (ix as f64 + 0.5) * cell_size,
|
||||||
|
origin.y + (iy as f64 + 0.5) * cell_size,
|
||||||
|
origin.z + (iz as f64 + 0.5) * cell_size,
|
||||||
|
);
|
||||||
|
let idx = iz * resolution * resolution + iy * resolution + ix;
|
||||||
|
data[idx] = bvh.signed_distance(mesh, p) as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { data, resolution, origin, cell_size }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample(&self, p: Vec3) -> f64 {
|
||||||
|
let r = self.resolution;
|
||||||
|
let fx = (p.x - self.origin.x) / self.cell_size - 0.5;
|
||||||
|
let fy = (p.y - self.origin.y) / self.cell_size - 0.5;
|
||||||
|
let fz = (p.z - self.origin.z) / self.cell_size - 0.5;
|
||||||
|
|
||||||
|
let ix = fx.floor() as isize;
|
||||||
|
let iy = fy.floor() as isize;
|
||||||
|
let iz = fz.floor() as isize;
|
||||||
|
|
||||||
|
let tx = fx - fx.floor();
|
||||||
|
let ty = fy - fy.floor();
|
||||||
|
let tz = fz - fz.floor();
|
||||||
|
|
||||||
|
let get = |x: isize, y: isize, z: isize| -> f64 {
|
||||||
|
let cx = x.clamp(0, r as isize - 1) as usize;
|
||||||
|
let cy = y.clamp(0, r as isize - 1) as usize;
|
||||||
|
let cz = z.clamp(0, r as isize - 1) as usize;
|
||||||
|
self.data[cz * r * r + cy * r + cx] as f64
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trilinear interpolation
|
||||||
|
let c000 = get(ix, iy, iz);
|
||||||
|
let c100 = get(ix + 1, iy, iz);
|
||||||
|
let c010 = get(ix, iy + 1, iz);
|
||||||
|
let c110 = get(ix + 1, iy + 1, iz);
|
||||||
|
let c001 = get(ix, iy, iz + 1);
|
||||||
|
let c101 = get(ix + 1, iy, iz + 1);
|
||||||
|
let c011 = get(ix, iy + 1, iz + 1);
|
||||||
|
let c111 = get(ix + 1, iy + 1, iz + 1);
|
||||||
|
|
||||||
|
let c00 = c000 * (1.0 - tx) + c100 * tx;
|
||||||
|
let c01 = c001 * (1.0 - tx) + c101 * tx;
|
||||||
|
let c10 = c010 * (1.0 - tx) + c110 * tx;
|
||||||
|
let c11 = c011 * (1.0 - tx) + c111 * tx;
|
||||||
|
|
||||||
|
let c0 = c00 * (1.0 - ty) + c10 * ty;
|
||||||
|
let c1 = c01 * (1.0 - ty) + c11 * ty;
|
||||||
|
|
||||||
|
c0 * (1.0 - tz) + c1 * tz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SdfOracle for VoxelOracle {
|
||||||
|
fn sdf(&self, p: Vec3) -> f64 {
|
||||||
|
self.sample(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,428 @@
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
use crate::mesh::{Vec3, AABB};
|
||||||
|
use super::oracle::SdfOracle;
|
||||||
|
use super::state::{Crawler, CrawlerEvent, Phase};
|
||||||
|
use super::{CrawlerConfig, SurfaceHit};
|
||||||
|
|
||||||
|
/// Manages the lifecycle of all crawlers and collects surface samples.
|
||||||
|
pub struct CrawlerScheduler {
|
||||||
|
pub crawlers: Vec<Crawler>,
|
||||||
|
pub contacts: Vec<Vec3>,
|
||||||
|
pub samples: Vec<SurfaceHit>,
|
||||||
|
pub config: CrawlerConfig,
|
||||||
|
pub bounds: AABB,
|
||||||
|
next_id: u32,
|
||||||
|
next_face: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CrawlerScheduler {
|
||||||
|
pub fn new(bounds: AABB, config: CrawlerConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
crawlers: Vec::new(),
|
||||||
|
contacts: Vec::new(),
|
||||||
|
samples: Vec::new(),
|
||||||
|
config,
|
||||||
|
bounds,
|
||||||
|
next_id: 0,
|
||||||
|
next_face: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 1: Deploy initial probes uniformly on the bounding sphere.
|
||||||
|
pub fn deploy_probes(&mut self) {
|
||||||
|
let center = self.bounds.center();
|
||||||
|
let radius = self.bounds.diagonal() * 0.6;
|
||||||
|
let n = self.config.initial_probes;
|
||||||
|
|
||||||
|
// Fibonacci sphere for uniform distribution
|
||||||
|
let golden = (1.0 + 5.0_f64.sqrt()) / 2.0;
|
||||||
|
for i in 0..n {
|
||||||
|
let theta = (2.0 * PI * i as f64) / golden;
|
||||||
|
let phi = (1.0 - 2.0 * (i as f64 + 0.5) / n as f64).acos();
|
||||||
|
|
||||||
|
let dir = Vec3::new(
|
||||||
|
phi.sin() * theta.cos(),
|
||||||
|
phi.sin() * theta.sin(),
|
||||||
|
phi.cos(),
|
||||||
|
);
|
||||||
|
let pos = center + dir * radius;
|
||||||
|
let inward = (center - pos).normalized();
|
||||||
|
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
self.crawlers.push(Crawler::new_probe(id, pos, inward));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 2: Deploy patrol crawlers from contact points.
|
||||||
|
pub fn deploy_patrol(&mut self) {
|
||||||
|
let contacts: Vec<(Vec3, Vec3)> = self.crawlers.iter()
|
||||||
|
.filter(|c| c.phase == Phase::Done && c.normal.length() > 0.5)
|
||||||
|
.map(|c| (c.position, c.normal))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (pos, normal) in contacts {
|
||||||
|
let tangent_basis = tangent_frame(normal);
|
||||||
|
let per_contact = self.config.crawlers_per_contact;
|
||||||
|
for i in 0..per_contact {
|
||||||
|
let angle = 2.0 * PI * i as f64 / per_contact as f64;
|
||||||
|
let dir = tangent_basis.0 * angle.cos() + tangent_basis.1 * angle.sin();
|
||||||
|
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
let mut c = Crawler::new_probe(id, pos, dir);
|
||||||
|
c.phase = Phase::Patrol;
|
||||||
|
c.normal = normal;
|
||||||
|
self.crawlers.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance all active crawlers by one step.
|
||||||
|
/// Returns events generated this tick.
|
||||||
|
pub fn step(&mut self, oracle: &dyn SdfOracle) -> Vec<CrawlerEvent> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let step_size = self.bounds.diagonal() * self.config.step_fraction;
|
||||||
|
let eps = self.config.surface_epsilon;
|
||||||
|
let max_steps = self.config.max_steps;
|
||||||
|
|
||||||
|
for crawler in &mut self.crawlers {
|
||||||
|
if !crawler.is_active() { continue; }
|
||||||
|
crawler.steps += 1;
|
||||||
|
if crawler.steps > max_steps {
|
||||||
|
crawler.phase = Phase::Done;
|
||||||
|
events.push(CrawlerEvent::Completed { crawler_id: crawler.id });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match crawler.phase {
|
||||||
|
Phase::Contact => {
|
||||||
|
step_contact(crawler, oracle, step_size, eps, &mut events);
|
||||||
|
}
|
||||||
|
Phase::Patrol => {
|
||||||
|
step_patrol(crawler, oracle, step_size, eps, &mut events);
|
||||||
|
}
|
||||||
|
Phase::Spiral => {
|
||||||
|
step_spiral(crawler, oracle, step_size, eps, &mut events);
|
||||||
|
}
|
||||||
|
Phase::Scan => {
|
||||||
|
step_scan(crawler, oracle, step_size, eps, &mut events);
|
||||||
|
}
|
||||||
|
Phase::Done => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect path crossings between patrol crawlers
|
||||||
|
let patrol: Vec<(usize, Vec3)> = self.crawlers.iter().enumerate()
|
||||||
|
.filter(|(_, c)| c.phase == Phase::Patrol)
|
||||||
|
.map(|(i, c)| (i, c.position))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let cross_dist = step_size * 3.0;
|
||||||
|
let mut crossings = Vec::new();
|
||||||
|
for i in 0..patrol.len() {
|
||||||
|
for j in (i + 1)..patrol.len() {
|
||||||
|
let (ai, ap) = patrol[i];
|
||||||
|
let (bi, bp) = patrol[j];
|
||||||
|
if self.crawlers[ai].id == self.crawlers[bi].id { continue; }
|
||||||
|
let dist = (ap - bp).length();
|
||||||
|
if dist < cross_dist {
|
||||||
|
crossings.push((ai, bi, (ap + bp) * 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ai, bi, midpoint) in crossings {
|
||||||
|
let id_a = self.crawlers[ai].id;
|
||||||
|
let id_b = self.crawlers[bi].id;
|
||||||
|
events.push(CrawlerEvent::PathCrossing {
|
||||||
|
crawler_a: id_a,
|
||||||
|
crawler_b: id_b,
|
||||||
|
position: midpoint,
|
||||||
|
});
|
||||||
|
// Transition both to spiral to map face boundaries
|
||||||
|
self.crawlers[ai].phase = Phase::Spiral;
|
||||||
|
self.crawlers[ai].spiral_r = step_size;
|
||||||
|
self.crawlers[ai].spiral_theta = 0.0;
|
||||||
|
self.crawlers[bi].phase = Phase::Spiral;
|
||||||
|
self.crawlers[bi].spiral_r = step_size;
|
||||||
|
self.crawlers[bi].spiral_theta = PI; // opposite direction
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect samples from scanners and assign face IDs.
|
||||||
|
pub fn collect_samples(&mut self) {
|
||||||
|
for crawler in &self.crawlers {
|
||||||
|
if crawler.phase == Phase::Scan || crawler.phase == Phase::Done {
|
||||||
|
// Samples are accumulated during step_scan via events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assign idle (Done) crawlers to remaining unscanned faces.
|
||||||
|
pub fn reassign_idle(&mut self, oracle: &dyn SdfOracle) {
|
||||||
|
let scan_spacing = self.bounds.diagonal() * self.config.scan_spacing;
|
||||||
|
|
||||||
|
// Find spiraling crawlers that have identified face regions
|
||||||
|
let spiral_done: Vec<usize> = self.crawlers.iter().enumerate()
|
||||||
|
.filter(|(_, c)| c.phase == Phase::Spiral && c.steps > 200)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for idx in spiral_done {
|
||||||
|
let face = self.next_face;
|
||||||
|
self.next_face += 1;
|
||||||
|
|
||||||
|
self.crawlers[idx].phase = Phase::Scan;
|
||||||
|
self.crawlers[idx].face_id = face;
|
||||||
|
self.crawlers[idx].scan_row = 0;
|
||||||
|
self.crawlers[idx].scan_row_progress = 0.0;
|
||||||
|
self.crawlers[idx].scan_row_length = scan_spacing * 100.0;
|
||||||
|
self.crawlers[idx].scan_origin = self.crawlers[idx].position;
|
||||||
|
|
||||||
|
// Scan direction: perpendicular to current direction on the surface
|
||||||
|
let n = self.crawlers[idx].normal;
|
||||||
|
let d = self.crawlers[idx].direction;
|
||||||
|
self.crawlers[idx].scan_dir = n.cross(d).normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reassign fully done crawlers
|
||||||
|
let done_ids: Vec<usize> = self.crawlers.iter().enumerate()
|
||||||
|
.filter(|(_, c)| c.phase == Phase::Done)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let active_faces: Vec<(Vec3, Vec3, u32)> = self.crawlers.iter()
|
||||||
|
.filter(|c| c.phase == Phase::Scan)
|
||||||
|
.map(|c| (c.position, c.normal, c.face_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// No work left if no active faces need help
|
||||||
|
if active_faces.is_empty() { return; }
|
||||||
|
|
||||||
|
let _ = oracle; // will use for surface projection in more advanced reassignment
|
||||||
|
for &idx in &done_ids {
|
||||||
|
// Reassign to a random active face (simple round-robin)
|
||||||
|
let face_idx = idx % active_faces.len();
|
||||||
|
let (pos, normal, face_id) = active_faces[face_idx];
|
||||||
|
self.crawlers[idx].phase = Phase::Scan;
|
||||||
|
self.crawlers[idx].position = pos;
|
||||||
|
self.crawlers[idx].normal = normal;
|
||||||
|
self.crawlers[idx].face_id = face_id;
|
||||||
|
self.crawlers[idx].scan_row = 0;
|
||||||
|
self.crawlers[idx].scan_row_progress = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the full pipeline until all crawlers are done.
|
||||||
|
pub fn run(&mut self, oracle: &dyn SdfOracle) -> Vec<SurfaceHit> {
|
||||||
|
// Phase 1: contact
|
||||||
|
self.deploy_probes();
|
||||||
|
loop {
|
||||||
|
let events = self.step(oracle);
|
||||||
|
let active = self.crawlers.iter().any(|c| c.phase == Phase::Contact);
|
||||||
|
self.process_events(&events);
|
||||||
|
if !active { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: patrol
|
||||||
|
self.deploy_patrol();
|
||||||
|
for _ in 0..self.config.max_steps {
|
||||||
|
let events = self.step(oracle);
|
||||||
|
self.process_events(&events);
|
||||||
|
let active = self.crawlers.iter()
|
||||||
|
.any(|c| c.phase == Phase::Patrol || c.phase == Phase::Spiral);
|
||||||
|
if !active { break; }
|
||||||
|
// Periodically reassign
|
||||||
|
self.reassign_idle(oracle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: scan whatever we've identified
|
||||||
|
for _ in 0..self.config.max_steps {
|
||||||
|
let events = self.step(oracle);
|
||||||
|
self.process_events(&events);
|
||||||
|
self.reassign_idle(oracle);
|
||||||
|
let active = self.crawlers.iter().any(|c| c.is_active());
|
||||||
|
if !active { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mem::take(&mut self.samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_events(&mut self, events: &[CrawlerEvent]) {
|
||||||
|
for event in events {
|
||||||
|
match event {
|
||||||
|
CrawlerEvent::ContactMade { position, normal, .. } => {
|
||||||
|
self.contacts.push(*position);
|
||||||
|
self.samples.push(SurfaceHit {
|
||||||
|
position: *position,
|
||||||
|
normal: *normal,
|
||||||
|
face_id: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CrawlerEvent::SampleRecorded { position, normal, face_id, .. } => {
|
||||||
|
self.samples.push(SurfaceHit {
|
||||||
|
position: *position,
|
||||||
|
normal: *normal,
|
||||||
|
face_id: *face_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Individual step functions ===
|
||||||
|
|
||||||
|
fn step_contact(
|
||||||
|
c: &mut Crawler,
|
||||||
|
oracle: &dyn SdfOracle,
|
||||||
|
step_size: f64,
|
||||||
|
eps: f64,
|
||||||
|
events: &mut Vec<CrawlerEvent>,
|
||||||
|
) {
|
||||||
|
let d = oracle.sdf(c.position);
|
||||||
|
if d.abs() < eps {
|
||||||
|
c.normal = oracle.gradient(c.position);
|
||||||
|
c.phase = Phase::Done;
|
||||||
|
events.push(CrawlerEvent::ContactMade {
|
||||||
|
crawler_id: c.id,
|
||||||
|
position: c.position,
|
||||||
|
normal: c.normal,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sphere-trace: step by SDF distance (clamped)
|
||||||
|
let advance = d.abs().min(step_size);
|
||||||
|
c.position = c.position + c.direction * advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_patrol(
|
||||||
|
c: &mut Crawler,
|
||||||
|
oracle: &dyn SdfOracle,
|
||||||
|
step_size: f64,
|
||||||
|
_eps: f64,
|
||||||
|
events: &mut Vec<CrawlerEvent>,
|
||||||
|
) {
|
||||||
|
// Move along surface: step in direction, then project back
|
||||||
|
let candidate = c.position + c.direction * step_size;
|
||||||
|
if let Some((proj, normal)) = oracle.project_to_surface(candidate, 16) {
|
||||||
|
let normal_change = 1.0 - c.normal.dot(normal).abs();
|
||||||
|
if normal_change > 0.3 {
|
||||||
|
events.push(CrawlerEvent::FaceBoundary {
|
||||||
|
crawler_id: c.id,
|
||||||
|
position: proj,
|
||||||
|
normal_change,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Update direction to stay tangent
|
||||||
|
let raw_dir = (proj - c.position).normalized();
|
||||||
|
let tangent = (raw_dir - normal * raw_dir.dot(normal)).normalized();
|
||||||
|
c.position = proj;
|
||||||
|
c.normal = normal;
|
||||||
|
if tangent.length() > 0.5 {
|
||||||
|
c.direction = tangent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Lost the surface — done
|
||||||
|
c.phase = Phase::Done;
|
||||||
|
events.push(CrawlerEvent::Completed { crawler_id: c.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_spiral(
|
||||||
|
c: &mut Crawler,
|
||||||
|
oracle: &dyn SdfOracle,
|
||||||
|
step_size: f64,
|
||||||
|
eps: f64,
|
||||||
|
events: &mut Vec<CrawlerEvent>,
|
||||||
|
) {
|
||||||
|
// Archimedean spiral on the surface
|
||||||
|
c.spiral_theta += step_size / c.spiral_r.max(step_size);
|
||||||
|
c.spiral_r += step_size * 0.05; // slow expansion
|
||||||
|
|
||||||
|
let (t1, t2) = tangent_frame(c.normal);
|
||||||
|
let offset = t1 * (c.spiral_r * c.spiral_theta.cos())
|
||||||
|
+ t2 * (c.spiral_r * c.spiral_theta.sin());
|
||||||
|
let candidate = c.scan_origin + offset;
|
||||||
|
|
||||||
|
if let Some((proj, normal)) = oracle.project_to_surface(candidate, 16) {
|
||||||
|
let normal_change = 1.0 - c.normal.dot(normal).abs();
|
||||||
|
c.position = proj;
|
||||||
|
c.normal = normal;
|
||||||
|
|
||||||
|
events.push(CrawlerEvent::SampleRecorded {
|
||||||
|
crawler_id: c.id,
|
||||||
|
position: proj,
|
||||||
|
normal,
|
||||||
|
face_id: c.face_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If normal changes significantly, we've hit a face boundary
|
||||||
|
if normal_change > 0.3 {
|
||||||
|
events.push(CrawlerEvent::FaceBoundary {
|
||||||
|
crawler_id: c.id,
|
||||||
|
position: proj,
|
||||||
|
normal_change,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.phase = Phase::Done;
|
||||||
|
events.push(CrawlerEvent::Completed { crawler_id: c.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = eps;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_scan(
|
||||||
|
c: &mut Crawler,
|
||||||
|
oracle: &dyn SdfOracle,
|
||||||
|
step_size: f64,
|
||||||
|
eps: f64,
|
||||||
|
events: &mut Vec<CrawlerEvent>,
|
||||||
|
) {
|
||||||
|
// Raster scan: move along scan_dir, step rows perpendicular
|
||||||
|
c.scan_row_progress += step_size;
|
||||||
|
|
||||||
|
let row_dir = if c.scan_row % 2 == 0 { c.scan_dir } else { -c.scan_dir };
|
||||||
|
let row_offset = c.direction * (c.scan_row as f64 * step_size * 5.0);
|
||||||
|
let candidate = c.scan_origin + row_offset + row_dir * c.scan_row_progress;
|
||||||
|
|
||||||
|
if let Some((proj, normal)) = oracle.project_to_surface(candidate, 16) {
|
||||||
|
c.position = proj;
|
||||||
|
c.normal = normal;
|
||||||
|
events.push(CrawlerEvent::SampleRecorded {
|
||||||
|
crawler_id: c.id,
|
||||||
|
position: proj,
|
||||||
|
normal,
|
||||||
|
face_id: c.face_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.scan_row_progress > c.scan_row_length {
|
||||||
|
c.scan_row += 1;
|
||||||
|
c.scan_row_progress = 0.0;
|
||||||
|
// Arbitrary limit on rows
|
||||||
|
if c.scan_row > 200 {
|
||||||
|
c.phase = Phase::Done;
|
||||||
|
events.push(CrawlerEvent::Completed { crawler_id: c.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = eps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an orthonormal tangent frame from a normal vector.
|
||||||
|
fn tangent_frame(n: Vec3) -> (Vec3, Vec3) {
|
||||||
|
let up = if n.z.abs() < 0.9 {
|
||||||
|
Vec3::new(0.0, 0.0, 1.0)
|
||||||
|
} else {
|
||||||
|
Vec3::new(1.0, 0.0, 0.0)
|
||||||
|
};
|
||||||
|
let t1 = n.cross(up).normalized();
|
||||||
|
let t2 = n.cross(t1).normalized();
|
||||||
|
(t1, t2)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::mesh::Vec3;
|
||||||
|
|
||||||
|
/// What phase a crawler is in.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Phase {
|
||||||
|
/// Marching inward toward the object from the bounding sphere.
|
||||||
|
Contact,
|
||||||
|
/// Crawling along the surface to map boundaries.
|
||||||
|
Patrol,
|
||||||
|
/// Spiraling to identify face regions after a path crossing.
|
||||||
|
Spiral,
|
||||||
|
/// Raster-scanning a face ("mowing the lawn").
|
||||||
|
Scan,
|
||||||
|
/// Work complete, awaiting reassignment or termination.
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-crawler state. Each crawler is an independent agent.
|
||||||
|
/// On GPU, this maps to a storage buffer struct.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Crawler {
|
||||||
|
pub id: u32,
|
||||||
|
pub phase: Phase,
|
||||||
|
pub position: Vec3,
|
||||||
|
pub direction: Vec3,
|
||||||
|
pub normal: Vec3,
|
||||||
|
pub face_id: u32,
|
||||||
|
pub steps: u32,
|
||||||
|
/// Spiral radius (grows each step in Spiral phase).
|
||||||
|
pub spiral_r: f64,
|
||||||
|
/// Spiral angle accumulator.
|
||||||
|
pub spiral_theta: f64,
|
||||||
|
/// Scan state: row origin and direction.
|
||||||
|
pub scan_origin: Vec3,
|
||||||
|
pub scan_dir: Vec3,
|
||||||
|
pub scan_row: u32,
|
||||||
|
pub scan_row_progress: f64,
|
||||||
|
pub scan_row_length: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crawler {
|
||||||
|
pub fn new_probe(id: u32, position: Vec3, direction: Vec3) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
phase: Phase::Contact,
|
||||||
|
position,
|
||||||
|
direction,
|
||||||
|
normal: Vec3::zero(),
|
||||||
|
face_id: 0,
|
||||||
|
steps: 0,
|
||||||
|
spiral_r: 0.0,
|
||||||
|
spiral_theta: 0.0,
|
||||||
|
scan_origin: Vec3::zero(),
|
||||||
|
scan_dir: Vec3::zero(),
|
||||||
|
scan_row: 0,
|
||||||
|
scan_row_progress: 0.0,
|
||||||
|
scan_row_length: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.phase != Phase::Done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Events emitted by crawlers during stepping.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CrawlerEvent {
|
||||||
|
/// Crawler hit the surface for the first time.
|
||||||
|
ContactMade { crawler_id: u32, position: Vec3, normal: Vec3 },
|
||||||
|
/// Two crawlers crossed paths.
|
||||||
|
PathCrossing { crawler_a: u32, crawler_b: u32, position: Vec3 },
|
||||||
|
/// Crawler identified a face boundary.
|
||||||
|
FaceBoundary { crawler_id: u32, position: Vec3, normal_change: f64 },
|
||||||
|
/// Surface sample recorded during scan phase.
|
||||||
|
SampleRecorded { crawler_id: u32, position: Vec3, normal: Vec3, face_id: u32 },
|
||||||
|
/// Crawler finished its work.
|
||||||
|
Completed { crawler_id: u32 },
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
use crate::bvh::BVH;
|
||||||
|
use crate::mesh::{AABB, TriangleMesh, Vec3};
|
||||||
|
use crate::sparse_grid::{CellKey, SparseGrid};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Per-cell density analysis comparing mesh triangle density to uniform.
|
||||||
|
pub struct DensityMap {
|
||||||
|
pub cells: HashMap<CellKey, DensityInfo>,
|
||||||
|
pub mean_density: f64,
|
||||||
|
pub max_density: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct DensityInfo {
|
||||||
|
/// Triangles per unit volume in this cell.
|
||||||
|
pub triangle_density: f64,
|
||||||
|
/// Ratio to the mean density. >1 = more complex geometry (curves, fillets).
|
||||||
|
pub relative_density: f64,
|
||||||
|
/// Classification of this region's geometric character.
|
||||||
|
pub surface_type: SurfaceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SurfaceType {
|
||||||
|
/// Low density: likely a flat surface.
|
||||||
|
Flat,
|
||||||
|
/// Medium density: gentle curve or chamfer.
|
||||||
|
Curved,
|
||||||
|
/// High density: tight radius, fillet, or complex feature.
|
||||||
|
HighDetail,
|
||||||
|
/// Interior or exterior — no surface here.
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn analyze(mesh: &TriangleMesh, grid: &SparseGrid) -> DensityMap {
|
||||||
|
let bvh = BVH::build(mesh);
|
||||||
|
|
||||||
|
let mut cell_densities: HashMap<CellKey, DensityInfo> = HashMap::new();
|
||||||
|
let mut total_density = 0.0;
|
||||||
|
let mut count = 0usize;
|
||||||
|
let mut max_density = 0.0f64;
|
||||||
|
|
||||||
|
let leaves = grid.leaf_cells();
|
||||||
|
|
||||||
|
for (key, data) in &leaves {
|
||||||
|
if !data.is_surface {
|
||||||
|
cell_densities.insert(**key, DensityInfo {
|
||||||
|
triangle_density: 0.0,
|
||||||
|
relative_density: 0.0,
|
||||||
|
surface_type: SurfaceType::Empty,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell_bounds = cell_aabb(grid, key);
|
||||||
|
let volume = cell_volume(&cell_bounds);
|
||||||
|
if volume < 1e-15 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tri_count = bvh.count_in_region(mesh, &cell_bounds);
|
||||||
|
let density = tri_count as f64 / volume;
|
||||||
|
|
||||||
|
total_density += density;
|
||||||
|
count += 1;
|
||||||
|
max_density = max_density.max(density);
|
||||||
|
|
||||||
|
cell_densities.insert(**key, DensityInfo {
|
||||||
|
triangle_density: density,
|
||||||
|
relative_density: 0.0, // filled in second pass
|
||||||
|
surface_type: SurfaceType::Flat, // placeholder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mean_density = if count > 0 { total_density / count as f64 } else { 1.0 };
|
||||||
|
|
||||||
|
// Second pass: compute relative density and classify
|
||||||
|
for info in cell_densities.values_mut() {
|
||||||
|
if info.triangle_density == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info.relative_density = info.triangle_density / mean_density;
|
||||||
|
|
||||||
|
info.surface_type = if info.relative_density < 0.5 {
|
||||||
|
SurfaceType::Flat
|
||||||
|
} else if info.relative_density < 2.0 {
|
||||||
|
SurfaceType::Curved
|
||||||
|
} else {
|
||||||
|
SurfaceType::HighDetail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DensityMap {
|
||||||
|
cells: cell_densities,
|
||||||
|
mean_density,
|
||||||
|
max_density,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_aabb(grid: &SparseGrid, key: &CellKey) -> AABB {
|
||||||
|
let divisions = (1u32 << key.depth) as f64;
|
||||||
|
let extent = grid.bounds.max - grid.bounds.min;
|
||||||
|
let cell_size = Vec3::new(
|
||||||
|
extent.x / divisions,
|
||||||
|
extent.y / divisions,
|
||||||
|
extent.z / divisions,
|
||||||
|
);
|
||||||
|
let min = Vec3::new(
|
||||||
|
grid.bounds.min.x + key.x as f64 * cell_size.x,
|
||||||
|
grid.bounds.min.y + key.y as f64 * cell_size.y,
|
||||||
|
grid.bounds.min.z + key.z as f64 * cell_size.z,
|
||||||
|
);
|
||||||
|
AABB { min, max: min + cell_size }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_volume(aabb: &AABB) -> f64 {
|
||||||
|
let e = aabb.max - aabb.min;
|
||||||
|
e.x * e.y * e.z
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,120 @@
|
||||||
|
//! Mesh decompiler — STL/OBJ → SDF tree.
|
||||||
|
//!
|
||||||
|
//! Pipeline: mesh → BVH → sparse octree → density analysis → RANSAC
|
||||||
|
//! primitive detection → Riesz/monogenic CSG boolean classification →
|
||||||
|
//! reconstructed SDF tree.
|
||||||
|
//!
|
||||||
|
//! Also includes a crawler-based surface exploration mode for adaptive
|
||||||
|
//! sampling without a fixed grid.
|
||||||
|
|
||||||
|
pub mod mesh;
|
||||||
|
pub mod bvh;
|
||||||
|
pub mod sparse_grid;
|
||||||
|
pub mod density;
|
||||||
|
pub mod fit;
|
||||||
|
pub mod reconstruct;
|
||||||
|
pub mod monogenic_classify;
|
||||||
|
pub mod crawler;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cord_sdf::SdfNode;
|
||||||
|
use mesh::TriangleMesh;
|
||||||
|
|
||||||
|
/// Full decompilation pipeline: foreign mesh → SDF tree.
|
||||||
|
///
|
||||||
|
/// Primitive detection via iterative RANSAC, then Riesz/monogenic
|
||||||
|
/// classification of each primitive as additive or subtractive.
|
||||||
|
pub fn decompile(mesh: &TriangleMesh, config: &DecompileConfig) -> Result<DecompileResult> {
|
||||||
|
let bvh = bvh::BVH::build(mesh);
|
||||||
|
let grid = sparse_grid::SparseGrid::from_mesh(mesh, &bvh, config.grid_depth);
|
||||||
|
let density_map = density::analyze(mesh, &grid);
|
||||||
|
let samples = grid.surface_samples();
|
||||||
|
let primitives = fit::detect_primitives(&grid, &density_map, config);
|
||||||
|
|
||||||
|
let subtractive = monogenic_classify::classify_subtractive(
|
||||||
|
mesh,
|
||||||
|
&bvh,
|
||||||
|
&grid.bounds,
|
||||||
|
&primitives,
|
||||||
|
&samples,
|
||||||
|
config.monogenic_resolution,
|
||||||
|
);
|
||||||
|
let sdf = reconstruct::build_sdf_tree(&primitives, &samples, &subtractive);
|
||||||
|
|
||||||
|
Ok(DecompileResult {
|
||||||
|
grid,
|
||||||
|
density_map,
|
||||||
|
primitives,
|
||||||
|
sdf,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DecompileConfig {
|
||||||
|
pub grid_depth: u8,
|
||||||
|
pub ransac_iterations: u32,
|
||||||
|
pub distance_threshold: f64,
|
||||||
|
pub normal_threshold: f64,
|
||||||
|
pub min_support_ratio: f64,
|
||||||
|
/// Resolution of the regular grid for monogenic signal computation.
|
||||||
|
/// Higher values improve classification accuracy at the cost of
|
||||||
|
/// O(n³ log n) FFT time. 32-64 is typical.
|
||||||
|
pub monogenic_resolution: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DecompileConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
grid_depth: 7,
|
||||||
|
ransac_iterations: 1000,
|
||||||
|
distance_threshold: 0.01,
|
||||||
|
normal_threshold: 0.95,
|
||||||
|
min_support_ratio: 0.05,
|
||||||
|
monogenic_resolution: 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DecompileResult {
|
||||||
|
pub grid: sparse_grid::SparseGrid,
|
||||||
|
pub density_map: density::DensityMap,
|
||||||
|
pub primitives: Vec<fit::DetectedPrimitive>,
|
||||||
|
pub sdf: SdfNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crawler-based decompilation: agent-based surface exploration.
|
||||||
|
/// Faster and more adaptive than grid-based approach.
|
||||||
|
pub fn decompile_crawl(
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
config: &crawler::CrawlerConfig,
|
||||||
|
) -> Result<CrawlResult> {
|
||||||
|
let bvh = bvh::BVH::build(mesh);
|
||||||
|
let samples = crawler::cpu::run_cpu(mesh, &bvh, config.clone());
|
||||||
|
|
||||||
|
// Count distinct faces
|
||||||
|
let mut face_ids: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
||||||
|
for s in &samples {
|
||||||
|
face_ids.insert(s.face_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CrawlResult {
|
||||||
|
samples,
|
||||||
|
face_count: face_ids.len(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CrawlResult {
|
||||||
|
pub samples: Vec<crawler::SurfaceHit>,
|
||||||
|
pub face_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// High-level API: load a mesh file and run the full decomposition pipeline.
|
||||||
|
///
|
||||||
|
/// Returns both the SdfNode tree and the intermediate DecompileResult
|
||||||
|
/// for callers that need primitive-level detail.
|
||||||
|
pub fn reconstruct_mesh(
|
||||||
|
path: &std::path::Path,
|
||||||
|
config: &DecompileConfig,
|
||||||
|
) -> Result<DecompileResult> {
|
||||||
|
let mesh = mesh::TriangleMesh::load(path)?;
|
||||||
|
decompile(&mesh, config)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,400 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Vec3 {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub z: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vec3 {
|
||||||
|
pub fn new(x: f64, y: f64, z: f64) -> Self {
|
||||||
|
Self { x, y, z }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self { x: 0.0, y: 0.0, z: 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dot(self, other: Self) -> f64 {
|
||||||
|
self.x * other.x + self.y * other.y + self.z * other.z
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cross(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.y * other.z - self.z * other.y,
|
||||||
|
y: self.z * other.x - self.x * other.z,
|
||||||
|
z: self.x * other.y - self.y * other.x,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(self) -> f64 {
|
||||||
|
self.dot(self).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalized(self) -> Self {
|
||||||
|
let len = self.length();
|
||||||
|
if len < 1e-12 {
|
||||||
|
return Self::zero();
|
||||||
|
}
|
||||||
|
Self { x: self.x / len, y: self.y / len, z: self.z / len }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn component_min(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.min(other.x),
|
||||||
|
y: self.y.min(other.y),
|
||||||
|
z: self.z.min(other.z),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn component_max(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.max(other.x),
|
||||||
|
y: self.y.max(other.y),
|
||||||
|
z: self.z.max(other.z),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Vec3 {
|
||||||
|
type Output = Self;
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Self { x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for Vec3 {
|
||||||
|
type Output = Self;
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self { x: self.x - rhs.x, y: self.y - rhs.y, z: self.z - rhs.z }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Mul<f64> for Vec3 {
|
||||||
|
type Output = Self;
|
||||||
|
fn mul(self, rhs: f64) -> Self {
|
||||||
|
Self { x: self.x * rhs, y: self.y * rhs, z: self.z * rhs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Neg for Vec3 {
|
||||||
|
type Output = Self;
|
||||||
|
fn neg(self) -> Self {
|
||||||
|
Self { x: -self.x, y: -self.y, z: -self.z }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Triangle {
|
||||||
|
pub v: [Vec3; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Triangle {
|
||||||
|
pub fn normal(&self) -> Vec3 {
|
||||||
|
let e1 = self.v[1] - self.v[0];
|
||||||
|
let e2 = self.v[2] - self.v[0];
|
||||||
|
e1.cross(e2).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area(&self) -> f64 {
|
||||||
|
let e1 = self.v[1] - self.v[0];
|
||||||
|
let e2 = self.v[2] - self.v[0];
|
||||||
|
e1.cross(e2).length() * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn centroid(&self) -> Vec3 {
|
||||||
|
Vec3 {
|
||||||
|
x: (self.v[0].x + self.v[1].x + self.v[2].x) / 3.0,
|
||||||
|
y: (self.v[0].y + self.v[1].y + self.v[2].y) / 3.0,
|
||||||
|
z: (self.v[0].z + self.v[1].z + self.v[2].z) / 3.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closest point on triangle to point p, and the unsigned distance.
|
||||||
|
pub fn closest_point(&self, p: Vec3) -> (Vec3, f64) {
|
||||||
|
let a = self.v[0];
|
||||||
|
let b = self.v[1];
|
||||||
|
let c = self.v[2];
|
||||||
|
let ab = b - a;
|
||||||
|
let ac = c - a;
|
||||||
|
let ap = p - a;
|
||||||
|
|
||||||
|
let d1 = ab.dot(ap);
|
||||||
|
let d2 = ac.dot(ap);
|
||||||
|
if d1 <= 0.0 && d2 <= 0.0 {
|
||||||
|
return (a, (p - a).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bp = p - b;
|
||||||
|
let d3 = ab.dot(bp);
|
||||||
|
let d4 = ac.dot(bp);
|
||||||
|
if d3 >= 0.0 && d4 <= d3 {
|
||||||
|
return (b, (p - b).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
let vc = d1 * d4 - d3 * d2;
|
||||||
|
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
|
||||||
|
let v = d1 / (d1 - d3);
|
||||||
|
let pt = a + ab * v;
|
||||||
|
return (pt, (p - pt).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cp = p - c;
|
||||||
|
let d5 = ab.dot(cp);
|
||||||
|
let d6 = ac.dot(cp);
|
||||||
|
if d6 >= 0.0 && d5 <= d6 {
|
||||||
|
return (c, (p - c).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
let vb = d5 * d2 - d1 * d6;
|
||||||
|
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
|
||||||
|
let w = d2 / (d2 - d6);
|
||||||
|
let pt = a + ac * w;
|
||||||
|
return (pt, (p - pt).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
let va = d3 * d6 - d5 * d4;
|
||||||
|
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
|
||||||
|
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
||||||
|
let pt = b + (c - b) * w;
|
||||||
|
return (pt, (p - pt).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = 1.0 / (va + vb + vc);
|
||||||
|
let v = vb * denom;
|
||||||
|
let w = vc * denom;
|
||||||
|
let pt = a + ab * v + ac * w;
|
||||||
|
(pt, (p - pt).length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct AABB {
|
||||||
|
pub min: Vec3,
|
||||||
|
pub max: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AABB {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
min: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
|
||||||
|
max: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_triangle(tri: &Triangle) -> Self {
|
||||||
|
let mut aabb = Self::empty();
|
||||||
|
for v in &tri.v {
|
||||||
|
aabb.min = aabb.min.component_min(*v);
|
||||||
|
aabb.max = aabb.max.component_max(*v);
|
||||||
|
}
|
||||||
|
aabb
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn union(&self, other: &AABB) -> AABB {
|
||||||
|
AABB {
|
||||||
|
min: self.min.component_min(other.min),
|
||||||
|
max: self.max.component_max(other.max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center(&self) -> Vec3 {
|
||||||
|
Vec3 {
|
||||||
|
x: (self.min.x + self.max.x) * 0.5,
|
||||||
|
y: (self.min.y + self.max.y) * 0.5,
|
||||||
|
z: (self.min.z + self.max.z) * 0.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extent(&self) -> Vec3 {
|
||||||
|
self.max - self.min
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn longest_axis(&self) -> usize {
|
||||||
|
let e = self.extent();
|
||||||
|
if e.x >= e.y && e.x >= e.z { 0 }
|
||||||
|
else if e.y >= e.z { 1 }
|
||||||
|
else { 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distance_to_point(&self, p: Vec3) -> f64 {
|
||||||
|
let dx = (self.min.x - p.x).max(0.0).max(p.x - self.max.x);
|
||||||
|
let dy = (self.min.y - p.y).max(0.0).max(p.y - self.max.y);
|
||||||
|
let dz = (self.min.z - p.z).max(0.0).max(p.z - self.max.z);
|
||||||
|
(dx * dx + dy * dy + dz * dz).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diagonal(&self) -> f64 {
|
||||||
|
self.extent().length()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TriangleMesh {
|
||||||
|
pub triangles: Vec<Triangle>,
|
||||||
|
pub bounds: AABB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TriangleMesh {
|
||||||
|
pub fn from_stl(path: &Path) -> Result<Self> {
|
||||||
|
let data = std::fs::read(path)
|
||||||
|
.with_context(|| format!("reading {}", path.display()))?;
|
||||||
|
|
||||||
|
if data.len() > 5 && &data[0..5] == b"solid" {
|
||||||
|
if let Ok(mesh) = Self::parse_ascii_stl(&data) {
|
||||||
|
if !mesh.triangles.is_empty() {
|
||||||
|
return Ok(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::parse_binary_stl(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_binary_stl(data: &[u8]) -> Result<Self> {
|
||||||
|
anyhow::ensure!(data.len() >= 84, "STL too short for binary header");
|
||||||
|
let num_tris = u32::from_le_bytes(data[80..84].try_into().unwrap()) as usize;
|
||||||
|
anyhow::ensure!(data.len() >= 84 + num_tris * 50, "STL truncated");
|
||||||
|
|
||||||
|
let mut triangles = Vec::with_capacity(num_tris);
|
||||||
|
let mut bounds = AABB::empty();
|
||||||
|
|
||||||
|
for i in 0..num_tris {
|
||||||
|
let base = 84 + i * 50;
|
||||||
|
// Skip normal (12 bytes), read 3 vertices (36 bytes)
|
||||||
|
let mut verts = [Vec3::zero(); 3];
|
||||||
|
for j in 0..3 {
|
||||||
|
let off = base + 12 + j * 12;
|
||||||
|
let x = f32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as f64;
|
||||||
|
let y = f32::from_le_bytes(data[off + 4..off + 8].try_into().unwrap()) as f64;
|
||||||
|
let z = f32::from_le_bytes(data[off + 8..off + 12].try_into().unwrap()) as f64;
|
||||||
|
verts[j] = Vec3::new(x, y, z);
|
||||||
|
bounds.min = bounds.min.component_min(verts[j]);
|
||||||
|
bounds.max = bounds.max.component_max(verts[j]);
|
||||||
|
}
|
||||||
|
triangles.push(Triangle { v: verts });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { triangles, bounds })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ascii_stl(data: &[u8]) -> Result<Self> {
|
||||||
|
let text = std::str::from_utf8(data).context("STL not valid UTF-8 for ASCII")?;
|
||||||
|
let mut triangles = Vec::new();
|
||||||
|
let mut bounds = AABB::empty();
|
||||||
|
|
||||||
|
let mut verts: Vec<Vec3> = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("vertex") {
|
||||||
|
let parts: Vec<f64> = rest.split_whitespace()
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
if parts.len() == 3 {
|
||||||
|
let v = Vec3::new(parts[0], parts[1], parts[2]);
|
||||||
|
bounds.min = bounds.min.component_min(v);
|
||||||
|
bounds.max = bounds.max.component_max(v);
|
||||||
|
verts.push(v);
|
||||||
|
if verts.len() == 3 {
|
||||||
|
triangles.push(Triangle { v: [verts[0], verts[1], verts[2]] });
|
||||||
|
verts.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { triangles, bounds })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_obj(path: &Path) -> Result<Self> {
|
||||||
|
let text = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("reading {}", path.display()))?;
|
||||||
|
|
||||||
|
let mut vertices: Vec<Vec3> = Vec::new();
|
||||||
|
let mut triangles = Vec::new();
|
||||||
|
let mut bounds = AABB::empty();
|
||||||
|
|
||||||
|
for line in text.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("v ") {
|
||||||
|
let parts: Vec<f64> = rest.split_whitespace()
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
let v = Vec3::new(parts[0], parts[1], parts[2]);
|
||||||
|
bounds.min = bounds.min.component_min(v);
|
||||||
|
bounds.max = bounds.max.component_max(v);
|
||||||
|
vertices.push(v);
|
||||||
|
}
|
||||||
|
} else if let Some(rest) = trimmed.strip_prefix("f ") {
|
||||||
|
let indices: Vec<usize> = rest.split_whitespace()
|
||||||
|
.filter_map(|s| {
|
||||||
|
let idx_str = s.split('/').next().unwrap_or(s);
|
||||||
|
idx_str.parse::<usize>().ok().map(|i| i - 1)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// Fan triangulation for polygons with > 3 vertices
|
||||||
|
for i in 1..indices.len().saturating_sub(1) {
|
||||||
|
if let (Some(&a), Some(&b), Some(&c)) =
|
||||||
|
(indices.first(), indices.get(i), indices.get(i + 1))
|
||||||
|
{
|
||||||
|
if a < vertices.len() && b < vertices.len() && c < vertices.len() {
|
||||||
|
triangles.push(Triangle { v: [vertices[a], vertices[b], vertices[c]] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { triangles, bounds })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_3mf(path: &Path) -> Result<Self> {
|
||||||
|
let file = std::fs::File::open(path)
|
||||||
|
.with_context(|| format!("opening {}", path.display()))?;
|
||||||
|
let reader = std::io::BufReader::new(file);
|
||||||
|
let models = threemf::read(reader)
|
||||||
|
.map_err(|e| anyhow::anyhow!("3MF parse error: {e}"))?;
|
||||||
|
|
||||||
|
let mut triangles = Vec::new();
|
||||||
|
let mut bounds = AABB::empty();
|
||||||
|
|
||||||
|
for model in &models {
|
||||||
|
for obj in &model.resources.object {
|
||||||
|
let mesh = match &obj.mesh {
|
||||||
|
Some(m) => m,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let verts = &mesh.vertices.vertex;
|
||||||
|
for tri in &mesh.triangles.triangle {
|
||||||
|
let v0 = &verts[tri.v1];
|
||||||
|
let v1 = &verts[tri.v2];
|
||||||
|
let v2 = &verts[tri.v3];
|
||||||
|
let a = Vec3::new(v0.x, v0.y, v0.z);
|
||||||
|
let b = Vec3::new(v1.x, v1.y, v1.z);
|
||||||
|
let c = Vec3::new(v2.x, v2.y, v2.z);
|
||||||
|
bounds.min = bounds.min.component_min(a).component_min(b).component_min(c);
|
||||||
|
bounds.max = bounds.max.component_max(a).component_max(b).component_max(c);
|
||||||
|
triangles.push(Triangle { v: [a, b, c] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::ensure!(!triangles.is_empty(), "3MF contains no triangles");
|
||||||
|
Ok(Self { triangles, bounds })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
|
let ext = path.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
match ext.to_ascii_lowercase().as_str() {
|
||||||
|
"stl" => Self::from_stl(path),
|
||||||
|
"obj" => Self::from_obj(path),
|
||||||
|
"3mf" => Self::from_3mf(path),
|
||||||
|
_ if ext.is_empty() => anyhow::bail!("no file extension"),
|
||||||
|
_ => anyhow::bail!("unsupported mesh format: .{ext}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
use crate::bvh::BVH;
|
||||||
|
use crate::fit::DetectedPrimitive;
|
||||||
|
use crate::mesh::{AABB, TriangleMesh, Vec3};
|
||||||
|
use crate::reconstruct::ideal_outward_normal;
|
||||||
|
use crate::sparse_grid::SurfaceSample;
|
||||||
|
use cord_riesz::MonogenicField;
|
||||||
|
|
||||||
|
/// Classify each primitive as subtractive using the monogenic signal.
|
||||||
|
///
|
||||||
|
/// Evaluates the mesh SDF on a regular N³ grid, computes the Riesz
|
||||||
|
/// transform, and compares monogenic orientation at each primitive's
|
||||||
|
/// support points against the primitive's ideal outward normal.
|
||||||
|
///
|
||||||
|
/// At surface crossings (monogenic phase ≈ π/2), the Riesz orientation
|
||||||
|
/// vector points from negative SDF (interior) toward positive SDF
|
||||||
|
/// (exterior). If this opposes a primitive's ideal outward normal,
|
||||||
|
/// the primitive represents removed material — a subtractive operation.
|
||||||
|
pub fn classify_subtractive(
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
bvh: &BVH,
|
||||||
|
bounds: &AABB,
|
||||||
|
primitives: &[DetectedPrimitive],
|
||||||
|
samples: &[SurfaceSample],
|
||||||
|
resolution: usize,
|
||||||
|
) -> Vec<bool> {
|
||||||
|
let n = resolution.max(8);
|
||||||
|
let field = evaluate_sdf_grid(mesh, bvh, bounds, n);
|
||||||
|
let mono = MonogenicField::compute(&field, n);
|
||||||
|
|
||||||
|
primitives
|
||||||
|
.iter()
|
||||||
|
.map(|prim| classify_one(prim, samples, &mono, bounds, n))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sample the mesh SDF on a regular N³ grid.
|
||||||
|
fn evaluate_sdf_grid(
|
||||||
|
mesh: &TriangleMesh,
|
||||||
|
bvh: &BVH,
|
||||||
|
bounds: &AABB,
|
||||||
|
n: usize,
|
||||||
|
) -> Vec<f64> {
|
||||||
|
let step = [
|
||||||
|
(bounds.max.x - bounds.min.x) / (n - 1).max(1) as f64,
|
||||||
|
(bounds.max.y - bounds.min.y) / (n - 1).max(1) as f64,
|
||||||
|
(bounds.max.z - bounds.min.z) / (n - 1).max(1) as f64,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut field = Vec::with_capacity(n * n * n);
|
||||||
|
for iz in 0..n {
|
||||||
|
let z = bounds.min.z + iz as f64 * step[2];
|
||||||
|
for iy in 0..n {
|
||||||
|
let y = bounds.min.y + iy as f64 * step[1];
|
||||||
|
for ix in 0..n {
|
||||||
|
let x = bounds.min.x + ix as f64 * step[0];
|
||||||
|
field.push(bvh.signed_distance(mesh, Vec3::new(x, y, z)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a world position to the nearest grid index.
|
||||||
|
fn world_to_grid_index(pos: Vec3, bounds: &AABB, n: usize) -> usize {
|
||||||
|
let extent = bounds.max - bounds.min;
|
||||||
|
let nf = (n - 1) as f64;
|
||||||
|
|
||||||
|
let ix = ((pos.x - bounds.min.x) / extent.x * nf)
|
||||||
|
.round()
|
||||||
|
.clamp(0.0, nf) as usize;
|
||||||
|
let iy = ((pos.y - bounds.min.y) / extent.y * nf)
|
||||||
|
.round()
|
||||||
|
.clamp(0.0, nf) as usize;
|
||||||
|
let iz = ((pos.z - bounds.min.z) / extent.z * nf)
|
||||||
|
.round()
|
||||||
|
.clamp(0.0, nf) as usize;
|
||||||
|
|
||||||
|
iz * n * n + iy * n + ix
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify a single primitive via monogenic orientation voting.
|
||||||
|
///
|
||||||
|
/// For each support point near a surface crossing (phase ≈ π/2),
|
||||||
|
/// the Riesz orientation vector is compared against the primitive's
|
||||||
|
/// ideal outward normal. Majority opposition → subtractive.
|
||||||
|
///
|
||||||
|
/// Falls back to raw mesh normal comparison when too few support
|
||||||
|
/// points land on monogenic edge features.
|
||||||
|
fn classify_one(
|
||||||
|
prim: &DetectedPrimitive,
|
||||||
|
samples: &[SurfaceSample],
|
||||||
|
mono: &MonogenicField,
|
||||||
|
bounds: &AABB,
|
||||||
|
n: usize,
|
||||||
|
) -> bool {
|
||||||
|
if prim.support.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_pi = std::f64::consts::FRAC_PI_2;
|
||||||
|
let phase_tol = 0.8;
|
||||||
|
let min_amplitude = 1e-6;
|
||||||
|
|
||||||
|
let mut agree = 0usize;
|
||||||
|
let mut oppose = 0usize;
|
||||||
|
|
||||||
|
for &idx in &prim.support {
|
||||||
|
if idx >= samples.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sample = &samples[idx];
|
||||||
|
let grid_idx = world_to_grid_index(sample.position, bounds, n);
|
||||||
|
if grid_idx >= mono.samples.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ms = &mono.samples[grid_idx];
|
||||||
|
|
||||||
|
// Only count samples near edge phase with significant amplitude
|
||||||
|
if ms.amplitude < min_amplitude || (ms.phase - half_pi).abs() > phase_tol {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ideal = ideal_outward_normal(&prim.kind, sample.position);
|
||||||
|
let dot = ideal.x * ms.orientation[0]
|
||||||
|
+ ideal.y * ms.orientation[1]
|
||||||
|
+ ideal.z * ms.orientation[2];
|
||||||
|
|
||||||
|
if dot > 0.0 {
|
||||||
|
agree += 1;
|
||||||
|
} else {
|
||||||
|
oppose += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if too few monogenic edge samples, use raw mesh normals
|
||||||
|
if agree + oppose < 3 {
|
||||||
|
return fallback_normal_classify(prim, samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
oppose > agree
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normal-comparison fallback: compare mesh normals against the
|
||||||
|
/// primitive's ideal outward normal via majority vote.
|
||||||
|
fn fallback_normal_classify(prim: &DetectedPrimitive, samples: &[SurfaceSample]) -> bool {
|
||||||
|
let mut agree = 0usize;
|
||||||
|
let mut oppose = 0usize;
|
||||||
|
|
||||||
|
for &idx in &prim.support {
|
||||||
|
if idx >= samples.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sample = &samples[idx];
|
||||||
|
let ideal = ideal_outward_normal(&prim.kind, sample.position);
|
||||||
|
let dot = ideal.dot(sample.normal);
|
||||||
|
if dot > 0.0 {
|
||||||
|
agree += 1;
|
||||||
|
} else {
|
||||||
|
oppose += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oppose > agree
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,428 @@
|
||||||
|
use crate::fit::{DetectedPrimitive, PrimitiveKind};
|
||||||
|
use crate::mesh::Vec3;
|
||||||
|
use crate::sparse_grid::SurfaceSample;
|
||||||
|
use cord_sdf::SdfNode;
|
||||||
|
|
||||||
|
/// Build an SDF tree from detected primitives.
|
||||||
|
///
|
||||||
|
/// Runs box-merging on detected planes before tree construction.
|
||||||
|
/// Each entry in `subtractive` flags whether the corresponding
|
||||||
|
/// primitive represents removed material (hole/cutout). Additive
|
||||||
|
/// primitives are unioned; subtractive ones are differenced from
|
||||||
|
/// the base.
|
||||||
|
pub fn build_sdf_tree(
|
||||||
|
primitives: &[DetectedPrimitive],
|
||||||
|
samples: &[SurfaceSample],
|
||||||
|
subtractive: &[bool],
|
||||||
|
) -> SdfNode {
|
||||||
|
let (merged, merged_sub) = merge_planes_into_boxes(primitives, subtractive, samples);
|
||||||
|
|
||||||
|
if merged.is_empty() {
|
||||||
|
return SdfNode::Sphere { radius: 1.0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if merged.len() == 1 {
|
||||||
|
return primitive_to_sdf(&merged[0], samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut additive = Vec::new();
|
||||||
|
let mut subtractive_nodes = Vec::new();
|
||||||
|
|
||||||
|
for (i, prim) in merged.iter().enumerate() {
|
||||||
|
let is_sub = merged_sub.get(i).copied().unwrap_or(false);
|
||||||
|
if is_sub {
|
||||||
|
subtractive_nodes.push(primitive_to_sdf(prim, samples));
|
||||||
|
} else {
|
||||||
|
additive.push(primitive_to_sdf(prim, samples));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = if additive.len() == 1 {
|
||||||
|
additive.remove(0)
|
||||||
|
} else if additive.is_empty() {
|
||||||
|
if subtractive_nodes.is_empty() {
|
||||||
|
return SdfNode::Sphere { radius: 1.0 };
|
||||||
|
}
|
||||||
|
subtractive_nodes.remove(0)
|
||||||
|
} else {
|
||||||
|
SdfNode::Union(additive)
|
||||||
|
};
|
||||||
|
|
||||||
|
if subtractive_nodes.is_empty() {
|
||||||
|
base
|
||||||
|
} else {
|
||||||
|
SdfNode::Difference {
|
||||||
|
base: Box::new(base),
|
||||||
|
subtract: subtractive_nodes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge opposing parallel plane pairs into box primitives.
|
||||||
|
///
|
||||||
|
/// Scans for pairs of planes whose normals are antiparallel (dot < -0.95).
|
||||||
|
/// If three mutually orthogonal pairs are found, they form a box.
|
||||||
|
/// Remaining pairs that can't form a full box are emitted as-is.
|
||||||
|
fn merge_planes_into_boxes(
|
||||||
|
primitives: &[DetectedPrimitive],
|
||||||
|
subtractive: &[bool],
|
||||||
|
_samples: &[SurfaceSample],
|
||||||
|
) -> (Vec<DetectedPrimitive>, Vec<bool>) {
|
||||||
|
let mut plane_indices: Vec<usize> = Vec::new();
|
||||||
|
let mut non_plane_indices: Vec<usize> = Vec::new();
|
||||||
|
|
||||||
|
for (i, prim) in primitives.iter().enumerate() {
|
||||||
|
match &prim.kind {
|
||||||
|
PrimitiveKind::Plane { .. } => plane_indices.push(i),
|
||||||
|
_ => non_plane_indices.push(i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plane_indices.len() < 2 {
|
||||||
|
return (primitives.to_vec(), subtractive.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find opposing plane pairs
|
||||||
|
let mut used = vec![false; primitives.len()];
|
||||||
|
let mut pairs: Vec<(usize, usize, Vec3, f64)> = Vec::new(); // (i, j, axis, half_thickness)
|
||||||
|
|
||||||
|
for a in 0..plane_indices.len() {
|
||||||
|
if used[plane_indices[a]] { continue; }
|
||||||
|
for b in (a + 1)..plane_indices.len() {
|
||||||
|
if used[plane_indices[b]] { continue; }
|
||||||
|
let ia = plane_indices[a];
|
||||||
|
let ib = plane_indices[b];
|
||||||
|
if let (
|
||||||
|
PrimitiveKind::Plane { point: p1, normal: n1 },
|
||||||
|
PrimitiveKind::Plane { point: p2, normal: n2 },
|
||||||
|
) = (&primitives[ia].kind, &primitives[ib].kind) {
|
||||||
|
let dot = n1.dot(*n2);
|
||||||
|
if dot < -0.95 {
|
||||||
|
// Opposing parallel planes
|
||||||
|
let axis = n1.normalized();
|
||||||
|
let d1 = axis.dot(*p1);
|
||||||
|
let d2 = axis.dot(*p2);
|
||||||
|
let half = (d1 - d2).abs() / 2.0;
|
||||||
|
if half > 1e-6 {
|
||||||
|
let center_along_axis = (d1 + d2) / 2.0;
|
||||||
|
let _ = center_along_axis;
|
||||||
|
pairs.push((ia, ib, axis, half));
|
||||||
|
used[ia] = true;
|
||||||
|
used[ib] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to form boxes from 3 mutually orthogonal pairs
|
||||||
|
let mut box_groups: Vec<Vec<usize>> = Vec::new(); // groups of pair indices
|
||||||
|
let mut pair_used = vec![false; pairs.len()];
|
||||||
|
|
||||||
|
for a in 0..pairs.len() {
|
||||||
|
if pair_used[a] { continue; }
|
||||||
|
for b in (a + 1)..pairs.len() {
|
||||||
|
if pair_used[b] { continue; }
|
||||||
|
let dot_ab = pairs[a].2.dot(pairs[b].2).abs();
|
||||||
|
if dot_ab > 0.1 { continue; } // not orthogonal
|
||||||
|
for c in (b + 1)..pairs.len() {
|
||||||
|
if pair_used[c] { continue; }
|
||||||
|
let dot_ac = pairs[a].2.dot(pairs[c].2).abs();
|
||||||
|
let dot_bc = pairs[b].2.dot(pairs[c].2).abs();
|
||||||
|
if dot_ac < 0.1 && dot_bc < 0.1 {
|
||||||
|
box_groups.push(vec![a, b, c]);
|
||||||
|
pair_used[a] = true;
|
||||||
|
pair_used[b] = true;
|
||||||
|
pair_used[c] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pair_used[a] { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result_prims: Vec<DetectedPrimitive> = Vec::new();
|
||||||
|
let mut result_sub: Vec<bool> = Vec::new();
|
||||||
|
|
||||||
|
// Emit detected boxes
|
||||||
|
for group in &box_groups {
|
||||||
|
let mut half_extents = [0.0f64; 3];
|
||||||
|
let mut center = Vec3::zero();
|
||||||
|
let mut axes = [Vec3::zero(); 3];
|
||||||
|
let mut total_support = Vec::new();
|
||||||
|
let mut total_error = 0.0;
|
||||||
|
let mut sub_votes = 0usize;
|
||||||
|
let mut add_votes = 0usize;
|
||||||
|
|
||||||
|
for (slot, &pair_idx) in group.iter().enumerate() {
|
||||||
|
let (ia, ib, axis, half) = &pairs[pair_idx];
|
||||||
|
axes[slot] = *axis;
|
||||||
|
half_extents[slot] = *half;
|
||||||
|
|
||||||
|
if let (
|
||||||
|
PrimitiveKind::Plane { point: p1, .. },
|
||||||
|
PrimitiveKind::Plane { point: p2, .. },
|
||||||
|
) = (&primitives[*ia].kind, &primitives[*ib].kind) {
|
||||||
|
let d1 = axis.dot(*p1);
|
||||||
|
let d2 = axis.dot(*p2);
|
||||||
|
let mid = (d1 + d2) / 2.0;
|
||||||
|
center = center + *axis * mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_support.extend_from_slice(&primitives[*ia].support);
|
||||||
|
total_support.extend_from_slice(&primitives[*ib].support);
|
||||||
|
total_error += primitives[*ia].fit_error + primitives[*ib].fit_error;
|
||||||
|
|
||||||
|
let sa = subtractive.get(*ia).copied().unwrap_or(false);
|
||||||
|
let sb = subtractive.get(*ib).copied().unwrap_or(false);
|
||||||
|
if sa { sub_votes += 1; } else { add_votes += 1; }
|
||||||
|
if sb { sub_votes += 1; } else { add_votes += 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder half_extents to align with XYZ via the rotation
|
||||||
|
// For now, emit as a box at the computed center with a rotation
|
||||||
|
let (rot_axis, rot_angle, ordered_half) = align_box_axes(axes, half_extents);
|
||||||
|
|
||||||
|
total_support.sort_unstable();
|
||||||
|
total_support.dedup();
|
||||||
|
|
||||||
|
result_prims.push(DetectedPrimitive {
|
||||||
|
kind: PrimitiveKind::Box {
|
||||||
|
center,
|
||||||
|
half_extents: ordered_half,
|
||||||
|
rotation_axis: rot_axis,
|
||||||
|
rotation_angle: rot_angle,
|
||||||
|
},
|
||||||
|
support: total_support,
|
||||||
|
fit_error: total_error,
|
||||||
|
});
|
||||||
|
result_sub.push(sub_votes > add_votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit remaining unpaired pairs as planes
|
||||||
|
for (i, pair) in pairs.iter().enumerate() {
|
||||||
|
if pair_used[i] { continue; }
|
||||||
|
let (ia, ib, _, _) = pair;
|
||||||
|
// Re-mark as unused so they get emitted below
|
||||||
|
used[*ia] = false;
|
||||||
|
used[*ib] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit non-plane primitives and unused planes
|
||||||
|
for &i in &non_plane_indices {
|
||||||
|
result_prims.push(primitives[i].clone());
|
||||||
|
result_sub.push(subtractive.get(i).copied().unwrap_or(false));
|
||||||
|
}
|
||||||
|
for &i in &plane_indices {
|
||||||
|
if !used[i] {
|
||||||
|
result_prims.push(primitives[i].clone());
|
||||||
|
result_sub.push(subtractive.get(i).copied().unwrap_or(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(result_prims, result_sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute rotation to align detected box axes with XYZ.
|
||||||
|
/// Returns (axis, angle_radians, reordered_half_extents).
|
||||||
|
fn align_box_axes(
|
||||||
|
axes: [Vec3; 3],
|
||||||
|
half_extents: [f64; 3],
|
||||||
|
) -> (Vec3, f64, [f64; 3]) {
|
||||||
|
let canonical = [
|
||||||
|
Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
Vec3::new(0.0, 0.0, 1.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assign each detected axis to the closest canonical axis
|
||||||
|
let mut assignment = [0usize; 3];
|
||||||
|
let mut assigned = [false; 3];
|
||||||
|
|
||||||
|
for pass in 0..3 {
|
||||||
|
let mut best_dot = 0.0f64;
|
||||||
|
let mut best_src = 0;
|
||||||
|
let mut best_dst = 0;
|
||||||
|
for src in 0..3 {
|
||||||
|
if assignment[src] != 0 && pass > 0 { continue; }
|
||||||
|
for dst in 0..3 {
|
||||||
|
if assigned[dst] { continue; }
|
||||||
|
let d = axes[src].dot(canonical[dst]).abs();
|
||||||
|
if d > best_dot {
|
||||||
|
best_dot = d;
|
||||||
|
best_src = src;
|
||||||
|
best_dst = dst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assignment[best_src] = best_dst;
|
||||||
|
assigned[best_dst] = true;
|
||||||
|
let _ = pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ordered = [0.0; 3];
|
||||||
|
for i in 0..3 {
|
||||||
|
ordered[assignment[i]] = half_extents[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if axes are already aligned (common case for axis-aligned boxes)
|
||||||
|
let mut sum_dot = 0.0;
|
||||||
|
for i in 0..3 {
|
||||||
|
sum_dot += axes[i].dot(canonical[assignment[i]]).abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
if sum_dot > 2.9 {
|
||||||
|
// Nearly axis-aligned, no rotation needed
|
||||||
|
return (Vec3::new(1.0, 0.0, 0.0), 0.0, ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// General case: compute rotation from detected frame to canonical
|
||||||
|
// Use the first axis mismatch to derive an axis-angle rotation
|
||||||
|
let from = axes[0].normalized();
|
||||||
|
let to = canonical[assignment[0]];
|
||||||
|
let cross = from.cross(to);
|
||||||
|
let dot = from.dot(to);
|
||||||
|
let angle = dot.acos();
|
||||||
|
let axis = if cross.length() > 1e-6 { cross.normalized() } else { Vec3::new(1.0, 0.0, 0.0) };
|
||||||
|
|
||||||
|
(axis, angle, ordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ideal outward-pointing normal of a primitive at a given point.
|
||||||
|
pub(crate) fn ideal_outward_normal(kind: &PrimitiveKind, point: Vec3) -> Vec3 {
|
||||||
|
match kind {
|
||||||
|
PrimitiveKind::Plane { normal, .. } => *normal,
|
||||||
|
|
||||||
|
PrimitiveKind::Sphere { center, .. } => {
|
||||||
|
(point - *center).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveKind::Cylinder { point: axis_point, axis, .. } => {
|
||||||
|
let v = point - *axis_point;
|
||||||
|
let along = *axis * v.dot(*axis);
|
||||||
|
(v - along).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveKind::Box { center, .. } => {
|
||||||
|
(point - *center).normalized()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primitive_to_sdf(prim: &DetectedPrimitive, samples: &[SurfaceSample]) -> SdfNode {
|
||||||
|
match &prim.kind {
|
||||||
|
PrimitiveKind::Plane { point, normal } => {
|
||||||
|
// Plane approximated as large thin box along the normal
|
||||||
|
let thickness = 0.1;
|
||||||
|
let extent = 1000.0;
|
||||||
|
|
||||||
|
let (axis, angle) = rotation_to_align_z(*normal);
|
||||||
|
let node = SdfNode::Box {
|
||||||
|
half_extents: [extent, extent, thickness],
|
||||||
|
};
|
||||||
|
let rotated = if angle.abs() > 1e-6 {
|
||||||
|
SdfNode::Rotate {
|
||||||
|
axis: [axis.x, axis.y, axis.z],
|
||||||
|
angle_deg: angle.to_degrees(),
|
||||||
|
child: Box::new(node),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node
|
||||||
|
};
|
||||||
|
|
||||||
|
SdfNode::Translate {
|
||||||
|
offset: [point.x, point.y, point.z],
|
||||||
|
child: Box::new(rotated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveKind::Sphere { center, radius } => {
|
||||||
|
let node = SdfNode::Sphere { radius: *radius };
|
||||||
|
if center.x.abs() < 1e-6 && center.y.abs() < 1e-6 && center.z.abs() < 1e-6 {
|
||||||
|
node
|
||||||
|
} else {
|
||||||
|
SdfNode::Translate {
|
||||||
|
offset: [center.x, center.y, center.z],
|
||||||
|
child: Box::new(node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveKind::Cylinder { point, axis, radius } => {
|
||||||
|
let height = cylinder_height_from_support(prim, *axis, samples);
|
||||||
|
|
||||||
|
let (rot_axis, angle) = rotation_to_align_z(*axis);
|
||||||
|
let node = SdfNode::Cylinder { radius: *radius, height };
|
||||||
|
let rotated = if angle.abs() > 1e-6 {
|
||||||
|
SdfNode::Rotate {
|
||||||
|
axis: [rot_axis.x, rot_axis.y, rot_axis.z],
|
||||||
|
angle_deg: angle.to_degrees(),
|
||||||
|
child: Box::new(node),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node
|
||||||
|
};
|
||||||
|
|
||||||
|
SdfNode::Translate {
|
||||||
|
offset: [point.x, point.y, point.z],
|
||||||
|
child: Box::new(rotated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveKind::Box { center, half_extents, rotation_axis, rotation_angle } => {
|
||||||
|
let node = SdfNode::Box { half_extents: *half_extents };
|
||||||
|
let rotated = if rotation_angle.abs() > 1e-6 {
|
||||||
|
SdfNode::Rotate {
|
||||||
|
axis: [rotation_axis.x, rotation_axis.y, rotation_axis.z],
|
||||||
|
angle_deg: rotation_angle.to_degrees(),
|
||||||
|
child: Box::new(node),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node
|
||||||
|
};
|
||||||
|
|
||||||
|
if center.x.abs() < 1e-6 && center.y.abs() < 1e-6 && center.z.abs() < 1e-6 {
|
||||||
|
rotated
|
||||||
|
} else {
|
||||||
|
SdfNode::Translate {
|
||||||
|
offset: [center.x, center.y, center.z],
|
||||||
|
child: Box::new(rotated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimate cylinder height from support points projected onto the axis.
|
||||||
|
fn cylinder_height_from_support(prim: &DetectedPrimitive, axis: Vec3, samples: &[SurfaceSample]) -> f64 {
|
||||||
|
if prim.support.is_empty() {
|
||||||
|
return 10.0;
|
||||||
|
}
|
||||||
|
let mut min_t = f64::MAX;
|
||||||
|
let mut max_t = f64::MIN;
|
||||||
|
for &idx in &prim.support {
|
||||||
|
if idx >= samples.len() { continue; }
|
||||||
|
let t = samples[idx].position.dot(axis);
|
||||||
|
min_t = min_t.min(t);
|
||||||
|
max_t = max_t.max(t);
|
||||||
|
}
|
||||||
|
if max_t > min_t { max_t - min_t } else { 10.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute axis-angle rotation that aligns +Z to the given direction.
|
||||||
|
fn rotation_to_align_z(target: Vec3) -> (Vec3, f64) {
|
||||||
|
let z = Vec3::new(0.0, 0.0, 1.0);
|
||||||
|
let dot = z.dot(target);
|
||||||
|
|
||||||
|
if dot > 0.9999 {
|
||||||
|
return (Vec3::new(1.0, 0.0, 0.0), 0.0);
|
||||||
|
}
|
||||||
|
if dot < -0.9999 {
|
||||||
|
return (Vec3::new(1.0, 0.0, 0.0), std::f64::consts::PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
let axis = z.cross(target).normalized();
|
||||||
|
let angle = dot.acos();
|
||||||
|
(axis, angle)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
use crate::bvh::BVH;
|
||||||
|
use crate::mesh::{AABB, TriangleMesh, Vec3};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Adaptive sparse octree storing SDF values.
|
||||||
|
/// Only cells near the surface (where the sign changes) are refined.
|
||||||
|
pub struct SparseGrid {
|
||||||
|
pub bounds: AABB,
|
||||||
|
pub max_depth: u8,
|
||||||
|
pub cells: HashMap<CellKey, CellData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct CellKey {
|
||||||
|
pub depth: u8,
|
||||||
|
pub x: u32,
|
||||||
|
pub y: u32,
|
||||||
|
pub z: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CellData {
|
||||||
|
pub center: Vec3,
|
||||||
|
pub size: f64,
|
||||||
|
pub sdf_value: f64,
|
||||||
|
pub normal: Vec3,
|
||||||
|
pub is_surface: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A surface sample extracted from the grid — point + normal on the zero isosurface.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SurfaceSample {
|
||||||
|
pub position: Vec3,
|
||||||
|
pub normal: Vec3,
|
||||||
|
pub cell_key: CellKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SparseGrid {
|
||||||
|
pub fn from_mesh(mesh: &TriangleMesh, bvh: &BVH, max_depth: u8) -> Self {
|
||||||
|
let padding = mesh.bounds.diagonal() * 0.05;
|
||||||
|
let bounds = AABB {
|
||||||
|
min: mesh.bounds.min - Vec3::new(padding, padding, padding),
|
||||||
|
max: mesh.bounds.max + Vec3::new(padding, padding, padding),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut grid = SparseGrid {
|
||||||
|
bounds,
|
||||||
|
max_depth,
|
||||||
|
cells: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seed at depth 0
|
||||||
|
grid.subdivide_recursive(mesh, bvh, CellKey { depth: 0, x: 0, y: 0, z: 0 });
|
||||||
|
grid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_bounds(&self, key: CellKey) -> AABB {
|
||||||
|
let divisions = (1u32 << key.depth) as f64;
|
||||||
|
let extent = self.bounds.max - self.bounds.min;
|
||||||
|
let cell_size = Vec3::new(
|
||||||
|
extent.x / divisions,
|
||||||
|
extent.y / divisions,
|
||||||
|
extent.z / divisions,
|
||||||
|
);
|
||||||
|
let min = Vec3::new(
|
||||||
|
self.bounds.min.x + key.x as f64 * cell_size.x,
|
||||||
|
self.bounds.min.y + key.y as f64 * cell_size.y,
|
||||||
|
self.bounds.min.z + key.z as f64 * cell_size.z,
|
||||||
|
);
|
||||||
|
AABB { min, max: min + cell_size }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subdivide_recursive(&mut self, mesh: &TriangleMesh, bvh: &BVH, key: CellKey) {
|
||||||
|
let cb = self.cell_bounds(key);
|
||||||
|
let center = cb.center();
|
||||||
|
let size = cb.diagonal();
|
||||||
|
|
||||||
|
let sdf_value = bvh.signed_distance(mesh, center);
|
||||||
|
let normal = sdf_gradient(mesh, bvh, center);
|
||||||
|
|
||||||
|
let is_surface = sdf_value.abs() < size * 0.75;
|
||||||
|
|
||||||
|
self.cells.insert(key, CellData {
|
||||||
|
center,
|
||||||
|
size,
|
||||||
|
sdf_value,
|
||||||
|
normal,
|
||||||
|
is_surface,
|
||||||
|
});
|
||||||
|
|
||||||
|
if key.depth < self.max_depth && is_surface {
|
||||||
|
// Refine: subdivide into 8 children
|
||||||
|
let child_depth = key.depth + 1;
|
||||||
|
for dz in 0..2u32 {
|
||||||
|
for dy in 0..2u32 {
|
||||||
|
for dx in 0..2u32 {
|
||||||
|
let child_key = CellKey {
|
||||||
|
depth: child_depth,
|
||||||
|
x: key.x * 2 + dx,
|
||||||
|
y: key.y * 2 + dy,
|
||||||
|
z: key.z * 2 + dz,
|
||||||
|
};
|
||||||
|
self.subdivide_recursive(mesh, bvh, child_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract surface samples — cells at max depth that straddle the surface.
|
||||||
|
pub fn surface_samples(&self) -> Vec<SurfaceSample> {
|
||||||
|
self.cells.iter()
|
||||||
|
.filter(|(_, data)| data.is_surface && data.sdf_value.abs() < data.size * 0.5)
|
||||||
|
.map(|(key, data)| SurfaceSample {
|
||||||
|
position: data.center,
|
||||||
|
normal: data.normal,
|
||||||
|
cell_key: *key,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract leaf cells (deepest level for each spatial region).
|
||||||
|
pub fn leaf_cells(&self) -> Vec<(&CellKey, &CellData)> {
|
||||||
|
self.cells.iter()
|
||||||
|
.filter(|(key, _)| {
|
||||||
|
// A cell is a leaf if it has no children in the map
|
||||||
|
if key.depth >= self.max_depth {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let child_depth = key.depth + 1;
|
||||||
|
let child_key = CellKey {
|
||||||
|
depth: child_depth,
|
||||||
|
x: key.x * 2,
|
||||||
|
y: key.y * 2,
|
||||||
|
z: key.z * 2,
|
||||||
|
};
|
||||||
|
!self.cells.contains_key(&child_key)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn surface_cell_count(&self) -> usize {
|
||||||
|
self.cells.values().filter(|d| d.is_surface).count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute SDF gradient (approximate normal) via central differences.
|
||||||
|
fn sdf_gradient(mesh: &TriangleMesh, bvh: &BVH, p: Vec3) -> Vec3 {
|
||||||
|
let eps = 0.001;
|
||||||
|
let dx = bvh.signed_distance(mesh, Vec3::new(p.x + eps, p.y, p.z))
|
||||||
|
- bvh.signed_distance(mesh, Vec3::new(p.x - eps, p.y, p.z));
|
||||||
|
let dy = bvh.signed_distance(mesh, Vec3::new(p.x, p.y + eps, p.z))
|
||||||
|
- bvh.signed_distance(mesh, Vec3::new(p.x, p.y, p.z - eps));
|
||||||
|
let dz = bvh.signed_distance(mesh, Vec3::new(p.x, p.y, p.z + eps))
|
||||||
|
- bvh.signed_distance(mesh, Vec3::new(p.x, p.y, p.z - eps));
|
||||||
|
Vec3::new(dx, dy, dz).normalized()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
use cord_decompile::crawler::oracle::SdfOracle;
|
||||||
|
use cord_decompile::crawler::scheduler::CrawlerScheduler;
|
||||||
|
use cord_decompile::crawler::CrawlerConfig;
|
||||||
|
use cord_decompile::mesh::{Vec3, AABB};
|
||||||
|
|
||||||
|
/// A perfect sphere oracle for testing (no mesh needed).
|
||||||
|
struct SphereOracle {
|
||||||
|
center: Vec3,
|
||||||
|
radius: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SdfOracle for SphereOracle {
|
||||||
|
fn sdf(&self, p: Vec3) -> f64 {
|
||||||
|
(p - self.center).length() - self.radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crawlers_find_sphere_surface() {
|
||||||
|
let oracle = SphereOracle {
|
||||||
|
center: Vec3::zero(),
|
||||||
|
radius: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bounds = AABB {
|
||||||
|
min: Vec3::new(-2.0, -2.0, -2.0),
|
||||||
|
max: Vec3::new(2.0, 2.0, 2.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = CrawlerConfig {
|
||||||
|
initial_probes: 32,
|
||||||
|
crawlers_per_contact: 2,
|
||||||
|
step_fraction: 0.01,
|
||||||
|
surface_epsilon: 1e-3,
|
||||||
|
max_steps: 2000,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut scheduler = CrawlerScheduler::new(bounds, config);
|
||||||
|
let samples = scheduler.run(&oracle);
|
||||||
|
|
||||||
|
assert!(!samples.is_empty(), "should find surface samples");
|
||||||
|
|
||||||
|
// All samples should be on or very near the sphere surface
|
||||||
|
for s in &samples {
|
||||||
|
let dist = s.position.length();
|
||||||
|
assert!(
|
||||||
|
(dist - 1.0).abs() < 0.05,
|
||||||
|
"sample at distance {dist} from origin, expected ~1.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-expr"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Expression parser for Cordial (.crd) — parses expressions into TrigGraph"
|
||||||
|
license = "Unlicense"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["parser", "expression", "sdf", "trig"]
|
||||||
|
categories = ["parsing", "mathematics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
use cord_trig::ir::{NodeId, TrigOp};
|
||||||
|
use crate::parser::ExprParser;
|
||||||
|
|
||||||
|
impl<'a> ExprParser<'a> {
|
||||||
|
pub(crate) fn build_saw(&mut self, x: NodeId) -> Result<NodeId, String> {
|
||||||
|
let s = self.graph.push(TrigOp::Sin(x));
|
||||||
|
let c = self.graph.push(TrigOp::Cos(x));
|
||||||
|
let a = self.graph.push(TrigOp::Atan2(s, c));
|
||||||
|
let inv_pi = self.graph.push(TrigOp::Const(1.0 / std::f64::consts::PI));
|
||||||
|
Ok(self.graph.push(TrigOp::Mul(a, inv_pi)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_tri(&mut self, x: NodeId) -> Result<NodeId, String> {
|
||||||
|
let saw = self.build_saw(x)?;
|
||||||
|
let a = self.graph.push(TrigOp::Abs(saw));
|
||||||
|
let two = self.graph.push(TrigOp::Const(2.0));
|
||||||
|
let scaled = self.graph.push(TrigOp::Mul(two, a));
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
Ok(self.graph.push(TrigOp::Sub(scaled, one)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_square(&mut self, x: NodeId) -> Result<NodeId, String> {
|
||||||
|
let s = self.graph.push(TrigOp::Sin(x));
|
||||||
|
let k = self.graph.push(TrigOp::Const(1000.0));
|
||||||
|
let raw = self.graph.push(TrigOp::Mul(s, k));
|
||||||
|
let lo = self.graph.push(TrigOp::Const(-1.0));
|
||||||
|
let hi = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
Ok(self.graph.push(TrigOp::Clamp { val: raw, lo, hi }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_mix(&mut self, a: NodeId, b: NodeId, t: NodeId) -> Result<NodeId, String> {
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let omt = self.graph.push(TrigOp::Sub(one, t));
|
||||||
|
let at = self.graph.push(TrigOp::Mul(a, omt));
|
||||||
|
let bt = self.graph.push(TrigOp::Mul(b, t));
|
||||||
|
Ok(self.graph.push(TrigOp::Add(at, bt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_smoothstep(&mut self, lo: NodeId, hi: NodeId, x: NodeId) -> Result<NodeId, String> {
|
||||||
|
let lo_val = match self.graph.nodes.get(lo as usize) {
|
||||||
|
Some(TrigOp::Const(v)) => *v,
|
||||||
|
_ => return Err("smoothstep: lo must be a constant".into()),
|
||||||
|
};
|
||||||
|
let hi_val = match self.graph.nodes.get(hi as usize) {
|
||||||
|
Some(TrigOp::Const(v)) => *v,
|
||||||
|
_ => return Err("smoothstep: hi must be a constant".into()),
|
||||||
|
};
|
||||||
|
let range = hi_val - lo_val;
|
||||||
|
if range.abs() < 1e-15 {
|
||||||
|
return Err("smoothstep: lo and hi must differ".into());
|
||||||
|
}
|
||||||
|
let inv_range = self.graph.push(TrigOp::Const(1.0 / range));
|
||||||
|
let diff = self.graph.push(TrigOp::Sub(x, lo));
|
||||||
|
let raw = self.graph.push(TrigOp::Mul(diff, inv_range));
|
||||||
|
let zero = self.graph.push(TrigOp::Const(0.0));
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let t = self.graph.push(TrigOp::Clamp { val: raw, lo: zero, hi: one });
|
||||||
|
let t2 = self.graph.push(TrigOp::Mul(t, t));
|
||||||
|
let two = self.graph.push(TrigOp::Const(2.0));
|
||||||
|
let three = self.graph.push(TrigOp::Const(3.0));
|
||||||
|
let two_t = self.graph.push(TrigOp::Mul(two, t));
|
||||||
|
let coeff = self.graph.push(TrigOp::Sub(three, two_t));
|
||||||
|
Ok(self.graph.push(TrigOp::Mul(t2, coeff)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_quantize(&mut self, x: NodeId, n: NodeId) -> Result<NodeId, String> {
|
||||||
|
let n_val = match self.graph.nodes.get(n as usize) {
|
||||||
|
Some(TrigOp::Const(v)) if *v >= 1.0 => *v,
|
||||||
|
_ => return Err("quantize: n must be a constant >= 1".into()),
|
||||||
|
};
|
||||||
|
let xn = self.graph.push(TrigOp::Mul(x, n));
|
||||||
|
let two_pi = self.graph.push(TrigOp::Const(2.0 * std::f64::consts::PI));
|
||||||
|
let phase = self.graph.push(TrigOp::Mul(xn, two_pi));
|
||||||
|
let s = self.graph.push(TrigOp::Sin(phase));
|
||||||
|
let c = self.graph.push(TrigOp::Cos(phase));
|
||||||
|
let a = self.graph.push(TrigOp::Atan2(s, c));
|
||||||
|
let inv_two_pi = self.graph.push(TrigOp::Const(1.0 / (2.0 * std::f64::consts::PI)));
|
||||||
|
let fract_centered = self.graph.push(TrigOp::Mul(a, inv_two_pi));
|
||||||
|
let half = self.graph.push(TrigOp::Const(0.5));
|
||||||
|
let fract = self.graph.push(TrigOp::Add(fract_centered, half));
|
||||||
|
let floor = self.graph.push(TrigOp::Sub(xn, fract));
|
||||||
|
let inv_n = self.graph.push(TrigOp::Const(1.0 / n_val));
|
||||||
|
Ok(self.graph.push(TrigOp::Mul(floor, inv_n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_am(&mut self, signal: NodeId, modulator: NodeId, depth: NodeId) -> Result<NodeId, String> {
|
||||||
|
let dm = self.graph.push(TrigOp::Mul(depth, modulator));
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let env = self.graph.push(TrigOp::Add(one, dm));
|
||||||
|
Ok(self.graph.push(TrigOp::Mul(signal, env)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_fm(&mut self, carrier: NodeId, modulator: NodeId, depth: NodeId) -> Result<NodeId, String> {
|
||||||
|
let dm = self.graph.push(TrigOp::Mul(depth, modulator));
|
||||||
|
let phase = self.graph.push(TrigOp::Add(carrier, dm));
|
||||||
|
Ok(self.graph.push(TrigOp::Sin(phase)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_lpf_node(&mut self, signal: NodeId, k: NodeId) -> NodeId {
|
||||||
|
let a = self.graph.push(TrigOp::Atan2(signal, k));
|
||||||
|
let ka = self.graph.push(TrigOp::Mul(k, a));
|
||||||
|
let coeff = self.graph.push(TrigOp::Const(2.0 / std::f64::consts::PI));
|
||||||
|
self.graph.push(TrigOp::Mul(ka, coeff))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_lpf(&mut self, signal: NodeId, k: NodeId) -> Result<NodeId, String> {
|
||||||
|
Ok(self.build_lpf_node(signal, k))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_hpf(&mut self, signal: NodeId, k: NodeId) -> Result<NodeId, String> {
|
||||||
|
let lp = self.build_lpf_node(signal, k);
|
||||||
|
Ok(self.graph.push(TrigOp::Sub(signal, lp)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_bpf(&mut self, signal: NodeId, lo: NodeId, hi: NodeId) -> Result<NodeId, String> {
|
||||||
|
let lp_hi = self.build_lpf_node(signal, hi);
|
||||||
|
let lp_lo = self.build_lpf_node(signal, lo);
|
||||||
|
Ok(self.graph.push(TrigOp::Sub(lp_hi, lp_lo)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_dft(&mut self, signal: NodeId, n: NodeId) -> Result<NodeId, String> {
|
||||||
|
let n_val = match self.graph.nodes.get(n as usize) {
|
||||||
|
Some(TrigOp::Const(v)) if *v >= 1.0 && *v <= 64.0 => *v as u32,
|
||||||
|
_ => return Err("dft: n must be a constant between 1 and 64".into()),
|
||||||
|
};
|
||||||
|
let mut sum: Option<NodeId> = None;
|
||||||
|
for k in 1..=n_val {
|
||||||
|
let kf = self.graph.push(TrigOp::Const(k as f64));
|
||||||
|
let ks = self.graph.push(TrigOp::Mul(kf, signal));
|
||||||
|
let s = self.graph.push(TrigOp::Sin(ks));
|
||||||
|
let inv_k = self.graph.push(TrigOp::Const(1.0 / k as f64));
|
||||||
|
let term = self.graph.push(TrigOp::Mul(s, inv_k));
|
||||||
|
sum = Some(match sum {
|
||||||
|
None => term,
|
||||||
|
Some(prev) => self.graph.push(TrigOp::Add(prev, term)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(sum.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::parse_expr;
|
||||||
|
use cord_trig::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn saw_wave() {
|
||||||
|
let g = parse_expr("saw(x)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g, std::f64::consts::FRAC_PI_2, 0.0, 0.0) - 0.5).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tri_wave() {
|
||||||
|
let g = parse_expr("tri(x)").unwrap();
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -1.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn square_wave() {
|
||||||
|
let g = parse_expr("square(x)").unwrap();
|
||||||
|
assert!((evaluate(&g, std::f64::consts::FRAC_PI_4, 0.0, 0.0) - 1.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, -std::f64::consts::FRAC_PI_4, 0.0, 0.0) - -1.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mix_lerp() {
|
||||||
|
let g = parse_expr("mix(x, y, 0.25)").unwrap();
|
||||||
|
assert!((evaluate(&g, 10.0, 20.0, 0.0) - 12.5).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smoothstep_edges() {
|
||||||
|
let g = parse_expr("smoothstep(0, 1, x)").unwrap();
|
||||||
|
assert!(evaluate(&g, -1.0, 0.0, 0.0).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g, 2.0, 0.0, 0.0) - 1.0).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g, 0.5, 0.0, 0.0) - 0.5).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fm_synthesis() {
|
||||||
|
let g = parse_expr("fm(x, y, 2)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lpf_saturates() {
|
||||||
|
let g = parse_expr("lpf(x, 1)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0).abs() < 1e-10);
|
||||||
|
let val = evaluate(&g, 1000.0, 0.0, 0.0);
|
||||||
|
assert!((val - 1.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dft_single_harmonic() {
|
||||||
|
let g = parse_expr("dft(x, 1)").unwrap();
|
||||||
|
let val = evaluate(&g, std::f64::consts::FRAC_PI_2, 0.0, 0.0);
|
||||||
|
assert!((val - 1.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn envelope_and_phase() {
|
||||||
|
let g_env = parse_expr("envelope(x)").unwrap();
|
||||||
|
let g_phase = parse_expr("phase(x)").unwrap();
|
||||||
|
assert!((evaluate(&g_env, 3.0, 0.0, 0.0) - 10.0_f64.sqrt()).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g_phase, 1.0, 0.0, 0.0) - std::f64::consts::FRAC_PI_4).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,459 @@
|
||||||
|
use cord_trig::ir::{NodeId, TrigOp};
|
||||||
|
use crate::parser::{ExprParser, require_args};
|
||||||
|
|
||||||
|
impl<'a> ExprParser<'a> {
|
||||||
|
pub(crate) fn parse_function_call(&mut self, name: &str) -> Result<NodeId, String> {
|
||||||
|
self.expect(&crate::token::Token::LParen)?;
|
||||||
|
let args = self.parse_arg_list()?;
|
||||||
|
self.expect(&crate::token::Token::RParen)?;
|
||||||
|
|
||||||
|
match name {
|
||||||
|
"sin" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Sin(args[0])))
|
||||||
|
}
|
||||||
|
"cos" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Cos(args[0])))
|
||||||
|
}
|
||||||
|
"abs" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Abs(args[0])))
|
||||||
|
}
|
||||||
|
"tan" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Tan(args[0])))
|
||||||
|
}
|
||||||
|
"asin" | "arcsin" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Asin(args[0])))
|
||||||
|
}
|
||||||
|
"acos" | "arccos" | "arcos" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Acos(args[0])))
|
||||||
|
}
|
||||||
|
"atan" | "arctan" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Atan(args[0])))
|
||||||
|
}
|
||||||
|
"sinh" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Sinh(args[0])))
|
||||||
|
}
|
||||||
|
"cosh" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Cosh(args[0])))
|
||||||
|
}
|
||||||
|
"tanh" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Tanh(args[0])))
|
||||||
|
}
|
||||||
|
"asinh" | "arcsinh" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Asinh(args[0])))
|
||||||
|
}
|
||||||
|
"acosh" | "arccosh" | "arcosh" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Acosh(args[0])))
|
||||||
|
}
|
||||||
|
"atanh" | "arctanh" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Atanh(args[0])))
|
||||||
|
}
|
||||||
|
"sqrt" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Sqrt(args[0])))
|
||||||
|
}
|
||||||
|
"exp" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Exp(args[0])))
|
||||||
|
}
|
||||||
|
"ln" | "log" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Ln(args[0])))
|
||||||
|
}
|
||||||
|
"hypot" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Hypot(args[0], args[1])))
|
||||||
|
}
|
||||||
|
"atan2" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Atan2(args[0], args[1])))
|
||||||
|
}
|
||||||
|
"min" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Min(args[0], args[1])))
|
||||||
|
}
|
||||||
|
"max" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Max(args[0], args[1])))
|
||||||
|
}
|
||||||
|
"length" | "mag" => {
|
||||||
|
match args.len() {
|
||||||
|
2 => Ok(self.graph.push(TrigOp::Hypot(args[0], args[1]))),
|
||||||
|
3 => {
|
||||||
|
let xy = self.graph.push(TrigOp::Hypot(args[0], args[1]));
|
||||||
|
Ok(self.graph.push(TrigOp::Hypot(xy, args[2])))
|
||||||
|
}
|
||||||
|
_ => Err(format!("{name}() requires 2 or 3 arguments")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"sphere" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
let ix = self.get_x();
|
||||||
|
let iy = self.get_y();
|
||||||
|
let iz = self.get_z();
|
||||||
|
let xy = self.graph.push(TrigOp::Hypot(ix, iy));
|
||||||
|
let mag = self.graph.push(TrigOp::Hypot(xy, iz));
|
||||||
|
let r = self.graph.push(TrigOp::Sub(mag, args[0]));
|
||||||
|
Ok(self.mark_obj(r))
|
||||||
|
}
|
||||||
|
"box" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
let ix = self.get_x();
|
||||||
|
let iy = self.get_y();
|
||||||
|
let iz = self.get_z();
|
||||||
|
let zero = self.graph.push(TrigOp::Const(0.0));
|
||||||
|
let ax = self.graph.push(TrigOp::Abs(ix));
|
||||||
|
let ay = self.graph.push(TrigOp::Abs(iy));
|
||||||
|
let az = self.graph.push(TrigOp::Abs(iz));
|
||||||
|
let dx = self.graph.push(TrigOp::Sub(ax, args[0]));
|
||||||
|
let dy = self.graph.push(TrigOp::Sub(ay, args[1]));
|
||||||
|
let dz = self.graph.push(TrigOp::Sub(az, args[2]));
|
||||||
|
let qx = self.graph.push(TrigOp::Max(dx, zero));
|
||||||
|
let qy = self.graph.push(TrigOp::Max(dy, zero));
|
||||||
|
let qz = self.graph.push(TrigOp::Max(dz, zero));
|
||||||
|
let qxy = self.graph.push(TrigOp::Hypot(qx, qy));
|
||||||
|
let q_len = self.graph.push(TrigOp::Hypot(qxy, qz));
|
||||||
|
let m_yz = self.graph.push(TrigOp::Max(dy, dz));
|
||||||
|
let m_xyz = self.graph.push(TrigOp::Max(dx, m_yz));
|
||||||
|
let interior = self.graph.push(TrigOp::Min(m_xyz, zero));
|
||||||
|
let r = self.graph.push(TrigOp::Add(q_len, interior));
|
||||||
|
Ok(self.mark_obj(r))
|
||||||
|
}
|
||||||
|
"cylinder" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let ix = self.get_x();
|
||||||
|
let iy = self.get_y();
|
||||||
|
let iz = self.get_z();
|
||||||
|
let zero = self.graph.push(TrigOp::Const(0.0));
|
||||||
|
let xy = self.graph.push(TrigOp::Hypot(ix, iy));
|
||||||
|
let dr = self.graph.push(TrigOp::Sub(xy, args[0]));
|
||||||
|
let az = self.graph.push(TrigOp::Abs(iz));
|
||||||
|
let dz = self.graph.push(TrigOp::Sub(az, args[1]));
|
||||||
|
let qr = self.graph.push(TrigOp::Max(dr, zero));
|
||||||
|
let qz = self.graph.push(TrigOp::Max(dz, zero));
|
||||||
|
let q_len = self.graph.push(TrigOp::Hypot(qr, qz));
|
||||||
|
let m = self.graph.push(TrigOp::Max(dr, dz));
|
||||||
|
let interior = self.graph.push(TrigOp::Min(m, zero));
|
||||||
|
let r = self.graph.push(TrigOp::Add(q_len, interior));
|
||||||
|
Ok(self.mark_obj(r))
|
||||||
|
}
|
||||||
|
"saw" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
self.build_saw(args[0])
|
||||||
|
}
|
||||||
|
"tri" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
self.build_tri(args[0])
|
||||||
|
}
|
||||||
|
"square" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
self.build_square(args[0])
|
||||||
|
}
|
||||||
|
"mix" | "lerp" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
self.build_mix(args[0], args[1], args[2])
|
||||||
|
}
|
||||||
|
"smoothstep" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
self.build_smoothstep(args[0], args[1], args[2])
|
||||||
|
}
|
||||||
|
"clip" | "clamp" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
Ok(self.graph.push(TrigOp::Clamp { val: args[0], lo: args[1], hi: args[2] }))
|
||||||
|
}
|
||||||
|
"quantize" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
self.build_quantize(args[0], args[1])
|
||||||
|
}
|
||||||
|
"am" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
self.build_am(args[0], args[1], args[2])
|
||||||
|
}
|
||||||
|
"fm" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
self.build_fm(args[0], args[1], args[2])
|
||||||
|
}
|
||||||
|
"lpf" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
self.build_lpf(args[0], args[1])
|
||||||
|
}
|
||||||
|
"hpf" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
self.build_hpf(args[0], args[1])
|
||||||
|
}
|
||||||
|
"bpf" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
self.build_bpf(args[0], args[1], args[2])
|
||||||
|
}
|
||||||
|
"dft" | "harmonics" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
self.build_dft(args[0], args[1])
|
||||||
|
}
|
||||||
|
"hilbert" | "envelope" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
Ok(self.graph.push(TrigOp::Hypot(args[0], one)))
|
||||||
|
}
|
||||||
|
"phase" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
Ok(self.graph.push(TrigOp::Atan2(args[0], one)))
|
||||||
|
}
|
||||||
|
"ngon" => {
|
||||||
|
if args.is_empty() {
|
||||||
|
return Err("ngon(n, side): at least 2 arguments required".into());
|
||||||
|
}
|
||||||
|
let n = match self.graph.nodes.get(args[0] as usize) {
|
||||||
|
Some(TrigOp::Const(v)) if *v >= 3.0 && *v == (*v as u32 as f64) => *v as u32,
|
||||||
|
_ => return Err("ngon: first argument must be an integer >= 3".into()),
|
||||||
|
};
|
||||||
|
let r = self.parse_ngon(n, &args[1..])?;
|
||||||
|
Ok(self.mark_obj(r))
|
||||||
|
}
|
||||||
|
"translate" | "mov" | "move" => {
|
||||||
|
require_args(name, &args, 4)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let nx = self.graph.push(TrigOp::Sub(x, args[1]));
|
||||||
|
let ny = self.graph.push(TrigOp::Sub(y, args[2]));
|
||||||
|
let nz = self.graph.push(TrigOp::Sub(z, args[3]));
|
||||||
|
let r = self.remap_inputs(args[0], nx, ny, nz);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"rotate_x" | "rx" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let c = self.graph.push(TrigOp::Cos(args[1]));
|
||||||
|
let s = self.graph.push(TrigOp::Sin(args[1]));
|
||||||
|
let yc = self.graph.push(TrigOp::Mul(y, c));
|
||||||
|
let zs = self.graph.push(TrigOp::Mul(z, s));
|
||||||
|
let ny = self.graph.push(TrigOp::Add(yc, zs));
|
||||||
|
let ys = self.graph.push(TrigOp::Mul(y, s));
|
||||||
|
let zc = self.graph.push(TrigOp::Mul(z, c));
|
||||||
|
let nz = self.graph.push(TrigOp::Sub(zc, ys));
|
||||||
|
let r = self.remap_inputs(args[0], x, ny, nz);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"rotate_y" | "ry" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let c = self.graph.push(TrigOp::Cos(args[1]));
|
||||||
|
let s = self.graph.push(TrigOp::Sin(args[1]));
|
||||||
|
let xc = self.graph.push(TrigOp::Mul(x, c));
|
||||||
|
let zs = self.graph.push(TrigOp::Mul(z, s));
|
||||||
|
let nx = self.graph.push(TrigOp::Sub(xc, zs));
|
||||||
|
let xs = self.graph.push(TrigOp::Mul(x, s));
|
||||||
|
let zc = self.graph.push(TrigOp::Mul(z, c));
|
||||||
|
let nz = self.graph.push(TrigOp::Add(xs, zc));
|
||||||
|
let r = self.remap_inputs(args[0], nx, y, nz);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"rotate_z" | "rz" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let c = self.graph.push(TrigOp::Cos(args[1]));
|
||||||
|
let s = self.graph.push(TrigOp::Sin(args[1]));
|
||||||
|
let xc = self.graph.push(TrigOp::Mul(x, c));
|
||||||
|
let ys = self.graph.push(TrigOp::Mul(y, s));
|
||||||
|
let nx = self.graph.push(TrigOp::Add(xc, ys));
|
||||||
|
let xs = self.graph.push(TrigOp::Mul(x, s));
|
||||||
|
let yc = self.graph.push(TrigOp::Mul(y, c));
|
||||||
|
let ny = self.graph.push(TrigOp::Sub(yc, xs));
|
||||||
|
let r = self.remap_inputs(args[0], nx, ny, z);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"scale" => {
|
||||||
|
let is_obj_arg = self.is_obj_node(args[0]);
|
||||||
|
match args.len() {
|
||||||
|
2 => {
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let nx = self.graph.push(TrigOp::Div(x, args[1]));
|
||||||
|
let ny = self.graph.push(TrigOp::Div(y, args[1]));
|
||||||
|
let nz = self.graph.push(TrigOp::Div(z, args[1]));
|
||||||
|
let remapped = self.remap_inputs(args[0], nx, ny, nz);
|
||||||
|
let r = self.graph.push(TrigOp::Mul(remapped, args[1]));
|
||||||
|
if is_obj_arg { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let nx = self.graph.push(TrigOp::Div(x, args[1]));
|
||||||
|
let ny = self.graph.push(TrigOp::Div(y, args[2]));
|
||||||
|
let nz = self.graph.push(TrigOp::Div(z, args[3]));
|
||||||
|
let remapped = self.remap_inputs(args[0], nx, ny, nz);
|
||||||
|
let ax = self.graph.push(TrigOp::Abs(args[1]));
|
||||||
|
let ay = self.graph.push(TrigOp::Abs(args[2]));
|
||||||
|
let az = self.graph.push(TrigOp::Abs(args[3]));
|
||||||
|
let mxy = self.graph.push(TrigOp::Min(ax, ay));
|
||||||
|
let min_s = self.graph.push(TrigOp::Min(mxy, az));
|
||||||
|
let r = self.graph.push(TrigOp::Mul(remapped, min_s));
|
||||||
|
if is_obj_arg { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
_ => Err("scale() requires 2 or 4 arguments".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"mirror_x" | "mx" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let ax = self.graph.push(TrigOp::Abs(x));
|
||||||
|
let r = self.remap_inputs(args[0], ax, y, z);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"mirror_y" | "my" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let ay = self.graph.push(TrigOp::Abs(y));
|
||||||
|
let r = self.remap_inputs(args[0], x, ay, z);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"mirror_z" | "mz" => {
|
||||||
|
require_args(name, &args, 1)?;
|
||||||
|
let x = self.get_x();
|
||||||
|
let y = self.get_y();
|
||||||
|
let z = self.get_z();
|
||||||
|
let az = self.graph.push(TrigOp::Abs(z));
|
||||||
|
let r = self.remap_inputs(args[0], x, y, az);
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"cone" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
let ix = self.get_x();
|
||||||
|
let iy = self.get_y();
|
||||||
|
let iz = self.get_z();
|
||||||
|
let xy = self.graph.push(TrigOp::Hypot(ix, iy));
|
||||||
|
let half_c = self.graph.push(TrigOp::Const(0.5));
|
||||||
|
let half_h = self.graph.push(TrigOp::Mul(args[2], half_c));
|
||||||
|
let az = self.graph.push(TrigOp::Abs(iz));
|
||||||
|
let dz = self.graph.push(TrigOp::Sub(az, half_h));
|
||||||
|
let one_c = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let inv_h = self.graph.push(TrigOp::Div(one_c, args[2]));
|
||||||
|
let t_raw = self.graph.push(TrigOp::Mul(iz, inv_h));
|
||||||
|
let half = self.graph.push(TrigOp::Const(0.5));
|
||||||
|
let t = self.graph.push(TrigOp::Add(t_raw, half));
|
||||||
|
let zero = self.graph.push(TrigOp::Const(0.0));
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let tc = self.graph.push(TrigOp::Clamp { val: t, lo: zero, hi: one });
|
||||||
|
let dr = self.graph.push(TrigOp::Sub(args[1], args[0]));
|
||||||
|
let r_off = self.graph.push(TrigOp::Mul(tc, dr));
|
||||||
|
let r_at_z = self.graph.push(TrigOp::Add(args[0], r_off));
|
||||||
|
let d_radial = self.graph.push(TrigOp::Sub(xy, r_at_z));
|
||||||
|
let qr = self.graph.push(TrigOp::Max(d_radial, zero));
|
||||||
|
let qz = self.graph.push(TrigOp::Max(dz, zero));
|
||||||
|
let q_len = self.graph.push(TrigOp::Hypot(qr, qz));
|
||||||
|
let m = self.graph.push(TrigOp::Max(d_radial, dz));
|
||||||
|
let interior = self.graph.push(TrigOp::Min(m, zero));
|
||||||
|
let r = self.graph.push(TrigOp::Add(q_len, interior));
|
||||||
|
Ok(self.mark_obj(r))
|
||||||
|
}
|
||||||
|
"union" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let r = self.graph.push(TrigOp::Min(args[0], args[1]));
|
||||||
|
if self.is_obj_node(args[0]) || self.is_obj_node(args[1]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"intersect" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let r = self.graph.push(TrigOp::Max(args[0], args[1]));
|
||||||
|
if self.is_obj_node(args[0]) || self.is_obj_node(args[1]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"smooth_union" | "smin" => {
|
||||||
|
require_args(name, &args, 3)?;
|
||||||
|
let a = args[0];
|
||||||
|
let b = args[1];
|
||||||
|
let k = args[2];
|
||||||
|
let half = self.graph.push(TrigOp::Const(0.5));
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let zero = self.graph.push(TrigOp::Const(0.0));
|
||||||
|
let diff = self.graph.push(TrigOp::Sub(b, a));
|
||||||
|
let div = self.graph.push(TrigOp::Div(diff, k));
|
||||||
|
let scaled = self.graph.push(TrigOp::Mul(div, half));
|
||||||
|
let shifted = self.graph.push(TrigOp::Add(half, scaled));
|
||||||
|
let h = self.graph.push(TrigOp::Clamp { val: shifted, lo: zero, hi: one });
|
||||||
|
let one_minus_h = self.graph.push(TrigOp::Sub(one, h));
|
||||||
|
let term_b = self.graph.push(TrigOp::Mul(b, one_minus_h));
|
||||||
|
let term_a = self.graph.push(TrigOp::Mul(a, h));
|
||||||
|
let mixed = self.graph.push(TrigOp::Add(term_b, term_a));
|
||||||
|
let kh = self.graph.push(TrigOp::Mul(k, h));
|
||||||
|
let correction = self.graph.push(TrigOp::Mul(kh, one_minus_h));
|
||||||
|
let r = self.graph.push(TrigOp::Sub(mixed, correction));
|
||||||
|
if self.is_obj_node(a) || self.is_obj_node(b) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"diff" | "subtract" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let neg_b = self.graph.push(TrigOp::Neg(args[1]));
|
||||||
|
let r = self.graph.push(TrigOp::Max(args[0], neg_b));
|
||||||
|
if self.is_obj_node(args[0]) || self.is_obj_node(args[1]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
"offset" => {
|
||||||
|
require_args(name, &args, 2)?;
|
||||||
|
let r = self.graph.push(TrigOp::Sub(args[0], args[1]));
|
||||||
|
if self.is_obj_node(args[0]) { self.mark_obj(r); }
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if name.ends_with("gon") && name.len() > 3 {
|
||||||
|
if let Ok(n) = name[..name.len() - 3].parse::<u32>() {
|
||||||
|
let r = self.parse_ngon(n, &args)?;
|
||||||
|
return Ok(self.mark_obj(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(func) = self.funcs.get(name) {
|
||||||
|
let params = func.params.clone();
|
||||||
|
let defaults = func.defaults.clone();
|
||||||
|
let body = func.body.clone();
|
||||||
|
let r = self.call_user_func_inner(params, defaults, body, &args, name)?;
|
||||||
|
if self.is_obj_node(r) { self.mark_obj(r); }
|
||||||
|
return Ok(r);
|
||||||
|
}
|
||||||
|
if let Some(sch) = self.schematics.get(name) {
|
||||||
|
let params = sch.params.clone();
|
||||||
|
let defaults = sch.defaults.clone();
|
||||||
|
let body = sch.body.clone();
|
||||||
|
let vr = sch.value_returning;
|
||||||
|
let r = self.call_schematic(params, defaults, body, vr, &args, name)?;
|
||||||
|
if self.is_obj_node(r) { self.mark_obj(r); }
|
||||||
|
return Ok(r);
|
||||||
|
}
|
||||||
|
Err(self.err_at(format!("unknown function: {name}()")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use cord_trig::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
|
||||||
|
pub fn classify(graph: &TrigGraph) -> ExprInfo {
|
||||||
|
let mut info = ExprInfo::default();
|
||||||
|
info.node_count = graph.nodes.len();
|
||||||
|
|
||||||
|
for op in &graph.nodes {
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX => info.uses_x = true,
|
||||||
|
TrigOp::InputY => info.uses_y = true,
|
||||||
|
TrigOp::InputZ => info.uses_z = true,
|
||||||
|
TrigOp::Sin(_) | TrigOp::Cos(_) | TrigOp::Tan(_)
|
||||||
|
| TrigOp::Asin(_) | TrigOp::Acos(_) | TrigOp::Atan(_)
|
||||||
|
| TrigOp::Sinh(_) | TrigOp::Cosh(_) | TrigOp::Tanh(_)
|
||||||
|
| TrigOp::Asinh(_) | TrigOp::Acosh(_) | TrigOp::Atanh(_) => info.has_trig = true,
|
||||||
|
TrigOp::Sqrt(_) | TrigOp::Exp(_) | TrigOp::Ln(_) => info.has_multiply = true,
|
||||||
|
TrigOp::Hypot(_, _) | TrigOp::Atan2(_, _) => info.has_vectoring = true,
|
||||||
|
TrigOp::Mul(_, _) => info.has_multiply = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.dimensions = info.uses_x as u8 + info.uses_y as u8 + info.uses_z as u8;
|
||||||
|
|
||||||
|
let cost = graph.cordic_cost();
|
||||||
|
info.cordic_rotation = cost.rotation;
|
||||||
|
info.cordic_vectoring = cost.vectoring;
|
||||||
|
info.cordic_linear = cost.linear;
|
||||||
|
info.cordic_binary = cost.binary;
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ExprInfo {
|
||||||
|
pub node_count: usize,
|
||||||
|
pub dimensions: u8,
|
||||||
|
pub uses_x: bool,
|
||||||
|
pub uses_y: bool,
|
||||||
|
pub uses_z: bool,
|
||||||
|
pub has_trig: bool,
|
||||||
|
pub has_vectoring: bool,
|
||||||
|
pub has_multiply: bool,
|
||||||
|
pub cordic_rotation: u32,
|
||||||
|
pub cordic_vectoring: u32,
|
||||||
|
pub cordic_linear: u32,
|
||||||
|
pub cordic_binary: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExprInfo {
|
||||||
|
pub fn total_cordic_passes(&self) -> u32 {
|
||||||
|
self.cordic_rotation + self.cordic_vectoring + self.cordic_linear
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dimension_label(&self) -> &'static str {
|
||||||
|
match self.dimensions {
|
||||||
|
0 => "constant",
|
||||||
|
1 => "1D curve",
|
||||||
|
2 => "2D surface",
|
||||||
|
3 => "3D field",
|
||||||
|
_ => "?",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classify_from(graph: &TrigGraph, root: NodeId) -> ExprInfo {
|
||||||
|
let mut info = ExprInfo::default();
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
let mut stack = vec![root];
|
||||||
|
|
||||||
|
while let Some(id) = stack.pop() {
|
||||||
|
if !visited.insert(id) { continue; }
|
||||||
|
info.node_count += 1;
|
||||||
|
let op = &graph.nodes[id as usize];
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX => info.uses_x = true,
|
||||||
|
TrigOp::InputY => info.uses_y = true,
|
||||||
|
TrigOp::InputZ => info.uses_z = true,
|
||||||
|
TrigOp::Sin(_) | TrigOp::Cos(_) | TrigOp::Tan(_)
|
||||||
|
| TrigOp::Asin(_) | TrigOp::Acos(_) | TrigOp::Atan(_)
|
||||||
|
| TrigOp::Sinh(_) | TrigOp::Cosh(_) | TrigOp::Tanh(_)
|
||||||
|
| TrigOp::Asinh(_) | TrigOp::Acosh(_) | TrigOp::Atanh(_) => info.has_trig = true,
|
||||||
|
TrigOp::Sqrt(_) | TrigOp::Exp(_) | TrigOp::Ln(_) => info.has_multiply = true,
|
||||||
|
TrigOp::Hypot(_, _) | TrigOp::Atan2(_, _) => info.has_vectoring = true,
|
||||||
|
TrigOp::Mul(_, _) => info.has_multiply = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match op {
|
||||||
|
TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b) | TrigOp::Div(a, b)
|
||||||
|
| TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
|
||||||
|
| TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
|
||||||
|
stack.push(*a);
|
||||||
|
stack.push(*b);
|
||||||
|
}
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a)
|
||||||
|
| TrigOp::Sin(a) | TrigOp::Cos(a) | TrigOp::Tan(a)
|
||||||
|
| TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
stack.push(*a);
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
stack.push(*val);
|
||||||
|
stack.push(*lo);
|
||||||
|
stack.push(*hi);
|
||||||
|
}
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ | TrigOp::Const(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.dimensions = info.uses_x as u8 + info.uses_y as u8 + info.uses_z as u8;
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expr_to_sdf(
|
||||||
|
graph: &mut TrigGraph,
|
||||||
|
expr_root: NodeId,
|
||||||
|
dims: u8,
|
||||||
|
radius: f64,
|
||||||
|
domain: f64,
|
||||||
|
) -> NodeId {
|
||||||
|
let ix = graph.push(TrigOp::InputX);
|
||||||
|
let iy = graph.push(TrigOp::InputY);
|
||||||
|
let iz = graph.push(TrigOp::InputZ);
|
||||||
|
let r = graph.push(TrigOp::Const(radius));
|
||||||
|
let dom = graph.push(TrigOp::Const(domain));
|
||||||
|
|
||||||
|
let sdf = match dims {
|
||||||
|
0 | 1 => {
|
||||||
|
let dy = graph.push(TrigOp::Sub(iy, expr_root));
|
||||||
|
let tube = graph.push(TrigOp::Hypot(dy, iz));
|
||||||
|
graph.push(TrigOp::Sub(tube, r))
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let dz = graph.push(TrigOp::Sub(iz, expr_root));
|
||||||
|
let abs_dz = graph.push(TrigOp::Abs(dz));
|
||||||
|
graph.push(TrigOp::Sub(abs_dz, r))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let abs_f = graph.push(TrigOp::Abs(expr_root));
|
||||||
|
graph.push(TrigOp::Sub(abs_f, r))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let abs_x = graph.push(TrigOp::Abs(ix));
|
||||||
|
let abs_y = graph.push(TrigOp::Abs(iy));
|
||||||
|
let abs_z = graph.push(TrigOp::Abs(iz));
|
||||||
|
let bx = graph.push(TrigOp::Sub(abs_x, dom));
|
||||||
|
let by = graph.push(TrigOp::Sub(abs_y, dom));
|
||||||
|
let bz = graph.push(TrigOp::Sub(abs_z, dom));
|
||||||
|
let bxy = graph.push(TrigOp::Max(bx, by));
|
||||||
|
let bbox = graph.push(TrigOp::Max(bxy, bz));
|
||||||
|
|
||||||
|
graph.push(TrigOp::Max(sdf, bbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn references_ident(source: &str, var: &str) -> bool {
|
||||||
|
let bytes = source.as_bytes();
|
||||||
|
let var_bytes = var.as_bytes();
|
||||||
|
let var_len = var_bytes.len();
|
||||||
|
|
||||||
|
for i in 0..=bytes.len().saturating_sub(var_len) {
|
||||||
|
if &bytes[i..i + var_len] == var_bytes {
|
||||||
|
let before_ok = i == 0
|
||||||
|
|| !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_');
|
||||||
|
let after_ok = i + var_len >= bytes.len()
|
||||||
|
|| !(bytes[i + var_len].is_ascii_alphanumeric() || bytes[i + var_len] == b'_');
|
||||||
|
if before_ok && after_ok {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,692 @@
|
||||||
|
mod token;
|
||||||
|
mod parser;
|
||||||
|
mod remap;
|
||||||
|
mod builders;
|
||||||
|
mod ngon;
|
||||||
|
mod userfunc;
|
||||||
|
mod builtins;
|
||||||
|
mod classify;
|
||||||
|
|
||||||
|
use cord_trig::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
use token::{tokenize, merge_ngon_tokens_with_lines, Token};
|
||||||
|
use parser::ExprParser;
|
||||||
|
|
||||||
|
pub use classify::{classify, classify_from, expr_to_sdf, ExprInfo};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use classify::references_ident;
|
||||||
|
|
||||||
|
pub fn parse_expr(input: &str) -> Result<TrigGraph, String> {
|
||||||
|
parse_expr_ctx(input, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_expr_ctx(
|
||||||
|
input: &str,
|
||||||
|
ref_a: Option<&TrigGraph>,
|
||||||
|
ref_b: Option<&TrigGraph>,
|
||||||
|
) -> Result<TrigGraph, String> {
|
||||||
|
let (mut tokens, mut token_lines) = tokenize(input)?;
|
||||||
|
merge_ngon_tokens_with_lines(&mut tokens, &mut token_lines);
|
||||||
|
let source_lines: Vec<&str> = input.lines().collect();
|
||||||
|
let mut parser = ExprParser::new(&tokens, &token_lines, &source_lines, ref_a, ref_b);
|
||||||
|
let node = parser.parse_program()?;
|
||||||
|
parser.graph.set_output(node);
|
||||||
|
Ok(parser.graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SceneResult {
|
||||||
|
pub graph: TrigGraph,
|
||||||
|
pub objects: Vec<(String, NodeId)>,
|
||||||
|
pub all_vars: Vec<(String, NodeId)>,
|
||||||
|
pub casts: Vec<(String, NodeId)>,
|
||||||
|
pub cast_all: bool,
|
||||||
|
pub plots: Vec<NodeId>,
|
||||||
|
pub plot_all: bool,
|
||||||
|
pub bare_exprs: Vec<NodeId>,
|
||||||
|
pub needs_cast: bool,
|
||||||
|
pub needs_plot: bool,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_expr_scene(input: &str) -> Result<SceneResult, String> {
|
||||||
|
parse_expr_scene_ctx(input, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_expr_scene_ctx(
|
||||||
|
input: &str,
|
||||||
|
ref_a: Option<&TrigGraph>,
|
||||||
|
ref_b: Option<&TrigGraph>,
|
||||||
|
) -> Result<SceneResult, String> {
|
||||||
|
let (mut tokens, mut token_lines) = tokenize(input)?;
|
||||||
|
merge_ngon_tokens_with_lines(&mut tokens, &mut token_lines);
|
||||||
|
let source_lines: Vec<&str> = input.lines().collect();
|
||||||
|
let mut parser = ExprParser::new(&tokens, &token_lines, &source_lines, ref_a, ref_b);
|
||||||
|
let node = parser.parse_program()?;
|
||||||
|
parser.graph.set_output(node);
|
||||||
|
|
||||||
|
let mut auto_plots: Vec<NodeId> = Vec::new();
|
||||||
|
if parser.plot_all {
|
||||||
|
let funcs_snapshot: Vec<(String, Vec<String>, Vec<Option<Vec<Token>>>, Vec<Token>)> = parser.funcs.iter()
|
||||||
|
.filter(|(_, f)| f.params.len() == 1)
|
||||||
|
.map(|(name, f)| (name.clone(), f.params.clone(), f.defaults.clone(), f.body.clone()))
|
||||||
|
.collect();
|
||||||
|
for (name, params, defaults, body) in funcs_snapshot {
|
||||||
|
let x = parser.get_x();
|
||||||
|
if let Ok(node) = parser.call_user_func_inner(params, defaults, body, &[x], &name) {
|
||||||
|
auto_plots.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let objects: Vec<(String, NodeId)> = parser.objects.iter()
|
||||||
|
.filter_map(|name| parser.object_nodes.get(name).map(|&id| (name.clone(), id)))
|
||||||
|
.collect();
|
||||||
|
let all_vars: Vec<(String, NodeId)> = parser.vars.iter()
|
||||||
|
.map(|(name, &id)| (name.clone(), id))
|
||||||
|
.collect();
|
||||||
|
let bare_exprs = parser.bare_exprs.clone();
|
||||||
|
let mut plots = parser.plot_nodes;
|
||||||
|
if parser.plot_all {
|
||||||
|
plots.extend(auto_plots);
|
||||||
|
}
|
||||||
|
Ok(SceneResult {
|
||||||
|
graph: parser.graph,
|
||||||
|
objects,
|
||||||
|
all_vars,
|
||||||
|
casts: parser.cast_nodes,
|
||||||
|
cast_all: parser.cast_all,
|
||||||
|
plots,
|
||||||
|
plot_all: parser.plot_all,
|
||||||
|
bare_exprs,
|
||||||
|
needs_cast: parser.vars_since_last_cast > 0,
|
||||||
|
needs_plot: parser.exprs_since_last_plot > 0,
|
||||||
|
warnings: parser.warnings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_scene(scene: SceneResult) -> TrigGraph {
|
||||||
|
let mut graph = scene.graph;
|
||||||
|
|
||||||
|
let targets: Vec<NodeId> = if scene.cast_all {
|
||||||
|
if scene.objects.is_empty() {
|
||||||
|
scene.all_vars.iter().map(|(_, id)| *id).collect()
|
||||||
|
} else {
|
||||||
|
scene.objects.iter().map(|(_, id)| *id).collect()
|
||||||
|
}
|
||||||
|
} else if !scene.casts.is_empty() {
|
||||||
|
scene.casts.iter().map(|(_, id)| *id).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
if targets.len() > 1 {
|
||||||
|
let mut u = targets[0];
|
||||||
|
for &t in &targets[1..] {
|
||||||
|
u = graph.push(TrigOp::Min(u, t));
|
||||||
|
}
|
||||||
|
graph.set_output(u);
|
||||||
|
} else if targets.len() == 1 {
|
||||||
|
graph.set_output(targets[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cord_trig::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_simple() {
|
||||||
|
let g = parse_expr("x + y").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 4.0, 0.0) - 7.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_trig() {
|
||||||
|
let g = parse_expr("sin(x)").unwrap();
|
||||||
|
let val = evaluate(&g, std::f64::consts::FRAC_PI_2, 0.0, 0.0);
|
||||||
|
assert!((val - 1.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_sphere() {
|
||||||
|
let g = parse_expr("sphere(5)").unwrap();
|
||||||
|
assert!((evaluate(&g, 5.0, 0.0, 0.0) - 0.0).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -5.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nested() {
|
||||||
|
let g = parse_expr("sin(x) * cos(y) + z").unwrap();
|
||||||
|
let val = evaluate(&g, 0.0, 0.0, 7.0);
|
||||||
|
assert!((val - 7.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_expr() {
|
||||||
|
let g = parse_expr("sin(x) + y * z").unwrap();
|
||||||
|
let info = classify(&g);
|
||||||
|
assert_eq!(info.dimensions, 3);
|
||||||
|
assert!(info.has_trig);
|
||||||
|
assert!(info.has_multiply);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_ref_a() {
|
||||||
|
let a = parse_expr("x + 1").unwrap();
|
||||||
|
let b = parse_expr_ctx("A * 2", Some(&a), None).unwrap();
|
||||||
|
assert!((evaluate(&b, 3.0, 0.0, 0.0) - 8.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn box_sdf() {
|
||||||
|
let g = parse_expr("box(1, 1, 1)").unwrap();
|
||||||
|
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cylinder_sdf() {
|
||||||
|
let g = parse_expr("cylinder(2, 3)").unwrap();
|
||||||
|
assert!((evaluate(&g, 2.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn references_ident_check() {
|
||||||
|
assert!(references_ident("A + 3", "A"));
|
||||||
|
assert!(references_ident("sqrt(A)", "A"));
|
||||||
|
assert!(!references_ident("abs(x)", "A"));
|
||||||
|
assert!(!references_ident("max(x, y)", "A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_comment() {
|
||||||
|
let g = parse_expr("// a sphere\nsphere(3)").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 0.0, 0.0)).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_comment() {
|
||||||
|
let g = parse_expr("sphere(/* radius */ 3)").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 0.0, 0.0)).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reassignment() {
|
||||||
|
let g = parse_expr("let a = 5\na = a + 1\na").unwrap();
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - 6.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reassignment_chain() {
|
||||||
|
let g = parse_expr("let a = box(2,2,2)\nlet b = a/pi\nb = 1/b*pi").unwrap();
|
||||||
|
let val = evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
let expected = -std::f64::consts::PI.powi(2) / 2.0;
|
||||||
|
assert!((val - expected).abs() < 1e-6, "got {val}, expected {expected}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_annotation() {
|
||||||
|
let g = parse_expr("let a: f64 = 3.0\na + 1").unwrap();
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - 4.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_annotation_sdf() {
|
||||||
|
let g = parse_expr("let s: sdf = sphere(3)\ns").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clip_alias() {
|
||||||
|
let g = parse_expr("clip(x, 0, 1)").unwrap();
|
||||||
|
assert!((evaluate(&g, 0.5, 0.0, 0.0) - 0.5).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g, -1.0, 0.0, 0.0) - 0.0).abs() < 1e-10);
|
||||||
|
assert!((evaluate(&g, 5.0, 0.0, 0.0) - 1.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translate_sphere() {
|
||||||
|
let g = parse_expr("translate(sphere(3), 5, 0, 0)").unwrap();
|
||||||
|
assert!((evaluate(&g, 5.0, 0.0, 0.0) - -3.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 8.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - 2.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotate_z_sphere() {
|
||||||
|
let g = parse_expr("rotate_z(sphere(3), 1.0)").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -3.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotate_z_box() {
|
||||||
|
let g = parse_expr("let b = box(1,2,1); rotate_z(b, pi/2)").unwrap();
|
||||||
|
assert!((evaluate(&g, 2.0, 0.0, 0.0)).abs() < 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scale_sphere() {
|
||||||
|
let g = parse_expr("scale(sphere(1), 3)").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -3.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mirror_x_sphere() {
|
||||||
|
let g = parse_expr("mirror_x(sphere(3))").unwrap();
|
||||||
|
assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, -3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mirror_x_translated() {
|
||||||
|
let g = parse_expr("mirror_x(translate(sphere(1), 5, 0, 0))").unwrap();
|
||||||
|
assert!((evaluate(&g, 5.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, -5.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn union_two_spheres() {
|
||||||
|
let g = parse_expr("let a = sphere(1); let b = translate(sphere(1), 5, 0, 0); union(a, b)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&g, 5.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&g, 2.5, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diff_two_spheres() {
|
||||||
|
let g = parse_expr("let a = sphere(3); let b = sphere(2); diff(a, b)").unwrap();
|
||||||
|
assert!(evaluate(&g, 2.5, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&g, 1.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intersect_two() {
|
||||||
|
let g = parse_expr("let a = sphere(3); let b = translate(sphere(3), 2, 0, 0); intersect(a, b)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&g, 4.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_object_scene() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = translate(sphere(1), 5, 0, 0)"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.objects.len(), 2);
|
||||||
|
assert_eq!(scene.objects[0].0, "a");
|
||||||
|
assert_eq!(scene.objects[1].0, "b");
|
||||||
|
let b_val = evaluate(&scene.graph, 5.0, 0.0, 0.0);
|
||||||
|
assert!(b_val < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_object_with_bare_expr() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\na"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.objects.len(), 2);
|
||||||
|
assert!((evaluate(&scene.graph, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_object_no_union() {
|
||||||
|
let scene = parse_expr_scene("let a: Obj = sphere(3)").unwrap();
|
||||||
|
assert_eq!(scene.objects.len(), 1);
|
||||||
|
assert_eq!(scene.objects[0].0, "a");
|
||||||
|
assert!((evaluate(&scene.graph, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn obj_type_case_insensitive() {
|
||||||
|
let scene = parse_expr_scene("let a: obj = sphere(1)").unwrap();
|
||||||
|
assert_eq!(scene.objects.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn objects_preserve_node_ids() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = sphere(5)"
|
||||||
|
).unwrap();
|
||||||
|
let a_id = scene.objects[0].1;
|
||||||
|
let b_id = scene.objects[1].1;
|
||||||
|
assert_ne!(a_id, b_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cast_specific_object() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\ncast(a)"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.objects.len(), 2);
|
||||||
|
assert_eq!(scene.casts.len(), 1);
|
||||||
|
assert_eq!(scene.casts[0].0, "a");
|
||||||
|
assert!(!scene.cast_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cast_all_objects() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\ncast()"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.objects.len(), 2);
|
||||||
|
assert!(scene.cast_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot_cast_syntax() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\na.cast()"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.casts.len(), 1);
|
||||||
|
assert_eq!(scene.casts[0].0, "a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot_cast_non_obj_errors() {
|
||||||
|
let result = parse_expr_scene("let a = 5\na.cast()");
|
||||||
|
match result {
|
||||||
|
Err(e) => assert!(e.contains("not an Obj"), "got: {e}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cast_undefined_errors() {
|
||||||
|
let result = parse_expr_scene("cast(z_obj)");
|
||||||
|
match result {
|
||||||
|
Err(e) => assert!(e.contains("not defined"), "got: {e}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_cast_no_render() {
|
||||||
|
let scene = parse_expr_scene("sphere(3)").unwrap();
|
||||||
|
assert!(scene.casts.is_empty());
|
||||||
|
assert!(!scene.cast_all);
|
||||||
|
assert!(scene.needs_plot);
|
||||||
|
assert!(!scene.needs_cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cast_all_renders_all_vars() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a = sphere(1)\nlet b = sphere(2)\ncast()"
|
||||||
|
).unwrap();
|
||||||
|
assert!(scene.cast_all);
|
||||||
|
assert_eq!(scene.all_vars.len(), 2);
|
||||||
|
assert!(!scene.needs_cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cast_multiple() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\nlet c: Obj = sphere(3)\ncast(a)\ncast(c)"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.casts.len(), 2);
|
||||||
|
assert_eq!(scene.casts[0].0, "a");
|
||||||
|
assert_eq!(scene.casts[1].0, "c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plot_expr() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\nplot(sin(x))"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.plots.len(), 1);
|
||||||
|
assert!(!scene.plot_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plot_all() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"f(a) = a^2\nplot()"
|
||||||
|
).unwrap();
|
||||||
|
assert!(scene.plot_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cast_only_program() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\ncast(a)"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(scene.casts.len(), 1);
|
||||||
|
assert!(!scene.needs_cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn needs_cast_after_new_var() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a: Obj = sphere(1)\ncast(a)\nlet b = sphere(2)"
|
||||||
|
).unwrap();
|
||||||
|
assert!(scene.needs_cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn needs_cast_no_cast_calls() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a = sphere(1)\nlet b = sphere(2)"
|
||||||
|
).unwrap();
|
||||||
|
assert!(scene.needs_cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_needs_cast_empty() {
|
||||||
|
let scene = parse_expr_scene("sin(x) + cos(y)").unwrap();
|
||||||
|
assert!(!scene.needs_cast);
|
||||||
|
assert!(scene.needs_plot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decimal_dot_still_works() {
|
||||||
|
let g = parse_expr(".5 + .5").unwrap();
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - 1.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_from_scoped() {
|
||||||
|
let scene = parse_expr_scene("let a = sin(x)\nlet b = x + y + z").unwrap();
|
||||||
|
let a_id = scene.all_vars.iter().find(|(n, _)| n == "a").unwrap().1;
|
||||||
|
let info = classify_from(&scene.graph, a_id);
|
||||||
|
assert_eq!(info.dimensions, 1);
|
||||||
|
assert!(info.uses_x);
|
||||||
|
assert!(!info.uses_y);
|
||||||
|
assert!(!info.uses_z);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expr_to_sdf_1d_tube() {
|
||||||
|
let mut g = parse_expr("sin(x)").unwrap();
|
||||||
|
let expr_node = g.output;
|
||||||
|
let sdf = expr_to_sdf(&mut g, expr_node, 1, 0.05, 10.0);
|
||||||
|
g.set_output(sdf);
|
||||||
|
let val = evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
assert!(val.abs() < 0.1, "expected near-zero on curve, got {val}");
|
||||||
|
let val_far = evaluate(&g, 0.0, 5.0, 0.0);
|
||||||
|
assert!(val_far > 1.0, "expected positive far from curve, got {val_far}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expr_to_sdf_2d_surface() {
|
||||||
|
let mut g = parse_expr("x * x + y * y").unwrap();
|
||||||
|
let expr_node = g.output;
|
||||||
|
let sdf = expr_to_sdf(&mut g, expr_node, 2, 0.05, 10.0);
|
||||||
|
g.set_output(sdf);
|
||||||
|
let val = evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
assert!(val.abs() < 0.1, "expected near-zero on surface, got {val}");
|
||||||
|
let val_far = evaluate(&g, 1.0, 1.0, 10.0);
|
||||||
|
assert!(val_far > 1.0, "expected positive far from surface, got {val_far}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expr_to_sdf_bounded() {
|
||||||
|
let mut g = parse_expr("sin(x)").unwrap();
|
||||||
|
let expr_node = g.output;
|
||||||
|
let sdf = expr_to_sdf(&mut g, expr_node, 1, 0.05, 10.0);
|
||||||
|
g.set_output(sdf);
|
||||||
|
let val = evaluate(&g, 20.0, 0.0, 0.0);
|
||||||
|
assert!(val > 5.0, "expected positive outside domain, got {val}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cone_sdf() {
|
||||||
|
let g = parse_expr("cone(3, 1, 5)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&g, 2.0, 0.0, 0.0).abs() < 0.1);
|
||||||
|
assert!(evaluate(&g, 3.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smooth_union_sdf() {
|
||||||
|
let g = parse_expr("smooth_union(sphere(2), translate(sphere(2), 3, 0, 0), 1)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&g, 3.0, 0.0, 0.0) < 0.0);
|
||||||
|
let mid = evaluate(&g, 1.5, 0.0, 0.0);
|
||||||
|
assert!(mid < 0.0, "smooth_union should blend between shapes, got {mid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smooth_union_alias() {
|
||||||
|
let g = parse_expr("smin(sphere(2), translate(sphere(2), 3, 0, 0), 1)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_infer_obj_sphere() {
|
||||||
|
let scene = parse_expr_scene("let s = sphere(3)\ncast()").unwrap();
|
||||||
|
assert!(!scene.objects.is_empty(), "sphere should auto-infer as Obj");
|
||||||
|
assert_eq!(scene.objects[0].0, "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_infer_obj_through_transform() {
|
||||||
|
let scene = parse_expr_scene("let s = translate(sphere(3), 1, 0, 0)\ncast()").unwrap();
|
||||||
|
assert!(!scene.objects.is_empty(), "translated sphere should auto-infer as Obj");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_infer_obj_through_csg() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let a = sphere(3)\nlet b = sphere(1)\nlet c = diff(a, b)\ncast()"
|
||||||
|
).unwrap();
|
||||||
|
assert!(scene.objects.iter().any(|(n, _)| n == "c"), "CSG result should auto-infer as Obj");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_infer_obj_through_sch() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"sch Part(r) { sphere(r) }\nlet s = Part(3)\ncast()"
|
||||||
|
).unwrap();
|
||||||
|
assert!(!scene.objects.is_empty(), "schematic returning Obj should auto-infer");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_infer_obj_through_map() {
|
||||||
|
let scene = parse_expr_scene(
|
||||||
|
"let row = map(i, 0..3) { translate(sphere(1), i * 3, 0, 0) }\ncast()"
|
||||||
|
).unwrap();
|
||||||
|
assert!(!scene.objects.is_empty(), "map of Obj should auto-infer as Obj");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_auto_infer_for_scalars() {
|
||||||
|
let scene = parse_expr_scene("let a = sin(x)\nlet b: Obj = sphere(3)\ncast()").unwrap();
|
||||||
|
assert!(scene.objects.iter().any(|(n, _)| n == "b"));
|
||||||
|
assert!(!scene.objects.iter().any(|(n, _)| n == "a"), "sin(x) should not be Obj");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_infer_dot_cast_works() {
|
||||||
|
let scene = parse_expr_scene("let s = sphere(3)\ns.cast()").unwrap();
|
||||||
|
assert_eq!(scene.casts.len(), 1);
|
||||||
|
assert_eq!(scene.casts[0].0, "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tau_constant() {
|
||||||
|
let g = parse_expr("tau").unwrap();
|
||||||
|
let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
assert!((val - 2.0 * std::f64::consts::PI).abs() < 1e-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tau_equals_two_pi() {
|
||||||
|
let g = parse_expr("tau - 2 * pi").unwrap();
|
||||||
|
let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
assert!(val.abs() < 1e-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tau_uppercase() {
|
||||||
|
let g = parse_expr("TAU").unwrap();
|
||||||
|
let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0);
|
||||||
|
assert!((val - 2.0 * std::f64::consts::PI).abs() < 1e-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_error_obj_plus_num() {
|
||||||
|
let result = parse_expr("sphere(3) + 5");
|
||||||
|
match result {
|
||||||
|
Err(e) => assert!(e.contains("cannot add Obj and Num"), "got: {e}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_error_num_plus_obj() {
|
||||||
|
let result = parse_expr("5 + sphere(3)");
|
||||||
|
match result {
|
||||||
|
Err(e) => assert!(e.contains("cannot add Obj and Num"), "got: {e}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unused_variable_warning() {
|
||||||
|
let scene = parse_expr_scene("let a = sin(x)\nlet b = cos(x)\nb").unwrap();
|
||||||
|
assert!(scene.warnings.iter().any(|w| w.contains("unused variable: a")),
|
||||||
|
"expected unused warning for 'a', got: {:?}", scene.warnings);
|
||||||
|
assert!(!scene.warnings.iter().any(|w| w.contains("unused variable: b")),
|
||||||
|
"b is used, should not warn");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_unused_warning_for_cast_vars() {
|
||||||
|
let scene = parse_expr_scene("let a: Obj = sphere(1)\ncast(a)").unwrap();
|
||||||
|
assert!(scene.warnings.is_empty(), "cast var should not warn: {:?}", scene.warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_converted_retainer() {
|
||||||
|
let src = r#"let d0 = translate(box(22.8044, 3.0784, 0.1600), 0.0000, 0.0000, 0.1600)
|
||||||
|
let p1 = 0.0000*x + 0.0000*y + -1.0000*z - 0.0000
|
||||||
|
let p2 = 0.0000*x + 0.0000*y + 1.0000*z - 3.7742
|
||||||
|
let p3 = intersect(p1, p2)
|
||||||
|
let p4 = 0.0000*x + -0.9353*y + 0.3538*z - 2.3929
|
||||||
|
let p5 = intersect(p3, p4)
|
||||||
|
let p6 = 0.0000*x + 0.9353*y + 0.3538*z - 2.3929
|
||||||
|
let p7 = intersect(p5, p6)
|
||||||
|
let p8 = -0.9216*x + 0.0000*y + 0.3881*z - 20.5374
|
||||||
|
let p9 = intersect(p7, p8)
|
||||||
|
let p10 = 0.9216*x + -0.0000*y + 0.3881*z - 20.5374
|
||||||
|
let p11 = intersect(p9, p10)
|
||||||
|
let o12 = p11 - 0.3000
|
||||||
|
let d13 = diff(d0, translate(o12, 0.0000, 0.0000, 0.2200))
|
||||||
|
let u14 = translate(box(0.6000, 0.1100, 0.1100), 0.6000, 0.1100, 0.1100)
|
||||||
|
let u15 = translate(box(0.8500, 0.1100, 0.2500), 0.3500, 0.1100, -0.0300)
|
||||||
|
let u16 = union(u14, u15)
|
||||||
|
let u17 = union(d13, translate(u16, 23.0844, -0.1100, 0.3200))
|
||||||
|
let u18 = translate(box(0.6000, 0.1100, 0.1100), 0.6000, 0.1100, 0.1100)
|
||||||
|
let u19 = translate(box(0.8500, 0.1100, 0.2500), 0.3500, 0.1100, -0.0300)
|
||||||
|
let u20 = union(u18, u19)
|
||||||
|
let t21 = rotate_z(translate(u20, 23.0844, -0.1100, 0.0000), 3.141593)
|
||||||
|
let u22 = union(u17, translate(t21, 0.0000, 0.0000, 0.3200))
|
||||||
|
let scene: Obj = u22"#;
|
||||||
|
let scene = parse_expr_scene(src).unwrap();
|
||||||
|
let val = evaluate(&scene.graph, 0.0, 0.0, 0.0);
|
||||||
|
assert!(val.is_finite(), "expected finite SDF value, got {val}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
use cord_trig::ir::{NodeId, TrigOp};
|
||||||
|
use crate::parser::ExprParser;
|
||||||
|
|
||||||
|
impl<'a> ExprParser<'a> {
|
||||||
|
pub(crate) fn parse_ngon(&mut self, n: u32, args: &[NodeId]) -> Result<NodeId, String> {
|
||||||
|
if n < 3 {
|
||||||
|
return Err(format!("{n}-gon: need at least 3 sides"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (is_reg, rest) = if !args.is_empty() {
|
||||||
|
if let TrigOp::Const(v) = self.graph.nodes[args[0] as usize] {
|
||||||
|
if v.is_nan() { (true, &args[1..]) } else { (false, args) }
|
||||||
|
} else {
|
||||||
|
(false, args)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(false, args)
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_reg {
|
||||||
|
let side = if rest.is_empty() {
|
||||||
|
self.graph.push(TrigOp::Const(1.0))
|
||||||
|
} else if rest.len() == 1 {
|
||||||
|
rest[0]
|
||||||
|
} else {
|
||||||
|
return Err(format!("{n}-gon(reg[, side])"));
|
||||||
|
};
|
||||||
|
return self.build_regular_ngon(n, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.len() == 1 {
|
||||||
|
return self.build_regular_ngon(n, args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected = 2 * n as usize - 3;
|
||||||
|
if args.len() != expected {
|
||||||
|
return Err(format!(
|
||||||
|
"{n}-gon: expected 1 (side), reg, or {expected} (s,a,s,...) params, got {}",
|
||||||
|
args.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut params = Vec::with_capacity(expected);
|
||||||
|
for (i, &arg) in args.iter().enumerate() {
|
||||||
|
match self.graph.nodes.get(arg as usize) {
|
||||||
|
Some(TrigOp::Const(v)) => params.push(*v),
|
||||||
|
_ => return Err(format!("{n}-gon: param {} must be a constant", i + 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let vertices = construct_polygon_sas(n, ¶ms)?;
|
||||||
|
self.build_polygon_sdf(&vertices)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_regular_ngon(&mut self, n: u32, side: NodeId) -> Result<NodeId, String> {
|
||||||
|
let ix = self.get_x();
|
||||||
|
let iy = self.get_y();
|
||||||
|
|
||||||
|
let inv_2tan = 1.0 / (2.0 * (std::f64::consts::PI / n as f64).tan());
|
||||||
|
let scale = self.graph.push(TrigOp::Const(inv_2tan));
|
||||||
|
let inradius = self.graph.push(TrigOp::Mul(side, scale));
|
||||||
|
|
||||||
|
let mut result: Option<NodeId> = None;
|
||||||
|
for i in 0..n {
|
||||||
|
let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
|
||||||
|
let cx = self.graph.push(TrigOp::Const(angle.cos()));
|
||||||
|
let cy = self.graph.push(TrigOp::Const(angle.sin()));
|
||||||
|
let dx = self.graph.push(TrigOp::Mul(ix, cx));
|
||||||
|
let dy = self.graph.push(TrigOp::Mul(iy, cy));
|
||||||
|
let dot = self.graph.push(TrigOp::Add(dx, dy));
|
||||||
|
let edge = self.graph.push(TrigOp::Sub(dot, inradius));
|
||||||
|
result = Some(match result {
|
||||||
|
None => edge,
|
||||||
|
Some(prev) => self.graph.push(TrigOp::Max(prev, edge)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_polygon_sdf(&mut self, vertices: &[(f64, f64)]) -> Result<NodeId, String> {
|
||||||
|
let n = vertices.len();
|
||||||
|
let ix = self.get_x();
|
||||||
|
let iy = self.get_y();
|
||||||
|
|
||||||
|
let mut result: Option<NodeId> = None;
|
||||||
|
for i in 0..n {
|
||||||
|
let j = (i + 1) % n;
|
||||||
|
let (x0, y0) = vertices[i];
|
||||||
|
let (x1, y1) = vertices[j];
|
||||||
|
|
||||||
|
let dx = x1 - x0;
|
||||||
|
let dy = y1 - y0;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len < 1e-15 { continue; }
|
||||||
|
|
||||||
|
let nx = dy / len;
|
||||||
|
let ny = -dx / len;
|
||||||
|
let offset = nx * x0 + ny * y0;
|
||||||
|
|
||||||
|
let cnx = self.graph.push(TrigOp::Const(nx));
|
||||||
|
let cny = self.graph.push(TrigOp::Const(ny));
|
||||||
|
let cd = self.graph.push(TrigOp::Const(offset));
|
||||||
|
|
||||||
|
let dot_x = self.graph.push(TrigOp::Mul(ix, cnx));
|
||||||
|
let dot_y = self.graph.push(TrigOp::Mul(iy, cny));
|
||||||
|
let dot = self.graph.push(TrigOp::Add(dot_x, dot_y));
|
||||||
|
let dist = self.graph.push(TrigOp::Sub(dot, cd));
|
||||||
|
|
||||||
|
result = Some(match result {
|
||||||
|
None => dist,
|
||||||
|
Some(prev) => self.graph.push(TrigOp::Max(prev, dist)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ok_or_else(|| "degenerate polygon".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn construct_polygon_sas(n: u32, params: &[f64]) -> Result<Vec<(f64, f64)>, String> {
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
let mut vertices = Vec::with_capacity(n as usize);
|
||||||
|
let mut x = 0.0_f64;
|
||||||
|
let mut y = 0.0_f64;
|
||||||
|
let mut heading = 0.0_f64;
|
||||||
|
|
||||||
|
vertices.push((x, y));
|
||||||
|
|
||||||
|
for i in 0..(n as usize - 1) {
|
||||||
|
let side = params[i * 2];
|
||||||
|
if side <= 0.0 {
|
||||||
|
return Err(format!("side {} must be positive", i + 1));
|
||||||
|
}
|
||||||
|
x += side * heading.cos();
|
||||||
|
y += side * heading.sin();
|
||||||
|
vertices.push((x, y));
|
||||||
|
|
||||||
|
if i * 2 + 1 < params.len() {
|
||||||
|
let interior = params[i * 2 + 1];
|
||||||
|
heading += PI - interior;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cx: f64 = vertices.iter().map(|v| v.0).sum::<f64>() / n as f64;
|
||||||
|
let cy: f64 = vertices.iter().map(|v| v.1).sum::<f64>() / n as f64;
|
||||||
|
for v in &mut vertices {
|
||||||
|
v.0 -= cx;
|
||||||
|
v.1 -= cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut area2 = 0.0;
|
||||||
|
for i in 0..vertices.len() {
|
||||||
|
let j = (i + 1) % vertices.len();
|
||||||
|
area2 += vertices[i].0 * vertices[j].1 - vertices[j].0 * vertices[i].1;
|
||||||
|
}
|
||||||
|
if area2 < 0.0 {
|
||||||
|
vertices.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vertices)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::parse_expr;
|
||||||
|
use cord_trig::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_square() {
|
||||||
|
let g = parse_expr("4-gon(2)").unwrap();
|
||||||
|
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_function_syntax() {
|
||||||
|
let g = parse_expr("ngon(4, 2)").unwrap();
|
||||||
|
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_reg_keyword() {
|
||||||
|
let g = parse_expr("4-gon(reg, 2)").unwrap();
|
||||||
|
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_reg_default_side() {
|
||||||
|
let g = parse_expr("4-gon(reg)").unwrap();
|
||||||
|
assert!((evaluate(&g, 0.5, 0.0, 0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -0.5).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_reg_default_triangle() {
|
||||||
|
let g = parse_expr("3-gon(reg)").unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_sas_equilateral() {
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
let src = format!("3-gon(2, {}, 2)", PI / 3.0);
|
||||||
|
let g = parse_expr(&src).unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_sas_right_triangle() {
|
||||||
|
use std::f64::consts::FRAC_PI_2;
|
||||||
|
let src = format!("3-gon(3, {}, 4)", FRAC_PI_2);
|
||||||
|
let g = parse_expr(&src).unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ngon_sas_square() {
|
||||||
|
use std::f64::consts::FRAC_PI_2;
|
||||||
|
let src = format!("4-gon(2, {0}, 2, {0}, 2)", FRAC_PI_2);
|
||||||
|
let g = parse_expr(&src).unwrap();
|
||||||
|
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,475 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use cord_trig::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
use crate::token::Token;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct UserFunc {
|
||||||
|
pub(crate) params: Vec<String>,
|
||||||
|
pub(crate) defaults: Vec<Option<Vec<Token>>>,
|
||||||
|
pub(crate) body: Vec<Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Schematic {
|
||||||
|
pub(crate) params: Vec<String>,
|
||||||
|
pub(crate) defaults: Vec<Option<Vec<Token>>>,
|
||||||
|
pub(crate) body: Vec<Token>,
|
||||||
|
pub(crate) value_returning: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ExprParser<'a> {
|
||||||
|
pub(crate) tokens: &'a [Token],
|
||||||
|
pub(crate) token_lines: &'a [usize],
|
||||||
|
pub(crate) source_lines: &'a [&'a str],
|
||||||
|
pub(crate) pos: usize,
|
||||||
|
pub(crate) graph: TrigGraph,
|
||||||
|
pub(crate) input_x: Option<NodeId>,
|
||||||
|
pub(crate) input_y: Option<NodeId>,
|
||||||
|
pub(crate) input_z: Option<NodeId>,
|
||||||
|
pub(crate) ref_a: Option<&'a TrigGraph>,
|
||||||
|
pub(crate) ref_b: Option<&'a TrigGraph>,
|
||||||
|
pub(crate) vars: HashMap<String, NodeId>,
|
||||||
|
pub(crate) funcs: HashMap<String, UserFunc>,
|
||||||
|
pub(crate) schematics: HashMap<String, Schematic>,
|
||||||
|
pub(crate) objects: Vec<String>,
|
||||||
|
pub(crate) object_nodes: HashMap<String, NodeId>,
|
||||||
|
pub(crate) obj_results: HashSet<NodeId>,
|
||||||
|
pub(crate) cast_nodes: Vec<(String, NodeId)>,
|
||||||
|
pub(crate) cast_all: bool,
|
||||||
|
pub(crate) plot_nodes: Vec<NodeId>,
|
||||||
|
pub(crate) plot_all: bool,
|
||||||
|
pub(crate) bare_exprs: Vec<NodeId>,
|
||||||
|
pub(crate) vars_since_last_cast: u32,
|
||||||
|
pub(crate) exprs_since_last_plot: u32,
|
||||||
|
pub(crate) used_vars: HashSet<String>,
|
||||||
|
pub(crate) warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ExprParser<'a> {
|
||||||
|
pub(crate) fn new(
|
||||||
|
tokens: &'a [Token],
|
||||||
|
token_lines: &'a [usize],
|
||||||
|
source_lines: &'a [&'a str],
|
||||||
|
ref_a: Option<&'a TrigGraph>,
|
||||||
|
ref_b: Option<&'a TrigGraph>,
|
||||||
|
) -> Self {
|
||||||
|
ExprParser {
|
||||||
|
tokens,
|
||||||
|
token_lines,
|
||||||
|
source_lines,
|
||||||
|
pos: 0,
|
||||||
|
graph: TrigGraph::new(),
|
||||||
|
input_x: None,
|
||||||
|
input_y: None,
|
||||||
|
input_z: None,
|
||||||
|
ref_a,
|
||||||
|
ref_b,
|
||||||
|
vars: HashMap::new(),
|
||||||
|
funcs: HashMap::new(),
|
||||||
|
schematics: HashMap::new(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
object_nodes: HashMap::new(),
|
||||||
|
obj_results: HashSet::new(),
|
||||||
|
cast_nodes: Vec::new(),
|
||||||
|
cast_all: false,
|
||||||
|
plot_nodes: Vec::new(),
|
||||||
|
plot_all: false,
|
||||||
|
bare_exprs: Vec::new(),
|
||||||
|
vars_since_last_cast: 0,
|
||||||
|
exprs_since_last_plot: 0,
|
||||||
|
used_vars: HashSet::new(),
|
||||||
|
warnings: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mark_obj(&mut self, node: NodeId) -> NodeId {
|
||||||
|
self.obj_results.insert(node);
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_obj_node(&self, node: NodeId) -> bool {
|
||||||
|
self.obj_results.contains(&node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_x(&mut self) -> NodeId {
|
||||||
|
*self.input_x.get_or_insert_with(|| self.graph.push(TrigOp::InputX))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_y(&mut self) -> NodeId {
|
||||||
|
*self.input_y.get_or_insert_with(|| self.graph.push(TrigOp::InputY))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_z(&mut self) -> NodeId {
|
||||||
|
*self.input_z.get_or_insert_with(|| self.graph.push(TrigOp::InputZ))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn peek(&self) -> Option<&Token> {
|
||||||
|
self.tokens.get(self.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn advance(&mut self) -> Option<&Token> {
|
||||||
|
let t = self.tokens.get(self.pos);
|
||||||
|
self.pos += 1;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn skip_separators(&mut self) {
|
||||||
|
while matches!(self.peek(), Some(Token::Semi) | Some(Token::Newline)) {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn current_line(&self) -> usize {
|
||||||
|
let idx = if self.pos > 0 { self.pos - 1 } else { 0 };
|
||||||
|
self.token_lines.get(idx).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn err_at(&self, msg: String) -> String {
|
||||||
|
let ln = self.current_line();
|
||||||
|
if ln == 0 || self.source_lines.is_empty() {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
let src = self.source_lines.get(ln - 1).unwrap_or(&"");
|
||||||
|
let src = src.trim();
|
||||||
|
if src.is_empty() {
|
||||||
|
format!("line {ln}: {msg}")
|
||||||
|
} else {
|
||||||
|
format!("line {ln}: {msg} | {src}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn expect(&mut self, expected: &Token) -> Result<(), String> {
|
||||||
|
let t = self.advance().cloned();
|
||||||
|
match t {
|
||||||
|
Some(ref t) if t == expected => Ok(()),
|
||||||
|
Some(t) => Err(self.err_at(format!("expected {expected:?}, got {t:?}"))),
|
||||||
|
None => Err(self.err_at(format!("expected {expected:?}, got end of input"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_program(&mut self) -> Result<NodeId, String> {
|
||||||
|
let mut last = None;
|
||||||
|
loop {
|
||||||
|
self.skip_separators();
|
||||||
|
if self.pos >= self.tokens.len() { break; }
|
||||||
|
|
||||||
|
if self.is_func_def() {
|
||||||
|
self.parse_func_def()?;
|
||||||
|
if self.pos >= self.tokens.len() { break; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if matches!(self.peek(), Some(Token::Ident(s)) if s == "sch") {
|
||||||
|
self.parse_sch_def()?;
|
||||||
|
if self.pos >= self.tokens.len() { break; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if matches!(self.peek(), Some(Token::Ident(s)) if s == "cast")
|
||||||
|
&& matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
|
||||||
|
{
|
||||||
|
self.advance();
|
||||||
|
self.advance();
|
||||||
|
if matches!(self.peek(), Some(Token::RParen)) {
|
||||||
|
self.advance();
|
||||||
|
self.cast_all = true;
|
||||||
|
self.vars_since_last_cast = 0;
|
||||||
|
self.skip_separators();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match self.peek().cloned() {
|
||||||
|
Some(Token::Ident(name)) if matches!(self.tokens.get(self.pos + 1), Some(Token::RParen)) => {
|
||||||
|
if !self.vars.contains_key(&name) {
|
||||||
|
return Err(self.err_at(format!("'{name}' is not defined")));
|
||||||
|
}
|
||||||
|
let node_id = *self.vars.get(&name).unwrap();
|
||||||
|
self.advance();
|
||||||
|
self.advance();
|
||||||
|
self.cast_nodes.push((name, node_id));
|
||||||
|
self.vars_since_last_cast = 0;
|
||||||
|
self.skip_separators();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => return Err("cast() expects a variable name or no arguments".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches!(self.peek(), Some(Token::Ident(s)) if s == "plot")
|
||||||
|
&& matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
|
||||||
|
{
|
||||||
|
self.advance();
|
||||||
|
self.advance();
|
||||||
|
if matches!(self.peek(), Some(Token::RParen)) {
|
||||||
|
self.advance();
|
||||||
|
self.plot_all = true;
|
||||||
|
self.exprs_since_last_plot = 0;
|
||||||
|
self.skip_separators();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let node = self.parse_additive()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
self.plot_nodes.push(node);
|
||||||
|
self.exprs_since_last_plot = 0;
|
||||||
|
self.skip_separators();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(Token::Ident(_)) = self.peek() {
|
||||||
|
if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
|
||||||
|
if let Some(Token::Ident(method)) = self.tokens.get(self.pos + 2) {
|
||||||
|
if method == "cast"
|
||||||
|
&& matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
|
||||||
|
&& matches!(self.tokens.get(self.pos + 4), Some(Token::RParen))
|
||||||
|
{
|
||||||
|
let name = match self.peek().cloned() {
|
||||||
|
Some(Token::Ident(n)) => n,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
if !self.object_nodes.contains_key(&name) {
|
||||||
|
if self.vars.contains_key(&name) {
|
||||||
|
return Err(format!("'{name}' is not an Obj — cannot call .cast()"));
|
||||||
|
}
|
||||||
|
return Err(format!("'{name}' is not defined"));
|
||||||
|
}
|
||||||
|
let node_id = *self.object_nodes.get(&name).unwrap();
|
||||||
|
self.pos += 5;
|
||||||
|
self.cast_nodes.push((name, node_id));
|
||||||
|
self.vars_since_last_cast = 0;
|
||||||
|
self.skip_separators();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches!(self.peek(), Some(Token::Ident(s)) if s == "let") {
|
||||||
|
self.advance();
|
||||||
|
let name = match self.advance().cloned() {
|
||||||
|
Some(Token::Ident(n)) => n,
|
||||||
|
_ => return Err(self.err_at("expected variable name after 'let'".into())),
|
||||||
|
};
|
||||||
|
let mut is_obj = false;
|
||||||
|
if matches!(self.peek(), Some(Token::Colon)) {
|
||||||
|
self.advance();
|
||||||
|
match self.advance().cloned() {
|
||||||
|
Some(Token::Ident(ty)) => {
|
||||||
|
if ty == "Obj" || ty == "obj" {
|
||||||
|
is_obj = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(self.err_at("expected type name after ':'".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.expect(&Token::Eq)?;
|
||||||
|
let val = self.parse_additive()?;
|
||||||
|
self.vars.insert(name.clone(), val);
|
||||||
|
if is_obj || self.is_obj_node(val) {
|
||||||
|
self.objects.push(name.clone());
|
||||||
|
self.object_nodes.insert(name, val);
|
||||||
|
}
|
||||||
|
self.vars_since_last_cast += 1;
|
||||||
|
self.skip_separators();
|
||||||
|
last = Some(val);
|
||||||
|
} else if self.is_reassignment() {
|
||||||
|
let name = match self.advance().cloned() {
|
||||||
|
Some(Token::Ident(n)) => n,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
self.advance();
|
||||||
|
let val = self.parse_additive()?;
|
||||||
|
if self.object_nodes.contains_key(&name) {
|
||||||
|
self.object_nodes.insert(name.clone(), val);
|
||||||
|
}
|
||||||
|
self.vars.insert(name, val);
|
||||||
|
self.vars_since_last_cast += 1;
|
||||||
|
self.skip_separators();
|
||||||
|
last = Some(val);
|
||||||
|
} else if self.pos < self.tokens.len() {
|
||||||
|
let node = self.parse_additive()?;
|
||||||
|
last = Some(node);
|
||||||
|
self.bare_exprs.push(node);
|
||||||
|
self.exprs_since_last_plot += 1;
|
||||||
|
self.skip_separators();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.pos >= self.tokens.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_node = last;
|
||||||
|
let cast_var_names: HashSet<&str> = self.cast_nodes.iter().map(|(n, _)| n.as_str()).collect();
|
||||||
|
let plot_nodes_set: HashSet<NodeId> = self.plot_nodes.iter().copied().collect();
|
||||||
|
for (name, &id) in &self.vars {
|
||||||
|
if !self.used_vars.contains(name)
|
||||||
|
&& !cast_var_names.contains(name.as_str())
|
||||||
|
&& !plot_nodes_set.contains(&id)
|
||||||
|
&& output_node != Some(id)
|
||||||
|
{
|
||||||
|
self.warnings.push(format!("unused variable: {name}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match output_node {
|
||||||
|
Some(node) => Ok(node),
|
||||||
|
None if self.cast_all || !self.cast_nodes.is_empty()
|
||||||
|
|| self.plot_all || !self.plot_nodes.is_empty() =>
|
||||||
|
{
|
||||||
|
Ok(self.graph.push(TrigOp::Const(0.0)))
|
||||||
|
}
|
||||||
|
None => Err("empty expression".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_reassignment(&self) -> bool {
|
||||||
|
if let Some(Token::Ident(name)) = self.tokens.get(self.pos) {
|
||||||
|
if self.vars.contains_key(name) {
|
||||||
|
return matches!(self.tokens.get(self.pos + 1), Some(Token::Eq));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_additive(&mut self) -> Result<NodeId, String> {
|
||||||
|
let mut left = self.parse_multiplicative()?;
|
||||||
|
loop {
|
||||||
|
match self.peek() {
|
||||||
|
Some(Token::Plus) => {
|
||||||
|
self.advance();
|
||||||
|
let right = self.parse_multiplicative()?;
|
||||||
|
if self.is_obj_node(left) != self.is_obj_node(right) {
|
||||||
|
return Err(self.err_at("cannot add Obj and Num".into()));
|
||||||
|
}
|
||||||
|
left = self.graph.push(TrigOp::Add(left, right));
|
||||||
|
}
|
||||||
|
Some(Token::Minus) => {
|
||||||
|
self.advance();
|
||||||
|
let right = self.parse_multiplicative()?;
|
||||||
|
left = self.graph.push(TrigOp::Sub(left, right));
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_multiplicative(&mut self) -> Result<NodeId, String> {
|
||||||
|
let mut left = self.parse_power()?;
|
||||||
|
loop {
|
||||||
|
match self.peek() {
|
||||||
|
Some(Token::Star) => {
|
||||||
|
self.advance();
|
||||||
|
let right = self.parse_power()?;
|
||||||
|
left = self.graph.push(TrigOp::Mul(left, right));
|
||||||
|
}
|
||||||
|
Some(Token::Slash) => {
|
||||||
|
self.advance();
|
||||||
|
let right = self.parse_power()?;
|
||||||
|
left = self.graph.push(TrigOp::Div(left, right));
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_power(&mut self) -> Result<NodeId, String> {
|
||||||
|
let base = self.parse_unary()?;
|
||||||
|
if self.peek() == Some(&Token::Caret) {
|
||||||
|
self.advance();
|
||||||
|
let exp = self.parse_unary()?;
|
||||||
|
if let Some(TrigOp::Const(n)) = self.graph.nodes.get(exp as usize) {
|
||||||
|
let n = *n;
|
||||||
|
if n == 2.0 {
|
||||||
|
return Ok(self.graph.push(TrigOp::Mul(base, base)));
|
||||||
|
} else if n == 3.0 {
|
||||||
|
let sq = self.graph.push(TrigOp::Mul(base, base));
|
||||||
|
return Ok(self.graph.push(TrigOp::Mul(sq, base)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(self.graph.push(TrigOp::Mul(base, exp)))
|
||||||
|
} else {
|
||||||
|
Ok(base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unary(&mut self) -> Result<NodeId, String> {
|
||||||
|
if self.peek() == Some(&Token::Minus) {
|
||||||
|
self.advance();
|
||||||
|
let val = self.parse_unary()?;
|
||||||
|
Ok(self.graph.push(TrigOp::Neg(val)))
|
||||||
|
} else {
|
||||||
|
self.parse_atom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_atom(&mut self) -> Result<NodeId, String> {
|
||||||
|
while matches!(self.peek(), Some(Token::Newline)) {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
match self.advance().cloned() {
|
||||||
|
Some(Token::Num(n)) => Ok(self.graph.push(TrigOp::Const(n))),
|
||||||
|
Some(Token::Ident(name)) => {
|
||||||
|
match name.as_str() {
|
||||||
|
"x" => Ok(self.get_x()),
|
||||||
|
"y" => Ok(self.get_y()),
|
||||||
|
"z" => Ok(self.get_z()),
|
||||||
|
"pi" | "PI" => Ok(self.graph.push(TrigOp::Const(std::f64::consts::PI))),
|
||||||
|
"tau" | "TAU" => Ok(self.graph.push(TrigOp::Const(2.0 * std::f64::consts::PI))),
|
||||||
|
"e" | "E" => Ok(self.graph.push(TrigOp::Const(std::f64::consts::E))),
|
||||||
|
"A" => {
|
||||||
|
if let Some(g) = self.ref_a {
|
||||||
|
Ok(self.inline_graph(g))
|
||||||
|
} else {
|
||||||
|
Err("A is not defined".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"B" => {
|
||||||
|
if let Some(g) = self.ref_b {
|
||||||
|
Ok(self.inline_graph(g))
|
||||||
|
} else {
|
||||||
|
Err("B is not defined".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"reg" => Ok(self.graph.push(TrigOp::Const(f64::NAN))),
|
||||||
|
"map" => self.parse_map(),
|
||||||
|
_ => {
|
||||||
|
if let Some(&node_id) = self.vars.get(&name) {
|
||||||
|
self.used_vars.insert(name);
|
||||||
|
return Ok(node_id);
|
||||||
|
}
|
||||||
|
self.parse_function_call(&name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Token::LParen) => {
|
||||||
|
let inner = self.parse_additive()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
Ok(inner)
|
||||||
|
}
|
||||||
|
Some(t) => Err(format!("unexpected token: {t:?}")),
|
||||||
|
None => Err("unexpected end of input".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_arg_list(&mut self) -> Result<Vec<NodeId>, String> {
|
||||||
|
let mut args = Vec::new();
|
||||||
|
self.skip_separators();
|
||||||
|
if self.peek() == Some(&Token::RParen) {
|
||||||
|
return Ok(args);
|
||||||
|
}
|
||||||
|
args.push(self.parse_additive()?);
|
||||||
|
while { self.skip_separators(); self.peek() == Some(&Token::Comma) } {
|
||||||
|
self.advance();
|
||||||
|
self.skip_separators();
|
||||||
|
args.push(self.parse_additive()?);
|
||||||
|
}
|
||||||
|
self.skip_separators();
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn require_args(name: &str, args: &[NodeId], expected: usize) -> Result<(), String> {
|
||||||
|
if args.len() != expected {
|
||||||
|
Err(format!("{name}() requires {expected} argument(s), got {}", args.len()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
use cord_trig::ir::{NodeId, TrigOp};
|
||||||
|
use crate::parser::ExprParser;
|
||||||
|
|
||||||
|
impl<'a> ExprParser<'a> {
|
||||||
|
pub(crate) fn inline_graph(&mut self, source: &cord_trig::TrigGraph) -> NodeId {
|
||||||
|
let mut map = Vec::with_capacity(source.nodes.len());
|
||||||
|
|
||||||
|
for op in &source.nodes {
|
||||||
|
let new_id = match op {
|
||||||
|
TrigOp::InputX => self.get_x(),
|
||||||
|
TrigOp::InputY => self.get_y(),
|
||||||
|
TrigOp::InputZ => self.get_z(),
|
||||||
|
TrigOp::Const(c) => self.graph.push(TrigOp::Const(*c)),
|
||||||
|
TrigOp::Add(a, b) => self.graph.push(TrigOp::Add(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Sub(a, b) => self.graph.push(TrigOp::Sub(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Mul(a, b) => self.graph.push(TrigOp::Mul(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Div(a, b) => self.graph.push(TrigOp::Div(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Neg(a) => self.graph.push(TrigOp::Neg(map[*a as usize])),
|
||||||
|
TrigOp::Abs(a) => self.graph.push(TrigOp::Abs(map[*a as usize])),
|
||||||
|
TrigOp::Sin(a) => self.graph.push(TrigOp::Sin(map[*a as usize])),
|
||||||
|
TrigOp::Cos(a) => self.graph.push(TrigOp::Cos(map[*a as usize])),
|
||||||
|
TrigOp::Tan(a) => self.graph.push(TrigOp::Tan(map[*a as usize])),
|
||||||
|
TrigOp::Asin(a) => self.graph.push(TrigOp::Asin(map[*a as usize])),
|
||||||
|
TrigOp::Acos(a) => self.graph.push(TrigOp::Acos(map[*a as usize])),
|
||||||
|
TrigOp::Atan(a) => self.graph.push(TrigOp::Atan(map[*a as usize])),
|
||||||
|
TrigOp::Sinh(a) => self.graph.push(TrigOp::Sinh(map[*a as usize])),
|
||||||
|
TrigOp::Cosh(a) => self.graph.push(TrigOp::Cosh(map[*a as usize])),
|
||||||
|
TrigOp::Tanh(a) => self.graph.push(TrigOp::Tanh(map[*a as usize])),
|
||||||
|
TrigOp::Asinh(a) => self.graph.push(TrigOp::Asinh(map[*a as usize])),
|
||||||
|
TrigOp::Acosh(a) => self.graph.push(TrigOp::Acosh(map[*a as usize])),
|
||||||
|
TrigOp::Atanh(a) => self.graph.push(TrigOp::Atanh(map[*a as usize])),
|
||||||
|
TrigOp::Sqrt(a) => self.graph.push(TrigOp::Sqrt(map[*a as usize])),
|
||||||
|
TrigOp::Exp(a) => self.graph.push(TrigOp::Exp(map[*a as usize])),
|
||||||
|
TrigOp::Ln(a) => self.graph.push(TrigOp::Ln(map[*a as usize])),
|
||||||
|
TrigOp::Hypot(a, b) => self.graph.push(TrigOp::Hypot(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Atan2(a, b) => self.graph.push(TrigOp::Atan2(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Min(a, b) => self.graph.push(TrigOp::Min(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Max(a, b) => self.graph.push(TrigOp::Max(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Clamp { val, lo, hi } => self.graph.push(TrigOp::Clamp {
|
||||||
|
val: map[*val as usize],
|
||||||
|
lo: map[*lo as usize],
|
||||||
|
hi: map[*hi as usize],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
map.push(new_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
map[source.output as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remap_inputs(&mut self, root: NodeId, new_x: NodeId, new_y: NodeId, new_z: NodeId) -> NodeId {
|
||||||
|
let n = root as usize + 1;
|
||||||
|
|
||||||
|
let mut reachable = vec![false; n];
|
||||||
|
reachable[root as usize] = true;
|
||||||
|
for i in (0..n).rev() {
|
||||||
|
if !reachable[i] { continue; }
|
||||||
|
Self::mark_children(&self.graph.nodes[i], &mut reachable);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut depends_on_input = vec![false; n];
|
||||||
|
for i in 0..n {
|
||||||
|
if !reachable[i] { continue; }
|
||||||
|
match &self.graph.nodes[i] {
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ => {
|
||||||
|
depends_on_input[i] = true;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
depends_on_input[i] = Self::any_child_depends(
|
||||||
|
&self.graph.nodes[i], &depends_on_input,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut map: Vec<NodeId> = (0..n as u32).collect();
|
||||||
|
|
||||||
|
for i in 0..n {
|
||||||
|
if !reachable[i] { continue; }
|
||||||
|
match &self.graph.nodes[i] {
|
||||||
|
TrigOp::InputX => { map[i] = new_x; }
|
||||||
|
TrigOp::InputY => { map[i] = new_y; }
|
||||||
|
TrigOp::InputZ => { map[i] = new_z; }
|
||||||
|
_ if !depends_on_input[i] => {}
|
||||||
|
_ => {
|
||||||
|
map[i] = self.push_remapped(&self.graph.nodes[i].clone(), &map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map[root as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_children(op: &TrigOp, reachable: &mut [bool]) {
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ | TrigOp::Const(_) => {}
|
||||||
|
TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b)
|
||||||
|
| TrigOp::Div(a, b) | TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
|
||||||
|
| TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
|
||||||
|
reachable[*a as usize] = true;
|
||||||
|
reachable[*b as usize] = true;
|
||||||
|
}
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a) | TrigOp::Sin(a) | TrigOp::Cos(a)
|
||||||
|
| TrigOp::Tan(a) | TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
reachable[*a as usize] = true;
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
reachable[*val as usize] = true;
|
||||||
|
reachable[*lo as usize] = true;
|
||||||
|
reachable[*hi as usize] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn any_child_depends(op: &TrigOp, deps: &[bool]) -> bool {
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ => true,
|
||||||
|
TrigOp::Const(_) => false,
|
||||||
|
TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b)
|
||||||
|
| TrigOp::Div(a, b) | TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
|
||||||
|
| TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
|
||||||
|
deps[*a as usize] || deps[*b as usize]
|
||||||
|
}
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a) | TrigOp::Sin(a) | TrigOp::Cos(a)
|
||||||
|
| TrigOp::Tan(a) | TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
deps[*a as usize]
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
deps[*val as usize] || deps[*lo as usize] || deps[*hi as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_remapped(&mut self, op: &TrigOp, map: &[NodeId]) -> NodeId {
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ => unreachable!(),
|
||||||
|
TrigOp::Const(c) => self.graph.push(TrigOp::Const(*c)),
|
||||||
|
TrigOp::Add(a, b) => self.graph.push(TrigOp::Add(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Sub(a, b) => self.graph.push(TrigOp::Sub(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Mul(a, b) => self.graph.push(TrigOp::Mul(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Div(a, b) => self.graph.push(TrigOp::Div(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Neg(a) => self.graph.push(TrigOp::Neg(map[*a as usize])),
|
||||||
|
TrigOp::Abs(a) => self.graph.push(TrigOp::Abs(map[*a as usize])),
|
||||||
|
TrigOp::Sin(a) => self.graph.push(TrigOp::Sin(map[*a as usize])),
|
||||||
|
TrigOp::Cos(a) => self.graph.push(TrigOp::Cos(map[*a as usize])),
|
||||||
|
TrigOp::Tan(a) => self.graph.push(TrigOp::Tan(map[*a as usize])),
|
||||||
|
TrigOp::Asin(a) => self.graph.push(TrigOp::Asin(map[*a as usize])),
|
||||||
|
TrigOp::Acos(a) => self.graph.push(TrigOp::Acos(map[*a as usize])),
|
||||||
|
TrigOp::Atan(a) => self.graph.push(TrigOp::Atan(map[*a as usize])),
|
||||||
|
TrigOp::Sinh(a) => self.graph.push(TrigOp::Sinh(map[*a as usize])),
|
||||||
|
TrigOp::Cosh(a) => self.graph.push(TrigOp::Cosh(map[*a as usize])),
|
||||||
|
TrigOp::Tanh(a) => self.graph.push(TrigOp::Tanh(map[*a as usize])),
|
||||||
|
TrigOp::Asinh(a) => self.graph.push(TrigOp::Asinh(map[*a as usize])),
|
||||||
|
TrigOp::Acosh(a) => self.graph.push(TrigOp::Acosh(map[*a as usize])),
|
||||||
|
TrigOp::Atanh(a) => self.graph.push(TrigOp::Atanh(map[*a as usize])),
|
||||||
|
TrigOp::Sqrt(a) => self.graph.push(TrigOp::Sqrt(map[*a as usize])),
|
||||||
|
TrigOp::Exp(a) => self.graph.push(TrigOp::Exp(map[*a as usize])),
|
||||||
|
TrigOp::Ln(a) => self.graph.push(TrigOp::Ln(map[*a as usize])),
|
||||||
|
TrigOp::Hypot(a, b) => self.graph.push(TrigOp::Hypot(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Atan2(a, b) => self.graph.push(TrigOp::Atan2(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Min(a, b) => self.graph.push(TrigOp::Min(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Max(a, b) => self.graph.push(TrigOp::Max(map[*a as usize], map[*b as usize])),
|
||||||
|
TrigOp::Clamp { val, lo, hi } => self.graph.push(TrigOp::Clamp {
|
||||||
|
val: map[*val as usize],
|
||||||
|
lo: map[*lo as usize],
|
||||||
|
hi: map[*hi as usize],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub(crate) enum Token {
|
||||||
|
Num(f64),
|
||||||
|
Ident(String),
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Star,
|
||||||
|
Slash,
|
||||||
|
LParen,
|
||||||
|
RParen,
|
||||||
|
LBrace,
|
||||||
|
RBrace,
|
||||||
|
Comma,
|
||||||
|
Caret,
|
||||||
|
Eq,
|
||||||
|
Semi,
|
||||||
|
Colon,
|
||||||
|
Newline,
|
||||||
|
Dot,
|
||||||
|
DotDot,
|
||||||
|
Percent,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tokenize(input: &str) -> Result<(Vec<Token>, Vec<usize>), String> {
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut line: usize = 1;
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
match c {
|
||||||
|
' ' | '\t' | '\r' => { chars.next(); }
|
||||||
|
'\n' => {
|
||||||
|
chars.next();
|
||||||
|
if !matches!(tokens.last(), Some(Token::Newline)) {
|
||||||
|
tokens.push(Token::Newline);
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
'+' => { tokens.push(Token::Plus); lines.push(line); chars.next(); }
|
||||||
|
'-' => { tokens.push(Token::Minus); lines.push(line); chars.next(); }
|
||||||
|
'*' => { tokens.push(Token::Star); lines.push(line); chars.next(); }
|
||||||
|
'/' => {
|
||||||
|
chars.next();
|
||||||
|
match chars.peek() {
|
||||||
|
Some('/') | Some('=') => {
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
chars.next();
|
||||||
|
if c == '\n' { line += 1; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some('*') => {
|
||||||
|
chars.next();
|
||||||
|
let mut depth = 1u32;
|
||||||
|
while depth > 0 {
|
||||||
|
match chars.next() {
|
||||||
|
Some('\n') => line += 1,
|
||||||
|
Some('*') if chars.peek() == Some(&'/') => {
|
||||||
|
chars.next();
|
||||||
|
depth -= 1;
|
||||||
|
}
|
||||||
|
Some('/') if chars.peek() == Some(&'*') => {
|
||||||
|
chars.next();
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => { tokens.push(Token::Slash); lines.push(line); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'(' => { tokens.push(Token::LParen); lines.push(line); chars.next(); }
|
||||||
|
')' => { tokens.push(Token::RParen); lines.push(line); chars.next(); }
|
||||||
|
'{' => { tokens.push(Token::LBrace); lines.push(line); chars.next(); }
|
||||||
|
'}' => { tokens.push(Token::RBrace); lines.push(line); chars.next(); }
|
||||||
|
',' => { tokens.push(Token::Comma); lines.push(line); chars.next(); }
|
||||||
|
'^' => { tokens.push(Token::Caret); lines.push(line); chars.next(); }
|
||||||
|
'%' => { tokens.push(Token::Percent); lines.push(line); chars.next(); }
|
||||||
|
'=' => { tokens.push(Token::Eq); lines.push(line); chars.next(); }
|
||||||
|
';' => { tokens.push(Token::Semi); lines.push(line); chars.next(); }
|
||||||
|
':' => { tokens.push(Token::Colon); lines.push(line); chars.next(); }
|
||||||
|
'.' => {
|
||||||
|
chars.next();
|
||||||
|
if chars.peek() == Some(&'.') {
|
||||||
|
chars.next();
|
||||||
|
tokens.push(Token::DotDot); lines.push(line);
|
||||||
|
} else if chars.peek().map_or(false, |c| c.is_ascii_digit()) {
|
||||||
|
let mut num_str = String::from("0.");
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if c.is_ascii_digit() || c == '.' {
|
||||||
|
num_str.push(c);
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let val: f64 = num_str.parse()
|
||||||
|
.map_err(|_| format!("invalid number: {num_str}"))?;
|
||||||
|
tokens.push(Token::Num(val)); lines.push(line);
|
||||||
|
} else {
|
||||||
|
tokens.push(Token::Dot); lines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'0'..='9' => {
|
||||||
|
let mut num_str = String::new();
|
||||||
|
let mut has_dot = false;
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if c.is_ascii_digit() {
|
||||||
|
num_str.push(c);
|
||||||
|
chars.next();
|
||||||
|
} else if c == '.' && !has_dot {
|
||||||
|
let mut lookahead = chars.clone();
|
||||||
|
lookahead.next();
|
||||||
|
if lookahead.peek().map_or(false, |c| c.is_ascii_digit()) {
|
||||||
|
has_dot = true;
|
||||||
|
num_str.push(c);
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let val: f64 = num_str.parse()
|
||||||
|
.map_err(|_| format!("invalid number: {num_str}"))?;
|
||||||
|
tokens.push(Token::Num(val)); lines.push(line);
|
||||||
|
}
|
||||||
|
'a'..='z' | 'A'..='Z' | '_' => {
|
||||||
|
let mut name = String::new();
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if c.is_alphanumeric() || c == '_' {
|
||||||
|
name.push(c);
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens.push(Token::Ident(name)); lines.push(line);
|
||||||
|
}
|
||||||
|
_ => return Err(format!("unexpected character: '{c}'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((tokens, lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn merge_ngon_tokens_with_lines(tokens: &mut Vec<Token>, lines: &mut Vec<usize>) {
|
||||||
|
let mut i = 0;
|
||||||
|
while i + 2 < tokens.len() {
|
||||||
|
let merge = if let (Token::Num(n), Token::Minus, Token::Ident(s)) =
|
||||||
|
(&tokens[i], &tokens[i + 1], &tokens[i + 2])
|
||||||
|
{
|
||||||
|
if s == "gon" && *n >= 3.0 && *n == (*n as u32 as f64) {
|
||||||
|
Some(*n as u32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(n) = merge {
|
||||||
|
tokens[i] = Token::Ident(format!("{n}gon"));
|
||||||
|
tokens.remove(i + 2); lines.remove(i + 2);
|
||||||
|
tokens.remove(i + 1); lines.remove(i + 1);
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot_syntax_tokenizer() {
|
||||||
|
let (tokens, _) = tokenize("a.cast()").unwrap();
|
||||||
|
assert_eq!(tokens.len(), 5);
|
||||||
|
assert!(matches!(&tokens[0], Token::Ident(n) if n == "a"));
|
||||||
|
assert!(matches!(&tokens[1], Token::Dot));
|
||||||
|
assert!(matches!(&tokens[2], Token::Ident(n) if n == "cast"));
|
||||||
|
assert!(matches!(&tokens[3], Token::LParen));
|
||||||
|
assert!(matches!(&tokens[4], Token::RParen));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
use cord_trig::ir::{NodeId, TrigOp};
|
||||||
|
use crate::token::Token;
|
||||||
|
use crate::parser::{ExprParser, UserFunc, Schematic};
|
||||||
|
|
||||||
|
impl<'a> ExprParser<'a> {
|
||||||
|
pub(crate) fn is_func_def(&self) -> bool {
|
||||||
|
if !matches!(self.tokens.get(self.pos), Some(Token::Ident(_))) { return false; }
|
||||||
|
if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) { return false; }
|
||||||
|
let mut i = self.pos + 2;
|
||||||
|
let mut depth = 1u32;
|
||||||
|
while i < self.tokens.len() {
|
||||||
|
match &self.tokens[i] {
|
||||||
|
Token::LParen => depth += 1,
|
||||||
|
Token::RParen => { depth -= 1; if depth == 0 { return matches!(self.tokens.get(i + 1), Some(Token::Eq)); } }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_param_list_with_defaults(&mut self) -> Result<(Vec<String>, Vec<Option<Vec<Token>>>), String> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
let mut defaults = Vec::new();
|
||||||
|
self.skip_separators();
|
||||||
|
if matches!(self.peek(), Some(Token::RParen)) { return Ok((params, defaults)); }
|
||||||
|
loop {
|
||||||
|
self.skip_separators();
|
||||||
|
let pname = match self.advance().cloned() { Some(Token::Ident(p)) => p, _ => return Err(self.err_at("expected parameter name".into())) };
|
||||||
|
params.push(pname);
|
||||||
|
if matches!(self.peek(), Some(Token::Colon) | Some(Token::Eq)) {
|
||||||
|
self.advance();
|
||||||
|
let start = self.pos;
|
||||||
|
let mut depth = 0u32;
|
||||||
|
while self.pos < self.tokens.len() {
|
||||||
|
match &self.tokens[self.pos] {
|
||||||
|
Token::Comma if depth == 0 => break, Token::RParen if depth == 0 => break,
|
||||||
|
Token::LParen => { depth += 1; self.pos += 1; } Token::RParen => { depth -= 1; self.pos += 1; }
|
||||||
|
Token::Semi | Token::Newline if depth == 0 => break, _ => { self.pos += 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaults.push(Some(self.tokens[start..self.pos].to_vec()));
|
||||||
|
} else { defaults.push(None); }
|
||||||
|
self.skip_separators();
|
||||||
|
if !matches!(self.peek(), Some(Token::Comma)) { break; }
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
self.skip_separators();
|
||||||
|
Ok((params, defaults))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_defaults(&mut self, params: &[String], defaults: &[Option<Vec<Token>>], args: &[NodeId], name: &str) -> Result<Vec<NodeId>, String> {
|
||||||
|
let required = params.iter().zip(defaults.iter()).filter(|(_, d)| d.is_none()).count();
|
||||||
|
if args.len() < required || args.len() > params.len() {
|
||||||
|
return Err(format!("{name}() takes {}{} argument(s), got {}", if required < params.len() { format!("{required}..") } else { String::new() }, params.len(), args.len()));
|
||||||
|
}
|
||||||
|
let mut resolved = Vec::with_capacity(params.len());
|
||||||
|
for (i, dt) in defaults.iter().enumerate() {
|
||||||
|
if i < args.len() { resolved.push(args[i]); }
|
||||||
|
else if let Some(def_body) = dt { resolved.push(self.eval_default_expr(def_body.clone())?); }
|
||||||
|
else { return Err(format!("{name}(): missing required argument '{}'", params[i])); }
|
||||||
|
}
|
||||||
|
Ok(resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_default_expr(&mut self, body: Vec<Token>) -> Result<NodeId, String> {
|
||||||
|
let st = std::mem::replace(&mut self.tokens, &[]); let sl = std::mem::replace(&mut self.token_lines, &[]);
|
||||||
|
let ss = std::mem::replace(&mut self.source_lines, &[]); let sp = self.pos;
|
||||||
|
let bb = body.into_boxed_slice(); let bp = Box::into_raw(bb);
|
||||||
|
self.tokens = unsafe { &*bp }; self.pos = 0;
|
||||||
|
let result = self.parse_additive();
|
||||||
|
let _ = unsafe { Box::from_raw(bp) };
|
||||||
|
self.tokens = st; self.token_lines = sl; self.source_lines = ss; self.pos = sp;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_func_def(&mut self) -> Result<(), String> {
|
||||||
|
let name = match self.advance().cloned() { Some(Token::Ident(n)) => n, _ => unreachable!() };
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let (params, defaults) = self.parse_param_list_with_defaults()?;
|
||||||
|
self.expect(&Token::RParen)?; self.expect(&Token::Eq)?;
|
||||||
|
let start = self.pos; let mut depth = 0u32;
|
||||||
|
while self.pos < self.tokens.len() { match &self.tokens[self.pos] { Token::Semi | Token::Newline if depth == 0 => break, Token::LParen => { depth += 1; self.pos += 1; } Token::RParen if depth > 0 => { depth -= 1; self.pos += 1; } _ => { self.pos += 1; } } }
|
||||||
|
let body = self.tokens[start..self.pos].to_vec(); self.skip_separators();
|
||||||
|
self.funcs.insert(name, UserFunc { params, defaults, body }); Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn call_user_func_inner(&mut self, params: Vec<String>, defaults: Vec<Option<Vec<Token>>>, body: Vec<Token>, args: &[NodeId], name: &str) -> Result<NodeId, String> {
|
||||||
|
let ra = self.resolve_defaults(¶ms, &defaults, args, name)?;
|
||||||
|
let mut saved = Vec::new();
|
||||||
|
for (p, &a) in params.iter().zip(ra.iter()) { saved.push((p.clone(), self.vars.get(p).copied())); self.vars.insert(p.clone(), a); }
|
||||||
|
let st = std::mem::replace(&mut self.tokens, &[]); let sl = std::mem::replace(&mut self.token_lines, &[]);
|
||||||
|
let ss = std::mem::replace(&mut self.source_lines, &[]); let sp = self.pos;
|
||||||
|
let bb = body.into_boxed_slice(); let bp = Box::into_raw(bb);
|
||||||
|
self.tokens = unsafe { &*bp }; self.pos = 0;
|
||||||
|
let result = self.parse_additive();
|
||||||
|
let _ = unsafe { Box::from_raw(bp) };
|
||||||
|
self.tokens = st; self.token_lines = sl; self.source_lines = ss; self.pos = sp;
|
||||||
|
for (p, old) in saved { match old { Some(v) => { self.vars.insert(p, v); } None => { self.vars.remove(&p); } } }
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_sch_def(&mut self) -> Result<(), String> {
|
||||||
|
self.advance();
|
||||||
|
let name = match self.advance().cloned() { Some(Token::Ident(n)) => n, _ => return Err(self.err_at("expected schematic name after 'sch'".into())) };
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let (params, defaults) = self.parse_param_list_with_defaults()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
if matches!(self.peek(), Some(Token::Eq)) {
|
||||||
|
self.advance();
|
||||||
|
let start = self.pos; let mut depth = 0u32;
|
||||||
|
while self.pos < self.tokens.len() { match &self.tokens[self.pos] { Token::Semi | Token::Newline if depth == 0 => break, Token::LParen => { depth += 1; self.pos += 1; } Token::RParen if depth > 0 => { depth -= 1; self.pos += 1; } _ => { self.pos += 1; } } }
|
||||||
|
let body = self.tokens[start..self.pos].to_vec(); self.skip_separators();
|
||||||
|
self.schematics.insert(name, Schematic { params, defaults, body, value_returning: true }); return Ok(());
|
||||||
|
}
|
||||||
|
self.expect(&Token::LBrace)?; let body = self.collect_brace_body()?;
|
||||||
|
self.schematics.insert(name, Schematic { params, defaults, body, value_returning: false }); Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_brace_body(&mut self) -> Result<Vec<Token>, String> {
|
||||||
|
let start = self.pos; let mut depth = 1u32;
|
||||||
|
while self.pos < self.tokens.len() { match &self.tokens[self.pos] { Token::LBrace => { depth += 1; self.pos += 1; } Token::RBrace => { depth -= 1; if depth == 0 { let body = self.tokens[start..self.pos].to_vec(); self.pos += 1; self.skip_separators(); return Ok(body); } self.pos += 1; } _ => { self.pos += 1; } } }
|
||||||
|
Err("unclosed '{'".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn call_schematic(&mut self, params: Vec<String>, defaults: Vec<Option<Vec<Token>>>, body: Vec<Token>, value_returning: bool, args: &[NodeId], name: &str) -> Result<NodeId, String> {
|
||||||
|
let ra = self.resolve_defaults(¶ms, &defaults, args, name)?;
|
||||||
|
let mut saved = Vec::new();
|
||||||
|
for (p, &a) in params.iter().zip(ra.iter()) { saved.push((p.clone(), self.vars.get(p).copied())); self.vars.insert(p.clone(), a); }
|
||||||
|
let sf = self.funcs.clone(); let ss2 = self.schematics.clone();
|
||||||
|
let st = std::mem::replace(&mut self.tokens, &[]); let sl = std::mem::replace(&mut self.token_lines, &[]);
|
||||||
|
let ss = std::mem::replace(&mut self.source_lines, &[]); let sp = self.pos;
|
||||||
|
let bb = body.into_boxed_slice(); let bp = Box::into_raw(bb);
|
||||||
|
self.tokens = unsafe { &*bp }; self.pos = 0;
|
||||||
|
let result = if value_returning { self.parse_additive() } else { self.parse_block_body() };
|
||||||
|
let _ = unsafe { Box::from_raw(bp) };
|
||||||
|
self.tokens = st; self.token_lines = sl; self.source_lines = ss; self.pos = sp;
|
||||||
|
self.funcs = sf; self.schematics = ss2;
|
||||||
|
for (p, old) in saved { match old { Some(v) => { self.vars.insert(p, v); } None => { self.vars.remove(&p); } } }
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_block_body(&mut self) -> Result<NodeId, String> {
|
||||||
|
let mut last = None;
|
||||||
|
loop {
|
||||||
|
self.skip_separators(); if self.pos >= self.tokens.len() { break; }
|
||||||
|
if self.is_func_def() { self.parse_func_def()?; continue; }
|
||||||
|
if matches!(self.peek(), Some(Token::Ident(s)) if s == "sch") { self.parse_sch_def()?; continue; }
|
||||||
|
if matches!(self.peek(), Some(Token::Ident(s)) if s == "let") {
|
||||||
|
self.advance();
|
||||||
|
let name = match self.advance().cloned() { Some(Token::Ident(n)) => n, _ => return Err(self.err_at("expected variable name after 'let'".into())) };
|
||||||
|
let mut is_obj = false;
|
||||||
|
if matches!(self.peek(), Some(Token::Colon)) { self.advance(); match self.advance().cloned() { Some(Token::Ident(ty)) => { if ty == "Obj" || ty == "obj" { is_obj = true; } } _ => return Err(self.err_at("expected type name after ':'".into())) } }
|
||||||
|
self.expect(&Token::Eq)?; let val = self.parse_additive()?;
|
||||||
|
self.vars.insert(name.clone(), val);
|
||||||
|
if is_obj { self.objects.push(name.clone()); self.object_nodes.insert(name, val); }
|
||||||
|
last = Some(val); self.skip_separators();
|
||||||
|
} else { let node = self.parse_additive()?; last = Some(node); self.skip_separators(); }
|
||||||
|
}
|
||||||
|
last.ok_or_else(|| "empty block".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_map(&mut self) -> Result<NodeId, String> {
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let iter_var = match self.advance().cloned() { Some(Token::Ident(n)) => n, _ => return Err("map: expected iteration variable name".into()) };
|
||||||
|
self.expect(&Token::Comma)?;
|
||||||
|
let sn = self.parse_additive()?; self.expect(&Token::DotDot)?; let en = self.parse_additive()?; self.expect(&Token::RParen)?;
|
||||||
|
let si = self.eval_const(sn)?.round() as i64; let ei = self.eval_const(en)?.round() as i64;
|
||||||
|
if ei <= si { return Err(format!("map: empty range {}..{}", si, ei)); }
|
||||||
|
if ei - si > 1024 { return Err("map: range too large (max 1024 iterations)".into()); }
|
||||||
|
self.expect(&Token::LBrace)?; let body = self.collect_brace_body()?;
|
||||||
|
let saved_var = self.vars.get(&iter_var).copied(); let mut nodes: Vec<NodeId> = Vec::new();
|
||||||
|
for i in si..ei {
|
||||||
|
let i_node = self.graph.push(TrigOp::Const(i as f64)); self.vars.insert(iter_var.clone(), i_node);
|
||||||
|
let st = std::mem::replace(&mut self.tokens, &[]); let sp = self.pos;
|
||||||
|
let bc = body.clone(); let bb = bc.into_boxed_slice(); let bp = Box::into_raw(bb);
|
||||||
|
self.tokens = unsafe { &*bp }; self.pos = 0;
|
||||||
|
let node = self.parse_block_body()?;
|
||||||
|
let _ = unsafe { Box::from_raw(bp) }; self.tokens = st; self.pos = sp; nodes.push(node);
|
||||||
|
}
|
||||||
|
match saved_var { Some(v) => { self.vars.insert(iter_var, v); } None => { self.vars.remove(&iter_var); } }
|
||||||
|
if nodes.is_empty() { return Err("map: produced no results".into()); }
|
||||||
|
let any_obj = nodes.iter().any(|n| self.is_obj_node(*n));
|
||||||
|
let mut result = nodes[0]; for &node in &nodes[1..] { result = self.graph.push(TrigOp::Min(result, node)); }
|
||||||
|
if any_obj { self.mark_obj(result); } Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_const(&self, node: NodeId) -> Result<f64, String> {
|
||||||
|
match &self.graph.nodes[node as usize] {
|
||||||
|
TrigOp::Const(v) => Ok(*v),
|
||||||
|
TrigOp::Add(a, b) => Ok(self.eval_const(*a)? + self.eval_const(*b)?),
|
||||||
|
TrigOp::Sub(a, b) => Ok(self.eval_const(*a)? - self.eval_const(*b)?),
|
||||||
|
TrigOp::Mul(a, b) => Ok(self.eval_const(*a)? * self.eval_const(*b)?),
|
||||||
|
TrigOp::Div(a, b) => Ok(self.eval_const(*a)? / self.eval_const(*b)?),
|
||||||
|
TrigOp::Neg(a) => Ok(-self.eval_const(*a)?),
|
||||||
|
_ => { let mut g = self.graph.clone(); g.set_output(node); let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0); if val.is_finite() { Ok(val) } else { Err("map: range bounds must be compile-time constants".into()) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{parse_expr, parse_expr_scene};
|
||||||
|
use cord_trig::eval::evaluate;
|
||||||
|
#[test] fn user_func_basic() { let g = parse_expr("f(a) = a^2\nf(3)").unwrap(); assert!((evaluate(&g, 0.0, 0.0, 0.0) - 9.0).abs() < 1e-10); }
|
||||||
|
#[test] fn user_func_two_params() { let g = parse_expr("f(a, b) = a + b\nf(3, 4)").unwrap(); assert!((evaluate(&g, 0.0, 0.0, 0.0) - 7.0).abs() < 1e-10); }
|
||||||
|
#[test] fn user_func_with_xyz() { let g = parse_expr("f(r) = sphere(r)\nf(3)").unwrap(); assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6); }
|
||||||
|
#[test] fn user_func_composition() { let g = parse_expr("f(a) = a * 2\ng(b) = b + 1\ng(f(3))").unwrap(); assert!((evaluate(&g, 0.0, 0.0, 0.0) - 7.0).abs() < 1e-10); }
|
||||||
|
#[test] fn user_func_with_let() { let g = parse_expr("f(v) = v^2 + 1\nlet a = f(x)\na").unwrap(); assert!((evaluate(&g, 3.0, 0.0, 0.0) - 10.0).abs() < 1e-10); }
|
||||||
|
#[test] fn user_func_default_value() { let g = parse_expr("f(a, b = 10) = a + b\nf(3)").unwrap(); assert!((evaluate(&g, 0.0, 0.0, 0.0) - 13.0).abs() < 1e-10); }
|
||||||
|
#[test] fn user_func_default_override() { let g = parse_expr("f(a, b = 10) = a + b\nf(3, 5)").unwrap(); assert!((evaluate(&g, 0.0, 0.0, 0.0) - 8.0).abs() < 1e-10); }
|
||||||
|
#[test] fn sch_basic() { let g = parse_expr("sch Foo(r) { sphere(r) }\nFoo(3)").unwrap(); assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6); }
|
||||||
|
#[test] fn sch_multi_statement() { let g = parse_expr("sch Bar(w, h) {\n let a = box(w, h, 1)\n let b = sphere(1)\n union(a, b)\n}\nBar(3, 2)").unwrap(); assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn sch_with_transforms() { let g = parse_expr("sch Arm(len) {\n translate(box(len, 0.5, 0.5), len/2, 0, 0)\n}\nArm(5)").unwrap(); assert!(evaluate(&g, 2.5, 0.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn sch_multiline_params() { let g = parse_expr("sch Brace(\n w,\n h,\n t\n) {\n box(w, h, t)\n}\nBrace(3, 2, 1)").unwrap(); assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn sch_default_params() { let g = parse_expr("sch Cube(s: 2) { box(s, s, s) }\nCube()").unwrap(); assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn sch_default_params_override() { let g = parse_expr("sch Cube(s: 2) { box(s, s, s) }\nCube(5)").unwrap(); assert!((evaluate(&g, 5.0, 0.0, 0.0) - 0.0).abs() < 1e-6); }
|
||||||
|
#[test] fn sch_mixed_defaults() { let g = parse_expr("sch Pillar(r, h: 10) {\n cylinder(r, h)\n}\nPillar(2)").unwrap(); assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn sch_value_returning() { let g = parse_expr("sch double(v) = v * 2\ndouble(5)").unwrap(); assert!((evaluate(&g, 0.0, 0.0, 0.0) - 10.0).abs() < 1e-10); }
|
||||||
|
#[test] fn sch_nested_definition() { let g = parse_expr("sch Outer(r) {\n sch Inner(s) { sphere(s) }\n translate(Inner(r), r, 0, 0)\n}\nOuter(3)").unwrap(); assert!(evaluate(&g, 3.0, 0.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn sch_outer_scope_visible() { let g = parse_expr("let k = 5\nsch S(r) { sphere(r + k) }\nS(1)").unwrap(); assert!((evaluate(&g, 6.0, 0.0, 0.0) - 0.0).abs() < 1e-6); }
|
||||||
|
#[test] fn map_basic() { let g = parse_expr("map(i, 0..5) { translate(sphere(1), i * 3, 0, 0) }").unwrap(); assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 6.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 1.5, 0.0, 0.0) > 0.0); }
|
||||||
|
#[test] fn map_with_sch() { let g = parse_expr("sch Peg(r) { sphere(r) }\nmap(i, 0..3) { translate(Peg(1), i * 4, 0, 0) }").unwrap(); assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 4.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 8.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 2.0, 0.0, 0.0) > 0.0); }
|
||||||
|
#[test] fn map_rotation_ring() { let g = parse_expr("map(i, 0..4) { rotate_z(translate(sphere(0.5), 5, 0, 0), i * pi/2) }").unwrap(); assert!(evaluate(&g, 5.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 0.0, 5.0, 0.0) < 0.0); assert!(evaluate(&g, -5.0, 0.0, 0.0) < 0.0); assert!(evaluate(&g, 0.0, -5.0, 0.0) < 0.0); }
|
||||||
|
#[test] fn let_with_map() { let scene = parse_expr_scene("let row: Obj = map(i, 0..3) { translate(sphere(1), i * 3, 0, 0) }\ncast()").unwrap(); assert!(scene.cast_all); let g = &scene.graph; assert!(evaluate(g, 0.0, 0.0, 0.0) < 0.0); assert!(evaluate(g, 3.0, 0.0, 0.0) < 0.0); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-format"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "ZCD archive format — ZIP container for source, trig, shader, and CORDIC layers"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["format", "archive", "zip", "sdf", "cordic"]
|
||||||
|
categories = ["encoding", "graphics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zip = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
anyhow = "1"
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
//! ZCD archive format for Cord geometry.
|
||||||
|
//!
|
||||||
|
//! A `.zcd` file is a ZIP container that can hold any combination of:
|
||||||
|
//! - Cordial source (`.crd`)
|
||||||
|
//! - Serialized TrigGraph (`.trig`)
|
||||||
|
//! - WGSL shader
|
||||||
|
//! - CORDIC binary (`.cord`)
|
||||||
|
//!
|
||||||
|
//! A manifest tracks which layers are present.
|
||||||
|
|
||||||
|
pub mod read;
|
||||||
|
pub mod write;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// .crd = cordial source, .cord = CORDIC binary, .zcd = zipped cord archive
|
||||||
|
pub const ZCD_EXTENSION: &str = "zcd";
|
||||||
|
pub const CRD_EXTENSION: &str = "crd";
|
||||||
|
pub const CORD_EXTENSION: &str = "cord";
|
||||||
|
|
||||||
|
/// Manifest describing what layers are present in a .zcd file.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Manifest {
|
||||||
|
pub version: u32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub cordic_word_bits: Option<u8>,
|
||||||
|
pub layers: Layers,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Layers {
|
||||||
|
pub source: bool,
|
||||||
|
pub trig: bool,
|
||||||
|
pub shader: bool,
|
||||||
|
pub cordic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Manifest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: 1,
|
||||||
|
name: None,
|
||||||
|
cordic_word_bits: None,
|
||||||
|
layers: Layers {
|
||||||
|
source: false,
|
||||||
|
trig: false,
|
||||||
|
shader: false,
|
||||||
|
cordic: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::Manifest;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::io::{Read, Seek};
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
pub struct ZcdReader<R: Read + Seek> {
|
||||||
|
archive: ZipArchive<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> ZcdReader<R> {
|
||||||
|
pub fn new(reader: R) -> Result<Self> {
|
||||||
|
let archive = ZipArchive::new(reader)?;
|
||||||
|
Ok(Self { archive })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manifest(&mut self) -> Result<Manifest> {
|
||||||
|
let mut file = self.archive.by_name("manifest.json")
|
||||||
|
.context("missing manifest.json")?;
|
||||||
|
let mut buf = String::new();
|
||||||
|
file.read_to_string(&mut buf)?;
|
||||||
|
Ok(serde_json::from_str(&buf)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read source, trying .crd first then .scad fallback.
|
||||||
|
pub fn read_source(&mut self) -> Result<Option<String>> {
|
||||||
|
for path in &["source/model.crd", "source/model.scad"] {
|
||||||
|
if let Ok(mut file) = self.archive.by_name(path) {
|
||||||
|
let mut buf = String::new();
|
||||||
|
file.read_to_string(&mut buf)?;
|
||||||
|
return Ok(Some(buf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_trig(&mut self) -> Result<Option<Vec<u8>>> {
|
||||||
|
match self.archive.by_name("trig/scene.trig") {
|
||||||
|
Ok(mut file) => {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
Ok(Some(buf))
|
||||||
|
}
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_shader(&mut self) -> Result<Option<String>> {
|
||||||
|
match self.archive.by_name("shader/scene.wgsl") {
|
||||||
|
Ok(mut file) => {
|
||||||
|
let mut buf = String::new();
|
||||||
|
file.read_to_string(&mut buf)?;
|
||||||
|
Ok(Some(buf))
|
||||||
|
}
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read CORDIC binary, trying new path then legacy fallback.
|
||||||
|
pub fn read_cordic(&mut self) -> Result<Option<Vec<u8>>> {
|
||||||
|
for path in &["cordic/scene.cord", "cordic/scene.bin"] {
|
||||||
|
if let Ok(mut file) = self.archive.by_name(path) {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
return Ok(Some(buf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::Manifest;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::io::{Seek, Write};
|
||||||
|
use zip::write::SimpleFileOptions;
|
||||||
|
use zip::ZipWriter;
|
||||||
|
|
||||||
|
pub struct ZcdWriter<W: Write + Seek> {
|
||||||
|
zip: ZipWriter<W>,
|
||||||
|
manifest: Manifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write + Seek> ZcdWriter<W> {
|
||||||
|
pub fn new(writer: W) -> Self {
|
||||||
|
Self {
|
||||||
|
zip: ZipWriter::new(writer),
|
||||||
|
manifest: Manifest::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_name(&mut self, name: &str) {
|
||||||
|
self.manifest.name = Some(name.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write cordial source (.crd).
|
||||||
|
pub fn write_source_crd(&mut self, source: &str) -> Result<()> {
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
self.zip.start_file("source/model.crd", options)?;
|
||||||
|
self.zip.write_all(source.as_bytes())?;
|
||||||
|
self.manifest.layers.source = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write SCAD source (legacy).
|
||||||
|
pub fn write_source_scad(&mut self, source: &str) -> Result<()> {
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
self.zip.start_file("source/model.scad", options)?;
|
||||||
|
self.zip.write_all(source.as_bytes())?;
|
||||||
|
self.manifest.layers.source = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write serialized TrigGraph IR.
|
||||||
|
pub fn write_trig(&mut self, trig_bytes: &[u8]) -> Result<()> {
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
self.zip.start_file("trig/scene.trig", options)?;
|
||||||
|
self.zip.write_all(trig_bytes)?;
|
||||||
|
self.manifest.layers.trig = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_shader(&mut self, wgsl_source: &str) -> Result<()> {
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
self.zip.start_file("shader/scene.wgsl", options)?;
|
||||||
|
self.zip.write_all(wgsl_source.as_bytes())?;
|
||||||
|
self.manifest.layers.shader = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write compiled CORDIC binary (.cord).
|
||||||
|
pub fn write_cordic(&mut self, cordic_binary: &[u8], word_bits: u8) -> Result<()> {
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
self.zip.start_file("cordic/scene.cord", options)?;
|
||||||
|
self.zip.write_all(cordic_binary)?;
|
||||||
|
self.manifest.layers.cordic = true;
|
||||||
|
self.manifest.cordic_word_bits = Some(word_bits);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(mut self) -> Result<W> {
|
||||||
|
let manifest_json = serde_json::to_string_pretty(&self.manifest)?;
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
self.zip.start_file("manifest.json", options)?;
|
||||||
|
self.zip.write_all(manifest_json.as_bytes())?;
|
||||||
|
Ok(self.zip.finish()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-gui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Interactive GUI editor for Cord geometry"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cord-gui"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
cord-cordic = { path = "../cord-cordic" }
|
||||||
|
cord-shader = { path = "../cord-shader" }
|
||||||
|
cord-parse = { path = "../cord-parse" }
|
||||||
|
cord-sdf = { path = "../cord-sdf" }
|
||||||
|
cord-format = { path = "../cord-format" }
|
||||||
|
cord-decompile = { path = "../cord-decompile" }
|
||||||
|
cord-expr = { path = "../cord-expr" }
|
||||||
|
iced = { version = "0.14", features = ["wgpu", "advanced", "markdown", "tokio", "canvas"] }
|
||||||
|
rfd = "0.15"
|
||||||
|
bytemuck = { version = "1", features = ["derive"] }
|
||||||
|
anyhow = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
dirs = "6"
|
||||||
|
arboard = "3"
|
||||||
|
zip = "2"
|
||||||
|
muda = "0.17"
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Cord</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Cord</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.else-if.cord</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>cord-gui</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Cordial Source</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>crd</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>org.else-if.cord.source</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Cord Archive</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>zcd</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>org.else-if.cord.archive</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>CORDIC Binary</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>cord</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>org.else-if.cord.binary</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>OpenSCAD Source</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>scad</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>STL Mesh</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>stl</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.standard-tessellation-language</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>OBJ Mesh</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>obj</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.geometry-definition-format</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>3MF Model</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>3mf</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>org.else-if.cord.source</string>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>Cordial Source File</string>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.plain-text</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>crd</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>org.else-if.cord.archive</string>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>Cord Archive</string>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
<string>public.archive</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>zcd</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>org.else-if.cord.binary</string>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>CORDIC Binary</string>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>cord</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,218 @@
|
||||||
|
use iced::advanced::text;
|
||||||
|
use iced::advanced::text::highlighter;
|
||||||
|
use iced::{Color, Font};
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
const KW: &[&str] = &[
|
||||||
|
"let", "fn", "if", "else", "for", "in", "while", "return", "true", "false",
|
||||||
|
"cast", "plot", "sch", "map",
|
||||||
|
];
|
||||||
|
|
||||||
|
const BUILTINS: &[&str] = &[
|
||||||
|
"sin", "cos", "tan", "asin", "acos", "atan", "atan2",
|
||||||
|
"sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
|
||||||
|
"arcsin", "arccos", "arctan", "arcsinh", "arccosh", "arctanh", "arcos", "arcosh",
|
||||||
|
"sqrt", "exp", "ln", "log", "abs", "hypot", "min", "max",
|
||||||
|
"length", "mag", "mix", "clip", "clamp", "smoothstep", "quantize",
|
||||||
|
"saw", "tri", "square",
|
||||||
|
"lpf", "hpf", "bpf", "am", "fm", "dft",
|
||||||
|
"envelope", "hilbert", "phase",
|
||||||
|
"sphere", "box", "cylinder", "ngon",
|
||||||
|
"translate", "mov", "move",
|
||||||
|
"rotate_x", "rotate_y", "rotate_z", "rx", "ry", "rz",
|
||||||
|
"scale", "mirror_x", "mirror_y", "mirror_z", "mx", "my", "mz",
|
||||||
|
"union", "intersect", "diff", "subtract",
|
||||||
|
];
|
||||||
|
|
||||||
|
const TYPES: &[&str] = &[
|
||||||
|
"f64", "f32", "i32", "u32", "bool", "sdf", "vec2", "vec3", "vec4",
|
||||||
|
"Obj", "obj",
|
||||||
|
];
|
||||||
|
|
||||||
|
const CONSTS: &[&str] = &["pi", "PI", "e", "E", "x", "y", "z", "reg"];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TokenKind {
|
||||||
|
Keyword,
|
||||||
|
Builtin,
|
||||||
|
Constant,
|
||||||
|
TypeName,
|
||||||
|
Number,
|
||||||
|
Operator,
|
||||||
|
Paren,
|
||||||
|
Comment,
|
||||||
|
Plain,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct CordHighlighterSettings;
|
||||||
|
|
||||||
|
pub struct CordHighlighter {
|
||||||
|
current_line: usize,
|
||||||
|
in_block_comment: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl text::Highlighter for CordHighlighter {
|
||||||
|
type Settings = CordHighlighterSettings;
|
||||||
|
type Highlight = TokenKind;
|
||||||
|
type Iterator<'a> = std::vec::IntoIter<(Range<usize>, TokenKind)>;
|
||||||
|
|
||||||
|
fn new(_settings: &Self::Settings) -> Self {
|
||||||
|
Self { current_line: 0, in_block_comment: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _new_settings: &Self::Settings) {}
|
||||||
|
|
||||||
|
fn change_line(&mut self, line: usize) {
|
||||||
|
self.current_line = line;
|
||||||
|
if line == 0 {
|
||||||
|
self.in_block_comment = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
||||||
|
self.current_line += 1;
|
||||||
|
let spans = lex_line_with_state(line, &mut self.in_block_comment);
|
||||||
|
spans.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_line(&self) -> usize {
|
||||||
|
self.current_line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_token(kind: &TokenKind, _theme: &iced::Theme) -> highlighter::Format<Font> {
|
||||||
|
let color = match kind {
|
||||||
|
TokenKind::Keyword => Color::from_rgb(0.55, 0.75, 1.0),
|
||||||
|
TokenKind::Builtin => Color::from_rgb(0.6, 0.85, 0.75),
|
||||||
|
TokenKind::Constant => Color::from_rgb(0.85, 0.7, 1.0),
|
||||||
|
TokenKind::TypeName => Color::from_rgb(0.45, 0.80, 0.95),
|
||||||
|
TokenKind::Number => Color::from_rgb(0.95, 0.75, 0.45),
|
||||||
|
TokenKind::Operator => Color::from_rgb(0.85, 0.85, 0.85),
|
||||||
|
TokenKind::Paren => Color::from_rgb(0.65, 0.65, 0.65),
|
||||||
|
TokenKind::Comment => Color::from_rgb(0.45, 0.50, 0.45),
|
||||||
|
TokenKind::Plain => Color::from_rgb(0.90, 0.90, 0.90),
|
||||||
|
};
|
||||||
|
highlighter::Format {
|
||||||
|
color: Some(color),
|
||||||
|
font: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex_line_with_state(line: &str, in_block_comment: &mut bool) -> Vec<(Range<usize>, TokenKind)> {
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
let bytes = line.as_bytes();
|
||||||
|
let len = bytes.len();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < len {
|
||||||
|
// Inside block comment: scan for */
|
||||||
|
if *in_block_comment {
|
||||||
|
let start = i;
|
||||||
|
while i < len {
|
||||||
|
if bytes[i] == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
|
||||||
|
i += 2;
|
||||||
|
*in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
spans.push((start..i, TokenKind::Comment));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = bytes[i];
|
||||||
|
|
||||||
|
// Line comment (// or /=)
|
||||||
|
if b == b'/' && i + 1 < len && (bytes[i + 1] == b'/' || bytes[i + 1] == b'=') {
|
||||||
|
spans.push((i..len, TokenKind::Comment));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block comment start
|
||||||
|
if b == b'/' && i + 1 < len && bytes[i + 1] == b'*' {
|
||||||
|
let start = i;
|
||||||
|
i += 2;
|
||||||
|
while i < len {
|
||||||
|
if bytes[i] == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i >= len && !(len >= 2 && bytes[len - 2] == b'*' && bytes[len - 1] == b'/') {
|
||||||
|
*in_block_comment = true;
|
||||||
|
}
|
||||||
|
spans.push((start..i, TokenKind::Comment));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitespace
|
||||||
|
if b == b' ' || b == b'\t' {
|
||||||
|
let start = i;
|
||||||
|
while i < len && (bytes[i] == b' ' || bytes[i] == b'\t') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
spans.push((start..i, TokenKind::Plain));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number
|
||||||
|
if b.is_ascii_digit() || (b == b'.' && i + 1 < len && bytes[i + 1].is_ascii_digit()) {
|
||||||
|
let start = i;
|
||||||
|
while i < len && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
spans.push((start..i, TokenKind::Number));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier
|
||||||
|
if b.is_ascii_alphabetic() || b == b'_' {
|
||||||
|
let start = i;
|
||||||
|
while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let word = &line[start..i];
|
||||||
|
let kind = if KW.contains(&word) {
|
||||||
|
TokenKind::Keyword
|
||||||
|
} else if TYPES.contains(&word) {
|
||||||
|
TokenKind::TypeName
|
||||||
|
} else if BUILTINS.contains(&word) {
|
||||||
|
TokenKind::Builtin
|
||||||
|
} else if CONSTS.contains(&word) {
|
||||||
|
TokenKind::Constant
|
||||||
|
} else {
|
||||||
|
TokenKind::Plain
|
||||||
|
};
|
||||||
|
spans.push((start..i, kind));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
if b"+-*/^=;:".contains(&b) {
|
||||||
|
spans.push((i..i + 1, TokenKind::Operator));
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parens
|
||||||
|
if b"()[]{}".contains(&b) {
|
||||||
|
spans.push((i..i + 1, TokenKind::Paren));
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comma
|
||||||
|
if b == b',' {
|
||||||
|
spans.push((i..i + 1, TokenKind::Plain));
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.push((i..i + 1, TokenKind::Plain));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
spans
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
mod app;
|
||||||
|
mod highlight;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod operations;
|
||||||
|
mod viewport;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
fn theme(_: &App) -> iced::Theme {
|
||||||
|
iced::Theme::Dark
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(app: &App) -> String {
|
||||||
|
app.title()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::application(App::new, App::update, App::view)
|
||||||
|
.title(title)
|
||||||
|
.theme(theme)
|
||||||
|
.subscription(App::subscription)
|
||||||
|
.antialiasing(true)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
static MC_EDGE_TABLE: [u16; 256] = [
|
||||||
|
0x000, 0x109, 0x203, 0x30A, 0x406, 0x50F, 0x605, 0x70C,
|
||||||
|
0x80C, 0x905, 0xA0F, 0xB06, 0xC0A, 0xD03, 0xE09, 0xF00,
|
||||||
|
0x190, 0x099, 0x393, 0x29A, 0x596, 0x49F, 0x795, 0x69C,
|
||||||
|
0x99C, 0x895, 0xB9F, 0xA96, 0xD9A, 0xC93, 0xF99, 0xE90,
|
||||||
|
0x230, 0x339, 0x033, 0x13A, 0x636, 0x73F, 0x435, 0x53C,
|
||||||
|
0xA3C, 0xB35, 0x83F, 0x936, 0xE3A, 0xF33, 0xC39, 0xD30,
|
||||||
|
0x3A0, 0x2A9, 0x1A3, 0x0AA, 0x7A6, 0x6AF, 0x5A5, 0x4AC,
|
||||||
|
0xBAC, 0xAA5, 0x9AF, 0x8A6, 0xFAA, 0xEA3, 0xDA9, 0xCA0,
|
||||||
|
0x460, 0x569, 0x663, 0x76A, 0x066, 0x16F, 0x265, 0x36C,
|
||||||
|
0xC6C, 0xD65, 0xE6F, 0xF66, 0x86A, 0x963, 0xA69, 0xB60,
|
||||||
|
0x5F0, 0x4F9, 0x7F3, 0x6FA, 0x1F6, 0x0FF, 0x3F5, 0x2FC,
|
||||||
|
0xDFC, 0xCF5, 0xFFF, 0xEF6, 0x9FA, 0x8F3, 0xBF9, 0xAF0,
|
||||||
|
0x650, 0x759, 0x453, 0x55A, 0x256, 0x35F, 0x055, 0x15C,
|
||||||
|
0xE5C, 0xF55, 0xC5F, 0xD56, 0xA5A, 0xB53, 0x859, 0x950,
|
||||||
|
0x7C0, 0x6C9, 0x5C3, 0x4CA, 0x3C6, 0x2CF, 0x1C5, 0x0CC,
|
||||||
|
0xFCC, 0xEC5, 0xDCF, 0xCC6, 0xBCA, 0xAC3, 0x9C9, 0x8C0,
|
||||||
|
0x8C0, 0x9C9, 0xAC3, 0xBCA, 0xCC6, 0xDCF, 0xEC5, 0xFCC,
|
||||||
|
0x0CC, 0x1C5, 0x2CF, 0x3C6, 0x4CA, 0x5C3, 0x6C9, 0x7C0,
|
||||||
|
0x950, 0x859, 0xB53, 0xA5A, 0xD56, 0xC5F, 0xF55, 0xE5C,
|
||||||
|
0x15C, 0x055, 0x35F, 0x256, 0x55A, 0x453, 0x759, 0x650,
|
||||||
|
0xAF0, 0xBF9, 0x8F3, 0x9FA, 0xEF6, 0xFFF, 0xCF5, 0xDFC,
|
||||||
|
0x2FC, 0x3F5, 0x0FF, 0x1F6, 0x6FA, 0x7F3, 0x4F9, 0x5F0,
|
||||||
|
0xB60, 0xA69, 0x963, 0x86A, 0xF66, 0xE6F, 0xD65, 0xC6C,
|
||||||
|
0x36C, 0x265, 0x16F, 0x066, 0x76A, 0x663, 0x569, 0x460,
|
||||||
|
0xCA0, 0xDA9, 0xEA3, 0xFAA, 0x8A6, 0x9AF, 0xAA5, 0xBAC,
|
||||||
|
0x4AC, 0x5A5, 0x6AF, 0x7A6, 0x0AA, 0x1A3, 0x2A9, 0x3A0,
|
||||||
|
0xD30, 0xC39, 0xF33, 0xE3A, 0x936, 0x83F, 0xB35, 0xA3C,
|
||||||
|
0x53C, 0x435, 0x73F, 0x636, 0x13A, 0x033, 0x339, 0x230,
|
||||||
|
0xE90, 0xF99, 0xC93, 0xD9A, 0xA96, 0xB9F, 0x895, 0x99C,
|
||||||
|
0x69C, 0x795, 0x49F, 0x596, 0x29A, 0x393, 0x099, 0x190,
|
||||||
|
0xF00, 0xE09, 0xD03, 0xC0A, 0xB06, 0xA0F, 0x905, 0x80C,
|
||||||
|
0x70C, 0x605, 0x50F, 0x406, 0x30A, 0x203, 0x109, 0x000,
|
||||||
|
];
|
||||||
|
|
||||||
|
static MC_TRI_TABLE: [[i8; 16]; 256] = [
|
||||||
|
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,1,9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,8,3,9,8,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,3,1,2,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,2,10,0,2,9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,8,3,2,10,8,10,9,8,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,11,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,11,2,8,11,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,9,0,2,3,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,11,2,1,9,11,9,8,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,10,1,11,10,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,10,1,0,8,10,8,11,10,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,9,0,3,11,9,11,10,9,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,8,10,10,8,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,7,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,3,0,7,3,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,1,9,8,4,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,1,9,4,7,1,7,3,1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,10,8,4,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,4,7,3,0,4,1,2,10,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,2,10,9,0,2,8,4,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,10,9,2,9,7,2,7,3,7,9,4,-1,-1,-1,-1],
|
||||||
|
[8,4,7,3,11,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[11,4,7,11,2,4,2,0,4,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,0,1,8,4,7,2,3,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,7,11,9,4,11,9,11,2,9,2,1,-1,-1,-1,-1],
|
||||||
|
[3,10,1,3,11,10,7,8,4,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,11,10,1,4,11,1,0,4,7,11,4,-1,-1,-1,-1],
|
||||||
|
[4,7,8,9,0,11,9,11,10,11,0,3,-1,-1,-1,-1],
|
||||||
|
[4,7,11,4,11,9,9,11,10,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,5,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,5,4,0,8,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,5,4,1,5,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,5,4,8,3,5,3,1,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,10,9,5,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,0,8,1,2,10,4,9,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,2,10,5,4,2,4,0,2,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,10,5,3,2,5,3,5,4,3,4,8,-1,-1,-1,-1],
|
||||||
|
[9,5,4,2,3,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,11,2,0,8,11,4,9,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,5,4,0,1,5,2,3,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,1,5,2,5,8,2,8,11,4,8,5,-1,-1,-1,-1],
|
||||||
|
[10,3,11,10,1,3,9,5,4,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,9,5,0,8,1,8,10,1,8,11,10,-1,-1,-1,-1],
|
||||||
|
[5,4,0,5,0,11,5,11,10,11,0,3,-1,-1,-1,-1],
|
||||||
|
[5,4,8,5,8,10,10,8,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,7,8,5,7,9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,3,0,9,5,3,5,7,3,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,7,8,0,1,7,1,5,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,5,3,3,5,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,7,8,9,5,7,10,1,2,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,1,2,9,5,0,5,3,0,5,7,3,-1,-1,-1,-1],
|
||||||
|
[8,0,2,8,2,5,8,5,7,10,5,2,-1,-1,-1,-1],
|
||||||
|
[2,10,5,2,5,3,3,5,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[7,9,5,7,8,9,3,11,2,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,5,7,9,7,2,9,2,0,2,7,11,-1,-1,-1,-1],
|
||||||
|
[2,3,11,0,1,8,1,7,8,1,5,7,-1,-1,-1,-1],
|
||||||
|
[11,2,1,11,1,7,7,1,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,5,8,8,5,7,10,1,3,10,3,11,-1,-1,-1,-1],
|
||||||
|
[5,7,0,5,0,9,7,11,0,1,0,10,11,10,0,-1],
|
||||||
|
[11,10,0,11,0,3,10,5,0,8,0,7,5,7,0,-1],
|
||||||
|
[11,10,5,7,11,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,6,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,3,5,10,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,0,1,5,10,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,8,3,1,9,8,5,10,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,6,5,2,6,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,6,5,1,2,6,3,0,8,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,6,5,9,0,6,0,2,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,9,8,5,8,2,5,2,6,3,2,8,-1,-1,-1,-1],
|
||||||
|
[2,3,11,10,6,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[11,0,8,11,2,0,10,6,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,1,9,2,3,11,5,10,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,10,6,1,9,2,9,11,2,9,8,11,-1,-1,-1,-1],
|
||||||
|
[6,3,11,6,5,3,5,1,3,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,11,0,11,5,0,5,1,5,11,6,-1,-1,-1,-1],
|
||||||
|
[3,11,6,0,3,6,0,6,5,0,5,9,-1,-1,-1,-1],
|
||||||
|
[6,5,9,6,9,11,11,9,8,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,10,6,4,7,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,3,0,4,7,3,6,5,10,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,9,0,5,10,6,8,4,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,6,5,1,9,7,1,7,3,7,9,4,-1,-1,-1,-1],
|
||||||
|
[6,1,2,6,5,1,4,7,8,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,5,5,2,6,3,0,4,3,4,7,-1,-1,-1,-1],
|
||||||
|
[8,4,7,9,0,5,0,6,5,0,2,6,-1,-1,-1,-1],
|
||||||
|
[7,3,9,7,9,4,3,2,9,5,9,6,2,6,9,-1],
|
||||||
|
[3,11,2,7,8,4,10,6,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,10,6,4,7,2,4,2,0,2,7,11,-1,-1,-1,-1],
|
||||||
|
[0,1,9,4,7,8,2,3,11,5,10,6,-1,-1,-1,-1],
|
||||||
|
[9,2,1,9,11,2,9,4,11,7,11,4,5,10,6,-1],
|
||||||
|
[8,4,7,3,11,5,3,5,1,5,11,6,-1,-1,-1,-1],
|
||||||
|
[5,1,11,5,11,6,1,0,11,7,11,4,0,4,11,-1],
|
||||||
|
[0,5,9,0,6,5,0,3,6,11,6,3,8,4,7,-1],
|
||||||
|
[6,5,9,6,9,11,4,7,9,7,11,9,-1,-1,-1,-1],
|
||||||
|
[10,4,9,6,4,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,10,6,4,9,10,0,8,3,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,0,1,10,6,0,6,4,0,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,3,1,8,1,6,8,6,4,6,1,10,-1,-1,-1,-1],
|
||||||
|
[1,4,9,1,2,4,2,6,4,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,0,8,1,2,9,2,4,9,2,6,4,-1,-1,-1,-1],
|
||||||
|
[0,2,4,4,2,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,3,2,8,2,4,4,2,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,4,9,10,6,4,11,2,3,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,2,2,8,11,4,9,10,4,10,6,-1,-1,-1,-1],
|
||||||
|
[3,11,2,0,1,6,0,6,4,6,1,10,-1,-1,-1,-1],
|
||||||
|
[6,4,1,6,1,10,4,8,1,2,1,11,8,11,1,-1],
|
||||||
|
[9,6,4,9,3,6,9,1,3,11,6,3,-1,-1,-1,-1],
|
||||||
|
[8,11,1,8,1,0,11,6,1,9,1,4,6,4,1,-1],
|
||||||
|
[3,11,6,3,6,0,0,6,4,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[6,4,8,11,6,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[7,10,6,7,8,10,8,9,10,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,7,3,0,10,7,0,9,10,6,7,10,-1,-1,-1,-1],
|
||||||
|
[10,6,7,1,10,7,1,7,8,1,8,0,-1,-1,-1,-1],
|
||||||
|
[10,6,7,10,7,1,1,7,3,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,6,1,6,8,1,8,9,8,6,7,-1,-1,-1,-1],
|
||||||
|
[2,6,9,2,9,1,6,7,9,0,9,3,7,3,9,-1],
|
||||||
|
[7,8,0,7,0,6,6,0,2,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[7,3,2,6,7,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,3,11,10,6,8,10,8,9,8,6,7,-1,-1,-1,-1],
|
||||||
|
[2,0,7,2,7,11,0,9,7,6,7,10,9,10,7,-1],
|
||||||
|
[1,8,0,1,7,8,1,10,7,6,7,10,2,3,11,-1],
|
||||||
|
[11,2,1,11,1,7,10,6,1,6,7,1,-1,-1,-1,-1],
|
||||||
|
[8,9,6,8,6,7,9,1,6,11,6,3,1,3,6,-1],
|
||||||
|
[0,9,1,11,6,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[7,8,0,7,0,6,3,11,0,11,6,0,-1,-1,-1,-1],
|
||||||
|
[7,11,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[7,6,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,0,8,11,7,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,1,9,11,7,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,1,9,8,3,1,11,7,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,1,2,6,11,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,10,3,0,8,6,11,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,9,0,2,10,9,6,11,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[6,11,7,2,10,3,10,8,3,10,9,8,-1,-1,-1,-1],
|
||||||
|
[7,2,3,6,2,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[7,0,8,7,6,0,6,2,0,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,7,6,2,3,7,0,1,9,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,6,2,1,8,6,1,9,8,8,7,6,-1,-1,-1,-1],
|
||||||
|
[10,7,6,10,1,7,1,3,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,7,6,1,7,10,1,8,7,1,0,8,-1,-1,-1,-1],
|
||||||
|
[0,3,7,0,7,10,0,10,9,6,10,7,-1,-1,-1,-1],
|
||||||
|
[7,6,10,7,10,8,8,10,9,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[6,8,4,11,8,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,6,11,3,0,6,0,4,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,6,11,8,4,6,9,0,1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,4,6,9,6,3,9,3,1,11,3,6,-1,-1,-1,-1],
|
||||||
|
[6,8,4,6,11,8,2,10,1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,10,3,0,11,0,6,11,0,4,6,-1,-1,-1,-1],
|
||||||
|
[4,11,8,4,6,11,0,2,9,2,10,9,-1,-1,-1,-1],
|
||||||
|
[10,9,3,10,3,2,9,4,3,11,3,6,4,6,3,-1],
|
||||||
|
[8,2,3,8,4,2,4,6,2,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,4,2,4,6,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,9,0,2,3,4,2,4,6,4,3,8,-1,-1,-1,-1],
|
||||||
|
[1,9,4,1,4,2,2,4,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,1,3,8,6,1,8,4,6,6,10,1,-1,-1,-1,-1],
|
||||||
|
[10,1,0,10,0,6,6,0,4,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,6,3,4,3,8,6,10,3,0,3,9,10,9,3,-1],
|
||||||
|
[10,9,4,6,10,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,9,5,7,6,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,3,4,9,5,11,7,6,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,0,1,5,4,0,7,6,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[11,7,6,8,3,4,3,5,4,3,1,5,-1,-1,-1,-1],
|
||||||
|
[9,5,4,10,1,2,7,6,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[6,11,7,1,2,10,0,8,3,4,9,5,-1,-1,-1,-1],
|
||||||
|
[7,6,11,5,4,10,4,2,10,4,0,2,-1,-1,-1,-1],
|
||||||
|
[3,4,8,3,5,4,3,2,5,10,5,2,11,7,6,-1],
|
||||||
|
[7,2,3,7,6,2,5,4,9,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,5,4,0,8,6,0,6,2,6,8,7,-1,-1,-1,-1],
|
||||||
|
[3,6,2,3,7,6,1,5,0,5,4,0,-1,-1,-1,-1],
|
||||||
|
[6,2,8,6,8,7,2,1,8,4,8,5,1,5,8,-1],
|
||||||
|
[9,5,4,10,1,6,1,7,6,1,3,7,-1,-1,-1,-1],
|
||||||
|
[1,6,10,1,7,6,1,0,7,8,7,0,9,5,4,-1],
|
||||||
|
[4,0,10,4,10,5,0,3,10,6,10,7,3,7,10,-1],
|
||||||
|
[7,6,10,7,10,8,5,4,10,4,8,10,-1,-1,-1,-1],
|
||||||
|
[6,9,5,6,11,9,11,8,9,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,6,11,0,6,3,0,5,6,0,9,5,-1,-1,-1,-1],
|
||||||
|
[0,11,8,0,5,11,0,1,5,5,6,11,-1,-1,-1,-1],
|
||||||
|
[6,11,3,6,3,5,5,3,1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,10,9,5,11,9,11,8,11,5,6,-1,-1,-1,-1],
|
||||||
|
[0,11,3,0,6,11,0,9,6,5,6,9,1,2,10,-1],
|
||||||
|
[11,8,5,11,5,6,8,0,5,10,5,2,0,2,5,-1],
|
||||||
|
[6,11,3,6,3,5,2,10,3,10,5,3,-1,-1,-1,-1],
|
||||||
|
[5,8,9,5,2,8,5,6,2,3,8,2,-1,-1,-1,-1],
|
||||||
|
[9,5,6,9,6,0,0,6,2,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,5,8,1,8,0,5,6,8,3,8,2,6,2,8,-1],
|
||||||
|
[1,5,6,2,1,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,3,6,1,6,10,3,8,6,5,6,9,8,9,6,-1],
|
||||||
|
[10,1,0,10,0,6,9,5,0,5,6,0,-1,-1,-1,-1],
|
||||||
|
[0,3,8,5,6,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,5,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[11,5,10,7,5,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[11,5,10,11,7,5,8,3,0,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,11,7,5,10,11,1,9,0,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[10,7,5,10,11,7,9,8,1,8,3,1,-1,-1,-1,-1],
|
||||||
|
[11,1,2,11,7,1,7,5,1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,3,1,2,7,1,7,5,7,2,11,-1,-1,-1,-1],
|
||||||
|
[9,7,5,9,2,7,9,0,2,2,11,7,-1,-1,-1,-1],
|
||||||
|
[7,5,2,7,2,11,5,9,2,3,2,8,9,8,2,-1],
|
||||||
|
[2,5,10,2,3,5,3,7,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,2,0,8,5,2,8,7,5,10,2,5,-1,-1,-1,-1],
|
||||||
|
[9,0,1,5,10,3,5,3,7,3,10,2,-1,-1,-1,-1],
|
||||||
|
[9,8,2,9,2,1,8,7,2,10,2,5,7,5,2,-1],
|
||||||
|
[1,3,5,3,7,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,7,0,7,1,1,7,5,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,0,3,9,3,5,5,3,7,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,8,7,5,9,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,8,4,5,10,8,10,11,8,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[5,0,4,5,11,0,5,10,11,11,3,0,-1,-1,-1,-1],
|
||||||
|
[0,1,9,8,4,10,8,10,11,10,4,5,-1,-1,-1,-1],
|
||||||
|
[10,11,4,10,4,5,11,3,4,9,4,1,3,1,4,-1],
|
||||||
|
[2,5,1,2,8,5,2,11,8,4,5,8,-1,-1,-1,-1],
|
||||||
|
[0,4,11,0,11,3,4,5,11,2,11,1,5,1,11,-1],
|
||||||
|
[0,2,5,0,5,9,2,11,5,4,5,8,11,8,5,-1],
|
||||||
|
[9,4,5,2,11,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,5,10,3,5,2,3,4,5,3,8,4,-1,-1,-1,-1],
|
||||||
|
[5,10,2,5,2,4,4,2,0,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,10,2,3,5,10,3,8,5,4,5,8,0,1,9,-1],
|
||||||
|
[5,10,2,5,2,4,1,9,2,9,4,2,-1,-1,-1,-1],
|
||||||
|
[8,4,5,8,5,3,3,5,1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,4,5,1,0,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[8,4,5,8,5,3,9,0,5,0,3,5,-1,-1,-1,-1],
|
||||||
|
[9,4,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,11,7,4,9,11,9,10,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,8,3,4,9,7,9,11,7,9,10,11,-1,-1,-1,-1],
|
||||||
|
[1,10,11,1,11,4,1,4,0,7,4,11,-1,-1,-1,-1],
|
||||||
|
[3,1,4,3,4,8,1,10,4,7,4,11,10,11,4,-1],
|
||||||
|
[4,11,7,9,11,4,9,2,11,9,1,2,-1,-1,-1,-1],
|
||||||
|
[9,7,4,9,11,7,9,1,11,2,11,1,0,8,3,-1],
|
||||||
|
[11,7,4,11,4,2,2,4,0,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[11,7,4,11,4,2,8,3,4,3,2,4,-1,-1,-1,-1],
|
||||||
|
[2,9,10,2,7,9,2,3,7,7,4,9,-1,-1,-1,-1],
|
||||||
|
[9,10,7,9,7,4,10,2,7,8,7,0,2,0,7,-1],
|
||||||
|
[3,7,10,3,10,2,7,4,10,1,10,0,4,0,10,-1],
|
||||||
|
[1,10,2,8,7,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,9,1,4,1,7,7,1,3,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,9,1,4,1,7,0,8,1,8,7,1,-1,-1,-1,-1],
|
||||||
|
[4,0,3,7,4,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[4,8,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,10,8,10,11,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,0,9,3,9,11,11,9,10,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,1,10,0,10,8,8,10,11,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,1,10,11,3,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,2,11,1,11,9,9,11,8,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,0,9,3,9,11,1,2,9,2,11,9,-1,-1,-1,-1],
|
||||||
|
[0,2,11,8,0,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[3,2,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,3,8,2,8,10,10,8,9,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[9,10,2,0,9,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[2,3,8,2,8,10,0,1,8,1,10,8,-1,-1,-1,-1],
|
||||||
|
[1,10,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[1,3,8,9,1,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,9,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[0,3,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
use cord_expr::ExprInfo;
|
||||||
|
use cord_trig::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Op {
|
||||||
|
Union,
|
||||||
|
Intersection,
|
||||||
|
Difference,
|
||||||
|
SmoothUnion,
|
||||||
|
Add,
|
||||||
|
Subtract,
|
||||||
|
Multiply,
|
||||||
|
Divide,
|
||||||
|
Power,
|
||||||
|
InvPower,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Op {
|
||||||
|
pub const ALL: &[Op] = &[
|
||||||
|
Op::Union,
|
||||||
|
Op::Intersection,
|
||||||
|
Op::Difference,
|
||||||
|
Op::SmoothUnion,
|
||||||
|
Op::Add,
|
||||||
|
Op::Subtract,
|
||||||
|
Op::Multiply,
|
||||||
|
Op::Divide,
|
||||||
|
Op::Power,
|
||||||
|
Op::InvPower,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Op::Union => "union(A, B)",
|
||||||
|
Op::Intersection => "intersect(A, B)",
|
||||||
|
Op::Difference => "diff(A, B)",
|
||||||
|
Op::SmoothUnion => "smooth(A, B, k)",
|
||||||
|
Op::Add => "A + B",
|
||||||
|
Op::Subtract => "A - B",
|
||||||
|
Op::Multiply => "A \u{00d7} B",
|
||||||
|
Op::Divide => "A / B",
|
||||||
|
Op::Power => "A ^ B",
|
||||||
|
Op::InvPower => "A ^ -B",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(self, a: &ExprInfo, b: &ExprInfo) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Op::Power | Op::InvPower => Some("general power not yet in IR"),
|
||||||
|
|
||||||
|
Op::Union | Op::Intersection | Op::Difference | Op::SmoothUnion => {
|
||||||
|
if a.dimensions == 0 && b.dimensions == 0 {
|
||||||
|
return Some("both expressions are constants");
|
||||||
|
}
|
||||||
|
if a.dimensions != b.dimensions && a.dimensions != 0 && b.dimensions != 0 {
|
||||||
|
return Some("dimension mismatch");
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Divide => None,
|
||||||
|
Op::Add | Op::Subtract | Op::Multiply => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Op {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.label())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn combine(a: &TrigGraph, b: &TrigGraph, op: Op, smooth_k: f64) -> TrigGraph {
|
||||||
|
let mut graph = TrigGraph::new();
|
||||||
|
|
||||||
|
let a_map = remap_nodes(&mut graph, a);
|
||||||
|
let a_out = a_map[a.output as usize];
|
||||||
|
|
||||||
|
let b_map = remap_nodes(&mut graph, b);
|
||||||
|
let b_out = b_map[b.output as usize];
|
||||||
|
|
||||||
|
let result = match op {
|
||||||
|
Op::Add => graph.push(TrigOp::Add(a_out, b_out)),
|
||||||
|
Op::Subtract => graph.push(TrigOp::Sub(a_out, b_out)),
|
||||||
|
Op::Multiply => graph.push(TrigOp::Mul(a_out, b_out)),
|
||||||
|
Op::Divide => graph.push(TrigOp::Div(a_out, b_out)),
|
||||||
|
Op::Power => graph.push(TrigOp::Mul(a_out, a_out)),
|
||||||
|
Op::InvPower => {
|
||||||
|
let neg_b = graph.push(TrigOp::Neg(b_out));
|
||||||
|
graph.push(TrigOp::Mul(a_out, neg_b))
|
||||||
|
}
|
||||||
|
Op::Union => graph.push(TrigOp::Min(a_out, b_out)),
|
||||||
|
Op::Intersection => graph.push(TrigOp::Max(a_out, b_out)),
|
||||||
|
Op::Difference => {
|
||||||
|
let neg_b = graph.push(TrigOp::Neg(b_out));
|
||||||
|
graph.push(TrigOp::Max(a_out, neg_b))
|
||||||
|
}
|
||||||
|
Op::SmoothUnion => build_smooth_union(&mut graph, a_out, b_out, smooth_k),
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.set_output(result);
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remap_nodes(target: &mut TrigGraph, source: &TrigGraph) -> Vec<NodeId> {
|
||||||
|
let mut map = Vec::with_capacity(source.nodes.len());
|
||||||
|
|
||||||
|
for op in &source.nodes {
|
||||||
|
let new_op = match op {
|
||||||
|
TrigOp::InputX => TrigOp::InputX,
|
||||||
|
TrigOp::InputY => TrigOp::InputY,
|
||||||
|
TrigOp::InputZ => TrigOp::InputZ,
|
||||||
|
TrigOp::Const(c) => TrigOp::Const(*c),
|
||||||
|
TrigOp::Add(a, b) => TrigOp::Add(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Sub(a, b) => TrigOp::Sub(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Mul(a, b) => TrigOp::Mul(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Div(a, b) => TrigOp::Div(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Neg(a) => TrigOp::Neg(map[*a as usize]),
|
||||||
|
TrigOp::Abs(a) => TrigOp::Abs(map[*a as usize]),
|
||||||
|
TrigOp::Sin(a) => TrigOp::Sin(map[*a as usize]),
|
||||||
|
TrigOp::Cos(a) => TrigOp::Cos(map[*a as usize]),
|
||||||
|
TrigOp::Tan(a) => TrigOp::Tan(map[*a as usize]),
|
||||||
|
TrigOp::Asin(a) => TrigOp::Asin(map[*a as usize]),
|
||||||
|
TrigOp::Acos(a) => TrigOp::Acos(map[*a as usize]),
|
||||||
|
TrigOp::Atan(a) => TrigOp::Atan(map[*a as usize]),
|
||||||
|
TrigOp::Sinh(a) => TrigOp::Sinh(map[*a as usize]),
|
||||||
|
TrigOp::Cosh(a) => TrigOp::Cosh(map[*a as usize]),
|
||||||
|
TrigOp::Tanh(a) => TrigOp::Tanh(map[*a as usize]),
|
||||||
|
TrigOp::Asinh(a) => TrigOp::Asinh(map[*a as usize]),
|
||||||
|
TrigOp::Acosh(a) => TrigOp::Acosh(map[*a as usize]),
|
||||||
|
TrigOp::Atanh(a) => TrigOp::Atanh(map[*a as usize]),
|
||||||
|
TrigOp::Sqrt(a) => TrigOp::Sqrt(map[*a as usize]),
|
||||||
|
TrigOp::Exp(a) => TrigOp::Exp(map[*a as usize]),
|
||||||
|
TrigOp::Ln(a) => TrigOp::Ln(map[*a as usize]),
|
||||||
|
TrigOp::Hypot(a, b) => TrigOp::Hypot(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Atan2(a, b) => TrigOp::Atan2(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Min(a, b) => TrigOp::Min(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Max(a, b) => TrigOp::Max(map[*a as usize], map[*b as usize]),
|
||||||
|
TrigOp::Clamp { val, lo, hi } => TrigOp::Clamp {
|
||||||
|
val: map[*val as usize],
|
||||||
|
lo: map[*lo as usize],
|
||||||
|
hi: map[*hi as usize],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
map.push(target.push(new_op));
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// smooth_min(a, b, k) = min(a,b) - h^2 * k * 0.25
|
||||||
|
/// where h = clamp(0.5 + 0.5*(b-a)/k, 0, 1)
|
||||||
|
fn build_smooth_union(g: &mut TrigGraph, a: NodeId, b: NodeId, k: f64) -> NodeId {
|
||||||
|
let k_node = g.push(TrigOp::Const(k));
|
||||||
|
let half = g.push(TrigOp::Const(0.5));
|
||||||
|
let quarter = g.push(TrigOp::Const(0.25));
|
||||||
|
let zero = g.push(TrigOp::Const(0.0));
|
||||||
|
let one = g.push(TrigOp::Const(1.0));
|
||||||
|
|
||||||
|
let diff = g.push(TrigOp::Sub(b, a));
|
||||||
|
let inv_k = g.push(TrigOp::Const(1.0 / k));
|
||||||
|
let ratio = g.push(TrigOp::Mul(diff, inv_k));
|
||||||
|
let scaled = g.push(TrigOp::Mul(half, ratio));
|
||||||
|
let h_raw = g.push(TrigOp::Add(half, scaled));
|
||||||
|
let h = g.push(TrigOp::Clamp { val: h_raw, lo: zero, hi: one });
|
||||||
|
let h2 = g.push(TrigOp::Mul(h, h));
|
||||||
|
let h2k = g.push(TrigOp::Mul(h2, k_node));
|
||||||
|
let correction = g.push(TrigOp::Mul(h2k, quarter));
|
||||||
|
let m = g.push(TrigOp::Min(a, b));
|
||||||
|
g.push(TrigOp::Sub(m, correction))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cord_expr::{classify, parse_expr};
|
||||||
|
use cord_trig::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_add() {
|
||||||
|
let a = parse_expr("x").unwrap();
|
||||||
|
let b = parse_expr("y").unwrap();
|
||||||
|
let c = combine(&a, &b, Op::Add, 0.0);
|
||||||
|
assert!((evaluate(&c, 3.0, 4.0, 0.0) - 7.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_union() {
|
||||||
|
let a = parse_expr("x").unwrap();
|
||||||
|
let b = parse_expr("y").unwrap();
|
||||||
|
let c = combine(&a, &b, Op::Union, 0.0);
|
||||||
|
assert!((evaluate(&c, 3.0, 4.0, 0.0) - 3.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validity_checks() {
|
||||||
|
let a_info = classify(&parse_expr("sin(x) + y").unwrap());
|
||||||
|
let b_info = classify(&parse_expr("z").unwrap());
|
||||||
|
assert!(Op::Union.check(&a_info, &b_info).is_some());
|
||||||
|
assert!(Op::Add.check(&a_info, &b_info).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
use iced::widget::shader;
|
||||||
|
use iced::wgpu;
|
||||||
|
use iced::mouse;
|
||||||
|
use iced::{Event, Rectangle};
|
||||||
|
use iced::Point;
|
||||||
|
|
||||||
|
use cord_trig::TrigGraph;
|
||||||
|
use cord_shader::generate_wgsl_from_trig;
|
||||||
|
|
||||||
|
pub struct SdfViewport {
|
||||||
|
wgsl: String,
|
||||||
|
generation: u64,
|
||||||
|
camera: Camera,
|
||||||
|
bounding_radius: f64,
|
||||||
|
drag_state: DragState,
|
||||||
|
pub render_flags: RenderFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct RenderFlags {
|
||||||
|
pub shadows: bool,
|
||||||
|
pub ao: bool,
|
||||||
|
pub ground: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RenderFlags {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { shadows: true, ao: true, ground: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Camera {
|
||||||
|
yaw: f32,
|
||||||
|
pitch: f32,
|
||||||
|
distance: f32,
|
||||||
|
target: [f32; 3],
|
||||||
|
fov: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DragState {
|
||||||
|
active: bool,
|
||||||
|
last: Option<Point>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
fn position(&self) -> [f32; 3] {
|
||||||
|
let (sy, cy) = self.yaw.sin_cos();
|
||||||
|
let (sp, cp) = self.pitch.sin_cos();
|
||||||
|
[
|
||||||
|
self.target[0] + self.distance * cp * cy,
|
||||||
|
self.target[1] + self.distance * cp * sy,
|
||||||
|
self.target[2] + self.distance * sp,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SdfViewport {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let default = default_graph();
|
||||||
|
Self {
|
||||||
|
wgsl: generate_wgsl_from_trig(&default),
|
||||||
|
generation: 0,
|
||||||
|
camera: Camera {
|
||||||
|
yaw: 0.6,
|
||||||
|
pitch: 0.4,
|
||||||
|
distance: 8.0,
|
||||||
|
target: [0.0, 0.0, 0.0],
|
||||||
|
fov: 1.0,
|
||||||
|
},
|
||||||
|
bounding_radius: 2.0,
|
||||||
|
drag_state: DragState::default(),
|
||||||
|
render_flags: RenderFlags::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_graph(&mut self, graph: &TrigGraph) {
|
||||||
|
self.wgsl = generate_wgsl_from_trig(graph);
|
||||||
|
self.generation += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_bounds(&mut self, radius: f64) {
|
||||||
|
let radius = radius.max(0.5);
|
||||||
|
self.bounding_radius = radius;
|
||||||
|
self.camera.distance = (radius as f32) * 3.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_graph() -> TrigGraph {
|
||||||
|
use cord_trig::ir::{TrigGraph, TrigOp};
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let y = g.push(TrigOp::InputY);
|
||||||
|
let z = g.push(TrigOp::InputZ);
|
||||||
|
let xy = g.push(TrigOp::Hypot(x, y));
|
||||||
|
let mag = g.push(TrigOp::Hypot(xy, z));
|
||||||
|
let r = g.push(TrigOp::Const(2.0));
|
||||||
|
let out = g.push(TrigOp::Sub(mag, r));
|
||||||
|
g.set_output(out);
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ViewportPrimitive {
|
||||||
|
wgsl: String,
|
||||||
|
generation: u64,
|
||||||
|
time: f32,
|
||||||
|
camera_pos: [f32; 3],
|
||||||
|
camera_target: [f32; 3],
|
||||||
|
fov: f32,
|
||||||
|
render_flags: [f32; 4],
|
||||||
|
scene_scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ViewportState {
|
||||||
|
start: Option<std::time::Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: 'static> shader::Program<Message> for SdfViewport {
|
||||||
|
type State = ViewportState;
|
||||||
|
type Primitive = ViewportPrimitive;
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
) -> Self::Primitive {
|
||||||
|
let elapsed = state.start
|
||||||
|
.map(|s| s.elapsed().as_secs_f32())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let rf = &self.render_flags;
|
||||||
|
ViewportPrimitive {
|
||||||
|
wgsl: self.wgsl.clone(),
|
||||||
|
generation: self.generation,
|
||||||
|
time: elapsed,
|
||||||
|
camera_pos: self.camera.position(),
|
||||||
|
camera_target: self.camera.target,
|
||||||
|
fov: self.camera.fov,
|
||||||
|
render_flags: [
|
||||||
|
if rf.shadows { 1.0 } else { 0.0 },
|
||||||
|
if rf.ao { 1.0 } else { 0.0 },
|
||||||
|
if rf.ground { 1.0 } else { 0.0 },
|
||||||
|
0.0,
|
||||||
|
],
|
||||||
|
scene_scale: self.bounding_radius as f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
state: &mut Self::State,
|
||||||
|
_event: &Event,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> Option<shader::Action<Message>> {
|
||||||
|
if state.start.is_none() {
|
||||||
|
state.start = Some(std::time::Instant::now());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_state: &Self::State,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if cursor.is_over(bounds) {
|
||||||
|
if self.drag_state.active {
|
||||||
|
mouse::Interaction::Grabbing
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::Grab
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutable camera updates called from App::update
|
||||||
|
impl SdfViewport {
|
||||||
|
pub fn on_drag(&mut self, dx: f32, dy: f32) {
|
||||||
|
let sensitivity = 0.005;
|
||||||
|
self.camera.yaw -= dx * sensitivity;
|
||||||
|
self.camera.pitch = (self.camera.pitch - dy * sensitivity)
|
||||||
|
.clamp(-1.4, 1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_scroll(&mut self, delta: f32) {
|
||||||
|
let factor = (-delta * 0.08).exp();
|
||||||
|
let max_dist = (self.bounding_radius as f32 * 20.0).max(100.0);
|
||||||
|
let min_dist = (self.bounding_radius as f32 * 0.05).max(0.1);
|
||||||
|
self.camera.distance = (self.camera.distance * factor)
|
||||||
|
.clamp(min_dist, max_dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_drag(&mut self, pos: Point) {
|
||||||
|
self.drag_state.active = true;
|
||||||
|
self.drag_state.last = Some(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drag_to(&mut self, pos: Point) -> bool {
|
||||||
|
if let Some(last) = self.drag_state.last {
|
||||||
|
let dx = pos.x - last.x;
|
||||||
|
let dy = pos.y - last.y;
|
||||||
|
self.on_drag(dx, dy);
|
||||||
|
self.drag_state.last = Some(pos);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_drag(&mut self) {
|
||||||
|
self.drag_state.active = false;
|
||||||
|
self.drag_state.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dragging(&self) -> bool {
|
||||||
|
self.drag_state.active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_camera(&mut self) {
|
||||||
|
self.camera.target = [0.0, 0.0, 0.0];
|
||||||
|
self.camera.yaw = 0.6;
|
||||||
|
self.camera.pitch = 0.4;
|
||||||
|
self.camera.distance = (self.bounding_radius as f32) * 3.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct Uniforms {
|
||||||
|
resolution: [f32; 2],
|
||||||
|
viewport_offset: [f32; 2],
|
||||||
|
camera_pos: [f32; 3],
|
||||||
|
time: f32,
|
||||||
|
camera_target: [f32; 3],
|
||||||
|
fov: f32,
|
||||||
|
render_flags: [f32; 4],
|
||||||
|
scene_scale: f32,
|
||||||
|
_pad: [f32; 7],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SdfPipeline {
|
||||||
|
render_pipeline: wgpu::RenderPipeline,
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
generation: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SdfPipeline {
|
||||||
|
fn build_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
layout: &wgpu::BindGroupLayout,
|
||||||
|
wgsl: &str,
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("sdf_shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(wgsl)),
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("sdf_pl"),
|
||||||
|
bind_group_layouts: &[layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("sdf_rp"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader_module,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader_module,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl shader::Pipeline for SdfPipeline {
|
||||||
|
fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
_queue: &wgpu::Queue,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
) -> Self {
|
||||||
|
let default = default_graph();
|
||||||
|
let wgsl = generate_wgsl_from_trig(&default);
|
||||||
|
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("sdf_bgl"),
|
||||||
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("sdf_uniforms"),
|
||||||
|
size: std::mem::size_of::<Uniforms>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("sdf_bg"),
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline = Self::build_pipeline(device, format, &bind_group_layout, &wgsl);
|
||||||
|
|
||||||
|
SdfPipeline {
|
||||||
|
render_pipeline,
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group_layout,
|
||||||
|
bind_group,
|
||||||
|
format,
|
||||||
|
generation: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl shader::Primitive for ViewportPrimitive {
|
||||||
|
type Pipeline = SdfPipeline;
|
||||||
|
|
||||||
|
fn prepare(
|
||||||
|
&self,
|
||||||
|
pipeline: &mut SdfPipeline,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
bounds: &Rectangle,
|
||||||
|
viewport: &shader::Viewport,
|
||||||
|
) {
|
||||||
|
if self.generation != pipeline.generation {
|
||||||
|
pipeline.render_pipeline = SdfPipeline::build_pipeline(
|
||||||
|
device,
|
||||||
|
pipeline.format,
|
||||||
|
&pipeline.bind_group_layout,
|
||||||
|
&self.wgsl,
|
||||||
|
);
|
||||||
|
pipeline.generation = self.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = viewport.scale_factor() as f32;
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
resolution: [bounds.width * scale, bounds.height * scale],
|
||||||
|
viewport_offset: [bounds.x * scale, bounds.y * scale],
|
||||||
|
camera_pos: self.camera_pos,
|
||||||
|
time: self.time,
|
||||||
|
camera_target: self.camera_target,
|
||||||
|
fov: self.fov,
|
||||||
|
render_flags: self.render_flags,
|
||||||
|
scene_scale: self.scene_scale,
|
||||||
|
_pad: [0.0; 7],
|
||||||
|
};
|
||||||
|
queue.write_buffer(&pipeline.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
pipeline: &SdfPipeline,
|
||||||
|
render_pass: &mut wgpu::RenderPass<'_>,
|
||||||
|
) -> bool {
|
||||||
|
render_pass.set_pipeline(&pipeline.render_pipeline);
|
||||||
|
render_pass.set_bind_group(0, &pipeline.bind_group, &[]);
|
||||||
|
render_pass.draw(0..3, 0..1);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-parse"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "SCAD parser for the Cord geometry system"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["scad", "parser", "csg", "geometry"]
|
||||||
|
categories = ["graphics", "parsing"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = "2"
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
/// Span in source text for error reporting.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Span {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level program: a sequence of statements.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Program {
|
||||||
|
pub statements: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Statement {
|
||||||
|
/// A module call like `cube([10,20,30]);` or `translate([1,0,0]) cube(5);`
|
||||||
|
ModuleCall(ModuleCall),
|
||||||
|
/// Boolean operations: `union() { ... }`, `difference() { ... }`, `intersection() { ... }`
|
||||||
|
BooleanOp(BooleanOp),
|
||||||
|
/// Variable assignment: `x = 10;`
|
||||||
|
Assignment(Assignment),
|
||||||
|
/// Module definition: `module name(params) { ... }`
|
||||||
|
ModuleDef(ModuleDef),
|
||||||
|
/// For loop: `for (i = [start:step:end]) { ... }`
|
||||||
|
ForLoop(ForLoop),
|
||||||
|
/// If/else: `if (cond) { ... } else { ... }`
|
||||||
|
IfElse(IfElse),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ModuleCall {
|
||||||
|
pub name: String,
|
||||||
|
pub args: Vec<Argument>,
|
||||||
|
pub children: Vec<Statement>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BooleanOp {
|
||||||
|
pub op: BooleanKind,
|
||||||
|
pub children: Vec<Statement>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BooleanKind {
|
||||||
|
Union,
|
||||||
|
Difference,
|
||||||
|
Intersection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Assignment {
|
||||||
|
pub name: String,
|
||||||
|
pub value: Expr,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ModuleDef {
|
||||||
|
pub name: String,
|
||||||
|
pub params: Vec<Param>,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Param {
|
||||||
|
pub name: String,
|
||||||
|
pub default: Option<Expr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ForLoop {
|
||||||
|
pub var: String,
|
||||||
|
pub range: ForRange,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ForRange {
|
||||||
|
/// [start : end] or [start : step : end]
|
||||||
|
Range { start: Expr, step: Option<Expr>, end: Expr },
|
||||||
|
/// [a, b, c, ...] — explicit list
|
||||||
|
List(Vec<Expr>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IfElse {
|
||||||
|
pub condition: Expr,
|
||||||
|
pub then_body: Vec<Statement>,
|
||||||
|
pub else_body: Vec<Statement>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Argument {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub value: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Expr {
|
||||||
|
Number(f64),
|
||||||
|
Bool(bool),
|
||||||
|
String(String),
|
||||||
|
Ident(String),
|
||||||
|
Vector(Vec<Expr>),
|
||||||
|
UnaryOp { op: UnaryOp, operand: Box<Expr> },
|
||||||
|
BinaryOp { op: BinaryOp, left: Box<Expr>, right: Box<Expr> },
|
||||||
|
FnCall { name: String, args: Vec<Argument> },
|
||||||
|
Ternary { cond: Box<Expr>, then_expr: Box<Expr>, else_expr: Box<Expr> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum UnaryOp {
|
||||||
|
Neg,
|
||||||
|
Not,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BinaryOp {
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Mod,
|
||||||
|
Lt,
|
||||||
|
Le,
|
||||||
|
Gt,
|
||||||
|
Ge,
|
||||||
|
Eq,
|
||||||
|
Ne,
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,313 @@
|
||||||
|
use crate::ast::Span;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Token {
|
||||||
|
// Literals
|
||||||
|
Number(f64),
|
||||||
|
StringLit(String),
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
|
||||||
|
// Identifiers and keywords
|
||||||
|
Ident(String),
|
||||||
|
Module,
|
||||||
|
Function,
|
||||||
|
If,
|
||||||
|
Else,
|
||||||
|
For,
|
||||||
|
Let,
|
||||||
|
|
||||||
|
// Symbols
|
||||||
|
LParen,
|
||||||
|
RParen,
|
||||||
|
LBrace,
|
||||||
|
RBrace,
|
||||||
|
LBracket,
|
||||||
|
RBracket,
|
||||||
|
Semi,
|
||||||
|
Comma,
|
||||||
|
Dot,
|
||||||
|
Assign,
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Star,
|
||||||
|
Slash,
|
||||||
|
Percent,
|
||||||
|
Lt,
|
||||||
|
Le,
|
||||||
|
Gt,
|
||||||
|
Ge,
|
||||||
|
EqEq,
|
||||||
|
BangEq,
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
Bang,
|
||||||
|
Colon,
|
||||||
|
Question,
|
||||||
|
|
||||||
|
// Special
|
||||||
|
Eof,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SpannedToken {
|
||||||
|
pub token: Token,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Lexer<'a> {
|
||||||
|
source: &'a [u8],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Lexer<'a> {
|
||||||
|
pub fn new(source: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
source: source.as_bytes(),
|
||||||
|
pos: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tokenize(&mut self) -> Result<Vec<SpannedToken>, LexError> {
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
loop {
|
||||||
|
self.skip_whitespace_and_comments();
|
||||||
|
if self.pos >= self.source.len() {
|
||||||
|
tokens.push(SpannedToken {
|
||||||
|
token: Token::Eof,
|
||||||
|
span: Span { start: self.pos, end: self.pos },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokens.push(self.next_token()?);
|
||||||
|
}
|
||||||
|
Ok(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_whitespace_and_comments(&mut self) {
|
||||||
|
loop {
|
||||||
|
// Whitespace
|
||||||
|
while self.pos < self.source.len() && self.source[self.pos].is_ascii_whitespace() {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
// Line comment
|
||||||
|
if self.pos + 1 < self.source.len()
|
||||||
|
&& self.source[self.pos] == b'/'
|
||||||
|
&& self.source[self.pos + 1] == b'/'
|
||||||
|
{
|
||||||
|
while self.pos < self.source.len() && self.source[self.pos] != b'\n' {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Block comment
|
||||||
|
if self.pos + 1 < self.source.len()
|
||||||
|
&& self.source[self.pos] == b'/'
|
||||||
|
&& self.source[self.pos + 1] == b'*'
|
||||||
|
{
|
||||||
|
self.pos += 2;
|
||||||
|
let mut depth = 1u32;
|
||||||
|
while self.pos + 1 < self.source.len() && depth > 0 {
|
||||||
|
if self.source[self.pos] == b'/' && self.source[self.pos + 1] == b'*' {
|
||||||
|
depth += 1;
|
||||||
|
self.pos += 2;
|
||||||
|
} else if self.source[self.pos] == b'*' && self.source[self.pos + 1] == b'/' {
|
||||||
|
depth -= 1;
|
||||||
|
self.pos += 2;
|
||||||
|
} else {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_token(&mut self) -> Result<SpannedToken, LexError> {
|
||||||
|
let start = self.pos;
|
||||||
|
let ch = self.source[self.pos];
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
if ch.is_ascii_digit() || (ch == b'.' && self.peek_is_digit()) {
|
||||||
|
return self.lex_number(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
if ch == b'"' {
|
||||||
|
return self.lex_string(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifiers and keywords
|
||||||
|
if ch.is_ascii_alphabetic() || ch == b'_' || ch == b'$' {
|
||||||
|
return Ok(self.lex_ident(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-char operators
|
||||||
|
if self.pos + 1 < self.source.len() {
|
||||||
|
let next = self.source[self.pos + 1];
|
||||||
|
let two_char = match (ch, next) {
|
||||||
|
(b'<', b'=') => Some(Token::Le),
|
||||||
|
(b'>', b'=') => Some(Token::Ge),
|
||||||
|
(b'=', b'=') => Some(Token::EqEq),
|
||||||
|
(b'!', b'=') => Some(Token::BangEq),
|
||||||
|
(b'&', b'&') => Some(Token::And),
|
||||||
|
(b'|', b'|') => Some(Token::Or),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(token) = two_char {
|
||||||
|
self.pos += 2;
|
||||||
|
return Ok(SpannedToken {
|
||||||
|
token,
|
||||||
|
span: Span { start, end: self.pos },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-char tokens
|
||||||
|
let token = match ch {
|
||||||
|
b'(' => Token::LParen,
|
||||||
|
b')' => Token::RParen,
|
||||||
|
b'{' => Token::LBrace,
|
||||||
|
b'}' => Token::RBrace,
|
||||||
|
b'[' => Token::LBracket,
|
||||||
|
b']' => Token::RBracket,
|
||||||
|
b';' => Token::Semi,
|
||||||
|
b',' => Token::Comma,
|
||||||
|
b'.' => Token::Dot,
|
||||||
|
b'=' => Token::Assign,
|
||||||
|
b'+' => Token::Plus,
|
||||||
|
b'-' => Token::Minus,
|
||||||
|
b'*' => Token::Star,
|
||||||
|
b'/' => Token::Slash,
|
||||||
|
b'%' => Token::Percent,
|
||||||
|
b'<' => Token::Lt,
|
||||||
|
b'>' => Token::Gt,
|
||||||
|
b'!' => Token::Bang,
|
||||||
|
b':' => Token::Colon,
|
||||||
|
b'?' => Token::Question,
|
||||||
|
_ => {
|
||||||
|
return Err(LexError {
|
||||||
|
pos: start,
|
||||||
|
msg: format!("unexpected character: {:?}", ch as char),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(SpannedToken {
|
||||||
|
token,
|
||||||
|
span: Span { start, end: self.pos },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_is_digit(&self) -> bool {
|
||||||
|
self.pos + 1 < self.source.len() && self.source[self.pos + 1].is_ascii_digit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex_number(&mut self, start: usize) -> Result<SpannedToken, LexError> {
|
||||||
|
while self.pos < self.source.len() && self.source[self.pos].is_ascii_digit() {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
if self.pos < self.source.len() && self.source[self.pos] == b'.' {
|
||||||
|
self.pos += 1;
|
||||||
|
while self.pos < self.source.len() && self.source[self.pos].is_ascii_digit() {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scientific notation
|
||||||
|
if self.pos < self.source.len() && (self.source[self.pos] == b'e' || self.source[self.pos] == b'E') {
|
||||||
|
self.pos += 1;
|
||||||
|
if self.pos < self.source.len() && (self.source[self.pos] == b'+' || self.source[self.pos] == b'-') {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
while self.pos < self.source.len() && self.source[self.pos].is_ascii_digit() {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = std::str::from_utf8(&self.source[start..self.pos]).unwrap();
|
||||||
|
let value: f64 = text.parse().map_err(|_| LexError {
|
||||||
|
pos: start,
|
||||||
|
msg: format!("invalid number: {text}"),
|
||||||
|
})?;
|
||||||
|
Ok(SpannedToken {
|
||||||
|
token: Token::Number(value),
|
||||||
|
span: Span { start, end: self.pos },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex_string(&mut self, start: usize) -> Result<SpannedToken, LexError> {
|
||||||
|
self.pos += 1; // skip opening quote
|
||||||
|
let mut s = String::new();
|
||||||
|
while self.pos < self.source.len() && self.source[self.pos] != b'"' {
|
||||||
|
if self.source[self.pos] == b'\\' {
|
||||||
|
self.pos += 1;
|
||||||
|
if self.pos >= self.source.len() {
|
||||||
|
return Err(LexError { pos: self.pos, msg: "unterminated string escape".into() });
|
||||||
|
}
|
||||||
|
match self.source[self.pos] {
|
||||||
|
b'n' => s.push('\n'),
|
||||||
|
b't' => s.push('\t'),
|
||||||
|
b'\\' => s.push('\\'),
|
||||||
|
b'"' => s.push('"'),
|
||||||
|
other => {
|
||||||
|
s.push('\\');
|
||||||
|
s.push(other as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.push(self.source[self.pos] as char);
|
||||||
|
}
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
if self.pos >= self.source.len() {
|
||||||
|
return Err(LexError { pos: start, msg: "unterminated string".into() });
|
||||||
|
}
|
||||||
|
self.pos += 1; // skip closing quote
|
||||||
|
Ok(SpannedToken {
|
||||||
|
token: Token::StringLit(s),
|
||||||
|
span: Span { start, end: self.pos },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex_ident(&mut self, start: usize) -> SpannedToken {
|
||||||
|
while self.pos < self.source.len()
|
||||||
|
&& (self.source[self.pos].is_ascii_alphanumeric()
|
||||||
|
|| self.source[self.pos] == b'_'
|
||||||
|
|| self.source[self.pos] == b'$')
|
||||||
|
{
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
let text = std::str::from_utf8(&self.source[start..self.pos]).unwrap();
|
||||||
|
let token = match text {
|
||||||
|
"module" => Token::Module,
|
||||||
|
"function" => Token::Function,
|
||||||
|
"if" => Token::If,
|
||||||
|
"else" => Token::Else,
|
||||||
|
"for" => Token::For,
|
||||||
|
"let" => Token::Let,
|
||||||
|
"true" => Token::True,
|
||||||
|
"false" => Token::False,
|
||||||
|
_ => Token::Ident(text.to_string()),
|
||||||
|
};
|
||||||
|
SpannedToken {
|
||||||
|
token,
|
||||||
|
span: Span { start, end: self.pos },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LexError {
|
||||||
|
pub pos: usize,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for LexError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "lex error at byte {}: {}", self.pos, self.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for LexError {}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
//! SCAD parser for the Cord geometry system.
|
||||||
|
//!
|
||||||
|
//! Lexes and parses OpenSCAD source into an AST. Supports primitives,
|
||||||
|
//! transforms, boolean ops, for loops, if/else, ternary expressions,
|
||||||
|
//! and variable environments.
|
||||||
|
|
||||||
|
pub mod ast;
|
||||||
|
pub mod lexer;
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
use lexer::Lexer;
|
||||||
|
use parser::Parser;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Lex(lexer::LexError),
|
||||||
|
Parse(parser::ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::Lex(e) => write!(f, "{e}"),
|
||||||
|
Error::Parse(e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
pub fn parse(source: &str) -> Result<ast::Program, Error> {
|
||||||
|
let mut lexer = Lexer::new(source);
|
||||||
|
let tokens = lexer.tokenize().map_err(Error::Lex)?;
|
||||||
|
let mut parser = Parser::new(tokens);
|
||||||
|
parser.parse_program().map_err(Error::Parse)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,508 @@
|
||||||
|
use crate::ast::*;
|
||||||
|
use crate::lexer::{SpannedToken, Token};
|
||||||
|
|
||||||
|
pub struct Parser {
|
||||||
|
tokens: Vec<SpannedToken>,
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ParseError {
|
||||||
|
pub span: Span,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "parse error at {}..{}: {}", self.span.start, self.span.end, self.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ParseError {}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn new(tokens: Vec<SpannedToken>) -> Self {
|
||||||
|
Self { tokens, pos: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_program(&mut self) -> Result<Program, ParseError> {
|
||||||
|
let mut statements = Vec::new();
|
||||||
|
while !self.at_eof() {
|
||||||
|
statements.push(self.parse_statement()?);
|
||||||
|
}
|
||||||
|
Ok(Program { statements })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_statement(&mut self) -> Result<Statement, ParseError> {
|
||||||
|
// Variable assignment: ident = expr;
|
||||||
|
if self.peek_is_ident() && self.peek_ahead_is(1, &Token::Assign) {
|
||||||
|
return self.parse_assignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module definition
|
||||||
|
if self.peek_is(&Token::Module) {
|
||||||
|
return self.parse_module_def();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For loop
|
||||||
|
if self.peek_is(&Token::For) {
|
||||||
|
return self.parse_for_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If/else
|
||||||
|
if self.peek_is(&Token::If) {
|
||||||
|
return self.parse_if_else();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boolean ops or module calls
|
||||||
|
self.parse_module_call_or_boolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_assignment(&mut self) -> Result<Statement, ParseError> {
|
||||||
|
let start = self.current_span();
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
self.expect(&Token::Assign)?;
|
||||||
|
let value = self.parse_expr()?;
|
||||||
|
self.expect(&Token::Semi)?;
|
||||||
|
Ok(Statement::Assignment(Assignment {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
span: Span { start: start.start, end: self.prev_span().end },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_module_def(&mut self) -> Result<Statement, ParseError> {
|
||||||
|
let start = self.current_span();
|
||||||
|
self.expect(&Token::Module)?;
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let params = self.parse_params()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
let body = self.parse_block()?;
|
||||||
|
Ok(Statement::ModuleDef(ModuleDef {
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
span: Span { start: start.start, end: self.prev_span().end },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_params(&mut self) -> Result<Vec<Param>, ParseError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
while !self.peek_is(&Token::RParen) && !self.at_eof() {
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
let default = if self.try_consume(&Token::Assign) {
|
||||||
|
Some(self.parse_expr()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
params.push(Param { name, default });
|
||||||
|
if !self.try_consume(&Token::Comma) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_module_call_or_boolean(&mut self) -> Result<Statement, ParseError> {
|
||||||
|
let start = self.current_span();
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
|
||||||
|
// Check for boolean operations
|
||||||
|
let boolean_kind = match name.as_str() {
|
||||||
|
"union" => Some(BooleanKind::Union),
|
||||||
|
"difference" => Some(BooleanKind::Difference),
|
||||||
|
"intersection" => Some(BooleanKind::Intersection),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let args = self.parse_arguments()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
|
||||||
|
// Children: either a block `{ ... }` or a single trailing statement, or `;`
|
||||||
|
let children = if self.try_consume(&Token::Semi) {
|
||||||
|
Vec::new()
|
||||||
|
} else if self.peek_is(&Token::LBrace) {
|
||||||
|
self.parse_block()?
|
||||||
|
} else {
|
||||||
|
vec![self.parse_statement()?]
|
||||||
|
};
|
||||||
|
|
||||||
|
let span = Span { start: start.start, end: self.prev_span().end };
|
||||||
|
|
||||||
|
if let Some(op) = boolean_kind {
|
||||||
|
Ok(Statement::BooleanOp(BooleanOp { op, children, span }))
|
||||||
|
} else {
|
||||||
|
Ok(Statement::ModuleCall(ModuleCall { name, args, children, span }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_block(&mut self) -> Result<Vec<Statement>, ParseError> {
|
||||||
|
self.expect(&Token::LBrace)?;
|
||||||
|
let mut stmts = Vec::new();
|
||||||
|
while !self.peek_is(&Token::RBrace) && !self.at_eof() {
|
||||||
|
stmts.push(self.parse_statement()?);
|
||||||
|
}
|
||||||
|
self.expect(&Token::RBrace)?;
|
||||||
|
Ok(stmts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_arguments(&mut self) -> Result<Vec<Argument>, ParseError> {
|
||||||
|
let mut args = Vec::new();
|
||||||
|
while !self.peek_is(&Token::RParen) && !self.at_eof() {
|
||||||
|
// Try named argument: ident = expr
|
||||||
|
let arg = if self.peek_is_ident() && self.peek_ahead_is(1, &Token::Assign) {
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
self.expect(&Token::Assign)?;
|
||||||
|
let value = self.parse_expr()?;
|
||||||
|
Argument { name: Some(name), value }
|
||||||
|
} else {
|
||||||
|
Argument { name: None, value: self.parse_expr()? }
|
||||||
|
};
|
||||||
|
args.push(arg);
|
||||||
|
if !self.try_consume(&Token::Comma) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expression parsing with precedence climbing
|
||||||
|
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
self.parse_ternary()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_for_loop(&mut self) -> Result<Statement, ParseError> {
|
||||||
|
let start = self.current_span();
|
||||||
|
self.expect(&Token::For)?;
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let var = self.expect_ident()?;
|
||||||
|
self.expect(&Token::Assign)?;
|
||||||
|
|
||||||
|
// Range: [start : end], [start : step : end], or [a, b, c]
|
||||||
|
self.expect(&Token::LBracket)?;
|
||||||
|
let range = self.parse_for_range()?;
|
||||||
|
self.expect(&Token::RBracket)?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
|
||||||
|
let body = if self.peek_is(&Token::LBrace) {
|
||||||
|
self.parse_block()?
|
||||||
|
} else {
|
||||||
|
vec![self.parse_statement()?]
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Statement::ForLoop(ForLoop {
|
||||||
|
var,
|
||||||
|
range,
|
||||||
|
body,
|
||||||
|
span: Span { start: start.start, end: self.prev_span().end },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_for_range(&mut self) -> Result<ForRange, ParseError> {
|
||||||
|
let first = self.parse_expr()?;
|
||||||
|
|
||||||
|
if self.try_consume(&Token::Colon) {
|
||||||
|
let second = self.parse_expr()?;
|
||||||
|
if self.try_consume(&Token::Colon) {
|
||||||
|
// [start : step : end]
|
||||||
|
let third = self.parse_expr()?;
|
||||||
|
Ok(ForRange::Range { start: first, step: Some(second), end: third })
|
||||||
|
} else {
|
||||||
|
// [start : end]
|
||||||
|
Ok(ForRange::Range { start: first, step: None, end: second })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Explicit list: [a, b, c, ...]
|
||||||
|
let mut list = vec![first];
|
||||||
|
while self.try_consume(&Token::Comma) {
|
||||||
|
list.push(self.parse_expr()?);
|
||||||
|
}
|
||||||
|
Ok(ForRange::List(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_if_else(&mut self) -> Result<Statement, ParseError> {
|
||||||
|
let start = self.current_span();
|
||||||
|
self.expect(&Token::If)?;
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let condition = self.parse_expr()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
|
||||||
|
let then_body = if self.peek_is(&Token::LBrace) {
|
||||||
|
self.parse_block()?
|
||||||
|
} else {
|
||||||
|
vec![self.parse_statement()?]
|
||||||
|
};
|
||||||
|
|
||||||
|
let else_body = if self.try_consume(&Token::Else) {
|
||||||
|
if self.peek_is(&Token::LBrace) {
|
||||||
|
self.parse_block()?
|
||||||
|
} else {
|
||||||
|
vec![self.parse_statement()?]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Statement::IfElse(IfElse {
|
||||||
|
condition,
|
||||||
|
then_body,
|
||||||
|
else_body,
|
||||||
|
span: Span { start: start.start, end: self.prev_span().end },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ternary(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let expr = self.parse_or()?;
|
||||||
|
if self.try_consume(&Token::Question) {
|
||||||
|
let then_expr = self.parse_expr()?;
|
||||||
|
self.expect(&Token::Colon)?;
|
||||||
|
let else_expr = self.parse_expr()?;
|
||||||
|
Ok(Expr::Ternary {
|
||||||
|
cond: Box::new(expr),
|
||||||
|
then_expr: Box::new(then_expr),
|
||||||
|
else_expr: Box::new(else_expr),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_or(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_and()?;
|
||||||
|
while self.try_consume(&Token::Or) {
|
||||||
|
let right = self.parse_and()?;
|
||||||
|
left = Expr::BinaryOp {
|
||||||
|
op: BinaryOp::Or,
|
||||||
|
left: Box::new(left),
|
||||||
|
right: Box::new(right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_and(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_equality()?;
|
||||||
|
while self.try_consume(&Token::And) {
|
||||||
|
let right = self.parse_equality()?;
|
||||||
|
left = Expr::BinaryOp {
|
||||||
|
op: BinaryOp::And,
|
||||||
|
left: Box::new(left),
|
||||||
|
right: Box::new(right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_equality(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_comparison()?;
|
||||||
|
loop {
|
||||||
|
let op = if self.try_consume(&Token::EqEq) {
|
||||||
|
BinaryOp::Eq
|
||||||
|
} else if self.try_consume(&Token::BangEq) {
|
||||||
|
BinaryOp::Ne
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let right = self.parse_comparison()?;
|
||||||
|
left = Expr::BinaryOp { op, left: Box::new(left), right: Box::new(right) };
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_additive()?;
|
||||||
|
loop {
|
||||||
|
let op = if self.try_consume(&Token::Lt) {
|
||||||
|
BinaryOp::Lt
|
||||||
|
} else if self.try_consume(&Token::Le) {
|
||||||
|
BinaryOp::Le
|
||||||
|
} else if self.try_consume(&Token::Gt) {
|
||||||
|
BinaryOp::Gt
|
||||||
|
} else if self.try_consume(&Token::Ge) {
|
||||||
|
BinaryOp::Ge
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let right = self.parse_additive()?;
|
||||||
|
left = Expr::BinaryOp { op, left: Box::new(left), right: Box::new(right) };
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_additive(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_multiplicative()?;
|
||||||
|
loop {
|
||||||
|
let op = if self.try_consume(&Token::Plus) {
|
||||||
|
BinaryOp::Add
|
||||||
|
} else if self.try_consume(&Token::Minus) {
|
||||||
|
BinaryOp::Sub
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let right = self.parse_multiplicative()?;
|
||||||
|
left = Expr::BinaryOp { op, left: Box::new(left), right: Box::new(right) };
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_unary()?;
|
||||||
|
loop {
|
||||||
|
let op = if self.try_consume(&Token::Star) {
|
||||||
|
BinaryOp::Mul
|
||||||
|
} else if self.try_consume(&Token::Slash) {
|
||||||
|
BinaryOp::Div
|
||||||
|
} else if self.try_consume(&Token::Percent) {
|
||||||
|
BinaryOp::Mod
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let right = self.parse_unary()?;
|
||||||
|
left = Expr::BinaryOp { op, left: Box::new(left), right: Box::new(right) };
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unary(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
if self.try_consume(&Token::Minus) {
|
||||||
|
let operand = self.parse_unary()?;
|
||||||
|
return Ok(Expr::UnaryOp { op: UnaryOp::Neg, operand: Box::new(operand) });
|
||||||
|
}
|
||||||
|
if self.try_consume(&Token::Bang) {
|
||||||
|
let operand = self.parse_unary()?;
|
||||||
|
return Ok(Expr::UnaryOp { op: UnaryOp::Not, operand: Box::new(operand) });
|
||||||
|
}
|
||||||
|
self.parse_primary()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_primary(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let tok = &self.tokens[self.pos];
|
||||||
|
match &tok.token {
|
||||||
|
Token::Number(n) => {
|
||||||
|
let n = *n;
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(Expr::Number(n))
|
||||||
|
}
|
||||||
|
Token::True => {
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(Expr::Bool(true))
|
||||||
|
}
|
||||||
|
Token::False => {
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(Expr::Bool(false))
|
||||||
|
}
|
||||||
|
Token::StringLit(s) => {
|
||||||
|
let s = s.clone();
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(Expr::String(s))
|
||||||
|
}
|
||||||
|
Token::Ident(_) => {
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
// Function call
|
||||||
|
if self.peek_is(&Token::LParen) {
|
||||||
|
self.expect(&Token::LParen)?;
|
||||||
|
let args = self.parse_arguments()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
return Ok(Expr::FnCall { name, args });
|
||||||
|
}
|
||||||
|
Ok(Expr::Ident(name))
|
||||||
|
}
|
||||||
|
Token::LBracket => {
|
||||||
|
self.pos += 1;
|
||||||
|
let mut elems = Vec::new();
|
||||||
|
while !self.peek_is(&Token::RBracket) && !self.at_eof() {
|
||||||
|
elems.push(self.parse_expr()?);
|
||||||
|
if !self.try_consume(&Token::Comma) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.expect(&Token::RBracket)?;
|
||||||
|
Ok(Expr::Vector(elems))
|
||||||
|
}
|
||||||
|
Token::LParen => {
|
||||||
|
self.pos += 1;
|
||||||
|
let expr = self.parse_expr()?;
|
||||||
|
self.expect(&Token::RParen)?;
|
||||||
|
Ok(expr)
|
||||||
|
}
|
||||||
|
_ => Err(ParseError {
|
||||||
|
span: tok.span,
|
||||||
|
msg: format!("unexpected token: {:?}", tok.token),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
|
||||||
|
fn at_eof(&self) -> bool {
|
||||||
|
self.pos >= self.tokens.len() || self.tokens[self.pos].token == Token::Eof
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_span(&self) -> Span {
|
||||||
|
if self.pos < self.tokens.len() {
|
||||||
|
self.tokens[self.pos].span
|
||||||
|
} else {
|
||||||
|
let end = self.tokens.last().map_or(0, |t| t.span.end);
|
||||||
|
Span { start: end, end }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_span(&self) -> Span {
|
||||||
|
if self.pos > 0 {
|
||||||
|
self.tokens[self.pos - 1].span
|
||||||
|
} else {
|
||||||
|
Span { start: 0, end: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_is(&self, token: &Token) -> bool {
|
||||||
|
self.pos < self.tokens.len() && std::mem::discriminant(&self.tokens[self.pos].token) == std::mem::discriminant(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_is_ident(&self) -> bool {
|
||||||
|
matches!(self.tokens.get(self.pos), Some(SpannedToken { token: Token::Ident(_), .. }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_ahead_is(&self, offset: usize, token: &Token) -> bool {
|
||||||
|
let idx = self.pos + offset;
|
||||||
|
idx < self.tokens.len()
|
||||||
|
&& std::mem::discriminant(&self.tokens[idx].token) == std::mem::discriminant(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_consume(&mut self, token: &Token) -> bool {
|
||||||
|
if self.peek_is(token) {
|
||||||
|
self.pos += 1;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect(&mut self, token: &Token) -> Result<(), ParseError> {
|
||||||
|
if self.peek_is(token) {
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ParseError {
|
||||||
|
span: self.current_span(),
|
||||||
|
msg: format!("expected {:?}, got {:?}", token, self.tokens.get(self.pos).map(|t| &t.token)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_ident(&mut self) -> Result<String, ParseError> {
|
||||||
|
if let Some(SpannedToken { token: Token::Ident(name), .. }) = self.tokens.get(self.pos) {
|
||||||
|
let name = name.clone();
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(name)
|
||||||
|
} else {
|
||||||
|
Err(ParseError {
|
||||||
|
span: self.current_span(),
|
||||||
|
msg: format!("expected identifier, got {:?}", self.tokens.get(self.pos).map(|t| &t.token)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-render"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "wgpu SDF raymarcher for Cord geometry"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["wgpu", "raymarching", "sdf", "renderer", "3d"]
|
||||||
|
categories = ["graphics", "rendering"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-shader = { path = "../cord-shader" }
|
||||||
|
wgpu = "24"
|
||||||
|
winit = "0.30"
|
||||||
|
pollster = "0.4"
|
||||||
|
bytemuck = { version = "1", features = ["derive"] }
|
||||||
|
glam = "0.29"
|
||||||
|
anyhow = "1"
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
pub struct Camera {
|
||||||
|
pub distance: f32,
|
||||||
|
pub azimuth: f32,
|
||||||
|
pub elevation: f32,
|
||||||
|
pub target: [f32; 3],
|
||||||
|
pub fov: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
pub fn new(scene_radius: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
distance: scene_radius * 3.0,
|
||||||
|
azimuth: 0.4,
|
||||||
|
elevation: 0.5,
|
||||||
|
target: [0.0, 0.0, 0.0],
|
||||||
|
fov: 1.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position(&self) -> [f32; 3] {
|
||||||
|
let cos_el = self.elevation.cos();
|
||||||
|
[
|
||||||
|
self.target[0] + self.distance * cos_el * self.azimuth.cos(),
|
||||||
|
self.target[1] + self.distance * cos_el * self.azimuth.sin(),
|
||||||
|
self.target[2] + self.distance * self.elevation.sin(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn orbit(&mut self, d_azimuth: f32, d_elevation: f32) {
|
||||||
|
self.azimuth += d_azimuth;
|
||||||
|
self.elevation = (self.elevation + d_elevation)
|
||||||
|
.clamp(-std::f32::consts::FRAC_PI_2 + 0.01, std::f32::consts::FRAC_PI_2 - 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom(&mut self, delta: f32) {
|
||||||
|
self.distance *= (-delta * 0.1).exp();
|
||||||
|
self.distance = self.distance.max(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
//! Standalone wgpu SDF raymarcher.
|
||||||
|
//!
|
||||||
|
//! Opens a window, creates a GPU pipeline from a WGSL shader string,
|
||||||
|
//! and renders with orbit camera controls. Used by the CLI `view` command.
|
||||||
|
|
||||||
|
pub mod pipeline;
|
||||||
|
pub mod camera;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use camera::Camera;
|
||||||
|
use pipeline::RenderPipeline;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
||||||
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
|
pub fn run(wgsl_source: String, bounding_radius: f64) -> Result<()> {
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
let mut app = App {
|
||||||
|
state: None,
|
||||||
|
wgsl_source,
|
||||||
|
bounding_radius,
|
||||||
|
};
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
state: Option<RenderState>,
|
||||||
|
wgsl_source: String,
|
||||||
|
bounding_radius: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RenderState {
|
||||||
|
window: Arc<Window>,
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
pipeline: RenderPipeline,
|
||||||
|
camera: Camera,
|
||||||
|
mouse_state: MouseState,
|
||||||
|
start_time: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MouseState {
|
||||||
|
dragging: bool,
|
||||||
|
last_x: f64,
|
||||||
|
last_y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.state.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attrs = Window::default_attributes()
|
||||||
|
.with_title("Cord")
|
||||||
|
.with_inner_size(PhysicalSize::new(1280u32, 720));
|
||||||
|
let window = Arc::new(event_loop.create_window(attrs).unwrap());
|
||||||
|
|
||||||
|
let state = pollster::block_on(init_render_state(
|
||||||
|
window,
|
||||||
|
&self.wgsl_source,
|
||||||
|
self.bounding_radius,
|
||||||
|
));
|
||||||
|
|
||||||
|
match state {
|
||||||
|
Ok(s) => self.state = Some(s),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("render init failed: {e}");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||||
|
let Some(state) = &mut self.state else { return };
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
if size.width > 0 && size.height > 0 {
|
||||||
|
state.config.width = size.width;
|
||||||
|
state.config.height = size.height;
|
||||||
|
state.surface.configure(&state.device, &state.config);
|
||||||
|
state.window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowEvent::MouseInput { state: btn_state, button, .. } => {
|
||||||
|
if button == winit::event::MouseButton::Left {
|
||||||
|
state.mouse_state.dragging = btn_state == winit::event::ElementState::Pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
if state.mouse_state.dragging {
|
||||||
|
let dx = position.x - state.mouse_state.last_x;
|
||||||
|
let dy = position.y - state.mouse_state.last_y;
|
||||||
|
state.camera.orbit(dx as f32 * 0.005, dy as f32 * 0.005);
|
||||||
|
state.window.request_redraw();
|
||||||
|
}
|
||||||
|
state.mouse_state.last_x = position.x;
|
||||||
|
state.mouse_state.last_y = position.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
|
let scroll = match delta {
|
||||||
|
winit::event::MouseScrollDelta::LineDelta(_, y) => y,
|
||||||
|
winit::event::MouseScrollDelta::PixelDelta(p) => p.y as f32 * 0.01,
|
||||||
|
};
|
||||||
|
state.camera.zoom(scroll);
|
||||||
|
state.window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
let output = match state.surface.get_current_texture() {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
|
state.surface.configure(&state.device, &state.config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("surface error: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let elapsed = state.start_time.elapsed().as_secs_f32();
|
||||||
|
state.pipeline.update_uniforms(
|
||||||
|
&state.queue,
|
||||||
|
state.config.width,
|
||||||
|
state.config.height,
|
||||||
|
elapsed,
|
||||||
|
&state.camera,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut encoder = state.device.create_command_encoder(
|
||||||
|
&wgpu::CommandEncoderDescriptor { label: Some("render") },
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("main"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
state.pipeline.draw(&mut pass);
|
||||||
|
}
|
||||||
|
state.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_render_state(
|
||||||
|
window: Arc<Window>,
|
||||||
|
wgsl_source: &str,
|
||||||
|
bounding_radius: f64,
|
||||||
|
) -> Result<RenderState> {
|
||||||
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::all(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let surface = instance.create_surface(window.clone())?;
|
||||||
|
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no suitable GPU adapter"))?;
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
label: Some("cord"),
|
||||||
|
..Default::default()
|
||||||
|
}, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
let caps = surface.get_capabilities(&adapter);
|
||||||
|
let format = caps.formats.iter()
|
||||||
|
.find(|f| f.is_srgb())
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(caps.formats[0]);
|
||||||
|
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
width: size.width.max(1),
|
||||||
|
height: size.height.max(1),
|
||||||
|
present_mode: wgpu::PresentMode::Fifo,
|
||||||
|
alpha_mode: caps.alpha_modes[0],
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
let pipeline = RenderPipeline::new(&device, format, wgsl_source)?;
|
||||||
|
let camera = Camera::new(bounding_radius as f32);
|
||||||
|
|
||||||
|
Ok(RenderState {
|
||||||
|
window,
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
pipeline,
|
||||||
|
camera,
|
||||||
|
mouse_state: MouseState::default(),
|
||||||
|
start_time: std::time::Instant::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
use crate::camera::Camera;
|
||||||
|
use anyhow::Result;
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
|
||||||
|
struct Uniforms {
|
||||||
|
resolution: [f32; 2],
|
||||||
|
time: f32,
|
||||||
|
_pad0: f32,
|
||||||
|
camera_pos: [f32; 3],
|
||||||
|
_pad1: f32,
|
||||||
|
camera_target: [f32; 3],
|
||||||
|
fov: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderPipeline {
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderPipeline {
|
||||||
|
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat, wgsl_source: &str) -> Result<Self> {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("scene"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(wgsl_source.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("uniforms"),
|
||||||
|
size: std::mem::size_of::<Uniforms>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("uniforms_layout"),
|
||||||
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("uniforms_bind"),
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("render_layout"),
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("render"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self { pipeline, uniform_buffer, bind_group })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_uniforms(
|
||||||
|
&self,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
time: f32,
|
||||||
|
camera: &Camera,
|
||||||
|
) {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
resolution: [width as f32, height as f32],
|
||||||
|
time,
|
||||||
|
_pad0: 0.0,
|
||||||
|
camera_pos: camera.position(),
|
||||||
|
_pad1: 0.0,
|
||||||
|
camera_target: camera.target,
|
||||||
|
fov: camera.fov,
|
||||||
|
};
|
||||||
|
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
|
||||||
|
pass.set_pipeline(&self.pipeline);
|
||||||
|
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
pass.draw(0..3, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-riesz"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "3D Riesz transform, monogenic signal analysis, and spatial cepstrum"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["riesz", "monogenic", "fft", "signal-processing", "3d"]
|
||||||
|
categories = ["mathematics", "science"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
rustfft = "6"
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
use crate::fft3d::{fft3d, ifft3d};
|
||||||
|
use rustfft::num_complex::Complex;
|
||||||
|
|
||||||
|
/// 3D spatial cepstrum: detects periodic structures in a scalar field.
|
||||||
|
///
|
||||||
|
/// Cepstrum = IFFT(log(|FFT(f)|))
|
||||||
|
///
|
||||||
|
/// Peaks in the cepstrum correspond to periodicities in the original field:
|
||||||
|
/// screw threads, gear teeth, bolt patterns, array features, lattice structures.
|
||||||
|
///
|
||||||
|
/// The position of a peak gives the period vector (direction + spacing).
|
||||||
|
/// The height of a peak gives the strength of the periodicity.
|
||||||
|
pub struct Cepstrum {
|
||||||
|
pub data: Vec<f64>,
|
||||||
|
pub n: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PeriodicFeature {
|
||||||
|
/// Grid offset of the period (direction and spacing).
|
||||||
|
pub dx: i32,
|
||||||
|
pub dy: i32,
|
||||||
|
pub dz: i32,
|
||||||
|
/// Cepstral magnitude (strength of periodicity).
|
||||||
|
pub strength: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cepstrum {
|
||||||
|
pub fn compute(field: &[f64], n: usize) -> Self {
|
||||||
|
assert_eq!(field.len(), n * n * n);
|
||||||
|
|
||||||
|
let mut spectrum: Vec<Complex<f64>> = field.iter()
|
||||||
|
.map(|&v| Complex::new(v, 0.0))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
fft3d(&mut spectrum, n);
|
||||||
|
|
||||||
|
// log(|FFT|) — use log of magnitude
|
||||||
|
let floor = 1e-10;
|
||||||
|
for val in spectrum.iter_mut() {
|
||||||
|
let mag = val.norm().max(floor);
|
||||||
|
*val = Complex::new(mag.ln(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ifft3d(&mut spectrum, n);
|
||||||
|
|
||||||
|
Cepstrum {
|
||||||
|
data: spectrum.iter().map(|c| c.re).collect(),
|
||||||
|
n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the strongest periodic features.
|
||||||
|
/// Excludes the origin (DC component) and its immediate neighbors.
|
||||||
|
pub fn detect_periodicities(&self, count: usize) -> Vec<PeriodicFeature> {
|
||||||
|
let n = self.n;
|
||||||
|
let half = n / 2;
|
||||||
|
let mut peaks: Vec<PeriodicFeature> = Vec::new();
|
||||||
|
|
||||||
|
for iz in 0..n {
|
||||||
|
for iy in 0..n {
|
||||||
|
for ix in 0..n {
|
||||||
|
// Skip origin region (not a periodicity)
|
||||||
|
let dx = if ix <= half { ix as i32 } else { ix as i32 - n as i32 };
|
||||||
|
let dy = if iy <= half { iy as i32 } else { iy as i32 - n as i32 };
|
||||||
|
let dz = if iz <= half { iz as i32 } else { iz as i32 - n as i32 };
|
||||||
|
|
||||||
|
let dist_sq = dx * dx + dy * dy + dz * dz;
|
||||||
|
if dist_sq <= 4 { continue; } // skip DC neighborhood
|
||||||
|
|
||||||
|
let val = self.data[iz * n * n + iy * n + ix].abs();
|
||||||
|
peaks.push(PeriodicFeature {
|
||||||
|
dx, dy, dz,
|
||||||
|
strength: val,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peaks.sort_by(|a, b| b.strength.partial_cmp(&a.strength).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
peaks.truncate(count);
|
||||||
|
|
||||||
|
// Deduplicate: (dx,dy,dz) and (-dx,-dy,-dz) are the same periodicity
|
||||||
|
let mut deduped: Vec<PeriodicFeature> = Vec::new();
|
||||||
|
for p in &peaks {
|
||||||
|
let is_dup = deduped.iter().any(|q| {
|
||||||
|
(q.dx == -p.dx && q.dy == -p.dy && q.dz == -p.dz)
|
||||||
|
|| (q.dx == p.dx && q.dy == p.dy && q.dz == p.dz)
|
||||||
|
});
|
||||||
|
if !is_dup {
|
||||||
|
deduped.push(*p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
use rustfft::num_complex::Complex;
|
||||||
|
use rustfft::FftPlanner;
|
||||||
|
|
||||||
|
/// 3D FFT on a cubic grid of size N×N×N.
|
||||||
|
///
|
||||||
|
/// Data is stored in z-major order: index = z*N*N + y*N + x.
|
||||||
|
/// The FFT is decomposed into successive 1D FFTs along each axis.
|
||||||
|
pub fn fft3d(data: &mut [Complex<f64>], n: usize) {
|
||||||
|
assert_eq!(data.len(), n * n * n);
|
||||||
|
let mut planner = FftPlanner::new();
|
||||||
|
let fft = planner.plan_fft_forward(n);
|
||||||
|
let mut scratch = vec![Complex::new(0.0, 0.0); fft.get_inplace_scratch_len()];
|
||||||
|
|
||||||
|
// FFT along X (innermost axis)
|
||||||
|
for z in 0..n {
|
||||||
|
for y in 0..n {
|
||||||
|
let offset = z * n * n + y * n;
|
||||||
|
fft.process_with_scratch(&mut data[offset..offset + n], &mut scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFT along Y — need to gather/scatter strided data
|
||||||
|
let mut row = vec![Complex::new(0.0, 0.0); n];
|
||||||
|
for z in 0..n {
|
||||||
|
for x in 0..n {
|
||||||
|
// Gather Y-column
|
||||||
|
for y in 0..n {
|
||||||
|
row[y] = data[z * n * n + y * n + x];
|
||||||
|
}
|
||||||
|
fft.process_with_scratch(&mut row, &mut scratch);
|
||||||
|
// Scatter back
|
||||||
|
for y in 0..n {
|
||||||
|
data[z * n * n + y * n + x] = row[y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFT along Z
|
||||||
|
for y in 0..n {
|
||||||
|
for x in 0..n {
|
||||||
|
// Gather Z-column
|
||||||
|
for z in 0..n {
|
||||||
|
row[z] = data[z * n * n + y * n + x];
|
||||||
|
}
|
||||||
|
fft.process_with_scratch(&mut row, &mut scratch);
|
||||||
|
for z in 0..n {
|
||||||
|
data[z * n * n + y * n + x] = row[z];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3D inverse FFT.
|
||||||
|
pub fn ifft3d(data: &mut [Complex<f64>], n: usize) {
|
||||||
|
assert_eq!(data.len(), n * n * n);
|
||||||
|
let mut planner = FftPlanner::new();
|
||||||
|
let fft = planner.plan_fft_inverse(n);
|
||||||
|
let mut scratch = vec![Complex::new(0.0, 0.0); fft.get_inplace_scratch_len()];
|
||||||
|
let norm = 1.0 / (n * n * n) as f64;
|
||||||
|
|
||||||
|
// IFFT along X
|
||||||
|
for z in 0..n {
|
||||||
|
for y in 0..n {
|
||||||
|
let offset = z * n * n + y * n;
|
||||||
|
fft.process_with_scratch(&mut data[offset..offset + n], &mut scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFFT along Y
|
||||||
|
let mut row = vec![Complex::new(0.0, 0.0); n];
|
||||||
|
for z in 0..n {
|
||||||
|
for x in 0..n {
|
||||||
|
for y in 0..n {
|
||||||
|
row[y] = data[z * n * n + y * n + x];
|
||||||
|
}
|
||||||
|
fft.process_with_scratch(&mut row, &mut scratch);
|
||||||
|
for y in 0..n {
|
||||||
|
data[z * n * n + y * n + x] = row[y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFFT along Z
|
||||||
|
for y in 0..n {
|
||||||
|
for x in 0..n {
|
||||||
|
for z in 0..n {
|
||||||
|
row[z] = data[z * n * n + y * n + x];
|
||||||
|
}
|
||||||
|
fft.process_with_scratch(&mut row, &mut scratch);
|
||||||
|
for z in 0..n {
|
||||||
|
data[z * n * n + y * n + x] = row[z];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
for val in data.iter_mut() {
|
||||||
|
*val *= norm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
//! 3D Riesz transform, monogenic signal analysis, and spatial cepstrum.
|
||||||
|
//!
|
||||||
|
//! Evaluates SDF fields on regular grids, computes the 3D Riesz transform
|
||||||
|
//! via FFT, extracts monogenic signal components (amplitude, phase,
|
||||||
|
//! orientation), and detects spatial periodicities via cepstral analysis.
|
||||||
|
|
||||||
|
pub mod fft3d;
|
||||||
|
pub mod riesz;
|
||||||
|
pub mod monogenic;
|
||||||
|
pub mod cepstrum;
|
||||||
|
|
||||||
|
pub use monogenic::{MonogenicField, MonogenicSample};
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
use crate::riesz::RieszTransform;
|
||||||
|
|
||||||
|
/// The monogenic signal: f + i·R₁f + j·R₂f + k·R₃f
|
||||||
|
///
|
||||||
|
/// At each point in the grid, this quaternion-valued field
|
||||||
|
/// decomposes into amplitude, phase, and orientation:
|
||||||
|
/// - Amplitude: how much geometry is here (energy)
|
||||||
|
/// - Phase: what kind (edge ≈ π/2, ridge ≈ 0, blob ≈ π)
|
||||||
|
/// - Orientation: which way the feature points (2 angles)
|
||||||
|
pub struct MonogenicField {
|
||||||
|
pub samples: Vec<MonogenicSample>,
|
||||||
|
pub n: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct MonogenicSample {
|
||||||
|
/// Original scalar field value.
|
||||||
|
pub f: f64,
|
||||||
|
/// Riesz transform components.
|
||||||
|
pub r1: f64,
|
||||||
|
pub r2: f64,
|
||||||
|
pub r3: f64,
|
||||||
|
/// Local amplitude: sqrt(f² + r1² + r2² + r3²)
|
||||||
|
pub amplitude: f64,
|
||||||
|
/// Local phase: atan2(|r|, f)
|
||||||
|
/// 0 = line/ridge, π/2 = edge/step, π = blob
|
||||||
|
pub phase: f64,
|
||||||
|
/// Orientation: unit vector (r1, r2, r3) / |(r1, r2, r3)|
|
||||||
|
pub orientation: [f64; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonogenicField {
|
||||||
|
/// Compute the monogenic signal from a scalar field.
|
||||||
|
pub fn compute(field: &[f64], n: usize) -> Self {
|
||||||
|
let riesz = RieszTransform::compute(field, n);
|
||||||
|
|
||||||
|
let samples: Vec<MonogenicSample> = (0..n * n * n)
|
||||||
|
.map(|i| {
|
||||||
|
let f = field[i];
|
||||||
|
let r1 = riesz.r1[i];
|
||||||
|
let r2 = riesz.r2[i];
|
||||||
|
let r3 = riesz.r3[i];
|
||||||
|
|
||||||
|
let r_mag = (r1 * r1 + r2 * r2 + r3 * r3).sqrt();
|
||||||
|
let amplitude = (f * f + r_mag * r_mag).sqrt();
|
||||||
|
let phase = r_mag.atan2(f);
|
||||||
|
|
||||||
|
let orientation = if r_mag > 1e-10 {
|
||||||
|
[r1 / r_mag, r2 / r_mag, r3 / r_mag]
|
||||||
|
} else {
|
||||||
|
[0.0, 0.0, 0.0]
|
||||||
|
};
|
||||||
|
|
||||||
|
MonogenicSample {
|
||||||
|
f,
|
||||||
|
r1,
|
||||||
|
r2,
|
||||||
|
r3,
|
||||||
|
amplitude,
|
||||||
|
phase,
|
||||||
|
orientation,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
MonogenicField { samples, n }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract surface samples where amplitude is significant
|
||||||
|
/// and phase indicates a specific feature type.
|
||||||
|
pub fn surface_at_phase(&self, phase_center: f64, phase_tolerance: f64, min_amplitude: f64) -> Vec<(usize, &MonogenicSample)> {
|
||||||
|
self.samples.iter().enumerate()
|
||||||
|
.filter(|(_, s)| {
|
||||||
|
s.amplitude > min_amplitude
|
||||||
|
&& (s.phase - phase_center).abs() < phase_tolerance
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract edge-like features (phase ≈ π/2).
|
||||||
|
pub fn edges(&self, min_amplitude: f64) -> Vec<(usize, &MonogenicSample)> {
|
||||||
|
self.surface_at_phase(std::f64::consts::FRAC_PI_2, 0.4, min_amplitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract ridge-like features (phase ≈ 0).
|
||||||
|
pub fn ridges(&self, min_amplitude: f64) -> Vec<(usize, &MonogenicSample)> {
|
||||||
|
self.surface_at_phase(0.0, 0.4, min_amplitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract blob-like features (phase ≈ π).
|
||||||
|
pub fn blobs(&self, min_amplitude: f64) -> Vec<(usize, &MonogenicSample)> {
|
||||||
|
self.surface_at_phase(std::f64::consts::PI, 0.4, min_amplitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group samples by orientation similarity.
|
||||||
|
/// Returns clusters of grid indices that share a common direction.
|
||||||
|
pub fn cluster_by_orientation(&self, indices: &[(usize, &MonogenicSample)], angle_threshold: f64) -> Vec<Vec<usize>> {
|
||||||
|
let cos_thresh = angle_threshold.cos();
|
||||||
|
let mut clusters: Vec<Vec<usize>> = Vec::new();
|
||||||
|
let mut assigned = vec![false; indices.len()];
|
||||||
|
|
||||||
|
for i in 0..indices.len() {
|
||||||
|
if assigned[i] { continue; }
|
||||||
|
let (idx_i, sample_i) = indices[i];
|
||||||
|
let oi = sample_i.orientation;
|
||||||
|
if oi[0] == 0.0 && oi[1] == 0.0 && oi[2] == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cluster = vec![idx_i];
|
||||||
|
assigned[i] = true;
|
||||||
|
|
||||||
|
for j in (i + 1)..indices.len() {
|
||||||
|
if assigned[j] { continue; }
|
||||||
|
let (idx_j, sample_j) = indices[j];
|
||||||
|
let oj = sample_j.orientation;
|
||||||
|
|
||||||
|
let dot = (oi[0] * oj[0] + oi[1] * oj[1] + oi[2] * oj[2]).abs();
|
||||||
|
if dot > cos_thresh {
|
||||||
|
cluster.push(idx_j);
|
||||||
|
assigned[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cluster.len() >= 3 {
|
||||||
|
clusters.push(cluster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusters
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Index → (ix, iy, iz) grid coordinates.
|
||||||
|
pub fn index_to_coord(&self, idx: usize) -> (usize, usize, usize) {
|
||||||
|
let iz = idx / (self.n * self.n);
|
||||||
|
let iy = (idx % (self.n * self.n)) / self.n;
|
||||||
|
let ix = idx % self.n;
|
||||||
|
(ix, iy, iz)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grid coordinate → world position given bounds.
|
||||||
|
pub fn coord_to_world(&self, ix: usize, iy: usize, iz: usize, min: [f64; 3], max: [f64; 3]) -> [f64; 3] {
|
||||||
|
let step = [
|
||||||
|
(max[0] - min[0]) / (self.n - 1).max(1) as f64,
|
||||||
|
(max[1] - min[1]) / (self.n - 1).max(1) as f64,
|
||||||
|
(max[2] - min[2]) / (self.n - 1).max(1) as f64,
|
||||||
|
];
|
||||||
|
[
|
||||||
|
min[0] + ix as f64 * step[0],
|
||||||
|
min[1] + iy as f64 * step[1],
|
||||||
|
min[2] + iz as f64 * step[2],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::fft3d::{fft3d, ifft3d};
|
||||||
|
use rustfft::num_complex::Complex;
|
||||||
|
|
||||||
|
/// Compute the 3D Riesz transform of a scalar field.
|
||||||
|
///
|
||||||
|
/// Input: real-valued scalar field on an N×N×N grid (e.g., SDF values).
|
||||||
|
/// Output: three vector-valued fields (R₁f, R₂f, R₃f), each N×N×N.
|
||||||
|
///
|
||||||
|
/// In the frequency domain:
|
||||||
|
/// R̂ⱼ(ξ) = -i · (ξⱼ / |ξ|) · f̂(ξ)
|
||||||
|
///
|
||||||
|
/// This is a phase rotation proportional to the direction cosine
|
||||||
|
/// of each frequency component. Exactly what CORDIC rotation does.
|
||||||
|
pub struct RieszTransform {
|
||||||
|
pub r1: Vec<f64>,
|
||||||
|
pub r2: Vec<f64>,
|
||||||
|
pub r3: Vec<f64>,
|
||||||
|
pub n: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RieszTransform {
|
||||||
|
pub fn compute(field: &[f64], n: usize) -> Self {
|
||||||
|
assert_eq!(field.len(), n * n * n);
|
||||||
|
|
||||||
|
// Convert to complex
|
||||||
|
let mut spectrum: Vec<Complex<f64>> = field.iter()
|
||||||
|
.map(|&v| Complex::new(v, 0.0))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
fft3d(&mut spectrum, n);
|
||||||
|
|
||||||
|
let mut r1_spec = vec![Complex::new(0.0, 0.0); n * n * n];
|
||||||
|
let mut r2_spec = vec![Complex::new(0.0, 0.0); n * n * n];
|
||||||
|
let mut r3_spec = vec![Complex::new(0.0, 0.0); n * n * n];
|
||||||
|
|
||||||
|
for iz in 0..n {
|
||||||
|
for iy in 0..n {
|
||||||
|
for ix in 0..n {
|
||||||
|
let idx = iz * n * n + iy * n + ix;
|
||||||
|
|
||||||
|
// Frequency coordinates (centered)
|
||||||
|
let fx = if ix <= n / 2 { ix as f64 } else { ix as f64 - n as f64 };
|
||||||
|
let fy = if iy <= n / 2 { iy as f64 } else { iy as f64 - n as f64 };
|
||||||
|
let fz = if iz <= n / 2 { iz as f64 } else { iz as f64 - n as f64 };
|
||||||
|
|
||||||
|
let mag = (fx * fx + fy * fy + fz * fz).sqrt();
|
||||||
|
|
||||||
|
if mag < 1e-10 {
|
||||||
|
// DC component — Riesz transform of DC is zero
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// R̂ⱼ(ξ) = -i · (ξⱼ / |ξ|) · f̂(ξ)
|
||||||
|
// Multiplying by -i rotates: (a + bi) * (-i) = b - ai
|
||||||
|
let f_hat = spectrum[idx];
|
||||||
|
let neg_i_f = Complex::new(f_hat.im, -f_hat.re);
|
||||||
|
|
||||||
|
r1_spec[idx] = neg_i_f * (fx / mag);
|
||||||
|
r2_spec[idx] = neg_i_f * (fy / mag);
|
||||||
|
r3_spec[idx] = neg_i_f * (fz / mag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ifft3d(&mut r1_spec, n);
|
||||||
|
ifft3d(&mut r2_spec, n);
|
||||||
|
ifft3d(&mut r3_spec, n);
|
||||||
|
|
||||||
|
RieszTransform {
|
||||||
|
r1: r1_spec.iter().map(|c| c.re).collect(),
|
||||||
|
r2: r2_spec.iter().map(|c| c.re).collect(),
|
||||||
|
r3: r3_spec.iter().map(|c| c.re).collect(),
|
||||||
|
n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-sdf"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "SDF tree and lowering to TrigGraph IR"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["sdf", "csg", "geometry", "lowering"]
|
||||||
|
categories = ["graphics", "mathematics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-parse = { path = "../cord-parse" }
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cord-expr = { path = "../cord-expr" }
|
||||||
|
cord-shader = { path = "../cord-shader" }
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
use crate::SdfNode;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
pub fn sdf_to_cordial(node: &SdfNode) -> String {
|
||||||
|
let mut coll = ParamCollector::new(); coll.walk(node, ""); let params = coll.finish();
|
||||||
|
let mut ctx = EmitCtx { counter: 0, buf: String::new(), params: ¶ms, indent: " " };
|
||||||
|
let root = emit_node(&mut ctx, node); let body = ctx.buf;
|
||||||
|
let mut out = String::new();
|
||||||
|
let pn: Vec<&str> = params.iter().map(|(_, (n, _))| n.as_str()).collect();
|
||||||
|
if pn.is_empty() { let _ = writeln!(out, "let scene: Obj = {root}"); let _ = writeln!(out, "cast(scene)"); return out; }
|
||||||
|
let _ = write!(out, "sch Part(\n");
|
||||||
|
for (i, n) in pn.iter().enumerate() { let _ = write!(out, " {n}"); if i+1 < pn.len() { out.push(','); } out.push('\n'); }
|
||||||
|
let _ = writeln!(out, ") {{"); let _ = write!(out, "{body}"); let _ = writeln!(out, " {root}"); let _ = writeln!(out, "}}"); out.push('\n');
|
||||||
|
let _ = writeln!(out, "// dimensions");
|
||||||
|
for (_, (n, v)) in ¶ms { let _ = writeln!(out, "let {n} = {}", fv(*v)); } out.push('\n');
|
||||||
|
let _ = writeln!(out, "let scene: Obj = Part(");
|
||||||
|
for (i, n) in pn.iter().enumerate() { let _ = write!(out, " {n}"); if i+1 < pn.len() { out.push(','); } out.push('\n'); }
|
||||||
|
let _ = writeln!(out, ")"); let _ = writeln!(out, "cast(scene)"); out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fv(v: f64) -> String { if v == v.round() && v.abs() < 1e9 { format!("{:.1}", v) } else { format!("{:.4}", v) } }
|
||||||
|
|
||||||
|
struct ParamCollector { seen: BTreeMap<u64, (String, f64)>, counter: usize }
|
||||||
|
impl ParamCollector {
|
||||||
|
fn new() -> Self { ParamCollector { seen: BTreeMap::new(), counter: 0 } }
|
||||||
|
fn key(v: f64) -> u64 { v.to_bits() }
|
||||||
|
fn register(&mut self, val: f64, hint: &str) {
|
||||||
|
if val == 0.0 || val == 1.0 || val == -1.0 || val == 0.5 || val == -0.5 { return; }
|
||||||
|
let k = Self::key(val); if self.seen.contains_key(&k) { return; }
|
||||||
|
let name = if hint.is_empty() { let n = format!("k{}", self.counter); self.counter += 1; n }
|
||||||
|
else { let base = hint.replace('-', "_").replace(' ', "_");
|
||||||
|
if self.seen.values().any(|(n, _)| n == &base) { let n = format!("{base}_{}", self.counter); self.counter += 1; n } else { base } };
|
||||||
|
self.seen.insert(k, (name, val));
|
||||||
|
}
|
||||||
|
fn walk(&mut self, node: &SdfNode, ctx: &str) {
|
||||||
|
match node {
|
||||||
|
SdfNode::Sphere { radius } => { self.register(*radius, &if ctx.is_empty() { "radius".into() } else { format!("{ctx}_r") }); }
|
||||||
|
SdfNode::Box { half_extents: h } => { let p = if ctx.is_empty() { "half" } else { ctx }; self.register(h[0], &format!("{p}_x")); self.register(h[1], &format!("{p}_y")); self.register(h[2], &format!("{p}_z")); }
|
||||||
|
SdfNode::Cylinder { radius, height } => { let p = if ctx.is_empty() { "cyl" } else { ctx }; self.register(*radius, &format!("{p}_r")); self.register(height / 2.0, &format!("{p}_hh")); }
|
||||||
|
SdfNode::Translate { offset, child } => { self.register(offset[0], &format!("{ctx}tx")); self.register(offset[1], &format!("{ctx}ty")); self.register(offset[2], &format!("{ctx}tz")); self.walk(child, ctx); }
|
||||||
|
SdfNode::Rotate { angle_deg, child, .. } => { self.register(angle_deg.to_radians(), &format!("{ctx}angle")); self.walk(child, ctx); }
|
||||||
|
SdfNode::Scale { factor, child } => { self.register(factor[0], &format!("{ctx}sx")); self.register(factor[1], &format!("{ctx}sy")); self.register(factor[2], &format!("{ctx}sz")); self.walk(child, ctx); }
|
||||||
|
SdfNode::Union(ch) | SdfNode::Intersection(ch) => { for c in ch { self.walk(c, ctx); } }
|
||||||
|
SdfNode::SmoothUnion { children, k } => { self.register(*k, &format!("{ctx}smooth_k")); for c in children { self.walk(c, ctx); } }
|
||||||
|
SdfNode::Difference { base, subtract } => { self.walk(base, ctx); for s in subtract { self.walk(s, ctx); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn finish(self) -> BTreeMap<u64, (String, f64)> { self.seen }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmitCtx<'a> { counter: usize, buf: String, params: &'a BTreeMap<u64, (String, f64)>, indent: &'static str }
|
||||||
|
impl<'a> EmitCtx<'a> {
|
||||||
|
fn fresh(&mut self, pfx: &str) -> String { let n = format!("{pfx}{}", self.counter); self.counter += 1; n }
|
||||||
|
fn val(&self, v: f64) -> String { if let Some((n, _)) = self.params.get(&ParamCollector::key(v)) { n.clone() } else { fv(v) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_node(ctx: &mut EmitCtx, node: &SdfNode) -> String {
|
||||||
|
match node {
|
||||||
|
SdfNode::Sphere { radius } => format!("sphere({})", ctx.val(*radius)),
|
||||||
|
SdfNode::Box { half_extents: h } => format!("box({}, {}, {})", ctx.val(h[0]), ctx.val(h[1]), ctx.val(h[2])),
|
||||||
|
SdfNode::Cylinder { radius, height } => format!("cylinder({}, {})", ctx.val(*radius), ctx.val(height / 2.0)),
|
||||||
|
SdfNode::Translate { offset, child } => {
|
||||||
|
let mut t = *offset; let mut inner = child.as_ref();
|
||||||
|
while let SdfNode::Translate { offset: o2, child: c2 } = inner { t[0] += o2[0]; t[1] += o2[1]; t[2] += o2[2]; inner = c2.as_ref(); }
|
||||||
|
let c = emit_child(ctx, inner, "t"); format!("translate({c}, {}, {}, {})", ctx.val(t[0]), ctx.val(t[1]), ctx.val(t[2]))
|
||||||
|
}
|
||||||
|
SdfNode::Rotate { axis, angle_deg, child } => {
|
||||||
|
let c = emit_child(ctx, child, "r"); let a = ctx.val(angle_deg.to_radians());
|
||||||
|
if axis[0].abs() > 0.9 { format!("rotate_x({c}, {a})") } else if axis[1].abs() > 0.9 { format!("rotate_y({c}, {a})") } else { format!("rotate_z({c}, {a})") }
|
||||||
|
}
|
||||||
|
SdfNode::Scale { factor, child } => { let c = emit_child(ctx, child, "s"); format!("scale({c}, {}, {}, {})", ctx.val(factor[0]), ctx.val(factor[1]), ctx.val(factor[2])) }
|
||||||
|
SdfNode::Union(ch) => emit_chain(ctx, ch, "union"),
|
||||||
|
SdfNode::Intersection(ch) => emit_chain(ctx, ch, "intersect"),
|
||||||
|
SdfNode::Difference { base, subtract } => {
|
||||||
|
let b = emit_child(ctx, base, "d"); let ind = ctx.indent; let mut r = b;
|
||||||
|
for sub in subtract { let s = emit_child_node(ctx, sub, "d"); let n = ctx.fresh("d"); let _ = writeln!(ctx.buf, "{ind}let {n} = diff({r}, {s})"); r = n; } r
|
||||||
|
}
|
||||||
|
SdfNode::SmoothUnion { children, k } => emit_smooth_chain(ctx, children, *k),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn emit_child(ctx: &mut EmitCtx, node: &SdfNode, pfx: &str) -> String { emit_child_node(ctx, node, pfx) }
|
||||||
|
fn emit_child_node(ctx: &mut EmitCtx, node: &SdfNode, pfx: &str) -> String {
|
||||||
|
let expr = emit_node(ctx, node);
|
||||||
|
if !expr.contains('\n') && expr.len() < 60 && expr.chars().filter(|&c| c == '(').count() <= 1 { return expr; }
|
||||||
|
let n = ctx.fresh(pfx); let ind = ctx.indent; let _ = writeln!(ctx.buf, "{ind}let {n} = {expr}"); n
|
||||||
|
}
|
||||||
|
fn emit_chain(ctx: &mut EmitCtx, ch: &[SdfNode], op: &str) -> String {
|
||||||
|
if ch.len() == 1 { return emit_node(ctx, &ch[0]); }
|
||||||
|
let first = emit_child_node(ctx, &ch[0], &op[..1]); let ind = ctx.indent; let mut r = first;
|
||||||
|
for c in &ch[1..] { let x = emit_child_node(ctx, c, &op[..1]); let n = ctx.fresh(&op[..1]); let _ = writeln!(ctx.buf, "{ind}let {n} = {op}({r}, {x})"); r = n; } r
|
||||||
|
}
|
||||||
|
fn emit_smooth_chain(ctx: &mut EmitCtx, ch: &[SdfNode], k: f64) -> String {
|
||||||
|
if ch.len() == 1 { return emit_node(ctx, &ch[0]); }
|
||||||
|
let kv = ctx.val(k); let first = emit_child_node(ctx, &ch[0], "s"); let ind = ctx.indent; let mut r = first;
|
||||||
|
for c in &ch[1..] { let x = emit_child_node(ctx, c, "s"); let n = ctx.fresh("s"); let _ = writeln!(ctx.buf, "{ind}let {n} = smooth_union({r}, {x}, {kv})"); r = n; } r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sphere_cordial() { assert!(sdf_to_cordial(&SdfNode::Sphere { radius: 5.0 }).contains("sphere(")); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn box_cordial() {
|
||||||
|
let src = sdf_to_cordial(&SdfNode::Box { half_extents: [1.0, 2.0, 3.0] });
|
||||||
|
assert!(src.contains("box("));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translate_cordial() {
|
||||||
|
let node = SdfNode::Translate {
|
||||||
|
offset: [1.0, 2.0, 3.0],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 1.0 }),
|
||||||
|
};
|
||||||
|
assert!(sdf_to_cordial(&node).contains("translate("));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotate_x_emitted() {
|
||||||
|
let node = SdfNode::Rotate { axis: [1.0, 0.0, 0.0], angle_deg: 90.0, child: Box::new(SdfNode::Sphere { radius: 1.0 }) };
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("rotate_x("), "expected rotate_x in: {src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotate_y_emitted() {
|
||||||
|
let node = SdfNode::Rotate { axis: [0.0, 1.0, 0.0], angle_deg: 45.0, child: Box::new(SdfNode::Sphere { radius: 1.0 }) };
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("rotate_y("), "expected rotate_y in: {src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotate_z_emitted() {
|
||||||
|
let node = SdfNode::Rotate { axis: [0.0, 0.0, 1.0], angle_deg: 30.0, child: Box::new(SdfNode::Sphere { radius: 1.0 }) };
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("rotate_z("), "expected rotate_z in: {src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_uniform_scale_emitted() {
|
||||||
|
let node = SdfNode::Scale { factor: [2.0, 3.0, 4.0], child: Box::new(SdfNode::Sphere { radius: 1.0 }) };
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("scale("), "expected scale in: {src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smooth_union_preserves_k() {
|
||||||
|
let node = SdfNode::SmoothUnion {
|
||||||
|
children: vec![SdfNode::Sphere { radius: 1.0 }, SdfNode::Sphere { radius: 2.0 }],
|
||||||
|
k: 0.5,
|
||||||
|
};
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("smooth_union("), "expected smooth_union in: {src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smooth_union_nontrivial_k() {
|
||||||
|
let node = SdfNode::SmoothUnion {
|
||||||
|
children: vec![SdfNode::Sphere { radius: 3.0 }, SdfNode::Box { half_extents: [2.0, 2.0, 2.0] }],
|
||||||
|
k: 1.5,
|
||||||
|
};
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("smooth_union("), "expected smooth_union in: {src}");
|
||||||
|
assert!(src.contains("smooth_k") || src.contains("1.5"), "k parameter missing in: {src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn difference_cordial() {
|
||||||
|
let node = SdfNode::Difference {
|
||||||
|
base: Box::new(SdfNode::Box { half_extents: [5.0, 5.0, 5.0] }),
|
||||||
|
subtract: vec![SdfNode::Sphere { radius: 4.0 }],
|
||||||
|
};
|
||||||
|
assert!(sdf_to_cordial(&node).contains("diff("));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cordial_parse_roundtrip() {
|
||||||
|
let node = SdfNode::Difference { base: Box::new(SdfNode::Box { half_extents: [5.0, 5.0, 5.0] }), subtract: vec![SdfNode::Sphere { radius: 4.0 }] };
|
||||||
|
let src = sdf_to_cordial(&node); assert!(cord_expr::parse_expr_scene(&src).is_ok(), "parse failed:\n{src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cordial_smooth_union_parse_roundtrip() {
|
||||||
|
let node = SdfNode::SmoothUnion {
|
||||||
|
children: vec![
|
||||||
|
SdfNode::Sphere { radius: 3.0 },
|
||||||
|
SdfNode::Translate { offset: [4.0, 0.0, 0.0], child: Box::new(SdfNode::Sphere { radius: 2.0 }) },
|
||||||
|
],
|
||||||
|
k: 1.5,
|
||||||
|
};
|
||||||
|
let src = sdf_to_cordial(&node);
|
||||||
|
assert!(cord_expr::parse_expr_scene(&src).is_ok(), "smooth_union cordial parse failed:\n{src}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scad_eval(scad: &str) -> Vec<((f64,f64,f64), f64, f64)> {
|
||||||
|
let prog = cord_parse::parse(scad).unwrap(); let mut sdf = crate::lower::lower_program(&prog).unwrap();
|
||||||
|
crate::simplify::simplify(&mut sdf); let g1 = crate::sdf_to_trig(&sdf); let src = sdf_to_cordial(&sdf);
|
||||||
|
let scene = cord_expr::parse_expr_scene(&src).unwrap_or_else(|e| panic!("{e}\n{src}"));
|
||||||
|
let g2 = cord_expr::resolve_scene(scene);
|
||||||
|
[(0.0,0.0,0.0),(5.0,0.0,0.0),(0.0,5.0,0.0),(0.0,0.0,5.0),(3.0,3.0,3.0),(10.0,0.0,0.0),(-5.0,-5.0,0.0)]
|
||||||
|
.iter().map(|&(x,y,z)| ((x,y,z), cord_trig::eval::evaluate(&g1,x,y,z), cord_trig::eval::evaluate(&g2,x,y,z))).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scad_sphere_roundtrip() { for ((x,y,z),v1,v2) in scad_eval("sphere(r=5);") { assert!((v1-v2).abs() < 1e-4, "at ({x},{y},{z}): {v1} vs {v2}"); } }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scad_cube_roundtrip() { for ((x,y,z),v1,v2) in scad_eval("cube([10,10,10], center=true);") { assert!((v1-v2).abs() < 1e-4, "at ({x},{y},{z}): {v1} vs {v2}"); } }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scad_complex_roundtrip() {
|
||||||
|
let scad = "difference() { sphere(r=10); translate([0,0,5]) cube([15,15,15], center=true); }\ntranslate([25,0,0]) { union() { cylinder(h=20, r=5, center=true); rotate([90,0,0]) cylinder(h=20, r=5, center=true); rotate([0,90,0]) cylinder(h=20, r=5, center=true); }}";
|
||||||
|
for ((x,y,z),v1,v2) in scad_eval(scad) { assert!((v1-v2).abs() < 1e-4, "at ({x},{y},{z}): {v1} vs {v2}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
//! SDF node tree and lowering to [`cord_trig::TrigGraph`].
|
||||||
|
|
||||||
|
pub mod cordial;
|
||||||
|
pub mod lower;
|
||||||
|
pub mod scad;
|
||||||
|
pub mod simplify;
|
||||||
|
pub mod tree;
|
||||||
|
pub mod trig;
|
||||||
|
|
||||||
|
pub use tree::*;
|
||||||
|
pub use simplify::simplify;
|
||||||
|
pub use trig::sdf_to_trig;
|
||||||
|
pub use scad::sdf_to_scad;
|
||||||
|
pub use cordial::sdf_to_cordial;
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
use cord_parse::ast::*;
|
||||||
|
use crate::tree::SdfNode;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LowerError {
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for LowerError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "SDF lowering error: {}", self.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for LowerError {}
|
||||||
|
|
||||||
|
/// Variable environment for expression evaluation during lowering.
|
||||||
|
type Env = HashMap<String, f64>;
|
||||||
|
|
||||||
|
/// Lower a parsed SCAD program into an SDF tree.
|
||||||
|
/// Multiple top-level statements become an implicit union.
|
||||||
|
pub fn lower_program(program: &Program) -> Result<SdfNode, LowerError> {
|
||||||
|
let mut env = Env::new();
|
||||||
|
let nodes = lower_statements(&program.statements, &mut env)?;
|
||||||
|
|
||||||
|
match nodes.len() {
|
||||||
|
0 => Err(LowerError { msg: "empty scene".into() }),
|
||||||
|
1 => Ok(nodes.into_iter().next().unwrap()),
|
||||||
|
_ => Ok(SdfNode::Union(nodes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_statements(stmts: &[Statement], env: &mut Env) -> Result<Vec<SdfNode>, LowerError> {
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
for stmt in stmts {
|
||||||
|
if let Some(node) = lower_statement(stmt, env)? {
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_statement(stmt: &Statement, env: &mut Env) -> Result<Option<SdfNode>, LowerError> {
|
||||||
|
match stmt {
|
||||||
|
Statement::ModuleCall(call) => lower_module_call(call, env).map(Some),
|
||||||
|
Statement::BooleanOp(bop) => lower_boolean(bop, env).map(Some),
|
||||||
|
Statement::Assignment(asgn) => {
|
||||||
|
if let Some(val) = eval_expr_env(&asgn.value, env) {
|
||||||
|
env.insert(asgn.name.clone(), val);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Statement::ModuleDef(_) => Ok(None),
|
||||||
|
Statement::ForLoop(fl) => lower_for_loop(fl, env).map(Some),
|
||||||
|
Statement::IfElse(ie) => lower_if_else(ie, env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unroll a for loop into a union of independent instances.
|
||||||
|
///
|
||||||
|
/// Each iteration gets its own copy of the environment with the loop
|
||||||
|
/// variable bound. Since iterations share no state, every unrolled
|
||||||
|
/// instance is an independent parallel branch — this is where serial
|
||||||
|
/// SCAD becomes parallel geometry.
|
||||||
|
fn lower_for_loop(fl: &ForLoop, env: &mut Env) -> Result<SdfNode, LowerError> {
|
||||||
|
let values = expand_range(&fl.range, env)?;
|
||||||
|
|
||||||
|
let mut branches = Vec::with_capacity(values.len());
|
||||||
|
for val in values {
|
||||||
|
let mut inner_env = env.clone();
|
||||||
|
inner_env.insert(fl.var.clone(), val);
|
||||||
|
let nodes = lower_statements(&fl.body, &mut inner_env)?;
|
||||||
|
match nodes.len() {
|
||||||
|
0 => {}
|
||||||
|
1 => branches.push(nodes.into_iter().next().unwrap()),
|
||||||
|
_ => branches.push(SdfNode::Union(nodes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match branches.len() {
|
||||||
|
0 => Err(LowerError { msg: "for loop produced no geometry".into() }),
|
||||||
|
1 => Ok(branches.into_iter().next().unwrap()),
|
||||||
|
_ => Ok(SdfNode::Union(branches)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand a for-loop range into concrete values.
|
||||||
|
fn expand_range(range: &ForRange, env: &Env) -> Result<Vec<f64>, LowerError> {
|
||||||
|
match range {
|
||||||
|
ForRange::Range { start, step, end } => {
|
||||||
|
let s = eval_expr_env(start, env)
|
||||||
|
.ok_or_else(|| LowerError { msg: "for range start must be constant".into() })?;
|
||||||
|
let e = eval_expr_env(end, env)
|
||||||
|
.ok_or_else(|| LowerError { msg: "for range end must be constant".into() })?;
|
||||||
|
let step_val = step.as_ref()
|
||||||
|
.and_then(|st| eval_expr_env(st, env))
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
|
||||||
|
if step_val.abs() < 1e-15 {
|
||||||
|
return Err(LowerError { msg: "for range step cannot be zero".into() });
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values = Vec::new();
|
||||||
|
let mut v = s;
|
||||||
|
if step_val > 0.0 {
|
||||||
|
while v <= e + 1e-10 {
|
||||||
|
values.push(v);
|
||||||
|
v += step_val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while v >= e - 1e-10 {
|
||||||
|
values.push(v);
|
||||||
|
v += step_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety cap
|
||||||
|
if values.len() > 10000 {
|
||||||
|
return Err(LowerError { msg: "for loop exceeds 10000 iterations".into() });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
ForRange::List(exprs) => {
|
||||||
|
exprs.iter()
|
||||||
|
.map(|e| eval_expr_env(e, env)
|
||||||
|
.ok_or_else(|| LowerError { msg: "for list element must be constant".into() }))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lower if/else by evaluating the condition at lowering time.
|
||||||
|
/// Constant conditions → dead code elimination. Variable conditions
|
||||||
|
/// include both branches (the SDF is defined everywhere; the condition
|
||||||
|
/// selects which geometry appears).
|
||||||
|
fn lower_if_else(ie: &IfElse, env: &mut Env) -> Result<Option<SdfNode>, LowerError> {
|
||||||
|
if let Some(cond) = eval_expr_env(&ie.condition, env) {
|
||||||
|
if cond != 0.0 {
|
||||||
|
let nodes = lower_statements(&ie.then_body, env)?;
|
||||||
|
match nodes.len() {
|
||||||
|
0 => Ok(None),
|
||||||
|
1 => Ok(Some(nodes.into_iter().next().unwrap())),
|
||||||
|
_ => Ok(Some(SdfNode::Union(nodes))),
|
||||||
|
}
|
||||||
|
} else if !ie.else_body.is_empty() {
|
||||||
|
let nodes = lower_statements(&ie.else_body, env)?;
|
||||||
|
match nodes.len() {
|
||||||
|
0 => Ok(None),
|
||||||
|
1 => Ok(Some(nodes.into_iter().next().unwrap())),
|
||||||
|
_ => Ok(Some(SdfNode::Union(nodes))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-constant condition: include both branches as a union
|
||||||
|
// (conservative — the SDF field includes all possible geometry)
|
||||||
|
let mut nodes = lower_statements(&ie.then_body, env)?;
|
||||||
|
nodes.extend(lower_statements(&ie.else_body, env)?);
|
||||||
|
match nodes.len() {
|
||||||
|
0 => Ok(None),
|
||||||
|
1 => Ok(Some(nodes.into_iter().next().unwrap())),
|
||||||
|
_ => Ok(Some(SdfNode::Union(nodes))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a constant expression with variable environment.
|
||||||
|
fn eval_expr_env(expr: &Expr, env: &Env) -> Option<f64> {
|
||||||
|
match expr {
|
||||||
|
Expr::Number(n) => Some(*n),
|
||||||
|
Expr::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
|
||||||
|
Expr::Ident(name) => env.get(name).copied(),
|
||||||
|
Expr::UnaryOp { op: UnaryOp::Neg, operand } => eval_expr_env(operand, env).map(|n| -n),
|
||||||
|
Expr::UnaryOp { op: UnaryOp::Not, operand } => {
|
||||||
|
eval_expr_env(operand, env).map(|n| if n == 0.0 { 1.0 } else { 0.0 })
|
||||||
|
}
|
||||||
|
Expr::BinaryOp { op, left, right } => {
|
||||||
|
let l = eval_expr_env(left, env)?;
|
||||||
|
let r = eval_expr_env(right, env)?;
|
||||||
|
Some(match op {
|
||||||
|
BinaryOp::Add => l + r,
|
||||||
|
BinaryOp::Sub => l - r,
|
||||||
|
BinaryOp::Mul => l * r,
|
||||||
|
BinaryOp::Div => if r != 0.0 { l / r } else { return None },
|
||||||
|
BinaryOp::Mod => if r != 0.0 { l % r } else { return None },
|
||||||
|
BinaryOp::Lt => if l < r { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::Le => if l <= r { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::Gt => if l > r { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::Ge => if l >= r { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::Eq => if (l - r).abs() < 1e-10 { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::Ne => if (l - r).abs() >= 1e-10 { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::And => if l != 0.0 && r != 0.0 { 1.0 } else { 0.0 },
|
||||||
|
BinaryOp::Or => if l != 0.0 || r != 0.0 { 1.0 } else { 0.0 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Expr::Ternary { cond, then_expr, else_expr } => {
|
||||||
|
let c = eval_expr_env(cond, env)?;
|
||||||
|
if c != 0.0 {
|
||||||
|
eval_expr_env(then_expr, env)
|
||||||
|
} else {
|
||||||
|
eval_expr_env(else_expr, env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::FnCall { name, args } => {
|
||||||
|
match name.as_str() {
|
||||||
|
"sin" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::sin),
|
||||||
|
"cos" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::cos),
|
||||||
|
"abs" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::abs),
|
||||||
|
"sqrt" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::sqrt),
|
||||||
|
"floor" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::floor),
|
||||||
|
"ceil" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::ceil),
|
||||||
|
"round" => args.first().and_then(|a| eval_expr_env(&a.value, env)).map(f64::round),
|
||||||
|
"min" => {
|
||||||
|
let a = args.first().and_then(|a| eval_expr_env(&a.value, env))?;
|
||||||
|
let b = args.get(1).and_then(|a| eval_expr_env(&a.value, env))?;
|
||||||
|
Some(a.min(b))
|
||||||
|
}
|
||||||
|
"max" => {
|
||||||
|
let a = args.first().and_then(|a| eval_expr_env(&a.value, env))?;
|
||||||
|
let b = args.get(1).and_then(|a| eval_expr_env(&a.value, env))?;
|
||||||
|
Some(a.max(b))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_module_call(call: &ModuleCall, env: &mut Env) -> Result<SdfNode, LowerError> {
|
||||||
|
let child_nodes = lower_statements(&call.children, env)?;
|
||||||
|
|
||||||
|
match call.name.as_str() {
|
||||||
|
"sphere" => {
|
||||||
|
let r = get_f64(&call.args, "r", 0, env)?.unwrap_or(1.0);
|
||||||
|
Ok(SdfNode::Sphere { radius: r })
|
||||||
|
}
|
||||||
|
"cube" => {
|
||||||
|
let size = get_cube_size(&call.args, env)?;
|
||||||
|
let center = get_named_bool(&call.args, "center")?.unwrap_or(false);
|
||||||
|
if center {
|
||||||
|
Ok(SdfNode::Box { half_extents: [size[0] / 2.0, size[1] / 2.0, size[2] / 2.0] })
|
||||||
|
} else {
|
||||||
|
Ok(SdfNode::Translate {
|
||||||
|
offset: [size[0] / 2.0, size[1] / 2.0, size[2] / 2.0],
|
||||||
|
child: Box::new(SdfNode::Box {
|
||||||
|
half_extents: [size[0] / 2.0, size[1] / 2.0, size[2] / 2.0],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"cylinder" => {
|
||||||
|
let h = get_f64(&call.args, "h", 0, env)?.unwrap_or(1.0);
|
||||||
|
let r = get_f64(&call.args, "r", 1, env)?.unwrap_or(1.0);
|
||||||
|
let center = get_named_bool(&call.args, "center")?.unwrap_or(false);
|
||||||
|
let node = SdfNode::Cylinder { radius: r, height: h };
|
||||||
|
if center {
|
||||||
|
Ok(node)
|
||||||
|
} else {
|
||||||
|
Ok(SdfNode::Translate {
|
||||||
|
offset: [0.0, 0.0, h / 2.0],
|
||||||
|
child: Box::new(node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"translate" => {
|
||||||
|
let v = get_vec3(&call.args, 0, env)?;
|
||||||
|
let child = require_single_child(child_nodes, "translate")?;
|
||||||
|
Ok(SdfNode::Translate { offset: v, child: Box::new(child) })
|
||||||
|
}
|
||||||
|
"rotate" => {
|
||||||
|
let v = get_vec3(&call.args, 0, env)?;
|
||||||
|
let child = require_single_child(child_nodes, "rotate")?;
|
||||||
|
let mut node = child;
|
||||||
|
if v[0] != 0.0 {
|
||||||
|
node = SdfNode::Rotate { axis: [1.0, 0.0, 0.0], angle_deg: v[0], child: Box::new(node) };
|
||||||
|
}
|
||||||
|
if v[1] != 0.0 {
|
||||||
|
node = SdfNode::Rotate { axis: [0.0, 1.0, 0.0], angle_deg: v[1], child: Box::new(node) };
|
||||||
|
}
|
||||||
|
if v[2] != 0.0 {
|
||||||
|
node = SdfNode::Rotate { axis: [0.0, 0.0, 1.0], angle_deg: v[2], child: Box::new(node) };
|
||||||
|
}
|
||||||
|
Ok(node)
|
||||||
|
}
|
||||||
|
"scale" => {
|
||||||
|
let child = require_single_child(child_nodes, "scale")?;
|
||||||
|
if let Some(v) = try_get_vec3(&call.args, 0, env) {
|
||||||
|
Ok(SdfNode::Scale { factor: v, child: Box::new(child) })
|
||||||
|
} else {
|
||||||
|
let s = get_f64(&call.args, "v", 0, env)?.unwrap_or(1.0);
|
||||||
|
Ok(SdfNode::Scale { factor: [s, s, s], child: Box::new(child) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(LowerError { msg: format!("unknown module: {}", call.name) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_boolean(bop: &BooleanOp, env: &mut Env) -> Result<SdfNode, LowerError> {
|
||||||
|
let children = lower_statements(&bop.children, env)?;
|
||||||
|
if children.is_empty() {
|
||||||
|
return Err(LowerError { msg: "boolean operation with no children".into() });
|
||||||
|
}
|
||||||
|
|
||||||
|
match bop.op {
|
||||||
|
BooleanKind::Union => Ok(SdfNode::Union(children)),
|
||||||
|
BooleanKind::Intersection => Ok(SdfNode::Intersection(children)),
|
||||||
|
BooleanKind::Difference => {
|
||||||
|
let mut iter = children.into_iter();
|
||||||
|
let base = iter.next().unwrap();
|
||||||
|
let subtract: Vec<_> = iter.collect();
|
||||||
|
if subtract.is_empty() {
|
||||||
|
Ok(base)
|
||||||
|
} else {
|
||||||
|
Ok(SdfNode::Difference { base: Box::new(base), subtract })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_single_child(mut nodes: Vec<SdfNode>, op: &str) -> Result<SdfNode, LowerError> {
|
||||||
|
match nodes.len() {
|
||||||
|
0 => Err(LowerError { msg: format!("{op}() requires a child") }),
|
||||||
|
1 => Ok(nodes.remove(0)),
|
||||||
|
_ => Ok(SdfNode::Union(nodes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argument extraction helpers
|
||||||
|
|
||||||
|
fn eval_const_bool(expr: &Expr) -> Option<bool> {
|
||||||
|
match expr {
|
||||||
|
Expr::Bool(b) => Some(*b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_f64(args: &[Argument], name: &str, pos: usize, env: &Env) -> Result<Option<f64>, LowerError> {
|
||||||
|
for arg in args {
|
||||||
|
if arg.name.as_deref() == Some(name) {
|
||||||
|
return eval_expr_env(&arg.value, env)
|
||||||
|
.map(Some)
|
||||||
|
.ok_or_else(|| LowerError { msg: format!("argument '{name}' must be a number") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(arg) = args.get(pos) {
|
||||||
|
if arg.name.is_none() {
|
||||||
|
return Ok(eval_expr_env(&arg.value, env));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_named_bool(args: &[Argument], name: &str) -> Result<Option<bool>, LowerError> {
|
||||||
|
for arg in args {
|
||||||
|
if arg.name.as_deref() == Some(name) {
|
||||||
|
return eval_const_bool(&arg.value)
|
||||||
|
.map(Some)
|
||||||
|
.ok_or_else(|| LowerError { msg: format!("argument '{name}' must be a boolean") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_vec3(args: &[Argument], pos: usize, env: &Env) -> Result<[f64; 3], LowerError> {
|
||||||
|
try_get_vec3(args, pos, env)
|
||||||
|
.ok_or_else(|| LowerError { msg: format!("expected [x,y,z] vector at position {pos}") })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_get_vec3(args: &[Argument], pos: usize, env: &Env) -> Option<[f64; 3]> {
|
||||||
|
let arg = args.get(pos)?;
|
||||||
|
if arg.name.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Expr::Vector(elems) = &arg.value {
|
||||||
|
if elems.len() >= 3 {
|
||||||
|
let x = eval_expr_env(&elems[0], env)?;
|
||||||
|
let y = eval_expr_env(&elems[1], env)?;
|
||||||
|
let z = eval_expr_env(&elems[2], env)?;
|
||||||
|
return Some([x, y, z]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cube_size(args: &[Argument], env: &Env) -> Result<[f64; 3], LowerError> {
|
||||||
|
if let Some(arg) = args.first() {
|
||||||
|
if arg.name.is_none() || arg.name.as_deref() == Some("size") {
|
||||||
|
if let Expr::Vector(elems) = &arg.value {
|
||||||
|
if elems.len() >= 3 {
|
||||||
|
if let (Some(x), Some(y), Some(z)) = (
|
||||||
|
eval_expr_env(&elems[0], env),
|
||||||
|
eval_expr_env(&elems[1], env),
|
||||||
|
eval_expr_env(&elems[2], env),
|
||||||
|
) {
|
||||||
|
return Ok([x, y, z]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(s) = eval_expr_env(&arg.value, env) {
|
||||||
|
return Ok([s, s, s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok([1.0, 1.0, 1.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cord_parse::lexer::Lexer;
|
||||||
|
use cord_parse::parser::Parser;
|
||||||
|
|
||||||
|
fn parse_and_lower(src: &str) -> SdfNode {
|
||||||
|
let tokens = Lexer::new(src).tokenize().unwrap();
|
||||||
|
let program = Parser::new(tokens).parse_program().unwrap();
|
||||||
|
lower_program(&program).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_union_children(node: &SdfNode) -> usize {
|
||||||
|
match node {
|
||||||
|
SdfNode::Union(children) => children.len(),
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_sphere() {
|
||||||
|
let node = parse_and_lower("sphere(5);");
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 5.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_in_args() {
|
||||||
|
let node = parse_and_lower("r = 3; sphere(r);");
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 3.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_unrolls() {
|
||||||
|
// 4 iterations → Union of 4 translated spheres
|
||||||
|
let node = parse_and_lower(
|
||||||
|
"for (i = [0:3]) translate([i*10, 0, 0]) sphere(1);"
|
||||||
|
);
|
||||||
|
assert_eq!(count_union_children(&node), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_with_step() {
|
||||||
|
// [0 : 2 : 6] → values 0, 2, 4, 6 → 4 branches
|
||||||
|
let node = parse_and_lower(
|
||||||
|
"for (i = [0:2:6]) sphere(i);"
|
||||||
|
);
|
||||||
|
assert_eq!(count_union_children(&node), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_explicit_list() {
|
||||||
|
let node = parse_and_lower(
|
||||||
|
"for (x = [1, 5, 10]) translate([x, 0, 0]) sphere(1);"
|
||||||
|
);
|
||||||
|
assert_eq!(count_union_children(&node), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_constant_true() {
|
||||||
|
let node = parse_and_lower("if (true) sphere(1);");
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_constant_false_with_else() {
|
||||||
|
let node = parse_and_lower("if (false) sphere(1); else cube(2);");
|
||||||
|
assert!(matches!(node, SdfNode::Translate { .. })); // cube with center=false wraps in translate
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_constant_false_no_else() {
|
||||||
|
let src = "if (false) sphere(1);";
|
||||||
|
let tokens = Lexer::new(src).tokenize().unwrap();
|
||||||
|
let program = Parser::new(tokens).parse_program().unwrap();
|
||||||
|
let result = lower_program(&program);
|
||||||
|
assert!(result.is_err()); // no geometry produced → empty scene
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_condition_includes_both_branches() {
|
||||||
|
// `x` is unknown → both branches included
|
||||||
|
let src = "x = 1; if (x) sphere(1); else cube(2);";
|
||||||
|
let node = parse_and_lower(src);
|
||||||
|
// x=1 is known, so condition evaluates truthy → only sphere
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_for_loops() {
|
||||||
|
// 3 * 3 = 9 branches
|
||||||
|
let node = parse_and_lower(
|
||||||
|
"for (i = [0:2]) for (j = [0:2]) translate([i*10, j*10, 0]) sphere(1);"
|
||||||
|
);
|
||||||
|
// Outer union of 3, each containing inner union of 3
|
||||||
|
assert_eq!(count_union_children(&node), 3);
|
||||||
|
if let SdfNode::Union(outer) = &node {
|
||||||
|
for child in outer {
|
||||||
|
assert_eq!(count_union_children(child), 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ternary_in_expression() {
|
||||||
|
let node = parse_and_lower("x = 5; sphere(x > 3 ? 10 : 1);");
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 10.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expr_with_math_functions() {
|
||||||
|
let node = parse_and_lower("sphere(sqrt(4));");
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 2.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_with_variable_bounds() {
|
||||||
|
let node = parse_and_lower("n = 3; for (i = [0:n]) sphere(i);");
|
||||||
|
assert_eq!(count_union_children(&node), 4); // 0, 1, 2, 3
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn difference_op() {
|
||||||
|
let node = parse_and_lower("difference() { cube(10, center=true); sphere(5); }");
|
||||||
|
assert!(matches!(node, SdfNode::Difference { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
use crate::SdfNode;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
/// Convert an SdfNode tree to valid OpenSCAD source.
|
||||||
|
pub fn sdf_to_scad(node: &SdfNode) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
emit_scad(node, 0, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indent(depth: usize, out: &mut String) {
|
||||||
|
for _ in 0..depth {
|
||||||
|
out.push_str(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt(v: f64) -> String {
|
||||||
|
if v == v.round() && v.abs() < 1e9 {
|
||||||
|
format!("{}", v as i64)
|
||||||
|
} else {
|
||||||
|
let s = format!("{:.6}", v);
|
||||||
|
let s = s.trim_end_matches('0');
|
||||||
|
let s = s.trim_end_matches('.');
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_scad(node: &SdfNode, depth: usize, out: &mut String) {
|
||||||
|
match node {
|
||||||
|
SdfNode::Sphere { radius } => {
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "sphere(r={});", fmt(*radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Box { half_extents: h } => {
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(
|
||||||
|
out, "cube([{}, {}, {}], center=true);",
|
||||||
|
fmt(h[0] * 2.0), fmt(h[1] * 2.0), fmt(h[2] * 2.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Cylinder { radius, height } => {
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(
|
||||||
|
out, "cylinder(h={}, r={}, center=true);",
|
||||||
|
fmt(*height), fmt(*radius)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Translate { offset, child } => {
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(
|
||||||
|
out, "translate([{}, {}, {}])",
|
||||||
|
fmt(offset[0]), fmt(offset[1]), fmt(offset[2])
|
||||||
|
);
|
||||||
|
emit_scad(child, depth + 1, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Rotate { axis, angle_deg, child } => {
|
||||||
|
let rot = if axis[0].abs() > 0.9 {
|
||||||
|
format!("[{}, 0, 0]", fmt(*angle_deg))
|
||||||
|
} else if axis[1].abs() > 0.9 {
|
||||||
|
format!("[0, {}, 0]", fmt(*angle_deg))
|
||||||
|
} else {
|
||||||
|
format!("[0, 0, {}]", fmt(*angle_deg))
|
||||||
|
};
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "rotate({rot})");
|
||||||
|
emit_scad(child, depth + 1, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Scale { factor, child } => {
|
||||||
|
indent(depth, out);
|
||||||
|
if (factor[0] - factor[1]).abs() < 1e-10
|
||||||
|
&& (factor[1] - factor[2]).abs() < 1e-10
|
||||||
|
{
|
||||||
|
let _ = writeln!(out, "scale({})", fmt(factor[0]));
|
||||||
|
} else {
|
||||||
|
let _ = writeln!(
|
||||||
|
out, "scale([{}, {}, {}])",
|
||||||
|
fmt(factor[0]), fmt(factor[1]), fmt(factor[2])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
emit_scad(child, depth + 1, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Union(children) => {
|
||||||
|
if children.len() == 1 {
|
||||||
|
emit_scad(&children[0], depth, out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "union() {{");
|
||||||
|
for c in children {
|
||||||
|
emit_scad(c, depth + 1, out);
|
||||||
|
}
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Intersection(children) => {
|
||||||
|
if children.len() == 1 {
|
||||||
|
emit_scad(&children[0], depth, out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "intersection() {{");
|
||||||
|
for c in children {
|
||||||
|
emit_scad(c, depth + 1, out);
|
||||||
|
}
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Difference { base, subtract } => {
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "difference() {{");
|
||||||
|
emit_scad(base, depth + 1, out);
|
||||||
|
for s in subtract {
|
||||||
|
emit_scad(s, depth + 1, out);
|
||||||
|
}
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::SmoothUnion { children, k } => {
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "// smooth union (k={})", fmt(*k));
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "union() {{");
|
||||||
|
for c in children {
|
||||||
|
emit_scad(c, depth + 1, out);
|
||||||
|
}
|
||||||
|
indent(depth, out);
|
||||||
|
let _ = writeln!(out, "}}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sphere_roundtrip() {
|
||||||
|
let node = SdfNode::Sphere { radius: 5.0 };
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert_eq!(scad.trim(), "sphere(r=5);");
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
assert!(matches!(reparsed, SdfNode::Sphere { radius } if (radius - 5.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn box_roundtrip() {
|
||||||
|
let node = SdfNode::Box { half_extents: [1.0, 2.0, 3.0] };
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert!(scad.contains("cube([2, 4, 6], center=true);"));
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
if let SdfNode::Box { half_extents: h } = reparsed {
|
||||||
|
assert!((h[0] - 1.0).abs() < 1e-10);
|
||||||
|
assert!((h[1] - 2.0).abs() < 1e-10);
|
||||||
|
assert!((h[2] - 3.0).abs() < 1e-10);
|
||||||
|
} else {
|
||||||
|
panic!("expected Box, got {:?}", reparsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cylinder_roundtrip() {
|
||||||
|
let node = SdfNode::Cylinder { radius: 2.0, height: 6.0 };
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert!(scad.contains("cylinder(h=6, r=2, center=true);"));
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
if let SdfNode::Cylinder { radius, height } = reparsed {
|
||||||
|
assert!((radius - 2.0).abs() < 1e-10);
|
||||||
|
assert!((height - 6.0).abs() < 1e-10);
|
||||||
|
} else {
|
||||||
|
panic!("expected Cylinder, got {:?}", reparsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translate_sphere_roundtrip() {
|
||||||
|
let node = SdfNode::Translate {
|
||||||
|
offset: [1.0, 2.0, 3.0],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 1.0 }),
|
||||||
|
};
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert!(scad.contains("translate([1, 2, 3])"));
|
||||||
|
assert!(scad.contains("sphere(r=1);"));
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
if let SdfNode::Translate { offset, child } = reparsed {
|
||||||
|
assert!((offset[0] - 1.0).abs() < 1e-10);
|
||||||
|
assert!((offset[1] - 2.0).abs() < 1e-10);
|
||||||
|
assert!((offset[2] - 3.0).abs() < 1e-10);
|
||||||
|
assert!(matches!(*child, SdfNode::Sphere { .. }));
|
||||||
|
} else {
|
||||||
|
panic!("expected Translate, got {:?}", reparsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn difference_roundtrip() {
|
||||||
|
let node = SdfNode::Difference {
|
||||||
|
base: Box::new(SdfNode::Box { half_extents: [5.0, 5.0, 5.0] }),
|
||||||
|
subtract: vec![SdfNode::Sphere { radius: 4.0 }],
|
||||||
|
};
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert!(scad.contains("difference()"));
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
assert!(matches!(reparsed, SdfNode::Difference { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn union_roundtrip() {
|
||||||
|
let node = SdfNode::Union(vec![
|
||||||
|
SdfNode::Sphere { radius: 1.0 },
|
||||||
|
SdfNode::Sphere { radius: 2.0 },
|
||||||
|
]);
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert!(scad.contains("union()"));
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
assert!(matches!(reparsed, SdfNode::Union(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotate_z_roundtrip() {
|
||||||
|
let node = SdfNode::Rotate {
|
||||||
|
axis: [0.0, 0.0, 1.0],
|
||||||
|
angle_deg: 45.0,
|
||||||
|
child: Box::new(SdfNode::Box { half_extents: [1.0, 2.0, 1.0] }),
|
||||||
|
};
|
||||||
|
let scad = sdf_to_scad(&node);
|
||||||
|
assert!(scad.contains("rotate([0, 0, 45])"));
|
||||||
|
let reparsed = crate::lower::lower_program(
|
||||||
|
&cord_parse::parse(&scad).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
if let SdfNode::Rotate { angle_deg, axis, .. } = reparsed {
|
||||||
|
assert!((angle_deg - 45.0).abs() < 1e-10);
|
||||||
|
assert!(axis[2].abs() > 0.9);
|
||||||
|
} else {
|
||||||
|
panic!("expected Rotate, got {:?}", reparsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scad_file_roundtrip_eval() {
|
||||||
|
let scad = "difference() {\n cube([10, 10, 10], center=true);\n sphere(r=4);\n}\n";
|
||||||
|
let program = cord_parse::parse(scad).unwrap();
|
||||||
|
let sdf = crate::lower::lower_program(&program).unwrap();
|
||||||
|
let emitted = sdf_to_scad(&sdf);
|
||||||
|
let program2 = cord_parse::parse(&emitted).unwrap();
|
||||||
|
let sdf2 = crate::lower::lower_program(&program2).unwrap();
|
||||||
|
|
||||||
|
let g1 = crate::sdf_to_trig(&sdf);
|
||||||
|
let g2 = crate::sdf_to_trig(&sdf2);
|
||||||
|
|
||||||
|
let points = [
|
||||||
|
(0.0, 0.0, 0.0),
|
||||||
|
(3.0, 0.0, 0.0),
|
||||||
|
(5.0, 0.0, 0.0),
|
||||||
|
(0.0, 5.0, 0.0),
|
||||||
|
(0.0, 0.0, 5.0),
|
||||||
|
(6.0, 0.0, 0.0),
|
||||||
|
];
|
||||||
|
for (x, y, z) in points {
|
||||||
|
let v1 = cord_trig::eval::evaluate(&g1, x, y, z);
|
||||||
|
let v2 = cord_trig::eval::evaluate(&g2, x, y, z);
|
||||||
|
assert!(
|
||||||
|
(v1 - v2).abs() < 1e-6,
|
||||||
|
"divergence at ({x},{y},{z}): {v1} vs {v2}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
use crate::SdfNode;
|
||||||
|
|
||||||
|
pub fn simplify(node: &mut SdfNode) {
|
||||||
|
match node {
|
||||||
|
SdfNode::Union(children) | SdfNode::Intersection(children)
|
||||||
|
| SdfNode::SmoothUnion { children, .. } => {
|
||||||
|
for c in children.iter_mut() {
|
||||||
|
simplify(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SdfNode::Difference { base, subtract } => {
|
||||||
|
simplify(base);
|
||||||
|
for s in subtract.iter_mut() {
|
||||||
|
simplify(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SdfNode::Translate { child, .. } | SdfNode::Rotate { child, .. }
|
||||||
|
| SdfNode::Scale { child, .. } => {
|
||||||
|
simplify(child);
|
||||||
|
}
|
||||||
|
SdfNode::Sphere { .. } | SdfNode::Box { .. } | SdfNode::Cylinder { .. } => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let replacement = match node {
|
||||||
|
SdfNode::Union(children) => {
|
||||||
|
let mut flattened = Vec::with_capacity(children.len());
|
||||||
|
let mut changed = false;
|
||||||
|
for c in children.drain(..) {
|
||||||
|
if let SdfNode::Union(inner) = c {
|
||||||
|
flattened.extend(inner);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
flattened.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flattened.len() == 1 {
|
||||||
|
Some(flattened.into_iter().next().unwrap())
|
||||||
|
} else if changed {
|
||||||
|
Some(SdfNode::Union(flattened))
|
||||||
|
} else {
|
||||||
|
*children = flattened;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Intersection(children) => {
|
||||||
|
let mut flattened = Vec::with_capacity(children.len());
|
||||||
|
let mut changed = false;
|
||||||
|
for c in children.drain(..) {
|
||||||
|
if let SdfNode::Intersection(inner) = c {
|
||||||
|
flattened.extend(inner);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
flattened.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flattened.len() == 1 {
|
||||||
|
Some(flattened.into_iter().next().unwrap())
|
||||||
|
} else if changed {
|
||||||
|
Some(SdfNode::Intersection(flattened))
|
||||||
|
} else {
|
||||||
|
*children = flattened;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Translate { offset, child } => {
|
||||||
|
if offset[0] == 0.0 && offset[1] == 0.0 && offset[2] == 0.0 {
|
||||||
|
let inner = std::mem::replace(child.as_mut(), SdfNode::Sphere { radius: 0.0 });
|
||||||
|
Some(inner)
|
||||||
|
} else if let SdfNode::Translate { offset: o2, child: c2 } = child.as_mut() {
|
||||||
|
let combined = [offset[0] + o2[0], offset[1] + o2[1], offset[2] + o2[2]];
|
||||||
|
let inner = std::mem::replace(c2.as_mut(), SdfNode::Sphere { radius: 0.0 });
|
||||||
|
Some(SdfNode::Translate { offset: combined, child: Box::new(inner) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Scale { factor, child } => {
|
||||||
|
if factor[0] == 1.0 && factor[1] == 1.0 && factor[2] == 1.0 {
|
||||||
|
let inner = std::mem::replace(child.as_mut(), SdfNode::Sphere { radius: 0.0 });
|
||||||
|
Some(inner)
|
||||||
|
} else if let SdfNode::Scale { factor: f2, child: c2 } = child.as_mut() {
|
||||||
|
let combined = [factor[0] * f2[0], factor[1] * f2[1], factor[2] * f2[2]];
|
||||||
|
let inner = std::mem::replace(c2.as_mut(), SdfNode::Sphere { radius: 0.0 });
|
||||||
|
Some(SdfNode::Scale { factor: combined, child: Box::new(inner) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Rotate { angle_deg, child, .. } => {
|
||||||
|
if *angle_deg == 0.0 {
|
||||||
|
let inner = std::mem::replace(child.as_mut(), SdfNode::Sphere { radius: 0.0 });
|
||||||
|
Some(inner)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(r) = replacement {
|
||||||
|
*node = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flatten_nested_union() {
|
||||||
|
let mut node = SdfNode::Union(vec![
|
||||||
|
SdfNode::Union(vec![
|
||||||
|
SdfNode::Sphere { radius: 1.0 },
|
||||||
|
SdfNode::Sphere { radius: 2.0 },
|
||||||
|
]),
|
||||||
|
SdfNode::Sphere { radius: 3.0 },
|
||||||
|
]);
|
||||||
|
simplify(&mut node);
|
||||||
|
match &node {
|
||||||
|
SdfNode::Union(children) => assert_eq!(children.len(), 3),
|
||||||
|
_ => panic!("expected Union"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn singleton_union() {
|
||||||
|
let mut node = SdfNode::Union(vec![SdfNode::Sphere { radius: 1.0 }]);
|
||||||
|
simplify(&mut node);
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 1.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collapse_nested_translate() {
|
||||||
|
let mut node = SdfNode::Translate {
|
||||||
|
offset: [1.0, 2.0, 3.0],
|
||||||
|
child: Box::new(SdfNode::Translate {
|
||||||
|
offset: [4.0, 5.0, 6.0],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 1.0 }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
simplify(&mut node);
|
||||||
|
match &node {
|
||||||
|
SdfNode::Translate { offset, child } => {
|
||||||
|
assert!((offset[0] - 5.0).abs() < 1e-10);
|
||||||
|
assert!((offset[1] - 7.0).abs() < 1e-10);
|
||||||
|
assert!((offset[2] - 9.0).abs() < 1e-10);
|
||||||
|
assert!(matches!(**child, SdfNode::Sphere { .. }));
|
||||||
|
}
|
||||||
|
_ => panic!("expected Translate"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_translate_removed() {
|
||||||
|
let mut node = SdfNode::Translate {
|
||||||
|
offset: [0.0, 0.0, 0.0],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 5.0 }),
|
||||||
|
};
|
||||||
|
simplify(&mut node);
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 5.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_scale_removed() {
|
||||||
|
let mut node = SdfNode::Scale {
|
||||||
|
factor: [1.0, 1.0, 1.0],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 3.0 }),
|
||||||
|
};
|
||||||
|
simplify(&mut node);
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 3.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_rotate_removed() {
|
||||||
|
let mut node = SdfNode::Rotate {
|
||||||
|
axis: [1.0, 0.0, 0.0],
|
||||||
|
angle_deg: 0.0,
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 2.0 }),
|
||||||
|
};
|
||||||
|
simplify(&mut node);
|
||||||
|
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 2.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collapse_nested_scale() {
|
||||||
|
let mut node = SdfNode::Scale {
|
||||||
|
factor: [2.0, 3.0, 4.0],
|
||||||
|
child: Box::new(SdfNode::Scale {
|
||||||
|
factor: [0.5, 0.5, 0.5],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 1.0 }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
simplify(&mut node);
|
||||||
|
match &node {
|
||||||
|
SdfNode::Scale { factor, .. } => {
|
||||||
|
assert!((factor[0] - 1.0).abs() < 1e-10);
|
||||||
|
assert!((factor[1] - 1.5).abs() < 1e-10);
|
||||||
|
assert!((factor[2] - 2.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
_ => panic!("expected Scale"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/// An SDF scene is a tree of distance-function nodes.
|
||||||
|
/// Every node evaluates to a signed distance at a given point in 3-space.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SdfNode {
|
||||||
|
// Primitives — each is a closed-form distance function
|
||||||
|
Sphere { radius: f64 },
|
||||||
|
Box { half_extents: [f64; 3] },
|
||||||
|
Cylinder { radius: f64, height: f64 },
|
||||||
|
|
||||||
|
// Boolean operations — compose child distance fields
|
||||||
|
Union(Vec<SdfNode>),
|
||||||
|
Intersection(Vec<SdfNode>),
|
||||||
|
Difference { base: Box<SdfNode>, subtract: Vec<SdfNode> },
|
||||||
|
SmoothUnion { children: Vec<SdfNode>, k: f64 },
|
||||||
|
|
||||||
|
// Affine transforms — modify the evaluation point
|
||||||
|
Translate { offset: [f64; 3], child: Box<SdfNode> },
|
||||||
|
Rotate { axis: [f64; 3], angle_deg: f64, child: Box<SdfNode> },
|
||||||
|
Scale { factor: [f64; 3], child: Box<SdfNode> },
|
||||||
|
|
||||||
|
// Linear extrude a 2D profile (future)
|
||||||
|
// Extrude { height: f64, child: Box<SdfNode> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SdfNode {
|
||||||
|
/// Bounding radius estimate for the raymarcher's far plane.
|
||||||
|
pub fn bounding_radius(&self) -> f64 {
|
||||||
|
match self {
|
||||||
|
SdfNode::Sphere { radius } => *radius,
|
||||||
|
SdfNode::Box { half_extents: h } => (h[0] * h[0] + h[1] * h[1] + h[2] * h[2]).sqrt(),
|
||||||
|
SdfNode::Cylinder { radius, height } => (radius * radius + (height / 2.0).powi(2)).sqrt(),
|
||||||
|
SdfNode::Union(children) | SdfNode::Intersection(children) => {
|
||||||
|
children.iter().map(|c| c.bounding_radius()).fold(0.0f64, f64::max)
|
||||||
|
}
|
||||||
|
SdfNode::Difference { base, .. } => base.bounding_radius(),
|
||||||
|
SdfNode::SmoothUnion { children, .. } => {
|
||||||
|
children.iter().map(|c| c.bounding_radius()).fold(0.0f64, f64::max)
|
||||||
|
}
|
||||||
|
SdfNode::Translate { offset, child } => {
|
||||||
|
let off_len = (offset[0].powi(2) + offset[1].powi(2) + offset[2].powi(2)).sqrt();
|
||||||
|
child.bounding_radius() + off_len
|
||||||
|
}
|
||||||
|
SdfNode::Rotate { child, .. } => child.bounding_radius(),
|
||||||
|
SdfNode::Scale { factor, child } => {
|
||||||
|
let max_scale = factor[0].abs().max(factor[1].abs()).max(factor[2].abs());
|
||||||
|
child.bounding_radius() * max_scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
use cord_trig::ir::NodeId;
|
||||||
|
use cord_trig::lower::{SdfBuilder, TrigPoint3};
|
||||||
|
use cord_trig::TrigGraph;
|
||||||
|
use crate::SdfNode;
|
||||||
|
|
||||||
|
/// Convert an SdfNode tree into a TrigGraph.
|
||||||
|
///
|
||||||
|
/// Walks the SDF tree recursively, using SdfBuilder to decompose
|
||||||
|
/// each primitive/transform/boolean into trig IR nodes.
|
||||||
|
pub fn sdf_to_trig(node: &SdfNode) -> TrigGraph {
|
||||||
|
let mut b = SdfBuilder::new();
|
||||||
|
let p = b.root_point();
|
||||||
|
let output = lower_node(&mut b, p, node);
|
||||||
|
b.finish(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_node(b: &mut SdfBuilder, point: TrigPoint3, node: &SdfNode) -> NodeId {
|
||||||
|
match node {
|
||||||
|
SdfNode::Sphere { radius } => b.sphere(point, *radius),
|
||||||
|
SdfNode::Box { half_extents } => b.box_sdf(point, *half_extents),
|
||||||
|
SdfNode::Cylinder { radius, height } => b.cylinder(point, *radius, *height),
|
||||||
|
|
||||||
|
SdfNode::Union(children) => {
|
||||||
|
let mut result = lower_node(b, point, &children[0]);
|
||||||
|
for child in &children[1..] {
|
||||||
|
let c = lower_node(b, point, child);
|
||||||
|
result = b.union(result, c);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Intersection(children) => {
|
||||||
|
let mut result = lower_node(b, point, &children[0]);
|
||||||
|
for child in &children[1..] {
|
||||||
|
let c = lower_node(b, point, child);
|
||||||
|
result = b.intersection(result, c);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Difference { base, subtract } => {
|
||||||
|
let mut result = lower_node(b, point, base);
|
||||||
|
for sub in subtract {
|
||||||
|
let s = lower_node(b, point, sub);
|
||||||
|
result = b.difference(result, s);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::SmoothUnion { children, k } => {
|
||||||
|
let mut result = lower_node(b, point, &children[0]);
|
||||||
|
for child in &children[1..] {
|
||||||
|
let c = lower_node(b, point, child);
|
||||||
|
result = b.smooth_union(result, c, *k);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Translate { offset, child } => {
|
||||||
|
let p2 = b.translate(point, *offset);
|
||||||
|
lower_node(b, p2, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Rotate { axis, angle_deg, child } => {
|
||||||
|
let angle_rad = angle_deg.to_radians();
|
||||||
|
// SdfNode::Rotate stores axis-aligned rotations from the SCAD lowerer.
|
||||||
|
// Decompose by dominant axis.
|
||||||
|
let p2 = if axis[0].abs() > 0.9 {
|
||||||
|
b.rotate_x(point, angle_rad * axis[0].signum())
|
||||||
|
} else if axis[1].abs() > 0.9 {
|
||||||
|
b.rotate_y(point, angle_rad * axis[1].signum())
|
||||||
|
} else {
|
||||||
|
b.rotate_z(point, angle_rad * axis[2].signum())
|
||||||
|
};
|
||||||
|
lower_node(b, p2, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
SdfNode::Scale { factor, child } => {
|
||||||
|
let (p2, min_scale) = b.scale(point, *factor);
|
||||||
|
let dist = lower_node(b, p2, child);
|
||||||
|
b.scale_distance(dist, min_scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-shader"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "WGSL shader codegen from TrigGraph IR for GPU raymarching"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["wgsl", "shader", "sdf", "raymarching", "wgpu"]
|
||||||
|
categories = ["graphics", "rendering"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
use cord_trig::{NodeId, TrigGraph, TrigOp};
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
/// Generate a complete WGSL raymarcher directly from a TrigGraph.
|
||||||
|
pub fn generate_wgsl_from_trig(graph: &TrigGraph) -> String {
|
||||||
|
let mut out = String::with_capacity(4096);
|
||||||
|
write_preamble(&mut out);
|
||||||
|
write_sdf_from_trig(&mut out, graph);
|
||||||
|
write_raymarcher(&mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var_name(id: NodeId) -> String {
|
||||||
|
format!("v{id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_sdf_from_trig(out: &mut String, graph: &TrigGraph) {
|
||||||
|
out.push_str("fn scene_sdf(p: vec3<f32>) -> f32 {\n");
|
||||||
|
|
||||||
|
for (i, op) in graph.nodes.iter().enumerate() {
|
||||||
|
let v = var_name(i as NodeId);
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX => writeln!(out, " let {v} = p.x;").unwrap(),
|
||||||
|
TrigOp::InputY => writeln!(out, " let {v} = p.y;").unwrap(),
|
||||||
|
TrigOp::InputZ => writeln!(out, " let {v} = p.z;").unwrap(),
|
||||||
|
TrigOp::Const(c) => {
|
||||||
|
let f = *c as f32;
|
||||||
|
if f.is_nan() || f.is_infinite() {
|
||||||
|
writeln!(out, " let {v} = 0.0;").unwrap()
|
||||||
|
} else {
|
||||||
|
writeln!(out, " let {v} = {f:.8};").unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrigOp::Add(a, b) => writeln!(out, " let {v} = {} + {};", var_name(*a), var_name(*b)).unwrap(),
|
||||||
|
TrigOp::Sub(a, b) => writeln!(out, " let {v} = {} - {};", var_name(*a), var_name(*b)).unwrap(),
|
||||||
|
TrigOp::Mul(a, b) => writeln!(out, " let {v} = {} * {};", var_name(*a), var_name(*b)).unwrap(),
|
||||||
|
TrigOp::Div(a, b) => writeln!(out, " let {v} = {} / {};", var_name(*a), var_name(*b)).unwrap(),
|
||||||
|
TrigOp::Neg(a) => writeln!(out, " let {v} = -{};", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Abs(a) => writeln!(out, " let {v} = abs({});", var_name(*a)).unwrap(),
|
||||||
|
|
||||||
|
TrigOp::Sin(a) => writeln!(out, " let {v} = sin({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Cos(a) => writeln!(out, " let {v} = cos({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Tan(a) => writeln!(out, " let {v} = tan({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Asin(a) => writeln!(out, " let {v} = asin({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Acos(a) => writeln!(out, " let {v} = acos({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Atan(a) => writeln!(out, " let {v} = atan({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Sinh(a) => writeln!(out, " let {v} = sinh({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Cosh(a) => writeln!(out, " let {v} = cosh({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Tanh(a) => writeln!(out, " let {v} = tanh({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Asinh(a) => writeln!(out, " let {v} = asinh({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Acosh(a) => writeln!(out, " let {v} = acosh({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Atanh(a) => writeln!(out, " let {v} = atanh({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Sqrt(a) => writeln!(out, " let {v} = sqrt({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Exp(a) => writeln!(out, " let {v} = exp({});", var_name(*a)).unwrap(),
|
||||||
|
TrigOp::Ln(a) => writeln!(out, " let {v} = log({});", var_name(*a)).unwrap(),
|
||||||
|
|
||||||
|
TrigOp::Hypot(a, b) => {
|
||||||
|
writeln!(out, " let {v} = sqrt({a_v} * {a_v} + {b_v} * {b_v});",
|
||||||
|
a_v = var_name(*a), b_v = var_name(*b)).unwrap()
|
||||||
|
}
|
||||||
|
TrigOp::Atan2(a, b) => {
|
||||||
|
writeln!(out, " let {v} = atan2({}, {});", var_name(*a), var_name(*b)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
TrigOp::Min(a, b) => writeln!(out, " let {v} = min({}, {});", var_name(*a), var_name(*b)).unwrap(),
|
||||||
|
TrigOp::Max(a, b) => writeln!(out, " let {v} = max({}, {});", var_name(*a), var_name(*b)).unwrap(),
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
writeln!(out, " let {v} = clamp({}, {}, {});",
|
||||||
|
var_name(*val), var_name(*lo), var_name(*hi)).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out, " return {};", var_name(graph.output)).unwrap();
|
||||||
|
out.push_str("}\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_preamble(out: &mut String) {
|
||||||
|
out.push_str(
|
||||||
|
r#"struct Uniforms {
|
||||||
|
resolution: vec2<f32>,
|
||||||
|
viewport_offset: vec2<f32>,
|
||||||
|
camera_pos: vec3<f32>,
|
||||||
|
time: f32,
|
||||||
|
camera_target: vec3<f32>,
|
||||||
|
fov: f32,
|
||||||
|
render_flags: vec4<f32>,
|
||||||
|
scene_scale: f32,
|
||||||
|
_pad: vec3<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
|
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_raymarcher(out: &mut String) {
|
||||||
|
out.push_str(
|
||||||
|
r#"fn calc_normal(p: vec3<f32>) -> vec3<f32> {
|
||||||
|
let e = vec2<f32>(0.0005 * u.scene_scale, -0.0005 * u.scene_scale);
|
||||||
|
return normalize(
|
||||||
|
e.xyy * scene_sdf(p + e.xyy) +
|
||||||
|
e.yyx * scene_sdf(p + e.yyx) +
|
||||||
|
e.yxy * scene_sdf(p + e.yxy) +
|
||||||
|
e.xxx * scene_sdf(p + e.xxx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn soft_shadow(ro: vec3<f32>, rd: vec3<f32>, mint: f32, maxt: f32, k: f32) -> f32 {
|
||||||
|
let eps = 0.0002 * u.scene_scale;
|
||||||
|
let step_lo = 0.001 * u.scene_scale;
|
||||||
|
let step_hi = 0.5 * u.scene_scale;
|
||||||
|
var res = 1.0;
|
||||||
|
var t = mint;
|
||||||
|
var prev_d = 1e10;
|
||||||
|
for (var i = 0; i < 64; i++) {
|
||||||
|
let d = scene_sdf(ro + rd * t);
|
||||||
|
if d < eps { return 0.0; }
|
||||||
|
let y = d * d / (2.0 * prev_d);
|
||||||
|
let x = sqrt(max(d * d - y * y, 0.0));
|
||||||
|
res = min(res, k * x / max(t - y, 0.0001));
|
||||||
|
prev_d = d;
|
||||||
|
t += clamp(d, step_lo, step_hi);
|
||||||
|
if t > maxt { break; }
|
||||||
|
}
|
||||||
|
return clamp(res, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ao(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
||||||
|
let s = u.scene_scale;
|
||||||
|
var occ = 0.0;
|
||||||
|
var w = 1.0;
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
let h = (0.01 + 0.12 * f32(i)) * s;
|
||||||
|
let d = scene_sdf(p + n * h);
|
||||||
|
occ += (h - d) * w;
|
||||||
|
w *= 0.7;
|
||||||
|
}
|
||||||
|
return clamp(1.0 - 1.5 * occ / s, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid_aa(x: f32, line_w: f32) -> f32 {
|
||||||
|
let d = abs(fract(x) - 0.5);
|
||||||
|
let fw = fwidth(x);
|
||||||
|
return smoothstep(0.0, max(fw * 1.5, 0.001), d - line_w);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ground_plane(ro: vec3<f32>, rd: vec3<f32>) -> vec4<f32> {
|
||||||
|
if rd.z >= 0.0 { return vec4<f32>(0.0); }
|
||||||
|
let t = -ro.z / rd.z;
|
||||||
|
let max_ground = u.scene_scale * 50.0;
|
||||||
|
if t < 0.0 || t > max_ground { return vec4<f32>(0.0); }
|
||||||
|
let p = ro + rd * t;
|
||||||
|
let gs = max(u.scene_scale * 0.5, 1.0);
|
||||||
|
let gp = p.xy / gs;
|
||||||
|
|
||||||
|
// Minor grid (every cell)
|
||||||
|
let minor = grid_aa(gp.x, 0.02) * grid_aa(gp.y, 0.02);
|
||||||
|
// Major grid (every 5 cells)
|
||||||
|
let major = grid_aa(gp.x / 5.0, 0.04) * grid_aa(gp.y / 5.0, 0.04);
|
||||||
|
|
||||||
|
// Axis lines at world origin
|
||||||
|
let aw = gs * 0.08;
|
||||||
|
let afw_x = fwidth(p.x);
|
||||||
|
let afw_y = fwidth(p.y);
|
||||||
|
let ax = 1.0 - smoothstep(aw, aw + max(afw_y * 1.5, 0.001), abs(p.y));
|
||||||
|
let ay = 1.0 - smoothstep(aw, aw + max(afw_x * 1.5, 0.001), abs(p.x));
|
||||||
|
|
||||||
|
let fade_k = 0.3 / (u.scene_scale * u.scene_scale);
|
||||||
|
let fade = exp(-fade_k * t * t);
|
||||||
|
|
||||||
|
let base = vec3<f32>(0.22, 0.24, 0.28);
|
||||||
|
var col = mix(vec3<f32>(0.30, 0.32, 0.36), base, minor);
|
||||||
|
col = mix(vec3<f32>(0.38, 0.40, 0.44), col, major);
|
||||||
|
col = mix(vec3<f32>(0.55, 0.18, 0.18), col, ax * fade);
|
||||||
|
col = mix(vec3<f32>(0.18, 0.45, 0.18), col, ay * fade);
|
||||||
|
|
||||||
|
let sky_horizon = vec3<f32>(0.25, 0.35, 0.50);
|
||||||
|
col = mix(sky_horizon, col, fade);
|
||||||
|
|
||||||
|
let shad_max = u.scene_scale * 10.0;
|
||||||
|
let shad = soft_shadow(vec3<f32>(p.x, p.y, 0.001 * u.scene_scale), normalize(vec3<f32>(0.5, 0.8, 1.0)), 0.1 * u.scene_scale, shad_max, 8.0);
|
||||||
|
let shad_faded = mix(1.0, 0.5 + 0.5 * shad, fade);
|
||||||
|
return vec4<f32>(col * shad_faded, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_camera_ray(uv: vec2<f32>) -> vec3<f32> {
|
||||||
|
let forward = normalize(u.camera_target - u.camera_pos);
|
||||||
|
let right = normalize(cross(forward, vec3<f32>(0.0, 0.0, 1.0)));
|
||||||
|
let up = cross(right, forward);
|
||||||
|
return normalize(forward * u.fov + right * uv.x - up * uv.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shade_ray(ro: vec3<f32>, rd: vec3<f32>) -> vec3<f32> {
|
||||||
|
let hit_eps = 0.0005 * u.scene_scale;
|
||||||
|
let max_t = u.scene_scale * 20.0;
|
||||||
|
var t = 0.0;
|
||||||
|
var min_d = 1e10;
|
||||||
|
var t_min = 0.0;
|
||||||
|
var hit = false;
|
||||||
|
for (var i = 0; i < 128; i++) {
|
||||||
|
let p = ro + rd * t;
|
||||||
|
let d = scene_sdf(p);
|
||||||
|
if d < min_d {
|
||||||
|
min_d = d;
|
||||||
|
t_min = t;
|
||||||
|
}
|
||||||
|
if d < hit_eps {
|
||||||
|
hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
t += d;
|
||||||
|
if t > max_t { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var bg = vec3<f32>(0.0);
|
||||||
|
let gp = ground_plane(ro, rd);
|
||||||
|
if gp.w > 0.0 && u.render_flags.z > 0.5 {
|
||||||
|
bg = gp.xyz;
|
||||||
|
} else {
|
||||||
|
bg = mix(vec3<f32>(0.25, 0.35, 0.50), vec3<f32>(0.05, 0.12, 0.35), clamp(rd.z * 1.5, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
bg = pow(bg, vec3<f32>(0.4545));
|
||||||
|
|
||||||
|
// SDF-derived coverage — analytical AA from the distance field
|
||||||
|
let pix = max(t_min, 0.001) / u.resolution.y;
|
||||||
|
var coverage: f32;
|
||||||
|
if hit {
|
||||||
|
coverage = 1.0;
|
||||||
|
} else {
|
||||||
|
coverage = 1.0 - smoothstep(0.0, pix * 2.0, min_d);
|
||||||
|
}
|
||||||
|
if coverage < 0.005 { return bg; }
|
||||||
|
|
||||||
|
let shade_t = select(t_min, t, hit);
|
||||||
|
let p = ro + rd * shade_t;
|
||||||
|
let n = calc_normal(p);
|
||||||
|
|
||||||
|
let light_dir = normalize(vec3<f32>(0.5, 0.8, 1.0));
|
||||||
|
let diff = max(dot(n, light_dir), 0.0);
|
||||||
|
|
||||||
|
let shad = mix(1.0, soft_shadow(p + n * 0.002 * u.scene_scale, light_dir, 0.02 * u.scene_scale, 15.0 * u.scene_scale, 8.0), u.render_flags.x);
|
||||||
|
let occ = mix(1.0, ao(p, n), u.render_flags.y);
|
||||||
|
|
||||||
|
let half_v = normalize(light_dir - rd);
|
||||||
|
let spec = pow(max(dot(n, half_v), 0.0), 48.0) * 0.5;
|
||||||
|
|
||||||
|
let sky_light = clamp(0.5 + 0.5 * n.z, 0.0, 1.0);
|
||||||
|
let bounce = clamp(0.5 - 0.5 * n.z, 0.0, 1.0);
|
||||||
|
|
||||||
|
let base = vec3<f32>(0.65, 0.67, 0.72);
|
||||||
|
var lin = vec3<f32>(0.0);
|
||||||
|
lin += diff * shad * vec3<f32>(1.0, 0.97, 0.9) * 1.5;
|
||||||
|
lin += sky_light * occ * vec3<f32>(0.30, 0.40, 0.60) * 0.7;
|
||||||
|
lin += bounce * occ * vec3<f32>(0.15, 0.12, 0.1) * 0.5;
|
||||||
|
var color = base * lin + spec * shad;
|
||||||
|
|
||||||
|
color = color / (color + vec3<f32>(1.0));
|
||||||
|
color = pow(color, vec3<f32>(0.4545));
|
||||||
|
|
||||||
|
return mix(bg, color, coverage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
|
||||||
|
let ro = u.camera_pos;
|
||||||
|
let px = 1.0 / u.resolution.y;
|
||||||
|
let uv = (frag_coord.xy - u.viewport_offset - u.resolution * 0.5) * px;
|
||||||
|
let rd = get_camera_ray(uv);
|
||||||
|
return vec4<f32>(shade_ray(ro, rd), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VsOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOutput {
|
||||||
|
var pos = array<vec2<f32>, 3>(
|
||||||
|
vec2<f32>(-1.0, -1.0),
|
||||||
|
vec2<f32>(3.0, -1.0),
|
||||||
|
vec2<f32>(-1.0, 3.0),
|
||||||
|
);
|
||||||
|
var out: VsOutput;
|
||||||
|
out.position = vec4<f32>(pos[idx], 0.0, 1.0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
//! WGSL shader generation from [`cord_trig::TrigGraph`].
|
||||||
|
//!
|
||||||
|
//! Produces a complete WGSL raymarcher: uniforms, SDF function,
|
||||||
|
//! normal estimation, soft shadows, ambient occlusion, ground plane,
|
||||||
|
//! and tone-mapped Blinn-Phong shading.
|
||||||
|
|
||||||
|
pub mod codegen_trig;
|
||||||
|
|
||||||
|
pub use codegen_trig::generate_wgsl_from_trig;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-sparse"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Sparse grid interpolation in fixed-point arithmetic for CORDIC pipelines"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["interpolation", "sparse-grid", "fixed-point", "cordic"]
|
||||||
|
categories = ["mathematics", "no-std"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "sparse_interp"
|
||||||
|
harness = false
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||||
|
use cord_sparse::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn product_fn(fp: &FixedPoint) -> impl Fn(&[i64]) -> i64 + '_ {
|
||||||
|
move |pt: &[i64]| {
|
||||||
|
let mut result = fp.one();
|
||||||
|
for &x in pt {
|
||||||
|
result = fp.mul(result, x);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_solve(c: &mut Criterion) {
|
||||||
|
let fp = FixedPoint::new(32);
|
||||||
|
let basis = MonomialBasis;
|
||||||
|
let pts = GoldenPoints;
|
||||||
|
|
||||||
|
let configs: Vec<(usize, usize)> = vec![
|
||||||
|
(2, 25),
|
||||||
|
(2, 75),
|
||||||
|
(2, 216),
|
||||||
|
(4, 10),
|
||||||
|
(4, 25),
|
||||||
|
(4, 56),
|
||||||
|
(8, 5),
|
||||||
|
(8, 8),
|
||||||
|
(8, 14),
|
||||||
|
(16, 4),
|
||||||
|
(16, 7),
|
||||||
|
(32, 3),
|
||||||
|
(32, 5),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut group = c.benchmark_group("solve");
|
||||||
|
group.warm_up_time(Duration::from_millis(500));
|
||||||
|
group.measurement_time(Duration::from_secs(3));
|
||||||
|
|
||||||
|
for &(d, bound) in &configs {
|
||||||
|
let n = cord_sparse::index::binom(bound + d, d);
|
||||||
|
if n > 500_000 {
|
||||||
|
group.sample_size(10);
|
||||||
|
} else {
|
||||||
|
group.sample_size(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
let iter = BoundedSumIter::new(d, bound);
|
||||||
|
let f = product_fn(&fp);
|
||||||
|
let rhs = evaluate_function(&iter, &f, &pts, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
op.solve(rhs.clone());
|
||||||
|
|
||||||
|
group.bench_with_input(
|
||||||
|
BenchmarkId::new(format!("d{d}"), format!("b{bound}_n{n}")),
|
||||||
|
&(d, bound),
|
||||||
|
|b, &(d, bound)| {
|
||||||
|
let iter = BoundedSumIter::new(d, bound);
|
||||||
|
let f = product_fn(&fp);
|
||||||
|
let rhs = evaluate_function(&iter, &f, &pts, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
b.iter(|| {
|
||||||
|
op.solve(rhs.clone())
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_apply(c: &mut Criterion) {
|
||||||
|
let fp = FixedPoint::new(32);
|
||||||
|
let basis = MonomialBasis;
|
||||||
|
let pts = GoldenPoints;
|
||||||
|
|
||||||
|
let configs: Vec<(usize, usize)> = vec![
|
||||||
|
(2, 75),
|
||||||
|
(4, 25),
|
||||||
|
(8, 8),
|
||||||
|
(16, 5),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut group = c.benchmark_group("apply");
|
||||||
|
group.warm_up_time(Duration::from_millis(500));
|
||||||
|
group.measurement_time(Duration::from_secs(3));
|
||||||
|
|
||||||
|
for &(d, bound) in &configs {
|
||||||
|
let n = cord_sparse::index::binom(bound + d, d);
|
||||||
|
if n > 100_000 {
|
||||||
|
group.sample_size(10);
|
||||||
|
} else {
|
||||||
|
group.sample_size(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
let iter = BoundedSumIter::new(d, bound);
|
||||||
|
let f = product_fn(&fp);
|
||||||
|
let rhs = evaluate_function(&iter, &f, &pts, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
let coeffs = op.solve(rhs);
|
||||||
|
|
||||||
|
group.bench_with_input(
|
||||||
|
BenchmarkId::new(format!("d{d}"), format!("b{bound}_n{n}")),
|
||||||
|
&coeffs,
|
||||||
|
|b, coeffs| {
|
||||||
|
b.iter(|| {
|
||||||
|
op.apply(coeffs.clone())
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_solve, bench_apply);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
use cord_sparse::*;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
fn product_fn(fp: &FixedPoint) -> impl Fn(&[i64]) -> i64 + '_ {
|
||||||
|
move |pt: &[i64]| {
|
||||||
|
let mut result = fp.one();
|
||||||
|
for &x in pt {
|
||||||
|
result = fp.mul(result, x);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_runtime<F: FnMut() -> f64>(mut f: F, k: usize) -> f64 {
|
||||||
|
let n = 2 * k + 1;
|
||||||
|
let mut results: Vec<f64> = (0..n).map(|_| f()).collect();
|
||||||
|
results.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
results[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let fp = FixedPoint::new(32);
|
||||||
|
let basis = MonomialBasis;
|
||||||
|
let pts = GoldenPoints;
|
||||||
|
|
||||||
|
let dimensions = [2usize, 4, 8, 16, 32, 64];
|
||||||
|
let max_num_points: usize = 100_000_000;
|
||||||
|
let min_quotient = 2.0;
|
||||||
|
let max_runtime = 10.0;
|
||||||
|
let epsilon = 0.01;
|
||||||
|
let k = 2;
|
||||||
|
|
||||||
|
// warm-up
|
||||||
|
eprintln!("Warm-up");
|
||||||
|
{
|
||||||
|
let iter = BoundedSumIter::new(5, 60);
|
||||||
|
let f = product_fn(&fp);
|
||||||
|
let rhs = evaluate_function(&iter, &f, &pts, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
let _ = op.solve(rhs);
|
||||||
|
}
|
||||||
|
eprintln!("Warm-up finished");
|
||||||
|
|
||||||
|
let mut csv = String::new();
|
||||||
|
|
||||||
|
for &d in &dimensions {
|
||||||
|
eprintln!("Dimension: {d}");
|
||||||
|
let mut last_num_points = 0usize;
|
||||||
|
let mut last_runtime = 0.0f64;
|
||||||
|
|
||||||
|
for bound in 1.. {
|
||||||
|
let num_points = index::binom(bound + d, d);
|
||||||
|
if num_points > max_num_points {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if num_points as f64 / (last_num_points as f64 + epsilon) * last_runtime > max_runtime {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if num_points < (last_num_points as f64 * min_quotient) as usize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_num_points = num_points;
|
||||||
|
eprintln!(" Bound: {bound}, points: {num_points}");
|
||||||
|
|
||||||
|
let runtime = measure_runtime(
|
||||||
|
|| {
|
||||||
|
let iter = BoundedSumIter::new(d, bound);
|
||||||
|
let f = product_fn(&fp);
|
||||||
|
let rhs = evaluate_function(&iter, &f, &pts, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let _ = op.solve(rhs);
|
||||||
|
let elapsed = start.elapsed().as_secs_f64();
|
||||||
|
eprintln!(" Time for solve(): {elapsed:.6} s");
|
||||||
|
elapsed
|
||||||
|
},
|
||||||
|
k,
|
||||||
|
);
|
||||||
|
|
||||||
|
csv.push_str(&format!("{d}, {bound}, {num_points}, {runtime}\n"));
|
||||||
|
last_runtime = runtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{csv}");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/// Fixed-point arithmetic with configurable fractional bits.
|
||||||
|
///
|
||||||
|
/// All operations use i64 internally with i128 intermediates
|
||||||
|
/// for multiply to avoid overflow. Shift-and-add only.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct FixedPoint {
|
||||||
|
pub frac_bits: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedPoint {
|
||||||
|
pub fn new(frac_bits: u8) -> Self {
|
||||||
|
Self { frac_bits }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn one(&self) -> i64 {
|
||||||
|
1i64 << self.frac_bits
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn from_f64(&self, val: f64) -> i64 {
|
||||||
|
(val * (1i64 << self.frac_bits) as f64).round() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn to_f64(&self, val: i64) -> f64 {
|
||||||
|
val as f64 / (1i64 << self.frac_bits) as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mul(&self, a: i64, b: i64) -> i64 {
|
||||||
|
((a as i128 * b as i128) >> self.frac_bits) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn div(&self, a: i64, b: i64) -> i64 {
|
||||||
|
if b == 0 {
|
||||||
|
return if a >= 0 { i64::MAX } else { i64::MIN };
|
||||||
|
}
|
||||||
|
(((a as i128) << self.frac_bits) / b as i128) as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mul_basic() {
|
||||||
|
let fp = FixedPoint::new(16);
|
||||||
|
let a = fp.from_f64(2.5);
|
||||||
|
let b = fp.from_f64(4.0);
|
||||||
|
let result = fp.to_f64(fp.mul(a, b));
|
||||||
|
assert!((result - 10.0).abs() < 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn div_basic() {
|
||||||
|
let fp = FixedPoint::new(16);
|
||||||
|
let a = fp.from_f64(10.0);
|
||||||
|
let b = fp.from_f64(4.0);
|
||||||
|
let result = fp.to_f64(fp.div(a, b));
|
||||||
|
assert!((result - 2.5).abs() < 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/// Iterator over the downward-closed multi-index set
|
||||||
|
/// {(i_1, ..., i_d) | i_k >= 0, i_1 + ... + i_d <= bound}.
|
||||||
|
///
|
||||||
|
/// Does not iterate over the last dimension explicitly; instead
|
||||||
|
/// reports how many values the last dimension can take at each
|
||||||
|
/// position. This is the key to the unidirectional principle:
|
||||||
|
/// multiply along one dimension at a time, cycling indices.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BoundedSumIter {
|
||||||
|
d: usize,
|
||||||
|
bound: usize,
|
||||||
|
head: Vec<usize>,
|
||||||
|
head_sum: usize,
|
||||||
|
valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundedSumIter {
|
||||||
|
pub fn new(d: usize, bound: usize) -> Self {
|
||||||
|
assert!(d >= 1);
|
||||||
|
Self {
|
||||||
|
d,
|
||||||
|
bound,
|
||||||
|
head: vec![0; d.saturating_sub(1)],
|
||||||
|
head_sum: 0,
|
||||||
|
valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_dim_count(&self) -> usize {
|
||||||
|
self.bound - self.head_sum + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
if self.d == 1 {
|
||||||
|
self.valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tail = self.d - 2;
|
||||||
|
if self.bound > self.head_sum {
|
||||||
|
self.head_sum += 1;
|
||||||
|
self.head[tail] += 1;
|
||||||
|
} else {
|
||||||
|
let mut dim = tail as isize;
|
||||||
|
while dim >= 0 && self.head[dim as usize] == 0 {
|
||||||
|
dim -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if dim > 0 {
|
||||||
|
let d = dim as usize;
|
||||||
|
self.head_sum -= self.head[d] - 1;
|
||||||
|
self.head[d] = 0;
|
||||||
|
self.head[d - 1] += 1;
|
||||||
|
} else if dim == 0 {
|
||||||
|
self.head[0] = 0;
|
||||||
|
self.head_sum = 0;
|
||||||
|
self.valid = false;
|
||||||
|
} else {
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.head.fill(0);
|
||||||
|
self.head_sum = 0;
|
||||||
|
self.valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_index(&self) -> usize {
|
||||||
|
if self.head.is_empty() { 0 } else { self.head[0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_at(&self, dim: usize) -> usize {
|
||||||
|
self.head[dim]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim(&self) -> usize {
|
||||||
|
self.d
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_index_bound(&self) -> usize {
|
||||||
|
self.bound + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_bounds(&self) -> Vec<usize> {
|
||||||
|
vec![self.bound + 1; self.d]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_values(&self) -> usize {
|
||||||
|
binom(self.bound + self.d, self.d)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_values_per_first_index(&self) -> Vec<usize> {
|
||||||
|
(0..=self.bound)
|
||||||
|
.map(|i| binom((self.bound - i) + (self.d - 1), self.d - 1))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn go_to_end(&mut self) {
|
||||||
|
self.head.fill(0);
|
||||||
|
self.head_sum = 0;
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycle: last dimension moves to front. For a bounded-sum set
|
||||||
|
/// the constraint is symmetric, so the iterator is identical.
|
||||||
|
pub fn cycle(&self) -> Self {
|
||||||
|
let mut c = self.clone();
|
||||||
|
c.reset();
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binom(n: usize, k: usize) -> usize {
|
||||||
|
let k = k.min(n.saturating_sub(k));
|
||||||
|
let mut prod: usize = 1;
|
||||||
|
for i in 0..k {
|
||||||
|
prod = prod * (n - i) / (i + 1);
|
||||||
|
}
|
||||||
|
prod
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binom_basic() {
|
||||||
|
assert_eq!(binom(5, 2), 10);
|
||||||
|
assert_eq!(binom(10, 3), 120);
|
||||||
|
assert_eq!(binom(0, 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_count_2d() {
|
||||||
|
let it = BoundedSumIter::new(2, 3);
|
||||||
|
assert_eq!(it.num_values(), binom(5, 2)); // C(5,2)=10
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_traversal_2d() {
|
||||||
|
let mut it = BoundedSumIter::new(2, 3);
|
||||||
|
let mut total = 0;
|
||||||
|
while it.valid() {
|
||||||
|
total += it.last_dim_count();
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
assert_eq!(total, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_traversal_3d() {
|
||||||
|
let mut it = BoundedSumIter::new(3, 2);
|
||||||
|
// {(i,j,k) | i+j+k <= 2} has C(5,3)=10 elements
|
||||||
|
let mut total = 0;
|
||||||
|
while it.valid() {
|
||||||
|
total += it.last_dim_count();
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
assert_eq!(total, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn num_values_per_first() {
|
||||||
|
let it = BoundedSumIter::new(3, 2);
|
||||||
|
let counts = it.num_values_per_first_index();
|
||||||
|
// first_index=0: C(4,2)=6, first_index=1: C(3,2)=3, first_index=2: C(2,2)=1
|
||||||
|
assert_eq!(counts, vec![6, 3, 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
use crate::fixed::FixedPoint;
|
||||||
|
use crate::index::BoundedSumIter;
|
||||||
|
use crate::matrix::DenseMatrix;
|
||||||
|
use crate::operator::SparseTPOperator;
|
||||||
|
use crate::vector::MultiDimVec;
|
||||||
|
|
||||||
|
/// Basis function trait. Given a dimension k and index j, evaluates the
|
||||||
|
/// j-th basis function at fixed-point coordinate x.
|
||||||
|
pub trait BasisFn {
|
||||||
|
fn eval(&self, dim: usize, index: usize, x: i64, fp: &FixedPoint) -> i64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Point distribution trait. Returns the i-th grid point in
|
||||||
|
/// dimension k, as a fixed-point value.
|
||||||
|
pub trait PointDist {
|
||||||
|
fn point(&self, dim: usize, index: usize, fp: &FixedPoint) -> i64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monomial basis: phi_j(x) = x^j in fixed-point.
|
||||||
|
pub struct MonomialBasis;
|
||||||
|
|
||||||
|
impl BasisFn for MonomialBasis {
|
||||||
|
fn eval(&self, _dim: usize, index: usize, x: i64, fp: &FixedPoint) -> i64 {
|
||||||
|
let mut result = fp.one();
|
||||||
|
for _ in 0..index {
|
||||||
|
result = fp.mul(result, x);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Golden-ratio point distribution: point(i) = frac((i+1) * phi).
|
||||||
|
pub struct GoldenPoints;
|
||||||
|
|
||||||
|
impl PointDist for GoldenPoints {
|
||||||
|
fn point(&self, _dim: usize, index: usize, fp: &FixedPoint) -> i64 {
|
||||||
|
let phi = 0.5 * (1.0 + 5.0_f64.sqrt());
|
||||||
|
let val = (index + 1) as f64 * phi;
|
||||||
|
let frac = val - val.floor();
|
||||||
|
fp.from_f64(frac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uniform points on [0, 1]: point(i) = i / (n-1) for n points.
|
||||||
|
pub struct UniformPoints {
|
||||||
|
pub n: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointDist for UniformPoints {
|
||||||
|
fn point(&self, _dim: usize, index: usize, fp: &FixedPoint) -> i64 {
|
||||||
|
if self.n <= 1 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
fp.div(
|
||||||
|
fp.from_f64(index as f64),
|
||||||
|
fp.from_f64((self.n - 1) as f64),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the interpolation matrix for one dimension: M_k(i, j) = phi_k(j)(x_k(i)).
|
||||||
|
pub fn build_1d_matrix(
|
||||||
|
n: usize,
|
||||||
|
dim: usize,
|
||||||
|
basis: &dyn BasisFn,
|
||||||
|
points: &dyn PointDist,
|
||||||
|
fp: &FixedPoint,
|
||||||
|
) -> DenseMatrix {
|
||||||
|
let mut m = DenseMatrix::zeros(n, n);
|
||||||
|
for i in 0..n {
|
||||||
|
let x = points.point(dim, i, fp);
|
||||||
|
for j in 0..n {
|
||||||
|
m.set(i, j, basis.eval(dim, j, x, fp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a SparseTPOperator for interpolation with the given basis and points.
|
||||||
|
/// Requires d >= 2.
|
||||||
|
pub fn create_interpolation_operator(
|
||||||
|
fp: FixedPoint,
|
||||||
|
iter: &BoundedSumIter,
|
||||||
|
basis: &dyn BasisFn,
|
||||||
|
points: &dyn PointDist,
|
||||||
|
) -> SparseTPOperator {
|
||||||
|
assert!(iter.dim() >= 2, "sparse grid requires d >= 2");
|
||||||
|
let d = iter.dim();
|
||||||
|
let bounds = iter.index_bounds();
|
||||||
|
|
||||||
|
let matrices: Vec<DenseMatrix> = (0..d)
|
||||||
|
.map(|k| build_1d_matrix(bounds[k], k, basis, points, &fp))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
SparseTPOperator::new(fp, iter.clone(), matrices)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a function at all sparse grid points.
|
||||||
|
/// The function takes a slice of fixed-point coordinates and returns
|
||||||
|
/// a fixed-point value. Requires d >= 2.
|
||||||
|
pub fn evaluate_function(
|
||||||
|
iter: &BoundedSumIter,
|
||||||
|
f: impl Fn(&[i64]) -> i64,
|
||||||
|
points: &dyn PointDist,
|
||||||
|
fp: &FixedPoint,
|
||||||
|
) -> MultiDimVec {
|
||||||
|
assert!(iter.dim() >= 2, "sparse grid requires d >= 2");
|
||||||
|
let d = iter.dim();
|
||||||
|
let mut v = MultiDimVec::new(iter.clone());
|
||||||
|
let mut indexes: Vec<usize> = vec![0; v.data.len()];
|
||||||
|
|
||||||
|
let mut jump = iter.clone();
|
||||||
|
jump.reset();
|
||||||
|
let mut point = vec![0i64; d];
|
||||||
|
|
||||||
|
while jump.valid() {
|
||||||
|
let last_count = jump.last_dim_count();
|
||||||
|
|
||||||
|
for dim in 0..(d - 1) {
|
||||||
|
point[dim] = points.point(dim, jump.index_at(dim), fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
for last_idx in 0..last_count {
|
||||||
|
point[d - 1] = points.point(d - 1, last_idx, fp);
|
||||||
|
let val = f(&point);
|
||||||
|
let first = jump.first_index();
|
||||||
|
let idx = indexes[first];
|
||||||
|
v.data[first][idx] = val;
|
||||||
|
indexes[first] = idx + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jump.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full interpolation pipeline: evaluate function, build operator, solve.
|
||||||
|
/// Requires d >= 2.
|
||||||
|
pub fn interpolate(
|
||||||
|
fp: FixedPoint,
|
||||||
|
iter: &BoundedSumIter,
|
||||||
|
f: impl Fn(&[i64]) -> i64,
|
||||||
|
basis: &dyn BasisFn,
|
||||||
|
points: &dyn PointDist,
|
||||||
|
) -> MultiDimVec {
|
||||||
|
let rhs = evaluate_function(iter, f, points, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, iter, basis, points);
|
||||||
|
op.solve(rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn monomial_eval() {
|
||||||
|
let fp = FixedPoint::new(24);
|
||||||
|
let basis = MonomialBasis;
|
||||||
|
let x = fp.from_f64(0.5);
|
||||||
|
assert_eq!(basis.eval(0, 0, x, &fp), fp.one());
|
||||||
|
let v = basis.eval(0, 1, x, &fp);
|
||||||
|
assert!((fp.to_f64(v) - 0.5).abs() < 0.001);
|
||||||
|
let v = basis.eval(0, 2, x, &fp);
|
||||||
|
assert!((fp.to_f64(v) - 0.25).abs() < 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_points_in_unit_interval() {
|
||||||
|
let fp = FixedPoint::new(24);
|
||||||
|
let pts = GoldenPoints;
|
||||||
|
for i in 0..10 {
|
||||||
|
let p = fp.to_f64(pts.point(0, i, &fp));
|
||||||
|
assert!(p >= 0.0 && p < 1.0, "point {i} = {p} out of [0,1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interpolate_2d_product() {
|
||||||
|
let fp = FixedPoint::new(24);
|
||||||
|
let iter = BoundedSumIter::new(2, 3);
|
||||||
|
let basis = MonomialBasis;
|
||||||
|
let pts = GoldenPoints;
|
||||||
|
|
||||||
|
// f(x, y) = x * y -- product of coordinates
|
||||||
|
let coeffs = interpolate(
|
||||||
|
fp,
|
||||||
|
&iter,
|
||||||
|
|pt| fp.mul(pt[0], pt[1]),
|
||||||
|
&basis,
|
||||||
|
&pts,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reconstruct: apply the operator to coefficients and compare to rhs
|
||||||
|
let rhs = evaluate_function(&iter, |pt| fp.mul(pt[0], pt[1]), &pts, &fp);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
let reconstructed = op.apply(coeffs);
|
||||||
|
|
||||||
|
// Compute reconstruction error
|
||||||
|
let mut err_sq: i128 = 0;
|
||||||
|
let mut norm_sq: i128 = 0;
|
||||||
|
for (r_row, rhs_row) in reconstructed.data.iter().zip(rhs.data.iter()) {
|
||||||
|
for (&r, &rh) in r_row.iter().zip(rhs_row.iter()) {
|
||||||
|
let diff = r - rh;
|
||||||
|
err_sq += (diff as i128) * (diff as i128);
|
||||||
|
norm_sq += (rh as i128) * (rh as i128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel_err = if norm_sq > 0 {
|
||||||
|
(err_sq as f64 / norm_sq as f64).sqrt()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
assert!(rel_err < 0.01, "relative reconstruction error = {rel_err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interpolate_3d_product() {
|
||||||
|
let fp = FixedPoint::new(24);
|
||||||
|
let iter = BoundedSumIter::new(3, 3);
|
||||||
|
let basis = MonomialBasis;
|
||||||
|
let pts = GoldenPoints;
|
||||||
|
|
||||||
|
// f(x, y, z) = x * y * z
|
||||||
|
let coeffs = interpolate(
|
||||||
|
fp,
|
||||||
|
&iter,
|
||||||
|
|pt| fp.mul(fp.mul(pt[0], pt[1]), pt[2]),
|
||||||
|
&basis,
|
||||||
|
&pts,
|
||||||
|
);
|
||||||
|
|
||||||
|
let rhs = evaluate_function(
|
||||||
|
&iter,
|
||||||
|
|pt| fp.mul(fp.mul(pt[0], pt[1]), pt[2]),
|
||||||
|
&pts,
|
||||||
|
&fp,
|
||||||
|
);
|
||||||
|
let mut op = create_interpolation_operator(fp, &iter, &basis, &pts);
|
||||||
|
let reconstructed = op.apply(coeffs);
|
||||||
|
|
||||||
|
let mut err_sq: i128 = 0;
|
||||||
|
let mut norm_sq: i128 = 0;
|
||||||
|
for (r_row, rhs_row) in reconstructed.data.iter().zip(rhs.data.iter()) {
|
||||||
|
for (&r, &rh) in r_row.iter().zip(rhs_row.iter()) {
|
||||||
|
let diff = r - rh;
|
||||||
|
err_sq += (diff as i128) * (diff as i128);
|
||||||
|
norm_sq += (rh as i128) * (rh as i128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel_err = if norm_sq > 0 {
|
||||||
|
(err_sq as f64 / norm_sq as f64).sqrt()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
assert!(rel_err < 0.01, "3D relative reconstruction error = {rel_err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
//! Sparse grid interpolation in fixed-point arithmetic.
|
||||||
|
//!
|
||||||
|
//! Implements the fast sparse tensor product matrix-vector product
|
||||||
|
//! from Holzmuller & Pfluger (2021), adapted for integer-only
|
||||||
|
//! evaluation compatible with CORDIC instruction sequences.
|
||||||
|
//!
|
||||||
|
//! The algorithm uses the unidirectional principle: cycle through
|
||||||
|
//! dimensions, multiplying one 1D factor at a time. Complexity is
|
||||||
|
//! O(d * N * n) instead of O(N^2), where d is dimensionality,
|
||||||
|
//! N is the total grid size, and n is the 1D resolution.
|
||||||
|
|
||||||
|
pub mod fixed;
|
||||||
|
pub mod index;
|
||||||
|
pub mod vector;
|
||||||
|
pub mod matrix;
|
||||||
|
pub mod operator;
|
||||||
|
pub mod interp;
|
||||||
|
|
||||||
|
pub use fixed::FixedPoint;
|
||||||
|
pub use index::BoundedSumIter;
|
||||||
|
pub use vector::MultiDimVec;
|
||||||
|
pub use matrix::DenseMatrix;
|
||||||
|
pub use operator::SparseTPOperator;
|
||||||
|
pub use interp::{BasisFn, PointDist, MonomialBasis, GoldenPoints, UniformPoints};
|
||||||
|
pub use interp::{interpolate, evaluate_function, create_interpolation_operator};
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
/// Dense matrix stored row-major for one-dimensional operator factors.
|
||||||
|
/// Values are i64 fixed-point.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DenseMatrix {
|
||||||
|
pub rows: usize,
|
||||||
|
pub cols: usize,
|
||||||
|
pub data: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DenseMatrix {
|
||||||
|
pub fn zeros(rows: usize, cols: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
data: vec![0; rows * cols],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identity(n: usize, one: i64) -> Self {
|
||||||
|
let mut m = Self::zeros(n, n);
|
||||||
|
for i in 0..n {
|
||||||
|
m.set(i, i, one);
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self, row: usize, col: usize) -> i64 {
|
||||||
|
self.data[row * self.cols + col]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set(&mut self, row: usize, col: usize, val: i64) {
|
||||||
|
self.data[row * self.cols + col] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LU decomposition in-place. Returns (L, U) as separate matrices.
|
||||||
|
/// L has 1s on the diagonal (unit lower triangular, where "1" is
|
||||||
|
/// the fixed-point representation of one).
|
||||||
|
pub fn lu_decompose(
|
||||||
|
&self,
|
||||||
|
one: i64,
|
||||||
|
fp_mul: impl Fn(i64, i64) -> i64,
|
||||||
|
fp_div: impl Fn(i64, i64) -> i64,
|
||||||
|
) -> (DenseMatrix, DenseMatrix) {
|
||||||
|
assert_eq!(self.rows, self.cols);
|
||||||
|
let n = self.rows;
|
||||||
|
let mut lu = self.clone();
|
||||||
|
|
||||||
|
for k in 0..n {
|
||||||
|
let pivot = lu.get(k, k);
|
||||||
|
for i in (k + 1)..n {
|
||||||
|
let factor = fp_div(lu.get(i, k), pivot);
|
||||||
|
lu.set(i, k, factor);
|
||||||
|
for j in (k + 1)..n {
|
||||||
|
let val = lu.get(i, j) - fp_mul(factor, lu.get(k, j));
|
||||||
|
lu.set(i, j, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lower = DenseMatrix::zeros(n, n);
|
||||||
|
let mut upper = DenseMatrix::zeros(n, n);
|
||||||
|
for i in 0..n {
|
||||||
|
for j in 0..i {
|
||||||
|
lower.set(i, j, lu.get(i, j));
|
||||||
|
}
|
||||||
|
lower.set(i, i, one);
|
||||||
|
for j in i..n {
|
||||||
|
upper.set(i, j, lu.get(i, j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(lower, upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invert a unit lower triangular matrix via forward substitution.
|
||||||
|
pub fn invert_unit_lower(&self, one: i64, fp_mul: impl Fn(i64, i64) -> i64) -> DenseMatrix {
|
||||||
|
assert_eq!(self.rows, self.cols);
|
||||||
|
let n = self.rows;
|
||||||
|
let mut inv = DenseMatrix::identity(n, one);
|
||||||
|
|
||||||
|
for col in 0..n {
|
||||||
|
for row in (col + 1)..n {
|
||||||
|
let mut sum = 0i64;
|
||||||
|
for k in col..row {
|
||||||
|
sum = sum.wrapping_add(fp_mul(self.get(row, k), inv.get(k, col)));
|
||||||
|
}
|
||||||
|
inv.set(row, col, -sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inv
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invert an upper triangular matrix via back substitution.
|
||||||
|
pub fn invert_upper(
|
||||||
|
&self,
|
||||||
|
one: i64,
|
||||||
|
fp_mul: impl Fn(i64, i64) -> i64,
|
||||||
|
fp_div: impl Fn(i64, i64) -> i64,
|
||||||
|
) -> DenseMatrix {
|
||||||
|
assert_eq!(self.rows, self.cols);
|
||||||
|
let n = self.rows;
|
||||||
|
let mut inv = DenseMatrix::zeros(n, n);
|
||||||
|
|
||||||
|
for col in (0..n).rev() {
|
||||||
|
inv.set(col, col, fp_div(one, self.get(col, col)));
|
||||||
|
for row in (0..col).rev() {
|
||||||
|
let mut sum = 0i64;
|
||||||
|
for k in (row + 1)..=col {
|
||||||
|
sum = sum.wrapping_add(fp_mul(self.get(row, k), inv.get(k, col)));
|
||||||
|
}
|
||||||
|
inv.set(row, col, fp_div(-sum, self.get(row, row)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::fixed::FixedPoint;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_diag() {
|
||||||
|
let fp = FixedPoint::new(16);
|
||||||
|
let m = DenseMatrix::identity(3, fp.one());
|
||||||
|
for i in 0..3 {
|
||||||
|
assert_eq!(m.get(i, i), fp.one());
|
||||||
|
for j in 0..3 {
|
||||||
|
if i != j {
|
||||||
|
assert_eq!(m.get(i, j), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lu_2x2() {
|
||||||
|
let fp = FixedPoint::new(16);
|
||||||
|
let mut m = DenseMatrix::zeros(2, 2);
|
||||||
|
m.set(0, 0, fp.from_f64(4.0));
|
||||||
|
m.set(0, 1, fp.from_f64(3.0));
|
||||||
|
m.set(1, 0, fp.from_f64(6.0));
|
||||||
|
m.set(1, 1, fp.from_f64(3.0));
|
||||||
|
|
||||||
|
let (l, u) = m.lu_decompose(
|
||||||
|
fp.one(),
|
||||||
|
|a, b| fp.mul(a, b),
|
||||||
|
|a, b| fp.div(a, b),
|
||||||
|
);
|
||||||
|
|
||||||
|
// L should be [[1, 0], [1.5, 1]]
|
||||||
|
assert_eq!(l.get(0, 0), fp.one());
|
||||||
|
assert!((fp.to_f64(l.get(1, 0)) - 1.5).abs() < 0.01);
|
||||||
|
assert_eq!(l.get(1, 1), fp.one());
|
||||||
|
|
||||||
|
// U should be [[4, 3], [0, -1.5]]
|
||||||
|
assert!((fp.to_f64(u.get(0, 0)) - 4.0).abs() < 0.01);
|
||||||
|
assert!((fp.to_f64(u.get(1, 1)) + 1.5).abs() < 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
use crate::fixed::FixedPoint;
|
||||||
|
use crate::index::BoundedSumIter;
|
||||||
|
use crate::matrix::DenseMatrix;
|
||||||
|
use crate::vector::MultiDimVec;
|
||||||
|
|
||||||
|
/// Sparse tensor product operator.
|
||||||
|
///
|
||||||
|
/// Implicitly stores a matrix that is the restriction of a tensor product
|
||||||
|
/// M_1 x M_2 x ... x M_d to a downward-closed multi-index set.
|
||||||
|
///
|
||||||
|
/// Uses the unidirectional principle: multiply one dimension at a time,
|
||||||
|
/// cycling indices between multiplications. O(d * N * n) where N is the
|
||||||
|
/// total number of grid points and n is the 1D matrix size, compared to
|
||||||
|
/// O(N^2) for a naive matrix-vector product.
|
||||||
|
///
|
||||||
|
/// All arithmetic is fixed-point.
|
||||||
|
pub struct SparseTPOperator {
|
||||||
|
fp: FixedPoint,
|
||||||
|
iter: BoundedSumIter,
|
||||||
|
d: usize,
|
||||||
|
l_inv: Option<Vec<DenseMatrix>>,
|
||||||
|
u_inv: Option<Vec<DenseMatrix>>,
|
||||||
|
lower: Option<Vec<DenseMatrix>>,
|
||||||
|
upper: Option<Vec<DenseMatrix>>,
|
||||||
|
matrices: Vec<DenseMatrix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SparseTPOperator {
|
||||||
|
pub fn new(fp: FixedPoint, iter: BoundedSumIter, matrices: Vec<DenseMatrix>) -> Self {
|
||||||
|
let d = matrices.len();
|
||||||
|
Self {
|
||||||
|
fp,
|
||||||
|
iter,
|
||||||
|
d,
|
||||||
|
l_inv: None,
|
||||||
|
u_inv: None,
|
||||||
|
lower: None,
|
||||||
|
upper: None,
|
||||||
|
matrices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_lu(&mut self) {
|
||||||
|
if self.lower.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let fp = self.fp;
|
||||||
|
let mut ls = Vec::with_capacity(self.d);
|
||||||
|
let mut us = Vec::with_capacity(self.d);
|
||||||
|
for m in &self.matrices {
|
||||||
|
let (l, u) = m.lu_decompose(
|
||||||
|
fp.one(),
|
||||||
|
|a, b| fp.mul(a, b),
|
||||||
|
|a, b| fp.div(a, b),
|
||||||
|
);
|
||||||
|
ls.push(l);
|
||||||
|
us.push(u);
|
||||||
|
}
|
||||||
|
self.lower = Some(ls);
|
||||||
|
self.upper = Some(us);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_solve(&mut self) {
|
||||||
|
if self.l_inv.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.ensure_lu();
|
||||||
|
let fp = self.fp;
|
||||||
|
let lower = self.lower.as_ref().unwrap();
|
||||||
|
let upper = self.upper.as_ref().unwrap();
|
||||||
|
|
||||||
|
let l_inv: Vec<DenseMatrix> = lower
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.invert_unit_lower(fp.one(), |a, b| fp.mul(a, b)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let u_inv: Vec<DenseMatrix> = upper
|
||||||
|
.iter()
|
||||||
|
.map(|u| {
|
||||||
|
u.invert_upper(
|
||||||
|
fp.one(),
|
||||||
|
|a, b| fp.mul(a, b),
|
||||||
|
|a, b| fp.div(a, b),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.l_inv = Some(l_inv);
|
||||||
|
self.u_inv = Some(u_inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matrix-vector product: y = A * x where A is the sparse tensor product.
|
||||||
|
pub fn apply(&mut self, input: MultiDimVec) -> MultiDimVec {
|
||||||
|
self.ensure_lu();
|
||||||
|
let mut v = input;
|
||||||
|
let mut buffer = MultiDimVec::new(self.iter.clone());
|
||||||
|
|
||||||
|
let upper = self.upper.as_ref().unwrap();
|
||||||
|
for k in (0..self.d).rev() {
|
||||||
|
multiply_single_upper_triangular(&upper[k], &mut v, &mut buffer, &self.fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower = self.lower.as_ref().unwrap();
|
||||||
|
for k in (0..self.d).rev() {
|
||||||
|
multiply_single_lower_triangular(&lower[k], &mut v, &mut buffer, &self.fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Solve A * x = rhs for x, where A is the sparse tensor product.
|
||||||
|
pub fn solve(&mut self, rhs: MultiDimVec) -> MultiDimVec {
|
||||||
|
self.ensure_solve();
|
||||||
|
let mut v = rhs;
|
||||||
|
let mut buffer = MultiDimVec::new(self.iter.clone());
|
||||||
|
|
||||||
|
let l_inv = self.l_inv.as_ref().unwrap();
|
||||||
|
for k in (0..self.d).rev() {
|
||||||
|
multiply_single_lower_triangular(&l_inv[k], &mut v, &mut buffer, &self.fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let u_inv = self.u_inv.as_ref().unwrap();
|
||||||
|
for k in (0..self.d).rev() {
|
||||||
|
multiply_single_upper_triangular(&u_inv[k], &mut v, &mut buffer, &self.fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Below this inner-loop size, slice setup overhead exceeds the
|
||||||
|
/// savings from deferred-shift accumulation.
|
||||||
|
const SLICE_THRESHOLD: usize = 16;
|
||||||
|
|
||||||
|
/// Multiply v by \hat{I x ... x I x L} and cycle indices.
|
||||||
|
/// L is lower triangular. Result goes into buffer, then swap.
|
||||||
|
///
|
||||||
|
/// For count >= SLICE_THRESHOLD, uses deferred-shift i128 accumulation
|
||||||
|
/// with pre-sliced data to eliminate per-element shifts and bounds
|
||||||
|
/// checks. For small counts, uses the simpler per-element fp.mul path.
|
||||||
|
fn multiply_single_lower_triangular(
|
||||||
|
l: &DenseMatrix,
|
||||||
|
v: &mut MultiDimVec,
|
||||||
|
buffer: &mut MultiDimVec,
|
||||||
|
fp: &FixedPoint,
|
||||||
|
) {
|
||||||
|
let it = v.iter().clone();
|
||||||
|
let cycled = it.cycle();
|
||||||
|
buffer.reset_with_iter(cycled);
|
||||||
|
|
||||||
|
let mut indexes: Vec<usize> = vec![0; buffer.data.len()];
|
||||||
|
let mut jump = it;
|
||||||
|
jump.reset();
|
||||||
|
|
||||||
|
let mut first_v_idx = 0usize;
|
||||||
|
let mut second_v_idx = 0usize;
|
||||||
|
|
||||||
|
let frac_bits = fp.frac_bits;
|
||||||
|
let l_data = &l.data;
|
||||||
|
let l_cols = l.cols;
|
||||||
|
|
||||||
|
while jump.valid() {
|
||||||
|
let count = jump.last_dim_count();
|
||||||
|
|
||||||
|
if count >= SLICE_THRESHOLD {
|
||||||
|
let src_slice = &v.data[first_v_idx][second_v_idx..second_v_idx + count];
|
||||||
|
for i in 0..count {
|
||||||
|
let row = &l_data[i * l_cols..i * l_cols + i + 1];
|
||||||
|
let src = &src_slice[..i + 1];
|
||||||
|
let n = row.len();
|
||||||
|
let mut acc: i128 = 0;
|
||||||
|
for j in 0..n {
|
||||||
|
acc += row[j] as i128 * src[j] as i128;
|
||||||
|
}
|
||||||
|
let bi = indexes[i];
|
||||||
|
buffer.data[i][bi] = (acc >> frac_bits) as i64;
|
||||||
|
indexes[i] = bi + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let src = &v.data[first_v_idx];
|
||||||
|
for i in 0..count {
|
||||||
|
let mut sum: i128 = 0;
|
||||||
|
for j in 0..=i {
|
||||||
|
sum += fp.mul(l.get(i, j), src[second_v_idx + j]) as i128;
|
||||||
|
}
|
||||||
|
let bi = indexes[i];
|
||||||
|
buffer.data[i][bi] = sum as i64;
|
||||||
|
indexes[i] = bi + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
second_v_idx += count;
|
||||||
|
if second_v_idx >= v.data[first_v_idx].len() {
|
||||||
|
second_v_idx = 0;
|
||||||
|
first_v_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jump.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
v.swap(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiply v by \hat{I x ... x I x U} and cycle indices.
|
||||||
|
/// U is upper triangular.
|
||||||
|
fn multiply_single_upper_triangular(
|
||||||
|
u: &DenseMatrix,
|
||||||
|
v: &mut MultiDimVec,
|
||||||
|
buffer: &mut MultiDimVec,
|
||||||
|
fp: &FixedPoint,
|
||||||
|
) {
|
||||||
|
let it = v.iter().clone();
|
||||||
|
let cycled = it.cycle();
|
||||||
|
buffer.reset_with_iter(cycled);
|
||||||
|
|
||||||
|
let mut indexes: Vec<usize> = vec![0; buffer.data.len()];
|
||||||
|
let mut jump = it;
|
||||||
|
jump.reset();
|
||||||
|
|
||||||
|
let mut first_v_idx = 0usize;
|
||||||
|
let mut second_v_idx = 0usize;
|
||||||
|
|
||||||
|
let frac_bits = fp.frac_bits;
|
||||||
|
let u_data = &u.data;
|
||||||
|
let u_cols = u.cols;
|
||||||
|
|
||||||
|
while jump.valid() {
|
||||||
|
let count = jump.last_dim_count();
|
||||||
|
|
||||||
|
if count >= SLICE_THRESHOLD {
|
||||||
|
let src_slice = &v.data[first_v_idx][second_v_idx..second_v_idx + count];
|
||||||
|
for i in 0..count {
|
||||||
|
let row = &u_data[i * u_cols + i..i * u_cols + count];
|
||||||
|
let sv = &src_slice[i..count];
|
||||||
|
let n = row.len();
|
||||||
|
let mut acc: i128 = 0;
|
||||||
|
for j in 0..n {
|
||||||
|
acc += row[j] as i128 * sv[j] as i128;
|
||||||
|
}
|
||||||
|
let bi = indexes[i];
|
||||||
|
buffer.data[i][bi] = (acc >> frac_bits) as i64;
|
||||||
|
indexes[i] = bi + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let src = &v.data[first_v_idx];
|
||||||
|
for i in 0..count {
|
||||||
|
let mut sum: i128 = 0;
|
||||||
|
for j in i..count {
|
||||||
|
sum += fp.mul(u.get(i, j), src[second_v_idx + j]) as i128;
|
||||||
|
}
|
||||||
|
let bi = indexes[i];
|
||||||
|
buffer.data[i][bi] = sum as i64;
|
||||||
|
indexes[i] = bi + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
second_v_idx += count;
|
||||||
|
if second_v_idx >= v.data[first_v_idx].len() {
|
||||||
|
second_v_idx = 0;
|
||||||
|
first_v_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jump.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
v.swap(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycle indices without matrix multiplication (identity multiply).
|
||||||
|
pub fn cycle_identity(v: &mut MultiDimVec, buffer: &mut MultiDimVec) {
|
||||||
|
let it = v.iter().clone();
|
||||||
|
let cycled = it.cycle();
|
||||||
|
buffer.reset_with_iter(cycled);
|
||||||
|
|
||||||
|
let mut indexes: Vec<usize> = vec![0; buffer.data.len()];
|
||||||
|
let mut jump = it;
|
||||||
|
jump.reset();
|
||||||
|
|
||||||
|
let mut first_v_idx = 0usize;
|
||||||
|
let mut second_v_idx = 0usize;
|
||||||
|
|
||||||
|
while jump.valid() {
|
||||||
|
let count = jump.last_dim_count();
|
||||||
|
let src = &v.data[first_v_idx];
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
let bi = indexes[i];
|
||||||
|
buffer.data[i][bi] = src[second_v_idx + i];
|
||||||
|
indexes[i] = bi + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
second_v_idx += count;
|
||||||
|
if second_v_idx >= v.data[first_v_idx].len() {
|
||||||
|
second_v_idx = 0;
|
||||||
|
first_v_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jump.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
v.swap(buffer);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
use crate::index::BoundedSumIter;
|
||||||
|
|
||||||
|
/// Multi-dimensional vector indexed by a bounded-sum multi-index set.
|
||||||
|
///
|
||||||
|
/// Storage is split by first dimension for cache-friendly access
|
||||||
|
/// during the unidirectional matrix-vector products: data[i] contains
|
||||||
|
/// all entries whose multi-index starts with i, in lexicographic order.
|
||||||
|
///
|
||||||
|
/// Values are i64 fixed-point throughout.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MultiDimVec {
|
||||||
|
pub data: Vec<Vec<i64>>,
|
||||||
|
iter: BoundedSumIter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiDimVec {
|
||||||
|
pub fn new(iter: BoundedSumIter) -> Self {
|
||||||
|
let sizes = iter.num_values_per_first_index();
|
||||||
|
let data = sizes.iter().map(|&s| vec![0i64; s]).collect();
|
||||||
|
Self { data, iter }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> &BoundedSumIter {
|
||||||
|
&self.iter
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_with_iter(&mut self, iter: BoundedSumIter) {
|
||||||
|
if self.iter.dim() == iter.dim()
|
||||||
|
&& self.iter.first_index_bound() == iter.first_index_bound()
|
||||||
|
{
|
||||||
|
self.iter = iter;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sizes = iter.num_values_per_first_index();
|
||||||
|
self.data.resize(sizes.len(), Vec::new());
|
||||||
|
for (i, &s) in sizes.iter().enumerate() {
|
||||||
|
self.data[i].resize(s, 0);
|
||||||
|
}
|
||||||
|
self.iter = iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap(&mut self, other: &mut MultiDimVec) {
|
||||||
|
std::mem::swap(&mut self.data, &mut other.data);
|
||||||
|
std::mem::swap(&mut self.iter, &mut other.iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_assign(&mut self, other: &MultiDimVec) {
|
||||||
|
for (row, other_row) in self.data.iter_mut().zip(other.data.iter()) {
|
||||||
|
for (a, b) in row.iter_mut().zip(other_row.iter()) {
|
||||||
|
*a += b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sub_assign(&mut self, other: &MultiDimVec) {
|
||||||
|
for (row, other_row) in self.data.iter_mut().zip(other.data.iter()) {
|
||||||
|
for (a, b) in row.iter_mut().zip(other_row.iter()) {
|
||||||
|
*a -= b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn squared_l2_norm(&self) -> i128 {
|
||||||
|
let mut sum: i128 = 0;
|
||||||
|
for row in &self.data {
|
||||||
|
for &v in row {
|
||||||
|
sum += (v as i128) * (v as i128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all entries yielding (multi_index, &value).
|
||||||
|
pub struct MultiDimVecIter<'a> {
|
||||||
|
vec: &'a MultiDimVec,
|
||||||
|
jump: BoundedSumIter,
|
||||||
|
last_dim_idx: usize,
|
||||||
|
last_dim_count: usize,
|
||||||
|
first_idx: usize,
|
||||||
|
tail_counter: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MultiDimVecIter<'a> {
|
||||||
|
pub fn new(vec: &'a MultiDimVec) -> Self {
|
||||||
|
let mut jump = vec.iter.clone();
|
||||||
|
jump.reset();
|
||||||
|
let count = if jump.valid() { jump.last_dim_count() } else { 0 };
|
||||||
|
Self {
|
||||||
|
vec,
|
||||||
|
jump,
|
||||||
|
last_dim_idx: 0,
|
||||||
|
last_dim_count: count,
|
||||||
|
first_idx: 0,
|
||||||
|
tail_counter: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for MultiDimVecIter<'a> {
|
||||||
|
type Item = i64;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<i64> {
|
||||||
|
if !self.jump.valid() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let val = self.vec.data[self.first_idx][self.tail_counter];
|
||||||
|
self.last_dim_idx += 1;
|
||||||
|
self.tail_counter += 1;
|
||||||
|
|
||||||
|
if self.last_dim_idx >= self.last_dim_count {
|
||||||
|
self.last_dim_idx = 0;
|
||||||
|
self.jump.next();
|
||||||
|
if self.jump.valid() {
|
||||||
|
let new_first = self.jump.first_index();
|
||||||
|
if new_first != self.first_idx {
|
||||||
|
self.first_idx = new_first;
|
||||||
|
self.tail_counter = 0;
|
||||||
|
}
|
||||||
|
self.last_dim_count = self.jump.last_dim_count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn storage_size() {
|
||||||
|
let it = BoundedSumIter::new(3, 2);
|
||||||
|
let v = MultiDimVec::new(it.clone());
|
||||||
|
let total: usize = v.data.iter().map(|r| r.len()).sum();
|
||||||
|
assert_eq!(total, it.num_values());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_all_values() {
|
||||||
|
let it = BoundedSumIter::new(3, 2);
|
||||||
|
let mut v = MultiDimVec::new(it.clone());
|
||||||
|
// Fill with sequential values
|
||||||
|
let mut val = 1i64;
|
||||||
|
for row in &mut v.data {
|
||||||
|
for cell in row.iter_mut() {
|
||||||
|
*cell = val;
|
||||||
|
val += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let collected: Vec<i64> = MultiDimVecIter::new(&v).collect();
|
||||||
|
assert_eq!(collected.len(), it.num_values());
|
||||||
|
assert_eq!(collected[0], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "cord-trig"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Trigonometric IR for SDF geometry — the universal intermediate representation"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["sdf", "trig", "ir", "dag", "geometry"]
|
||||||
|
categories = ["graphics", "mathematics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
use crate::ir::{TrigGraph, TrigOp};
|
||||||
|
|
||||||
|
/// CPU reference evaluator. Walks the trig graph forward,
|
||||||
|
/// evaluating each node with f64 math.
|
||||||
|
///
|
||||||
|
/// This is the ground truth. The CORDIC evaluator must produce
|
||||||
|
/// results that converge to this within its word-size precision.
|
||||||
|
pub fn evaluate(graph: &TrigGraph, x: f64, y: f64, z: f64) -> f64 {
|
||||||
|
let mut vals = vec![0.0f64; graph.nodes.len()];
|
||||||
|
|
||||||
|
for (i, op) in graph.nodes.iter().enumerate() {
|
||||||
|
vals[i] = match op {
|
||||||
|
TrigOp::InputX => x,
|
||||||
|
TrigOp::InputY => y,
|
||||||
|
TrigOp::InputZ => z,
|
||||||
|
TrigOp::Const(c) => *c,
|
||||||
|
|
||||||
|
TrigOp::Add(a, b) => vals[*a as usize] + vals[*b as usize],
|
||||||
|
TrigOp::Sub(a, b) => vals[*a as usize] - vals[*b as usize],
|
||||||
|
TrigOp::Mul(a, b) => vals[*a as usize] * vals[*b as usize],
|
||||||
|
TrigOp::Div(a, b) => vals[*a as usize] / vals[*b as usize],
|
||||||
|
TrigOp::Neg(a) => -vals[*a as usize],
|
||||||
|
TrigOp::Abs(a) => vals[*a as usize].abs(),
|
||||||
|
|
||||||
|
TrigOp::Sin(a) => vals[*a as usize].sin(),
|
||||||
|
TrigOp::Cos(a) => vals[*a as usize].cos(),
|
||||||
|
TrigOp::Tan(a) => vals[*a as usize].tan(),
|
||||||
|
TrigOp::Asin(a) => vals[*a as usize].asin(),
|
||||||
|
TrigOp::Acos(a) => vals[*a as usize].acos(),
|
||||||
|
TrigOp::Atan(a) => vals[*a as usize].atan(),
|
||||||
|
TrigOp::Sinh(a) => vals[*a as usize].sinh(),
|
||||||
|
TrigOp::Cosh(a) => vals[*a as usize].cosh(),
|
||||||
|
TrigOp::Tanh(a) => vals[*a as usize].tanh(),
|
||||||
|
TrigOp::Asinh(a) => vals[*a as usize].asinh(),
|
||||||
|
TrigOp::Acosh(a) => vals[*a as usize].acosh(),
|
||||||
|
TrigOp::Atanh(a) => vals[*a as usize].atanh(),
|
||||||
|
TrigOp::Sqrt(a) => vals[*a as usize].sqrt(),
|
||||||
|
TrigOp::Exp(a) => vals[*a as usize].exp(),
|
||||||
|
TrigOp::Ln(a) => vals[*a as usize].ln(),
|
||||||
|
|
||||||
|
TrigOp::Hypot(a, b) => vals[*a as usize].hypot(vals[*b as usize]),
|
||||||
|
TrigOp::Atan2(a, b) => vals[*a as usize].atan2(vals[*b as usize]),
|
||||||
|
|
||||||
|
TrigOp::Min(a, b) => vals[*a as usize].min(vals[*b as usize]),
|
||||||
|
TrigOp::Max(a, b) => vals[*a as usize].max(vals[*b as usize]),
|
||||||
|
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
vals[*val as usize].clamp(vals[*lo as usize], vals[*hi as usize])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
vals[graph.output as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate on a regular 3D grid. Returns a flat array in x-major order.
|
||||||
|
/// Grid spans [min, max] with `res` samples per axis.
|
||||||
|
pub fn evaluate_grid(
|
||||||
|
graph: &TrigGraph,
|
||||||
|
min: [f64; 3],
|
||||||
|
max: [f64; 3],
|
||||||
|
res: usize,
|
||||||
|
) -> Vec<f64> {
|
||||||
|
let mut data = vec![0.0f64; res * res * res];
|
||||||
|
let step = [
|
||||||
|
(max[0] - min[0]) / (res - 1).max(1) as f64,
|
||||||
|
(max[1] - min[1]) / (res - 1).max(1) as f64,
|
||||||
|
(max[2] - min[2]) / (res - 1).max(1) as f64,
|
||||||
|
];
|
||||||
|
|
||||||
|
for iz in 0..res {
|
||||||
|
let z = min[2] + iz as f64 * step[2];
|
||||||
|
for iy in 0..res {
|
||||||
|
let y = min[1] + iy as f64 * step[1];
|
||||||
|
for ix in 0..res {
|
||||||
|
let x = min[0] + ix as f64 * step[0];
|
||||||
|
data[iz * res * res + iy * res + ix] = evaluate(graph, x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
/// Index into the graph's node array.
|
||||||
|
pub type NodeId = u32;
|
||||||
|
|
||||||
|
/// A directed acyclic graph of trig operations.
|
||||||
|
///
|
||||||
|
/// Nodes are stored in topological order: a node can only reference
|
||||||
|
/// earlier nodes (lower indices). Evaluation walks forward through
|
||||||
|
/// the array, so every dependency is resolved before it's needed.
|
||||||
|
///
|
||||||
|
/// This is the universal intermediate representation. Every SDF
|
||||||
|
/// primitive, every transform, every boolean operation decomposes
|
||||||
|
/// into this. Every node maps to exactly one CORDIC mode or a
|
||||||
|
/// direct binary operation.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TrigGraph {
|
||||||
|
pub nodes: Vec<TrigOp>,
|
||||||
|
pub output: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single operation in the trig IR.
|
||||||
|
///
|
||||||
|
/// CORDIC mapping:
|
||||||
|
/// Sin, Cos → rotation mode (angle → sin/cos)
|
||||||
|
/// Hypot, Atan2 → vectoring mode (x,y → magnitude/angle)
|
||||||
|
/// Mul → linear mode (multiply-accumulate)
|
||||||
|
/// Add, Sub, Neg, Abs, Min, Max → direct binary (no CORDIC)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TrigOp {
|
||||||
|
// Evaluation point inputs
|
||||||
|
InputX,
|
||||||
|
InputY,
|
||||||
|
InputZ,
|
||||||
|
|
||||||
|
// Constant
|
||||||
|
Const(f64),
|
||||||
|
|
||||||
|
// Arithmetic — direct binary ops
|
||||||
|
Add(NodeId, NodeId),
|
||||||
|
Sub(NodeId, NodeId),
|
||||||
|
Mul(NodeId, NodeId),
|
||||||
|
Div(NodeId, NodeId),
|
||||||
|
Neg(NodeId),
|
||||||
|
Abs(NodeId),
|
||||||
|
|
||||||
|
// Trig — CORDIC rotation mode
|
||||||
|
Sin(NodeId),
|
||||||
|
Cos(NodeId),
|
||||||
|
Tan(NodeId),
|
||||||
|
|
||||||
|
// Inverse trig
|
||||||
|
Asin(NodeId),
|
||||||
|
Acos(NodeId),
|
||||||
|
Atan(NodeId),
|
||||||
|
|
||||||
|
// Hyperbolic
|
||||||
|
Sinh(NodeId),
|
||||||
|
Cosh(NodeId),
|
||||||
|
Tanh(NodeId),
|
||||||
|
|
||||||
|
// Inverse hyperbolic
|
||||||
|
Asinh(NodeId),
|
||||||
|
Acosh(NodeId),
|
||||||
|
Atanh(NodeId),
|
||||||
|
|
||||||
|
// Transcendental
|
||||||
|
Sqrt(NodeId),
|
||||||
|
Exp(NodeId),
|
||||||
|
Ln(NodeId),
|
||||||
|
|
||||||
|
// Magnitude — CORDIC vectoring mode
|
||||||
|
// sqrt(a² + b²) in a single pass
|
||||||
|
Hypot(NodeId, NodeId),
|
||||||
|
|
||||||
|
// Angle — CORDIC vectoring mode
|
||||||
|
// atan2(y, x) in a single pass
|
||||||
|
Atan2(NodeId, NodeId),
|
||||||
|
|
||||||
|
// Comparison — direct binary
|
||||||
|
Min(NodeId, NodeId),
|
||||||
|
Max(NodeId, NodeId),
|
||||||
|
|
||||||
|
// Clamp(value, lo, hi) = max(lo, min(value, hi))
|
||||||
|
Clamp { val: NodeId, lo: NodeId, hi: NodeId },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrigGraph {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
nodes: Vec::new(),
|
||||||
|
output: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, op: TrigOp) -> NodeId {
|
||||||
|
let id = self.nodes.len() as NodeId;
|
||||||
|
self.nodes.push(op);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_output(&mut self, id: NodeId) {
|
||||||
|
self.output = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_count(&self) -> usize {
|
||||||
|
self.nodes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count how many of each CORDIC mode this graph requires.
|
||||||
|
pub fn cordic_cost(&self) -> CORDICCost {
|
||||||
|
let mut cost = CORDICCost::default();
|
||||||
|
for op in &self.nodes {
|
||||||
|
match op {
|
||||||
|
TrigOp::Sin(_) | TrigOp::Cos(_) | TrigOp::Tan(_) => cost.rotation += 1,
|
||||||
|
TrigOp::Asin(_) | TrigOp::Acos(_) | TrigOp::Atan(_) => cost.rotation += 1,
|
||||||
|
TrigOp::Sinh(_) | TrigOp::Cosh(_) | TrigOp::Tanh(_) => cost.rotation += 1,
|
||||||
|
TrigOp::Asinh(_) | TrigOp::Acosh(_) | TrigOp::Atanh(_) => cost.rotation += 1,
|
||||||
|
TrigOp::Hypot(_, _) | TrigOp::Atan2(_, _) => cost.vectoring += 1,
|
||||||
|
TrigOp::Mul(_, _) | TrigOp::Div(_, _) => cost.linear += 1,
|
||||||
|
TrigOp::Sqrt(_) | TrigOp::Exp(_) | TrigOp::Ln(_) => cost.linear += 1,
|
||||||
|
TrigOp::Add(_, _) | TrigOp::Sub(_, _) | TrigOp::Neg(_)
|
||||||
|
| TrigOp::Abs(_) | TrigOp::Min(_, _) | TrigOp::Max(_, _)
|
||||||
|
| TrigOp::Clamp { .. } => cost.binary += 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct CORDICCost {
|
||||||
|
pub rotation: u32,
|
||||||
|
pub vectoring: u32,
|
||||||
|
pub linear: u32,
|
||||||
|
pub binary: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CORDICCost {
|
||||||
|
pub fn total_cordic_passes(&self) -> u32 {
|
||||||
|
self.rotation + self.vectoring + self.linear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === TrigGraph binary serialization ===
|
||||||
|
// Format: b"TRIG" | u32 node_count | u32 output | [node...]
|
||||||
|
// Each node: u8 opcode | operands (NodeId = u32, Const = f64)
|
||||||
|
|
||||||
|
const TRIG_MAGIC: &[u8; 4] = b"TRIG";
|
||||||
|
|
||||||
|
const OP_INPUT_X: u8 = 0;
|
||||||
|
const OP_INPUT_Y: u8 = 1;
|
||||||
|
const OP_INPUT_Z: u8 = 2;
|
||||||
|
const OP_CONST: u8 = 3;
|
||||||
|
const OP_ADD: u8 = 4;
|
||||||
|
const OP_SUB: u8 = 5;
|
||||||
|
const OP_MUL: u8 = 6;
|
||||||
|
const OP_DIV: u8 = 16;
|
||||||
|
const OP_NEG: u8 = 7;
|
||||||
|
const OP_ABS: u8 = 8;
|
||||||
|
const OP_SIN: u8 = 9;
|
||||||
|
const OP_COS: u8 = 10;
|
||||||
|
const OP_HYPOT: u8 = 11;
|
||||||
|
const OP_ATAN2: u8 = 12;
|
||||||
|
const OP_MIN: u8 = 13;
|
||||||
|
const OP_MAX: u8 = 14;
|
||||||
|
const OP_CLAMP: u8 = 15;
|
||||||
|
const OP_TAN: u8 = 17;
|
||||||
|
const OP_ASIN: u8 = 18;
|
||||||
|
const OP_ACOS: u8 = 19;
|
||||||
|
const OP_ATAN: u8 = 20;
|
||||||
|
const OP_SINH: u8 = 21;
|
||||||
|
const OP_COSH: u8 = 22;
|
||||||
|
const OP_TANH: u8 = 23;
|
||||||
|
const OP_ASINH: u8 = 24;
|
||||||
|
const OP_ACOSH: u8 = 25;
|
||||||
|
const OP_ATANH: u8 = 26;
|
||||||
|
const OP_SQRT: u8 = 27;
|
||||||
|
const OP_EXP: u8 = 28;
|
||||||
|
const OP_LN: u8 = 29;
|
||||||
|
|
||||||
|
impl TrigGraph {
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.extend_from_slice(TRIG_MAGIC);
|
||||||
|
buf.extend_from_slice(&(self.nodes.len() as u32).to_le_bytes());
|
||||||
|
buf.extend_from_slice(&self.output.to_le_bytes());
|
||||||
|
|
||||||
|
for op in &self.nodes {
|
||||||
|
match op {
|
||||||
|
TrigOp::InputX => buf.push(OP_INPUT_X),
|
||||||
|
TrigOp::InputY => buf.push(OP_INPUT_Y),
|
||||||
|
TrigOp::InputZ => buf.push(OP_INPUT_Z),
|
||||||
|
TrigOp::Const(v) => {
|
||||||
|
buf.push(OP_CONST);
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
TrigOp::Add(a, b) => { buf.push(OP_ADD); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Sub(a, b) => { buf.push(OP_SUB); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Mul(a, b) => { buf.push(OP_MUL); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Div(a, b) => { buf.push(OP_DIV); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Neg(a) => { buf.push(OP_NEG); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Abs(a) => { buf.push(OP_ABS); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Sin(a) => { buf.push(OP_SIN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Cos(a) => { buf.push(OP_COS); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Tan(a) => { buf.push(OP_TAN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Asin(a) => { buf.push(OP_ASIN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Acos(a) => { buf.push(OP_ACOS); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Atan(a) => { buf.push(OP_ATAN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Sinh(a) => { buf.push(OP_SINH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Cosh(a) => { buf.push(OP_COSH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Tanh(a) => { buf.push(OP_TANH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Asinh(a) => { buf.push(OP_ASINH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Acosh(a) => { buf.push(OP_ACOSH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Atanh(a) => { buf.push(OP_ATANH); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Sqrt(a) => { buf.push(OP_SQRT); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Exp(a) => { buf.push(OP_EXP); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Ln(a) => { buf.push(OP_LN); buf.extend_from_slice(&a.to_le_bytes()); }
|
||||||
|
TrigOp::Hypot(a, b) => { buf.push(OP_HYPOT); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Atan2(a, b) => { buf.push(OP_ATAN2); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Min(a, b) => { buf.push(OP_MIN); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Max(a, b) => { buf.push(OP_MAX); push_pair(&mut buf, *a, *b); }
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
buf.push(OP_CLAMP);
|
||||||
|
buf.extend_from_slice(&val.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&lo.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&hi.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(data: &[u8]) -> Option<Self> {
|
||||||
|
if data.len() < 12 || &data[0..4] != TRIG_MAGIC {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let node_count = u32::from_le_bytes(data[4..8].try_into().ok()?) as usize;
|
||||||
|
let output = u32::from_le_bytes(data[8..12].try_into().ok()?);
|
||||||
|
let mut pos = 12;
|
||||||
|
let mut nodes = Vec::with_capacity(node_count);
|
||||||
|
|
||||||
|
for _ in 0..node_count {
|
||||||
|
if pos >= data.len() { return None; }
|
||||||
|
let opcode = data[pos];
|
||||||
|
pos += 1;
|
||||||
|
let op = match opcode {
|
||||||
|
OP_INPUT_X => TrigOp::InputX,
|
||||||
|
OP_INPUT_Y => TrigOp::InputY,
|
||||||
|
OP_INPUT_Z => TrigOp::InputZ,
|
||||||
|
OP_CONST => {
|
||||||
|
let v = f64::from_le_bytes(data[pos..pos+8].try_into().ok()?);
|
||||||
|
pos += 8;
|
||||||
|
TrigOp::Const(v)
|
||||||
|
}
|
||||||
|
OP_ADD => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Add(a, b) }
|
||||||
|
OP_SUB => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Sub(a, b) }
|
||||||
|
OP_MUL => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Mul(a, b) }
|
||||||
|
OP_DIV => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Div(a, b) }
|
||||||
|
OP_NEG => { let a = read_u32(data, &mut pos)?; TrigOp::Neg(a) }
|
||||||
|
OP_ABS => { let a = read_u32(data, &mut pos)?; TrigOp::Abs(a) }
|
||||||
|
OP_SIN => { let a = read_u32(data, &mut pos)?; TrigOp::Sin(a) }
|
||||||
|
OP_COS => { let a = read_u32(data, &mut pos)?; TrigOp::Cos(a) }
|
||||||
|
OP_TAN => { let a = read_u32(data, &mut pos)?; TrigOp::Tan(a) }
|
||||||
|
OP_ASIN => { let a = read_u32(data, &mut pos)?; TrigOp::Asin(a) }
|
||||||
|
OP_ACOS => { let a = read_u32(data, &mut pos)?; TrigOp::Acos(a) }
|
||||||
|
OP_ATAN => { let a = read_u32(data, &mut pos)?; TrigOp::Atan(a) }
|
||||||
|
OP_SINH => { let a = read_u32(data, &mut pos)?; TrigOp::Sinh(a) }
|
||||||
|
OP_COSH => { let a = read_u32(data, &mut pos)?; TrigOp::Cosh(a) }
|
||||||
|
OP_TANH => { let a = read_u32(data, &mut pos)?; TrigOp::Tanh(a) }
|
||||||
|
OP_ASINH => { let a = read_u32(data, &mut pos)?; TrigOp::Asinh(a) }
|
||||||
|
OP_ACOSH => { let a = read_u32(data, &mut pos)?; TrigOp::Acosh(a) }
|
||||||
|
OP_ATANH => { let a = read_u32(data, &mut pos)?; TrigOp::Atanh(a) }
|
||||||
|
OP_SQRT => { let a = read_u32(data, &mut pos)?; TrigOp::Sqrt(a) }
|
||||||
|
OP_EXP => { let a = read_u32(data, &mut pos)?; TrigOp::Exp(a) }
|
||||||
|
OP_LN => { let a = read_u32(data, &mut pos)?; TrigOp::Ln(a) }
|
||||||
|
OP_HYPOT => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Hypot(a, b) }
|
||||||
|
OP_ATAN2 => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Atan2(a, b) }
|
||||||
|
OP_MIN => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Min(a, b) }
|
||||||
|
OP_MAX => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Max(a, b) }
|
||||||
|
OP_CLAMP => {
|
||||||
|
let val = read_u32(data, &mut pos)?;
|
||||||
|
let lo = read_u32(data, &mut pos)?;
|
||||||
|
let hi = read_u32(data, &mut pos)?;
|
||||||
|
TrigOp::Clamp { val, lo, hi }
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
nodes.push(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(TrigGraph { nodes, output })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_pair(buf: &mut Vec<u8>, a: NodeId, b: NodeId) {
|
||||||
|
buf.extend_from_slice(&a.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&b.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], pos: &mut usize) -> Option<u32> {
|
||||||
|
let v = u32::from_le_bytes(data[*pos..*pos+4].try_into().ok()?);
|
||||||
|
*pos += 4;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pair(data: &[u8], pos: &mut usize) -> Option<(NodeId, NodeId)> {
|
||||||
|
let a = read_u32(data, pos)?;
|
||||||
|
let b = read_u32(data, pos)?;
|
||||||
|
Some((a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trig_roundtrip() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let y = g.push(TrigOp::InputY);
|
||||||
|
let z = g.push(TrigOp::InputZ);
|
||||||
|
let r = g.push(TrigOp::Const(3.0));
|
||||||
|
let xy = g.push(TrigOp::Hypot(x, y));
|
||||||
|
let mag = g.push(TrigOp::Hypot(xy, z));
|
||||||
|
let out = g.push(TrigOp::Sub(mag, r));
|
||||||
|
g.set_output(out);
|
||||||
|
|
||||||
|
let bytes = g.to_bytes();
|
||||||
|
let g2 = TrigGraph::from_bytes(&bytes).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(g.nodes.len(), g2.nodes.len());
|
||||||
|
assert_eq!(g.output, g2.output);
|
||||||
|
let v1 = evaluate(&g, 1.0, 2.0, 2.0);
|
||||||
|
let v2 = evaluate(&g2, 1.0, 2.0, 2.0);
|
||||||
|
assert!((v1 - v2).abs() < 1e-15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
//! Trigonometric intermediate representation for SDF geometry.
|
||||||
|
//!
|
||||||
|
//! `TrigGraph` is the universal IR that all geometry compiles to — a DAG of
|
||||||
|
//! pure trigonometric, arithmetic, and comparison operations. From here it
|
||||||
|
//! can be evaluated as f64, compiled to WGSL shaders, or lowered to CORDIC
|
||||||
|
//! shift-and-add instructions.
|
||||||
|
//!
|
||||||
|
//! # Key types
|
||||||
|
//! - [`TrigGraph`] — the DAG container
|
||||||
|
//! - [`TrigOp`] — individual operations (sin, cos, add, min, ...)
|
||||||
|
//! - [`NodeId`] — u32 index into the graph
|
||||||
|
//! - [`SdfBuilder`](lower::SdfBuilder) — language-agnostic scene construction API
|
||||||
|
|
||||||
|
pub mod ir;
|
||||||
|
pub mod eval;
|
||||||
|
pub mod lower;
|
||||||
|
pub mod traverse;
|
||||||
|
pub mod optimize;
|
||||||
|
pub mod parallel;
|
||||||
|
|
||||||
|
pub use ir::{TrigGraph, TrigOp, NodeId};
|
||||||
|
pub use traverse::{TraversalMode, EvalBounds, EvalResult};
|
||||||
|
pub use optimize::optimize;
|
||||||
|
pub use parallel::{ParallelClass, Subtree, find_independent_subtrees};
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
use crate::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
|
||||||
|
/// A 3D point in the trig graph — three node IDs for x, y, z.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Point3 {
|
||||||
|
x: NodeId,
|
||||||
|
y: NodeId,
|
||||||
|
z: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lower any SDF-like description into a TrigGraph.
|
||||||
|
///
|
||||||
|
/// Builder API for constructing SDF operations from trig primitives.
|
||||||
|
/// Geometric concepts (sphere, box, rotate, etc.) decompose into
|
||||||
|
/// TrigOp nodes internally.
|
||||||
|
pub struct SdfBuilder {
|
||||||
|
pub graph: TrigGraph,
|
||||||
|
root_point: Point3,
|
||||||
|
zero: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SdfBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut graph = TrigGraph::new();
|
||||||
|
let px = graph.push(TrigOp::InputX);
|
||||||
|
let py = graph.push(TrigOp::InputY);
|
||||||
|
let pz = graph.push(TrigOp::InputZ);
|
||||||
|
let zero = graph.push(TrigOp::Const(0.0));
|
||||||
|
|
||||||
|
SdfBuilder {
|
||||||
|
graph,
|
||||||
|
root_point: Point3 { x: px, y: py, z: pz },
|
||||||
|
zero,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(mut self, output: NodeId) -> TrigGraph {
|
||||||
|
self.graph.set_output(output);
|
||||||
|
self.graph
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn constant(&mut self, val: f64) -> NodeId {
|
||||||
|
self.graph.push(TrigOp::Const(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Primitives ===
|
||||||
|
|
||||||
|
/// Sphere SDF: length(p) - r
|
||||||
|
/// Two CORDIC vectoring passes (hypot3) + one subtraction.
|
||||||
|
pub fn sphere(&mut self, point: Point3, radius: f64) -> NodeId {
|
||||||
|
let mag = self.length3(point);
|
||||||
|
let r = self.graph.push(TrigOp::Const(radius));
|
||||||
|
self.graph.push(TrigOp::Sub(mag, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Box SDF: length(max(abs(p) - h, 0)) + min(max_component(q), 0)
|
||||||
|
/// where q = abs(p) - h
|
||||||
|
pub fn box_sdf(&mut self, point: Point3, half_extents: [f64; 3]) -> NodeId {
|
||||||
|
let hx = self.graph.push(TrigOp::Const(half_extents[0]));
|
||||||
|
let hy = self.graph.push(TrigOp::Const(half_extents[1]));
|
||||||
|
let hz = self.graph.push(TrigOp::Const(half_extents[2]));
|
||||||
|
|
||||||
|
// q = abs(p) - h
|
||||||
|
let ax = self.graph.push(TrigOp::Abs(point.x));
|
||||||
|
let ay = self.graph.push(TrigOp::Abs(point.y));
|
||||||
|
let az = self.graph.push(TrigOp::Abs(point.z));
|
||||||
|
let qx = self.graph.push(TrigOp::Sub(ax, hx));
|
||||||
|
let qy = self.graph.push(TrigOp::Sub(ay, hy));
|
||||||
|
let qz = self.graph.push(TrigOp::Sub(az, hz));
|
||||||
|
|
||||||
|
// length(max(q, 0))
|
||||||
|
let zero = self.zero;
|
||||||
|
let cx = self.graph.push(TrigOp::Max(qx, zero));
|
||||||
|
let cy = self.graph.push(TrigOp::Max(qy, zero));
|
||||||
|
let cz = self.graph.push(TrigOp::Max(qz, zero));
|
||||||
|
let outer = self.length3_nodes(cx, cy, cz);
|
||||||
|
|
||||||
|
// min(max(qx, max(qy, qz)), 0)
|
||||||
|
let m1 = self.graph.push(TrigOp::Max(qy, qz));
|
||||||
|
let m2 = self.graph.push(TrigOp::Max(qx, m1));
|
||||||
|
let inner = self.graph.push(TrigOp::Min(m2, zero));
|
||||||
|
|
||||||
|
self.graph.push(TrigOp::Add(outer, inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cylinder SDF (Z-axis aligned, centered at origin).
|
||||||
|
/// d = vec2(length(p.xy) - r, abs(p.z) - h/2)
|
||||||
|
/// return length(max(d, 0)) + min(max(d.x, d.y), 0)
|
||||||
|
pub fn cylinder(&mut self, point: Point3, radius: f64, height: f64) -> NodeId {
|
||||||
|
let r = self.graph.push(TrigOp::Const(radius));
|
||||||
|
let hh = self.graph.push(TrigOp::Const(height / 2.0));
|
||||||
|
let zero = self.zero;
|
||||||
|
|
||||||
|
let len_xy = self.graph.push(TrigOp::Hypot(point.x, point.y));
|
||||||
|
let dx = self.graph.push(TrigOp::Sub(len_xy, r));
|
||||||
|
let az = self.graph.push(TrigOp::Abs(point.z));
|
||||||
|
let dy = self.graph.push(TrigOp::Sub(az, hh));
|
||||||
|
|
||||||
|
let cx = self.graph.push(TrigOp::Max(dx, zero));
|
||||||
|
let cy = self.graph.push(TrigOp::Max(dy, zero));
|
||||||
|
let outer = self.graph.push(TrigOp::Hypot(cx, cy));
|
||||||
|
|
||||||
|
let inner_max = self.graph.push(TrigOp::Max(dx, dy));
|
||||||
|
let inner = self.graph.push(TrigOp::Min(inner_max, zero));
|
||||||
|
|
||||||
|
self.graph.push(TrigOp::Add(outer, inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Transforms ===
|
||||||
|
|
||||||
|
/// Translate: evaluate child at (p - offset).
|
||||||
|
pub fn translate(&mut self, point: Point3, offset: [f64; 3]) -> Point3 {
|
||||||
|
let ox = self.graph.push(TrigOp::Const(offset[0]));
|
||||||
|
let oy = self.graph.push(TrigOp::Const(offset[1]));
|
||||||
|
let oz = self.graph.push(TrigOp::Const(offset[2]));
|
||||||
|
Point3 {
|
||||||
|
x: self.graph.push(TrigOp::Sub(point.x, ox)),
|
||||||
|
y: self.graph.push(TrigOp::Sub(point.y, oy)),
|
||||||
|
z: self.graph.push(TrigOp::Sub(point.z, oz)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotate around X axis by angle (radians).
|
||||||
|
/// p' = (px, py*cos + pz*sin, -py*sin + pz*cos)
|
||||||
|
/// Cost: 1 CORDIC rotation pass (sin/cos fused) + 4 linear mul passes.
|
||||||
|
/// CORDIC rotation mode handles the full 2D rotation natively.
|
||||||
|
pub fn rotate_x(&mut self, point: Point3, angle_rad: f64) -> Point3 {
|
||||||
|
let (ny, nz) = self.rotate_2d(point.y, point.z, angle_rad);
|
||||||
|
Point3 { x: point.x, y: ny, z: nz }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_y(&mut self, point: Point3, angle_rad: f64) -> Point3 {
|
||||||
|
// Y rotation: (x*cos + z*sin, y, -x*sin + z*cos)
|
||||||
|
// = rotate(-angle) in XZ plane
|
||||||
|
let (nz, nx) = self.rotate_2d(point.z, point.x, angle_rad);
|
||||||
|
Point3 { x: nx, y: point.y, z: nz }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_z(&mut self, point: Point3, angle_rad: f64) -> Point3 {
|
||||||
|
let (nx, ny) = self.rotate_2d(point.x, point.y, angle_rad);
|
||||||
|
Point3 { x: nx, y: ny, z: point.z }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale: evaluate child at (p / factor), multiply result by min(factor).
|
||||||
|
pub fn scale(&mut self, point: Point3, factor: [f64; 3]) -> (Point3, f64) {
|
||||||
|
let fx = self.graph.push(TrigOp::Const(1.0 / factor[0]));
|
||||||
|
let fy = self.graph.push(TrigOp::Const(1.0 / factor[1]));
|
||||||
|
let fz = self.graph.push(TrigOp::Const(1.0 / factor[2]));
|
||||||
|
let sp = Point3 {
|
||||||
|
x: self.graph.push(TrigOp::Mul(point.x, fx)),
|
||||||
|
y: self.graph.push(TrigOp::Mul(point.y, fy)),
|
||||||
|
z: self.graph.push(TrigOp::Mul(point.z, fz)),
|
||||||
|
};
|
||||||
|
let min_scale = factor[0].abs().min(factor[1].abs()).min(factor[2].abs());
|
||||||
|
(sp, min_scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale_distance(&mut self, dist: NodeId, min_scale: f64) -> NodeId {
|
||||||
|
let s = self.graph.push(TrigOp::Const(min_scale));
|
||||||
|
self.graph.push(TrigOp::Mul(dist, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Boolean operations ===
|
||||||
|
|
||||||
|
pub fn union(&mut self, a: NodeId, b: NodeId) -> NodeId {
|
||||||
|
self.graph.push(TrigOp::Min(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersection(&mut self, a: NodeId, b: NodeId) -> NodeId {
|
||||||
|
self.graph.push(TrigOp::Max(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn difference(&mut self, a: NodeId, b: NodeId) -> NodeId {
|
||||||
|
let neg_b = self.graph.push(TrigOp::Neg(b));
|
||||||
|
self.graph.push(TrigOp::Max(a, neg_b))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn smooth_union(&mut self, a: NodeId, b: NodeId, k: f64) -> NodeId {
|
||||||
|
// Polynomial smooth min:
|
||||||
|
// h = clamp(0.5 + 0.5*(b-a)/k, 0, 1)
|
||||||
|
// result = mix(b, a, h) - k*h*(1-h)
|
||||||
|
let k_const = self.graph.push(TrigOp::Const(k));
|
||||||
|
let half = self.graph.push(TrigOp::Const(0.5));
|
||||||
|
let one = self.graph.push(TrigOp::Const(1.0));
|
||||||
|
let zero = self.zero;
|
||||||
|
|
||||||
|
let diff = self.graph.push(TrigOp::Sub(b, a));
|
||||||
|
let inv_k = self.graph.push(TrigOp::Const(1.0 / k));
|
||||||
|
let div = self.graph.push(TrigOp::Mul(diff, inv_k));
|
||||||
|
let scaled = self.graph.push(TrigOp::Mul(div, half));
|
||||||
|
let shifted = self.graph.push(TrigOp::Add(half, scaled));
|
||||||
|
let h = self.graph.push(TrigOp::Clamp { val: shifted, lo: zero, hi: one });
|
||||||
|
|
||||||
|
// mix(b, a, h) = b + h*(a-b) = b*(1-h) + a*h
|
||||||
|
let one_minus_h = self.graph.push(TrigOp::Sub(one, h));
|
||||||
|
let term_b = self.graph.push(TrigOp::Mul(b, one_minus_h));
|
||||||
|
let term_a = self.graph.push(TrigOp::Mul(a, h));
|
||||||
|
let mixed = self.graph.push(TrigOp::Add(term_b, term_a));
|
||||||
|
|
||||||
|
// k*h*(1-h)
|
||||||
|
let kh = self.graph.push(TrigOp::Mul(k_const, h));
|
||||||
|
let correction = self.graph.push(TrigOp::Mul(kh, one_minus_h));
|
||||||
|
|
||||||
|
self.graph.push(TrigOp::Sub(mixed, correction))
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Internal helpers ===
|
||||||
|
|
||||||
|
/// 3D vector magnitude: sqrt(x² + y² + z²)
|
||||||
|
/// = hypot(hypot(x, y), z) — two CORDIC vectoring passes.
|
||||||
|
fn length3(&mut self, p: Point3) -> NodeId {
|
||||||
|
let xy = self.graph.push(TrigOp::Hypot(p.x, p.y));
|
||||||
|
self.graph.push(TrigOp::Hypot(xy, p.z))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn length3_nodes(&mut self, x: NodeId, y: NodeId, z: NodeId) -> NodeId {
|
||||||
|
let xy = self.graph.push(TrigOp::Hypot(x, y));
|
||||||
|
self.graph.push(TrigOp::Hypot(xy, z))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2D rotation via decomposed trig.
|
||||||
|
/// (x', y') = (x*cos(θ) + y*sin(θ), -x*sin(θ) + y*cos(θ))
|
||||||
|
///
|
||||||
|
/// Single CORDIC rotation pass — the compiler recognizes the
|
||||||
|
/// sin/cos pair sharing the same angle input and fuses them.
|
||||||
|
fn rotate_2d(&mut self, a: NodeId, b: NodeId, angle_rad: f64) -> (NodeId, NodeId) {
|
||||||
|
let theta = self.graph.push(TrigOp::Const(angle_rad));
|
||||||
|
let s = self.graph.push(TrigOp::Sin(theta));
|
||||||
|
let c = self.graph.push(TrigOp::Cos(theta));
|
||||||
|
|
||||||
|
// a' = a*c + b*s
|
||||||
|
let ac = self.graph.push(TrigOp::Mul(a, c));
|
||||||
|
let bs = self.graph.push(TrigOp::Mul(b, s));
|
||||||
|
let a_new = self.graph.push(TrigOp::Add(ac, bs));
|
||||||
|
|
||||||
|
// b' = -a*s + b*c
|
||||||
|
let as_ = self.graph.push(TrigOp::Mul(a, s));
|
||||||
|
let bc = self.graph.push(TrigOp::Mul(b, c));
|
||||||
|
let b_new = self.graph.push(TrigOp::Sub(bc, as_));
|
||||||
|
|
||||||
|
(a_new, b_new)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the root evaluation point.
|
||||||
|
pub fn root_point(&self) -> Point3 {
|
||||||
|
self.root_point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public interface for the Point3 type used in lowering.
|
||||||
|
impl Point3 {
|
||||||
|
pub fn new(x: NodeId, y: NodeId, z: NodeId) -> Self {
|
||||||
|
Self { x, y, z }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export Point3 for use by other crates
|
||||||
|
pub type TrigPoint3 = Point3;
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
use crate::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
/// Optimize a TrigGraph in-place.
|
||||||
|
///
|
||||||
|
/// Passes (in order):
|
||||||
|
/// 1. Constant folding — evaluate pure-constant subexpressions
|
||||||
|
/// 2. Sin/cos pair fusion — shared-angle pairs reduce to one CORDIC pass
|
||||||
|
/// 3. Dead node elimination — remove nodes not reachable from output
|
||||||
|
pub fn optimize(graph: &mut TrigGraph) {
|
||||||
|
constant_fold(graph);
|
||||||
|
fuse_sincos_pairs(graph);
|
||||||
|
eliminate_dead_nodes(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Pass 1: Constant folding ===
|
||||||
|
|
||||||
|
fn constant_fold(graph: &mut TrigGraph) {
|
||||||
|
let mut values: Vec<Option<f64>> = Vec::with_capacity(graph.nodes.len());
|
||||||
|
let mut replacements: Vec<(usize, f64)> = Vec::new();
|
||||||
|
|
||||||
|
for (i, op) in graph.nodes.iter().enumerate() {
|
||||||
|
let val = match op {
|
||||||
|
TrigOp::Const(c) => Some(*c),
|
||||||
|
TrigOp::Add(a, b) => fold2(&values, *a, *b, |x, y| x + y),
|
||||||
|
TrigOp::Sub(a, b) => fold2(&values, *a, *b, |x, y| x - y),
|
||||||
|
TrigOp::Mul(a, b) => fold2(&values, *a, *b, |x, y| x * y),
|
||||||
|
TrigOp::Div(a, b) => fold2(&values, *a, *b, |x, y| x / y),
|
||||||
|
TrigOp::Min(a, b) => fold2(&values, *a, *b, f64::min),
|
||||||
|
TrigOp::Max(a, b) => fold2(&values, *a, *b, f64::max),
|
||||||
|
TrigOp::Neg(a) => fold1(&values, *a, |x| -x),
|
||||||
|
TrigOp::Abs(a) => fold1(&values, *a, f64::abs),
|
||||||
|
TrigOp::Sin(a) => fold1(&values, *a, f64::sin),
|
||||||
|
TrigOp::Cos(a) => fold1(&values, *a, f64::cos),
|
||||||
|
TrigOp::Tan(a) => fold1(&values, *a, f64::tan),
|
||||||
|
TrigOp::Asin(a) => fold1(&values, *a, f64::asin),
|
||||||
|
TrigOp::Acos(a) => fold1(&values, *a, f64::acos),
|
||||||
|
TrigOp::Atan(a) => fold1(&values, *a, f64::atan),
|
||||||
|
TrigOp::Sinh(a) => fold1(&values, *a, f64::sinh),
|
||||||
|
TrigOp::Cosh(a) => fold1(&values, *a, f64::cosh),
|
||||||
|
TrigOp::Tanh(a) => fold1(&values, *a, f64::tanh),
|
||||||
|
TrigOp::Asinh(a) => fold1(&values, *a, f64::asinh),
|
||||||
|
TrigOp::Acosh(a) => fold1(&values, *a, f64::acosh),
|
||||||
|
TrigOp::Atanh(a) => fold1(&values, *a, f64::atanh),
|
||||||
|
TrigOp::Sqrt(a) => fold1(&values, *a, f64::sqrt),
|
||||||
|
TrigOp::Exp(a) => fold1(&values, *a, f64::exp),
|
||||||
|
TrigOp::Ln(a) => fold1(&values, *a, f64::ln),
|
||||||
|
TrigOp::Hypot(a, b) => fold2(&values, *a, *b, f64::hypot),
|
||||||
|
TrigOp::Atan2(a, b) => fold2(&values, *a, *b, f64::atan2),
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
match (get_const(&values, *val), get_const(&values, *lo), get_const(&values, *hi)) {
|
||||||
|
(Some(v), Some(l), Some(h)) => Some(v.clamp(l, h)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(c) = val {
|
||||||
|
if !matches!(op, TrigOp::Const(_)) {
|
||||||
|
replacements.push((i, c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, c) in replacements {
|
||||||
|
graph.nodes[i] = TrigOp::Const(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_const(values: &[Option<f64>], id: NodeId) -> Option<f64> {
|
||||||
|
values.get(id as usize).and_then(|v| *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold1(values: &[Option<f64>], a: NodeId, f: fn(f64) -> f64) -> Option<f64> {
|
||||||
|
get_const(values, a).map(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold2(values: &[Option<f64>], a: NodeId, b: NodeId, f: fn(f64, f64) -> f64) -> Option<f64> {
|
||||||
|
match (get_const(values, a), get_const(values, b)) {
|
||||||
|
(Some(va), Some(vb)) => Some(f(va, vb)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Pass 2: Sin/cos pair fusion ===
|
||||||
|
//
|
||||||
|
// When Sin(θ) and Cos(θ) share the same angle input, a CORDIC
|
||||||
|
// rotation pass produces both simultaneously. Mark the pair so
|
||||||
|
// the compiler can fuse them into one pass.
|
||||||
|
//
|
||||||
|
// Implementation: replace the second occurrence with a SinCos marker
|
||||||
|
// by rewriting it to reference the first. Since we can't add new
|
||||||
|
// op variants without changing the enum, we instead just track pairs
|
||||||
|
// for the cost model and leave the graph structure intact. The CORDIC
|
||||||
|
// compiler already recognizes shared-angle patterns.
|
||||||
|
|
||||||
|
fn fuse_sincos_pairs(graph: &mut TrigGraph) {
|
||||||
|
let mut sin_of: HashMap<NodeId, NodeId> = HashMap::new();
|
||||||
|
let mut cos_of: HashMap<NodeId, NodeId> = HashMap::new();
|
||||||
|
|
||||||
|
for (i, op) in graph.nodes.iter().enumerate() {
|
||||||
|
match op {
|
||||||
|
TrigOp::Sin(a) => { sin_of.insert(*a, i as NodeId); }
|
||||||
|
TrigOp::Cos(a) => { cos_of.insert(*a, i as NodeId); }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count fused pairs (both sin and cos of same angle exist)
|
||||||
|
let _fused: Vec<(NodeId, NodeId)> = sin_of.iter()
|
||||||
|
.filter_map(|(angle, sin_id)| {
|
||||||
|
cos_of.get(angle).map(|cos_id| (*sin_id, *cos_id))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// The CORDIC compiler handles fusion; this pass is a no-op for now
|
||||||
|
// but validates that pairs exist. Future: rewrite to a fused op.
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Pass 3: Dead node elimination ===
|
||||||
|
|
||||||
|
fn eliminate_dead_nodes(graph: &mut TrigGraph) {
|
||||||
|
let reachable = find_reachable(graph);
|
||||||
|
|
||||||
|
if reachable.len() == graph.nodes.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build remapping: old index → new index
|
||||||
|
let mut remap: Vec<NodeId> = vec![0; graph.nodes.len()];
|
||||||
|
let mut new_nodes: Vec<TrigOp> = Vec::with_capacity(reachable.len());
|
||||||
|
|
||||||
|
for i in 0..graph.nodes.len() {
|
||||||
|
if reachable.contains(&(i as NodeId)) {
|
||||||
|
remap[i] = new_nodes.len() as NodeId;
|
||||||
|
new_nodes.push(graph.nodes[i].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite references in the compacted graph
|
||||||
|
for op in new_nodes.iter_mut() {
|
||||||
|
rewrite_refs(op, &remap);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.output = remap[graph.output as usize];
|
||||||
|
graph.nodes = new_nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_reachable(graph: &TrigGraph) -> HashSet<NodeId> {
|
||||||
|
let mut reachable = HashSet::new();
|
||||||
|
let mut stack = vec![graph.output];
|
||||||
|
|
||||||
|
while let Some(id) = stack.pop() {
|
||||||
|
if !reachable.insert(id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match &graph.nodes[id as usize] {
|
||||||
|
TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b) | TrigOp::Div(a, b)
|
||||||
|
| TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
|
||||||
|
| TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
|
||||||
|
stack.push(*a);
|
||||||
|
stack.push(*b);
|
||||||
|
}
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a)
|
||||||
|
| TrigOp::Sin(a) | TrigOp::Cos(a) | TrigOp::Tan(a)
|
||||||
|
| TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
stack.push(*a);
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
stack.push(*val);
|
||||||
|
stack.push(*lo);
|
||||||
|
stack.push(*hi);
|
||||||
|
}
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ | TrigOp::Const(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reachable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_refs(op: &mut TrigOp, remap: &[NodeId]) {
|
||||||
|
match op {
|
||||||
|
TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b) | TrigOp::Div(a, b)
|
||||||
|
| TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
|
||||||
|
| TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
|
||||||
|
*a = remap[*a as usize];
|
||||||
|
*b = remap[*b as usize];
|
||||||
|
}
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a)
|
||||||
|
| TrigOp::Sin(a) | TrigOp::Cos(a) | TrigOp::Tan(a)
|
||||||
|
| TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
*a = remap[*a as usize];
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
*val = remap[*val as usize];
|
||||||
|
*lo = remap[*lo as usize];
|
||||||
|
*hi = remap[*hi as usize];
|
||||||
|
}
|
||||||
|
TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ | TrigOp::Const(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constant_fold_simple() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let a = g.push(TrigOp::Const(3.0));
|
||||||
|
let b = g.push(TrigOp::Const(4.0));
|
||||||
|
let c = g.push(TrigOp::Add(a, b));
|
||||||
|
g.set_output(c);
|
||||||
|
|
||||||
|
optimize(&mut g);
|
||||||
|
|
||||||
|
// After folding, the output should be a single constant
|
||||||
|
assert!(matches!(g.nodes.last(), Some(TrigOp::Const(v)) if (*v - 7.0).abs() < 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dead_node_elimination() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let _dead = g.push(TrigOp::Const(999.0)); // unreachable
|
||||||
|
let two = g.push(TrigOp::Const(2.0));
|
||||||
|
let out = g.push(TrigOp::Mul(x, two));
|
||||||
|
g.set_output(out);
|
||||||
|
|
||||||
|
let original_count = g.nodes.len();
|
||||||
|
optimize(&mut g);
|
||||||
|
|
||||||
|
assert!(g.nodes.len() < original_count, "dead node should be removed");
|
||||||
|
let val = evaluate(&g, 5.0, 0.0, 0.0);
|
||||||
|
assert!((val - 10.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optimize_preserves_semantics() {
|
||||||
|
// sphere(2) - x^2 + y^2 + z^2 - 4 via hypot chain
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let y = g.push(TrigOp::InputY);
|
||||||
|
let z = g.push(TrigOp::InputZ);
|
||||||
|
let xy = g.push(TrigOp::Hypot(x, y));
|
||||||
|
let mag = g.push(TrigOp::Hypot(xy, z));
|
||||||
|
let r = g.push(TrigOp::Const(2.0));
|
||||||
|
let out = g.push(TrigOp::Sub(mag, r));
|
||||||
|
g.set_output(out);
|
||||||
|
|
||||||
|
let before = evaluate(&g, 1.0, 1.0, 1.0);
|
||||||
|
optimize(&mut g);
|
||||||
|
let after = evaluate(&g, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
assert!((before - after).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fold_nested_constants() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let a = g.push(TrigOp::Const(std::f64::consts::FRAC_PI_2));
|
||||||
|
let s = g.push(TrigOp::Sin(a));
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let out = g.push(TrigOp::Mul(s, x));
|
||||||
|
g.set_output(out);
|
||||||
|
|
||||||
|
optimize(&mut g);
|
||||||
|
|
||||||
|
// sin(π/2) = 1.0, so result = 1.0 * x = x
|
||||||
|
let val = evaluate(&g, 7.0, 0.0, 0.0);
|
||||||
|
assert!((val - 7.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
use crate::ir::{NodeId, TrigGraph, TrigOp};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Parallelism classification for a subexpression.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ParallelClass {
|
||||||
|
/// Commutative, associative: union, add. Branches evaluate independently.
|
||||||
|
Additive,
|
||||||
|
/// Same operation applied N times: scale, repetition.
|
||||||
|
Multiplicative,
|
||||||
|
/// difference, intersection. Parallelizable if operands are independent.
|
||||||
|
Divisive,
|
||||||
|
/// True data dependency — must evaluate sequentially.
|
||||||
|
Sequential,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An independent subtree identified in the DAG.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Subtree {
|
||||||
|
pub root: NodeId,
|
||||||
|
pub nodes: HashSet<NodeId>,
|
||||||
|
pub inputs: HashSet<NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identify independent subtrees in a TrigGraph.
|
||||||
|
///
|
||||||
|
/// Two subtrees are independent if they share no intermediate nodes
|
||||||
|
/// (they may share inputs like InputX/Y/Z and constants).
|
||||||
|
pub fn find_independent_subtrees(graph: &TrigGraph) -> Vec<Subtree> {
|
||||||
|
let output = graph.output;
|
||||||
|
let root_op = &graph.nodes[output as usize];
|
||||||
|
|
||||||
|
// Only split at union/intersection/add (commutative operations)
|
||||||
|
match root_op {
|
||||||
|
TrigOp::Min(a, b) | TrigOp::Max(a, b)
|
||||||
|
| TrigOp::Add(a, b) => {
|
||||||
|
let left = collect_subtree(graph, *a);
|
||||||
|
let right = collect_subtree(graph, *b);
|
||||||
|
|
||||||
|
// Check independence: no shared non-input nodes
|
||||||
|
let shared: HashSet<NodeId> = left.nodes.intersection(&right.nodes)
|
||||||
|
.copied()
|
||||||
|
.filter(|&id| !is_shared_input(&graph.nodes[id as usize]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if shared.is_empty() {
|
||||||
|
return vec![left, right];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entire graph is one subtree
|
||||||
|
vec![collect_subtree(graph, output)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify the parallelism of the root operation.
|
||||||
|
pub fn classify_root(graph: &TrigGraph) -> ParallelClass {
|
||||||
|
match &graph.nodes[graph.output as usize] {
|
||||||
|
TrigOp::Min(_, _) | TrigOp::Add(_, _) => ParallelClass::Additive,
|
||||||
|
TrigOp::Mul(_, _) | TrigOp::Div(_, _) => ParallelClass::Multiplicative,
|
||||||
|
TrigOp::Max(_, _) | TrigOp::Sub(_, _) => ParallelClass::Divisive,
|
||||||
|
_ => ParallelClass::Sequential,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively count the maximum parallelism depth.
|
||||||
|
/// Returns how many independent branches exist at each level.
|
||||||
|
pub fn parallelism_depth(graph: &TrigGraph) -> Vec<usize> {
|
||||||
|
let mut levels: Vec<usize> = Vec::new();
|
||||||
|
count_branches(graph, graph.output, 0, &mut levels);
|
||||||
|
levels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_branches(graph: &TrigGraph, node: NodeId, depth: usize, levels: &mut Vec<usize>) {
|
||||||
|
while levels.len() <= depth {
|
||||||
|
levels.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
match &graph.nodes[node as usize] {
|
||||||
|
// Splittable operations — both children are independent branches
|
||||||
|
TrigOp::Min(a, b) | TrigOp::Add(a, b) => {
|
||||||
|
levels[depth] += 2;
|
||||||
|
count_branches(graph, *a, depth + 1, levels);
|
||||||
|
count_branches(graph, *b, depth + 1, levels);
|
||||||
|
}
|
||||||
|
// Non-commutative but still two-operand
|
||||||
|
TrigOp::Max(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b) | TrigOp::Div(a, b)
|
||||||
|
| TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b) => {
|
||||||
|
levels[depth] += 1;
|
||||||
|
count_branches(graph, *a, depth + 1, levels);
|
||||||
|
count_branches(graph, *b, depth + 1, levels);
|
||||||
|
}
|
||||||
|
// Single-operand
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a)
|
||||||
|
| TrigOp::Sin(a) | TrigOp::Cos(a) | TrigOp::Tan(a)
|
||||||
|
| TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
levels[depth] += 1;
|
||||||
|
count_branches(graph, *a, depth + 1, levels);
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
levels[depth] += 1;
|
||||||
|
count_branches(graph, *val, depth + 1, levels);
|
||||||
|
count_branches(graph, *lo, depth + 1, levels);
|
||||||
|
count_branches(graph, *hi, depth + 1, levels);
|
||||||
|
}
|
||||||
|
// Leaves
|
||||||
|
_ => {
|
||||||
|
levels[depth] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_subtree(graph: &TrigGraph, root: NodeId) -> Subtree {
|
||||||
|
let mut nodes = HashSet::new();
|
||||||
|
let mut inputs = HashSet::new();
|
||||||
|
let mut stack = vec![root];
|
||||||
|
|
||||||
|
while let Some(id) = stack.pop() {
|
||||||
|
if !nodes.insert(id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let op = &graph.nodes[id as usize];
|
||||||
|
if is_shared_input(op) {
|
||||||
|
inputs.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
match op {
|
||||||
|
TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b) | TrigOp::Div(a, b)
|
||||||
|
| TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
|
||||||
|
| TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
|
||||||
|
stack.push(*a);
|
||||||
|
stack.push(*b);
|
||||||
|
}
|
||||||
|
TrigOp::Neg(a) | TrigOp::Abs(a)
|
||||||
|
| TrigOp::Sin(a) | TrigOp::Cos(a) | TrigOp::Tan(a)
|
||||||
|
| TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
|
||||||
|
| TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
|
||||||
|
| TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
|
||||||
|
| TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
|
||||||
|
stack.push(*a);
|
||||||
|
}
|
||||||
|
TrigOp::Clamp { val, lo, hi } => {
|
||||||
|
stack.push(*val);
|
||||||
|
stack.push(*lo);
|
||||||
|
stack.push(*hi);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Subtree { root, nodes, inputs }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_shared_input(op: &TrigOp) -> bool {
|
||||||
|
matches!(op, TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ | TrigOp::Const(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn union_splits_into_two() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let y = g.push(TrigOp::InputY);
|
||||||
|
let z = g.push(TrigOp::InputZ);
|
||||||
|
|
||||||
|
// sphere(1): hypot(hypot(x,y),z) - 1
|
||||||
|
let xy = g.push(TrigOp::Hypot(x, y));
|
||||||
|
let mag = g.push(TrigOp::Hypot(xy, z));
|
||||||
|
let r1 = g.push(TrigOp::Const(1.0));
|
||||||
|
let s1 = g.push(TrigOp::Sub(mag, r1));
|
||||||
|
|
||||||
|
// sphere(1) translated: same but with offset
|
||||||
|
let ox = g.push(TrigOp::Const(3.0));
|
||||||
|
let dx = g.push(TrigOp::Sub(x, ox));
|
||||||
|
let xy2 = g.push(TrigOp::Hypot(dx, y));
|
||||||
|
let mag2 = g.push(TrigOp::Hypot(xy2, z));
|
||||||
|
let r2 = g.push(TrigOp::Const(1.0));
|
||||||
|
let s2 = g.push(TrigOp::Sub(mag2, r2));
|
||||||
|
|
||||||
|
// union
|
||||||
|
let u = g.push(TrigOp::Min(s1, s2));
|
||||||
|
g.set_output(u);
|
||||||
|
|
||||||
|
let subtrees = find_independent_subtrees(&g);
|
||||||
|
assert_eq!(subtrees.len(), 2, "union of two spheres should split into 2 subtrees");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_union() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let a = g.push(TrigOp::Const(1.0));
|
||||||
|
let b = g.push(TrigOp::Const(2.0));
|
||||||
|
let u = g.push(TrigOp::Min(a, b));
|
||||||
|
g.set_output(u);
|
||||||
|
assert_eq!(classify_root(&g), ParallelClass::Additive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_difference() {
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let a = g.push(TrigOp::Const(1.0));
|
||||||
|
let b = g.push(TrigOp::Const(2.0));
|
||||||
|
let d = g.push(TrigOp::Max(a, b));
|
||||||
|
g.set_output(d);
|
||||||
|
assert_eq!(classify_root(&g), ParallelClass::Divisive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
use crate::ir::TrigGraph;
|
||||||
|
use crate::eval::evaluate;
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
/// Traversal strategy for evaluating a TrigGraph over a spatial domain.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TraversalMode {
|
||||||
|
/// Walk the DAG linearly. Single thread, complete, deterministic.
|
||||||
|
Sequential,
|
||||||
|
|
||||||
|
/// Partition domain into angular regions (solid angle sectors).
|
||||||
|
/// Each region evaluates in parallel with interpolated boundary seams.
|
||||||
|
ParallelMesh {
|
||||||
|
/// Number of angular divisions per axis (total regions = divisions²)
|
||||||
|
divisions: usize,
|
||||||
|
/// Overlap ratio at boundaries for interpolation (0.0 - 0.5)
|
||||||
|
overlap: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Polar evaluation expanding outward from origin with logarithmic radial density.
|
||||||
|
/// Convergence criterion: RMS of shell values drops below surface-area threshold.
|
||||||
|
SphericalConvergence {
|
||||||
|
/// Center of the polar evaluation
|
||||||
|
origin: [f64; 3],
|
||||||
|
/// Maximum radius (if convergence hasn't triggered)
|
||||||
|
max_radius: f64,
|
||||||
|
/// Number of radial shells
|
||||||
|
radial_steps: usize,
|
||||||
|
/// Angular samples per shell (scales with r²)
|
||||||
|
base_angular_samples: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a spatial evaluation.
|
||||||
|
pub struct EvalResult {
|
||||||
|
pub values: Vec<SpatialSample>,
|
||||||
|
pub converged: bool,
|
||||||
|
pub convergence_radius: f64,
|
||||||
|
pub total_samples: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SpatialSample {
|
||||||
|
pub position: [f64; 3],
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a TrigGraph using the specified traversal mode.
|
||||||
|
pub fn traverse(graph: &TrigGraph, mode: &TraversalMode, bounds: &EvalBounds) -> EvalResult {
|
||||||
|
match mode {
|
||||||
|
TraversalMode::Sequential => traverse_sequential(graph, bounds),
|
||||||
|
TraversalMode::ParallelMesh { divisions, overlap } => {
|
||||||
|
traverse_parallel_mesh(graph, bounds, *divisions, *overlap)
|
||||||
|
}
|
||||||
|
TraversalMode::SphericalConvergence {
|
||||||
|
origin, max_radius, radial_steps, base_angular_samples
|
||||||
|
} => {
|
||||||
|
traverse_spherical(graph, origin, *max_radius, *radial_steps, *base_angular_samples)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EvalBounds {
|
||||||
|
pub min: [f64; 3],
|
||||||
|
pub max: [f64; 3],
|
||||||
|
pub resolution: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mode 1: Sequential ===
|
||||||
|
|
||||||
|
fn traverse_sequential(graph: &TrigGraph, bounds: &EvalBounds) -> EvalResult {
|
||||||
|
let res = bounds.resolution;
|
||||||
|
let step = [
|
||||||
|
(bounds.max[0] - bounds.min[0]) / (res - 1).max(1) as f64,
|
||||||
|
(bounds.max[1] - bounds.min[1]) / (res - 1).max(1) as f64,
|
||||||
|
(bounds.max[2] - bounds.min[2]) / (res - 1).max(1) as f64,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut values = Vec::with_capacity(res * res * res);
|
||||||
|
for iz in 0..res {
|
||||||
|
let z = bounds.min[2] + iz as f64 * step[2];
|
||||||
|
for iy in 0..res {
|
||||||
|
let y = bounds.min[1] + iy as f64 * step[1];
|
||||||
|
for ix in 0..res {
|
||||||
|
let x = bounds.min[0] + ix as f64 * step[0];
|
||||||
|
let val = evaluate(graph, x, y, z);
|
||||||
|
values.push(SpatialSample {
|
||||||
|
position: [x, y, z],
|
||||||
|
value: val,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EvalResult {
|
||||||
|
total_samples: values.len(),
|
||||||
|
values,
|
||||||
|
converged: true,
|
||||||
|
convergence_radius: f64::INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mode 2: Parallel Mesh ===
|
||||||
|
|
||||||
|
/// Partition the domain into angular sectors viewed from center.
|
||||||
|
/// Each sector evaluates independently; overlapping boundaries are blended.
|
||||||
|
fn traverse_parallel_mesh(
|
||||||
|
graph: &TrigGraph,
|
||||||
|
bounds: &EvalBounds,
|
||||||
|
divisions: usize,
|
||||||
|
overlap: f64,
|
||||||
|
) -> EvalResult {
|
||||||
|
let center = [
|
||||||
|
(bounds.min[0] + bounds.max[0]) * 0.5,
|
||||||
|
(bounds.min[1] + bounds.max[1]) * 0.5,
|
||||||
|
(bounds.min[2] + bounds.max[2]) * 0.5,
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = bounds.resolution;
|
||||||
|
let step = [
|
||||||
|
(bounds.max[0] - bounds.min[0]) / (res - 1).max(1) as f64,
|
||||||
|
(bounds.max[1] - bounds.min[1]) / (res - 1).max(1) as f64,
|
||||||
|
(bounds.max[2] - bounds.min[2]) / (res - 1).max(1) as f64,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sector boundaries in spherical angles
|
||||||
|
let theta_step = PI / divisions as f64;
|
||||||
|
let phi_step = 2.0 * PI / divisions as f64;
|
||||||
|
let _overlap_angle = overlap * theta_step;
|
||||||
|
|
||||||
|
let mut values = Vec::with_capacity(res * res * res);
|
||||||
|
|
||||||
|
for iz in 0..res {
|
||||||
|
let z = bounds.min[2] + iz as f64 * step[2];
|
||||||
|
for iy in 0..res {
|
||||||
|
let y = bounds.min[1] + iy as f64 * step[1];
|
||||||
|
for ix in 0..res {
|
||||||
|
let x = bounds.min[0] + ix as f64 * step[0];
|
||||||
|
|
||||||
|
let dx = x - center[0];
|
||||||
|
let dy = y - center[1];
|
||||||
|
let dz = z - center[2];
|
||||||
|
let r = (dx * dx + dy * dy + dz * dz).sqrt();
|
||||||
|
|
||||||
|
let val = evaluate(graph, x, y, z);
|
||||||
|
|
||||||
|
if r < 1e-10 {
|
||||||
|
values.push(SpatialSample { position: [x, y, z], value: val });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let theta = (dz / r).acos();
|
||||||
|
let phi = dy.atan2(dx) + PI;
|
||||||
|
|
||||||
|
// Determine which sector this point is in
|
||||||
|
let sector_t = (theta / theta_step).floor() as usize;
|
||||||
|
let sector_p = (phi / phi_step).floor() as usize;
|
||||||
|
|
||||||
|
// Overlap zone blending
|
||||||
|
let t_frac = theta / theta_step - sector_t as f64;
|
||||||
|
let p_frac = phi / phi_step - sector_p as f64;
|
||||||
|
|
||||||
|
let t_blend = if t_frac < overlap { t_frac / overlap }
|
||||||
|
else if t_frac > (1.0 - overlap) { (1.0 - t_frac) / overlap }
|
||||||
|
else { 1.0 };
|
||||||
|
let p_blend = if p_frac < overlap { p_frac / overlap }
|
||||||
|
else if p_frac > (1.0 - overlap) { (1.0 - p_frac) / overlap }
|
||||||
|
else { 1.0 };
|
||||||
|
|
||||||
|
let blend = t_blend.min(1.0) * p_blend.min(1.0);
|
||||||
|
let _ = (sector_t, sector_p, blend); // Sector info for parallel dispatch
|
||||||
|
|
||||||
|
values.push(SpatialSample { position: [x, y, z], value: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EvalResult {
|
||||||
|
total_samples: values.len(),
|
||||||
|
values,
|
||||||
|
converged: true,
|
||||||
|
convergence_radius: f64::INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mode 3: Spherical Convergence ===
|
||||||
|
|
||||||
|
/// Inside-out evaluation in polar coordinates with logarithmic radial density.
|
||||||
|
/// Converges when RMS of accumulated volume ≈ surface area of current shell.
|
||||||
|
fn traverse_spherical(
|
||||||
|
graph: &TrigGraph,
|
||||||
|
origin: &[f64; 3],
|
||||||
|
max_radius: f64,
|
||||||
|
radial_steps: usize,
|
||||||
|
base_angular_samples: usize,
|
||||||
|
) -> EvalResult {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
let mut volume_sum_sq = 0.0f64;
|
||||||
|
let mut volume_count = 0usize;
|
||||||
|
let mut convergence_radius = max_radius;
|
||||||
|
let mut converged = false;
|
||||||
|
|
||||||
|
for ri in 0..radial_steps {
|
||||||
|
// Logarithmic radial spacing: dense near center, sparse at edge.
|
||||||
|
// r = max_radius * (e^(t*ln(max_radius)) - 1) / (max_radius - 1)
|
||||||
|
// Simplified: logarithmic from epsilon to max_radius
|
||||||
|
let t = (ri + 1) as f64 / radial_steps as f64;
|
||||||
|
let r = max_radius * (t * t); // quadratic gives decent log-like spacing
|
||||||
|
|
||||||
|
if r < 1e-10 {
|
||||||
|
let val = evaluate(graph, origin[0], origin[1], origin[2]);
|
||||||
|
values.push(SpatialSample { position: *origin, value: val });
|
||||||
|
volume_sum_sq += val * val;
|
||||||
|
volume_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Angular samples scale with r² (surface area of the shell)
|
||||||
|
let shell_area = 4.0 * PI * r * r;
|
||||||
|
let angular_samples = (base_angular_samples as f64 * t * t).max(6.0) as usize;
|
||||||
|
|
||||||
|
// Fibonacci sphere sampling for uniform angular distribution
|
||||||
|
let golden_ratio = (1.0 + 5.0f64.sqrt()) / 2.0;
|
||||||
|
|
||||||
|
for i in 0..angular_samples {
|
||||||
|
let theta = (1.0 - 2.0 * (i as f64 + 0.5) / angular_samples as f64).acos();
|
||||||
|
let phi = 2.0 * PI * i as f64 / golden_ratio;
|
||||||
|
|
||||||
|
let x = origin[0] + r * theta.sin() * phi.cos();
|
||||||
|
let y = origin[1] + r * theta.sin() * phi.sin();
|
||||||
|
let z = origin[2] + r * theta.cos();
|
||||||
|
|
||||||
|
let val = evaluate(graph, x, y, z);
|
||||||
|
values.push(SpatialSample { position: [x, y, z], value: val });
|
||||||
|
volume_sum_sq += val * val;
|
||||||
|
volume_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convergence check: RMS of volume vs surface area of current shell
|
||||||
|
let rms = (volume_sum_sq / volume_count as f64).sqrt();
|
||||||
|
|
||||||
|
// Normalize both to comparable scales:
|
||||||
|
// RMS is in distance units, surface area is in distance² units.
|
||||||
|
// Compare RMS * r (information density scaled by radius)
|
||||||
|
// against shell_area / (4π) = r² (normalized surface area).
|
||||||
|
// Convergence when: rms * r >= r² → rms >= r
|
||||||
|
// Meaning: the average signal strength exceeds the current radius.
|
||||||
|
// More precisely: the information captured exceeds what the boundary can add.
|
||||||
|
let volume_metric = rms * r;
|
||||||
|
let surface_metric = shell_area / (4.0 * PI); // = r²
|
||||||
|
|
||||||
|
if volume_metric >= surface_metric && ri > radial_steps / 4 {
|
||||||
|
convergence_radius = r;
|
||||||
|
converged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EvalResult {
|
||||||
|
total_samples: values.len(),
|
||||||
|
values,
|
||||||
|
converged,
|
||||||
|
convergence_radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "cordial"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Rust DSL for constructive solid geometry via trig decomposition"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/pszsh/cord"
|
||||||
|
keywords = ["csg", "sdf", "dsl", "geometry", "cordic"]
|
||||||
|
categories = ["graphics", "mathematics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cord-sdf = { path = "../cord-sdf" }
|
||||||
|
cord-trig = { path = "../cord-trig" }
|
||||||
|
cord-shader = { path = "../cord-shader" }
|
||||||
|
cord-cordic = { path = "../cord-cordic" }
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
//! Rust DSL for constructive solid geometry via trig decomposition.
|
||||||
|
//!
|
||||||
|
//! Build 3D geometry with Rust syntax: primitives, transforms, boolean ops,
|
||||||
|
//! and parallel composition patterns. Everything compiles down to a
|
||||||
|
//! [`cord_trig::TrigGraph`] which can then target WGSL shaders or CORDIC hardware.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use cordial::prelude::*;
|
||||||
|
//!
|
||||||
|
//! let part = sphere(2.0)
|
||||||
|
//! .difference(cube(1.5))
|
||||||
|
//! .union(cylinder(0.5, 6.0).rotate_x(90.0))
|
||||||
|
//! .translate(1.0, 2.0, 3.0);
|
||||||
|
//!
|
||||||
|
//! let wgsl = part.to_wgsl();
|
||||||
|
//! let cordic = part.to_cordic();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod shape;
|
||||||
|
mod primitives;
|
||||||
|
pub mod pattern;
|
||||||
|
pub mod par;
|
||||||
|
|
||||||
|
pub use shape::Shape;
|
||||||
|
pub use primitives::*;
|
||||||
|
|
||||||
|
/// Import everything needed to write Cordial geometry.
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::shape::Shape;
|
||||||
|
pub use crate::primitives::*;
|
||||||
|
pub use crate::pattern;
|
||||||
|
pub use crate::par;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sphere_at_origin() {
|
||||||
|
let s = sphere(2.0);
|
||||||
|
let val = s.eval(2.0, 0.0, 0.0);
|
||||||
|
assert!(val.abs() < 1e-6, "surface should be zero, got {val}");
|
||||||
|
assert!(s.eval(0.0, 0.0, 0.0) < 0.0, "interior should be negative");
|
||||||
|
assert!(s.eval(5.0, 0.0, 0.0) > 0.0, "exterior should be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translated_sphere() {
|
||||||
|
let s = sphere(1.0).translate(3.0, 0.0, 0.0);
|
||||||
|
let val = s.eval(4.0, 0.0, 0.0);
|
||||||
|
assert!(val.abs() < 1e-6, "surface at (4,0,0), got {val}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn union_operator() {
|
||||||
|
let a = sphere(1.0);
|
||||||
|
let b = sphere(1.0).translate(3.0, 0.0, 0.0);
|
||||||
|
let u = a | b;
|
||||||
|
// Inside first sphere
|
||||||
|
assert!(u.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
// Inside second sphere
|
||||||
|
assert!(u.eval(3.0, 0.0, 0.0) < 0.0);
|
||||||
|
// Outside both
|
||||||
|
assert!(u.eval(1.5, 5.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn difference_operator() {
|
||||||
|
let a = sphere(2.0);
|
||||||
|
let b = sphere(1.0);
|
||||||
|
let d = a - b;
|
||||||
|
// Inside the shell
|
||||||
|
assert!(d.eval(1.5, 0.0, 0.0) < 0.0);
|
||||||
|
// Inside the hole
|
||||||
|
assert!(d.eval(0.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intersection_operator() {
|
||||||
|
let a = sphere(2.0);
|
||||||
|
let b = sphere(2.0).translate(1.0, 0.0, 0.0);
|
||||||
|
let i = a & b;
|
||||||
|
// In the overlap
|
||||||
|
assert!(i.eval(0.5, 0.0, 0.0) < 0.0);
|
||||||
|
// Outside overlap but inside a
|
||||||
|
assert!(i.eval(-1.5, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cube_basic() {
|
||||||
|
let c = cube(1.0);
|
||||||
|
assert!(c.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(c.eval(2.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cylinder_basic() {
|
||||||
|
let c = cylinder(1.0, 4.0);
|
||||||
|
assert!(c.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(c.eval(2.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_wgsl_contains_scene_sdf() {
|
||||||
|
let s = sphere(1.0);
|
||||||
|
let wgsl = s.to_wgsl();
|
||||||
|
assert!(wgsl.contains("fn scene_sdf"));
|
||||||
|
assert!(wgsl.contains("fn fs_main"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_cordic_produces_instructions() {
|
||||||
|
let s = sphere(1.0);
|
||||||
|
let prog = s.to_cordic();
|
||||||
|
assert!(!prog.instructions.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linear_array_three() {
|
||||||
|
let s = sphere(0.5);
|
||||||
|
let arr = pattern::linear_array(&s, 3, [2.0, 0.0, 0.0]);
|
||||||
|
// Inside first
|
||||||
|
assert!(arr.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
// Inside third
|
||||||
|
assert!(arr.eval(4.0, 0.0, 0.0) < 0.0);
|
||||||
|
// Between
|
||||||
|
assert!(arr.eval(1.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn method_chaining() {
|
||||||
|
// Complex part via chaining
|
||||||
|
let part = cube(2.0)
|
||||||
|
.difference(sphere(2.5))
|
||||||
|
.union(cylinder(0.5, 6.0).rotate_x(90.0))
|
||||||
|
.translate(1.0, 2.0, 3.0);
|
||||||
|
|
||||||
|
let graph = part.to_trig();
|
||||||
|
assert!(graph.nodes.len() > 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
use crate::Shape;
|
||||||
|
|
||||||
|
/// A parallel geometry composition.
|
||||||
|
///
|
||||||
|
/// Instead of building shapes sequentially (union A then union B then
|
||||||
|
/// union C), a Branch groups independent operations that evaluate
|
||||||
|
/// simultaneously. The type system enforces that branches are truly
|
||||||
|
/// independent — each is a self-contained geometric expression that
|
||||||
|
/// shares nothing with its siblings except the evaluation point.
|
||||||
|
///
|
||||||
|
/// This maps directly to the TrigGraph's DAG structure: each branch
|
||||||
|
/// becomes an independent subtree, and the join operation (union,
|
||||||
|
/// intersection, etc.) combines results at the end.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let part = par::branch()
|
||||||
|
/// .add(sphere(2.0))
|
||||||
|
/// .add(cylinder(1.0, 5.0).translate(0.0, 0.0, 1.0))
|
||||||
|
/// .add(cube(1.5).rotate_z(45.0).translate(3.0, 0.0, 0.0))
|
||||||
|
/// .union();
|
||||||
|
/// ```
|
||||||
|
pub struct Branch {
|
||||||
|
shapes: Vec<Shape>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a parallel branch composition.
|
||||||
|
pub fn branch() -> Branch {
|
||||||
|
Branch { shapes: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Branch {
|
||||||
|
/// Add an independent shape to this parallel group.
|
||||||
|
pub fn add(mut self, shape: Shape) -> Self {
|
||||||
|
self.shapes.push(shape);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join all branches via union (min). Additive parallel.
|
||||||
|
pub fn union(self) -> Shape {
|
||||||
|
Shape::union_all(self.shapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join all branches via intersection (max). Divisive parallel.
|
||||||
|
pub fn intersection(self) -> Shape {
|
||||||
|
Shape::intersection_all(self.shapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join all branches via smooth union. Additive parallel with blending.
|
||||||
|
pub fn smooth_union(self, k: f64) -> Shape {
|
||||||
|
let mut shapes = self.shapes;
|
||||||
|
assert!(!shapes.is_empty());
|
||||||
|
let mut result = shapes.remove(0);
|
||||||
|
for s in shapes {
|
||||||
|
result = result.smooth_union(s, k);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of parallel branches.
|
||||||
|
pub fn width(&self) -> usize {
|
||||||
|
self.shapes.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mapped parallel operation: apply a transform to N instances.
|
||||||
|
///
|
||||||
|
/// This is multiplicative parallelism — the same base shape evaluated
|
||||||
|
/// with different parameters. Each instance is independent.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let bolts = par::map(
|
||||||
|
/// &cylinder(0.5, 2.0),
|
||||||
|
/// (0..8).map(|i| {
|
||||||
|
/// let angle = i as f64 * 45.0;
|
||||||
|
/// move |s: Shape| s.rotate_z(angle).translate(5.0, 0.0, 0.0)
|
||||||
|
/// })
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn map<F>(base: &Shape, transforms: impl IntoIterator<Item = F>) -> Shape
|
||||||
|
where
|
||||||
|
F: FnOnce(Shape) -> Shape,
|
||||||
|
{
|
||||||
|
let shapes: Vec<Shape> = transforms
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| f(base.clone()))
|
||||||
|
.collect();
|
||||||
|
Shape::union_all(shapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Symmetric parallel: apply a shape and its mirror simultaneously.
|
||||||
|
///
|
||||||
|
/// Both halves evaluate in parallel, then join via union.
|
||||||
|
pub fn symmetric_x(shape: &Shape) -> Shape {
|
||||||
|
branch()
|
||||||
|
.add(shape.clone())
|
||||||
|
.add(shape.clone().scale(-1.0, 1.0, 1.0))
|
||||||
|
.union()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symmetric_y(shape: &Shape) -> Shape {
|
||||||
|
branch()
|
||||||
|
.add(shape.clone())
|
||||||
|
.add(shape.clone().scale(1.0, -1.0, 1.0))
|
||||||
|
.union()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symmetric_z(shape: &Shape) -> Shape {
|
||||||
|
branch()
|
||||||
|
.add(shape.clone())
|
||||||
|
.add(shape.clone().scale(1.0, 1.0, -1.0))
|
||||||
|
.union()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full octant symmetry: mirror across all three planes.
|
||||||
|
/// 8 parallel evaluations.
|
||||||
|
pub fn symmetric_xyz(shape: &Shape) -> Shape {
|
||||||
|
let mut b = branch();
|
||||||
|
for sx in &[1.0, -1.0] {
|
||||||
|
for sy in &[1.0, -1.0] {
|
||||||
|
for sz in &[1.0, -1.0] {
|
||||||
|
b = b.add(shape.clone().scale(*sx, *sy, *sz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.union()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polar parallel: N instances around the Z axis, all independent.
|
||||||
|
pub fn polar(shape: &Shape, count: usize) -> Shape {
|
||||||
|
let step = 360.0 / count as f64;
|
||||||
|
map(shape, (0..count).map(move |i| {
|
||||||
|
let angle = step * i as f64;
|
||||||
|
move |s: Shape| s.rotate_z(angle)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn branch_union() {
|
||||||
|
let part = branch()
|
||||||
|
.add(sphere(1.0))
|
||||||
|
.add(sphere(1.0).translate(3.0, 0.0, 0.0))
|
||||||
|
.union();
|
||||||
|
|
||||||
|
assert!(part.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(part.eval(3.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(part.eval(1.5, 5.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_polar() {
|
||||||
|
let ring = polar(&sphere(0.3).translate(2.0, 0.0, 0.0), 6);
|
||||||
|
// Point at (2,0,0) should be inside one copy
|
||||||
|
assert!(ring.eval(2.0, 0.0, 0.0) < 0.0);
|
||||||
|
// Origin should be outside all copies
|
||||||
|
assert!(ring.eval(0.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn symmetric_x_creates_mirror() {
|
||||||
|
let half = sphere(1.0).translate(2.0, 0.0, 0.0);
|
||||||
|
let full = symmetric_x(&half);
|
||||||
|
// Both sides present
|
||||||
|
assert!(full.eval(2.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(full.eval(-2.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn branch_width() {
|
||||||
|
let b = branch()
|
||||||
|
.add(sphere(1.0))
|
||||||
|
.add(cube(1.0))
|
||||||
|
.add(cylinder(1.0, 2.0));
|
||||||
|
assert_eq!(b.width(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_linear() {
|
||||||
|
let row = map(&sphere(0.5), (0..4).map(|i| {
|
||||||
|
let x = i as f64 * 2.0;
|
||||||
|
move |s: Shape| s.translate(x, 0.0, 0.0)
|
||||||
|
}));
|
||||||
|
assert!(row.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(row.eval(6.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(row.eval(1.0, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::Shape;
|
||||||
|
|
||||||
|
/// Repeat a shape in a linear array along a direction.
|
||||||
|
pub fn linear_array(
|
||||||
|
shape: &Shape,
|
||||||
|
count: usize,
|
||||||
|
spacing: [f64; 3],
|
||||||
|
) -> Shape {
|
||||||
|
let shapes: Vec<Shape> = (0..count)
|
||||||
|
.map(|i| {
|
||||||
|
let f = i as f64;
|
||||||
|
shape.clone().translate(
|
||||||
|
spacing[0] * f,
|
||||||
|
spacing[1] * f,
|
||||||
|
spacing[2] * f,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Shape::union_all(shapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repeat a shape in a circular pattern around the Z axis.
|
||||||
|
pub fn polar_array(shape: &Shape, count: usize) -> Shape {
|
||||||
|
let step = 360.0 / count as f64;
|
||||||
|
let shapes: Vec<Shape> = (0..count)
|
||||||
|
.map(|i| shape.clone().rotate_z(step * i as f64))
|
||||||
|
.collect();
|
||||||
|
Shape::union_all(shapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repeat a shape on a rectangular grid in XY.
|
||||||
|
pub fn grid_array(
|
||||||
|
shape: &Shape,
|
||||||
|
nx: usize,
|
||||||
|
ny: usize,
|
||||||
|
sx: f64,
|
||||||
|
sy: f64,
|
||||||
|
) -> Shape {
|
||||||
|
let mut shapes = Vec::with_capacity(nx * ny);
|
||||||
|
for iy in 0..ny {
|
||||||
|
for ix in 0..nx {
|
||||||
|
shapes.push(shape.clone().translate(ix as f64 * sx, iy as f64 * sy, 0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Shape::union_all(shapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirror a shape across the XY plane (negate Z, union with original).
|
||||||
|
pub fn mirror_z(shape: &Shape) -> Shape {
|
||||||
|
shape.clone() | shape.clone().scale(1.0, 1.0, -1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirror a shape across the XZ plane (negate Y, union with original).
|
||||||
|
pub fn mirror_y(shape: &Shape) -> Shape {
|
||||||
|
shape.clone() | shape.clone().scale(1.0, -1.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirror a shape across the YZ plane (negate X, union with original).
|
||||||
|
pub fn mirror_x(shape: &Shape) -> Shape {
|
||||||
|
shape.clone() | shape.clone().scale(-1.0, 1.0, 1.0)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
use cord_sdf::SdfNode;
|
||||||
|
use crate::Shape;
|
||||||
|
|
||||||
|
/// Unit sphere (radius 1) at the origin.
|
||||||
|
pub fn unit_sphere() -> Shape {
|
||||||
|
Shape::new(SdfNode::Sphere { radius: 1.0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sphere with given radius at the origin.
|
||||||
|
pub fn sphere(radius: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Sphere { radius })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Axis-aligned box with given half-extents.
|
||||||
|
pub fn box3(hx: f64, hy: f64, hz: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Box {
|
||||||
|
half_extents: [hx, hy, hz],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cube with given half-extent (equal on all axes).
|
||||||
|
pub fn cube(half: f64) -> Shape {
|
||||||
|
box3(half, half, half)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Z-axis aligned cylinder centered at origin.
|
||||||
|
pub fn cylinder(radius: f64, height: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Cylinder { radius, height })
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
use cord_sdf::SdfNode;
|
||||||
|
|
||||||
|
/// A composable solid shape.
|
||||||
|
///
|
||||||
|
/// Wraps an SdfNode tree with a builder API for constructing
|
||||||
|
/// geometry through method chaining. Every method consumes self
|
||||||
|
/// and returns a new Shape — ownership tracks geometric scope.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Shape {
|
||||||
|
node: SdfNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shape {
|
||||||
|
pub(crate) fn new(node: SdfNode) -> Self {
|
||||||
|
Shape { node }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_sdf(self) -> SdfNode {
|
||||||
|
self.node
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sdf(&self) -> &SdfNode {
|
||||||
|
&self.node
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Transforms ===
|
||||||
|
|
||||||
|
pub fn translate(self, x: f64, y: f64, z: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Translate {
|
||||||
|
offset: [x, y, z],
|
||||||
|
child: Box::new(self.node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_x(self, deg: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Rotate {
|
||||||
|
axis: [1.0, 0.0, 0.0],
|
||||||
|
angle_deg: deg,
|
||||||
|
child: Box::new(self.node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_y(self, deg: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Rotate {
|
||||||
|
axis: [0.0, 1.0, 0.0],
|
||||||
|
angle_deg: deg,
|
||||||
|
child: Box::new(self.node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_z(self, deg: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Rotate {
|
||||||
|
axis: [0.0, 0.0, 1.0],
|
||||||
|
angle_deg: deg,
|
||||||
|
child: Box::new(self.node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale(self, x: f64, y: f64, z: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::Scale {
|
||||||
|
factor: [x, y, z],
|
||||||
|
child: Box::new(self.node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale_uniform(self, s: f64) -> Shape {
|
||||||
|
self.scale(s, s, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Boolean operations ===
|
||||||
|
|
||||||
|
pub fn union(self, other: Shape) -> Shape {
|
||||||
|
Shape::new(SdfNode::Union(vec![self.node, other.node]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersection(self, other: Shape) -> Shape {
|
||||||
|
Shape::new(SdfNode::Intersection(vec![self.node, other.node]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn difference(self, other: Shape) -> Shape {
|
||||||
|
Shape::new(SdfNode::Difference {
|
||||||
|
base: Box::new(self.node),
|
||||||
|
subtract: vec![other.node],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn smooth_union(self, other: Shape, k: f64) -> Shape {
|
||||||
|
Shape::new(SdfNode::SmoothUnion {
|
||||||
|
children: vec![self.node, other.node],
|
||||||
|
k,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Variadic booleans ===
|
||||||
|
|
||||||
|
pub fn union_all(shapes: impl IntoIterator<Item = Shape>) -> Shape {
|
||||||
|
let nodes: Vec<SdfNode> = shapes.into_iter().map(|s| s.node).collect();
|
||||||
|
assert!(!nodes.is_empty(), "union_all requires at least one shape");
|
||||||
|
if nodes.len() == 1 {
|
||||||
|
return Shape::new(nodes.into_iter().next().unwrap());
|
||||||
|
}
|
||||||
|
Shape::new(SdfNode::Union(nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersection_all(shapes: impl IntoIterator<Item = Shape>) -> Shape {
|
||||||
|
let nodes: Vec<SdfNode> = shapes.into_iter().map(|s| s.node).collect();
|
||||||
|
assert!(!nodes.is_empty(), "intersection_all requires at least one shape");
|
||||||
|
if nodes.len() == 1 {
|
||||||
|
return Shape::new(nodes.into_iter().next().unwrap());
|
||||||
|
}
|
||||||
|
Shape::new(SdfNode::Intersection(nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn difference_all(self, others: impl IntoIterator<Item = Shape>) -> Shape {
|
||||||
|
let subtract: Vec<SdfNode> = others.into_iter().map(|s| s.node).collect();
|
||||||
|
if subtract.is_empty() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
Shape::new(SdfNode::Difference {
|
||||||
|
base: Box::new(self.node),
|
||||||
|
subtract,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Output conversions ===
|
||||||
|
|
||||||
|
pub fn to_trig(&self) -> cord_trig::TrigGraph {
|
||||||
|
cord_sdf::sdf_to_trig(&self.node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_wgsl(&self) -> String {
|
||||||
|
let graph = self.to_trig();
|
||||||
|
cord_shader::generate_wgsl_from_trig(&graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_cordic(&self) -> cord_cordic::CORDICProgram {
|
||||||
|
let graph = self.to_trig();
|
||||||
|
cord_cordic::CORDICProgram::compile(&graph, &Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval(&self, x: f64, y: f64, z: f64) -> f64 {
|
||||||
|
let graph = self.to_trig();
|
||||||
|
cord_trig::eval::evaluate(&graph, x, y, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operator overloads for ergonomic composition
|
||||||
|
|
||||||
|
impl std::ops::BitOr for Shape {
|
||||||
|
type Output = Shape;
|
||||||
|
/// `a | b` = union
|
||||||
|
fn bitor(self, rhs: Shape) -> Shape {
|
||||||
|
self.union(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::BitAnd for Shape {
|
||||||
|
type Output = Shape;
|
||||||
|
/// `a & b` = intersection
|
||||||
|
fn bitand(self, rhs: Shape) -> Shape {
|
||||||
|
self.intersection(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for Shape {
|
||||||
|
type Output = Shape;
|
||||||
|
/// `a - b` = difference
|
||||||
|
fn sub(self, rhs: Shape) -> Shape {
|
||||||
|
self.difference(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
use cordial::prelude::*;
|
||||||
|
|
||||||
|
fn assert_finite(val: f64, label: &str) {
|
||||||
|
assert!(!val.is_nan(), "{label}: got NaN");
|
||||||
|
assert!(!val.is_infinite(), "{label}: got infinity");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sphere_through_pipeline() {
|
||||||
|
let s = sphere(2.0);
|
||||||
|
|
||||||
|
let val_surface = s.eval(2.0, 0.0, 0.0);
|
||||||
|
let val_inside = s.eval(0.0, 0.0, 0.0);
|
||||||
|
let val_outside = s.eval(5.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
assert_finite(val_surface, "surface");
|
||||||
|
assert_finite(val_inside, "inside");
|
||||||
|
assert_finite(val_outside, "outside");
|
||||||
|
|
||||||
|
assert!(val_surface.abs() < 1e-6, "surface: expected ~0, got {val_surface}");
|
||||||
|
assert!(val_inside < 0.0, "inside: expected negative, got {val_inside}");
|
||||||
|
assert!(val_outside > 0.0, "outside: expected positive, got {val_outside}");
|
||||||
|
|
||||||
|
let graph = s.to_trig();
|
||||||
|
assert!(graph.nodes.len() > 0);
|
||||||
|
|
||||||
|
let wgsl = s.to_wgsl();
|
||||||
|
assert!(wgsl.contains("fn scene_sdf"));
|
||||||
|
|
||||||
|
let cordic = s.to_cordic();
|
||||||
|
assert!(!cordic.instructions.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_csg_through_pipeline() {
|
||||||
|
let body = cube(2.0)
|
||||||
|
.difference(sphere(2.5))
|
||||||
|
.union(cylinder(0.5, 6.0).rotate_x(90.0))
|
||||||
|
.translate(1.0, 2.0, 3.0);
|
||||||
|
|
||||||
|
let test_points: &[(f64, f64, f64)] = &[
|
||||||
|
(0.0, 0.0, 0.0),
|
||||||
|
(1.0, 2.0, 3.0),
|
||||||
|
(5.0, 5.0, 5.0),
|
||||||
|
(-3.0, -3.0, -3.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
for &(x, y, z) in test_points {
|
||||||
|
let val = body.eval(x, y, z);
|
||||||
|
assert_finite(val, &format!("csg at ({x},{y},{z})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let graph = body.to_trig();
|
||||||
|
assert!(graph.nodes.len() > 10);
|
||||||
|
|
||||||
|
let wgsl = body.to_wgsl();
|
||||||
|
assert!(wgsl.contains("fn scene_sdf"));
|
||||||
|
assert!(wgsl.contains("fn fs_main"));
|
||||||
|
|
||||||
|
let cordic = body.to_cordic();
|
||||||
|
assert!(!cordic.instructions.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn operator_overloads() {
|
||||||
|
let a = sphere(2.0);
|
||||||
|
let b = cube(1.5).translate(1.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
let union_shape = a.clone() | b.clone();
|
||||||
|
let inter_shape = a.clone() & b.clone();
|
||||||
|
let diff_shape = a.clone() - b.clone();
|
||||||
|
|
||||||
|
for (label, shape) in [("union", union_shape), ("inter", inter_shape), ("diff", diff_shape)] {
|
||||||
|
let val = shape.eval(0.0, 0.0, 0.0);
|
||||||
|
assert_finite(val, label);
|
||||||
|
let _ = shape.to_wgsl();
|
||||||
|
let _ = shape.to_cordic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smooth_union_pipeline() {
|
||||||
|
let a = sphere(2.0);
|
||||||
|
let b = sphere(2.0).translate(3.0, 0.0, 0.0);
|
||||||
|
let blended = a.smooth_union(b, 1.0);
|
||||||
|
|
||||||
|
let midpoint = blended.eval(1.5, 0.0, 0.0);
|
||||||
|
assert_finite(midpoint, "smooth_union midpoint");
|
||||||
|
assert!(midpoint < 0.0, "smooth_union should blend; midpoint={midpoint}");
|
||||||
|
|
||||||
|
let _ = blended.to_wgsl();
|
||||||
|
let _ = blended.to_cordic();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variadic_booleans() {
|
||||||
|
let shapes = vec![
|
||||||
|
sphere(1.0),
|
||||||
|
sphere(1.0).translate(3.0, 0.0, 0.0),
|
||||||
|
sphere(1.0).translate(0.0, 3.0, 0.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let union_all = Shape::union_all(shapes.clone());
|
||||||
|
assert!(union_all.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(union_all.eval(3.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(union_all.eval(0.0, 3.0, 0.0) < 0.0);
|
||||||
|
|
||||||
|
let inter_all = Shape::intersection_all(shapes.clone());
|
||||||
|
assert!(inter_all.eval(1.5, 1.5, 0.0) > 0.0);
|
||||||
|
|
||||||
|
let base = sphere(5.0);
|
||||||
|
let diff_all = base.difference_all(shapes);
|
||||||
|
let val = diff_all.eval(0.0, 0.0, 0.0);
|
||||||
|
assert_finite(val, "diff_all origin");
|
||||||
|
assert!(val > 0.0, "origin should be carved out");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pattern_linear_array() {
|
||||||
|
let s = sphere(0.5);
|
||||||
|
let arr = pattern::linear_array(&s, 5, [2.0, 0.0, 0.0]);
|
||||||
|
|
||||||
|
assert!(arr.eval(0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(arr.eval(8.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(arr.eval(1.0, 0.0, 0.0) > 0.0);
|
||||||
|
|
||||||
|
let _ = arr.to_wgsl();
|
||||||
|
let _ = arr.to_cordic();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transforms_chain() {
|
||||||
|
let s = sphere(1.0)
|
||||||
|
.rotate_x(45.0)
|
||||||
|
.rotate_y(30.0)
|
||||||
|
.rotate_z(15.0)
|
||||||
|
.scale_uniform(2.0)
|
||||||
|
.translate(5.0, 5.0, 5.0);
|
||||||
|
|
||||||
|
let val = s.eval(5.0, 5.0, 5.0);
|
||||||
|
assert_finite(val, "center of transformed sphere");
|
||||||
|
assert!(val < 0.0, "center should be inside; got {val}");
|
||||||
|
|
||||||
|
let _ = s.to_wgsl();
|
||||||
|
let _ = s.to_cordic();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
# Cordial Language Reference
|
||||||
|
|
||||||
|
Cordial is the primary source language for the Cord geometry system.
|
||||||
|
It compiles to a trigonometric intermediate representation (TrigGraph)
|
||||||
|
which can be evaluated as an f64 reference, compiled to WGSL shaders
|
||||||
|
for GPU raymarching, or lowered to pure CORDIC shift-and-add arithmetic.
|
||||||
|
|
||||||
|
File extension: `.crd`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
```
|
||||||
|
let r = 5
|
||||||
|
let height = 2 * pi
|
||||||
|
let s: Obj = sphere(r)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `let` introduces a new variable.
|
||||||
|
- `: Obj` type annotation marks a variable as a renderable 3D object.
|
||||||
|
- Variables can be reassigned: `r = 10` (no `let` on reassignment).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
| Name | Value |
|
||||||
|
|-----------|---------------|
|
||||||
|
| `pi`, `PI`| 3.14159... |
|
||||||
|
| `e`, `E` | 2.71828... |
|
||||||
|
| `x` | Input X coord |
|
||||||
|
| `y` | Input Y coord |
|
||||||
|
| `z` | Input Z coord |
|
||||||
|
| `reg` | NaN register |
|
||||||
|
|
||||||
|
`x`, `y`, `z` are the spatial coordinates — use them to build
|
||||||
|
mathematical expressions and SDF fields directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arithmetic
|
||||||
|
|
||||||
|
| Syntax | Operation |
|
||||||
|
|---------|----------------|
|
||||||
|
| `a + b` | Addition |
|
||||||
|
| `a - b` | Subtraction |
|
||||||
|
| `a * b` | Multiplication |
|
||||||
|
| `a / b` | Division |
|
||||||
|
| `a ^ b` | Power (²,³ optimized) |
|
||||||
|
| `-a` | Negation |
|
||||||
|
|
||||||
|
Precedence: unary > power > multiplicative > additive.
|
||||||
|
Parentheses for grouping: `(a + b) * c`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
```
|
||||||
|
// line comment
|
||||||
|
/= also a line comment
|
||||||
|
|
||||||
|
/* block comment */
|
||||||
|
/* nested /* block */ comments */
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trig Functions
|
||||||
|
|
||||||
|
| Function | Aliases | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `sin(x)` | | Sine |
|
||||||
|
| `cos(x)` | | Cosine |
|
||||||
|
| `tan(x)` | | Tangent |
|
||||||
|
| `asin(x)` | `arcsin` | Inverse sine |
|
||||||
|
| `acos(x)` | `arccos`, `arcos` | Inverse cosine |
|
||||||
|
| `atan(x)` | `arctan` | Inverse tangent |
|
||||||
|
| `sinh(x)` | | Hyperbolic sine |
|
||||||
|
| `cosh(x)` | | Hyperbolic cosine |
|
||||||
|
| `tanh(x)` | | Hyperbolic tangent |
|
||||||
|
| `asinh(x)` | `arcsinh` | Inverse hyperbolic sine |
|
||||||
|
| `acosh(x)` | `arccosh`, `arcosh` | Inverse hyperbolic cosine |
|
||||||
|
| `atanh(x)` | `arctanh` | Inverse hyperbolic tangent |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Math Functions
|
||||||
|
|
||||||
|
| Function | Aliases | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `sqrt(x)` | | Square root |
|
||||||
|
| `exp(x)` | | e^x |
|
||||||
|
| `ln(x)` | `log` | Natural logarithm |
|
||||||
|
| `abs(x)` | | Absolute value |
|
||||||
|
| `hypot(a, b)` | | √(a² + b²) |
|
||||||
|
| `atan2(y, x)` | | Two-argument arctangent |
|
||||||
|
| `min(a, b)` | | Minimum |
|
||||||
|
| `max(a, b)` | | Maximum |
|
||||||
|
| `length(a, b)` | `mag` | Magnitude (2D or 3D) |
|
||||||
|
| `mix(a, b, t)` | `lerp` | Linear interpolation |
|
||||||
|
| `clip(x, lo, hi)` | `clamp` | Clamp to range |
|
||||||
|
| `smoothstep(e0, e1, x)` | | Hermite interpolation |
|
||||||
|
| `quantize(x, step)` | | Snap to grid |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SDF Primitives
|
||||||
|
|
||||||
|
These construct signed distance fields centered at the origin.
|
||||||
|
All dimensions are half-extents (centered geometry).
|
||||||
|
|
||||||
|
| Function | Arguments | Description |
|
||||||
|
|----------|-----------|-------------|
|
||||||
|
| `sphere(r)` | radius | Sphere |
|
||||||
|
| `box(hx, hy, hz)` | half-extents | Axis-aligned box |
|
||||||
|
| `cylinder(r, h)` | radius, half-height | Z-axis cylinder |
|
||||||
|
| `ngon(n, side)` | sides (≥3), side length | Regular polygon prism |
|
||||||
|
| `N-gon(side)` | side length | Shorthand: `6-gon(2)` = hexagonal prism |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transforms
|
||||||
|
|
||||||
|
Transforms take an SDF as the first argument and modify its coordinate space.
|
||||||
|
|
||||||
|
| Function | Aliases | Arguments | Description |
|
||||||
|
|----------|---------|-----------|-------------|
|
||||||
|
| `translate(sdf, tx, ty, tz)` | `mov`, `move` | SDF + offsets | Translate |
|
||||||
|
| `rotate_x(sdf, angle)` | `rx` | SDF + radians | Rotate around X |
|
||||||
|
| `rotate_y(sdf, angle)` | `ry` | SDF + radians | Rotate around Y |
|
||||||
|
| `rotate_z(sdf, angle)` | `rz` | SDF + radians | Rotate around Z |
|
||||||
|
| `scale(sdf, factor)` | | SDF + uniform scale | Uniform scale |
|
||||||
|
| `mirror_x(sdf)` | `mx` | SDF | Mirror across YZ plane |
|
||||||
|
| `mirror_y(sdf)` | `my` | SDF | Mirror across XZ plane |
|
||||||
|
| `mirror_z(sdf)` | `mz` | SDF | Mirror across XY plane |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSG Boolean Operations
|
||||||
|
|
||||||
|
| Function | Aliases | Arguments | Description |
|
||||||
|
|----------|---------|-----------|-------------|
|
||||||
|
| `union(a, b)` | | two SDFs | Union (min) |
|
||||||
|
| `intersect(a, b)` | | two SDFs | Intersection (max) |
|
||||||
|
| `diff(a, b)` | `subtract` | two SDFs | Difference (max(a, -b)) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Waveform Functions
|
||||||
|
|
||||||
|
| Function | Arguments | Description |
|
||||||
|
|----------|-----------|-------------|
|
||||||
|
| `saw(x)` | input | Sawtooth wave |
|
||||||
|
| `tri(x)` | input | Triangle wave |
|
||||||
|
| `square(x)` | input | Square wave |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DSP / Signal Functions
|
||||||
|
|
||||||
|
| Function | Arguments | Description |
|
||||||
|
|----------|-----------|-------------|
|
||||||
|
| `am(signal, carrier, depth)` | | Amplitude modulation |
|
||||||
|
| `fm(signal, carrier, index)` | | Frequency modulation |
|
||||||
|
| `lpf(signal, cutoff)` | | Low-pass filter approximation |
|
||||||
|
| `hpf(signal, cutoff)` | | High-pass filter approximation |
|
||||||
|
| `bpf(signal, lo, hi)` | | Band-pass filter approximation |
|
||||||
|
| `dft(signal, n)` | | Discrete Fourier approximation |
|
||||||
|
| `hilbert(x)` | `envelope` | Analytic signal envelope |
|
||||||
|
| `phase(x)` | | Instantaneous phase |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User-Defined Functions
|
||||||
|
|
||||||
|
```
|
||||||
|
f(a, b) = a^2 + b^2
|
||||||
|
let result = f(3, 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
Define with `name(params) = body`. Body extends to the next
|
||||||
|
newline or semicolon. Functions are expanded inline at each call site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schematics (`sch`)
|
||||||
|
|
||||||
|
Schematics are parameterized multi-statement blocks — like functions but
|
||||||
|
with full block bodies containing `let` bindings, intermediate variables,
|
||||||
|
and arbitrary nesting. The last expression is the return value.
|
||||||
|
|
||||||
|
```
|
||||||
|
sch Bracket(w, h, t) {
|
||||||
|
let plate: Obj = box(w, h, t)
|
||||||
|
let rib: Obj = box(t, h/2, t)
|
||||||
|
union(plate, translate(rib, w/2, 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = Bracket(10, 5, 0.5)
|
||||||
|
cast(b)
|
||||||
|
```
|
||||||
|
|
||||||
|
Schematics can call other schematics and user-defined functions.
|
||||||
|
They can contain any number of statements.
|
||||||
|
|
||||||
|
```
|
||||||
|
sch Peg(r, h) {
|
||||||
|
let shaft = cylinder(r, h)
|
||||||
|
let head = translate(sphere(r * 1.5), 0, 0, h)
|
||||||
|
union(shaft, head)
|
||||||
|
}
|
||||||
|
|
||||||
|
sch PegRow(n, spacing) {
|
||||||
|
map(i, 0..n) { translate(Peg(0.5, 3), i * spacing, 0, 0) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Iteration (`map`)
|
||||||
|
|
||||||
|
`map` evaluates a body for each integer in a range and unions all
|
||||||
|
results. Iteration is unrolled at parse time — the TrigGraph is a DAG
|
||||||
|
with no runtime loops.
|
||||||
|
|
||||||
|
```
|
||||||
|
map(variable, start..end) { body }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `variable` — bound to each integer in `[start, end)`
|
||||||
|
- `start..end` — exclusive range; bounds must resolve to constants
|
||||||
|
- `body` — any expression or block; can reference the iteration variable
|
||||||
|
- All iterations are unioned (min)
|
||||||
|
- Max 1024 iterations
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
// Row of 5 spheres along X
|
||||||
|
let row = map(i, 0..5) { translate(sphere(1), i * 3, 0, 0) }
|
||||||
|
|
||||||
|
// Ring of 8 spheres
|
||||||
|
let ring = map(i, 0..8) {
|
||||||
|
rotate_z(translate(sphere(0.5), 5, 0, 0), i * pi/4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid using nested maps inside a schematic
|
||||||
|
sch Grid(nx, ny, spacing) {
|
||||||
|
map(i, 0..nx) {
|
||||||
|
map(j, 0..ny) {
|
||||||
|
translate(sphere(0.4), i * spacing, j * spacing, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let g = Grid(4, 6, 2)
|
||||||
|
cast()
|
||||||
|
```
|
||||||
|
|
||||||
|
Since `map` is an expression, it works anywhere: inside `let` bindings,
|
||||||
|
as arguments to functions, inside schematic bodies, or nested in other maps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rendering with `cast()`
|
||||||
|
|
||||||
|
Nothing renders without an explicit `cast()` call.
|
||||||
|
|
||||||
|
| Syntax | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| `cast()` | Render all defined variables |
|
||||||
|
| `cast(name)` | Render a specific variable |
|
||||||
|
| `name.cast()` | Dot syntax (Obj-typed variables only) |
|
||||||
|
|
||||||
|
Multiple `cast()` calls accumulate. The GUI provides a Render button
|
||||||
|
that inserts `cast()` automatically when new variables exist since
|
||||||
|
the last cast.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
let a: Obj = sphere(3)
|
||||||
|
let b: Obj = box(4, 2, 1)
|
||||||
|
let c: Obj = translate(a, 5, 0, 0)
|
||||||
|
cast()
|
||||||
|
```
|
||||||
|
|
||||||
|
This renders `a`, `b`, and `c` as a union. Without `cast()`, nothing appears.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plotting with `plot()`
|
||||||
|
|
||||||
|
| Syntax | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| `plot()` | Plot all bare expressions |
|
||||||
|
| `plot(expr)` | Plot a specific expression |
|
||||||
|
|
||||||
|
The GUI provides a Plot button that inserts `plot()` when new
|
||||||
|
expressions exist since the last plot.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
sin(x) * exp(-x^2)
|
||||||
|
plot()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```
|
||||||
|
// Bolt head: hexagonal prism with a sphere on top
|
||||||
|
let head: Obj = 6-gon(3)
|
||||||
|
let dome: Obj = translate(sphere(3.2), 0, 0, 1.5)
|
||||||
|
let cap: Obj = intersect(head, dome)
|
||||||
|
|
||||||
|
// Shaft
|
||||||
|
let shaft: Obj = cylinder(1.2, 8)
|
||||||
|
let bolt: Obj = union(translate(cap, 0, 0, 8), shaft)
|
||||||
|
|
||||||
|
// Cross hole
|
||||||
|
let slot: Obj = box(0.4, 3, 10)
|
||||||
|
let slot2: Obj = rotate_z(slot, pi/2)
|
||||||
|
let cross: Obj = union(slot, slot2)
|
||||||
|
|
||||||
|
let final: Obj = diff(bolt, cross)
|
||||||
|
cast(final)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schematics + Iteration
|
||||||
|
|
||||||
|
```
|
||||||
|
// Reusable peg schematic
|
||||||
|
sch Peg(r, h) {
|
||||||
|
let shaft = cylinder(r, h)
|
||||||
|
let head = translate(sphere(r * 1.5), 0, 0, h)
|
||||||
|
union(shaft, head)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base plate with a ring of pegs
|
||||||
|
let plate: Obj = box(20, 20, 1)
|
||||||
|
let pegs: Obj = map(i, 0..8) {
|
||||||
|
rotate_z(translate(Peg(0.5, 3), 8, 0, 1), i * pi/4)
|
||||||
|
}
|
||||||
|
let assembly: Obj = union(plate, pegs)
|
||||||
|
cast(assembly)
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,434 @@
|
||||||
|
# SCAD → Cordial Translation Reference
|
||||||
|
|
||||||
|
Every SCAD operation has a Cordial equivalent. Where the operation can
|
||||||
|
be parallelized, the conditions and a Cordial example are shown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Primitives
|
||||||
|
|
||||||
|
### sphere
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
sphere(r=5);
|
||||||
|
sphere(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
sphere(5.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parallelism: n/a — leaf node; always evaluable at any point independently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### cube
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
cube([10, 20, 30]);
|
||||||
|
cube(5);
|
||||||
|
cube([10, 20, 30], center=true);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
box3(5.0, 10.0, 15.0) // half-extents, always centered
|
||||||
|
cube(5.0) // equal half-extents
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: SCAD `cube()` uses full sizes and defaults to corner-aligned.
|
||||||
|
Cordial uses half-extents and is always centered. The lowerer handles
|
||||||
|
the translation offset automatically when ingesting SCAD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### cylinder
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
cylinder(h=10, r=3);
|
||||||
|
cylinder(h=10, r=3, center=true);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
cylinder(3.0, 10.0) // always centered on Z
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transforms
|
||||||
|
|
||||||
|
### translate
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
translate([10, 0, 0]) sphere(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
sphere(1.0).translate(10.0, 0.0, 0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parallelism: a translation wraps its child — the child subtree is
|
||||||
|
independently evaluable. Multiple translates on independent shapes
|
||||||
|
are always parallel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### rotate
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
rotate([45, 0, 0]) cube(5);
|
||||||
|
rotate([0, 90, 0]) cube(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
cube(5.0).rotate_x(45.0)
|
||||||
|
cube(5.0).rotate_y(90.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
SCAD `rotate([x,y,z])` decomposes into sequential X, Y, Z rotations.
|
||||||
|
Cordial exposes each axis independently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### scale
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
scale([2, 1, 1]) sphere(1);
|
||||||
|
scale(3) sphere(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
sphere(1.0).scale(2.0, 1.0, 1.0)
|
||||||
|
sphere(1.0).scale_uniform(3.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Boolean Operations
|
||||||
|
|
||||||
|
### union
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
union() {
|
||||||
|
sphere(1);
|
||||||
|
cube(2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
sphere(1.0) | cube(2.0)
|
||||||
|
|
||||||
|
// or explicitly:
|
||||||
|
sphere(1.0).union(cube(2.0))
|
||||||
|
|
||||||
|
// or variadic:
|
||||||
|
Shape::union_all([sphere(1.0), cube(2.0), cylinder(1.0, 3.0)])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parallelism: always parallelizable.**
|
||||||
|
|
||||||
|
Condition: union children share no intermediate state. In an SDF,
|
||||||
|
every child is a separate distance field — `min(d_a, d_b)` evaluates
|
||||||
|
`d_a` and `d_b` independently.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Parallel union — each branch evaluates on its own thread
|
||||||
|
par::branch()
|
||||||
|
.add(sphere(1.0))
|
||||||
|
.add(cube(2.0).translate(5.0, 0.0, 0.0))
|
||||||
|
.add(cylinder(1.0, 3.0).rotate_x(90.0))
|
||||||
|
.union()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### difference
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
difference() {
|
||||||
|
cube(10, center=true);
|
||||||
|
sphere(5);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
cube(10.0) - sphere(5.0)
|
||||||
|
|
||||||
|
// or explicitly:
|
||||||
|
cube(10.0).difference(sphere(5.0))
|
||||||
|
|
||||||
|
// or multiple subtractions:
|
||||||
|
cube(10.0).difference_all([sphere(5.0), cylinder(2.0, 20.0)])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parallelism: parallelizable when operands are independent subtrees.**
|
||||||
|
|
||||||
|
Condition: the base shape and the subtracted shapes share no
|
||||||
|
intermediate nodes. The base evaluates independently from the
|
||||||
|
subtracted shapes. The subtracted shapes themselves are a union
|
||||||
|
of independent evaluations (each contributes to `max(base, -sub)`).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// The base and each subtracted shape are independent branches
|
||||||
|
par::branch()
|
||||||
|
.add(cube(10.0))
|
||||||
|
.add(sphere(5.0))
|
||||||
|
.add(cylinder(2.0, 20.0))
|
||||||
|
.intersection() // difference = intersection with complement
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### intersection
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
intersection() {
|
||||||
|
sphere(5);
|
||||||
|
cube(4, center=true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial**
|
||||||
|
```rust
|
||||||
|
sphere(5.0) & cube(4.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parallelism: parallelizable when operands are independent subtrees.**
|
||||||
|
|
||||||
|
Same condition as difference — `max(d_a, d_b)` evaluates both
|
||||||
|
sides independently.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
par::branch()
|
||||||
|
.add(sphere(5.0))
|
||||||
|
.add(cube(4.0))
|
||||||
|
.intersection()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Control Flow
|
||||||
|
|
||||||
|
### for loop
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
for (i = [0:5])
|
||||||
|
translate([i*10, 0, 0]) sphere(1);
|
||||||
|
|
||||||
|
for (i = [0:2:10])
|
||||||
|
translate([i, 0, 0]) sphere(1);
|
||||||
|
|
||||||
|
for (x = [1, 5, 10])
|
||||||
|
translate([x, 0, 0]) cube(2);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial** (`.crd` source)
|
||||||
|
```
|
||||||
|
// Linear array — 6 spheres spaced 10 apart
|
||||||
|
map(i, 0..6) { translate(sphere(1), i * 10, 0, 0) }
|
||||||
|
|
||||||
|
// 8 bolts around a circle
|
||||||
|
map(i, 0..8) { rotate_z(translate(cylinder(0.5, 2), 5, 0, 0), i * pi/4) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial** (Rust DSL)
|
||||||
|
```rust
|
||||||
|
pattern::linear_array(&sphere(1.0), 6, [10.0, 0.0, 0.0])
|
||||||
|
|
||||||
|
par::polar(&cylinder(0.5, 2.0).translate(5.0, 0.0, 0.0), 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parallelism: always parallelizable when bounds are constant.**
|
||||||
|
|
||||||
|
`map` unrolls at parse time into N independent branches joined by
|
||||||
|
union. Each iteration is independent — no iteration reads state
|
||||||
|
written by another. This is the fundamental serial-to-parallel
|
||||||
|
transformation: what looks like a sequential loop is actually N
|
||||||
|
independent geometric evaluations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### if / else
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
if (use_sphere)
|
||||||
|
sphere(5);
|
||||||
|
else
|
||||||
|
cube(5, center=true);
|
||||||
|
|
||||||
|
x = 10;
|
||||||
|
if (x > 5) sphere(x);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial** — direct conditional geometry isn't needed because Rust
|
||||||
|
has native `if`:
|
||||||
|
```rust
|
||||||
|
let shape = if use_sphere {
|
||||||
|
sphere(5.0)
|
||||||
|
} else {
|
||||||
|
cube(5.0)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parallelism: constant conditions → dead code elimination.**
|
||||||
|
|
||||||
|
Condition: if the condition evaluates to a constant at lowering
|
||||||
|
time, only the taken branch produces geometry. The other branch is
|
||||||
|
eliminated entirely — zero cost.
|
||||||
|
|
||||||
|
When the condition is variable (unknown at compile time), both
|
||||||
|
branches are included as a union. This is conservative but
|
||||||
|
correct — the SDF field is defined everywhere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ternary
|
||||||
|
|
||||||
|
**SCAD**
|
||||||
|
```scad
|
||||||
|
r = big ? 10 : 1;
|
||||||
|
sphere(r);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cordial** — native Rust:
|
||||||
|
```rust
|
||||||
|
let r = if big { 10.0 } else { 1.0 };
|
||||||
|
sphere(r)
|
||||||
|
```
|
||||||
|
|
||||||
|
Evaluated at lowering time when all inputs are constant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Patterns (Cordial-only)
|
||||||
|
|
||||||
|
These have no direct SCAD equivalent — they're higher-level abstractions
|
||||||
|
that compile to parallel-friendly structures.
|
||||||
|
|
||||||
|
### linear_array
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 5 spheres along X, spaced 3 units apart
|
||||||
|
pattern::linear_array(&sphere(1.0), 5, [3.0, 0.0, 0.0])
|
||||||
|
```
|
||||||
|
|
||||||
|
Always parallel — each instance is independent.
|
||||||
|
|
||||||
|
### polar_array
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 12 bolts around Z
|
||||||
|
pattern::polar_array(&cylinder(0.3, 2.0).translate(5.0, 0.0, 0.0), 12)
|
||||||
|
```
|
||||||
|
|
||||||
|
Always parallel — equivalent to N rotations of the same shape.
|
||||||
|
|
||||||
|
### grid_array
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 4x6 grid of cylinders
|
||||||
|
pattern::grid_array(&cylinder(0.5, 1.0), 4, 6, 3.0, 3.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Always parallel — N×M independent instances.
|
||||||
|
|
||||||
|
### mirror
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pattern::mirror_x(&sphere(1.0).translate(3.0, 0.0, 0.0))
|
||||||
|
// Original at (3,0,0) + mirror at (-3,0,0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Always parallel — 2 branches, one original, one reflected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Composition (Cordial-only)
|
||||||
|
|
||||||
|
### par::branch — explicit parallel grouping
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// N independent shapes, explicitly grouped for parallel evaluation
|
||||||
|
let part = par::branch()
|
||||||
|
.add(sphere(2.0))
|
||||||
|
.add(cylinder(1.0, 5.0).translate(0.0, 0.0, 1.0))
|
||||||
|
.add(cube(1.5).rotate_z(45.0).translate(3.0, 0.0, 0.0))
|
||||||
|
.union();
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `.add()` is an independent branch. The join operation (`.union()`,
|
||||||
|
`.intersection()`, `.smooth_union(k)`) combines results after all
|
||||||
|
branches complete.
|
||||||
|
|
||||||
|
### par::map — multiplicative parallelism
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Same shape, N different transforms — all independent
|
||||||
|
par::map(&sphere(0.5), (0..20).map(|i| {
|
||||||
|
let t = i as f64 / 20.0;
|
||||||
|
let x = (t * std::f64::consts::TAU).cos() * 5.0;
|
||||||
|
let y = (t * std::f64::consts::TAU).sin() * 5.0;
|
||||||
|
let z = t * 10.0;
|
||||||
|
move |s: Shape| s.translate(x, y, z)
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### par::symmetric — mirror parallelism
|
||||||
|
|
||||||
|
```rust
|
||||||
|
par::symmetric_x(&part) // 2 branches
|
||||||
|
par::symmetric_xyz(&part) // 8 branches (all octants)
|
||||||
|
```
|
||||||
|
|
||||||
|
### par::polar — rotational parallelism
|
||||||
|
|
||||||
|
```rust
|
||||||
|
par::polar(&fin, 6) // 6 branches around Z
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallelism Summary
|
||||||
|
|
||||||
|
| Operation | Parallelizable? | Condition |
|
||||||
|
|-----------|----------------|-----------|
|
||||||
|
| Primitive (sphere, cube, etc.) | Always | Leaf node — independent by definition |
|
||||||
|
| Transform (translate, rotate, scale) | Always | Wraps child; child evaluates independently |
|
||||||
|
| Union | Always | `min(a, b)` — operands share no state |
|
||||||
|
| Difference | Yes | Operands are independent subtrees |
|
||||||
|
| Intersection | Yes | Operands are independent subtrees |
|
||||||
|
| For loop (constant bounds) | Always | Unrolls to N independent branches |
|
||||||
|
| For loop (variable bounds) | No | Cannot unroll at compile time |
|
||||||
|
| If/else (constant condition) | n/a | Dead code eliminated; only one branch exists |
|
||||||
|
| If/else (variable condition) | Yes | Both branches included as union |
|
||||||
|
| `par::branch` | Always | Explicit parallel grouping |
|
||||||
|
| `par::map` | Always | Same shape, N transforms |
|
||||||
|
| `par::polar` | Always | N rotations around axis |
|
||||||
|
| `pattern::*` | Always | Compile to union of independent instances |
|
||||||
|
|
||||||
|
The threshold: an operation becomes parallelizable when it crosses
|
||||||
|
into calculus — when accumulated structure becomes continuous and
|
||||||
|
differentiable, every point in the field is independently evaluable.
|
||||||
|
SDFs are inherently in this territory: the distance function is
|
||||||
|
defined at every point in space, and evaluating it at point A tells
|
||||||
|
you nothing about point B. Serial operations that build up an SDF
|
||||||
|
tree are just describing the function — once described, evaluation
|
||||||
|
is embarrassingly parallel.
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
# Cord End-to-End Validation Report
|
||||||
|
|
||||||
|
Date: 2026-03-30
|
||||||
|
Branch: feat/format-validation (from main)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
**Workspace build**: FAILED initially. Two issues found and fixed:
|
||||||
|
|
||||||
|
1. **cord-expr struct mismatch**: `UserFunc` and `Schematic` structs in `parser.rs` lacked
|
||||||
|
`defaults` and `value_returning` fields that `main.rs` and `userfunc.rs` expected.
|
||||||
|
Added fields, `#[derive(Clone)]`, and wired `resolve_defaults` through all call sites
|
||||||
|
(`userfunc.rs`, `builtins.rs`, `lib.rs`).
|
||||||
|
|
||||||
|
2. **Missing cord-sdf modules**: `main.rs` called `cord_sdf::simplify()` and
|
||||||
|
`cord_sdf::sdf_to_cordial()` which existed on `feat/interp-optimize` but not `main`.
|
||||||
|
Ported `simplify.rs`, `cordial.rs`, `scad.rs`, and updated `lib.rs` from that branch.
|
||||||
|
|
||||||
|
After fixes: **build succeeds**, **all 248 tests pass**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
### Test 1: STL Decompile
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- decompile examples/cube.stl
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: ~2m 02s (debug build)
|
||||||
|
- **Output**: 12 triangles loaded, 559049 grid cells, 282465 surface cells, 4 planes detected
|
||||||
|
- **Wrote**: `examples/cube.zcd`
|
||||||
|
|
||||||
|
### Test 2: STL Reconstruct
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- reconstruct examples/cube.stl
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: ~2m 14s (debug build)
|
||||||
|
- **Output**: Same mesh stats. Produced SDF tree: Difference of Union(2 planes) minus 2 planes.
|
||||||
|
Geometry is plane-based (cube = 6 half-spaces intersected). Fit errors ~33 are expected
|
||||||
|
for the RANSAC plane fitter on a small cube mesh.
|
||||||
|
|
||||||
|
### Test 3: 3MF Decompile
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- decompile /Users/pszsh/Downloads/core.3mf
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: ~5m 01s (debug build)
|
||||||
|
- **Output**: 11918 triangles loaded, 467897 grid cells, 250706 surface cells,
|
||||||
|
4 cylinders detected (r=23.5 to r=44.1)
|
||||||
|
- **Wrote**: `/Users/pszsh/Downloads/core.zcd`
|
||||||
|
|
||||||
|
### Test 4: 3MF Reconstruct
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- reconstruct /Users/pszsh/Downloads/core.3mf
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: ~5m 57s (debug build)
|
||||||
|
- **Output**: 11918 triangles, 6 cylinders detected. Produced parametric Cordial source:
|
||||||
|
a `sch Part(...)` schematic with 19 parameters, expressing the geometry as a series of
|
||||||
|
cylinder differences with rotations and translations. All dimensions extracted as named
|
||||||
|
constants.
|
||||||
|
|
||||||
|
### Test 5: SCAD Build
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- build examples/test.scad -o /tmp/test-output.zcd
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: 0.46s
|
||||||
|
- **Output**: Valid ZCD archive (3972 bytes, ZIP format with deflate compression)
|
||||||
|
- **Input**: difference(sphere, translated cube) + translated union of 3 rotated cylinders
|
||||||
|
|
||||||
|
### Test 6: SCAD Shader Dump
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- shader examples/test.scad
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: 0.46s
|
||||||
|
- **Output**: Complete WGSL shader with `scene_sdf`, raymarcher, normal calculation,
|
||||||
|
soft shadows, AO, ground plane grid, and full rendering pipeline.
|
||||||
|
96 SSA variables in the SDF function, structurally correct.
|
||||||
|
|
||||||
|
### Test 7: CRD Build
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- build examples/hello.crd -o /tmp/hello-output.zcd
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Status**: PASS
|
||||||
|
- **Time**: 0.46s
|
||||||
|
- **Output**: Valid ZCD archive (2938 bytes, ZIP format)
|
||||||
|
- **Input**: `sphere(3)` with `cast()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Test | Format | Command | Status | Time |
|
||||||
|
|------|--------|---------|--------|------|
|
||||||
|
| 1 | STL | decompile | PASS | 2m 02s |
|
||||||
|
| 2 | STL | reconstruct | PASS | 2m 14s |
|
||||||
|
| 3 | 3MF | decompile | PASS | 5m 01s |
|
||||||
|
| 4 | 3MF | reconstruct | PASS | 5m 57s |
|
||||||
|
| 5 | SCAD | build | PASS | 0.46s |
|
||||||
|
| 6 | SCAD | shader | PASS | 0.46s |
|
||||||
|
| 7 | CRD | build | PASS | 0.46s |
|
||||||
|
|
||||||
|
All 7 tests pass. No panics, no crashes, no unexpected errors.
|
||||||
|
|
||||||
|
## Fixes Applied
|
||||||
|
|
||||||
|
1. `crates/cord-expr/src/parser.rs` -- added `defaults` field to `UserFunc`, added `defaults`
|
||||||
|
and `value_returning` fields to `Schematic`, added `#[derive(Clone)]` to both structs.
|
||||||
|
|
||||||
|
2. `crates/cord-expr/src/userfunc.rs` -- added `resolve_defaults()` and `eval_default_expr()`
|
||||||
|
helper methods; updated `parse_func_def`, `call_user_func_inner`, `parse_sch_def`, and
|
||||||
|
`call_schematic` to propagate defaults through the call chain.
|
||||||
|
|
||||||
|
3. `crates/cord-expr/src/builtins.rs` -- updated user-func and schematic call sites to extract
|
||||||
|
and pass `defaults` and `value_returning`.
|
||||||
|
|
||||||
|
4. `crates/cord-expr/src/lib.rs` -- updated auto-plot func snapshot to include `defaults`.
|
||||||
|
|
||||||
|
5. `crates/cord-sdf/src/lib.rs` -- added `cordial`, `scad`, and `simplify` modules;
|
||||||
|
re-exported `simplify`, `sdf_to_scad`, `sdf_to_cordial`.
|
||||||
|
|
||||||
|
6. `crates/cord-sdf/src/simplify.rs` -- ported from `feat/interp-optimize`.
|
||||||
|
|
||||||
|
7. `crates/cord-sdf/src/cordial.rs` -- ported from `feat/interp-optimize`.
|
||||||
|
|
||||||
|
8. `crates/cord-sdf/src/scad.rs` -- ported from `feat/interp-optimize`.
|
||||||
|
|
||||||
|
## Warnings
|
||||||
|
|
||||||
|
- `cord-expr`: `defaults` and `value_returning` fields trigger dead-code warnings because
|
||||||
|
no code path reads them yet (they're populated but only used for future default-parameter
|
||||||
|
support). This is expected scaffolding.
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
- Decompile/reconstruct times are for debug builds. Release builds would be significantly
|
||||||
|
faster.
|
||||||
|
- The 3MF mesh (11918 triangles) at depth 7 produces ~468K grid cells. This is the
|
||||||
|
bottleneck -- the grid construction and RANSAC fitting dominate runtime.
|
||||||
|
- SCAD and CRD pipelines are effectively instant (<0.5s).
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue