--- lua-tikz3dtools-scene.lua
--- Scene management, TeX command registration, and rendering for lua-tikz3dtools.

local Vector
local Matrix
local Geometry

local Scene = {}

--- Set class references (called after all modules are loaded).
function Scene._set_classes(V, M, G)
    Vector = V
    Matrix = M
    Geometry = G
end

-- ================================================================
-- Sandboxed evaluation environment
-- ================================================================

--- Build a safe environment table for load().
--- Whitelists only math functions and user-defined objects.
--- os, io, debug, dofile, loadfile, require are NOT reachable.
local function make_safe_env(extra)
    local env = {}
    -- Whitelist standard math
    local safe_math = {
        "abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "cosh",
        "deg", "exp", "floor", "fmod", "frexp", "huge", "ldexp", "log",
        "log10", "max", "min", "modf", "pi", "pow", "rad", "random",
        "randomseed", "sin", "sinh", "sqrt", "tan", "tanh", "maxinteger",
        "mininteger", "tointeger", "type",
    }
    for _, k in ipairs(safe_math) do
        if math[k] ~= nil then
            env[k] = math[k]
        end
    end
    -- Whitelist table utilities needed by expressions
    env.table = { unpack = table.unpack, insert = table.insert, remove = table.remove, sort = table.sort }
    env.unpack = table.unpack
    env.type = type
    env.tostring = tostring
    env.tonumber = tonumber
    env.pairs = pairs
    env.ipairs = ipairs
    env.select = select
    env.pcall = pcall
    env.error = error
    env.setmetatable = setmetatable
    env.getmetatable = getmetatable
    env.rawget = rawget
    env.rawset = rawset
    -- Copy extra entries (user objects, Vector, Matrix, tau, etc.)
    if extra then
        for k, v in pairs(extra) do
            env[k] = v
        end
    end
    return env
end

-- ================================================================
-- TeX command registration helper
-- https://tex.stackexchange.com/a/747040
-- ================================================================

local function register_tex_cmd(name, func, args, protected)
    name = "__lua_tikztdtools_" .. name .. ":" .. ("n"):rep(#args)
    local scanners = {}
    for _, arg in ipairs(args) do
        scanners[#scanners+1] = token['scan_' .. arg]
    end
    local scanning_func = function()
        local values = {}
        for _, scanner in ipairs(scanners) do
            values[#values+1] = scanner()
        end
        func(table.unpack(values))
    end
    local index = luatexbase.new_luafunction(name)
    lua.get_functions_table()[index] = scanning_func
    if protected then
        token.set_lua(name, index, "protected")
    else
        token.set_lua(name, index)
    end
end

-- ================================================================
-- Global scene state
-- ================================================================

local lua_tikz3dtools = {}
lua_tikz3dtools.simplices = {}
lua_tikz3dtools.lights = {}

-- The safe math environment exposed to user expressions
lua_tikz3dtools.math = make_safe_env()
lua_tikz3dtools.math.Vector = Vector
lua_tikz3dtools.math.Matrix = Matrix
lua_tikz3dtools.math.tau = 2 * math.pi

--- Late-init: called after Vector/Matrix are set to update the env table
function Scene._init_math_env()
    lua_tikz3dtools.math.Vector = Vector
    lua_tikz3dtools.math.Matrix = Matrix
    lua_tikz3dtools.math.tau = 2 * math.pi
end

-- ================================================================
-- Expression evaluators (sandboxed)
-- ================================================================

local function single_string_expression(str)
    return load(
        ("return %s"):format(str),
        "expression",
        "t",
        lua_tikz3dtools.math
    )()
end

local function body_expression(str)
    return load(str, "expression", "t", lua_tikz3dtools.math)()
end

local function single_string_function(str)
    return load(("return function(u) %s end"):format(str), "expression", "t", lua_tikz3dtools.math)()
end

local function double_string_function(str)
    return load(("return function(u,v) %s end"):format(str), "expression", "t", lua_tikz3dtools.math)()
end

local function triple_string_function(str)
    return load(("return function(u,v,w) %s end"):format(str), "expression", "t", lua_tikz3dtools.math)()
end

-- ================================================================
-- Append functions
-- ================================================================

local function append_point(hash)
    local v              = body_expression(hash.v)
    local transformation = single_string_expression(hash.transformation)
    local filloptions    = hash.filloptions
    local filter         = hash.filter
    if v then
        local the_simplex = v:multiply(transformation)
        table.insert(
            lua_tikz3dtools.simplices,
            {
                simplex     = the_simplex,
                filloptions = filloptions,
                type        = "point",
                filter      = filter
            }
        )
    end
end

local function append_surface(hash)
    local ustart         = single_string_expression(hash.ustart)
    local ustop          = single_string_expression(hash.ustop)
    local usamples       = single_string_expression(hash.usamples)
    local vstart         = single_string_expression(hash.vstart)
    local vstop          = single_string_expression(hash.vstop)
    local vsamples       = single_string_expression(hash.vsamples)
    local transformation = single_string_expression(hash.transformation)
    local f              = double_string_function(hash.v)
    local filloptions    = hash.filloptions
    local filter         = hash.filter

    assert(usamples and usamples >= 2, "usamples must be >= 2, got: " .. tostring(usamples))
    assert(vsamples and vsamples >= 2, "vsamples must be >= 2, got: " .. tostring(vsamples))

    local ustep = (ustop - ustart) / (usamples - 1)
    local vstep = (vstop - vstart) / (vsamples - 1)

    local function parametric_surface(u, v)
        return f(u, v)
    end

    for i = 0, usamples - 2 do
        local u = ustart + i * ustep
        for j = 0, vsamples - 2 do
            local v = vstart + j * vstep
            local A = parametric_surface(u, v)
            local B = parametric_surface(u + ustep, v)
            local C = parametric_surface(u + ustep, v + vstep)
            local D = parametric_surface(u, v + vstep)
            if A and B and C and D then
                local simplex1 = Matrix:_new{A:to_table(), B:to_table(), C:to_table()}:multiply(transformation)
                local simplex2 = Matrix:_new{A:to_table(), D:to_table(), C:to_table()}:multiply(transformation)
                if not (
                    Geometry.hpoint_point_intersecting(A, B)
                    or Geometry.hpoint_point_intersecting(B, C)
                    or Geometry.hpoint_point_intersecting(A, C)
                ) then
                    table.insert(lua_tikz3dtools.simplices, {
                        simplex     = simplex1,
                        filloptions = filloptions,
                        type        = "triangle",
                        filter      = filter
                    })
                end
                if not (
                    Geometry.hpoint_point_intersecting(A, D)
                    or Geometry.hpoint_point_intersecting(D, C)
                    or Geometry.hpoint_point_intersecting(A, C)
                ) then
                    table.insert(lua_tikz3dtools.simplices, {
                        simplex     = simplex2,
                        filloptions = filloptions,
                        type        = "triangle",
                        filter      = filter
                    })
                end
            end
        end
    end
end

local function append_triangle(hash)
    local A              = single_string_expression(hash.A)
    local B              = single_string_expression(hash.B)
    local C              = single_string_expression(hash.C)
    local transformation = single_string_expression(hash.transformation)
    local filter         = hash.filter
    local filloptions    = hash.filloptions

    if A[1] and B[1] and C[1] then
        if not (
            Geometry.hpoint_point_intersecting(A, B)
            or Geometry.hpoint_point_intersecting(B, C)
            or Geometry.hpoint_point_intersecting(C, A)
        ) then
            local the_simplex = Matrix:_new{A:to_table(), B:to_table(), C:to_table()}:multiply(transformation)
            table.insert(lua_tikz3dtools.simplices, {
                simplex     = the_simplex,
                filloptions = filloptions,
                type        = "triangle",
                filter      = filter
            })
        end
    end
end

local function append_label(hash)
    local v              = body_expression(hash.v)
    local filter         = hash.filter
    local text           = hash.text
    local transformation = single_string_expression(hash.transformation)
    if v then
        local the_simplex = v:multiply(transformation)
        table.insert(lua_tikz3dtools.simplices, {
            simplex     = the_simplex,
            text        = text,
            type        = "label",
            filter      = filter
        })
    end
end

local function append_light(hash)
    local v = body_expression(hash.v)
    if v and getmetatable(v) == Vector then
        table.insert(lua_tikz3dtools.lights, v)
    else
        error("Invalid light vector: " .. tostring(v))
    end
end

local function append_curve(hash)
    local ustart         = single_string_expression(hash.ustart)
    local ustop          = single_string_expression(hash.ustop)
    local usamples       = single_string_expression(hash.usamples)
    local transformation = single_string_expression(hash.transformation)
    local f              = single_string_function(hash.v)
    local filter         = hash.filter
    local drawoptions    = hash.drawoptions
    local arrowoptions   = hash.arrowtip
    local tailoptions    = hash.arrowtail

    assert(usamples and usamples >= 2, "usamples must be >= 2, got: " .. tostring(usamples))

    local ustep = (ustop - ustart) / (usamples - 1)

    local function parametric_curve(u)
        return f(u)
    end

    for i = 0, usamples - 2 do
        local u = ustart + i * ustep
        local A = parametric_curve(u)
        local B = parametric_curve(u + ustep)
        if A and B then
            local simplex = Matrix:_new{A:to_table(), B:to_table()}:multiply(transformation)
            table.insert(lua_tikz3dtools.simplices, {
                simplex      = simplex,
                drawoptions  = drawoptions,
                type         = "line segment",
                filter       = filter
            })
            if i == 0 and tailoptions then
                local P = parametric_curve(ustart):multiply(transformation)
                local U = P:hsub(parametric_curve(ustart+ustep):multiply(transformation)):hnormalize()
                local V = U:orthogonal_vector():hnormalize()
                local W = U:hhypercross(V):hnormalize()
                append_surface{
                    ustart = "0", ustop = "tau", usamples = "6",
                    vstart = "0+pi/2", vstop = "pi", vsamples = "2",
                    v = "return Vector:_new{0.1*sin(v)*cos(u), 0.1*sin(v)*sin(u), 0*0.1*cos(v), 1}",
                    filloptions = tailoptions,
                    transformation = ([[
                        Matrix:new{
                            {%f,%f,%f,0},{%f,%f,%f,0},{%f,%f,%f,0},{%f,%f,%f,1}
                        }
                    ]]):format(
                        W[1],W[2],W[3], V[1],V[2],V[3], U[1],U[2],U[3], P[1],P[2],P[3]
                    ),
                    filter = filter
                }
            end
            if i == usamples - 2 and arrowoptions then
                local P = parametric_curve(ustop):multiply(transformation)
                local U = P:hsub(parametric_curve(ustop - ustep):multiply(transformation)):hnormalize()
                local V = U:orthogonal_vector():hnormalize()
                local W = U:hhypercross(V):hnormalize()
                append_surface{
                    ustart = "0", ustop = "0.1", usamples = "2",
                    vstart = "0", vstop = "1", vsamples = "4",
                    v = "return Vector:_new{u*cos(v*tau), u*sin(v*tau), -u, 1}",
                    filloptions = arrowoptions,
                    transformation = ([[
                        Matrix:new{
                            {%f,%f,%f,0},{%f,%f,%f,0},{%f,%f,%f,0},{%f,%f,%f,1}
                        }
                    ]]):format(
                        W[1],W[2],W[3], V[1],V[2],V[3], U[1],U[2],U[3], P[1],P[2],P[3]
                    ),
                    filter = filter
                }
            end
        end
    end
end

local function append_solid(hash)
    local ustart   = single_string_expression(hash.ustart)
    local ustop    = single_string_expression(hash.ustop)
    local usamples = single_string_expression(hash.usamples)
    local vstart   = single_string_expression(hash.vstart)
    local vstop    = single_string_expression(hash.vstop)
    local vsamples = single_string_expression(hash.vsamples)
    local wstart   = single_string_expression(hash.wstart)
    local wstop    = single_string_expression(hash.wstop)
    local wsamples = single_string_expression(hash.wsamples)
    local filloptions    = hash.filloptions
    local transformation = single_string_expression(hash.transformation)
    local filter = hash.filter
    local f = triple_string_function(hash.v)

    assert(usamples and usamples >= 2, "usamples must be >= 2, got: " .. tostring(usamples))
    assert(vsamples and vsamples >= 2, "vsamples must be >= 2, got: " .. tostring(vsamples))
    assert(wsamples and wsamples >= 2, "wsamples must be >= 2, got: " .. tostring(wsamples))

    local function parametric_solid(u, v, w)
        return f(u, v, w)
    end

    local ustep = (ustop - ustart) / (usamples - 1)
    local vstep = (vstop - vstart) / (vsamples - 1)
    local wstep = (wstop - wstart) / (wsamples - 1)

    local function tessellate_face(fixed_var, fixed_val, s1_start, s1_step, s1_count, s2_start, s2_step, s2_count)
        for i = 0, s1_count - 2 do
            local s1 = s1_start + i * s1_step
            for j = 0, s2_count - 2 do
                local s2 = s2_start + j * s2_step
                local A, B, C, D
                if fixed_var == "u" then
                    A = parametric_solid(fixed_val, s1, s2)
                    B = parametric_solid(fixed_val, s1 + s1_step, s2)
                    C = parametric_solid(fixed_val, s1 + s1_step, s2 + s2_step)
                    D = parametric_solid(fixed_val, s1, s2 + s2_step)
                elseif fixed_var == "v" then
                    A = parametric_solid(s1, fixed_val, s2)
                    B = parametric_solid(s1 + s1_step, fixed_val, s2)
                    C = parametric_solid(s1 + s1_step, fixed_val, s2 + s2_step)
                    D = parametric_solid(s1, fixed_val, s2 + s2_step)
                elseif fixed_var == "w" then
                    A = parametric_solid(s1, s2, fixed_val)
                    B = parametric_solid(s1 + s1_step, s2, fixed_val)
                    C = parametric_solid(s1 + s1_step, s2 + s2_step, fixed_val)
                    D = parametric_solid(s1, s2 + s2_step, fixed_val)
                end
                if A and B and D then
                    local simplex = Matrix:_new{A:to_table(), B:to_table(), D:to_table()}:multiply(transformation)
                    table.insert(lua_tikz3dtools.simplices, {
                        simplex     = simplex,
                        filloptions = filloptions,
                        type        = "triangle",
                        filter      = filter
                    })
                end
                if B and C and D then
                    local simplex = Matrix:_new{B:to_table(), C:to_table(), D:to_table()}:multiply(transformation)
                    table.insert(lua_tikz3dtools.simplices, {
                        simplex     = simplex,
                        filloptions = filloptions,
                        type        = "triangle",
                        filter      = filter
                    })
                end
            end
        end
    end

    tessellate_face("u", ustart, vstart, vstep, vsamples, wstart, wstep, wsamples)
    tessellate_face("u", ustop,  vstart, vstep, vsamples, wstart, wstep, wsamples)
    tessellate_face("v", vstart, ustart, ustep, usamples, wstart, wstep, wsamples)
    tessellate_face("v", vstop,  ustart, ustep, usamples, wstart, wstep, wsamples)
    tessellate_face("w", wstart, ustart, ustep, usamples, vstart, vstep, vsamples)
    tessellate_face("w", wstop,  ustart, ustep, usamples, vstart, vstep, vsamples)
end

-- ================================================================
-- Filters (sandboxed)
-- ================================================================

local function apply_filters(simplices)
    local new_simplices = {}

    for _, simplex in ipairs(simplices) do
        local env = make_safe_env(lua_tikz3dtools.math)

        if simplex.type == "point" then
            env.A = Vector:_new(simplex.simplex:to_table())
        elseif simplex.type == "line segment" then
            env.A = Vector:_new(simplex.simplex[1])
            env.B = Vector:_new(simplex.simplex[2])
        elseif simplex.type == "triangle" then
            env.A = Vector:_new(simplex.simplex[1])
            env.B = Vector:_new(simplex.simplex[2])
            env.C = Vector:_new(simplex.simplex[3])
        elseif simplex.type == "label" then
            env.A = Vector:_new(simplex.simplex:to_table())
        end

        local filter_src = ("return function()\n%s\nend")
            :format(simplex.filter or "return true")

        local chunk, _ = load(filter_src, "filter", "t", env)
        if chunk then
            local ok, fn = pcall(chunk)
            if ok and fn then
                local ok2, result = pcall(fn)
                if ok2 and result then
                    table.insert(new_simplices, simplex)
                end
            end
        end
    end

    return new_simplices
end

-- ================================================================
-- Display / render
-- ================================================================

local function display_simplices()
    print("Time:" .. os.date("%X") .. " Displaying " .. #lua_tikz3dtools.simplices .. " simplices.")

    -- Pre-compute bbox2 for all simplices
    for _, s in ipairs(lua_tikz3dtools.simplices) do
        if s.type ~= "point" and s.type ~= "label" then
            s.bbox2 = s.simplex:get_bbox2()
        end
    end

    lua_tikz3dtools.simplices = Geometry.partition_simplices_by_parents(
        lua_tikz3dtools.simplices,
        lua_tikz3dtools.simplices
    )
    print("Time:" .. os.date("%X") .. " After partitioning, " .. #lua_tikz3dtools.simplices .. " simplices remain.")

    lua_tikz3dtools.simplices = apply_filters(lua_tikz3dtools.simplices)
    print("Time:" .. os.date("%X") .. " After filtering, " .. #lua_tikz3dtools.simplices .. " simplices remain.")

    -- Re-compute bbox2 after filtering (some simplices removed)
    for _, s in ipairs(lua_tikz3dtools.simplices) do
        if s.type ~= "point" and s.type ~= "label" and not s.bbox2 then
            s.bbox2 = s.simplex:get_bbox2()
        end
    end

    lua_tikz3dtools.simplices = Geometry.scc(lua_tikz3dtools.simplices)
    print("Time:" .. os.date("%X") .. " After occlusion sorting, " .. #lua_tikz3dtools.simplices .. " simplices remain.")

    local labels = {}
    for _, simplex in ipairs(lua_tikz3dtools.simplices) do
        if simplex.type == "point" then
            tex.sprint(
                ("\\path[%s] (%f,%f) circle[radius = 0.06];")
                :format(simplex.filloptions, simplex.simplex[1], simplex.simplex[2])
            )
        elseif simplex.type == "line segment" then
            tex.sprint(
                ("\\path[%s] (%f,%f) -- (%f,%f);")
                :format(
                    simplex.drawoptions,
                    simplex.simplex[1][1], simplex.simplex[1][2],
                    simplex.simplex[2][1], simplex.simplex[2][2]
                )
            )
        elseif simplex.type == "triangle" then
            local num_lights = #lua_tikz3dtools.lights
            if num_lights > 0 then
                local A = Vector:_new(simplex.simplex[1])
                local B = Vector:_new(simplex.simplex[2])
                local C = Vector:_new(simplex.simplex[3])
                local normal = (B:hsub(A)):hhypercross(C:hsub(A)):hnormalize()

                local total_intensity = 0
                for _, light in ipairs(lua_tikz3dtools.lights) do
                    local light_dir = light:hnormalize()
                    local cos_theta = math.abs(normal:hinner(light_dir))
                    if cos_theta > 1 then cos_theta = 1 end
                    -- Linear falloff: 0° → 1.0, 90° → 0.0
                    local theta = math.deg(math.acos(cos_theta))
                    total_intensity = total_intensity + (1 - theta / 90)
                end

                local avg_intensity = math.floor((total_intensity / num_lights) * 100 + 0.01)
                tex.sprint(("\\colorlet{ltdtbrightness}{white!%f!black}"):format(avg_intensity))
            else
                tex.sprint(("\\colorlet{ltdtbrightness}{white!%f!black}"):format(0))
            end

            tex.sprint(
                ("\\path[%s] (%f,%f) -- (%f,%f) -- (%f,%f) -- cycle;")
                :format(
                    simplex.filloptions,
                    simplex.simplex[1][1], simplex.simplex[1][2],
                    simplex.simplex[2][1], simplex.simplex[2][2],
                    simplex.simplex[3][1], simplex.simplex[3][2]
                )
            )
        elseif simplex.type == "label" then
            table.insert(labels, simplex)
        end
    end

    for _, simplex in ipairs(labels) do
        tex.sprint(
            ("\\node at (%f,%f) {%s};")
            :format(simplex.simplex[1], simplex.simplex[2], simplex.text)
        )
    end

    lua_tikz3dtools.simplices = {}
    lua_tikz3dtools.lights = {}
end

-- ================================================================
-- set_object
-- ================================================================

local function set_object(hash)
    local object = single_string_expression(hash.object)
    local name = hash.name
    lua_tikz3dtools.math[name] = object
end

-- ================================================================
-- Register all TeX commands
-- ================================================================

--- Read a TeX macro, returning a fallback if undefined.
local function get_macro_or(name, fallback)
    local val = token.get_macro(name)
    if val == nil or val == "" then return fallback end
    return val
end

function Scene.register_commands()
    register_tex_cmd("appendpoint", function()
        append_point{
            v              = token.get_macro("luatikztdtools@p@p@v"),
            filloptions    = get_macro_or("luatikztdtools@p@p@filloptions", ""),
            transformation = get_macro_or("luatikztdtools@p@p@transformation", "Matrix:identity3()"),
            filter         = get_macro_or("luatikztdtools@p@p@filter", "return true")
        }
    end, { })

    register_tex_cmd("appendsurface", function()
        append_surface{
            ustart         = token.get_macro("luatikztdtools@p@s@ustart"),
            ustop          = token.get_macro("luatikztdtools@p@s@ustop"),
            usamples       = token.get_macro("luatikztdtools@p@s@usamples"),
            vstart         = token.get_macro("luatikztdtools@p@s@vstart"),
            vstop          = token.get_macro("luatikztdtools@p@s@vstop"),
            vsamples       = token.get_macro("luatikztdtools@p@s@vsamples"),
            v              = token.get_macro("luatikztdtools@p@s@v"),
            transformation = get_macro_or("luatikztdtools@p@s@transformation", "Matrix:identity3()"),
            filloptions    = get_macro_or("luatikztdtools@p@s@filloptions", ""),
            filter         = get_macro_or("luatikztdtools@p@s@filter", "return true"),
        }
    end, { })

    register_tex_cmd("appendtriangle", function()
        append_triangle{
            A              = token.get_macro("luatikztdtools@p@t@A"),
            B              = token.get_macro("luatikztdtools@p@t@B"),
            C              = token.get_macro("luatikztdtools@p@t@C"),
            transformation = get_macro_or("luatikztdtools@p@t@transformation", "Matrix:identity3()"),
            filloptions    = get_macro_or("luatikztdtools@p@t@filloptions", ""),
            filter         = get_macro_or("luatikztdtools@p@t@filter", "return true"),
        }
    end, { })

    register_tex_cmd("appendlabel", function()
        append_label{
            v              = token.get_macro("luatikztdtools@p@l@v"),
            text           = token.get_macro("luatikztdtools@p@l@text"),
            transformation = get_macro_or("luatikztdtools@p@l@transformation", "Matrix:identity3()"),
            filter         = get_macro_or("luatikztdtools@p@l@filter", "return true")
        }
    end, { })

    register_tex_cmd("appendlight", function()
        append_light{
            v = token.get_macro("luatikztdtools@p@la@v")
        }
    end, { })

    register_tex_cmd("appendcurve", function()
        append_curve{
            ustart         = token.get_macro("luatikztdtools@p@c@ustart"),
            ustop          = token.get_macro("luatikztdtools@p@c@ustop"),
            usamples       = token.get_macro("luatikztdtools@p@c@usamples"),
            v              = token.get_macro("luatikztdtools@p@c@v"),
            transformation = get_macro_or("luatikztdtools@p@c@transformation", "Matrix:identity3()"),
            drawoptions    = get_macro_or("luatikztdtools@p@c@drawoptions", ""),
            arrowtip       = token.get_macro("luatikztdtools@p@c@arrowtip"),
            arrowtail      = token.get_macro("luatikztdtools@p@c@arrowtail"),
            filter         = get_macro_or("luatikztdtools@p@c@filter", "return true")
        }
    end, { })

    register_tex_cmd("appendsolid", function()
        append_solid{
            ustart         = token.get_macro("luatikztdtools@p@solid@ustart"),
            ustop          = token.get_macro("luatikztdtools@p@solid@ustop"),
            usamples       = token.get_macro("luatikztdtools@p@solid@usamples"),
            vstart         = token.get_macro("luatikztdtools@p@solid@vstart"),
            vstop          = token.get_macro("luatikztdtools@p@solid@vstop"),
            vsamples       = token.get_macro("luatikztdtools@p@solid@vsamples"),
            wstart         = token.get_macro("luatikztdtools@p@solid@wstart"),
            wstop          = token.get_macro("luatikztdtools@p@solid@wstop"),
            wsamples       = token.get_macro("luatikztdtools@p@solid@wsamples"),
            v              = token.get_macro("luatikztdtools@p@solid@v"),
            transformation = get_macro_or("luatikztdtools@p@solid@transformation", "Matrix:identity3()"),
            filloptions    = get_macro_or("luatikztdtools@p@solid@filloptions", ""),
            filter         = get_macro_or("luatikztdtools@p@solid@filter", "return true")
        }
    end, { })

    register_tex_cmd("displaysimplices", function()
        display_simplices()
    end, { })

    register_tex_cmd("setobject", function()
        set_object{
            name   = token.get_macro("luatikztdtools@p@m@name"),
            object = token.get_macro("luatikztdtools@p@m@object"),
        }
    end, { })
end

return Scene
