Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@
- [x] String literals + `str()`, `chr()`, `ord()`
- [x] `len()` on strings

### Tier C — Module System Completeness
- [ ] `children()` / `$children`
- [ ] `echo()`
- [ ] `assert()`
- [ ] Recursive functions (enabled by user-defined functions)
### Tier C — Module System Completeness
- [x] `children()` / `$children`
- [x] `echo()`
- [x] `assert()`
- [x] Recursive functions (enabled by user-defined functions)

### Tier D — Geometry Operations
- [ ] `multmatrix()`
Expand Down
11 changes: 11 additions & 0 deletions src/app/MeshBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ void MeshBuilder::buildOne(std::filesystem::path path, int gen) {
csg::CsgEvaluator csgEval;
auto scene = csgEval.evaluate(ast);

// Forward echo() output as Info diagnostics
for (const auto& msg : scene.echoMessages) {
lang::Diagnostic d;
d.level = lang::DiagLevel::Info;
d.message = msg;
result->diags.push_back(std::move(d));
}
// Forward assert() failures as Error diagnostics
for (auto& d : scene.evalDiags)
result->diags.push_back(std::move(d));

if (gen != m_currentGen.load()) return;

// ---- Phase: Meshing (Manifold — the slow part) ----
Expand Down
127 changes: 127 additions & 0 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "CsgEvaluator.h"
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include <cstdio>

namespace chisel::csg {

Expand All @@ -27,18 +28,26 @@ CsgScene CsgEvaluator::evaluate(const ParseResult& result, Interpreter& interp)
m_moduleDefs[def.name] = &def;

CsgScene scene;
m_scene = &scene;
scene.globalFn = result.globalFn;
scene.globalFs = result.globalFs;
scene.globalFa = result.globalFa;

// Make special variables readable in expression context
interp.setVar("$fn", Value::fromNumber(result.globalFn));
interp.setVar("$fs", Value::fromNumber(result.globalFs));
interp.setVar("$fa", Value::fromNumber(result.globalFa));

const glm::mat4 identity{1.0f};
for (const auto& root : result.roots) {
if (auto node = evalNode(*root, identity))
scene.roots.push_back(std::move(node));
}

m_interp = nullptr;
m_scene = nullptr;
m_moduleDefs.clear();
m_childrenStack.clear();
return scene;
}

Expand Down Expand Up @@ -371,10 +380,77 @@ CsgNodePtr CsgEvaluator::evalFor(const ForNode& node, const glm::mat4& xform) {
return makeBoolean(std::move(u));
}

// ---------------------------------------------------------------------------
// formatValue — convert a Value to a human-readable string (for echo/assert)
// ---------------------------------------------------------------------------
std::string CsgEvaluator::formatValue(const Value& v) {
if (v.isNumber()) {
char buf[64];
double n = v.asNumber();
if (n == static_cast<double>(static_cast<long long>(n)) && n > -1e15 && n < 1e15)
std::snprintf(buf, sizeof(buf), "%lld", static_cast<long long>(n));
else
std::snprintf(buf, sizeof(buf), "%g", n);
return buf;
}
if (v.isBool()) return v.asBool() ? "true" : "false";
if (v.isString()) return "\"" + v.asString() + "\"";
if (v.isUndef()) return "undef";
if (v.isVector()) {
std::string s = "[";
for (std::size_t i = 0; i < v.asVec().size(); ++i) {
if (i > 0) s += ", ";
s += formatValue(v.asVec()[i]);
}
s += "]";
return s;
}
return "undef";
}

// ---------------------------------------------------------------------------
// Module call — bind args, evaluate body, restore environment
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalModuleCall(const ModuleCallNode& call, const glm::mat4& xform) {
// ---- Built-in: children() ----
if (call.name == "children") return evalChildren(call, xform);

// ---- Built-in: echo(...) ----
if (call.name == "echo") {
if (m_scene) {
std::string msg = "ECHO:";
bool first = true;
for (const auto& arg : call.args) {
Value v = m_interp->evaluate(*arg.value);
msg += first ? " " : ", ";
first = false;
msg += formatValue(v);
}
m_scene->echoMessages.push_back(std::move(msg));
}
return nullptr;
}

// ---- Built-in: assert(cond [, msg]) ----
if (call.name == "assert") {
if (!call.args.empty() && m_scene) {
Value cond = m_interp->evaluate(*call.args[0].value);
if (!bool(cond)) {
lang::Diagnostic d;
d.level = lang::DiagLevel::Error;
d.loc = call.loc;
if (call.args.size() >= 2) {
Value msgVal = m_interp->evaluate(*call.args[1].value);
d.message = "assert failed: " + formatValue(msgVal);
} else {
d.message = "assert failed";
}
m_scene->evalDiags.push_back(std::move(d));
}
}
return nullptr;
}

auto it = m_moduleDefs.find(call.name);
if (it == m_moduleDefs.end()) return nullptr; // undefined module

Expand Down Expand Up @@ -408,13 +484,18 @@ CsgNodePtr CsgEvaluator::evalModuleCall(const ModuleCallNode& call, const glm::m
m_interp->setVar(param.name, m_interp->evaluate(*param.defaultVal));
}

// Expose $children count and push the children context for children() access
m_interp->setVar("$children", Value::fromNumber(static_cast<double>(call.children.size())));
m_childrenStack.push_back(&call.children);

// Evaluate the module body and collect geometry
std::vector<CsgNodePtr> all;
for (const auto& child : def.body) {
if (auto c = evalNode(*child, xform))
all.push_back(std::move(c));
}

m_childrenStack.pop_back();
// Restore the caller's environment
m_interp->restoreEnv(std::move(savedEnv));

Expand All @@ -427,6 +508,52 @@ CsgNodePtr CsgEvaluator::evalModuleCall(const ModuleCallNode& call, const glm::m
return makeBoolean(std::move(u));
}

// ---------------------------------------------------------------------------
// children() — evaluate the active module's children with correct nesting.
// Pops the stack before evaluating so any children() calls *inside* a child
// node see the grandparent module's children (correct OpenSCAD semantics).
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalChildren(const ModuleCallNode& call, const glm::mat4& xform) {
if (m_childrenStack.empty()) return nullptr;

const auto* activeChildren = m_childrenStack.back();
if (!activeChildren || activeChildren->empty()) return nullptr;

// Pop current frame so nested children() calls see the parent context
m_childrenStack.pop_back();

std::vector<CsgNodePtr> all;

if (call.args.empty()) {
// children() — evaluate all children
for (const auto& child : *activeChildren) {
if (auto c = evalNode(*child, xform))
all.push_back(std::move(c));
}
} else {
// children(i) — evaluate the i-th child
Value idxVal = m_interp->evaluate(*call.args[0].value);
if (idxVal.isNumber()) {
int idx = static_cast<int>(idxVal.asNumber());
if (idx >= 0 && idx < static_cast<int>(activeChildren->size())) {
if (auto c = evalNode(*(*activeChildren)[static_cast<std::size_t>(idx)], xform))
all.push_back(std::move(c));
}
}
}

// Restore the frame
m_childrenStack.push_back(activeChildren);

if (all.empty()) return nullptr;
if (all.size() == 1) return std::move(all[0]);

CsgBoolean u;
u.op = CsgBoolean::Op::Union;
u.children = std::move(all);
return makeBoolean(std::move(u));
}

// ---------------------------------------------------------------------------
// Extrusion — build a CsgExtrusion from an ExtrusionNode
// ---------------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions src/csg/CsgEvaluator.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,27 @@ class CsgEvaluator {
// Module definitions indexed by name — populated at evaluate() entry.
std::unordered_map<std::string, const chisel::lang::ModuleDef*> m_moduleDefs;

// Stack of children vectors for children() access inside module bodies.
// Each user module call pushes its children; evalChildren pops/re-pushes for nesting.
std::vector<const std::vector<chisel::lang::AstNodePtr>*> m_childrenStack;

// Non-owning pointer to the scene being built — valid during evaluate().
CsgScene* m_scene = nullptr;

CsgNodePtr evalNode(const chisel::lang::AstNode& node, const glm::mat4& xform);
CsgNodePtr evalPrimitive(const chisel::lang::PrimitiveNode& p, const glm::mat4& xform);
CsgNodePtr evalBoolean(const chisel::lang::BooleanNode& b, const glm::mat4& xform);
CsgNodePtr evalTransform(const chisel::lang::TransformNode& t, const glm::mat4& xform);
CsgNodePtr evalIf(const chisel::lang::IfNode& n, const glm::mat4& xform);
CsgNodePtr evalFor(const chisel::lang::ForNode& n, const glm::mat4& xform);
CsgNodePtr evalModuleCall(const chisel::lang::ModuleCallNode& n, const glm::mat4& xform);
CsgNodePtr evalChildren(const chisel::lang::ModuleCallNode& n, const glm::mat4& xform);
CsgNodePtr evalExtrusion(const chisel::lang::ExtrusionNode& e, const glm::mat4& xform);
CsgNodePtr evalLet(const chisel::lang::LetNode& n, const glm::mat4& xform);

glm::mat4 makeMatrix(const chisel::lang::TransformNode& t) const;

static std::string formatValue(const chisel::lang::Value& v);
};

} // namespace chisel::csg
5 changes: 4 additions & 1 deletion src/csg/CsgNode.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#include "lang/Diagnostic.h"
#include <glm/glm.hpp>
#include <memory>
#include <string>
Expand Down Expand Up @@ -86,10 +87,12 @@ inline CsgNodePtr makeExtrusion(CsgExtrusion e) {
// CsgScene — output of CsgEvaluator
// ---------------------------------------------------------------------------
struct CsgScene {
std::vector<CsgNodePtr> roots;
std::vector<CsgNodePtr> roots;
double globalFn = 0.0;
double globalFs = 2.0;
double globalFa = 12.0;
std::vector<std::string> echoMessages; // echo() output (one entry per call)
std::vector<lang::Diagnostic> evalDiags; // assert() failures and other runtime errors
};

} // namespace chisel::csg
6 changes: 4 additions & 2 deletions src/editor/DiagnosticsPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ void DiagnosticsPanel::drawInline() {
for (int i = 0; i < static_cast<int>(m_diags.size()); ++i) {
const auto& d = m_diags[i];
ImVec4 col = (d.level == chisel::lang::DiagLevel::Error)
? ImVec4{1.0f, 0.3f, 0.3f, 1.0f}
: ImVec4{1.0f, 0.8f, 0.3f, 1.0f};
? ImVec4{1.0f, 0.3f, 0.3f, 1.0f} // red
: (d.level == chisel::lang::DiagLevel::Info)
? ImVec4{0.3f, 0.9f, 0.9f, 1.0f} // cyan — echo() output
: ImVec4{1.0f, 0.8f, 0.3f, 1.0f}; // yellow — warnings

ImGui::PushStyleColor(ImGuiCol_Text, col);

Expand Down
5 changes: 5 additions & 0 deletions src/lang/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ ExprPtr Parser::parsePrimary() {
expect(TokenKind::RBracket, "expected ']'");
return makeExpr(std::move(vlit));
}
// Special variable reference: $fn, $fs, $fa, $children, etc.
if (check(TokenKind::SpecialVar)) {
const Token& tok = advance();
return makeExpr(VarRef{tok.text, tok.loc});
}
// Identifier — variable reference or function call
if (check(TokenKind::Ident)) {
const Token& name_tok = advance();
Expand Down
Loading
Loading