local U = require("texecole-util") local M = {} local function fmt(v) return (("%.4f"):format(v)):gsub("0+$", ""):gsub("%.$", "") end local function projector(attrs) local alpha = math.rad(tonumber(attrs.angle or "") or 45) local k = tonumber(attrs.ratio or "") or 0.5 local c, s = k * math.cos(alpha), k * math.sin(alpha) return function(p) return p[1] + c * p[2], p[3] + s * p[2] end end local function shoelace(pts) local a = 0 for i = 1, #pts do local p, q = pts[i], pts[i % #pts + 1] a = a + p[1] * q[2] - q[1] * p[2] end return a / 2 end local function poly_render(verts, faces, names, proj, ox) local P = {} for i, v in ipairs(verts) do local x, y = proj(v) P[i] = { x + ox, y } end local vis = {} for fi, f in ipairs(faces) do local pts = {} for _, vi in ipairs(f) do pts[#pts+1] = P[vi] end vis[fi] = shoelace(pts) > 1e-9 end local edges = {} local function ekey(i, j) if i > j then i, j = j, i end return i .. "-" .. j end for fi, f in ipairs(faces) do for k = 1, #f do local i, j = f[k], f[k % #f + 1] local key = ekey(i, j) local e = edges[key] if not e then e = { i = math.min(i,j), j = math.max(i,j), faces = {} }; edges[key] = e end e.faces[#e.faces+1] = fi end end local out, xmin, xmax = {}, math.huge, -math.huge for _, p in ipairs(P) do if p[1] < xmin then xmin = p[1] end if p[1] > xmax then xmax = p[1] end end local solidseg, dashseg = {}, {} for _, e in pairs(edges) do local anyvis = false for _, fi in ipairs(e.faces) do if vis[fi] then anyvis = true end end local seg = ("(%s,%s) -- (%s,%s)"):format( fmt(P[e.i][1]), fmt(P[e.i][2]), fmt(P[e.j][1]), fmt(P[e.j][2])) if anyvis then solidseg[#solidseg+1] = seg else dashseg[#dashseg+1] = seg end end for _, s in ipairs(dashseg) do out[#out+1] = "\\draw[dashed] " .. s .. ";" end for _, s in ipairs(solidseg) do out[#out+1] = "\\draw " .. s .. ";" end if names then local cx, cy = 0, 0 for _, p in ipairs(P) do cx = cx + p[1]; cy = cy + p[2] end cx, cy = cx / #P, cy / #P local inc = {} for _, e in pairs(edges) do local a = math.atan(P[e.j][2]-P[e.i][2], P[e.j][1]-P[e.i][1]) inc[e.i] = inc[e.i] or {}; inc[e.i][#inc[e.i]+1] = a inc[e.j] = inc[e.j] or {}; inc[e.j][#inc[e.j]+1] = a + math.pi end for i, nm in ipairs(names) do if nm and nm ~= "" then local rx, ry = P[i][1] - cx, P[i][2] - cy local rn = math.sqrt(rx*rx + ry*ry) local radial = (rn > 1e-9) and math.atan(ry, rx) or (math.pi/2) local ang = radial local dirs = inc[i] if dirs and #dirs > 0 then table.sort(dirs, function(a, b) return a % (2*math.pi) < b % (2*math.pi) end) local bestgap, bestang, bestrad = -1, radial, math.huge for k = 1, #dirs do local a1 = dirs[k] % (2*math.pi) local a2 = (k < #dirs) and dirs[k+1] % (2*math.pi) or (dirs[1] % (2*math.pi) + 2*math.pi) local gap = a2 - a1 local mid = (a1 + a2) / 2 local dr = math.abs(((mid - radial) + math.pi) % (2*math.pi) - math.pi) if gap > bestgap + 1e-6 or (gap > bestgap - 1e-6 and dr < bestrad) then bestgap, bestang, bestrad = gap, mid, dr end end ang = bestang end out[#out+1] = ("\\node[font=\\footnotesize] at (%s,%s) {$%s$};") :format(fmt(P[i][1] + 0.3 * math.cos(ang)), fmt(P[i][2] + 0.3 * math.sin(ang)), nm) end end end return table.concat(out, "\n"), xmin, xmax end local function box_geometry(a, b, c) local verts = { {0,0,0}, {a,0,0}, {a,b,0}, {0,b,0}, {0,0,c}, {a,0,c}, {a,b,c}, {0,b,c}, } local faces = { {1,4,3,2}, {5,6,7,8}, {1,2,6,5}, {3,4,8,7}, {1,5,8,4}, {2,3,7,6}, } return verts, faces end local function pyramid_geometry(b, h) local verts = { {b/2, b/2, h}, {0,0,0}, {b,0,0}, {b,b,0}, {0,b,0}, } local faces = { {2,5,4,3}, {2,3,1}, {3,4,1}, {4,5,1}, {5,2,1}, } return verts, faces end local N = 36 local function ellipse_path(r, z, proj, ox, t0, t1) local pts = {} for i = 0, N do local t = t0 + (t1 - t0) * i / N local x, y = proj({ r * math.cos(t), r * math.sin(t), z }) pts[#pts+1] = ("(%s,%s)"):format(fmt(x + ox), fmt(y)) end return table.concat(pts, " -- ") end local function cylinder_render(r, h, proj, ox) local out = {} out[#out+1] = "\\draw[dashed] " .. ellipse_path(r, 0, proj, ox, 0, math.pi) .. ";" out[#out+1] = "\\draw " .. ellipse_path(r, 0, proj, ox, math.pi, 2*math.pi) .. ";" out[#out+1] = "\\draw " .. ellipse_path(r, h, proj, ox, 0, 2*math.pi) .. ";" local x2 = (proj({0,1,0})) local c = x2 local ts = { math.atan(c), math.atan(c) + math.pi } for _, t in ipairs(ts) do local bx, by = proj({ r * math.cos(t), r * math.sin(t), 0 }) local tx, ty = proj({ r * math.cos(t), r * math.sin(t), h }) out[#out+1] = ("\\draw (%s,%s) -- (%s,%s);") :format(fmt(bx + ox), fmt(by), fmt(tx + ox), fmt(ty)) end return table.concat(out, "\n") end local function cone_render(r, h, proj, ox) local out = {} out[#out+1] = "\\draw[dashed] " .. ellipse_path(r, 0, proj, ox, 0, math.pi) .. ";" out[#out+1] = "\\draw " .. ellipse_path(r, 0, proj, ox, math.pi, 2*math.pi) .. ";" local ax, ay = proj({0, 0, h}) ax = ax + ox local function g(t) local px, py = proj({ r * math.cos(t), r * math.sin(t), 0 }) px = px + ox local dx, dy = proj({ -r * math.sin(t), r * math.cos(t), 0 }) return (px - ax) * dy - (py - ay) * dx end local roots = {} local steps = 720 local prev = g(0) for i = 1, steps do local t = 2 * math.pi * i / steps local cur = g(t) if prev * cur <= 0 and #roots < 2 then local lo, hi = 2 * math.pi * (i - 1) / steps, t for _ = 1, 50 do local mid = (lo + hi) / 2 if g(lo) * g(mid) <= 0 then hi = mid else lo = mid end end roots[#roots+1] = (lo + hi) / 2 end prev = cur end for _, t in ipairs(roots) do local px, py = proj({ r * math.cos(t), r * math.sin(t), 0 }) out[#out+1] = ("\\draw (%s,%s) -- (%s,%s);") :format(fmt(ax), fmt(ay), fmt(px + ox), fmt(py)) end return table.concat(out, "\n") end local function sphere_render(r, proj, ox) local out = {} out[#out+1] = ("\\draw (%s,0) circle [radius=%s];"):format(fmt(ox), fmt(r)) out[#out+1] = "\\draw[dashed] " .. ellipse_path(r, 0, proj, ox, 0, math.pi) .. ";" out[#out+1] = "\\draw " .. ellipse_path(r, 0, proj, ox, math.pi, 2*math.pi) .. ";" return table.concat(out, "\n") end local KINDS = { cube=true, cuboid=true, pyramid=true, cylinder=true, cone=true, sphere=true } local function num_attr(attrs, key, what) local v = tonumber(attrs[key] or "") if not v or v <= 0 then error("texecole: solid " .. what .. " needs " .. key .. ":{a positive number}") end return v end function M.render(rest, state) local kind, tail = rest:match("^%s*(%S+)%s*(.*)$") if not KINDS[kind] then error("texecole: solid takes cube, cuboid, pyramid, cylinder, " .. "cone or sphere (got '" .. tostring(kind) .. "')") end local names, optstr = nil, {} for w in tail:gmatch("%S+") do if not names and w:match("^%a+$") and not w:find(":") then names = {} for ch in w:gmatch("%a") do names[#names+1] = ch end else optstr[#optstr+1] = w end end local attrs = U.parse_attrs(table.concat(optstr, " "), { tag = "solid" }) local proj = projector(attrs) local ox = state.cursor local tex, xmax, _ if kind == "cube" or kind == "cuboid" then local a, b, c if kind == "cube" then a = num_attr(attrs, "edge", "cube"); b, c = a, a else local sv = attrs.sides if not sv then error("texecole: solid cuboid needs sides:{a, b, c}") end a, b, c = sv:match("^%s*([%d%.]+)%s*,%s*([%d%.]+)%s*,%s*([%d%.]+)%s*$") a, b, c = tonumber(a), tonumber(b), tonumber(c) if not a then error("texecole: solid cuboid sides:{a, b, c} needs three numbers") end end if names and #names ~= 8 then error("texecole: solid " .. kind .. " names 8 vertices (ABCDEFGH), got " .. #names) end local verts, faces = box_geometry(a, b, c) tex, _, xmax = poly_render(verts, faces, names, proj, ox) state.cursor = xmax + 1.5 elseif kind == "pyramid" then local b = num_attr(attrs, "base", "pyramid") local h = num_attr(attrs, "height", "pyramid") if names and #names ~= 5 then error("texecole: solid pyramid names 5 vertices (SABCD), got " .. #names) end local verts, faces = pyramid_geometry(b, h) tex, _, xmax = poly_render(verts, faces, names, proj, ox) state.cursor = xmax + 1.5 elseif kind == "cylinder" then local r = num_attr(attrs, "radius", "cylinder") local h = num_attr(attrs, "height", "cylinder") tex = cylinder_render(r, h, proj, ox + r) state.cursor = ox + 2*r + 1.9 elseif kind == "cone" then local r = num_attr(attrs, "radius", "cone") local h = num_attr(attrs, "height", "cone") tex = cone_render(r, h, proj, ox + r) state.cursor = ox + 2*r + 1.9 else local r = num_attr(attrs, "radius", "sphere") tex = sphere_render(r, proj, ox + r) state.cursor = ox + 2*r + 1.9 end return tex end return M