模組:沙盒/a2569875/PythonScript/ExtraLibrarys/Matplotlib/MathEngine
外观
-- Module:PythonScript/ExtraLibrarys/Matplotlib/MathEngine
-- 負責 Matplotlib 3D 空間矩陣變換、相機正交/透視投影、曲面法向量光照 (Flat Shading)、
-- 畫家演算法深度排序 (Z-sorting)、3D 座標盒幾何判定、NumPy 網格解包與複變定義域著色計算
local p = {}
local RendererUtils = require("Module:沙盒/a2569875/PythonScript/ExtraLibrarys/Matplotlib/RendererUtils")
local sin, cos, sqrt, abs, floor, max, min, rad, atan2, log, ceil = math.sin, math.cos, math.sqrt, math.abs, math.floor, math.max, math.min, math.rad, math.atan2, math.log, math.ceil
local format, sort = string.format, table.sort
-- ==========================================
-- 1. Python / NumPy 陣列解包與數值正規化工具
-- ==========================================
local function unwrap_number(v)
if type(v) == "number" then return v end
if type(v) == "boolean" then return v and 1 or 0 end
if type(v) == "table" then
if v.__type == "int" then
return tonumber(v.value) or 0
elseif v.__type == "instance" and v.__builtin_value ~= nil then
return unwrap_number(v.__builtin_value)
elseif v.__class and v.__class.__name == "NoneType" then
return 0 / 0
end
end
return tonumber(v) or (0 / 0)
end
p.unwrap_number = unwrap_number
-- 將 Python list / tuple / NumPy 1D ndarray 轉為純 Lua 1D number array
function p.to1DArray(obj)
if obj == nil then return {} end
if type(obj) == "table" then
if obj.__class and obj.__class.__name == "NoneType" then return {} end
-- 原生 Lua array
if obj.__type == nil and #obj > 0 and type(obj[1]) == "number" then
return obj
end
-- Python list / tuple
if obj.elements then
local out = {}
for i = 1, #obj.elements do
out[i] = unwrap_number(obj.elements[i])
end
return out
end
-- NumPy ndarray 或自訂實例
if obj.__type == "instance" and obj.__dict then
local d = obj.__dict
if type(d._data) == "table" and d._shape then
local out = {}
for i = 1, #d._data do
out[i] = unwrap_number(d._data[i])
end
return out
elseif type(d.data) == "table" and d.data.elements then
return p.to1DArray(d.data)
end
local PyClass = require("Module:沙盒/a2569875/PythonScript/Class")
local ok, tolist_m = pcall(PyClass.get_attr, obj, "tolist")
if ok and tolist_m then
local ok2, lst = pcall(require("Module:沙盒/a2569875/PythonScript/Evaluator").invoke, tolist_m, {}, {})
if ok2 and type(lst) == "table" then
return p.to1DArray(lst)
end
end
end
end
return {}
end
-- 將 Python 2D list 或 NumPy 2D ndarray 轉為純 Lua 2D 矩陣 grid[r][c]
function p.to2DGrid(obj)
if obj == nil then return {} end
if type(obj) == "table" then
if obj.__class and obj.__class.__name == "NoneType" then return {} end
if obj.__type == nil and #obj > 0 and type(obj[1]) == "table" then
return obj
end
if obj.elements then
local out = {}
for r = 1, #obj.elements do
local rowObj = obj.elements[r]
if type(rowObj) == "table" then
out[r] = p.to1DArray(rowObj)
else
out[r] = { unwrap_number(rowObj) }
end
end
return out
end
if obj.__type == "instance" and obj.__dict then
local d = obj.__dict
if type(d._data) == "table" and type(d._shape) == "table" then
local shape = d._shape.elements or d._shape
local nRows = tonumber(shape[1]) or 0
local nCols = tonumber(shape[2]) or 0
if nRows > 0 and nCols > 0 then
local out = {}
local idx = 1
for r = 1, nRows do
local row = {}
for c = 1, nCols do
row[c] = unwrap_number(d._data[idx])
idx = idx + 1
end
out[r] = row
end
return out
end
end
local PyClass = require("Module:沙盒/a2569875/PythonScript/Class")
local ok, tolist_m = pcall(PyClass.get_attr, obj, "tolist")
if ok and tolist_m then
local ok2, lst = pcall(require("Module:沙盒/a2569875/PythonScript/Evaluator").invoke, tolist_m, {}, {})
if ok2 and type(lst) == "table" then
return p.to2DGrid(lst)
end
end
end
end
return {}
end
-- 將 (X, Y, Z) 2D 陣列組裝為 3D 頂點網格 grid[u][v] = {x, y, z},並支援 rstride / cstride 與自動安全降採樣
function p.prepareSurfaceGrid(X_in, Y_in, Z_in, rstride, cstride, maxDivs)
local Z = p.to2DGrid(Z_in)
local nRows = #Z
if nRows < 2 then return {}, 0, 0 end
local nCols = #(Z[1] or {})
if nCols < 2 then return {}, 0, 0 end
local X = p.to2DGrid(X_in)
local Y = p.to2DGrid(Y_in)
-- 支援 X 與 Y 傳入 1D 座標向量的情況 (自動擴展為 2D meshgrid)
if #X == 1 or (#X == nCols and #(X[1] or {}) == 1) then
local x1d = p.to1DArray(X_in)
X = {}
for r = 1, nRows do
local row = {}
for c = 1, nCols do row[c] = x1d[c] or c end
X[r] = row
end
end
if #Y == 1 or (#Y == nRows and #(Y[1] or {}) == 1) then
local y1d = p.to1DArray(Y_in)
Y = {}
for r = 1, nRows do
local row = {}
for c = 1, nCols do row[c] = y1d[r] or r end
Y[r] = row
end
end
maxDivs = maxDivs or 36
local rs = max(1, floor(tonumber(rstride) or ceil((nRows - 1) / maxDivs)))
local cs = max(1, floor(tonumber(cstride) or ceil((nCols - 1) / maxDivs)))
local rIndices, cIndices = {}, {}
for r = 1, nRows, rs do rIndices[#rIndices + 1] = r end
if rIndices[#rIndices] ~= nRows then rIndices[#rIndices + 1] = nRows end
for c = 1, nCols, cs do cIndices[#cIndices + 1] = c end
if cIndices[#cIndices] ~= nCols then cIndices[#cIndices + 1] = nCols end
local grid = {}
for ru, rIdx in ipairs(rIndices) do
local row = {}
local xRow = X[rIdx] or {}
local yRow = Y[rIdx] or {}
local zRow = Z[rIdx] or {}
for cv, cIdx in ipairs(cIndices) do
local vx = tonumber(xRow[cIdx]) or cIdx
local vy = tonumber(yRow[cIdx]) or rIdx
local vz = tonumber(zRow[cIdx])
if vx and vy and vz and vx == vx and vy == vy and vz == vz and abs(vx) < 1e12 and abs(vy) < 1e12 and abs(vz) < 1e12 then
row[cv] = { vx, vy, vz }
else
row[cv] = false
end
end
grid[ru] = row
end
return grid, #rIndices - 1, #cIndices - 1
end
-- 掃描所有 3D series (surface3d, wireframe3d, curve3d, scatter3d) 計算 X, Y, Z 邊界
function p.compute3DBounds(series_list, axOpts)
axOpts = axOpts or {}
local dataXMin, dataXMax = math.huge, -math.huge
local dataYMin, dataYMax = math.huge, -math.huge
local dataZMin, dataZMax = math.huge, -math.huge
local function rec(vx, vy, vz)
if vx and vy and vz and vx == vx and vy == vy and vz == vz and abs(vx) < 1e12 and abs(vy) < 1e12 and abs(vz) < 1e12 then
if vx < dataXMin then dataXMin = vx end
if vx > dataXMax then dataXMax = vx end
if vy < dataYMin then dataYMin = vy end
if vy > dataYMax then dataYMax = vy end
if vz < dataZMin then dataZMin = vz end
if vz > dataZMax then dataZMax = vz end
end
end
for _, s in ipairs(series_list or {}) do
if s.kind == "surface3d" or s.kind == "wireframe3d" then
local grid = s.grid or {}
for r = 1, #grid do
local row = grid[r]
for c = 1, #row do
local pt = row[c]
if pt then rec(pt[1], pt[2], pt[3]) end
end
end
elseif s.kind == "curve3d" or s.kind == "scatter3d" then
local pts = s.points or {}
for k = 1, #pts do
local pt = pts[k]
if pt then rec(pt[1], pt[2], pt[3]) end
end
end
end
local xMin = tonumber(axOpts.xlim and axOpts.xlim[1]) or (dataXMin < math.huge and dataXMin or -3)
local xMax = tonumber(axOpts.xlim and axOpts.xlim[2]) or (dataXMax > -math.huge and dataXMax or 3)
local yMin = tonumber(axOpts.ylim and axOpts.ylim[1]) or (dataYMin < math.huge and dataYMin or -3)
local yMax = tonumber(axOpts.ylim and axOpts.ylim[2]) or (dataYMax > -math.huge and dataYMax or 3)
local zMin = tonumber(axOpts.zlim and axOpts.zlim[1]) or (dataZMin < math.huge and dataZMin or -1)
local zMax = tonumber(axOpts.zlim and axOpts.zlim[2]) or (dataZMax > -math.huge and dataZMax or 1)
if xMin == xMax then xMin, xMax = xMin - 1, xMax + 1 end
if yMin == yMax then yMin, yMax = yMin - 1, yMax + 1 end
if zMin == zMax then zMin, zMax = zMin - 1, zMax + 1 end
if not (axOpts.xlim and axOpts.xlim[1]) and not (axOpts.xlim and axOpts.xlim[2]) then
local xSpan = xMax - xMin
xMin = xMin - xSpan * 0.025
xMax = xMax + xSpan * 0.025
end
if not (axOpts.ylim and axOpts.ylim[1]) and not (axOpts.ylim and axOpts.ylim[2]) then
local ySpan = yMax - yMin
yMin = yMin - ySpan * 0.025
yMax = yMax + ySpan * 0.025
end
if not (axOpts.zlim and axOpts.zlim[1]) and not (axOpts.zlim and axOpts.zlim[2]) then
local zSpan = zMax - zMin
zMin = zMin - zSpan * 0.05
zMax = zMax + zSpan * 0.05
end
return {
xMin = xMin, xMax = xMax,
yMin = yMin, yMax = yMax,
zMin = zMin, zMax = zMax,
dataZMin = (dataZMin < math.huge) and dataZMin or zMin,
dataZMax = (dataZMax > -math.huge) and dataZMax or zMax
}
end
-- ==========================================
-- 2. 3D 相機矩陣與座標盒幾何判定
-- ==========================================
function p.buildCamera(bounds, plotCenter, plotRadius, opts)
opts = opts or {}
local thetaDeg = tonumber(opts.azim or opts.view_theta or opts.azimuth) or -60
local phiDeg = tonumber(opts.elev or opts.view_phi or opts.elevation) or 28
local rollDeg = tonumber(opts.roll or opts.view_roll) or 0
local projMode = string.lower(tostring(opts.proj_type or opts.projection or "ortho"))
local camDist = max(2.2, tonumber(opts.view_dist or opts.distance) or 4.5)
local xMin, xMax = bounds.xMin, bounds.xMax
local yMin, yMax = bounds.yMin, bounds.yMax
local zMin, zMax = bounds.zMin, bounds.zMax
local xMid, xSpan = (xMin + xMax) * 0.5, max(1e-9, xMax - xMin)
local yMid, ySpan = (yMin + yMax) * 0.5, max(1e-9, yMax - yMin)
local zMid, zSpan = (zMin + zMax) * 0.5, max(1e-9, zMax - zMin)
-- 決定正規化立方體半邊長 (預設 1 : 1 : 0.78,支援 equal_scale 與 box_aspect)
local bx, by, bz = 1.0, 1.0, 0.78
if opts.equal_scale then
local maxSpan = max(xSpan, ySpan, zSpan)
bx = xSpan / maxSpan
by = ySpan / maxSpan
bz = zSpan / maxSpan
elseif opts.box_aspect then
local rx, ry, rz = 1, 1, 0.78
if type(opts.box_aspect) == "table" then
local el = opts.box_aspect.elements or opts.box_aspect
rx = unwrap_number(el[1]) or 1
ry = unwrap_number(el[2]) or 1
rz = unwrap_number(el[3]) or 0.78
elseif type(opts.box_aspect) == "string" then
local parts = mw.text.split(opts.box_aspect, "%s*[,:]%s*")
if #parts >= 3 then
rx, ry, rz = tonumber(parts[1]) or 1, tonumber(parts[2]) or 1, tonumber(parts[3]) or 0.78
end
end
local m = max(rx, ry, rz, 1e-6)
bx, by, bz = rx / m, ry / m, rz / m
end
local th = rad(thetaDeg)
local ph = rad(phiDeg)
local rl = rad(rollDeg)
local cosT, sinT = cos(th), sin(th)
local cosP, sinP = cos(ph), sin(ph)
local cosR, sinR = cos(rl), sin(rl)
-- 相機基底向量:右向量 R、上向量 U、視線深度向量 F (由原點指向相機)
local rx0, ry0, rz0 = -sinT, cosT, 0
local ux0, uy0, uz0 = -sinP * cosT, -sinP * sinT, cosP
local fx, fy, fz = cosP * cosT, cosP * sinT, sinP
local rx = rx0 * cosR + ux0 * sinR
local ry = ry0 * cosR + uy0 * sinR
local rz = rz0 * cosR + uz0 * sinR
local ux = -rx0 * sinR + ux0 * cosR
local uy = -ry0 * sinR + uy0 * cosR
local uz = -rz0 * sinR + uz0 * cosR
local cx, cy = plotCenter[1], plotCenter[2]
local scalePx = plotRadius
local function toNorm(x, y, z)
return ((x - xMid) / (xSpan * 0.5)) * bx,
((y - yMid) / (ySpan * 0.5)) * by,
((z - zMid) / (zSpan * 0.5)) * bz
end
local function projectNorm(nx, ny, nz)
local camX = nx * rx + ny * ry + nz * rz
local camY = nx * ux + ny * uy + nz * uz
local depth = nx * fx + ny * fy + nz * fz
local persp = 1.0
if projMode == "perspective" or projMode == "persp" then
persp = camDist / max(0.6, camDist - depth)
end
local px = cx + camX * scalePx * persp
local py = cy - camY * scalePx * persp
return px, py, depth
end
local function projectPoint(x, y, z)
local nx, ny, nz = toNorm(x, y, z)
return projectNorm(nx, ny, nz)
end
return {
toNorm = toNorm,
projectNorm = projectNorm,
projectPoint = projectPoint,
viewDir = { fx, fy, fz },
rightDir = { rx, ry, rz },
upDir = { ux, uy, uz },
boxScale = { bx, by, bz },
bounds = bounds
}
end
function p.buildBoxGeometry(cam)
local bx, by, bz = cam.boxScale[1], cam.boxScale[2], cam.boxScale[3]
local fx, fy, fz = cam.viewDir[1], cam.viewDir[2], cam.viewDir[3]
local backX = (fx >= 0) and -bx or bx
local frontX = -backX
local backY = (fy >= 0) and -by or by
local frontY = -backY
local backZ = (fz >= 0) and -bz or bz
local frontZ = -backZ
local b = cam.bounds
local xBackVal = (backX < 0) and b.xMin or b.xMax
local xFrontVal = (frontX < 0) and b.xMin or b.xMax
local yBackVal = (backY < 0) and b.yMin or b.yMax
local yFrontVal = (frontY < 0) and b.yMin or b.yMax
local zBackVal = (backZ < 0) and b.zMin or b.zMax
local zFrontVal = (frontZ < 0) and b.zMin or b.zMax
local backFaces = {
{
plane = "XY",
zVal = zBackVal,
corners = {
{ -bx, -by, backZ }, { bx, -by, backZ }, { bx, by, backZ }, { -bx, by, backZ }
}
},
{
plane = "YZ",
xVal = xBackVal,
corners = {
{ backX, -by, -bz }, { backX, by, -bz }, { backX, by, bz }, { backX, -by, bz }
}
},
{
plane = "XZ",
yVal = yBackVal,
corners = {
{ -bx, backY, -bz }, { bx, backY, -bz }, { bx, backY, bz }, { -bx, backY, bz }
}
}
}
local pZ1x = cam.projectNorm(frontX, backY, 0)
local pZ2x = cam.projectNorm(backX, frontY, 0)
local zAxisX, zAxisY, zTickDirX, zTickDirY
if pZ1x <= pZ2x then
zAxisX, zAxisY = frontX, backY
zTickDirX, zTickDirY = 0, (backY < 0 and -1 or 1)
else
zAxisX, zAxisY = backX, frontY
zTickDirX, zTickDirY = (backX < 0 and -1 or 1), 0
end
return {
backFaces = backFaces,
backX = backX, frontX = frontX,
backY = backY, frontY = frontY,
backZ = backZ, frontZ = frontZ,
xBackVal = xBackVal, xFrontVal = xFrontVal,
yBackVal = yBackVal, yFrontVal = yFrontVal,
zBackVal = zBackVal, zFrontVal = zFrontVal,
axes = {
x = {
yNorm = frontY, zNorm = backZ,
tickVec = { 0, (frontY < 0 and -1 or 1), 0 }
},
y = {
xNorm = frontX, zNorm = backZ,
tickVec = { (frontX < 0 and -1 or 1), 0, 0 }
},
z = {
xNorm = zAxisX, yNorm = zAxisY,
tickVec = { zTickDirX, zTickDirY, 0 }
}
}
}
end
-- ==========================================
-- 3. 3D 圖元投影、法向量雙面光照與畫家演算法 (Z-sorting)
-- ==========================================
function p.buildScenePrimitives(series_list, bounds, cam, palette, opts)
opts = opts or {}
local primitives = {}
local b = bounds
local zDataSpan = max(1e-9, (b.dataZMax or b.zMax) - (b.dataZMin or b.zMin))
local zDataMin = b.dataZMin or b.zMin
-- 主光源向量 (結合相機左上方與世界正上方)
local lx = cam.viewDir[1] * 0.45 - cam.rightDir[1] * 0.35 + cam.upDir[1] * 0.75
local ly = cam.viewDir[2] * 0.45 - cam.rightDir[2] * 0.35 + cam.upDir[2] * 0.75
local lz = cam.viewDir[3] * 0.45 - cam.rightDir[3] * 0.35 + cam.upDir[3] * 0.75 + 0.4
local lLen = sqrt(lx * lx + ly * ly + lz * lz)
if lLen > 1e-9 then lx, ly, lz = lx / lLen, ly / lLen, lz / lLen end
for sIdx, s in ipairs(series_list or {}) do
local defaultCol = palette[(sIdx - 1) % #palette + 1]
local baseColor, baseAlpha = RendererUtils.parseColor(s.color, defaultCol)
local sAlpha = tonumber(s.alpha) or baseAlpha or (s.kind == "surface3d" and 0.92 or 1.0)
local cmapName = s.cmap or opts.colormap
if cmapName == "none" or cmapName == "no" or cmapName == "false" then cmapName = nil end
if s.kind == "surface3d" then
local grid = s.grid or {}
local nu = s.nu or (#grid - 1)
local nv = s.nv or (#(grid[1] or {}) - 1)
local showMesh = (s.edgecolor ~= "none" and s.mesh ~= false)
local edgeColOverride = (s.edgecolor and s.edgecolor ~= "auto" and s.edgecolor ~= "none") and RendererUtils.parseColor(s.edgecolor) or nil
local edgeLW = tonumber(s.linewidth) or (showMesh and 0.45 or 0.15)
local useCmap = cmapName or (not s.color and "viridis" or nil)
for u = 1, nu do
local row0 = grid[u]
local row1 = grid[u + 1]
if row0 and row1 then
for v = 1, nv do
local p00, p10, p11, p01 = row0[v], row1[v], row1[v + 1], row0[v + 1]
if p00 and p10 and p11 and p01 then
local nx00, ny00, nz00 = cam.toNorm(p00[1], p00[2], p00[3])
local nx10, ny10, nz10 = cam.toNorm(p10[1], p10[2], p10[3])
local nx11, ny11, nz11 = cam.toNorm(p11[1], p11[2], p11[3])
local nx01, ny01, nz01 = cam.toNorm(p01[1], p01[2], p01[3])
local x00, y00, d00 = cam.projectNorm(nx00, ny00, nz00)
local x10, y10, d10 = cam.projectNorm(nx10, ny10, nz10)
local x11, y11, d11 = cam.projectNorm(nx11, ny11, nz11)
local x01, y01, d01 = cam.projectNorm(nx01, ny01, nz01)
-- 對角線外積計算正規化法向量
local ax, ay, az = nx11 - nx00, ny11 - ny00, nz11 - nz00
local bx, by, bz = nx01 - nx10, ny01 - ny10, nz01 - nz10
local vnx = ay * bz - az * by
local vny = az * bx - ax * bz
local vnz = ax * by - ay * bx
local nLen = sqrt(vnx * vnx + vny * vny + vnz * vnz)
local diffuse = 0.5
if nLen > 1e-9 then
vnx, vny, vnz = vnx / nLen, vny / nLen, vnz / nLen
diffuse = abs(vnx * lx + vny * ly + vnz * lz)
end
local shadeFactor = (s.shade == false) and 1.0 or (0.52 + 0.62 * diffuse)
local avgZ = (p00[3] + p10[3] + p11[3] + p01[3]) * 0.25
local fillColor
if useCmap then
local tNorm = (avgZ - zDataMin) / zDataSpan
fillColor = RendererUtils.sampleColormap(useCmap, tNorm, shadeFactor)
else
fillColor = RendererUtils.shadeColor(baseColor, shadeFactor)
end
local strokeCol = edgeColOverride or (showMesh and RendererUtils.shadeColor(fillColor, 0.76) or fillColor)
local avgDepth = (d00 + d10 + d11 + d01) * 0.25
primitives[#primitives + 1] = {
kind = "quad",
depth = avgDepth,
pts = format("%.2f,%.2f %.2f,%.2f %.2f,%.2f %.2f,%.2f", x00, y00, x10, y10, x11, y11, x01, y01),
fill = fillColor,
stroke = strokeCol,
strokeW = edgeLW,
alpha = sAlpha
}
end
end
end
end
elseif s.kind == "wireframe3d" then
local grid = s.grid or {}
local nu = s.nu or (#grid - 1)
local nv = s.nv or (#(grid[1] or {}) - 1)
local lw = tonumber(s.linewidth) or 1.1
for u = 1, nu + 1 do
local row = grid[u]
if row then
for v = 1, nv do
local p0, p1 = row[v], row[v + 1]
if p0 and p1 then
local x0, y0, d0 = cam.projectPoint(p0[1], p0[2], p0[3])
local x1, y1, d1 = cam.projectPoint(p1[1], p1[2], p1[3])
primitives[#primitives + 1] = {
kind = "segment",
depth = (d0 + d1) * 0.5,
x1 = x0, y1 = y0, x2 = x1, y2 = y1,
stroke = baseColor, strokeW = lw, alpha = sAlpha
}
end
end
end
end
for v = 1, nv + 1 do
for u = 1, nu do
local p0 = grid[u] and grid[u][v]
local p1 = grid[u + 1] and grid[u + 1][v]
if p0 and p1 then
local x0, y0, d0 = cam.projectPoint(p0[1], p0[2], p0[3])
local x1, y1, d1 = cam.projectPoint(p1[1], p1[2], p1[3])
primitives[#primitives + 1] = {
kind = "segment",
depth = (d0 + d1) * 0.5,
x1 = x0, y1 = y0, x2 = x1, y2 = y1,
stroke = baseColor, strokeW = lw, alpha = sAlpha
}
end
end
end
elseif s.kind == "curve3d" then
local pts = s.points or {}
local lw = tonumber(s.linewidth) or 2.2
local dash = RendererUtils.getDashArray(s.linestyle)
local prevP, prevX, prevY, prevD = nil, nil, nil, nil
for k = 1, #pts do
local pt = pts[k]
if pt then
local px, py, d = cam.projectPoint(pt[1], pt[2], pt[3])
if prevP and s.linestyle ~= "none" and lw > 0 then
primitives[#primitives + 1] = {
kind = "segment",
depth = (prevD + d) * 0.5,
x1 = prevX, y1 = prevY,
x2 = px, y2 = py,
stroke = baseColor,
strokeW = lw,
dash = dash,
alpha = sAlpha
}
end
if s.marker and s.marker ~= "none" then
primitives[#primitives + 1] = {
kind = "point",
depth = d + 1e-4,
cx = px, cy = py,
r = tonumber(s.markersize) or max(2.8, lw * 1.2),
shape = s.marker,
fill = baseColor,
stroke = s.edgecolor or baseColor,
strokeW = tonumber(s.edgewidth) or 0.6,
alpha = sAlpha
}
end
prevP, prevX, prevY, prevD = pt, px, py, d
else
prevP = nil
end
end
elseif s.kind == "scatter3d" then
local pts = s.points or {}
local cArr = s.c_array
local sArr = s.s_array
local baseR = tonumber(s.markersize) or 3.8
local shape = s.marker or "circle"
local cMin, cMax = math.huge, -math.huge
if cArr and #cArr > 0 then
for _, cv in ipairs(cArr) do
if type(cv) == "number" and cv == cv then
if cv < cMin then cMin = cv end
if cv > cMax then cMax = cv end
end
end
end
local cSpan = (cMax > cMin) and (cMax - cMin) or 1
for k = 1, #pts do
local pt = pts[k]
if pt then
local px, py, d = cam.projectPoint(pt[1], pt[2], pt[3])
local ptColor = baseColor
if cArr and cArr[k] ~= nil then
if type(cArr[k]) == "number" then
local tNorm = (cArr[k] - cMin) / cSpan
ptColor = RendererUtils.sampleColormap(cmapName or "viridis", tNorm, 1.0)
else
ptColor = RendererUtils.parseColor(cArr[k], baseColor)
end
elseif cmapName then
local tNorm = (pt[3] - zDataMin) / zDataSpan
ptColor = RendererUtils.sampleColormap(cmapName, tNorm, 1.0)
end
local ptR = baseR
if sArr and tonumber(sArr[k]) then
ptR = max(1.5, sqrt(tonumber(sArr[k])) * 0.6)
end
primitives[#primitives + 1] = {
kind = "point",
depth = d,
cx = px, cy = py,
r = ptR,
shape = shape,
fill = ptColor,
stroke = s.edgecolor and RendererUtils.parseColor(s.edgecolor) or RendererUtils.shadeColor(ptColor, 0.72),
strokeW = tonumber(s.linewidth) or 0.7,
alpha = sAlpha
}
end
end
end
end
-- 畫家演算法:由遠到近 (depth 由小到大) 排序
sort(primitives, function(a, b)
return a.depth < b.depth
end)
return primitives
end
-- ==========================================
-- 4. 複變函數定義域著色 (Domain Coloring) 像素生成引擎
-- ==========================================
local rad_to_deg = 180 / math.pi
-- 將單一複數值 (re, im) 透過相位與模長轉為 #rrggbb 色碼
function p.complexToColor(re, im)
if not re or not im or re ~= re or im ~= im or abs(re) >= 1e12 or abs(im) >= 1e12 then
return "#7f7f7f"
end
local abs_v = sqrt(re * re + im * im)
local arg_d = atan2(im, re) * rad_to_deg
local h = arg_d % 360
if h < 0 then h = h + 360 end
local log_a = log(abs_v + 1)
local s = max(0, min(1, 1 / (log_a * 0.3 + 1)))
local b = max(0, min(1, 1 - 1 / (log_a * 5 + 1.1)))
return RendererUtils.hsbToHex(h, s, b)
end
-- 根據 Python 傳入的複數函數 fn(z) 或 2D 複數陣列,產生定義域著色像素矩陣與 tt_grid 數據
function p.buildComplexDomainPixels(fn_or_grid, x_start, x_end, y_start, y_end, resolution)
local res = max(16, min(120, tonumber(resolution) or 60))
local xMin = tonumber(x_start) or -5
local xMax = tonumber(x_end) or 5
local yMin = tonumber(y_start) or -5
local yMax = tonumber(y_end) or 5
local pixels = {}
local tt_grid = {}
-- 情況 A:傳入的是 2D 複數或實數陣列
if type(fn_or_grid) == "table" and fn_or_grid.elements then
local rows = fn_or_grid.elements
local nR = #rows
for r = 1, nR do
local rowElts = (type(rows[r]) == "table" and rows[r].elements) or rows[r] or {}
local nC = #rowElts
local pRow = {}
local gy = yMin + ((r - 1) / max(1, nR - 1)) * (yMax - yMin)
for c = 1, nC do
local gx = xMin + ((c - 1) / max(1, nC - 1)) * (xMax - xMin)
local cell = rowElts[c]
local re, im = 0, 0
if type(cell) == "table" and cell.__type == "complex" then
re, im = tonumber(cell.real) or 0, tonumber(cell.imag) or 0
else
re = unwrap_number(cell)
end
if re and im and re == re and im == im and abs(re) < 1e12 and abs(im) < 1e12 then
local hex_c = p.complexToColor(re, im)
pRow[c] = hex_c
tt_grid[#tt_grid + 1] = {
col = c, row = nR - r + 1,
x = gx, y = gy,
re = re, im = im,
color = hex_c
}
else
pRow[c] = "#7f7f7f"
end
end
-- Y 軸由上(yMax)往下(yMin)排列
pixels[nR - r + 1] = pRow
end
return {
pixels = pixels,
tt_grid = tt_grid,
x_start = xMin, x_end = xMax,
y_start = yMin, y_end = yMax
}
end
-- 情況 B:傳入的是可呼叫的 Python 函數 f(z)
local Evaluator = require("Module:沙盒/a2569875/PythonScript/Evaluator")
local Cmath = require("Module:沙盒/a2569875/PythonScript/Runtime/Cmath").Cmath
local dx = (xMax - xMin) / max(1, res - 1)
local dy = (yMax - yMin) / max(1, res - 1)
for r = 1, res do
local gy = yMax - (r - 1) * dy
local pRow = {}
for c = 1, res do
local gx = xMin + (c - 1) * dx
local z_in = Cmath.make_complex(gx, gy)
local ok, w_out = pcall(Evaluator.invoke, fn_or_grid, { z_in }, {})
local re, im = nil, nil
if ok and w_out ~= nil then
if type(w_out) == "table" and w_out.__type == "complex" then
re, im = tonumber(w_out.real), tonumber(w_out.imag) or 0
elseif type(w_out) == "number" then
re, im = w_out, 0
end
end
if re and im and re == re and im == im and abs(re) < 1e12 and abs(im) < 1e12 then
local hex_c = p.complexToColor(re, im)
pRow[c] = hex_c
tt_grid[#tt_grid + 1] = {
col = c, row = r,
x = gx, y = gy,
re = re, im = im,
color = hex_c
}
else
pRow[c] = "#7f7f7f"
end
end
pixels[r] = pRow
end
return {
pixels = pixels,
tt_grid = tt_grid,
x_start = xMin, x_end = xMax,
y_start = yMin, y_end = yMax
}
end
return p