mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Scripting: Lua 5.4 with a fry library (shapes, own meshes, lathe, extrude, add/subtract/overlap, points, the look); a Script panel (F5 / Ctrl+Enter runs, one undo step, an error takes it all back), Script menu, example scripts, fry_run; tests run every example
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
04c8985374
commit
62afbf2b1e
14 changed files with 1220 additions and 3 deletions
|
|
@ -47,4 +47,4 @@ add_custom_target(content ALL
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_subdirectory(tests EXCLUDE_FROM_ALL)
|
add_subdirectory(tests EXCLUDE_FROM_ALL)
|
||||||
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} --output-on-failure
|
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} --output-on-failure
|
||||||
DEPENDS mesh_test USES_TERMINAL VERBATIM)
|
DEPENDS mesh_test script_test USES_TERMINAL VERBATIM)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ FetchContent_Declare(opusfile GIT_REPOSITORY https://github.com/xiph/opusfile.gi
|
||||||
FetchContent_Declare(dejavu URL https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.zip
|
FetchContent_Declare(dejavu URL https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.zip
|
||||||
URL_HASH SHA256=7576310b219e04159d35ff61dd4a4ec4cdba4f35c00e002a136f00e96a908b0a
|
URL_HASH SHA256=7576310b219e04159d35ff61dd4a4ec4cdba4f35c00e002a136f00e96a908b0a
|
||||||
DOWNLOAD_EXTRACT_TIMESTAMP ON SOURCE_SUBDIR no-cmake)
|
DOWNLOAD_EXTRACT_TIMESTAMP ON SOURCE_SUBDIR no-cmake)
|
||||||
|
# Lua: the scripting language (Script panel, scripts/*.lua). Small, plain C, nothing else needed.
|
||||||
|
FetchContent_Declare(lua URL https://www.lua.org/ftp/lua-5.4.7.tar.gz
|
||||||
|
URL_HASH SHA256=9fbf5e28ef86c69858f6d3d34eccc32e911c1a28b4120ff3e84aaa70cfbf1e30
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP ON SOURCE_SUBDIR no-cmake)
|
||||||
# KEEL_YAPF_DIR=/path/to/YAPF uses a local checkout instead.
|
# KEEL_YAPF_DIR=/path/to/YAPF uses a local checkout instead.
|
||||||
FetchContent_Declare(yapf GIT_REPOSITORY https://github.com/Nexoniarz/yapf.git
|
FetchContent_Declare(yapf GIT_REPOSITORY https://github.com/Nexoniarz/yapf.git
|
||||||
GIT_TAG 06a4e7db9e7e6d90132473e5606f0e6f5b1d6001 SYSTEM) # YAPF 2 (+ yapf_tex_to_rgba8: any surface as RGBA8)
|
GIT_TAG 06a4e7db9e7e6d90132473e5606f0e6f5b1d6001 SYSTEM) # YAPF 2 (+ yapf_tex_to_rgba8: any surface as RGBA8)
|
||||||
|
|
@ -113,6 +117,24 @@ if(NOT MSVC)
|
||||||
target_compile_options(yapf PRIVATE -O2) # its codecs and the BC7 encoder, even in debug builds
|
target_compile_options(yapf PRIVATE -O2) # its codecs and the BC7 encoder, even in debug builds
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Lua's core and the libraries scripts get (no io, os or loading modules: a script changes the
|
||||||
|
# scene, not the computer).
|
||||||
|
FetchContent_MakeAvailable(lua)
|
||||||
|
set(_lua_src lapi lcode lctype ldebug ldo ldump lfunc lgc llex lmem lobject lopcodes lparser lstate lstring ltable ltm lundump lvm lzio
|
||||||
|
lauxlib lbaselib lcorolib ldblib lmathlib lstrlib ltablib lutf8lib)
|
||||||
|
list(TRANSFORM _lua_src PREPEND ${lua_SOURCE_DIR}/src/)
|
||||||
|
list(TRANSFORM _lua_src APPEND .c)
|
||||||
|
# As C++: its errors are then exceptions, which unwind through the bindings' C++ properly.
|
||||||
|
set_source_files_properties(${_lua_src} PROPERTIES LANGUAGE CXX)
|
||||||
|
add_library(lua STATIC ${_lua_src})
|
||||||
|
if(NOT MSVC)
|
||||||
|
target_compile_options(lua PRIVATE -w)
|
||||||
|
endif()
|
||||||
|
target_include_directories(lua SYSTEM PUBLIC ${lua_SOURCE_DIR}/src)
|
||||||
|
if(NOT WIN32)
|
||||||
|
target_link_libraries(lua PRIVATE m)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Audio: miniaudio for output; Ogg Opus decoding (opusfile).
|
# Audio: miniaudio for output; Ogg Opus decoding (opusfile).
|
||||||
set(OPUS_BUILD_TESTING OFF)
|
set(OPUS_BUILD_TESTING OFF)
|
||||||
set(OPUS_BUILD_PROGRAMS OFF)
|
set(OPUS_BUILD_PROGRAMS OFF)
|
||||||
|
|
|
||||||
16
content/Fry3DX/scripts/box_with_lid.lua
Normal file
16
content/Fry3DX/scripts/box_with_lid.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- A box to print, with a lid and holes for M3 screws in its corners.
|
||||||
|
local w, d, h, wall = 0.08, 0.06, 0.04, 0.002
|
||||||
|
local box = fry.add("rounded_cube", {width = w, depth = d, height = h, name = "Box", color = "#d9d4c7"})
|
||||||
|
local inside = fry.add("cube", {width = w - 2 * wall, depth = d - 2 * wall, height = h})
|
||||||
|
inside.position = {0, 0, wall}
|
||||||
|
box:subtract(inside)
|
||||||
|
local lid = fry.add("cube", {width = w, depth = d, height = wall, name = "Lid", color = "#ff7828"})
|
||||||
|
lid.position = {w + 0.02, 0, 0}
|
||||||
|
for sx = -1, 1, 2 do
|
||||||
|
for sy = -1, 1, 2 do
|
||||||
|
local hole = fry.add("cylinder", {radius = 0.0016, height = 0.01, sides = 24})
|
||||||
|
hole.position = {w + 0.02 + sx * (w / 2 - 0.006), sy * (d / 2 - 0.006), -0.002}
|
||||||
|
lid:subtract(hole)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fry.frame()
|
||||||
14
content/Fry3DX/scripts/gear.lua
Normal file
14
content/Fry3DX/scripts/gear.lua
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- A gear for 3D printing, with a hole for an M5 screw.
|
||||||
|
local teeth, radius, depth, thick = 16, 0.03, 0.004, 0.006 -- metres
|
||||||
|
local outline = {}
|
||||||
|
for i = 0, teeth * 4 - 1 do
|
||||||
|
local a = i / (teeth * 4) * 2 * math.pi
|
||||||
|
local r = (i % 4 < 2) and radius + depth or radius
|
||||||
|
outline[#outline + 1] = {math.cos(a) * r, math.sin(a) * r}
|
||||||
|
end
|
||||||
|
local gear = fry.extrude(outline, thick, "Gear")
|
||||||
|
gear.color = "#e0a030"
|
||||||
|
local hole = fry.add("cylinder", {radius = 0.0027, height = 0.02, sides = 32})
|
||||||
|
hole.position = {0, 0, -0.005}
|
||||||
|
gear:subtract(hole)
|
||||||
|
fry.frame()
|
||||||
21
content/Fry3DX/scripts/little_town.lua
Normal file
21
content/Fry3DX/scripts/little_town.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- A little town: random houses on a grid, each a folder of its own.
|
||||||
|
math.randomseed(7)
|
||||||
|
local roofs = {"#a8432f", "#5a6470", "#8c5a3c"}
|
||||||
|
for x = -2, 2 do
|
||||||
|
for y = -2, 2 do
|
||||||
|
local w, d, h = 0.6 + math.random() * 0.3, 0.6 + math.random() * 0.3, 0.5 + math.random() * 0.9
|
||||||
|
local walls = fry.add("cube", {width = w, depth = d, height = h, color = "#e8e0d0", name = "Walls"})
|
||||||
|
walls.position = {x * 1.4, y * 1.4, 0}
|
||||||
|
-- a wedge rises towards -X: two of them back to back make a ridge
|
||||||
|
local roof = fry.add("wedge", {width = d + 0.1, length = w / 2 + 0.05, height = 0.35, name = "Roof"})
|
||||||
|
local other = roof:copy()
|
||||||
|
roof.position = {x * 1.4 + w / 4 + 0.025, y * 1.4, h}
|
||||||
|
other.position = {x * 1.4 - w / 4 - 0.025, y * 1.4, h}
|
||||||
|
other.turn = {0, 0, 180}
|
||||||
|
local c = roofs[math.random(#roofs)]
|
||||||
|
roof.color, other.color = c, c
|
||||||
|
fry.group(string.format("House %d,%d", x, y), {walls, roof, other})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fry.look{sun_turn = 60, sun_height = 35}
|
||||||
|
fry.frame()
|
||||||
15
content/Fry3DX/scripts/spiral_stairs.lua
Normal file
15
content/Fry3DX/scripts/spiral_stairs.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- Spiral stairs round a pillar: change the numbers and run it again (F5).
|
||||||
|
local steps, rise, turn = 18, 0.18, 22 -- how many, how high each, degrees each
|
||||||
|
local width = 0.9
|
||||||
|
|
||||||
|
fry.add("pillar", {radius = 0.12, height = steps * rise + 0.3, color = "#8a8a90", name = "Post"})
|
||||||
|
local made = {}
|
||||||
|
for i = 0, steps - 1 do
|
||||||
|
local step = fry.add("cube", {width = width, depth = 0.32, height = 0.05, color = "#b07a4a", name = "Step"})
|
||||||
|
step.turn = {0, 0, i * turn}
|
||||||
|
local a = math.rad(i * turn)
|
||||||
|
step.position = {math.cos(a) * width / 2, math.sin(a) * width / 2, (i + 1) * rise}
|
||||||
|
made[#made + 1] = step
|
||||||
|
end
|
||||||
|
fry.group("Stairs", made)
|
||||||
|
fry.frame()
|
||||||
15
content/Fry3DX/scripts/vase.lua
Normal file
15
content/Fry3DX/scripts/vase.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- A vase turned on a lathe: the profile is {out, up} points from the bottom.
|
||||||
|
local profile = {}
|
||||||
|
local height, n = 0.4, 24
|
||||||
|
for i = 0, n do
|
||||||
|
local t = i / n
|
||||||
|
-- a belly low down, a neck, a lip
|
||||||
|
local r = 0.06 + 0.07 * math.sin(t * math.pi * 1.1) + 0.02 * t * t
|
||||||
|
profile[#profile + 1] = {r, t * height}
|
||||||
|
end
|
||||||
|
table.insert(profile, 1, {0, 0})
|
||||||
|
local vase = fry.lathe(profile, 64, "Vase")
|
||||||
|
vase.color = "#3c6e9f"
|
||||||
|
vase.rough = 0.25
|
||||||
|
vase:thicken(0.004) -- walls, so it prints hollow
|
||||||
|
fry.frame()
|
||||||
12
content/Fry3DX/scripts/waves.lua
Normal file
12
content/Fry3DX/scripts/waves.lua
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- Points moved by a formula: a plane made into waves, then given a thickness.
|
||||||
|
local p = fry.add("plane", {width = 2, depth = 2, cuts = 40, name = "Waves"})
|
||||||
|
local pts = p:points()
|
||||||
|
for i, v in ipairs(pts) do
|
||||||
|
v.z = 0.08 * math.sin(v.x * 6) * math.cos(v.y * 5)
|
||||||
|
end
|
||||||
|
p:setpoints(pts)
|
||||||
|
p:thicken(0.03)
|
||||||
|
p.smooth = true
|
||||||
|
p.color = "#2f8f9d"
|
||||||
|
p:move(0, 0, 0.12)
|
||||||
|
fry.frame()
|
||||||
|
|
@ -4,6 +4,9 @@ add_library(fry_core STATIC mesh.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.
|
||||||
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static)
|
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static)
|
||||||
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
||||||
|
# Scripts (Lua) that build and change the scene: also without a window, for the tests.
|
||||||
|
add_library(fry_script STATIC script.cpp)
|
||||||
|
target_link_libraries(fry_script PUBLIC fry_core PRIVATE lua)
|
||||||
|
|
||||||
if(ANDROID) # SDL's Java side loads the app as libmain.so and calls its (SDL_)main
|
if(ANDROID) # SDL's Java side loads the app as libmain.so and calls its (SDL_)main
|
||||||
add_library(fry3dx SHARED app.cpp icons.cpp)
|
add_library(fry3dx SHARED app.cpp icons.cpp)
|
||||||
|
|
@ -16,9 +19,9 @@ if(WIN32) # a window program (no console window), with its icon
|
||||||
set_target_properties(fry3dx PROPERTIES WIN32_EXECUTABLE ON)
|
set_target_properties(fry3dx PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
target_link_options(fry3dx PRIVATE /ENTRY:mainCRTStartup)
|
target_link_options(fry3dx PRIVATE /ENTRY:mainCRTStartup)
|
||||||
endif()
|
endif()
|
||||||
target_link_libraries(fry3dx PRIVATE fry_core keel stb)
|
target_link_libraries(fry3dx PRIVATE fry_core fry_script keel stb)
|
||||||
add_dependencies(fry3dx content)
|
add_dependencies(fry3dx content)
|
||||||
foreach(t fry_core fry3dx)
|
foreach(t fry_core fry_script fry3dx)
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(${t} PRIVATE /W3)
|
target_compile_options(${t} PRIVATE /W3)
|
||||||
else()
|
else()
|
||||||
|
|
|
||||||
175
src/app.cpp
175
src/app.cpp
|
|
@ -6,6 +6,7 @@
|
||||||
#include "icons.h"
|
#include "icons.h"
|
||||||
#include "mesh.h"
|
#include "mesh.h"
|
||||||
#include "scene.h"
|
#include "scene.h"
|
||||||
|
#include "script.h"
|
||||||
#include "shapes.h"
|
#include "shapes.h"
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
@ -123,6 +124,20 @@ struct MaterialInfo {
|
||||||
const char* name;
|
const char* name;
|
||||||
float density; // g/cm3
|
float density; // g/cm3
|
||||||
};
|
};
|
||||||
|
// What the Script panel starts with.
|
||||||
|
const char* kFirstScript = R"(-- A script builds or changes the scene. F5 or Ctrl+Enter runs it;
|
||||||
|
-- Ctrl+Z takes back everything it did. The ? button lists what there is.
|
||||||
|
|
||||||
|
local legs = 6
|
||||||
|
for i = 1, legs do
|
||||||
|
local a = math.rad(i * 360 / legs)
|
||||||
|
local leg = fry.add("cylinder", {radius = 0.03, height = 0.7, color = "#c47a3c"})
|
||||||
|
leg.position = {math.cos(a) * 0.45, math.sin(a) * 0.45, 0}
|
||||||
|
end
|
||||||
|
local top = fry.add("cylinder", {radius = 0.6, height = 0.05, sides = 48, color = "#d9a066"})
|
||||||
|
top.position = {0, 0, 0.7}
|
||||||
|
fry.frame()
|
||||||
|
)";
|
||||||
const MaterialInfo kMaterials[] = {{"PLA", 1.24f}, {"PETG", 1.27f}, {"ABS", 1.04f}, {"ASA", 1.07f}, {"TPU", 1.21f}, {"Nylon", 1.14f}, {"Resin", 1.12f}};
|
const MaterialInfo kMaterials[] = {{"PLA", 1.24f}, {"PETG", 1.27f}, {"ABS", 1.04f}, {"ASA", 1.07f}, {"TPU", 1.21f}, {"Nylon", 1.14f}, {"Resin", 1.12f}};
|
||||||
|
|
||||||
const PaintToolInfo kPaintTools[] = {
|
const PaintToolInfo kPaintTools[] = {
|
||||||
|
|
@ -257,6 +272,14 @@ public:
|
||||||
static keel::Cmd open("fry_open", [](const keel::Args& a) { if (a.size() > 1) self->openFile(a[1]); }, "Fry3DX: open a .fry file");
|
static keel::Cmd open("fry_open", [](const keel::Args& a) { if (a.size() > 1) self->openFile(a[1]); }, "Fry3DX: open a .fry file");
|
||||||
static keel::Cmd save("fry_save", [](const keel::Args& a) { if (a.size() > 1) self->saveFile(a[1]); }, "Fry3DX: save as");
|
static keel::Cmd save("fry_save", [](const keel::Args& a) { if (a.size() > 1) self->saveFile(a[1]); }, "Fry3DX: save as");
|
||||||
static keel::Cmd exp("fry_export", [](const keel::Args& a) { if (a.size() > 1) self->exportFile(a[1]); }, "Fry3DX: export glTF");
|
static keel::Cmd exp("fry_export", [](const keel::Args& a) { if (a.size() > 1) self->exportFile(a[1]); }, "Fry3DX: export glTF");
|
||||||
|
static keel::Cmd run("fry_run", [](const keel::Args& a) {
|
||||||
|
if (a.size() < 2) return;
|
||||||
|
std::ifstream in(a[1], std::ios::binary);
|
||||||
|
if (!in) return keel::log::warn("Couldn't open {}", a[1]);
|
||||||
|
self->scriptCode_.assign(std::istreambuf_iterator<char>(in), {});
|
||||||
|
self->scriptPath_ = a[1], self->showScript_ = true;
|
||||||
|
self->runScriptNow();
|
||||||
|
}, "Fry3DX: run a Lua script file (fry_run path/to/script.lua)");
|
||||||
static keel::Cmd add("fry_add", [](const keel::Args& a) { if (a.size() > 1) self->addShape(a[1]); }, "Fry3DX: add a shape (cube, rounded_cube, table...)");
|
static keel::Cmd add("fry_add", [](const keel::Args& a) { if (a.size() > 1) self->addShape(a[1]); }, "Fry3DX: add a shape (cube, rounded_cube, table...)");
|
||||||
static keel::Cmd view("fry_view", [](const keel::Args& a) {
|
static keel::Cmd view("fry_view", [](const keel::Args& a) {
|
||||||
static const char* names[] = {"persp", "front", "back", "left", "right", "top", "bottom"};
|
static const char* names[] = {"persp", "front", "back", "left", "right", "top", "bottom"};
|
||||||
|
|
@ -541,6 +564,7 @@ public:
|
||||||
if (!touch()) statusBar();
|
if (!touch()) statusBar();
|
||||||
if (!capturing() && (!touch() || viewsOpen_)) viewButtons();
|
if (!capturing() && (!touch() || viewsOpen_)) viewButtons();
|
||||||
if (!capturing() && touch()) touchBar();
|
if (!capturing() && touch()) touchBar();
|
||||||
|
if (showScript_) scriptPanel();
|
||||||
if (showHelp_) helpWindow();
|
if (showHelp_) helpWindow();
|
||||||
if (showAbout_) aboutWindow();
|
if (showAbout_) aboutWindow();
|
||||||
copiesWindow();
|
copiesWindow();
|
||||||
|
|
@ -765,6 +789,11 @@ private:
|
||||||
|
|
||||||
// ── UI ──
|
// ── UI ──
|
||||||
int appliedTheme_ = -1;
|
int appliedTheme_ = -1;
|
||||||
|
// The Script panel: the script being written, where it's from, what it printed.
|
||||||
|
bool showScript_ = false, showScriptHelp_ = false;
|
||||||
|
std::string scriptCode_ = kFirstScript, scriptPath_;
|
||||||
|
std::vector<std::pair<std::string, bool>> scriptLog_; // (line, an error)
|
||||||
|
bool scriptLogToBottom_ = false;
|
||||||
bool styled_ = false, layoutDone_ = false, showHelp_ = false, showAbout_ = false;
|
bool styled_ = false, layoutDone_ = false, showHelp_ = false, showAbout_ = false;
|
||||||
std::string iniPath_, message_;
|
std::string iniPath_, message_;
|
||||||
double messageAt_ = -100.0;
|
double messageAt_ = -100.0;
|
||||||
|
|
@ -1264,6 +1293,11 @@ private:
|
||||||
SDL_ShowSaveFileDialog(done, this, w, kind == 8 ? mf : stl, 1, at);
|
SDL_ShowSaveFileDialog(done, this, w, kind == 8 ? mf : stl, 1, at);
|
||||||
}
|
}
|
||||||
else if (kind == 5 || kind == 7) SDL_ShowSaveFileDialog(done, this, w, png, 1, at);
|
else if (kind == 5 || kind == 7) SDL_ShowSaveFileDialog(done, this, w, png, 1, at);
|
||||||
|
else if (kind == 10 || kind == 11) {
|
||||||
|
static const SDL_DialogFileFilter lua[] = {{"Lua scripts", "lua"}};
|
||||||
|
if (kind == 10) SDL_ShowOpenFileDialog(done, this, w, lua, 1, at, false);
|
||||||
|
else SDL_ShowSaveFileDialog(done, this, w, lua, 1, scriptPath_.empty() || isUri(scriptPath_) ? at : scriptPath_.c_str());
|
||||||
|
}
|
||||||
else SDL_ShowOpenFileDialog(done, this, w, pics, 1, at, false);
|
else SDL_ShowOpenFileDialog(done, this, w, pics, 1, at, false);
|
||||||
}
|
}
|
||||||
void takeDialogResult() {
|
void takeDialogResult() {
|
||||||
|
|
@ -1285,6 +1319,8 @@ private:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isUri(file) && (kind == 8 || kind == 9)) return exportFile(file, -1, kind == 8 ? ".3mf" : ".stl");
|
if (isUri(file) && (kind == 8 || kind == 9)) return exportFile(file, -1, kind == 8 ? ".3mf" : ".stl");
|
||||||
|
if (kind == 10) return openScript(isUri(file) ? fromUri(file, "opened.lua") : file, fileName(file));
|
||||||
|
if (kind == 11) return saveScript(file);
|
||||||
if (kind == 1) openFile(file);
|
if (kind == 1) openFile(file);
|
||||||
else if (kind == 2) saveFile(file);
|
else if (kind == 2) saveFile(file);
|
||||||
else if (kind == 3) exportFile(file, filter);
|
else if (kind == 3) exportFile(file, filter);
|
||||||
|
|
@ -4300,6 +4336,7 @@ private:
|
||||||
if (io.WantTextInput) return;
|
if (io.WantTextInput) return;
|
||||||
const bool ctrl = io.KeyCtrl, shift = io.KeyShift, alt = io.KeyAlt;
|
const bool ctrl = io.KeyCtrl, shift = io.KeyShift, alt = io.KeyAlt;
|
||||||
auto key = [](ImGuiKey k) { return ImGui::IsKeyPressed(k, false); };
|
auto key = [](ImGuiKey k) { return ImGui::IsKeyPressed(k, false); };
|
||||||
|
if (key(ImGuiKey_F5)) return runScriptNow();
|
||||||
if (ctrl && key(ImGuiKey_1)) setTab(Tab::Model);
|
if (ctrl && key(ImGuiKey_1)) setTab(Tab::Model);
|
||||||
if (ctrl && key(ImGuiKey_2)) setTab(Tab::Paint);
|
if (ctrl && key(ImGuiKey_2)) setTab(Tab::Paint);
|
||||||
if (ctrl && key(ImGuiKey_3)) setTab(Tab::Layout);
|
if (ctrl && key(ImGuiKey_3)) setTab(Tab::Layout);
|
||||||
|
|
@ -4925,6 +4962,7 @@ private:
|
||||||
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.04f, nullptr, ¢er);
|
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.04f, nullptr, ¢er);
|
||||||
const ImGuiID picture = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.45f, nullptr, ¢er); // (the Paint tab's)
|
const ImGuiID picture = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.45f, nullptr, ¢er); // (the Paint tab's)
|
||||||
ImGui::DockBuilderDockWindow("Picture", picture);
|
ImGui::DockBuilderDockWindow("Picture", picture);
|
||||||
|
ImGui::DockBuilderDockWindow("Script", picture);
|
||||||
ImGui::DockBuilderDockWindow("Toolbox", left);
|
ImGui::DockBuilderDockWindow("Toolbox", left);
|
||||||
ImGui::DockBuilderDockWindow("Tool options", leftBottom);
|
ImGui::DockBuilderDockWindow("Tool options", leftBottom);
|
||||||
ImGui::DockBuilderDockWindow("Layers", right);
|
ImGui::DockBuilderDockWindow("Layers", right);
|
||||||
|
|
@ -5050,6 +5088,17 @@ private:
|
||||||
if (ImGui::MenuItem("Frame everything", "Home")) frameAll();
|
if (ImGui::MenuItem("Frame everything", "Home")) frameAll();
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
if (ImGui::BeginMenu("Script")) {
|
||||||
|
ImGui::MenuItem("The Script panel", nullptr, &showScript_);
|
||||||
|
if (ImGui::MenuItem("Run it", "F5")) runScriptNow();
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem("Open a script...")) showScript_ = true, dialog(10);
|
||||||
|
if (ImGui::MenuItem("Save the script as...")) dialog(11);
|
||||||
|
exampleScripts();
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem("What scripts can do")) showScript_ = showScriptHelp_ = true;
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
if (ImGui::BeginMenu("Help")) {
|
if (ImGui::BeginMenu("Help")) {
|
||||||
if (ImGui::MenuItem("Keys and mouse", "F1")) showHelp_ = true;
|
if (ImGui::MenuItem("Keys and mouse", "F1")) showHelp_ = true;
|
||||||
if (ImGui::MenuItem("About Fry3DX")) showAbout_ = true;
|
if (ImGui::MenuItem("About Fry3DX")) showAbout_ = true;
|
||||||
|
|
@ -6048,6 +6097,132 @@ private:
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────── scripts ─────────────────────────────
|
||||||
|
|
||||||
|
void scriptLine(std::string text, bool error = false) {
|
||||||
|
size_t at = 0;
|
||||||
|
for (size_t nl; (nl = text.find('\n', at)) != std::string::npos; at = nl + 1) scriptLog_.push_back({text.substr(at, nl - at), error});
|
||||||
|
scriptLog_.push_back({text.substr(at), error});
|
||||||
|
if (scriptLog_.size() > 2000) scriptLog_.erase(scriptLog_.begin(), scriptLog_.begin() + 500);
|
||||||
|
scriptLogToBottom_ = true;
|
||||||
|
}
|
||||||
|
// Runs the Script panel's script: one undo step; if it stops with an error, everything it did
|
||||||
|
// is taken back.
|
||||||
|
void runScriptNow() {
|
||||||
|
if (mode_ == Mode::Edit) leaveEdit();
|
||||||
|
const std::string name = scriptPath_.empty() ? "script" : fileName(scriptPath_);
|
||||||
|
pushUndo();
|
||||||
|
bool frame = false;
|
||||||
|
fry::ScriptHost host{&scene_, &selected_, &active_, [this](const std::string& t) { scriptLine(t); }, [&] { frame = true; }};
|
||||||
|
const fry::ScriptResult r = fry::runScript(host, scriptCode_, name);
|
||||||
|
touchAll();
|
||||||
|
if (r.ok && frame) frameAll();
|
||||||
|
if (!r.ok) {
|
||||||
|
undo();
|
||||||
|
redo_.clear();
|
||||||
|
scriptLine(r.error, true);
|
||||||
|
showScript_ = true;
|
||||||
|
return say("The script stopped (nothing it did was kept): " + r.error.substr(0, r.error.find('\n')));
|
||||||
|
}
|
||||||
|
dirty_ = true;
|
||||||
|
say(r.objectsMade ? std::format("{} ran: {} object{} made (Ctrl+Z takes it all back)", name, r.objectsMade, r.objectsMade == 1 ? "" : "s")
|
||||||
|
: std::format("{} ran (Ctrl+Z takes it back)", name));
|
||||||
|
}
|
||||||
|
void openScript(const std::string& path, const std::string& shownName) {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
if (!in) return say("Couldn't open " + shownName);
|
||||||
|
scriptCode_.assign(std::istreambuf_iterator<char>(in), {});
|
||||||
|
scriptPath_ = shownName;
|
||||||
|
showScript_ = true;
|
||||||
|
say("Opened " + shownName + ": F5 runs it");
|
||||||
|
}
|
||||||
|
void saveScript(std::string file) {
|
||||||
|
if (isUri(file)) {
|
||||||
|
const std::string temp = tempFile("script.lua");
|
||||||
|
std::string error;
|
||||||
|
const bool ok = writeFileSafely(temp, scriptCode_.data(), scriptCode_.size(), &error) && toUri(temp, file);
|
||||||
|
return say(ok ? "Saved " + fileName(file) : "Couldn't write the file you picked");
|
||||||
|
}
|
||||||
|
if (stdfs::path(file).extension() != ".lua") file += ".lua";
|
||||||
|
std::string error;
|
||||||
|
if (!writeFileSafely(file, scriptCode_.data(), scriptCode_.size(), &error)) return say("Couldn't save the script: " + error);
|
||||||
|
scriptPath_ = file;
|
||||||
|
say("Saved " + fileName(file));
|
||||||
|
}
|
||||||
|
// The scripts that come with it (content/Fry3DX/scripts), as menu items that load one.
|
||||||
|
void exampleScripts() {
|
||||||
|
if (!ImGui::BeginMenu("Examples")) return;
|
||||||
|
for (const std::string& f : keel::fs::list("scripts", ".lua")) {
|
||||||
|
std::string name = stdfs::path(f).stem().string();
|
||||||
|
std::replace(name.begin(), name.end(), '_', ' ');
|
||||||
|
if (ImGui::MenuItem(name.c_str()))
|
||||||
|
if (auto text = keel::fs::readText(f)) scriptCode_ = *text, scriptPath_ = stdfs::path(f).filename().string(), showScript_ = true;
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
static int growString(ImGuiInputTextCallbackData* d) {
|
||||||
|
if (d->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||||
|
auto* str = static_cast<std::string*>(d->UserData);
|
||||||
|
str->resize(size_t(d->BufTextLen));
|
||||||
|
d->Buf = str->data();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void scriptPanel() {
|
||||||
|
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + vp->WorkSize.y * 0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(std::min(620.0f, vp->WorkSize.x * 0.45f), vp->WorkSize.y * 0.7f), ImGuiCond_FirstUseEver);
|
||||||
|
if (!ImGui::Begin("Script", &showScript_)) return ImGui::End();
|
||||||
|
pushAccent();
|
||||||
|
if (ImGui::Button("Run (F5)")) runScriptNow();
|
||||||
|
popAccent();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Open...")) dialog(10);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Save as...")) dialog(11);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Examples")) ImGui::OpenPopup("examples");
|
||||||
|
if (ImGui::BeginPopup("examples")) {
|
||||||
|
for (const std::string& f : keel::fs::list("scripts", ".lua")) {
|
||||||
|
std::string name = stdfs::path(f).stem().string();
|
||||||
|
std::replace(name.begin(), name.end(), '_', ' ');
|
||||||
|
if (ImGui::Selectable(name.c_str()))
|
||||||
|
if (auto text = keel::fs::readText(f)) scriptCode_ = *text, scriptPath_ = stdfs::path(f).filename().string();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(showScriptHelp_ ? "Hide help" : "?")) showScriptHelp_ = !showScriptHelp_;
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Everything a script can use");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("%s", scriptPath_.empty() ? "(not saved)" : fileName(scriptPath_).c_str());
|
||||||
|
|
||||||
|
const float logH = std::max(90.0f, ImGui::GetContentRegionAvail().y * 0.22f);
|
||||||
|
ImGui::PushFont(keel::ui::font(keel::ui::Font::Mono), 0.0f);
|
||||||
|
if (showScriptHelp_) {
|
||||||
|
ImGui::BeginChild("help", ImVec2(0, ImGui::GetContentRegionAvail().y * 0.45f), ImGuiChildFlags_FrameStyle);
|
||||||
|
ImGui::TextUnformatted(fry::scriptHelp());
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
const ImVec2 editor(-FLT_MIN, std::max(120.0f, ImGui::GetContentRegionAvail().y - logH - ImGui::GetStyle().ItemSpacing.y));
|
||||||
|
ImGui::InputTextMultiline("##code", scriptCode_.data(), scriptCode_.capacity() + 1, editor,
|
||||||
|
ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_CallbackResize, growString, &scriptCode_);
|
||||||
|
const bool typing = ImGui::IsItemActive();
|
||||||
|
ImGui::BeginChild("log", ImVec2(0, 0), ImGuiChildFlags_FrameStyle);
|
||||||
|
if (scriptLog_.empty()) ImGui::TextDisabled("What it prints shows here.");
|
||||||
|
for (const auto& [line, error] : scriptLog_) {
|
||||||
|
if (error) ImGui::PushStyleColor(ImGuiCol_Text, roleColor(keel::ui::Role::DangerText));
|
||||||
|
ImGui::TextUnformatted(line.c_str());
|
||||||
|
if (error) ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
if (std::exchange(scriptLogToBottom_, false)) ImGui::SetScrollHereY(1.0f);
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopFont();
|
||||||
|
if (typing && ImGui::GetIO().KeyCtrl && (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false))) runScriptNow();
|
||||||
|
if (typing && ImGui::IsKeyPressed(ImGuiKey_F5, false)) runScriptNow();
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
void helpWindow() {
|
void helpWindow() {
|
||||||
ImGui::SetNextWindowSize(ImVec2(580, 620), ImGuiCond_FirstUseEver);
|
ImGui::SetNextWindowSize(ImVec2(580, 620), ImGuiCond_FirstUseEver);
|
||||||
if (!ImGui::Begin("Keys and mouse", &showHelp_)) return ImGui::End();
|
if (!ImGui::Begin("Keys and mouse", &showHelp_)) return ImGui::End();
|
||||||
|
|
|
||||||
806
src/script.cpp
Normal file
806
src/script.cpp
Normal file
|
|
@ -0,0 +1,806 @@
|
||||||
|
#include "script.h"
|
||||||
|
#include "shapes.h"
|
||||||
|
|
||||||
|
// (Lua is built as C++ here — cmake/Dependencies.cmake — so its errors are exceptions and unwind
|
||||||
|
// through this file's strings and vectors properly.)
|
||||||
|
#include <lauxlib.h>
|
||||||
|
#include <lua.h>
|
||||||
|
#include <lualib.h>
|
||||||
|
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
namespace fry {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const char* kObject = "fry.object";
|
||||||
|
const char* kVec = "fry.vec";
|
||||||
|
|
||||||
|
struct Context {
|
||||||
|
ScriptHost* host;
|
||||||
|
std::chrono::steady_clock::time_point start;
|
||||||
|
int made = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Context& context(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, "fry.context");
|
||||||
|
auto* c = static_cast<Context*>(lua_touserdata(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return *c;
|
||||||
|
}
|
||||||
|
Scene& scene(lua_State* L) { return *context(L).host->scene; }
|
||||||
|
|
||||||
|
Object* findObject(Scene& s, uint32_t id) {
|
||||||
|
for (Object& o : s.objects)
|
||||||
|
if (o.id == id) return &o;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lower(std::string s) {
|
||||||
|
for (char& c : s) c = char(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Vectors: {x, y, z} tables that print and add up ──
|
||||||
|
|
||||||
|
void pushVec(lua_State* L, glm::vec3 v) {
|
||||||
|
lua_createtable(L, 0, 3);
|
||||||
|
lua_pushnumber(L, v.x), lua_setfield(L, -2, "x");
|
||||||
|
lua_pushnumber(L, v.y), lua_setfield(L, -2, "y");
|
||||||
|
lua_pushnumber(L, v.z), lua_setfield(L, -2, "z");
|
||||||
|
luaL_setmetatable(L, kVec);
|
||||||
|
}
|
||||||
|
// A vector from a table ({x=, y=, z=} or {1, 2, 3}) or one number (the same all ways).
|
||||||
|
glm::vec3 toVec(lua_State* L, int i) {
|
||||||
|
if (lua_isnumber(L, i)) return glm::vec3(float(lua_tonumber(L, i)));
|
||||||
|
luaL_checktype(L, i, LUA_TTABLE);
|
||||||
|
glm::vec3 v(0.0f);
|
||||||
|
const char* names[] = {"x", "y", "z"};
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
if (lua_getfield(L, i, names[k]) == LUA_TNIL) lua_pop(L, 1), lua_rawgeti(L, i, k + 1);
|
||||||
|
v[k] = float(lua_tonumber(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
// Arguments from i: three numbers, or one vector.
|
||||||
|
glm::vec3 argVec(lua_State* L, int i) {
|
||||||
|
if (lua_isnumber(L, i) && lua_isnumber(L, i + 1)) return {float(lua_tonumber(L, i)), float(luaL_checknumber(L, i + 1)), float(luaL_optnumber(L, i + 2, 0))};
|
||||||
|
return toVec(L, i);
|
||||||
|
}
|
||||||
|
int vecOp(lua_State* L, char op) {
|
||||||
|
const bool scalarB = lua_isnumber(L, 2), scalarA = lua_isnumber(L, 1);
|
||||||
|
const glm::vec3 a = toVec(L, 1), b = toVec(L, 2);
|
||||||
|
if (op == '+') pushVec(L, a + b);
|
||||||
|
else if (op == '-') pushVec(L, a - b);
|
||||||
|
else if (op == '*') pushVec(L, a * b);
|
||||||
|
else pushVec(L, scalarA && !scalarB ? b / a : a / b);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int vecAdd(lua_State* L) { return vecOp(L, '+'); }
|
||||||
|
int vecSub(lua_State* L) { return vecOp(L, '-'); }
|
||||||
|
int vecMul(lua_State* L) { return vecOp(L, '*'); }
|
||||||
|
int vecDiv(lua_State* L) { return vecOp(L, '/'); }
|
||||||
|
int vecUnm(lua_State* L) { return pushVec(L, -toVec(L, 1)), 1; }
|
||||||
|
int vecLen(lua_State* L) { return lua_pushnumber(L, glm::length(toVec(L, 1))), 1; }
|
||||||
|
int vecString(lua_State* L) {
|
||||||
|
const glm::vec3 v = toVec(L, 1);
|
||||||
|
lua_pushstring(L, std::format("({:.4g}, {:.4g}, {:.4g})", v.x, v.y, v.z).c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A colour: {r, g, b} (0..1), "#rrggbb", or a vector.
|
||||||
|
glm::vec3 toColor(lua_State* L, int i) {
|
||||||
|
if (lua_type(L, i) == LUA_TSTRING) {
|
||||||
|
std::string s = lua_tostring(L, i);
|
||||||
|
if (!s.empty() && s[0] == '#') s.erase(0, 1);
|
||||||
|
if (s.size() == 3) s = {s[0], s[0], s[1], s[1], s[2], s[2]};
|
||||||
|
unsigned v = 0;
|
||||||
|
if (s.size() != 6 || std::sscanf(s.c_str(), "%x", &v) != 1) luaL_error(L, "a colour is \"#rrggbb\" or {r, g, b} (0 to 1)");
|
||||||
|
return glm::vec3(float((v >> 16) & 255), float((v >> 8) & 255), float(v & 255)) / 255.0f;
|
||||||
|
}
|
||||||
|
if (lua_istable(L, i)) {
|
||||||
|
glm::vec3 c(0.0f);
|
||||||
|
const char* names[] = {"r", "g", "b"};
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
if (lua_getfield(L, i, names[k]) == LUA_TNIL) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (lua_getfield(L, i, k == 0 ? "x" : k == 1 ? "y" : "z") == LUA_TNIL) lua_pop(L, 1), lua_rawgeti(L, i, k + 1);
|
||||||
|
}
|
||||||
|
c[k] = float(lua_tonumber(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
return glm::clamp(c, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
luaL_error(L, "a colour is \"#rrggbb\" or {r, g, b} (0 to 1)");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Objects: handles by id ──
|
||||||
|
|
||||||
|
void pushObject(lua_State* L, uint32_t id) {
|
||||||
|
*static_cast<uint32_t*>(lua_newuserdatauv(L, sizeof(uint32_t), 0)) = id;
|
||||||
|
luaL_setmetatable(L, kObject);
|
||||||
|
}
|
||||||
|
uint32_t objectId(lua_State* L, int i) { return *static_cast<uint32_t*>(luaL_checkudata(L, i, kObject)); }
|
||||||
|
Object& object(lua_State* L, int i) {
|
||||||
|
Object* o = findObject(scene(L), objectId(L, i));
|
||||||
|
if (!o) luaL_error(L, "that object isn't there any more");
|
||||||
|
return *o;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string uniqueName(Scene& s, const std::string& base) {
|
||||||
|
auto taken = [&](const std::string& n) { return std::any_of(s.objects.begin(), s.objects.end(), [&](const Object& o) { return o.name == n; }); };
|
||||||
|
if (!taken(base)) return base;
|
||||||
|
for (int i = 2;; ++i)
|
||||||
|
if (!taken(std::format("{} {}", base, i))) return std::format("{} {}", base, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object& newObject(lua_State* L, const std::string& name, Mesh mesh) {
|
||||||
|
Scene& s = scene(L);
|
||||||
|
Object o;
|
||||||
|
o.id = s.nextId++;
|
||||||
|
o.name = uniqueName(s, name);
|
||||||
|
o.mesh = std::move(mesh);
|
||||||
|
s.objects.push_back(std::move(o));
|
||||||
|
++context(L).made;
|
||||||
|
return s.objects.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handMade(Object& o) { o.recipe.clear(), o.settings.clear(); }
|
||||||
|
|
||||||
|
std::string mirrorText(int m) {
|
||||||
|
std::string s;
|
||||||
|
for (int a = 0; a < 3; ++a)
|
||||||
|
if (m & (1 << a)) s += char('x' + a);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading a property: false if there's none by that name.
|
||||||
|
bool getProperty(lua_State* L, Object& o, const std::string& k) {
|
||||||
|
if (k == "id") lua_pushinteger(L, o.id);
|
||||||
|
else if (k == "name") lua_pushstring(L, o.name.c_str());
|
||||||
|
else if (k == "position") pushVec(L, o.position);
|
||||||
|
else if (k == "turn") pushVec(L, toEuler(o.rotation));
|
||||||
|
else if (k == "size") pushVec(L, o.scale);
|
||||||
|
else if (k == "color" || k == "colour") {
|
||||||
|
lua_createtable(L, 3, 0);
|
||||||
|
for (int i = 0; i < 3; ++i) lua_pushnumber(L, o.color[i]), lua_rawseti(L, -2, i + 1);
|
||||||
|
} else if (k == "metal") lua_pushnumber(L, o.metal);
|
||||||
|
else if (k == "rough") lua_pushnumber(L, o.rough);
|
||||||
|
else if (k == "smooth") lua_pushboolean(L, o.smooth);
|
||||||
|
else if (k == "visible") lua_pushboolean(L, o.visible);
|
||||||
|
else if (k == "locked") lua_pushboolean(L, o.locked);
|
||||||
|
else if (k == "mirror") lua_pushstring(L, mirrorText(o.mirror).c_str());
|
||||||
|
else if (k == "round") lua_pushnumber(L, o.round);
|
||||||
|
else if (k == "round_steps") lua_pushinteger(L, o.roundSteps);
|
||||||
|
else if (k == "smooth_levels") lua_pushinteger(L, o.smoothLevels);
|
||||||
|
else if (k == "shape") lua_pushstring(L, o.recipe.c_str());
|
||||||
|
else if (k == "point_count") lua_pushinteger(L, lua_Integer(o.mesh.verts.size()));
|
||||||
|
else if (k == "face_count") lua_pushinteger(L, lua_Integer(o.mesh.faces.size()));
|
||||||
|
else return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing one: false if there's none by that name.
|
||||||
|
bool setProperty(lua_State* L, Object& o, const std::string& k, int v) {
|
||||||
|
if (k == "name") o.name = luaL_checkstring(L, v);
|
||||||
|
else if (k == "position") o.position = toVec(L, v);
|
||||||
|
else if (k == "turn") o.rotation = fromEuler(toVec(L, v));
|
||||||
|
else if (k == "size") o.scale = toVec(L, v);
|
||||||
|
else if (k == "color" || k == "colour") o.color = toColor(L, v);
|
||||||
|
else if (k == "metal") o.metal = std::clamp(float(luaL_checknumber(L, v)), 0.0f, 1.0f);
|
||||||
|
else if (k == "rough") o.rough = std::clamp(float(luaL_checknumber(L, v)), 0.0f, 1.0f);
|
||||||
|
else if (k == "smooth") o.smooth = lua_toboolean(L, v);
|
||||||
|
else if (k == "visible") o.visible = lua_toboolean(L, v);
|
||||||
|
else if (k == "locked") o.locked = lua_toboolean(L, v);
|
||||||
|
else if (k == "mirror") {
|
||||||
|
const std::string s = lower(luaL_checkstring(L, v));
|
||||||
|
o.mirror = (s.find('x') != std::string::npos ? 1 : 0) | (s.find('y') != std::string::npos ? 2 : 0) | (s.find('z') != std::string::npos ? 4 : 0);
|
||||||
|
} else if (k == "round") o.round = std::max(0.0f, float(luaL_checknumber(L, v)));
|
||||||
|
else if (k == "round_steps") o.roundSteps = std::clamp(int(luaL_checkinteger(L, v)), 1, 12);
|
||||||
|
else if (k == "smooth_levels") o.smoothLevels = std::clamp(int(luaL_checkinteger(L, v)), 0, 4);
|
||||||
|
else return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A library shape's setting by name (any case): its index, or -1.
|
||||||
|
int settingIndex(const ShapeRecipe& r, const std::string& name) {
|
||||||
|
for (size_t i = 0; i < r.settings.size(); ++i)
|
||||||
|
if (lower(r.settings[i].name) == lower(name)) return int(i);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
void remake(Object& o) {
|
||||||
|
if (const ShapeRecipe* r = findRecipe(o.recipe)) o.mesh = makeFromRecipe(*r, o.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The objects in arguments from `first` on: objects, or lists of them.
|
||||||
|
std::vector<uint32_t> objectArgs(lua_State* L, int first) {
|
||||||
|
std::vector<uint32_t> ids;
|
||||||
|
for (int i = first; i <= lua_gettop(L); ++i) {
|
||||||
|
if (lua_istable(L, i) && !luaL_testudata(L, i, kObject)) {
|
||||||
|
const lua_Integer n = luaL_len(L, i);
|
||||||
|
for (lua_Integer k = 1; k <= n; ++k) {
|
||||||
|
lua_rawgeti(L, i, k);
|
||||||
|
ids.push_back(objectId(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
} else ids.push_back(objectId(L, i));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Object methods ──
|
||||||
|
|
||||||
|
int objMove(lua_State* L) {
|
||||||
|
object(L, 1).position += argVec(L, 2);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objRotate(lua_State* L) {
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
o.rotation = glm::normalize(fromEuler(argVec(L, 2)) * o.rotation);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objScale(lua_State* L) {
|
||||||
|
object(L, 1).scale *= lua_gettop(L) == 2 && lua_isnumber(L, 2) ? glm::vec3(float(lua_tonumber(L, 2))) : argVec(L, 2);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objDelete(lua_State* L) {
|
||||||
|
const uint32_t id = objectId(L, 1);
|
||||||
|
Context& c = context(L);
|
||||||
|
std::erase_if(c.host->scene->objects, [&](const Object& o) { return o.id == id; });
|
||||||
|
if (c.host->selected) c.host->selected->erase(id);
|
||||||
|
if (c.host->active && *c.host->active == id) *c.host->active = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int objCopy(lua_State* L) {
|
||||||
|
Object copy = object(L, 1);
|
||||||
|
Scene& s = scene(L);
|
||||||
|
copy.id = s.nextId++;
|
||||||
|
copy.name = uniqueName(s, copy.name);
|
||||||
|
s.objects.push_back(std::move(copy));
|
||||||
|
++context(L).made;
|
||||||
|
pushObject(L, s.objects.back().id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objCombine(lua_State* L, Combine op) {
|
||||||
|
const uint32_t keep = objectId(L, 1);
|
||||||
|
const std::vector<uint32_t> others = objectArgs(L, 2);
|
||||||
|
Scene& s = scene(L);
|
||||||
|
Object& a = object(L, 1);
|
||||||
|
Mesh result = a.hasFinish() ? finishedMesh(a) : a.mesh;
|
||||||
|
const glm::mat4 into = glm::inverse(objectMatrix(a));
|
||||||
|
for (uint32_t id : others) {
|
||||||
|
const Object* b = findObject(s, id);
|
||||||
|
if (!b || id == keep) continue;
|
||||||
|
Mesh m = b->hasFinish() ? finishedMesh(*b) : b->mesh;
|
||||||
|
transform(m, into * objectMatrix(*b));
|
||||||
|
if (!combine(result, m, op)) return luaL_error(L, op == Combine::Overlap ? "they don't overlap: nothing would be left" : "that wouldn't leave anything");
|
||||||
|
}
|
||||||
|
Object& kept = object(L, 1);
|
||||||
|
kept.mesh = std::move(result);
|
||||||
|
kept.mirror = 0, kept.round = 0, kept.smoothLevels = 0;
|
||||||
|
handMade(kept);
|
||||||
|
std::erase_if(s.objects, [&](const Object& o) { return o.id != keep && std::find(others.begin(), others.end(), o.id) != others.end(); });
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objAdd(lua_State* L) { return objCombine(L, Combine::Add); }
|
||||||
|
int objSubtract(lua_State* L) { return objCombine(L, Combine::Subtract); }
|
||||||
|
int objOverlap(lua_State* L) { return objCombine(L, Combine::Overlap); }
|
||||||
|
int objJoin(lua_State* L) {
|
||||||
|
const uint32_t keep = objectId(L, 1);
|
||||||
|
const std::vector<uint32_t> others = objectArgs(L, 2);
|
||||||
|
Scene& s = scene(L);
|
||||||
|
const glm::mat4 into = glm::inverse(objectMatrix(object(L, 1)));
|
||||||
|
Mesh joined = object(L, 1).mesh;
|
||||||
|
for (uint32_t id : others)
|
||||||
|
if (const Object* b = findObject(s, id); b && id != keep) append(joined, b->mesh, into * objectMatrix(*b));
|
||||||
|
Object& a = object(L, 1);
|
||||||
|
a.mesh = std::move(joined);
|
||||||
|
handMade(a);
|
||||||
|
std::erase_if(s.objects, [&](const Object& o) { return o.id != keep && std::find(others.begin(), others.end(), o.id) != others.end(); });
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objThicken(lua_State* L) {
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
thicken(o.mesh, float(luaL_checknumber(L, 2)));
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objSubdivide(lua_State* L) {
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
const bool smooth = lua_isnone(L, 2) || lua_toboolean(L, 2);
|
||||||
|
const int times = std::clamp(int(luaL_optinteger(L, 3, 1)), 1, 4);
|
||||||
|
for (int i = 0; i < times; ++i) subdivide(o.mesh, smooth);
|
||||||
|
if (o.mesh.faces.size() > 2000000) return luaL_error(L, "that's too many faces");
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objBevel(lua_State* L) {
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
const float width = float(luaL_checknumber(L, 2));
|
||||||
|
const int steps = std::clamp(int(luaL_optinteger(L, 3, 3)), 1, 12);
|
||||||
|
const float angle = float(luaL_optnumber(L, 4, 30.0));
|
||||||
|
bevelEdges(o.mesh, sharpEdges(o.mesh, angle), width, steps);
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objBake(lua_State* L) {
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
o.mesh = finishedMesh(o);
|
||||||
|
o.mirror = 0, o.round = 0, o.smoothLevels = 0;
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objPoints(lua_State* L) {
|
||||||
|
const Object& o = object(L, 1);
|
||||||
|
lua_createtable(L, int(o.mesh.verts.size()), 0);
|
||||||
|
for (size_t i = 0; i < o.mesh.verts.size(); ++i) pushVec(L, o.mesh.verts[i]), lua_rawseti(L, -2, lua_Integer(i + 1));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objFaces(lua_State* L) {
|
||||||
|
const Object& o = object(L, 1);
|
||||||
|
lua_createtable(L, int(o.mesh.faces.size()), 0);
|
||||||
|
for (size_t f = 0; f < o.mesh.faces.size(); ++f) {
|
||||||
|
lua_createtable(L, int(o.mesh.faces[f].size()), 0);
|
||||||
|
for (size_t c = 0; c < o.mesh.faces[f].size(); ++c) lua_pushinteger(L, o.mesh.faces[f][c] + 1), lua_rawseti(L, -2, lua_Integer(c + 1));
|
||||||
|
lua_rawseti(L, -2, lua_Integer(f + 1));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::vector<glm::vec3> pointList(lua_State* L, int i) {
|
||||||
|
luaL_checktype(L, i, LUA_TTABLE);
|
||||||
|
std::vector<glm::vec3> pts;
|
||||||
|
const lua_Integer n = luaL_len(L, i);
|
||||||
|
for (lua_Integer k = 1; k <= n; ++k) {
|
||||||
|
lua_rawgeti(L, i, k);
|
||||||
|
pts.push_back(toVec(L, lua_gettop(L)));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
std::vector<std::vector<int>> faceList(lua_State* L, int i, size_t points) {
|
||||||
|
luaL_checktype(L, i, LUA_TTABLE);
|
||||||
|
std::vector<std::vector<int>> faces;
|
||||||
|
const lua_Integer n = luaL_len(L, i);
|
||||||
|
for (lua_Integer k = 1; k <= n; ++k) {
|
||||||
|
lua_rawgeti(L, i, k);
|
||||||
|
luaL_checktype(L, -1, LUA_TTABLE);
|
||||||
|
std::vector<int> f;
|
||||||
|
const lua_Integer m = luaL_len(L, -1);
|
||||||
|
for (lua_Integer c = 1; c <= m; ++c) {
|
||||||
|
lua_rawgeti(L, -1, c);
|
||||||
|
const lua_Integer v = lua_tointeger(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (v < 1 || size_t(v) > points) luaL_error(L, "face %d: point %d isn't one of the %d points", int(k), int(v), int(points));
|
||||||
|
f.push_back(int(v - 1));
|
||||||
|
}
|
||||||
|
if (f.size() < 3) luaL_error(L, "face %d has fewer than 3 points", int(k));
|
||||||
|
faces.push_back(std::move(f));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
int objSetPoints(lua_State* L) {
|
||||||
|
std::vector<glm::vec3> pts = pointList(L, 2);
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
if (pts.size() != o.mesh.verts.size()) return luaL_error(L, "it has %d points, not %d (setshape changes how many)", int(o.mesh.verts.size()), int(pts.size()));
|
||||||
|
o.mesh.verts = std::move(pts);
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objSetShape(lua_State* L) {
|
||||||
|
std::vector<glm::vec3> pts = pointList(L, 2);
|
||||||
|
auto faces = faceList(L, 3, pts.size());
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
o.mesh = {std::move(pts), std::move(faces)};
|
||||||
|
o.uvs.clear(), o.picture.reset(), o.layers.clear();
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objSet(lua_State* L) { // o:set("Width", 2) or o:set{width = 2, height = 1}
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
const ShapeRecipe* r = findRecipe(o.recipe);
|
||||||
|
if (!r) return luaL_error(L, "%s isn't a library shape any more (its shape was changed by hand)", o.name.c_str());
|
||||||
|
auto one = [&](const std::string& name, float value) {
|
||||||
|
const int i = settingIndex(*r, name);
|
||||||
|
if (i < 0) luaL_error(L, "a %s has no setting called %s", r->name, name.c_str());
|
||||||
|
o.settings.resize(r->settings.size());
|
||||||
|
const ShapeSetting& st = r->settings[size_t(i)];
|
||||||
|
o.settings[size_t(i)] = std::clamp(st.whole ? std::round(value) : value, st.min, st.max);
|
||||||
|
};
|
||||||
|
if (lua_istable(L, 2)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, 2)) one(luaL_checkstring(L, -2), float(luaL_checknumber(L, -1))), lua_pop(L, 1);
|
||||||
|
} else one(luaL_checkstring(L, 2), float(luaL_checknumber(L, 3)));
|
||||||
|
remake(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objSettings(lua_State* L) {
|
||||||
|
const Object& o = object(L, 1);
|
||||||
|
lua_newtable(L);
|
||||||
|
if (const ShapeRecipe* r = findRecipe(o.recipe)) {
|
||||||
|
const std::vector<float> s = o.settings.size() == r->settings.size() ? o.settings : defaultSettings(*r);
|
||||||
|
for (size_t i = 0; i < r->settings.size(); ++i) lua_pushnumber(L, s[i]), lua_setfield(L, -2, lower(r->settings[i].name).c_str());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objBounds(lua_State* L) {
|
||||||
|
const Object& o = object(L, 1);
|
||||||
|
glm::vec3 lo(1e30f), hi(-1e30f);
|
||||||
|
const glm::mat4 m = objectMatrix(o);
|
||||||
|
for (const glm::vec3& v : o.mesh.verts) {
|
||||||
|
const glm::vec3 w = glm::vec3(m * glm::vec4(v, 1.0f));
|
||||||
|
lo = glm::min(lo, w), hi = glm::max(hi, w);
|
||||||
|
}
|
||||||
|
if (o.mesh.verts.empty()) lo = hi = o.position;
|
||||||
|
pushVec(L, lo), pushVec(L, hi);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int objSelect(lua_State* L) {
|
||||||
|
const uint32_t id = objectId(L, 1);
|
||||||
|
Context& c = context(L);
|
||||||
|
if (c.host->selected) c.host->selected->insert(id);
|
||||||
|
if (c.host->active) *c.host->active = id;
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objDrop(lua_State* L) { // stands it on the ground (z = 0)
|
||||||
|
lua_settop(L, 1);
|
||||||
|
lua_getfield(L, 1, "bounds");
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_call(L, 1, 1);
|
||||||
|
const float bottom = toVec(L, 2).z;
|
||||||
|
object(L, 1).position.z -= bottom;
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const luaL_Reg kMethods[] = {
|
||||||
|
{"move", objMove}, {"rotate", objRotate}, {"scale", objScale}, {"delete", objDelete}, {"copy", objCopy},
|
||||||
|
{"add", objAdd}, {"subtract", objSubtract}, {"overlap", objOverlap}, {"join", objJoin}, {"thicken", objThicken},
|
||||||
|
{"subdivide", objSubdivide}, {"bevel", objBevel}, {"bake", objBake}, {"points", objPoints}, {"faces", objFaces},
|
||||||
|
{"setpoints", objSetPoints}, {"setshape", objSetShape}, {"set", objSet}, {"settings", objSettings}, {"bounds", objBounds},
|
||||||
|
{"select", objSelect}, {"drop", objDrop}, {nullptr, nullptr},
|
||||||
|
};
|
||||||
|
|
||||||
|
int objIndex(lua_State* L) {
|
||||||
|
const std::string k = luaL_checkstring(L, 2);
|
||||||
|
for (const luaL_Reg* m = kMethods; m->name; ++m)
|
||||||
|
if (k == m->name) return lua_pushcfunction(L, m->func), 1;
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
if (getProperty(L, o, k)) return 1;
|
||||||
|
if (const ShapeRecipe* r = findRecipe(o.recipe); r && settingIndex(*r, k) >= 0) { // o.width, o.sides... for library shapes
|
||||||
|
const std::vector<float> s = o.settings.size() == r->settings.size() ? o.settings : defaultSettings(*r);
|
||||||
|
return lua_pushnumber(L, s[size_t(settingIndex(*r, k))]), 1;
|
||||||
|
}
|
||||||
|
return luaL_error(L, "objects have no %s (see the Script panel's help)", k.c_str());
|
||||||
|
}
|
||||||
|
int objNewIndex(lua_State* L) {
|
||||||
|
const std::string k = luaL_checkstring(L, 2);
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
if (setProperty(L, o, k, 3)) return 0;
|
||||||
|
if (const ShapeRecipe* r = findRecipe(o.recipe); r && settingIndex(*r, k) >= 0) {
|
||||||
|
lua_settop(L, 3);
|
||||||
|
lua_pushcfunction(L, objSet);
|
||||||
|
lua_pushvalue(L, 1), lua_pushvalue(L, 2), lua_pushvalue(L, 3);
|
||||||
|
lua_call(L, 3, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return luaL_error(L, "objects have no %s to change (see the Script panel's help)", k.c_str());
|
||||||
|
}
|
||||||
|
int objString(lua_State* L) {
|
||||||
|
const Object* o = findObject(scene(L), objectId(L, 1));
|
||||||
|
lua_pushstring(L, o ? std::format("{} (#{})", o->name, o->id).c_str() : "(gone)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int objEq(lua_State* L) { return lua_pushboolean(L, objectId(L, 1) == objectId(L, 2)), 1; }
|
||||||
|
|
||||||
|
// Sets the properties (and library settings) in a table on the object at index o.
|
||||||
|
void applyOptions(lua_State* L, int obj, int opts) {
|
||||||
|
if (!lua_istable(L, opts)) return;
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, opts)) {
|
||||||
|
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||||
|
lua_pushvalue(L, -2), lua_insert(L, -2); // key, key, value
|
||||||
|
lua_pushcfunction(L, objNewIndex);
|
||||||
|
lua_pushvalue(L, obj), lua_pushvalue(L, -4), lua_pushvalue(L, -4);
|
||||||
|
lua_call(L, 3, 0);
|
||||||
|
lua_pop(L, 2);
|
||||||
|
} else lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── fry.* ──
|
||||||
|
|
||||||
|
int fryAdd(lua_State* L) {
|
||||||
|
const std::string id = lower(luaL_checkstring(L, 1));
|
||||||
|
const ShapeRecipe* r = findRecipe(id);
|
||||||
|
if (!r) return luaL_error(L, "there's no shape called %s (fry.shapes() lists them)", id.c_str());
|
||||||
|
Object& o = newObject(L, r->name, makeFromRecipe(*r, defaultSettings(*r)));
|
||||||
|
o.recipe = r->id, o.settings = defaultSettings(*r), o.smooth = r->smooth;
|
||||||
|
pushObject(L, o.id);
|
||||||
|
applyOptions(L, lua_gettop(L), 2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryMesh(lua_State* L) {
|
||||||
|
std::vector<glm::vec3> pts = pointList(L, 1);
|
||||||
|
auto faces = faceList(L, 2, pts.size());
|
||||||
|
Object& o = newObject(L, luaL_optstring(L, 3, "Mesh"), Mesh{std::move(pts), std::move(faces)});
|
||||||
|
pushObject(L, o.id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryLathe(lua_State* L) { // a profile of {out, up} points turned round Z
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
std::vector<glm::vec2> profile;
|
||||||
|
const lua_Integer n = luaL_len(L, 1);
|
||||||
|
for (lua_Integer k = 1; k <= n; ++k) {
|
||||||
|
lua_rawgeti(L, 1, k);
|
||||||
|
const glm::vec3 v = toVec(L, lua_gettop(L));
|
||||||
|
profile.push_back({std::max(v.x, 0.0f), v.y});
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
if (profile.size() < 2) return luaL_error(L, "a lathe needs two or more {out, up} points");
|
||||||
|
const int sides = std::clamp(int(luaL_optinteger(L, 2, 32)), 3, 512);
|
||||||
|
Object& o = newObject(L, luaL_optstring(L, 3, "Turned"), lathe(profile, sides));
|
||||||
|
o.smooth = true;
|
||||||
|
pushObject(L, o.id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryExtrude(lua_State* L) { // an outline on the ground ({x, y} points) made `height` tall
|
||||||
|
std::vector<glm::vec3> pts = pointList(L, 1);
|
||||||
|
const float h = float(luaL_checknumber(L, 2));
|
||||||
|
if (pts.size() < 3) return luaL_error(L, "an outline needs three or more points");
|
||||||
|
for (glm::vec3& p : pts) p.z = 0;
|
||||||
|
Mesh m = extrudeOutline(pts, glm::vec3(0, 0, 1), h);
|
||||||
|
transform(m, glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, h * 0.5f)));
|
||||||
|
Object& o = newObject(L, luaL_optstring(L, 3, "Outline"), std::move(m));
|
||||||
|
pushObject(L, o.id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryObjects(lua_State* L) {
|
||||||
|
const Scene& s = scene(L);
|
||||||
|
lua_createtable(L, int(s.objects.size()), 0);
|
||||||
|
for (size_t i = 0; i < s.objects.size(); ++i) pushObject(L, s.objects[i].id), lua_rawseti(L, -2, lua_Integer(i + 1));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryFind(lua_State* L) {
|
||||||
|
const std::string name = luaL_checkstring(L, 1);
|
||||||
|
for (const Object& o : scene(L).objects)
|
||||||
|
if (o.name == name) return pushObject(L, o.id), 1;
|
||||||
|
return lua_pushnil(L), 1;
|
||||||
|
}
|
||||||
|
int frySelected(lua_State* L) {
|
||||||
|
Context& c = context(L);
|
||||||
|
lua_newtable(L);
|
||||||
|
if (!c.host->selected) return 1;
|
||||||
|
int i = 0;
|
||||||
|
for (const Object& o : c.host->scene->objects) // (in the scene's order, the last picked last)
|
||||||
|
if (c.host->selected->count(o.id) && (!c.host->active || o.id != *c.host->active)) pushObject(L, o.id), lua_rawseti(L, -2, ++i);
|
||||||
|
if (c.host->active && c.host->selected->count(*c.host->active) && findObject(*c.host->scene, *c.host->active))
|
||||||
|
pushObject(L, *c.host->active), lua_rawseti(L, -2, ++i);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int frySelect(lua_State* L) {
|
||||||
|
const std::vector<uint32_t> ids = objectArgs(L, 1);
|
||||||
|
Context& c = context(L);
|
||||||
|
if (c.host->selected) c.host->selected->clear(), c.host->selected->insert(ids.begin(), ids.end());
|
||||||
|
if (c.host->active) *c.host->active = ids.empty() ? 0 : ids.back();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int fryClear(lua_State* L) {
|
||||||
|
Context& c = context(L);
|
||||||
|
c.host->scene->objects.clear();
|
||||||
|
c.host->scene->folders.clear();
|
||||||
|
if (c.host->selected) c.host->selected->clear();
|
||||||
|
if (c.host->active) *c.host->active = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int fryGroup(lua_State* L) {
|
||||||
|
const std::string name = luaL_checkstring(L, 1);
|
||||||
|
const std::vector<uint32_t> ids = objectArgs(L, 2);
|
||||||
|
Scene& s = scene(L);
|
||||||
|
Folder f;
|
||||||
|
f.id = s.nextId++;
|
||||||
|
f.name = name;
|
||||||
|
for (Object& o : s.objects)
|
||||||
|
if (std::find(ids.begin(), ids.end(), o.id) != ids.end()) o.folder = f.id;
|
||||||
|
s.folders.push_back(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int fryShapes(lua_State* L) {
|
||||||
|
lua_newtable(L);
|
||||||
|
int i = 0;
|
||||||
|
for (const ShapeRecipe& r : shapeRecipes()) {
|
||||||
|
lua_createtable(L, 0, 4);
|
||||||
|
lua_pushstring(L, r.id), lua_setfield(L, -2, "id");
|
||||||
|
lua_pushstring(L, r.name), lua_setfield(L, -2, "name");
|
||||||
|
lua_pushboolean(L, r.starter), lua_setfield(L, -2, "starter");
|
||||||
|
lua_createtable(L, int(r.settings.size()), 0);
|
||||||
|
for (size_t k = 0; k < r.settings.size(); ++k) lua_pushstring(L, lower(r.settings[k].name).c_str()), lua_rawseti(L, -2, lua_Integer(k + 1));
|
||||||
|
lua_setfield(L, -2, "settings");
|
||||||
|
lua_rawseti(L, -2, ++i);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryLook(lua_State* L) { // fry.look{sun_turn = 40, sky = false, ...}; returns the look as it is
|
||||||
|
Look& k = scene(L).look;
|
||||||
|
struct Num {
|
||||||
|
const char* name;
|
||||||
|
float* v;
|
||||||
|
} nums[] = {{"sun_turn", &k.sunTurn}, {"sun_height", &k.sunHeight}, {"sun_strength", &k.sunStrength}, {"exposure", &k.exposure},
|
||||||
|
{"bloom", &k.bloom}, {"saturation", &k.saturation}, {"vignette", &k.vignette}, {"shadow_soft", &k.shadowSoft}};
|
||||||
|
struct Col {
|
||||||
|
const char* name;
|
||||||
|
glm::vec3* v;
|
||||||
|
} cols[] = {{"sun_color", &k.sunColor}, {"background", &k.background}, {"ground_color", &k.groundColor}};
|
||||||
|
if (lua_istable(L, 1)) {
|
||||||
|
for (const Num& n : nums)
|
||||||
|
if (lua_getfield(L, 1, n.name) != LUA_TNIL) *n.v = float(luaL_checknumber(L, -1)), lua_pop(L, 1);
|
||||||
|
else lua_pop(L, 1);
|
||||||
|
for (const Col& c : cols)
|
||||||
|
if (lua_getfield(L, 1, c.name) != LUA_TNIL) *c.v = toColor(L, lua_gettop(L)), lua_pop(L, 1);
|
||||||
|
else lua_pop(L, 1);
|
||||||
|
if (lua_getfield(L, 1, "sky") != LUA_TNIL) k.sky = lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (lua_getfield(L, 1, "ground") != LUA_TNIL) k.ground = lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
lua_newtable(L);
|
||||||
|
for (const Num& n : nums) lua_pushnumber(L, *n.v), lua_setfield(L, -2, n.name);
|
||||||
|
for (const Col& c : cols) {
|
||||||
|
lua_createtable(L, 3, 0);
|
||||||
|
for (int i = 0; i < 3; ++i) lua_pushnumber(L, (*c.v)[i]), lua_rawseti(L, -2, i + 1);
|
||||||
|
lua_setfield(L, -2, c.name);
|
||||||
|
}
|
||||||
|
lua_pushboolean(L, k.sky), lua_setfield(L, -2, "sky");
|
||||||
|
lua_pushboolean(L, k.ground), lua_setfield(L, -2, "ground");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fryVec(lua_State* L) { return pushVec(L, lua_istable(L, 1) ? toVec(L, 1) : glm::vec3(float(luaL_optnumber(L, 1, 0)), float(luaL_optnumber(L, 2, 0)), float(luaL_optnumber(L, 3, 0)))), 1; }
|
||||||
|
int fryPrint(lua_State* L) {
|
||||||
|
std::string line;
|
||||||
|
for (int i = 1; i <= lua_gettop(L); ++i) {
|
||||||
|
if (i > 1) line += "\t";
|
||||||
|
line += luaL_tolstring(L, i, nullptr);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
if (const auto& p = context(L).host->print) p(line);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int fryFrame(lua_State* L) {
|
||||||
|
if (const auto& f = context(L).host->frame) f();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int fryHelp(lua_State* L) {
|
||||||
|
if (const auto& p = context(L).host->print) p(scriptHelp());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const luaL_Reg kFry[] = {
|
||||||
|
{"add", fryAdd}, {"mesh", fryMesh}, {"lathe", fryLathe}, {"extrude", fryExtrude}, {"objects", fryObjects}, {"find", fryFind},
|
||||||
|
{"selected", frySelected}, {"select", frySelect}, {"clear", fryClear}, {"group", fryGroup}, {"shapes", fryShapes}, {"look", fryLook},
|
||||||
|
{"vec", fryVec}, {"say", fryPrint}, {"frame", fryFrame}, {"help", fryHelp}, {nullptr, nullptr},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stops a script that's run too long (an endless loop).
|
||||||
|
void watchdog(lua_State* L, lua_Debug*) {
|
||||||
|
const Context& c = context(L);
|
||||||
|
if (std::chrono::duration<double>(std::chrono::steady_clock::now() - c.start).count() > c.host->timeLimit)
|
||||||
|
luaL_error(L, "stopped: it ran for more than %d seconds", int(c.host->timeLimit));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not more memory than this (a runaway table).
|
||||||
|
void* allocate(void* ud, void* ptr, size_t old, size_t size) {
|
||||||
|
size_t& used = *static_cast<size_t*>(ud);
|
||||||
|
if (size == 0) {
|
||||||
|
used -= ptr ? old : 0;
|
||||||
|
std::free(ptr);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const size_t before = ptr ? old : 0;
|
||||||
|
if (used - before + size > (size_t(1) << 30)) return nullptr;
|
||||||
|
void* p = std::realloc(ptr, size);
|
||||||
|
if (p) used = used - before + size;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
int traceback(lua_State* L) {
|
||||||
|
luaL_traceback(L, L, lua_tostring(L, 1), 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScriptResult runScript(ScriptHost& host, const std::string& code, const std::string& name) {
|
||||||
|
size_t used = 0;
|
||||||
|
lua_State* L = lua_newstate(allocate, &used);
|
||||||
|
if (!L) return {false, "out of memory"};
|
||||||
|
Context ctx{&host, std::chrono::steady_clock::now()};
|
||||||
|
lua_pushlightuserdata(L, &ctx);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, "fry.context");
|
||||||
|
const luaL_Reg libs[] = {{LUA_GNAME, luaopen_base}, {LUA_TABLIBNAME, luaopen_table}, {LUA_STRLIBNAME, luaopen_string},
|
||||||
|
{LUA_MATHLIBNAME, luaopen_math}, {LUA_UTF8LIBNAME, luaopen_utf8}, {LUA_COLIBNAME, luaopen_coroutine}};
|
||||||
|
for (const luaL_Reg& l : libs) luaL_requiref(L, l.name, l.func, 1), lua_pop(L, 1);
|
||||||
|
for (const char* gone : {"dofile", "loadfile", "load", "collectgarbage"}) lua_pushnil(L), lua_setglobal(L, gone);
|
||||||
|
lua_pushcfunction(L, fryPrint);
|
||||||
|
lua_setglobal(L, "print");
|
||||||
|
|
||||||
|
luaL_newmetatable(L, kVec);
|
||||||
|
const luaL_Reg vecMeta[] = {{"__add", vecAdd}, {"__sub", vecSub}, {"__mul", vecMul}, {"__div", vecDiv},
|
||||||
|
{"__unm", vecUnm}, {"__len", vecLen}, {"__tostring", vecString}, {nullptr, nullptr}};
|
||||||
|
luaL_setfuncs(L, vecMeta, 0);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
luaL_newmetatable(L, kObject);
|
||||||
|
const luaL_Reg objMeta[] = {{"__index", objIndex}, {"__newindex", objNewIndex}, {"__tostring", objString}, {"__eq", objEq}, {nullptr, nullptr}};
|
||||||
|
luaL_setfuncs(L, objMeta, 0);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
luaL_newlib(L, kFry);
|
||||||
|
lua_setglobal(L, "fry");
|
||||||
|
lua_sethook(L, watchdog, LUA_MASKCOUNT, 100000);
|
||||||
|
|
||||||
|
ScriptResult result;
|
||||||
|
lua_pushcfunction(L, traceback);
|
||||||
|
try {
|
||||||
|
if (luaL_loadbuffer(L, code.data(), code.size(), ("=" + name).c_str()) != LUA_OK || lua_pcall(L, 0, 0, 1) != LUA_OK) {
|
||||||
|
result.ok = false;
|
||||||
|
result.error = lua_tostring(L, -1) ? lua_tostring(L, -1) : "an error";
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) { // (out of memory inside the modelling code)
|
||||||
|
result.ok = false;
|
||||||
|
result.error = e.what();
|
||||||
|
}
|
||||||
|
result.objectsMade = ctx.made;
|
||||||
|
lua_close(L);
|
||||||
|
// Nothing selected that's gone.
|
||||||
|
if (host.selected)
|
||||||
|
std::erase_if(*host.selected, [&](uint32_t id) { return !findObject(*host.scene, id); });
|
||||||
|
if (host.active && !findObject(*host.scene, *host.active)) *host.active = host.selected && !host.selected->empty() ? *host.selected->rbegin() : 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* scriptHelp() {
|
||||||
|
return R"(Scripts are Lua (lua.org/manual/5.4). Sizes are metres, turns degrees, Z is up.
|
||||||
|
|
||||||
|
fry.add(shape, {setting = value, property = value, ...}) a library shape: fry.add("cube", {width = 2, color = "#ff8000"})
|
||||||
|
fry.shapes() every shape: {id, name, settings}
|
||||||
|
fry.mesh(points, faces, name) your own: points {{x,y,z}, ...}, faces {{1,2,3,4}, ...} (counter-clockwise from outside)
|
||||||
|
fry.lathe(profile, sides, name) a profile of {out, up} points turned round the Z axis (bottles, vases, bowls)
|
||||||
|
fry.extrude(outline, height, name) an outline of {x, y} points on the ground, made that tall
|
||||||
|
fry.objects() fry.find(name) fry.selected() fry.select(objects) fry.clear()
|
||||||
|
fry.group(name, objects) puts them in a folder
|
||||||
|
fry.look{sun_turn, sun_height, sun_strength, sun_color, sky, ground, background, ground_color, exposure, bloom, saturation, vignette, shadow_soft}
|
||||||
|
fry.vec(x, y, z) a vector (+ - * / work on them; #v is its length)
|
||||||
|
fry.frame() fry.say(text) print(...) fry.help()
|
||||||
|
|
||||||
|
An object (o):
|
||||||
|
o.name o.position o.turn o.size o.color o.metal o.rough o.smooth o.visible o.locked
|
||||||
|
o.mirror ("x", "xy"...) o.round o.round_steps o.smooth_levels (finishing touches, not baked)
|
||||||
|
o.width, o.sides ... a library shape's own settings, changeable until its shape is changed by hand
|
||||||
|
o.id o.shape o.point_count o.face_count (to read)
|
||||||
|
o:move(x, y, z) o:rotate(x, y, z) o:scale(k or x, y, z) o:drop() (stand it on the ground)
|
||||||
|
o:copy() o:delete() o:select() o:bounds() -> lowest, highest corner
|
||||||
|
o:add(others...) o:subtract(others...) o:overlap(others...) o:join(others...) (the others go)
|
||||||
|
o:thicken(t) o:subdivide(smooth, times) o:bevel(width, steps, angle) o:bake()
|
||||||
|
o:points() o:faces() o:setpoints(points) o:setshape(points, faces) o:set(name, value) o:settings()
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fry
|
||||||
37
src/script.h
Normal file
37
src/script.h
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#pragma once
|
||||||
|
// Scripts: Lua that builds and changes the scene (the Script panel, and scripts/*.lua). They get
|
||||||
|
// the `fry` library (shapes, objects, combining, the look) and Lua's own string, table, math
|
||||||
|
// and utf8; no io, os or loading modules: a script changes the scene, not the computer.
|
||||||
|
// The whole API is listed in scriptHelp().
|
||||||
|
#include "scene.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fry {
|
||||||
|
|
||||||
|
// What a script works on: the app's scene and its selection (the last picked: `active`).
|
||||||
|
struct ScriptHost {
|
||||||
|
Scene* scene = nullptr;
|
||||||
|
std::set<uint32_t>* selected = nullptr;
|
||||||
|
uint32_t* active = nullptr;
|
||||||
|
std::function<void(const std::string&)> print; // print(), fry.say
|
||||||
|
std::function<void()> frame; // fry.frame(): show everything (may be empty)
|
||||||
|
double timeLimit = 20.0; // seconds before a script's stopped
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScriptResult {
|
||||||
|
bool ok = true;
|
||||||
|
std::string error; // what went wrong, with the line
|
||||||
|
int objectsMade = 0; // (counted for the message afterwards)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs `code` once (a fresh Lua each time); `name` shows in errors ("hello.lua").
|
||||||
|
ScriptResult runScript(ScriptHost& host, const std::string& code, const std::string& name = "script");
|
||||||
|
|
||||||
|
// Every fry.* function and object property, one per line, for the Script panel's help.
|
||||||
|
const char* scriptHelp();
|
||||||
|
|
||||||
|
} // namespace fry
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
add_executable(mesh_test mesh_test.cpp)
|
add_executable(mesh_test mesh_test.cpp)
|
||||||
target_link_libraries(mesh_test PRIVATE fry_core)
|
target_link_libraries(mesh_test PRIVATE fry_core)
|
||||||
add_test(NAME mesh_test COMMAND mesh_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
add_test(NAME mesh_test COMMAND mesh_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
add_executable(script_test script_test.cpp)
|
||||||
|
target_link_libraries(script_test PRIVATE fry_script)
|
||||||
|
target_compile_definitions(script_test PRIVATE FRY_SCRIPTS_DIR="${PROJECT_SOURCE_DIR}/content/Fry3DX/scripts")
|
||||||
|
add_test(NAME script_test COMMAND script_test)
|
||||||
|
|
|
||||||
77
tests/script_test.cpp
Normal file
77
tests/script_test.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Scripts without a window: they make, change, combine and delete objects; mistakes come back as
|
||||||
|
// errors with their line, and an endless loop is stopped.
|
||||||
|
#include "script.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace fry;
|
||||||
|
|
||||||
|
static int g_failures = 0;
|
||||||
|
#define CHECK(cond, ...) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) ++g_failures, std::printf("FAIL: " __VA_ARGS__); \
|
||||||
|
else std::printf("ok: " __VA_ARGS__); \
|
||||||
|
std::printf("\n"); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Scene scene;
|
||||||
|
std::set<uint32_t> selected;
|
||||||
|
uint32_t active = 0;
|
||||||
|
std::string out;
|
||||||
|
ScriptHost host{&scene, &selected, &active, [&](const std::string& s) { out += s + "\n"; }, {}, 2.0};
|
||||||
|
|
||||||
|
ScriptResult r = runScript(host, R"(
|
||||||
|
local box = fry.add("cube", {width = 2, color = "#ff8000", name = "Box"})
|
||||||
|
box.position = {0, 0, 1}
|
||||||
|
box:move(1, 0, 0)
|
||||||
|
print(box.name, box.width, box.position.x)
|
||||||
|
local c = fry.add("cylinder", {radius = 0.3, height = 4, position = {1, 0.1, 0}})
|
||||||
|
box:subtract(c)
|
||||||
|
fry.add("sphere"):delete()
|
||||||
|
local t = fry.lathe({{0, 0}, {0.3, 0}, {0.35, 0.5}, {0.1, 0.8}, {0, 0.8}}, 24, "Vase")
|
||||||
|
fry.extrude({{0, 0}, {1, 0}, {0, 1}}, 0.2, "Wedge")
|
||||||
|
fry.group("Things", {t})
|
||||||
|
fry.select(box)
|
||||||
|
)");
|
||||||
|
CHECK(r.ok, "a script runs (%s)", r.error.c_str());
|
||||||
|
CHECK(scene.objects.size() == 3, "box, vase and wedge are left (%zu)", scene.objects.size());
|
||||||
|
CHECK(out.find("Box\t2.0\t1.0") != std::string::npos, "print and properties: %s", out.c_str());
|
||||||
|
const Object& box = scene.objects[0];
|
||||||
|
CHECK(box.name == "Box" && box.recipe.empty() && box.mesh.faces.size() > 6, "the box had the cylinder taken out (%zu faces)", box.mesh.faces.size());
|
||||||
|
CHECK(box.color.r > 0.99f && box.color.g > 0.49f && box.color.g < 0.51f, "colour from #ff8000");
|
||||||
|
CHECK(scene.folders.size() == 1 && scene.objects[1].folder == scene.folders[0].id, "grouped into a folder");
|
||||||
|
CHECK(active == box.id && selected.count(box.id), "selection from the script");
|
||||||
|
|
||||||
|
r = runScript(host, "local o = fry.add('cube')\no.wobble = 3", "bad.lua");
|
||||||
|
CHECK(!r.ok && r.error.find("bad.lua:2") != std::string::npos, "an error says where: %s", r.error.substr(0, 80).c_str());
|
||||||
|
r = runScript(host, "fry.add('teapot')");
|
||||||
|
CHECK(!r.ok && r.error.find("no shape called teapot") != std::string::npos, "an unknown shape is an error");
|
||||||
|
r = runScript(host, "while true do end");
|
||||||
|
CHECK(!r.ok && r.error.find("stopped") != std::string::npos, "an endless loop is stopped");
|
||||||
|
r = runScript(host, "io.open('x')");
|
||||||
|
CHECK(!r.ok, "no io");
|
||||||
|
r = runScript(host, "local o = fry.add('cube')\nlocal n = #o:points()\nlocal p = o:points()\nfor i = 1, n do p[i].z = p[i].z * 2 end\no:setpoints(p)\nlocal lo, hi = o:bounds()\nprint(hi.z - lo.z)");
|
||||||
|
CHECK(r.ok && out.find("\n2.0\n") != std::string::npos, "points round-trip (%s)", r.error.c_str());
|
||||||
|
r = runScript(host, "fry.clear()");
|
||||||
|
CHECK(r.ok && scene.objects.empty() && active == 0 && selected.empty(), "clear");
|
||||||
|
|
||||||
|
// The examples that come with it all run.
|
||||||
|
for (const auto& e : std::filesystem::directory_iterator(FRY_SCRIPTS_DIR)) {
|
||||||
|
std::ifstream in(e.path());
|
||||||
|
const std::string code((std::istreambuf_iterator<char>(in)), {});
|
||||||
|
Scene fresh;
|
||||||
|
std::set<uint32_t> sel;
|
||||||
|
uint32_t act = 0;
|
||||||
|
ScriptHost h{&fresh, &sel, &act, {}, {}, 20.0};
|
||||||
|
r = runScript(h, code, e.path().filename().string());
|
||||||
|
CHECK(r.ok && !fresh.objects.empty(), "example %s (%d objects) %s", e.path().filename().string().c_str(), r.objectsMade, r.error.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(g_failures ? "%d FAILED\n" : "all passed\n", g_failures);
|
||||||
|
return g_failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue