-- luadraw_graph3d.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 -- ce module ajoute les bases du dessin 3d local ld = luadraw require 'luadraw_matrix3d' -- charge également la classe pt3d dans ld.pt3d local cpx = ld.cpx local Z = cpx.Z local pt3d = ld.pt3d local toPoint3d = pt3d.toPoint3d local isPoint3d = pt3d.isPoint3d local isID3d = ld.isID3d local ID3d = ld.ID3d local mtransform3d = ld.mtransform3d local mLtransform3d = ld.mLtransform3d local composematrix3d = ld.composematrix3d local invmatrix3d = ld.invmatrix3d local matrix3dof = ld.matrix3dof 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 require 'luadraw_transformations3d' require 'luadraw_lines3d' require 'luadraw_build3d' local Tscene3d = require 'luadraw_scene3d' local Scene3d ld.projection_mode = "ortho" local projection_msg = { ["ortho"] = "orthographic projection", ["yz"] = "cavalier perspective on yz-plane", ["xz"] = "cavalier perspective on xz-plane", ["xy"] = "cavalier perspective on xy-plane", ["iso"] = "isometric perspective", ["central"] = "central projection" } ld.Hiddenlines = false ld.Hiddenlinestyle = "dotted" ld.Hiddenlinescale = 2/3 ld.top, ld.right, ld.bottom, ld.left, ld.all = 8, 4, 2, 1, 15 ld.mWireframe, ld.mFlat, ld.mFlatHidden, ld.mShaded, ld.mShadedHidden, ld.mShadedOnly = 0, 1, 2, 3, 4, 5 ld.mGrid = 1 -- cylinder, sphere, cone ld.mBorder = 2 -- sphere --MODE_WIREFRAME = 0 --MODE_FLAT = 1 --MODE_FLAT_HIDDEN_EDGES = 2 --MODE_SHADED = 3 --MODE_SHADED_HIDDEN_EDGES = 4 --MODE_SHADED_NO_EDGE = 5 local default_values = function() ld.Hiddenlines = false ld.Hiddenlinestyle = "dotted" ld.Hiddenlinescale = 2/3 end local luadraw_graph2d = ld.graph --require "luadraw_graph2d" local luadraw_graph3d = {} setmetatable(luadraw_graph3d, {__index = luadraw_graph2d}) -- obligatoire pour l'héritage local perspective = function(mode,k,alpha,d,look) -- change the type of projection -- mode = "iso" (isometric projection) or -- mode = "yz" or "xz" or "xy" (cavalier perspective) -- parameters for cavalier perspective : k is a ratio and alpha is an angle in degrees mode = mode or "ortho" -- default value local r, k_cos_alpha, k_sin_alpha, theta, phi if (mode == "central") or (mode == "ortho") then theta = k or 30 phi = alpha or 60 d = d or 15 else k = k or 0.5 alpha = alpha or 45 k = math.abs(k) alpha = alpha*ld.deg -- conversion degrees -> radians (deg = pi/180) r = 1/math.sqrt(1+k^2) k_cos_alpha = k*math.cos(alpha) -- to calculate them only once k_sin_alpha = k*math.sin(alpha) end local f = nil -- function if mode == "yz" then -- In this perspective, vecJ becomes 1 and vecK becomes i phi = math.acos(k_sin_alpha*r)*ld.rad -- angles in degrees for viewdir (rad = 180/pi) theta = cpx.arg(Z(1,k_cos_alpha))*ld.rad f = function(A) if isPoint3d(A) then -- We only transform the 3D points; the rest remains unchanged. return Z(A.y-k_cos_alpha*A.x, A.z-k_sin_alpha*A.x) else return A end end elseif mode == "xz" then -- In this perspective, vecI becomes 1 and vecK becomes i phi = math.acos(k_sin_alpha*r)*ld.rad -- angles in degrees for viewdir (rad = 180/pi) theta = cpx.arg(Z(k_cos_alpha,-1))*ld.rad f = function(A) if isPoint3d(A) then -- We only transform the 3D points; the rest remains unchanged. return Z(A.x+k_cos_alpha*A.y, A.z+k_sin_alpha*A.y) else return A end end elseif mode == "xy" then -- In this perspective, vecI becomes 1 and vecJ becomes i phi = math.acos(r)*ld.rad -- angles in degrees for viewdir (rad = 180/pi) theta = alpha*ld.rad f = function(A) if isPoint3d(A) then -- We only transform the 3D points; the rest remains unchanged. return Z(A.x-k_cos_alpha*A.z, A.y-k_sin_alpha*A.z) else return A end end elseif mode == "iso" then -- isometric perspective, one unit on each axe has the same length on screen phi = math.acos(1/math.sqrt(3))*ld.rad -- angles in degrees for viewdir theta = 45 local a, b = math.sqrt(2)/2, 1/math.sqrt(6) f = function(A) if isPoint3d(A) then -- We only transform the 3D points; the rest remains unchanged. return Z( a*(A.y-A.x), 2*b*A.z-b*(A.x+A.y) ) else return A end end elseif mode == "central" then if luadraw.central_perspective == nil then print("You need : require 'luadraw_central_perspective' before using perspective('central()'") function luadraw_graph3d:Proj3d(L) return self:orthographic_Proj3d(L) -- default projection end return {theta,phi,"ortho"} else return luadraw.central_perspective(theta,phi,d,look) end else mode = "ortho" function luadraw_graph3d:Proj3d(L) return self:orthographic_Proj3d(L) -- default projection end end if f ~= nil then function luadraw_graph3d:Proj3d(L) -- we redefine Proj3d 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 end return {theta,phi,mode} end local calc_viewdir = function(viewdir) if viewdir == "xOy" then return {-90,0,"ortho"} elseif viewdir == "yOz" then return {0,90,"ortho"} elseif viewdir == "xOz" then return {-90,90,"ortho"} elseif type(viewdir) == "string" then return perspective(viewdir) elseif type(viewdir) == "table" then if type(viewdir[1]) == "number" then table.insert(viewdir,1,"ortho") end return perspective(table.unpack(viewdir)) end end --- Constructeur function luadraw_graph3d:new(args) -- argument de la forme : -- {window3d={x1,x2,y1,y2,z1,z2}, viewdir={30,60}, adjust2d=true/false, window={x1,x2,y1,y2,xscale,yscale}, margin={left, right, top, bottom}, size={large, haut, ratio}, bg="color", border = true/false, bbox=true/false} local graph3d = luadraw_graph2d:new(args) -- obligatoire, on utilise le constructeur de luadraw_calc default_values() args.adjust2d = args.adjust2d or false -- adjust2d= false or true args.viewdir = args.viewdir or {"ortho", 30, 60} setmetatable(graph3d, {__index = luadraw_graph3d}) -- obligatoire, permet d'utiliser self graph3d.param["viewport3d"] = args.window3d or {-5,5,-5,5,-5,5,1,1,1} local x1,x2,y1,y2,z1,z2,xsc,ysc,zsc = table.unpack(graph3d.param.viewport3d) xsc = xsc or 1; ysc = ysc or 1; zsc = zsc or 1 graph3d.param["viewport3d"] = {x1,x2,y1,y2,z1,z2,xsc,ysc,zsc} if (xsc ~= 1) or (ysc ~= 1) or (zsc ~= 1) then graph3d.scalexyz = function(A) if pt3d.isPoint3d(A) then return M(xsc*A.x, ysc*A.y, zsc*A.z) else return A end end else graph3d.scalexyz = 1 end graph3d.matrix3d = {M(0,0,0), M(1,0,0), M(0,1,0), M(0,0,1)} local a, b, mode = table.unpack( calc_viewdir(args.viewdir) ) graph3d.param["viewdir"] = {a, b}-- viewdir theta et phi en degrés a = a*ld.deg; b = b*ld.deg; -- conversion en radians if mode == nil then mode = "ortho" end if (ld.projection_mode == "central") and (mode ~= "central") then ld.close_central() end if mode == "ortho" then function luadraw_graph3d:Proj3d(L) return self:orthographic_Proj3d(L) end end ld.projection_mode = mode graph3d.cosTheta = math.cos(a) -- pour accélérer les calculs, les cos et sin ne sont calculés qu'une fois graph3d.sinTheta = math.sin(a) graph3d.cosPhi = math.cos(b) graph3d.sinPhi = math.sin(b) graph3d.Normal = M(graph3d.cosTheta*graph3d.sinPhi, graph3d.sinTheta*graph3d.sinPhi, graph3d.cosPhi) if args.adjust2d then local x1,x2,y1,y2,z1,z2 = table.unpack(graph3d.param.viewport3d) local box = ld.parallelep( M(x1,y1,z1), (x2-x1)*vecI, (y2-y1)*vecJ, (z2-z1)*vecK ) -- boite 3d --local L = {} --for _, A in ipairs(box.vertices) do -- projection des sommets de la boite sur le plan 2d -- table.insert(L, Z(graph3d.cosTheta*A.y-graph3d.sinTheta*A.x, -graph3d.cosPhi*graph3d.cosTheta*A.x-graph3d.cosPhi*graph3d.sinTheta*A.y+graph3d.sinPhi*A.z)) --end x1, x2, y1, y2 = ld.getbounds( graph3d:Proj3d(box.vertices) ) x1 = x1-(x2-x1)/5; x2 = x2+(x2-x1)/20 y1 = y1-(y2-y1)/5; y2 = y2+(y2-y1)/20 --x1 = math.floor(x1); x2 = math.ceil(x2) --y1 = math.floor(y1); y2 = math.ceil(y2) graph3d.param.viewport = {x1,x2,y1,y2} -- redimensionnement de la vue 2d graph3d.param.coordsystem = {x1,x2,y1,y2} -- redimensionnement de la vue utilisateur graph3d.advancedparam = table.copy(graph3d.currentparam) graph3d.deferredparam = table.copy(graph3d.currentparam) local lg, ht = (graph3d.Xmax-graph3d.Xmin)*graph3d.Xscale, (graph3d.Ymax-graph3d.Ymin)*graph3d.Yscale graph3d.Xmin = x1; graph3d.Xmax = x2; graph3d.Ymin = y1; graph3d.Ymax = y2 local ratio if args.size ~= nil then ratio = args.size[3] end local Ratio = ratio or (graph3d.Xscale / graph3d.Yscale) if lg > 0 then graph3d.Xscale = lg / (graph3d.Xmax-graph3d.Xmin) end if ht > 0 then graph3d.Yscale = ht / (graph3d.Ymax-graph3d.Ymin) end if Ratio > 0 then local aux = Ratio*graph3d.Yscale if aux > graph3d.Xscale then graph3d.Yscale = graph3d.Xscale/Ratio else graph3d.Xscale = aux end end end -- infos sur le graphe print("\n3d window = ", table.unpack(graph3d.param.viewport3d)) print("projection mode = ", projection_msg[ld.projection_mode]) print("viewdir = ", table.unpack(graph3d.param.viewdir)) print("2d window = ", table.unpack(graph3d.param.viewport)) return graph3d end -- calcul matriciel function luadraw_graph3d:Savematrix() table.insert(self.pilematrix, table.copy(self.matrix)) table.insert(self.pilematrix, table.copy(self.matrix3d)) end function luadraw_graph3d:Restorematrix() self.matrix3d = table.remove(self.pilematrix) self.matrix = table.remove(self.pilematrix) end function luadraw_graph3d:Det3d() -- renvoie +1 ou -1 suivant que le déterminant de la matrice de transformation 3d est positif ou négatif local o,u,v,w = table.unpack(self.matrix3d) if pt3d.det(u,v,w) > 0 then return 1 else return -1 end end -- sauvegarde et restauration des paramètres graphiques (fenêtre, styles, matrices) function luadraw_graph3d:Saveattr() table.insert(self.pile, table.copy(self.matrix3d)) table.insert(self.pile, table.copy(self.matrix)) table.insert(self.pile, table.copy(self.param)) self:Writeln("\\begin{scope}") end function luadraw_graph3d:Restoreattr() self.param = table.remove(self.pile) self.matrix = table.remove(self.pile) self.matrix3d = table.remove(self.pile) self:Writeln("\\end{scope}") end function luadraw_graph3d:IDmatrix3d() self.matrix3d = ID3d end function luadraw_graph3d:Setmatrix3d(M) self.matrix3d = M end function luadraw_graph3d:Composematrix3d(M) self.matrix3d = composematrix3d(self.matrix3d,M) end function luadraw_graph3d:Mtransform3d(L) return mtransform3d(L,self.matrix3d) end function luadraw_graph3d:MLtransform3d(L) return mLtransform3d(L,self.matrix3d) end function luadraw_graph3d:Shift3d(V) self:Composematrix3d({V,vecI,vecJ,vecK}) end function luadraw_graph3d:Rotate3d(angle,axe) -- angle en degrés -- axe = {A,u} axe de la rotation orienté par u self:Composematrix3d(matrix3dof(function(M) return ld.rotate3d(M,angle,axe) end)) end function luadraw_graph3d:Scale3d(k,center) -- homothétie de center center (origine par défaut) -- et de rapport k self:Composematrix3d(matrix3dof(function(M) return ld.scale3d(M,k,center) end)) end -- vue function luadraw_graph3d:Viewport3d(x1,x2,y1,y2,z1,z2) if (x1 == nil) or (x2 == nil) or (y1 == nil) or (y2 == nil) or (z1 == nil) or (z2 == nil) then return end if x1 > x2 then x1, x2 = x2, x1 end if y1 > y2 then y1, y2 = y2, y1 end if z1 > z2 then z1, z2 = z2, z1 end self.matrix3d = ID3d -- ancienne matrice perdue donc il faut sauver avant self.param.viewport3d = {x1,x2,y1,y2,z1,z2} end function luadraw_graph3d:Setviewdir(theta,phi) -- direction de l'observateur avec theta et phi en degrés if (theta == nil) then return end local viewdir, mode if type(theta) == "number" then viewdir = {"ortho", theta, phi} else viewdir = theta end theta, phi, mode = table.unpack( calc_viewdir(viewdir) ) if (ld.projection_mode == "central") and (mode ~= "central") then ld.close_central() end if mode == "ortho" then function luadraw_graph3d:Proj3d(L) return self:orthographic_Proj3d(L) end end ld.projection_mode = mode self.param.viewdir = {theta,phi} local a, b = theta*ld.deg, phi*ld.deg self.cosTheta = math.cos(a) -- pour accélérer les calculs, les cos et sin ne sont calculés qu'une fois self.sinTheta = math.sin(a) self.cosPhi = math.cos(b) self.sinPhi = math.sin(b) self.Normal = M(self.cosTheta*self.sinPhi, self.sinTheta*self.sinPhi, self.cosPhi) end function luadraw_graph3d:Getviewdir() -- renvoie la direction de l'observateur avec angles theta et phi en degrés return self.param.viewdir end function luadraw_graph3d:ScreenX() -- renvoie les coordonnées spatiales du premier vecteur de base du plan de l'écran (affixe 1) if ld.projection_mode == "ortho" then -- c'est l'image du vecteur vecJ par la rotation d'axe Oz et d'angle theta return M(-self.sinTheta, self.cosTheta,0) elseif ld.projection_mode == "yz" then return vecJ elseif ld.projection_mode == "xz" then return vecI elseif ld.projection_mode == "xy" then return vecI elseif ld.projection_mode == "iso" then return M(-1/math.sqrt(2),1/math.sqrt(2),0) elseif ld.projection_mode == "central" then return self:Screenpos(Z(1,0)) end end function luadraw_graph3d:ScreenY() -- renvoie les coordonnées spatiales du deuxième vecteur de base du plan de l'écran (affixe i) if ld.projection_mode == "ortho" then -- c'est le produit vectoriel entre les vecteurs self.Normal et self:ScreenX() return M(-self.cosPhi*self.cosTheta, -self.cosPhi*self.sinTheta, self.sinPhi) elseif ld.projection_mode == "yz" then return vecK elseif ld.projection_mode == "xz" then return vecK elseif ld.projection_mode == "xy" then return vecJ elseif ld.projection_mode == "iso" then return M(0,0,math.sqrt(6)/2) elseif ld.projection_mode == "central" then return self:Screenpos(Z(0,1)) end end function luadraw_graph3d:Screenpos(z,d) -- renvoie les coordonnées spatiales du vecteur 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 = toComplex(z) local A = z.re*self:ScreenX() + z.im*self:ScreenY() d = d or 500 return A + d*self.Normal end function luadraw_graph3d:Box3d() -- renvoie la fenêtre 3d courante sous forme d'un polyèdre local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d) return ld.parallelep(M(x1,y1,z1), (x2-x1)*vecI, (y2-y1)*vecJ, (z2-z1)*vecK) end -- projection sur l'écran function luadraw_graph3d:orthographic_Proj3d(L) -- projection de points sur l'écran (plan passant par l'origine et normal au vecteur self.Normal -- L est un point3d ou une liste de point3d ou une liste de listes de point3d local projAdot = function(A) -- le projeté d'un vecteur u sur le plan de l'écran est .ScreenX + .ScreenY -- c'est donc le point d'affixe + i* sur l'écran -- le projeté d'un point A est le projeté du vecteur OA if isPoint3d(A) then -- on ne projette que les points 3d return Z(self.cosTheta*A.y-self.sinTheta*A.x, -self.cosPhi*self.cosTheta*A.x-self.cosPhi*self.sinTheta*A.y+self.sinPhi*A.z) else return A --le reste est renvoyé tel quel end end if (L == nil) or (type(L) ~= "table") then return end if not isID3d(self.matrix3d) then L = mtransform3d(L,self.matrix3d) end if self.scalexyz ~= 1 then L = ld.ftransform3d(L, self.scalexyz) -- we apply the scales on x, y and z axis end local rep if isPoint3d(L) then rep = projAdot(L) -- un seul point elseif isPoint3d(L[1]) then -- liste de points rep = {} for _, A in ipairs(L) do table.insert(rep, projAdot(A)) end else --liste de listes rep = {} for _, cp in ipairs(L) do local aux = {} for _,A in ipairs(cp) do table.insert(aux, projAdot(A)) end table.insert(rep, aux) end end return rep end function luadraw_graph3d:Proj3d(L) return self:orthographic_Proj3d(L) end function luadraw_graph3d:Proj3dV(L) -- projection de vecteurs sur l'écran (plan passant par l'origine et normal au vecteur self.Normal -- L est un point3d ou une liste de point3d ou une liste de listes de point3d if (L == nil) or (type(L) ~= "table") then return end local oldmatrix3d = self.matrix3d if not isID3d(self.matrix3d) then L = mLtransform3d(L,self.matrix3d) --<- c'est ici que ce fait la différence avec les points end self.matrix3d = ID3d local rep = self:Proj3d(L) self.matrix3d = oldmatrix3d return rep end ------ dessins de lignes 3d function luadraw_graph3d:Dpolyline3d(L,close,draw_options,clip) if type(close) ~= "boolean" then clip = draw_options; draw_options = close; close = false end clip = clip or false if clip then L = ld.clippolyline3d(L,self:Box3d()) end local aux = self:Proj3d(L) self:Dpolyline(aux,close,draw_options) end function luadraw_graph3d:Dline3d(d,B,draw_options,clip) --trace la droite d (si B=nil) ou bien la droite passant par les points d et B if not isPoint3d(B) then clip = draw_options; draw_options = B; B = nil end clip = clip or false local A, u if B == nil then A = d[1]; B = A+d[2] else A = d end if clip then self:Dpolyline3d( ld.clipline3d({A,B-A},self:Box3d()),false,draw_options) else self:Dline(self:Proj3d(A),self:Proj3d(B),draw_options) end end function luadraw_graph3d:Dseg3d(seg,scale,draw_options,clip) --trace le segment seg clip = clip or false if clip then seg = ld.clippolyline3d(seg,self:Box3d()) if seg ~= nil then seg = seg[1] end end self:Dseg(self:Proj3d(seg),scale,draw_options) end function luadraw_graph3d:Dparametric3d(p,args) -- dessin d'une courbe paramétrée par la fonction p:t -> p(t) sur l'intervalle [t1;t2] (à valeurs dans R^3) -- args est une table à 5 entrées args = { t = {t1,t2}, nbdots = 50,clip=false discont =false, nbdiv = 5, draw_options = "" } args = args or {} local t = args.t or {self:Xinf(), self:Xsup()} local nbdots = args.nbdots or 50 local discont = args.discont or false local clip = args.clip or false local nbdiv = args.nbdiv or 5 local draw_options = args.draw_options or "" if draw_options == "" then draw_options = "line join=round" else draw_options = "line join=round,"..draw_options end -- jointure arrondie local t1, t2 = table.unpack(t) if t1 > t2 then t1, t2 = t2, t1 end local C = ld.parametric3d(p,t1,t2,nbdots,discont,nbdiv) self:Dpolyline3d(C,false,draw_options,clip) end function luadraw_graph3d:Darc3d(B,A,C,R,sens,normal,draw_options,clip) -- dessine un arc de cercle de centre A, dans le plan ABC, de AB vers AC. -- ce plan est orienté par le vecteur AB^AC ou le vecteur normal s'il est précisé if type(normal) == "string" then clip = draw_options; draw_options = normal; normal = nil end clip = clip or false if clip then local chem = ld.arc3d(B,A,C,R,sens,normal) self:Dpolyline3d(chem,false,draw_options,clip) else local chem if ld.projection_mode ~= "central" then chem = ld.arc3db(B,A,C,R,sens,normal) else chem = self:arc3db(B,A,C,R,sens,normal) end --self:Dpath(self:Proj3d(chem),draw_options) self:Dpath3d(chem,draw_options) end end function luadraw_graph3d:Dcircle3d(C,R,normal,draw_options,clip) -- dessine un cercle de centre C de rayon R. -- dans le plan défini par C et le vecteur normal if (type(C) == "table") and (not isPoint3d(C)) then -- C est une table if type(R) ~= "number" then clip = draw_options; draw_options = R end C,R,normal = table.unpack(C) end clip = clip or false if R == 0 then self:Ddots3d(C) else if clip then local chem = ld.circle3d(C,R,normal) self:Dpolyline3d(chem,false,draw_options,clip) else local chem if ld.projection_mode ~= "central" then chem = ld.circle3db(C,R,normal) else chem = self:circle3db(C,R,normal) end --chem = ld.circle3db(C,R,normal) --self:Dpath(self:Proj3d(chem),draw_options) self:Dpath3d(chem,draw_options) end end end function luadraw_graph3d:Dangle3d(B,A,C,r,draw_options,clip) if type(r) ~= "number" then clip = draw_options; draw_options = r; r = nil end r = r or 0.25 clip = clip or false local u, v = B-A, C-A u, v = r*pt3d.normalize(u), r*pt3d.normalize(v) return self:Dpolyline3d( {A+u,A+u+v,A+v},false,draw_options,clip) end -------- points et labels function luadraw_graph3d:Clipdots(L) local x1,x2,y1,y2,z1,z2 = table.unpack( self.param.viewport3d ) local rep = {} local isin = function(A) local x,y,z = A.x, A.y, A.z if (x1<=x) and (x<=x2) and (y1<=y) and (y<=y2) and (z1<=z) and (z<=z2) then return A end end return ld.ftransform3d(L,isin) end function luadraw_graph3d:Ddots3d(L,mark_options,clip) clip = clip or false if clip then L = self:Clipdots(L) end self:Ddots(self:Proj3d(L), mark_options) end function luadraw_graph3d:Dballdots3d(L,color,scale,clip) -- points sphériques clip = clip or false if clip then L = self:Clipdots(L) end if L == nil then return end if isPoint3d(L) then L = {L} end color = color or "black" scale = scale or 1 local r = 0.075*scale L = self:Proj3d(L) if not ld.isID(self.matrix) then L = self:Mtransform(L) end for _, z in ipairs(L) do self:Writeln("\\fill[opacity=1,ball color="..color.."] "..self:Coord(z).." circle [radius="..ld.strReal(r).."cm];") end end function luadraw_graph3d:Dcrossdots3d(L,color,scale,angle,clip) -- points en forme de croix dans un plan -- L est une liste du type {point 3d, vecteur normal} ou { {point3d, vecteur normal}, {point3d, vecteur normal}, ...} color = color or self.param.linecolor scale = scale or 1 angle= angle or 0 local long = 0.125*scale local lg, A, normal, a, b, c = {} clip = clip or false local x1,x2,y1,y2,z1,z2 = table.unpack( self.param.viewport3d ) local calcAdot = function() local n = pt3d.normalize(normal) local u = pt3d.prod(n,self.Normal) if pt3d.N1(u)<1e-10 then u = self:ScreenX() else u = pt3d.normalize(u)end local v = pt3d.prod(n,u) if angle ~= 0 then u, v = table.unpack( ld.rotate3d({u,v},angle,{Origin,n}) ) end a, b, c = table.unpack(self:Proj3d({A,A+u-v,A+u+v})) -- Proj3d s'applique aux points (pas aux vecteurs) b = b-a; c = c-a b, c = long*b/self:Abs(b), long*c/self:Abs(c) end local oldcolor = self.param.linecolor local oldstyle = self.param.linestyle local oldwidth = self.param.linewidth local oldfillstyle = self.param.fillstyle self:Lineoptions("solid",color,4); self:Filloptions("none") if isPoint3d(L[1]) then L = {L} end for _, P in ipairs(L) do A = P[1]; if (not clip) or ( (x1<=A.x) and (A.x<=x2) and (y1<=A.y) and (A.y<=y2) and (z1<=A.z) and (A.z<=z2) ) then normal = P[2]; calcAdot() table.append(lg, {{a+b,a-b},{a-c,a+c}}) end end self:Dpolyline(lg) self:Lineoptions(oldstyle,oldcolor,oldwidth); self:Filloptions(oldfillstyle) end function luadraw_graph3d:Dlabel3d(...) local args = {} local text, anchor, anchor2d, options local dir = {} local n = select("#", ...) -- Nombre total d'arguments for i = 1, n-2, 3 do -- avec un pas de 3 (1,4,7...) text, anchor, options = select(i, ...) -- Récupère les 3 args if anchor ~= nil then anchor2d = self:Proj3d(anchor) options.dir = options.dir or dir dir = options.dir if #options.dir > 1 then local U, V = table.unpack(options.dir) U = pt3d.normalize(U); V = pt3d.normalize(V) options.dir = {self:Proj3d(anchor+U)-anchor2d, self:Proj3d(anchor+V)-anchor2d} -- ce sont des vecteurs end table.append(args,{text,anchor2d,options}) else print("Warning : the anchor point associated with the text "..text.." is equal to nil") end end self:Dlabel(table.unpack(args)) end ------- solides sans facettes (fil de fer) function luadraw_graph3d:Define_temp_color(argsColor) if type(argsColor) == "table" then argsColor = ld.rgb(argsColor) argsColor = string.sub(argsColor,2,#argsColor-1) -- on retire les accolades end if (type(argsColor) == "string") and (string.find(argsColor,"%A")~=nil) -- contient caractère non alpha numérique then self:Writeln("\\colorlet{tempColor}{"..argsColor.."}") return "tempColor" else return argsColor end end function luadraw_graph3d:Dplane(P,V,L1,L2,mode,draw_options) -- dessine des bords du plan P={A,u} -- v doit être un vecteur non nul de ce plan -- on construit un parallélogramme dont un côté est L1*v/abs(v) et l'autre L2*W/abs(w) où w = u^v -- le mode indique les bords à dessiner : -- mode = [top(0/1), right(0/1), bottom(0/1), left(0/1)]_2 (écriture binaire) if type(mode) ~= "number" then draw_options = mode; mode = 15 end -- 15 =all if mode ==nil then mode = 15 end local A, u = table.unpack(P) u = pt3d.normalize(u) V = pt3d.normalize(V) local W = pt3d.prod(u,V) V = L1*V W = L2*W --W = V+W local Dep, L = A+W/2-V/2, {} if mode & 8 == 8 then table.insert(L, {Dep, Dep+V}) end -- top if mode & 4 == 4 then table.insert(L, {Dep+V,Dep+V-W}) end --right if mode & 2 == 2 then table.insert(L, {Dep-W,Dep-W+V}) end --bottom if mode & 1 == 1 then table.insert(L, {Dep,Dep-W}) end --left L = ld.merge3d(L) self:Dpolyline3d(L,(mode == 15),draw_options) end ------ dessins de facettes --------------------------------------------- function luadraw_graph3d:Cosine_incidence(n,A) -- cosinus de l'angle d'incidence entre le vecteur n (unitaire) au point A et le vecteur dirigé vers l'observateur return pt3d.dot(self.Normal,n) end function luadraw_graph3d:Observer_distance(A) -- l'abscisse de A sur l'axe issue de Origine, dirigé vers l'observateur return pt3d.dot(self.Normal,A) end function luadraw_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 pt3d.dot(N,self.Normal) > 0 end function luadraw_graph3d:Classifyfacet(F) -- F est une liste de facettes ou un polyèdre -- la fonction renvoie 2 listes : les facettes visibles, et les facettes cachées local list, list2 if F.vertices ~= nil then list = ld.poly2facet(F) else list = F end if not isID3d(self.matrix3d) then list2 = self:Mtransform3d(list) else list2 = list end local V, H = {}, {} for k,facet in ipairs(list2) do if self:Isvisible(facet) then table.insert(V,list[k]) else table.insert(H,list[k]) end end if self:Det3d() > 0 then return V, H else return H, V end end function luadraw_graph3d:Sortfacet(F,backculling) -- F est une liste de facettes avec coordonnées 3d -- la fonction trie les facettes suivant la côte du centre de gravité -- le long de l'axe (O,n); de la plus petite côte à la plus grande -- backculling = true/false (élimine les facettes non visibles) -- la fonction renvoie une liste de facettes avec coordonnées 3d if #F == 1 then return F end local F1 = F if not isID3d(self.matrix3d) then F1 = self:Mtransform3d(F) end backculling = backculling or false local rep, aux = {}, {} for k,L in ipairs(F1) do -- on travaille sur les sommets transformés local G1 = pt3d.isobar3d(L) table.insert(aux, {k,self:Observer_distance(G1)}) --pt3d.dot(G1,self.Normal)}) end table.sort(aux, function(e1,e2) return ((e1[2]