5

For some reason, a metapost label is appearing correctly for 87 iterations of a \dorecurse{90}, but has disappeared for the final three.

I want to know why this is occuring.

The example is trimmed down significantly, but perhaps it could be made more minimal. When I tried cutting more out, the error did not appear, hence my leaving it in.

% main.tex
\setupbodyfont[modern,24pt]
\ctxloadluafile{parametric.lua}
\processMPfigurefile{register_mp_cmd.mp}
\starttext
    \dorecurse{3*30}{%
        \startMPpage
            jasper_point[% center
                fx = "-15"
                ,fy = "5"
                ,fz = "1"
            ] ;
            jasper_curve[% main circle
                ustart="0",
                ustop="1",
                usamples="200",
                fx="-15 + 5*cos(u*tau)",
                fy="5 + 5*sin(u*tau)"
            ] ;
            jasper_point[% first point on top arc
                fx="-15+5*cos(40*tau/360)"
                ,fy="5+5*sin(40*tau/360)"
                ,fz="2"
            ] ;
            jasper_append_label[
                px="-15+6*cos(40*tau/360)"
                ,py="5+6*sin(40*tau/360)"
                ,text="\im{B}"
            ] ;
            jasper_curve[% top arc
                ustart="40"
                ,ustop="150"
                ,usamples=100
                ,fx="-15+5*cos((u-40)*tau/360*#1/90+40*tau/360)"
                ,fy="5+5*sin((u-40)*tau/360*#1/90+40*tau/360)"
                ,fz ="1"
                ,drawcommands="withpen pencircle scaled .3cm"
            ] ;
            jasper_point[% second point on top arc
                ,fx="-15+5*cos((150-40)*tau/360*#1/90+40*tau/360)"
                ,fy="5+5*sin((150-40)*tau/360*#1/90+40*tau/360)"
                ,fz="2"
            ] ;
            jasper_append_label[
                px="-15+6*cos((150-40)*tau/360*#1/90+40*tau/360)"
                ,py="5+6*sin((150-40)*tau/360*#1/90+40*tau/360)"
                ,text="\im{A}"
            ] ;
            jasper_append_label[
                px="-15+6*cos(((150-40)*tau/360*#1/90+40*tau/360+40*tau/360)/2)"
                ,py="5+6*sin(((150-40)*tau/360*#1/90+40*tau/360+40*tau/360)/2)"
                ,text="\im{m}"
            ] ;
            jasper_point[% first point on bottom arc
                fx="-15+5*cos(-10*tau/360)"
                ,fy="5+5*sin(-10*tau/360)"
                ,fz="2"
            ] ;
            jasper_append_label[
                px="-15+6*cos(-10*tau/360)"
                ,py="5+6*sin(-10*tau/360)"
                ,text="\im{C}"
            ] ;
            jasper_curve[% bottom arc
                ustart="-10"
                ,ustop="-120"
                ,usamples=100
                ,fx="-15+5*cos((u+10)*tau/360*#1/90-10*tau/360)"
                ,fy="5+5*sin((u+10)*tau/360*#1/90-10*tau/360)"
                ,fz ="1"
                ,drawcommands="withpen pencircle scaled .3cm"
            ] ;
            jasper_point[% second point on top arc
                ,fx="-15+5*cos((-120+10)*tau/360*#1/90-10*tau/360)"
                ,fy="5+5*sin((-120+10)*tau/360*#1/90-10*tau/360)"
                ,fz="2"
            ] ;
            jasper_append_label[
                px="-15+6*cos((-120+10)*tau/360*#1/90-10*tau/360)"
                ,py="5+6*sin((-120+10)*tau/360*#1/90-10*tau/360)"
                ,text="\im{D}"
            ] ;
            jasper_append_label[
                px="-15+6*cos(((-120+10)*tau/360*#1/90-10*tau/360-10*tau/360)/2)"
                ,py="5+6*sin(((-120+10)*tau/360*#1/90-10*tau/360-10*tau/360)/2)"
                ,text="\im{n}"
            ] ;
            jasper_render_segments ;
        \stopMPpage%
    }
\stoptext
-- parametric.lua
local mm = require "linalg"
local ENV_ = {}
for k, v in pairs(_G) do ENV_[k] = v end
for k, v in pairs(mm) do ENV_[k] = v end
for k, v in pairs(math) do ENV_[k] = v end

local segments = {}
local observer_dir = { { 0, 0, -1, 1} }
local observer_pos = { { 0, 0, 0, 1} }

local function single_string_function(expr)
    local func = load(
        ("return function(u) return (%s) end"):format(expr),
        "example",
        "t",
        ENV_
    )
    if not func then
        error("Failed to parse expression: " .. expr)
        return
    end
    local ok, result = pcall(func)
    if not ok then
        error("Error evaluating expression: " .. result)
        return
    end
    if type(result) ~= "function" then
        error("Expected a function, got: " .. type(result))
        return
    end

    return result
end

local function single_string_expression(str)
    if type(str) ~= "string" then
        error("single_string_expression: expected a string, got " .. type(str) .. " (" .. tostring(str) .. ")")
    end
    local chunk, err = load(("return %s"):format(str), "expression", "t", ENV_)
    if not chunk then
        error("Failed to parse expression: " .. tostring(str) .. "\nError: " .. tostring(err))
    end
    local ok, result = pcall(chunk)
    if not ok then
        error("Error evaluating expression: " .. tostring(result))
    end
    return result
end

function mp.jasper_label_generate()
    local x = single_string_expression(tostring(metapost.getparameterset("px")))
    local y = single_string_expression(tostring(metapost.getparameterset("py")))
    local z = single_string_expression(tostring(metapost.getparameterset("pz")))
    local text_transformation = single_string_expression(metapost.getparameterset("texttransformation"))
    local transformation = single_string_expression(metapost.getparameterset("transformation"))
    local text = metapost.getparameterset("text")
    local position = mm.matrix_multiply({{x,y,z,1}},transformation)
    table.insert(
        segments
        ,{
            segment = position
            ,text = text 
            ,text_transformation = text_transformation
            ,transformation = transformation
        }

    )
end



function mp.jasper_point_generate()
    local x              = single_string_expression(metapost.getparameterset("fx"))
    local y              = single_string_expression(metapost.getparameterset("fy"))
    local z              = single_string_expression(metapost.getparameterset("fz"))
    local transformation = single_string_expression(metapost.getparameterset("transformation"))
    local draw           = metapost.getparameterset("drawcommands")    

    local P = {{x,y,z,1}}

    local A = P
    local the_segment = mm.matrix_multiply(A, transformation)
    table.insert(
        segments, 
        { 
            segment = the_segment, 
            draw_options = draw
        }
    )
end



function mp.jasper_curve_generate()
    local start          = single_string_expression(tostring(metapost.getparameterset("ustart")))
    local stop           = single_string_expression(tostring(metapost.getparameterset("ustop")))
    local samples        = single_string_expression(tostring(metapost.getparameterset("usamples")))
    local x              = single_string_function(metapost.getparameterset("fx"))
    local y              = single_string_function(metapost.getparameterset("fy"))
    local z              = single_string_function(metapost.getparameterset("fz"))
    local transformation = single_string_expression(metapost.getparameterset("transformation"))
    local draw           = metapost.getparameterset("drawcommands")

    local step = (stop - start) / (samples - 1)
    local result = {}
    

    local function parametric_curve(u)
        return { { x(u), y(u), z(u), 1 } }
    end

    for i = 0, samples - 2 do
        local u = start + i * step
        local A = parametric_curve(u)
        local B = parametric_curve(u+step)
        local the_segment = mm.matrix_multiply({ A[1], B[1] }, transformation)
        table.insert(
            segments, 
            { 
                segment = the_segment, 
                draw_options = draw
            }
        )
    end
end





local function compare_segments(triangle_1,triangle_2)
    if #triangle_1.segment == 1 or #triangle_2.segment == 1 then
        local function depth_mid(seg)
            if #seg == 1 then
                -- line/curve segment: midpoint of S, E
                local S = seg[1]
                local mid = {{
                    S[1],S[2],S[3],1
                }}
                return mm.dot_product(mid, observer_dir)
            else
                -- triangle: centroid of P, Q, R
                local P, Q = seg[1], seg[2]
                local cent = {{
                    (P[1] + Q[1]) / 2,
                    (P[2] + Q[2]) / 2,
                    (P[3] + Q[3]) / 2,
                    1
                }}
                return mm.dot_product(cent, observer_dir)
            end
        end
        local a = depth_mid(triangle_1.segment)
        local b = depth_mid(triangle_2.segment)
        return a > b
    end
    -- 1) if *either* is a 2‐point line/curve (#==3), depth‐sort by midpoint along observer
    if #triangle_1.segment == 2 or #triangle_2.segment == 2 then
        local function depth_mid(seg)
            if #seg == 2 then
                -- line/curve segment: midpoint of S, E
                local S, E = seg[1], seg[2]
                local mid = {{
                    (S[1] + E[1]) / 2,
                    (S[2] + E[2]) / 2,
                    (S[3] + E[3]) / 2,
                    1
                }}
                return mm.dot_product(mid, observer_dir)
            else
                -- triangle: centroid of P, Q, R
                local P, Q, R = seg[1], seg[2], seg[3]
                local cent = {{
                    (P[1] + Q[1] + R[1]) / 3,
                    (P[2] + Q[2] + R[2]) / 3,
                    (P[3] + Q[3] + R[3]) / 3,
                    1
                }}
                return mm.dot_product(cent, observer_dir)
            end
        end
        local a = depth_mid(triangle_1.segment)
        local b = depth_mid(triangle_2.segment)
        return a > b
    end
end

function mp.jasper_render_segments_generate()
    table.sort(segments, compare_segments)

    for _, segment in ipairs(segments) do
        local pts = {}
        local skip = false

        -- Apply reciprocal transform
        if #segment.segment ~= 0 then
            for _, P in ipairs(segment.segment) do
                local R = mm.reciprocate_by_homogenous({P})[1]
                if math.abs(R[1]) > 20 or math.abs(R[2]) > 20 then
                    skip = true
                    break
                end
                table.insert(pts, R)
            end
        end

        if not skip then
            if #pts == 2 then
                -- Line segment
                local A, B = pts[1], pts[2]
                mp.print(string.format(
                    "path jasper_tmp ; jasper_tmp := (%f,%f) -- (%f,%f) ; draw jasper_tmp scaled 1cm %s ;",
                    A[1], A[2], B[1], B[2],
                    segment.draw_options or ""
                ))

            elseif #pts == 3 then
                -- Filled triangle
                local A, B, C = pts[1], pts[2], pts[3]
                mp.print(string.format(
                    "path jasper_tmp ; jasper_tmp := (%f,%f) -- (%f,%f) -- (%f,%f) -- cycle ; fill jasper_tmp scaled 1cm %s ; draw jasper_tmp scaled 1cm %s ;",
                    A[1], A[2], B[1], B[2], C[1], C[2],
                    segment.fill_options or "", segment.draw_options or ""
                ))

            elseif #pts == 1 and segment.text then
                -- Label / node
                local P = pts[1]
                mp.print(string.format(
                    [[
                        picture tmp ;
                        tmp := textext(\"%s\") ;
                        label(tmp, (%f,%f) scaled 1cm) ;
                    ]],
                    segment.text
                    ,P[1]
                    ,P[2]
                ))

            elseif #pts == 1 and not segment.text then
                -- Label / node
                local P = pts[1]
                mp.print(string.format(
                    [[
                        fill fullcircle scaled 0.02cm shifted (%f,%f) scaled 1cm %s;
                    ]]
                    ,P[1]
                    ,P[2]
                    ,segment.draw_options
                ))
            end
        end
    end

    segments = {}
end
-- linalg.lua
local mm = {}
mm.tau = 2*math.pi 
local cos, sin = math.cos, math.sin
--- matrix multiplication
---
--- @param A table<table<number>> left matrix
--- @param B table<table<number>> right matrix
--- @return table<table<number>> the product
function mm.matrix_multiply(A, B)
    local rows_A = #A
    local columns_A = #A[1]
    local rows_B = #B
    local columns_B = #B[1]
    assert(
        columns_A == rows_B,
        string.format(
            [[
                Wrong size matrices for multiplication.
                Size A: %d,%d Size B: %d,%d
            ]],
            rows_A, columns_A,
            rows_B, columns_B
        )
    )
    local product = {}
    for row = 1, rows_A do
        product[row] = {}
        for column = 1, columns_B do
            product[row][column] = 0
            for dot_product_step = 1, columns_A do
                local a = A[row][dot_product_step]
                local b = B[dot_product_step][column]
                assert(type(a) == "number", 
                    string.format("Expected number but got %s in A[%d][%d]", type(a), row, dot_product_step))
                assert(type(b) == "number", 
                    string.format("Expected number but got %s in B[%d][%d]", type(b), dot_product_step, column))
                product[row][column] = product[row][column] + a * b
            end
        end
    end
    return product
end
function mm.reciprocate_by_homogenous(vector)
    local result = {}
    for i = 1, #vector do
        local row = vector[i]
        local w = row[4]
        if w == 0 then
            error("Cannot reciprocate row " .. i .. ": homogeneous coordinate w = 0")
        end
        --if w<0 then w=-w end
        result[i] = {
            row[1]/w,
            row[2]/w,
            row[3]/w,
            1
        }
    end
    return result
end
function mm.dot_product(u,v)
    local result = u[1][1]*v[1][1] + u[1][2]*v[1][2] + u[1][3]*v[1][3]
    return result
end
function mm.identity_matrix()
    local I = {}
    for i = 1, 4 do
        I[i] = {}
        for j = 1, 4 do
            I[i][j] = (i == j) and 1 or 0
        end
    end
    return I
end
function mm.midpoint(triangle)
    local P,Q,R = table.unpack(triangle)
    local x = (P[1]+Q[1]+R[1])/3
    local y = (P[2]+Q[2]+R[2])/3
    local z = (P[3]+Q[3]+R[3])/3
    return {{x,y,z,1}}
end
return mm
% register_mp_cmd.mp
tau := 2 * pi ;

presetparameters "label" [
    px               = "0",
    py               = "0",
    pz               = "0",
    text            = "Label",
    transformation  = "identity_matrix()",
    texttransformation = "identity_matrix()"
] ;
def jasper_append_label = applyparameters "label" "jasper_do_label" enddef ;
vardef jasper_do_label =
    pushparameters "label" ;
    lua.mp.jasper_label_generate() ;
    popparameters ;
enddef ;

presetparameters "point" [
    fx              = "0",
    fy              = "0",
    fz              = "0",
    drawcommands    = "withcolor black",
    transformation  = "identity_matrix()"
] ;
def jasper_point = applyparameters "point" "jasper_do_point" enddef ;
vardef jasper_do_point =
    pushparameters "point" ;
    lua.mp.jasper_point_generate() ;
    popparameters ;
enddef ;

presetparameters "curve" [
    ustart          = 0,
    ustop           = tau,
    usamples        = 36,
    fx              = "cos(u)",
    fy              = "sin(u)",
    fz              = "u",
    drawcommands    = "withcolor black",
    transformation  = "identity_matrix()"
] ;
def jasper_curve = applyparameters "curve" "jasper_do_curve" enddef ;
vardef jasper_do_curve =
    pushparameters "curve" ;
    lua.mp.jasper_curve_generate() ;
    popparameters ;
enddef ;

def jasper_render_segments = jasper_do_render_segments enddef ;
vardef jasper_do_render_segments =
    lua.mp.jasper_render_segments_generate() ;
enddef ;

output

1 Answer 1

3

Line 200 of parametric.lua was clipping the segments to a small square (20cm X 20cm), and the label exceeded that.

if math.abs(R[1]) > 20 or math.abs(R[2]) > 20 then should be if math.abs(R[1]) > 50 or math.abs(R[2]) > 50 then.

This is a short answer, but I think that it is still meaningful because the original question shows good interfacing, which there isn't a lot to read about in the first place.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.