-- luadraw_spherical.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 local ld = luadraw local pt3d = ld.pt3d local Origin, vecI, vecJ, vecK, M, Ms = pt3d.Origin, pt3d.vecI, pt3d.vecJ, pt3d.vecK, pt3d.M, pt3d.Ms local graph3d = ld.graph3d local insidelabelcolor = "darkgray" local hiddendelayed = false local arrowBstyle = "->" local arrowAstyle = "<-" local arrowABstyle = "<->" local sphere = {["C"]=Origin, ["R"]=3, ["color"]="orange", ["opacity"]=1, ["mode"]=ld.mBorder, ["edgecolor"]="LightGray", ["edgestyle"] = "solid", ["edgewidth"] = "4", ["hiddenstyle"] = ld.Hiddenlinestyle, ["hiddencolor"] = "gray", ["show"] = true, ["horizon"] = nil } -- sphere definition local before_sphere = {} local after_sphere = {} local hidden_part = {} function graph3d:Clear_spherical() before_sphere = {} after_sphere = {} hidden_part = {} sphere = {["C"]=Origin, ["R"]=3, ["color"]="orange", ["opacity"]=1, ["mode"]=ld.mBorder, ["edgecolor"]="LightGray", ["edgestyle"] = "solid", ["edgewidth"] = "4", ["hiddenstyle"] = ld.Hiddenlinestyle, ["hiddencolor"] = "gray", ["show"] = true, ["horizon"] = nil } -- sphere definition insidelabelcolor = "DarkGray" arrowBstyle = "->" arrowAstyle = "<-" arrowABstyle = "<->" end function graph3d:Define_sphere( args ) args = args or {} if args.center ~= nil then sphere.C = args.center end if args.radius ~= nil then sphere.R = args.radius end local Ct, R = sphere.C, sphere.R if args.color ~= nil then sphere.color = args.color end if args.opacity ~= nil then sphere.opacity = args.opacity end if args.mode ~= nil then sphere.mode = args.mode end if args.edgecolor ~= nil then sphere.edgecolor = args.edgecolor end if args.edgestyle ~= nil then sphere.edgestyle = args.edgestyle end if args.hiddenstyle ~= nil then sphere.hiddenstyle = args.hiddenstyle end if args.hiddencolor ~= nil then sphere.hiddencolor = args.hiddencolor end if args.edgewidth ~= nil then sphere.edgewidth = args.edgewidth end if args.show ~= nil then sphere.show = args.show end insidelabelcolor = args.insidelabelcolor or "DarkGray" arrowBstyle = args.arrowBstyle or "->" arrowAstyle = args.arrowBstyle or "<-" arrowABstyle = args.arrowABstyle or "<->" local mat = self.matrix3d local N, cam = self.Normal, ld.camera if not ld.isID3d(mat) then mat = ld.invmatrix3d(mat) N = ld.mLtransform3d(N,mat); cam = ld.mtransform3d(cam,mat) end if ld.projection_mode == "central" then sphere.horizon = { ld.interSS({Ct,R}, {(Ct+cam)/2,pt3d.abs(Ct-cam)/2}) } else sphere.horizon = {Ct, R, N} end end function ld.sM(x,y,z) -- or sM(theta,phi) (theta, phi degrees), define a spherical dot in (C,vecI,vecJ,vecK) local C, R = sphere.C, sphere.R if z ~= nil then --cartesian coordinates return C + R*pt3d.normalize(M(x,y,z)) else -- spherical coordinates return C + Ms(R,x*ld.deg,y*ld.deg) end end function ld.toSphere(A) local C, R = sphere.C, sphere.R local u = A-C if pt3d.N1(u)< 1e-12 then return end return C+R*pt3d.normalize(u) end local visibledot = function(A) if sphere.horizon == nil then print("Please define sphere first"); return end local I, r, n = table.unpack(sphere.horizon) return pt3d.dot(A-I,n) >= 0 end function graph3d:Dspherical() local oldlinestyle = self.param.linestyle local oldlinecolor = self.param.linecolor local oldlinewidth = self.param.linewidth local oldlineopacity = self.param.lineopacity local oldfillstyle = self.param.fillstyle local oldfillcolor = self.param.fillcolor local oldfillopacity = self.param.fillopacity local display_elt = function(elt) -- elt={path,style,color,width,opacity,arrows} ou -- elt={text,anchor,options} (labels) if type(elt[1]) == "string" then -- a label self:Dlabel3d(elt[1],elt[2],elt[3]) elseif pt3d.isPoint3d(elt[1]) then -- a dot self:Lineoptions(oldlinestyle, oldlinecolor, oldlinewidth); self:Lineopacity(oldlineopacity) self:Filloptions(oldfillstyle,oldfillcolor,oldfillopacity) self:Ddots3d(elt[1],elt[2]) else self:Lineoptions(elt[2],elt[3],elt[4]); self:Lineopacity(elt[5]) local arrowstyle, arrows = "", elt[6] if arrows == 1 then arrowstyle = arrowBstyle elseif arrows == 2 then arrowstyle = arrowABstyle elseif arrows == -1 then arrowstyle = arrowAstyle else arrows = 0 end if (elt[7] ~= nil) and (elt[7] ~= "") then -- fill --self:Filloptions("gradient","ball color="..elt[7],elt[8]) self:Filloptions("full",elt[7],elt[8]) else self:Filloptions("none",nil,1) end self:Linecap("round"); -- pour que les liaisons soient correctes entre segments successifs if arrows ~= 0 then self:Dpath3d(elt[1],"arrows="..arrowstyle) else self:Dpath3d(elt[1]) end end end for _, elt in ipairs(before_sphere) do display_elt(elt) end if sphere.show then self:Lineoptions(oldlinestyle, oldlinecolor, oldlinewidth); self:Lineopacity(oldlineopacity) self:Dsphere(sphere.C, sphere.R, {mode=sphere.mode, color=sphere.color, opacity=sphere.opacity, edgecolor=sphere.edgecolor, edgewidth=sphere.edgewidth, edgestyle=sphere.edgestyle, hiddenstyle=sphere.hiddenstyle, hiddencolor=sphere.hiddencolor}) end for _, elt in ipairs(after_sphere) do display_elt(elt) end if hiddendelayed then self:Begindeferred() end if sphere.show and ld.Hiddenlines and (sphere.edgestyle ~= "noline") and (sphere.hiddenstyle ~= "noline") then self:Dsphere(sphere.C, sphere.R, {mode=ld.mBorder,edgecolor=sphere.edgecolor, edgewidth=3*sphere.edgewidth/4, edgestyle=sphere.hiddenstyle}) end for _, elt in ipairs(hidden_part) do display_elt(elt) end if hiddendelayed then self:Enddeferred() end self:Lineoptions(oldlinestyle, oldlinecolor, oldlinewidth); self:Lineopacity(oldlineopacity) self:Filloptions(oldfillstyle,oldfillcolor,oldfillopacity) self:Clear_spherical() end -- ajouter un cercle tracé sur la sphère function graph3d:DScircle(P,options) -- P={A,u} (plane) -- options = {style=, color=, width=, opacity=, out=} options = options or {} local style = options.style or self.param.linestyle local color = options.color or self.param.linecolor local width = options.width or self.param.linewidth local opacity = options.opacity or self.param.lineopacity local hidden = options.hidden if hidden == nil then hidden = ld.Hiddenlines end local out = options.out -- returns end points of hidden part if it exists local C, R = sphere.C, sphere.R local N, angle = self.Normal local acircle = function(I,r,v,u) -- when we have to draw a circle local w = pt3d.prod(u,vecI) if pt3d.N1(w) < 1e-12 then w =pt3d.prod(u,vecJ) end local J = I+r*pt3d.normalize(w) -- a point of the circle if visibledot(I) then --(pt3d.dot(v,N) > 0) then -- visible table.insert(after_sphere, {{J,I,u,"c"},style,color,width,opacity}) if hidden and hiddendelayed and (style ~= "noline") then table.insert(hidden_part, {{J,I,u,"c"},ld.Hiddenlinestyle,color,width,opacity}) end elseif hidden and (style ~= "noline") then table.insert(hidden_part, {{J,I,u,"c"},ld.Hiddenlinestyle,color,width,opacity}) else --self:Beginadvanced() table.insert(before_sphere, {{J,I,u,"c"},style,color,width,opacity}) --self:Endadvanced() end end local A, b, c, u if #P == 3 then A, b, c = table.unpack(P) A= ld.toSphere(A); b = ld.toSphere(b); c = ld.toSphere(c) u = pt3d.prod(b-A,c-A) P = {A,u} else A, u = table.unpack(P) end local I = ld.proj3d(C,P) -- center if ld.projection_mode == "central" then N = ld.camera-I end local mat = self.matrix3d if not ld.isID3d(mat) then mat = ld.invmatrix3d(mat); N = ld.mLtransform3d(N,mat) end if pt3d.dot(u,I-C) < 0 then u = -u end local d = pt3d.abs(C-I) if d >= R then return end -- no circle local v, r = I-C if pt3d.N1(v) < 1e-12 then -- C is on P (big circle) v = Origin; r = R; --angle = 0 else r = math.sqrt(R^2- pt3d.abs2(v)) end if pt3d.N1(pt3d.prod(u,N))< 1e-12 then -- P has the same direction than screen acircle(I,r,v,u) else local n2 = pt3d.normalize(pt3d.prod(N,u)) local n1 = pt3d.normalize(pt3d.prod(u,n2)) if ld.projection_mode == "central" then angle = (r*r-pt3d.dot(v,N))/(r*pt3d.dot(n1,N)) else angle = -pt3d.dot(v,N)/(r*pt3d.dot(n1,N)) end if math.abs(angle) > 1+1e-6 then acircle(I,r,v,u,angle) elseif (1