跳转到内容

模組:沙盒/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