-- luadraw_graph3d.lua -- date 2026/05/29 -- version 3.1 -- 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.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" 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 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} graph3d.matrix3d = {M(0,0,0), M(xsc,0,0), M(0,ysc,0), M(0,0,zsc)} 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 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 local chem = ld.circle3db(C,R,normal) local 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() ld.insert(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 ld.insert(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:Dcylinder(A,r,V,B,args) -- ou Dcylinder(A,r,B,args): cylindre droit de A vers B -- ou Dcylinder(A,V,r,args): ancienne syntaxe, -- dessine un cylindre en fil de fer -- A est le centre d'une face circulaire de rayon r orthogonale au vecteur V -- l'autre face a pour centre B -- args est une table à 6 champs : -- {mode =0/1, hiddenstyle="dotted", hiddencolor = linecolor, edgecolor=linecolor, color="", opacity=1} -- mode = 0 fil de fer -- mode = 1 grille -- color = "" : pas de remplissage, color ~= "" remplissage avec ball color if isPoint3d(r) then -- ancienne syntaxe A,V,r,args local R = r r = V; V = R; args = B; B = A+V elseif not isPoint3d(B) then -- syntaxe A,r,B,args args = B; B = V; V = B-A end args = args or {} args.color = args.color or "" args.color = self:Define_temp_color(args.color) args.edgecolor = args.edgecolor or self.param.linecolor args.edgestyle = args.edgestyle or self.param.linestyle args.edgewidth = args.edgewidth or self.param.linewidth args.hiddencolor = args.hiddencolor or args.edgecolor args.hiddenstyle = args.hiddenstyle or ld.Hiddenlinestyle local out = args.out -- outline of cylinder --if not Hiddenlines then args.hiddenstyle = "noline" end args.mode = args.mode or 0 args.opacity = args.opacity or 1 args.gradsection = args.gradsection or {25,18,50} args.gradside= args.gradside or {50,10,100} local lsection, msection, rsection = table.unpack( args.gradsection) local lside, mside, rside = table.unpack( args.gradside) local gradStyleSide = "left color="..args.color.."!"..tostring(lside)..",right color = "..args.color.."!"..tostring(rside)..",middle color="..args.color.."!"..tostring(mside) local gradStyleSection = "left color="..args.color.."!"..tostring(lsection)..",right color = "..args.color.."!"..tostring(rsection)..",middle color="..args.color.."!"..tostring(msection) local oldfillstyle = self.param.fillstyle local oldfillopacity = self.param.fillopacity local oldfillcolor = self.param.fillcolor local oldlinestyle = self.param.linestyle local oldlineopacity = self.param.lineopacity local oldlinecolor = self.param.linecolor local oldlinewidth = self.param.linewidth local mat = self.matrix3d local N = mLtransform3d(self.Normal,invmatrix3d(mat)) if pt3d.dot(self.Normal,self:MLtransform3d(V)) <= 0 then V = -V end if pt3d.dot(self:MLtransform3d(V),self:MLtransform3d(B-A)) < 0 then A,B=B,A end -- V et B-A dans le même sens local W = B-A local angle = self:Arg(self:Proj3dV(W))*ld.rad if angle < 0 then angle = angle+180 elseif angle > 180 then angle = angle-180 end self:Lineoptions(args.edgestyle,args.edgecolor,args.edgewidth) local I = pt3d.normalize(V) -- vecteur normal au plan et dans la direction du sommet B J = pt3d.prod(I,N); J = pt3d.normalize(J) if (J == nil) then -- le plan de la base circulaire est l'écran J = self:ScreenX() end local K = pt3d.prod(I,J) -- base = {A+r.cos(t)J+r.sin(t)K / t in [-pi,pi]} local xn,yn, zn = pt3d.dot(N,I), pt3d.dot(N,J), pt3d.dot(N,K) local xw,yw,zw = pt3d.dot(W,I), pt3d.dot(W,J), pt3d.dot(W,K) local t = ld.solve(function(t) return math.sin(t)*(xn*zw-zn*xw)+math.cos(t)*(xn*yw-xw*yn) end,-math.pi/2,3*math.pi/2) local dcircle = function(center) if args.color ~= "" then self:Filloptions("gradient", gradStyleSection..",shading angle="..ld.strReal(angle),args.opacity) else self:Filloptions("none") end self:Dcircle3d(center,r,V) if out ~= nil then out.visible = {r*J,center,V,"c"} out.hidden = {} end end if (t == nil) or (#t == 1) then dcircle(A) else t1, t2 = table.unpack(t) local M1 = A+math.cos(t1)*r*J+math.sin(t1)*r*K local M2 = A+math.cos(t2)*r*J+math.sin(t2)*r*K if math.cos(t1)*math.sin(t2)-math.cos(t2)*math.sin(t1)< 0 then M1,M2 = M2,M1 end local N1 = M1+W local N2 = M2+W if pt3d.N1(M2-M1) < 1e-12 then -- points confondus dcircle(A) else if args.color == "" then self:Filloptions("none") else self:Filloptions("gradient", gradStyleSide..",shading angle="..ld.strReal(angle),args.opacity) end if args.mode == 1 then self:Linestyle("noline") end -- on voit la base circulaire en B, le vecteur I sort du cylindre en B et est dirigé vers l'observateur local sens = 1 --if pt3d.det(W,A-M1,M2-M1)*pt3d.det(W,B-N1,N2-N1) < 0 then sens = -sens end if cpx.det(self:Proj3dV(B-A),self:Proj3dV(B-N2))*self:Det3d() < 0 then sens = -sens end --self:Dpath3d({M1,A,M2,r,sens,V,"ca",N2,"l",B,N1,r,sens,V,"ca","cl"}) self:Dpath3d({M1,A,M2,r,sens,V,"ca",N2,"l",B,N1,r,-sens,V,"ca","cl"}, "draw=none") self:Filloptions("none") self:Dpath3d({N1,M1,"l",A,M2,r,sens,V,"ca",N2,"l"}) dcircle(B) --self:Filloptions("none") --self:Darc3d(N1,B,N2,r,sens,V) if (args.mode ~= 1) and (args.hiddenstyle ~= "noline") then -- partie cachée self:Filloptions("none") self:Lineoptions(args.hiddenstyle,args.hiddencolor,args.edgewidth) self:Darc3d(M1,A,M2,r,-sens,V) end if out ~= nil then out.visible = {N1,M1,"l",A,M2,r,sens,V,"ca",N2,"l",B,V,"c"} out.hidden = {M1,A,M2,r,-sens,V,"ca"} end --self:Ddots3d({B,N1}); self:Dpolyline3d({B,B+I}); self:Darc3d(B+J,B,B+K,r/2,1,I,"->") if args.mode == 1 then -- arêtes --self:Linestyle(oldlinestyle) self:Dpoly(ld.cylinder(A,r,V,B,35,false), {mode=0,hiddenstyle=args.hiddenstyle, edgecolor=args.edgecolor,hiddencolor=args.hiddencolor, edgestyle=args.edgestyle, edgewidth=args.edgewidth}) end end end self:Filloptions(oldfillstyle,oldfillcolor,oldfillopacity) self:Lineoptions(oldlinestyle,oldlinecolor,oldlinewidth) self:Lineopacity(oldlineopacity) end function luadraw_graph3d:Dcone(C,r,V,A,args) -- ou Dcone(C,r,A,args) -- ou Dcone(A,V,r,args) (ancienne syntaxe) -- dessine un cône en fil de fer -- A est le sommet -- le centre de la face circulaire de rayon r orthogonale au vecteur V est C -- args est une table à 5 champs : -- {mode =0/1, hiddenstyle="dotted", hiddencolor = linecolor, edgecolor= linecolor, color="", opacity=1} -- mode = 0 fil de fer -- mode = 1 grille -- color = "" : pas de remplissage, color ~= "" remplissage avec gradient bi linéaire if isPoint3d(r) then -- ancien format : sommet, vecteur, rayon, args (cône droit) args = A; A = C r, V = V, r C = A+V elseif not isPoint3d(A) then -- format C,r,A,args (cône droit) args = A; A = V; V = A-C end args = args or {} args.color = args.color or "" args.color = self:Define_temp_color(args.color) args.edgecolor = args.edgecolor or self.param.linecolor args.edgestyle = args.edgestyle or self.param.linestyle args.edgewidth = args.edgewidth or self.param.linewidth args.hiddencolor = args.hiddencolor or args.edgecolor args.hiddenstyle = args.hiddenstyle or ld.Hiddenlinestyle --if not Hiddenlines then args.hiddenstyle = "noline" end args.mode = args.mode or 0 args.opacity = args.opacity or 1 args.gradsection = args.gradsection or {25,18,50} args.gradside= args.gradside or {50,10,100} local lsection, msection, rsection = table.unpack( args.gradsection) local lside, mside, rside = table.unpack( args.gradside) local gradStyleSide = "left color="..args.color.."!"..tostring(lside)..",right color = "..args.color.."!"..tostring(rside)..",middle color="..args.color.."!"..tostring(mside) local gradStyleSection = "left color="..args.color.."!"..tostring(lsection)..",right color = "..args.color.."!"..tostring(rsection)..",middle color="..args.color.."!"..tostring(msection) local oldfillstyle = self.param.fillstyle local oldfillopacity = self.param.fillopacity local oldfillcolor = self.param.fillcolor local oldlinestyle = self.param.linestyle local oldlineopacity = self.param.lineopacity local oldlinecolor = self.param.linecolor local oldlinewidth = self.param.linewidth self:Lineoptions(args.edgestyle,args.edgecolor,args.edgewidth) if pt3d.dot(self:MLtransform3d(V),self:MLtransform3d(A-C)) < 0 then V = -V end -- V et A-C dans le même sens local I = pt3d.normalize(V) -- vecteur normal au plan et dans la direction du sommet A local mat = self.matrix3d local N = mLtransform3d(self.Normal,invmatrix3d(mat)) local J = pt3d.prod(I,N); J = pt3d.normalize(J) if (J == nil) then -- le plan de la base circulaire est l'écran J = self:ScreenX() end local K = pt3d.prod(I,J) -- base = {C+r.cos(t)J+r.sin(t)K / t in [-pi,pi]} local xn,yn, zn = pt3d.dot(N,I), pt3d.dot(N,J), pt3d.dot(N,K) local W = C-A local angle = self:Arg(self:Proj3dV(W))*ld.rad if angle < 0 then angle = angle+180 elseif angle > 180 then angle = angle-180 end local dcircle = function(center) if args.color ~= "" then self:Filloptions("gradient",gradStyleSection..",shading angle="..ld.strReal(angle),args.opacity) else self:Filloptions("none") end self:Dcircle3d(center,r,V) end local xw,yw,zw = pt3d.dot(W,I), pt3d.dot(W,J), pt3d.dot(W,K) local t = ld.solve(function(t) return math.sin(t)*(xn*zw-zn*xw)+math.cos(t)*(xn*yw-xw*yn)+r*xn end,0,2*math.pi) if (t == nil) or (#t == 1) then dcircle(C) else t1, t2 = table.unpack(t) local M1 = C+math.cos(t1)*r*J+math.sin(t1)*r*K local M2 = C+math.cos(t2)*r*J+math.sin(t2)*r*K if math.cos(t1)*math.sin(t2)-math.cos(t2)*math.sin(t1)< 0 then M1,M2 = M2,M1 end if pt3d.N1(M2-M1) < 1e-12 then -- points confondus dcircle(C) else if args.color == "" then self:Filloptions("none") else --self:Filloptions("gradient", "left color=white,right color = "..args.color..", shading angle="..strReal(angle),args.opacity) self:Filloptions("gradient",gradStyleSide..",shading angle="..ld.strReal(angle),args.opacity) end if args.mode == 1 then self:Linestyle("noline") end local sens = 1 if cpx.det(self:Proj3dV(C-M1),self:Proj3dV(M2-M1))*cpx.det(self:Proj3dV(A-M1),self:Proj3dV(M2-M1)) < 0 then sens = -1 end if pt3d.dot(self.Normal,self:MLtransform3d(V)) <= 0 then -- on voit la base circulaire self:Dpath3d({A,M1,"l",C,M2,r,-sens,V,"ca","cl"},"draw=none") self:Filloptions("none") self:Dpath3d({M1,A,M2,"l"}) dcircle(C) --self:Filloptions("none") --self:Darc3d(M1,C,M2,r,-sens,V) --print("base vue"); self:Ddots3d({C,M1}); self:Darc3d(C+J,C,C+K,r/2,1,'->') else -- on ne voit pas la base circulaire self:Dpath3d({A,M1,"l",C,M2,r,sens,V,"ca","cl"}) if (args.mode ~= 1) and (args.hiddenstyle ~= "noline") then -- partie cachée self:Filloptions("none") self:Lineoptions(args.hiddenstyle,args.hiddencolor) self:Darc3d(M1,C,M2,r,-sens,V) --print("base pas vue"); self:Ddots3d({C,M1}); self:Darc3d(C+J,C,C+K,r/2,1,'->') end end if args.mode == 1 then -- arêtes self:Linestyle(oldlinestyle) self:Dpoly(ld.cone(C,r,V,A,35,false), {mode=0,hiddenstyle=args.hiddenstyle, edgecolor=args.edgecolor,hiddencolor=args.hiddencolor}) end end end self:Filloptions(oldfillstyle,oldfillcolor,oldfillopacity) self:Lineoptions(oldlinestyle,oldlinecolor,oldlinewidth); self:Lineopacity(oldlineopacity) end function luadraw_graph3d:Dfrustum(A,R,r,V,B,args) -- ou Dfrustum(A,R,r,V,args) pour un cône droit -- frustum drawn without facets (tronc de cône) -- dessine un tronc de cône en fil de fer -- A est le centre de la face de rayon R -- le centre de l'autre face C=A+V et son rayon est r -- args est une table à 5 champs : -- {mode =0/1, hiddenstyle="dotted", hiddencolor = linecolor, edgecolor=linecolor, color="", opacity=1} -- mode = 0 fil de fer -- mode = 1 grille -- color = "" : pas de remplissage, color ~= "" remplissage avec linéaire if R == r then -- cylinder if not isPoint3d(B) then self:Dcylinder(A,V,R,B) -- B is args in this case else self:Dcylinder(A,R,V,B,args) end return end local C if isPoint3d(B) then -- slanted frustum C = dproj3d(B,{A,V}) V = C-A local U1, U2, U3, h U1 = pt3d.normalize(V) U2 = pt3d.prod(U1,vecJ) if pt3d.N1(U2) < 1e-12 then U2 = pt3d.prod(U1,vecI) end U2 = pt3d.normalize(U2) U3 = pt3d.prod(U1,U2) h = pt3d.abs(V) local f = function(m) return A + pt3d.dot(m-A,U1)*(B-A)/h + pt3d.dot(m-A,U2)*U2 + pt3d.dot(m-A,U3)*U3 end self:Savematrix() self:Composematrix3d( matrix3dof(f) ) else C = A+V; args = B; B = nil end local dcircle = function() if args.color ~= "" then --self:Filloptions("gradient",gradStyleSection,args.opacity) else self:Filloptions("none") end self:Dcircle3d(A,R,V) if pt3d.dot(self:MLtransform3d(V),self.Normal) >= 0 then self:Filloptions("none") self:Dcircle3d(C,r,V) else if (args.mode ~= 1) and (args.hiddenstyle ~= "noline") then -- partie cachée self:Filloptions("none") self:Lineoptions(args.hiddenstyle,args.hiddencolor,args.edgewidth) self:Dcircle3d(C,r,V) end end end args = args or {} args.color = args.color or "" args.color = self:Define_temp_color(args.color) args.edgecolor = args.edgecolor or self.param.linecolor args.edgestyle = args.edgestyle or self.param.linestyle args.edgewidth = args.edgewidth or self.param.linewidth args.hiddencolor = args.hiddencolor or args.edgecolor args.hiddenstyle = args.hiddenstyle or ld.Hiddenlinestyle --if not Hiddenlines then args.hiddenstyle = "noline" end args.mode = args.mode or 0 args.opacity = args.opacity or 1 args.mode = args.mode or 0 args.gradsection = args.gradsection or {25,18,50} args.gradside= args.gradside or {50,10,100} local lsection, msection, rsection = table.unpack( args.gradsection) local lside, mside, rside = table.unpack( args.gradside) local gradStyleSide = "left color="..args.color.."!"..tostring(lside)..",right color = "..args.color.."!"..tostring(rside)..",middle color="..args.color.."!"..tostring(mside) local gradStyleSection = "left color="..args.color.."!"..tostring(lsection)..",right color = "..args.color.."!"..tostring(rsection)..",middle color="..args.color.."!"..tostring(msection) local oldfillstyle = self.param.fillstyle local oldfillopacity = self.param.fillopacity local oldfillcolor = self.param.fillcolor local oldlinestyle = self.param.linestyle local oldlineopacity = self.param.lineopacity local oldlinecolor = self.param.linecolor local oldlinewidth = self.param.linewidth self:Lineoptions(args.edgestyle,args.edgecolor,args.edgewidth) if R < r then A, C = C, A; V = -V R, r = r, R end local k = R/(R-r) local H = V local V = k*V local S = A+V local mat = self.matrix3d local N = mLtransform3d(self.Normal,invmatrix3d(mat)) local I = pt3d.normalize(V) local J = pt3d.prod(I,N); J = pt3d.normalize(J) if (J == nil) then -- le plan de la base circulaire est l'écran J = self:ScreenX() end local K = pt3d.prod(I,J) -- base = {A+r.cos(t)J+r.sin(t)K / t in [-pi,pi]} local xn,yn, zn = pt3d.dot(N,I), pt3d.dot(N,J), pt3d.dot(N,K) local W = A-S local xw,yw,zw = pt3d.dot(W,I), pt3d.dot(W,J), pt3d.dot(W,K) local t = ld.solve(function(t) return math.sin(t)*(xn*zw-zn*xw)+math.cos(t)*(xn*yw-xw*yn)+R*xn end,0,2*math.pi) if (t == nil) or (#t == 1) then dcircle() else local angle = self:Arg(self:Proj3dV(W))*ld.rad if angle < 0 then angle = angle+180 elseif angle > 180 then angle = angle-180 end t1, t2 = table.unpack(t) local M3 = A+math.cos(t1)*R*J+math.sin(t1)*R*K local M4 = A+math.cos(t2)*R*J+math.sin(t2)*R*K if math.cos(t1)*math.sin(t2)-math.cos(t2)*math.sin(t1)< 0 then M3,M4 = M4,M3 end if pt3d.N1(M3-M4) < 1e-12 then -- points confondus dcircle() else local M1, M2 = table.unpack( ld.scale3d({M3,M4}, r/R, S) ) --self:Ddots3d({M1,M2}) if args.color == "" then self:Filloptions("none") else self:Filloptions("gradient",gradStyleSide..",shading angle="..ld.strReal(angle),args.opacity) end if args.mode == 1 then self:Linestyle("noline") end local sens = 1 --if cpx.det(self:Proj3d(C-M1),self:Proj3d(M2-M1))*cpx.det(self:Proj3d(A-M1),self:Proj3d(M2-M1)) < 0 then sens = -1 end if pt3d.det(I,C-M1,M2-M1)*pt3d.det(I,A-M1,M2-M1) < 0 then sens = -1 end if pt3d.dot(self.Normal,self:MLtransform3d(V)) >= 0 then -- on voit la petite base circulaire (C,r) self:Dpath3d({M3,M1,"l",C,M2,r,-sens,V,"ca",M4,"l",A,M3,R,sens,V,"ca"}, "draw=none") self:Filloptions("none") self:Dpath3d({M2,M4,"l",A,M3,R,sens,V,"ca",M1,"l"}) if args.color ~= "" then self:Filloptions("gradient",gradStyleSection..",shading angle="..ld.strReal(angle),args.opacity) else self:Filloptions("none") end --self:Darc3d(M1,C,M2,r,-sens,V) self:Dcircle3d(C,r,V) if (args.mode ~= 1) and (args.hiddenstyle ~= "noline") then -- partie cachée self:Filloptions("none") self:Lineoptions(args.hiddenstyle,args.hiddencolor,args.edgewidth) self:Darc3d(M4,A,M3,R,-sens,V) end else -- la grande base circulaire (A,R) self:Dpath3d({M3,M1,"l",C,M2,r,sens,V,"ca",M4,"l",A,M3,R,-sens,V,"ca"}, "draw=none") self:Filloptions("none") self:Dpath3d({M3,M1,"l",C,M2,r,sens,V,"ca",M4,"l"}) if args.color ~= "" then self:Filloptions("gradient",gradStyleSection..",shading angle="..ld.strReal(angle),args.opacity) else self:Filloptions("none") end --self:Darc3d(M4,A,M3,R,-sens,V) self:Dcircle3d(A,R,V) if (args.mode ~= 1) and (args.hiddenstyle ~= "noline") then -- partie cachée self:Filloptions("none") self:Lineoptions(args.hiddenstyle,args.hiddencolor,args.edgewidth) self:Darc3d(M1,C,M2,r,-sens,V) end end if args.mode == 1 then -- arêtes --self:Linestyle(oldlinestyle) -- la matrice a déjà été changée! self:Dpoly(ld.frustum(A,R,r,H,35,false),{mode=0, hiddenstyle=args.hiddenstyle, hiddencolor=args.hiddencolor, edgewidth=args.edgewidth, edgestyle=args.edgestyle}) end end end if B ~= nil then self:Restorematrix() end self:Filloptions(oldfillstyle,oldfillcolor,oldfillopacity) self:Lineoptions(oldlinestyle,oldlinecolor,oldlinewidth); self:Lineopacity(oldlineopacity) 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 function luadraw_graph3d:Dsphere(A,r,args) -- dessine une sphère en fil de fer -- A est le sommet, r le rayon -- args est une table à 5 champs : -- {mode=0/1/2, hiddenstyle="dotted", hiddencolor = linecolor, edgecolor=linecolor,color="", opacity=1} -- color = "" : pas de remplissage, color ~= "" remplissage avec ball color -- si mode 1 : edgestyle = linestyle, edgecolor = linecolor, edgewidth = linewidth -- mode = 0 contour avec équateur -- mode = 1 contour avec méridiens et fuseaux -- mode = 2 contour seulement (cercle) args = args or {} args.color = args.color or "" args.edgecolor = args.edgecolor or self.param.linecolor args.hiddencolor = args.hiddencolor or args.edgecolor args.hiddenstyle = args.hiddenstyle or ld.Hiddenlinestyle --if not Hiddenlines then args.hiddenstyle = "noline" end args.edgestyle = args.edgestyle or self.param.linestyle args.edgecolor = args.edgecolor or self.param.linecolor args.edgewidth = args.edgewidth or self.param.linewidth args.mode = args.mode or 0 args.opacity = args.opacity or 1 local oldfillstyle = self.param.fillstyle local oldfillopacity = self.param.fillopacity local oldfillcolor = self.param.fillcolor local oldlinestyle = self.param.linestyle local oldlineopacity = self.param.lineopacity local oldlinecolor = self.param.linecolor local oldlinewidth = self.param.linewidth self:Linecolor(args.edgecolor) local V = (3*self:ScreenY()+self.Normal)/4 if args.color ~= "" then self:Filloptions("gradient", "ball color="..args.color, args.opacity) else self:Filloptions("none") end --self:Dcircle(self:Proj3d(A),r) local mat = invmatrix3d( self.matrix3d ) local N = mLtransform3d(self.Normal,mat) self:Lineoptions(args.edgestyle,args.edgecolor,args.edgewidth) self:Dcircle3d(A,r,N) if args.mode == 0 then -- équateur local u = pt3d.normalize(pt3d.prod(N,V)) local M1, M2 = A+r*u, A-r*u self:Filloptions("none") --; self:Lineoptions(args.edgestyle,args.edgecolor,args.edgewidth) self:Darc3d(M1,A,M2,r,1,V) self:Lineoptions(args.hiddenstyle,args.hiddencolor) self:Darc3d(M1,A,M2,r,-1,V) elseif args.mode == 1 then -- grille self:Dpoly(ld.sphere(A,r),{mode=0,hiddenstyle=args.hiddenstyle,hiddencolor=args.hiddencolor,edgestyle=args.edgestyle,edgecolor=args.edgecolor,edgewidth=args.edgewidth}) end self:Filloptions(oldfillstyle,oldfillcolor,oldfillopacity) self:Lineoptions(oldlinestyle,oldlinecolor,oldlinewidth); self:Lineopacity(oldlineopacity) 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 return V, H 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]