模組:Graph/MathEngine
外观
-- Module:Graph/MathEngine
-- 維基特化版 Vega 3D 數學核心:Z-Up 右手座標系、自動視窗縮放、背面剔除、科學色階光影與畫家演算法 (Z-Sorting)
-- 注意:本模組由 SVGRenderer 按需延遲載入 (Lazy-loaded),純 2D 圖表不會載入此模組。
local p = {}
local Utils = require('Module:Graph/RenderUtils')
local sin, cos, sqrt, pi = math.sin, math.cos, math.sqrt, math.pi
local floor, max, min, abs = math.floor, math.max, math.min, math.abs
local format = string.format
local DEG2RAD = pi / 180
-- 內建科學視覺化連續色階 (共用 RenderUtils 科學色階表,含 jet, viridis, coolwarm, plasma, inferno, magma, rainbow, gray, greys)
local COLORMAPS = Utils.COLORMAPS
p.COLORMAPS = COLORMAPS
-- 建立 Z-Up 3D 相機與投影器
-- 正規化世界座標:wx ∈ [-1,1] (左前 X 軸), wy ∈ [-1,1] (右前 Y 軸), wz ∈ [-1,1] (垂直高度 Z 軸)
function p.createCamera(opts)
opts = opts or {}
local theta = (tonumber(opts.theta) or 45) * DEG2RAD
local phi = max(5, min(85, tonumber(opts.phi) or 28)) * DEG2RAD
local dist = tonumber(opts.distance)
if dist == nil then dist = 5.5 end
local cx = tonumber(opts.cx) or 200
local cy = tonumber(opts.cy) or 150
local scale = tonumber(opts.scale) or 100
local cosT, sinT = cos(theta), sin(theta)
local cosP, sinP = cos(phi), sin(phi)
-- 平行光源單位向量 (相機空間:左上方偏前)
local lx, ly, lz = -0.35, 0.65, 0.67
local cam = {
theta = theta,
phi = phi,
distance = dist,
cx = cx,
cy = cy,
scale = scale,
cosT = cosT,
sinT = sinT,
cosP = cosP,
sinP = sinP
}
-- 將正規化世界座標 (wx, wy, wz) 投影至 2D 螢幕與相機深度 (嚴格右手正交基底)
function cam.project(wx, wy, wz)
-- 1. 繞垂直 Z 軸旋轉 (Azimuth): wx=(-1,-1) 位於左方, (1,-1) 位於最前緣下方, (1,1) 位於右方
local x1 = wx * cosT + wy * sinT
local d1 = wx * sinT - wy * cosT
-- 2. 繞水平軸俯仰傾斜 (Elevation): wz 朝上, d1 愈大離鏡頭愈近 (螢幕越偏下方)
local xCam = x1
local yCam = wz * cosP - d1 * sinP
local depth = d1 * cosP + wz * sinP
-- 3. 透視投影 (dist <= 0 時為正交投影)
local pFactor = 1.0
if dist > 0 then
local denom = max(0.8, dist - depth)
pFactor = dist / denom
end
return {
sx = cx + xCam * pFactor * scale,
sy = cy - yCam * pFactor * scale,
xCam = xCam,
yCam = yCam,
depth = depth,
pScale = pFactor
}
end
-- 自動校準 cx, cy, scale,使 [-1,1]^3 座標盒與刻度完美填滿指定繪圖區而不裁切
function cam.fitToViewport(left, top, width, height)
local oldCx, oldCy, oldScale = cx, cy, scale
cx, cy, scale = 0, 0, 1
local minX, maxX, minY, maxY = math.huge, -math.huge, math.huge, -math.huge
for ix = -1, 1, 2 do
for iy = -1, 1, 2 do
for iz = -1, 1, 2 do
local pt = cam.project(ix, iy, iz)
if pt.sx < minX then minX = pt.sx end
if pt.sx > maxX then maxX = pt.sx end
if pt.sy < minY then minY = pt.sy end
if pt.sy > maxY then maxY = pt.sy end
end
end
end
local spanX = max(1e-4, maxX - minX)
local spanY = max(1e-4, maxY - minY)
scale = min(width / spanX, height / spanY) * 0.88
cx = left + width * 0.5 - ((minX + maxX) * 0.5) * scale
cy = top + height * 0.5 - ((minY + maxY) * 0.5) * scale
cam.cx, cam.cy, cam.scale = cx, cy, scale
return oldCx, oldCy, oldScale
end
-- 旋轉法向量 (nx, ny, nz) 至相機空間並計算蘭伯特光照強度係數
function cam.computeLightFactor(nx, ny, nz, baseBias)
local x1 = nx * cosT + ny * sinT
local d1 = nx * sinT - ny * cosT
local xCam = x1
local yCam = nz * cosP - d1 * sinP
local zCam = d1 * cosP + nz * sinP
local dot = abs(xCam * lx + yCam * ly + zCam * lz)
local bias = baseBias or 0.72
return min(1.20, max(0.52, bias + 0.42 * dot))
end
return cam
end
-- 根據光照強度調整色碼明暗度
function p.shadeColor(hexStr, factor)
local rgb = Utils.parseRGB(hexStr)
local r, g, b = rgb[1], rgb[2], rgb[3]
if factor > 1.0 then
local t = min(1, (factor - 1.0) * 1.35)
r = r + (255 - r) * t
g = g + (255 - g) * t
b = b + (255 - b) * t
else
r = r * factor
g = g * factor
b = b * factor
end
return Utils.rgbToHex(r, g, b)
end
-- 將正規化數值 t ∈ [0, 1] 映射為科學色階 Hex (含 jet 原生分段線性公式與各科學色階)
function p.sampleColormap(t, mapName)
return Utils.sampleColormap(t, mapName)
end
-- 建立 3D 曲面網格四邊形面片 (自動計算雙向切向量外積法向量與光影,供 surface3d 使用)
function p.buildQuadFace(cam, w1, w2, w3, w4, baseColor, alpha, orderSeed)
local p1 = cam.project(w1[1], w1[2], w1[3])
local p2 = cam.project(w2[1], w2[2], w2[3])
local p3 = cam.project(w3[1], w3[2], w3[3])
local p4 = cam.project(w4[1], w4[2], w4[3])
-- 計算法向量 (w2 - w1) × (w4 - w1)
local ux, uy, uz = w2[1] - w1[1], w2[2] - w1[2], w2[3] - w1[3]
local vx, vy, vz = w4[1] - w1[1], w4[2] - w1[2], w4[3] - w1[3]
local nx = uy * vz - uz * vy
local ny = uz * vx - ux * vz
local nz = ux * vy - uy * vx
local len = sqrt(nx * nx + ny * ny + nz * nz)
if len > 1e-9 then
nx, ny, nz = nx / len, ny / len, nz / len
else
nx, ny, nz = 0, 0, 1
end
local lightFactor = cam.computeLightFactor(nx, ny, nz, 0.74)
local fillHex = p.shadeColor(baseColor, lightFactor)
local strokeHex = p.shadeColor(baseColor, lightFactor * 0.78)
local avgDepth = (p1.depth + p2.depth + p3.depth + p4.depth) * 0.25
return {
kind = "poly",
depth = avgDepth,
order = orderSeed or 0,
points = format("%.2f,%.2f %.2f,%.2f %.2f,%.2f %.2f,%.2f", p1.sx, p1.sy, p2.sx, p2.sy, p3.sx, p3.sy, p4.sx, p4.sy),
fill = fillHex,
stroke = strokeHex,
strokeWidth = 0.45,
alpha = alpha or 0.92,
cx = (p1.sx + p2.sx + p3.sx + p4.sx) * 0.25,
cy = (p1.sy + p2.sy + p3.sy + p4.sy) * 0.25
}
end
-- 產生單一 Z-Up 3D 立體柱 (Cuboid) 的可見面列表 (內建 2D 螢幕空間外積背面剔除)
-- 輸入範圍:x0..x1 (X 軸), y0..y1 (Y 軸), z0..z1 (垂直高度 Z 軸) ∈ [-1, 1]
function p.buildCuboidFaces(cam, x0, x1, y0, y1, z0, z1, baseColor, alpha, orderSeed)
if x0 > x1 then x0, x1 = x1, x0 end
if y0 > y1 then y0, y1 = y1, y0 end
if z0 > z1 then z0, z1 = z1, z0 end
local v = {
cam.project(x0, y0, z0), -- 1: 底-左後
cam.project(x1, y0, z0), -- 2: 底-前
cam.project(x1, y1, z0), -- 3: 底-右前
cam.project(x0, y1, z0), -- 4: 底-後
cam.project(x0, y0, z1), -- 5: 頂-左後
cam.project(x1, y0, z1), -- 6: 頂-前
cam.project(x1, y1, z1), -- 7: 頂-右前
cam.project(x0, y1, z1) -- 8: 頂-後
}
-- 六個面的逆時針頂點索引 (由外側觀看) 與外法向量
local faceDefs = {
{ idx = {5, 6, 7, 8}, nx = 0, ny = 0, nz = 1, bias = 0.88 }, -- Top (+Z)
{ idx = {1, 4, 3, 2}, nx = 0, ny = 0, nz = -1, bias = 0.50 }, -- Bottom (-Z)
{ idx = {2, 3, 7, 6}, nx = 1, ny = 0, nz = 0, bias = 0.78 }, -- Front-Right (+X)
{ idx = {4, 1, 5, 8}, nx = -1, ny = 0, nz = 0, bias = 0.60 }, -- Back-Left (-X)
{ idx = {3, 4, 8, 7}, nx = 0, ny = 1, nz = 0, bias = 0.68 }, -- Back-Right (+Y)
{ idx = {1, 2, 6, 5}, nx = 0, ny = -1, nz = 0, bias = 0.74 } -- Front-Left (-Y)
}
local barCenter = cam.project((x0 + x1) * 0.5, (y0 + y1) * 0.5, (z0 + z1) * 0.5)
local topCenter = cam.project((x0 + x1) * 0.5, (y0 + y1) * 0.5, z1)
local facesOut = {}
local seedBase = (orderSeed or 0) * 10
for fIdx = 1, 6 do
local fd = faceDefs[fIdx]
local p1, p2, p3, p4 = v[fd.idx[1]], v[fd.idx[2]], v[fd.idx[3]], v[fd.idx[4]]
-- 2D 螢幕空間外積背面剔除:在 SVG 座標系 (Y 向下) 中,正對鏡頭的逆時針面 cross2D < 0
local cross2D = (p2.sx - p1.sx) * (p3.sy - p1.sy) - (p2.sy - p1.sy) * (p3.sx - p1.sx)
if cross2D < -1e-2 then
local faceDepth = (p1.depth + p2.depth + p3.depth + p4.depth) * 0.25
local lightFactor = cam.computeLightFactor(fd.nx, fd.ny, fd.nz, fd.bias)
local shadedHex = p.shadeColor(baseColor, lightFactor)
local ptsStr = format("%.2f,%.2f %.2f,%.2f %.2f,%.2f %.2f,%.2f",
p1.sx, p1.sy, p2.sx, p2.sy, p3.sx, p3.sy, p4.sx, p4.sy)
facesOut[#facesOut + 1] = {
kind = "poly",
depth = barCenter.depth + faceDepth * 0.08,
order = seedBase + fIdx,
points = ptsStr,
fill = shadedHex,
stroke = p.shadeColor(baseColor, lightFactor * 0.80),
strokeWidth = 0.6,
alpha = alpha or 1.0
}
end
end
return facesOut, topCenter, barCenter
end
-- 建構開放式科學 3D 座標底座 (對齊 FunctionGraph 3D 視覺風格:底面網格 + 外圍三主軸 + 外伸實體刻度線)
function p.buildBoxStage(cam, xTickNorms, yTickNorms, zTickNorms)
-- 根據方位角自動判定哪兩條底邊位於前緣、哪個角點位於最左側立起 Z 軸
local frontX = (cam.sinT >= 0) and 1 or -1
local frontY = (cam.cosT >= 0) and -1 or 1
local zAxisX = (cam.cosT >= 0) and -1 or 1
local zAxisY = (cam.sinT >= 0) and -1 or 1
local floorZ = -1
local grids = {}
local tickLines = {}
local xAnchors, yAnchors, zAnchors = {}, {}, {}
local function makeLine(a, b)
local p1, p2 = cam.project(a[1], a[2], a[3]), cam.project(b[1], b[2], b[3])
return { x1 = p1.sx, y1 = p1.sy, x2 = p2.sx, y2 = p2.sy }
end
-- 1. 底板四邊外框線 (z = -1)
grids[#grids + 1] = makeLine({-1, -1, floorZ}, {1, -1, floorZ})
grids[#grids + 1] = makeLine({1, -1, floorZ}, {1, 1, floorZ})
grids[#grids + 1] = makeLine({1, 1, floorZ}, {-1, 1, floorZ})
grids[#grids + 1] = makeLine({-1, 1, floorZ}, {-1, -1, floorZ})
local tickLen = 0.065
local lblOffset = 0.16
-- 2. X 軸底板格線、外伸刻度線與標籤錨點 (沿 y = frontY 前緣)
if type(xTickNorms) == "table" then
local outY = frontY + (frontY > 0 and tickLen or -tickLen)
local txtY = frontY + (frontY > 0 and lblOffset or -lblOffset)
for _, t in ipairs(xTickNorms) do
local wx = t.norm
grids[#grids + 1] = makeLine({wx, -1, floorZ}, {wx, 1, floorZ})
tickLines[#tickLines + 1] = makeLine({wx, frontY, floorZ}, {wx, outY, floorZ})
local pt = cam.project(wx, txtY, floorZ)
xAnchors[#xAnchors + 1] = { sx = pt.sx, sy = pt.sy + 4, label = t.label }
end
end
-- 3. Y 軸底板格線、外伸刻度線與標籤錨點 (沿 x = frontX 前緣)
if type(yTickNorms) == "table" then
local outX = frontX + (frontX > 0 and tickLen or -tickLen)
local txtX = frontX + (frontX > 0 and lblOffset or -lblOffset)
for _, t in ipairs(yTickNorms) do
local wy = t.norm
grids[#grids + 1] = makeLine({-1, wy, floorZ}, {1, wy, floorZ})
tickLines[#tickLines + 1] = makeLine({frontX, wy, floorZ}, {outX, wy, floorZ})
local pt = cam.project(txtX, wy, floorZ)
yAnchors[#yAnchors + 1] = { sx = pt.sx, sy = pt.sy + 4, label = t.label }
end
end
-- 4. Z 軸垂直刻度線與標籤錨點 (立於最左側角點 x = zAxisX, y = zAxisY)
if type(zTickNorms) == "table" then
local outZX = zAxisX + (zAxisX > 0 and tickLen or -tickLen) * 0.7
local outZY = zAxisY + (zAxisY > 0 and tickLen or -tickLen) * 0.7
local txtZX = zAxisX + (zAxisX > 0 and lblOffset or -lblOffset) * 0.85
local txtZY = zAxisY + (zAxisY > 0 and lblOffset or -lblOffset) * 0.85
for _, t in ipairs(zTickNorms) do
local wz = t.norm
tickLines[#tickLines + 1] = makeLine({zAxisX, zAxisY, wz}, {outZX, outZY, wz})
local pt = cam.project(txtZX, txtZY, wz)
zAnchors[#zAnchors + 1] = { sx = pt.sx - 2, sy = pt.sy + 3.5, label = t.label, alignRight = true }
end
end
-- 5. 三條外圍實心主軸線 (X 前緣、Y 前緣、Z 左側垂直軸)
local axesLines = {
makeLine({-1, frontY, floorZ}, {1, frontY, floorZ}),
makeLine({frontX, -1, floorZ}, {frontX, 1, floorZ}),
makeLine({zAxisX, zAxisY, -1}, {zAxisX, zAxisY, 1})
}
-- 6. 三軸標題錨點 (x 於左前緣中央外側、y 於右前緣中央外側、z 於垂直軸正上方,與 FunctionGraph 一致)
local titleAnchors = {
x = cam.project(0, frontY + (frontY > 0 and 0.34 or -0.34), floorZ - 0.04),
y = cam.project(frontX + (frontX > 0 and 0.34 or -0.34), 0, floorZ - 0.04),
z = cam.project(zAxisX, zAxisY, 1.18)
}
return {
grids = grids,
tickLines = tickLines,
axesLines = axesLines,
xAnchors = xAnchors,
yAnchors = yAnchors,
zAnchors = zAnchors,
titleAnchors = titleAnchors,
floorZ = floorZ
}
end
-- 畫家演算法 (Painter's Algorithm):依深度由遠到近 (depth 升冪) 嚴格弱序排序
function p.depthSort(items)
if type(items) ~= "table" or #items <= 1 then return items end
table.sort(items, function(a, b)
if type(a) ~= "table" or type(b) ~= "table" then return false end
local da = tonumber(a.depth) or 0
local db = tonumber(b.depth) or 0
if abs(da - db) > 1e-6 then
return da < db
end
local oa = tonumber(a.order) or 0
local ob = tonumber(b.order) or 0
if oa == ob then return false end
return oa < ob
end)
return items
end
return p