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 ;