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
11 changes: 9 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,16 @@ target_compile_definitions(chiselcad PRIVATE
)

# ============================================================
# Tests (Catch2)
# Tests (Catch2 — built from source to match the active toolchain)
# ============================================================
enable_testing()
find_package(Catch2 CONFIG REQUIRED)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
)
FetchContent_MakeAvailable(Catch2)

set(CHISELCAD_TEST_SOURCES
tests/test_main.cpp
Expand Down Expand Up @@ -197,6 +203,7 @@ else()
target_compile_options(chiselcad_tests PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif()

list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(Catch)
catch_discover_tests(chiselcad_tests)

Expand Down
72 changes: 36 additions & 36 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
# ChiselCAD — Roadmap

## v1 — Core CSG (current focus)
## v1 — Core CSG

- [ ] CMake + vcpkg build scaffold
- [ ] Vulkan context, swapchain, ImGui integration
- [ ] Lexer + recursive descent parser (core subset)
- [ ] CSG tree evaluator (AST → CsgNode tree)
- [ ] Primitive tessellator (cube, sphere, cylinder)
- [ ] Manifold boolean evaluation (union, difference, intersection)
- [ ] Async eval pipeline with std::stop_token cancellation
- [ ] Preview render mode (color-coded primitives)
- [ ] Result render mode (evaluated mesh, PBR shading)
- [ ] GPU mesh double-buffer swap
- [ ] Arcball orbit camera
- [ ] File watcher + VS Code external editor integration
- [ ] Diagnostics panel with clickable jump-to-line
- [ ] Binary STL export
- [ ] Embedded ImGuiColorTextEdit editor
- [ ] Mesh cache (LRU, hash-keyed by CSG subtree)
- [x] CMake + vcpkg build scaffold
- [x] Vulkan context, swapchain, ImGui integration
- [x] Lexer + recursive descent parser (core subset)
- [x] CSG tree evaluator (AST → CsgNode tree)
- [x] Primitive tessellator (cube, sphere, cylinder)
- [x] Manifold boolean evaluation (union, difference, intersection)
- [x] Async eval pipeline with std::stop_token cancellation
- [x] Preview render mode (color-coded primitives)
- [x] Result render mode (evaluated mesh, Blinn-Phong shading)
- [x] GPU mesh double-buffer swap
- [x] Arcball orbit camera
- [x] File watcher + VS Code external editor integration
- [x] Diagnostics panel with clickable jump-to-line
- [x] Binary STL export
- [x] Embedded ImGuiColorTextEdit editor
- [x] Mesh cache (LRU, hash-keyed by CSG subtree)

## v2 — Language Expansion
## v2 — Language Expansion

- [ ] Full OpenSCAD language: `for`, `if`, `let`, variables, math functions
- [ ] User-defined modules and function literals
- [ ] 2D primitives: `square`, `circle`, `polygon`
- [ ] Extrusion: `linear_extrude`, `rotate_extrude`
- [ ] `hull()` and `minkowski()`
- [x] Full OpenSCAD language: `for`, `if`, `let`, variables, math functions
- [x] User-defined modules and function literals
- [x] 2D primitives: `square`, `circle`, `polygon`
- [x] Extrusion: `linear_extrude`, `rotate_extrude`
- [x] `hull()` and `minkowski()`

## v2.5 — OpenSCAD Language Completeness

### Tier A — High Impact (used constantly)
- [ ] List indexing `v[i]`
- [ ] Ternary operator `condition ? a : b`
- [ ] User-defined functions `function f(x) = expr;`
- [ ] `let` expression `let (x=10) child`
- [ ] `undef` literal
- [ ] `concat()` built-in
### Tier A — High Impact (used constantly)
- [x] List indexing `v[i]`
- [x] Ternary operator `condition ? a : b`
- [x] User-defined functions `function f(x) = expr;`
- [x] `let` expression `let (x=10) child`
- [x] `undef` literal
- [x] `concat()` built-in

### Tier B — Math & String Completeness
- [ ] Inverse trig: `asin()`, `acos()`, `atan()`, `atan2()`
- [ ] Vector math: `norm()`, `cross()`, `sign()`
- [ ] `rands()`, `lookup()`
- [ ] String literals + `str()`, `chr()`, `ord()`
- [ ] `len()` on strings
### Tier B — Math & String Completeness
- [x] Inverse trig: `asin()`, `acos()`, `atan()`, `atan2()`
- [x] Vector math: `norm()`, `cross()`, `sign()`
- [x] `rands()`, `lookup()`
- [x] String literals + `str()`, `chr()`, `ord()`
- [x] `len()` on strings

### Tier C — Module System Completeness
- [ ] `children()` / `$children`
Expand Down
22 changes: 11 additions & 11 deletions src/app/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,18 +274,18 @@ void Application::initImGui() {
ImGui_ImplGlfw_InitForVulkan(m_window, true);

ImGui_ImplVulkan_InitInfo ii{};
ii.Instance = m_ctx.instance();
ii.PhysicalDevice = m_ctx.physDevice();
ii.Device = m_ctx.device();
ii.QueueFamily = m_ctx.graphicsFamily();
ii.Queue = m_ctx.graphicsQueue();
ii.DescriptorPool = m_imguiPool;
ii.RenderPass = m_swapchain.renderPass();
ii.MinImageCount = 2;
ii.ImageCount = m_swapchain.imageCount();
ii.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
ii.Instance = m_ctx.instance();
ii.PhysicalDevice = m_ctx.physDevice();
ii.Device = m_ctx.device();
ii.QueueFamily = m_ctx.graphicsFamily();
ii.Queue = m_ctx.graphicsQueue();
ii.DescriptorPool = m_imguiPool;
ii.MinImageCount = 2;
ii.ImageCount = m_swapchain.imageCount();
ii.ApiVersion = VK_API_VERSION_1_3;
ii.PipelineInfoMain.RenderPass = m_swapchain.renderPass();
ii.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
ImGui_ImplVulkan_Init(&ii);
ImGui_ImplVulkan_CreateFontsTexture();
}

void Application::shutdownImGui() {
Expand Down
8 changes: 7 additions & 1 deletion src/lang/Expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace chisel::lang {
struct NumberLit;
struct BoolLit;
struct UndefLit;
struct StringLit;
struct VectorLit;
struct VarRef;
struct BinaryExpr;
Expand All @@ -23,7 +24,7 @@ struct IndexExpr;
struct LetExpr;
struct FunctionCall;

using ExprNode = std::variant<NumberLit, BoolLit, UndefLit, VectorLit, VarRef,
using ExprNode = std::variant<NumberLit, BoolLit, UndefLit, StringLit, VectorLit, VarRef,
BinaryExpr, UnaryExpr, TernaryExpr, IndexExpr,
LetExpr, FunctionCall>;
using ExprPtr = std::unique_ptr<ExprNode>;
Expand All @@ -50,6 +51,11 @@ struct UndefLit {
SourceLoc loc;
};

struct StringLit {
std::string value;
SourceLoc loc;
};

struct VectorLit {
std::vector<ExprPtr> elements;
SourceLoc loc;
Expand Down
54 changes: 54 additions & 0 deletions src/lang/Interpreter.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "Interpreter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <random>

static constexpr double kDeg2Rad = 3.14159265358979323846 / 180.0;
static constexpr double kRad2Deg = 180.0 / 3.14159265358979323846;
Expand Down Expand Up @@ -37,6 +39,9 @@ Value Interpreter::evaluate(const ExprNode& expr) {
else if constexpr (std::is_same_v<T, UndefLit>) {
return Value::undef();
}
else if constexpr (std::is_same_v<T, StringLit>) {
return Value::fromString(node.value);
}
else if constexpr (std::is_same_v<T, VectorLit>) {
std::vector<Value> elems;
elems.reserve(node.elements.size());
Expand Down Expand Up @@ -397,6 +402,55 @@ Value Interpreter::callBuiltin(const std::string& name,
return Value::undef();
}

// ---- rands(min, max, count [, seed]) ----
if (name == "rands") {
if (args.size() < 3) return Value::undef();
double minVal = num(0);
double maxVal = num(1);
int count = static_cast<int>(num(2));
if (count <= 0 || minVal > maxVal) return Value::fromVec({});
std::mt19937_64 rng;
if (args.size() >= 4 && args[3].isNumber())
rng.seed(static_cast<uint64_t>(args[3].asNumber()));
else
rng.seed(std::random_device{}());
std::uniform_real_distribution<double> dist(minVal, maxVal);
std::vector<Value> result;
result.reserve(static_cast<std::size_t>(count));
for (int i = 0; i < count; ++i)
result.push_back(Value::fromNumber(dist(rng)));
return Value::fromVec(std::move(result));
}

// ---- lookup(key, [[k0,v0],[k1,v1],...]) ----
if (name == "lookup") {
if (args.size() < 2 || !args[0].isNumber() || !args[1].isVector()) return Value::undef();
double key = args[0].asNumber();
const auto& table = args[1].asVec();
if (table.empty()) return Value::undef();

// Collect valid [key, val] pairs
std::vector<std::pair<double, double>> pairs;
for (const auto& entry : table) {
if (entry.isVector() && entry.asVec().size() >= 2 &&
entry.asVec()[0].isNumber() && entry.asVec()[1].isNumber())
pairs.push_back({entry.asVec()[0].asNumber(), entry.asVec()[1].asNumber()});
}
if (pairs.empty()) return Value::undef();
std::sort(pairs.begin(), pairs.end(), [](const auto& a, const auto& b){ return a.first < b.first; });

if (key <= pairs.front().first) return Value::fromNumber(pairs.front().second);
if (key >= pairs.back().first) return Value::fromNumber(pairs.back().second);

for (std::size_t i = 1; i < pairs.size(); ++i) {
if (key <= pairs[i].first) {
double t = (key - pairs[i-1].first) / (pairs[i].first - pairs[i-1].first);
return Value::fromNumber(pairs[i-1].second + t * (pairs[i].second - pairs[i-1].second));
}
}
return Value::fromNumber(pairs.back().second);
}

return Value::undef(); // unknown function
}

Expand Down
41 changes: 41 additions & 0 deletions src/lang/Lexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ std::vector<Token> Lexer::tokenize() {

uint32_t startOffset = static_cast<uint32_t>(m_pos);

// String literals
if (c == '"') {
tokens.push_back(scanString(startOffset));
continue;
}

// Numbers: digits or leading dot (e.g. .5)
if (std::isdigit(static_cast<unsigned char>(c)) ||
(c == '.' && std::isdigit(static_cast<unsigned char>(peek(1))))) {
Expand Down Expand Up @@ -276,6 +282,41 @@ Token Lexer::scanSpecialVar(uint32_t startOffset) {
return t;
}

Token Lexer::scanString(uint32_t startOffset) {
uint32_t startLine = m_line;
uint32_t startCol = m_col;

advance(); // consume opening '"'
std::string value;
while (!atEnd() && peek() != '"') {
char ch = advance();
if (ch == '\\') {
if (atEnd()) break;
char esc = advance();
switch (esc) {
case '"': value += '"'; break;
case '\\': value += '\\'; break;
case 'n': value += '\n'; break;
case 't': value += '\t'; break;
case 'r': value += '\r'; break;
default: value += esc; break;
}
} else {
value += ch;
}
}
if (!atEnd()) advance(); // consume closing '"'
else addError("unterminated string literal", {startLine, startCol, startOffset});

Token t;
t.kind = TokenKind::String;
t.loc.offset = startOffset;
t.loc.line = startLine;
t.loc.col = startCol;
t.text = std::move(value);
return t;
}

void Lexer::skipLineComment() {
// consume '//'
advance(); advance();
Expand Down
1 change: 1 addition & 0 deletions src/lang/Lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Lexer {
Token scanNumber(uint32_t startOffset);
Token scanIdentOrKeyword(uint32_t startOffset);
Token scanSpecialVar(uint32_t startOffset);
Token scanString(uint32_t startOffset);
void skipLineComment();
void skipBlockComment();

Expand Down
5 changes: 5 additions & 0 deletions src/lang/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,11 @@ ExprPtr Parser::parsePrimary() {
const Token& tok = advance();
return makeExpr(NumberLit{tok.numberValue(), tok.loc});
}
// String literal
if (check(TokenKind::String)) {
const Token& tok = advance();
return makeExpr(StringLit{tok.text, tok.loc});
}
// Bool literals
if (check(TokenKind::True)) {
SourceLoc loc = advance().loc;
Expand Down
1 change: 1 addition & 0 deletions src/lang/Token.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct SourceLoc {
enum class TokenKind : uint8_t {
// Literals
Number, // 3.14 .5 1e3
String, // "hello"
True, // true
False, // false

Expand Down
Loading
Loading