跳转到内容

模組: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