-- luadraw_central_perspective.lua -- date 2026/08/04 -- version 3.4 -- Copyright 2026 Patrick Fradin -- This work may be distributed and/or modified under the -- conditions of the LaTeX Project Public License. -- The latest version of this license is in -- https://www.ctan.org/license/lppl --functions to be redefined local graph3d = luadraw.graph3d local ld = luadraw local cpx = ld.cpx local Z = cpx.Z local pt3d = ld.pt3d local toPoint3d = pt3d.toPoint3d local isPoint3d = pt3d.isPoint3d local Origin, vecI, vecJ, vecK = pt3d.Origin, pt3d.vecI, pt3d.vecJ, pt3d.vecK local M, Mc, Ms = pt3d.M, pt3d.Mc, pt3d.Ms local map = ld.map local notDef = ld.notDef --local old_Dcone = graph3d.Dcone --local old_Dcylinder = graph3d.Dcylinder --local old_Dfrustum = graph3d.Dfrustum --local old_Dsphere = graph3d.Dsphere local old_Proj3d = graph3d.Proj3d local old_Proj3dV = graph3d.Proj3dV local old_Cosine_incidence = graph3d.Cosine_incidence local old_Isvisible = graph3d.Isvisible local old_Screenpos = graph3d.Screenpos local old_Observer_distance = graph3d.Observer_distance local camera = nil local target = Origin --This function is automatically called by the perspective function. function luadraw.central_perspective(theta,phi,d,look) -- or central_perspective(camera, look) local N local cos, sin, tet, ph = math.cos, math.sin if isPoint3d(theta) then camera = theta target = phi or Origin if type(target) == "number" then target = Origin end N = pt3d.normalize(camera-target) ph = pt3d.angle(vecK, N) phi = ph*ld.rad -- phi en degrés tet = pt3d.angle(vecI, ld.pxy(N)) theta = tet*ld.rad -- theta en degrés d = pt3d.abs(camera-target) else camera = nil theta = theta or 30 phi = phi or 60 d = d or 15 target = look or Origin d = math.abs(d) tet, ph = theta*ld.deg, phi*ld.deg end local cosTheta, sinTheta, cosPhi, sinPhi = cos(tet), sin(tet), cos(ph), sin(ph) if N == nil then N = M(cosTheta*sinPhi,sinTheta*sinPhi,cosPhi) end local mat = {Origin, M(-sinTheta,-cosPhi*cosTheta,cosTheta*sinPhi), M(cosTheta,-cosPhi*sinTheta,sinTheta*sinPhi), M(0,sinPhi,cosPhi) } -- mat.A(x,y,z) gives (a,b,c), and Z(a,b) is the affix of the projection of A on screen local invmat = ld.invmatrix3d(mat) -- if Z(a,b) is the affix of the projection on screen, invmat.(a,b,0) gives A(x,y,z) on the screen so that the projection of A has the affix Z(a,b) if camera == nil then camera = target+d*N end -- camera local Zlookat = Z( cosTheta*target.y-sinTheta*target.x, -cosPhi*cosTheta*target.x-cosPhi*sinTheta*target.y+sinPhi*target.z) function graph3d:Isvisible(facet) -- facet est une liste de points 3d coplanaires -- la fonction renvoie true si la facette est visible (vecteur normal de même sens que n) local N = pt3d.prod(facet[2]-facet[1], facet[3]-facet[1]) return self:Cosine_incidence(N, pt3d.isobar3d(facet)) > 0 end function graph3d:Proj3d(L) -- we redefine Proj3d local f = function(A) if isPoint3d(A) then local den = pt3d.dot(camera-A,N) if math.abs(den) > 1e-17 then local k = d/den local A1 = camera-k*(camera-A) return Z( cosTheta*A1.y-sinTheta*A1.x, -cosPhi*cosTheta*A1.x-cosPhi*sinTheta*A1.y+sinPhi*A1.z)-Zlookat end else return A end end L = self:Mtransform3d(L) -- we apply the 3D matrix of the graph if self.scalexyz ~= 1 then L = ld.ftransform3d(L, self.scalexyz) -- we apply the scales on x, y and z axis end return ld.ftransform3d(L,f) -- we return the projection on screen end function graph3d:Proj3dV(L,A) -- we redefine Proj3dV, the origin for the vectors is the point A (target by defaut) A = A or target local B = self:Proj3d(A) local f = function(v) if isPoint3d(v) then return self:Proj3d(A+v)-B else return v end end return ld.ftransform3d(L,f) -- we return the projection on screen end function graph3d:Screenpos(z,d) -- renvoie les coordonnées spatiales d'un point ayant comme projeté sur l'écran le point d'affixe z, -- et se trouvant à une distance d (algébrique) du plan de l'écran z = cpx.toComplex(z) return ld.applymatrix3d(M(z.re+Zlookat.re, z.im+Zlookat.im, pt3d.dot(target,N)), invmat) end function graph3d:Cosine_incidence(n,A) -- cosinus de l'angle d'incidence entre le vecteur n au point A et le vecteur dirigé vers l'observateur return pt3d.dot(pt3d.normalize(camera-A),n) end function graph3d:Observer_distance(A) -- l'abscisse de A sur l'axe issue de Origine, dirigé vers l'observateur return -pt3d.abs(camera-A) end function graph3d:arc3db(B,A,C,r,sens,n) local n1, n2, V = pt3d.normalize(B-A), pt3d.normalize(C-A) if n == nil then V = pt3d.normalize(pt3d.prod(n1,n2)) else V = pt3d.normalize(n) end if pt3d.abs(V)<1e-12 then V = pt3d.normalize(n) end B = A+r*n1; C = A+r*n2 local N = pt3d.normalize(ld.camera-A) local U = pt3d.prod(V,self.Normal) if (pt3d.abs(U)<1e-12) then --plans parallèles local A1 = ld.interDP( {A,A-camera},{target,self.Normal}) local B1 = ld.interDP( {B,B-camera},{target,self.Normal}) local r1 = pt3d.abs(B1-A1) local alpha = pt3d.abs(A1-camera)/pt3d.abs(A-camera) --self:Darc(self:Proj3d(B), self:Proj3d(A), self:Proj3d(C),r*alpha,sens,draw_options) return ld.arc3db(B,A,C,r1/alpha,sens,n) elseif math.abs( pt3d.dot(V,N) ) < 1e-12 then --plans perpendiculaires --print("ok") return ld.polyline2path3d( ld.arc3d(B,A,C,r,sens,n) ) else local pred = function(z) if type(z) == "string" then return z else local P = self:Screenpos(z) return ld.proj3dO(P, {A,V}, camera-P) end end local N2 = pt3d.normalize(pt3d.prod(U,V)) local N1 = pt3d.normalize(U) local a1,a2,a3,a4,b,c,O,u,v local mat3d = self.matrix3d self:IDmatrix3d() a1,a2,a3,a4,b,c = table.unpack( self:Proj3d({A-r*N2, A+r*N2, A-r*N1, A+r*N1, B, C}) ) O = (a1+a2)/2; u = cpx.normalize(a4-a3); v = a1-O local mat = {O,cpx.abs(v)*u,v} if math.abs(cpx.det(mat[2],mat[3])) < 1e-8 then return ld.polyline2path3d( ld.arc3d(B,A,C,r,sens,n) ) end local invm = ld.invmatrix(mat) a4,b,c = table.unpack( ld.mtransform({a4,b,c}, ld.invmatrix(mat)) ) local y = a4.im local x = a4.re local alpha = x/math.sqrt(1-y^2) local L = ld.mtransform( ld.ellipticarcb(b,0,c,alpha,1,sens), mat) self.matrix3d = mat3d return ld.ftransform(L, pred) end end function graph3d:circle3db(O,r,n) local U = pt3d.prod(n,self.Normal) if pt3d.abs(U) < 1e-12 then -- U est nul U = pt3d.prod(n,vecJ) if pt3d.abs(U) < 1e-12 then -- U est nul U = pt3d.prod(n,vecI) end end U = O+r*pt3d.normalize(U) return self:arc3db(U,O,U,r,1,n) end ld.camera = camera ld.target = target return {theta,phi,"central"} end --This function is automatically called at the next perspective change. function luadraw.close_central() --graph3d.Dcone = old_Dcone --graph3d.Dcylinder = old_Dcylinder --graph3d.Dfrustum = old_Dfrustum --graph3d.Dsphere = old_Dsphere graph3d.Proj3d = old_Proj3d graph3d.Proj3dV = old_Proj3dV graph3d.Cosine_incidence = old_Cosine_incidence graph3d.Isvisible = old_Isvisible graph3d.ScreenPos = old_Screenpos graph3d.Observer_distance = old_Observer_distance end