-- luadraw_lines3d.lua (chargé par luadraw__graph3d)
-- 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

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

function ld.getbounds3d(L)
-- renvoie les limites xmin,xmax,ymin,ymax,zmin,zmax de la ligne polygonale 3d L
    if (L == nil) or (type(L) ~= "table") or (#L == 0) then return end
    if isPoint3d(L[1]) then L = {L} end -- liste de points 3d
    local A = L[1][1]
    local xmin, xmax, ymin, ymax, zmin, zmax = A.x, A.x, A.y, A.y, A.z, A.z
    for _,cp in ipairs(L) do
        for _,A in ipairs(cp) do
            --if isPoint3d(A) then
                if A.x < xmin then xmin = A.x end
                if A.x > xmax then xmax = A.x end
                if A.y < ymin then ymin = A.y end
                if A.y > ymax then ymax = A.y end
                if A.z < zmin then zmin = A.z end
                if A.z > zmax then zmax = A.z end
            --end
        end
    end
    return xmin, xmax, ymin, ymax, zmin, zmax
end

function ld.merge3d(L,epsilon)
-- L est une  liste de listes de points3d (ligne polygonale de l'espace), 
-- on recolle au mieux les composantes
    epsilon = epsilon or 1e-10
    local S, num, aux, rep = {}, {}, {}, {}
    for _, cp in ipairs(L) do
        aux = {}
        for _,A in ipairs(cp) do
            table.insert(aux, pt3d.insert3d(S,A,epsilon))
        end
        table.insert(num, aux)
    end
    for _,cp in ipairs(ld.merge(num)) do
        aux = {}
        for _, k in ipairs(cp) do
            table.insert(aux,S[k])
        end
        table.insert(rep, aux)
    end
    return rep
end

-- calcul de courbes
function ld.parametric3d(p,t1,t2,nbdots,discont,nbdiv) 
-- le paramétrage p est une fonction : t (réel) -> p(t) (pt3d)
    local saut = (discont or false)
    local niveau = (nbdiv or 5)
    local curve = {}
    local lastJump = true
    local dep = t1
    local fin = t2
    local nb = (nbdots or 40)
    local pas = (fin - dep) / (nb-1)
    local cp = {} -- composante connexe courante
    local t = dep
    local count = 0
    local seuil = 3--math.abs(pas)*2.5 --empirique
    

    local closeCp = function ()
            if count > 1 then
                table.insert(curve, cp)
            end
            lastJump = true
            count = 0
            cp ={}
        end
    
    local addCp = function (z)
            lastJump = false
            table.insert(cp, z)
            count = count + 1
        end
        
    local middle -- fonction récursive middle    
    middle = function(t1,t2,f1,f2,n,d12)
        if (n > niveau) then 
            if saut then closeCp() end -- fermer la composante connexe
        else
           local tm = (t1 + t2) / 2  -- dichotomie
           local fm = ld.evalf(p,tm)
           if (fm ~= nil) then
                if (f1 ~= nil) and (f2 ~= nil) then
                    if d12 == nil then d12 = pt3d.abs(f2-f1) end
                    local d1m, dm2 = pt3d.abs(f1-fm), pt3d.abs(f2-fm)
                    if (d12>seuil) or (d1m+dm2>1.0005*d12)
                    then
                        middle(t1,tm,f1,fm,n+1,d1m)
                        addCp(fm)
                        middle(tm,t2,fm,f2,n+1,dm2)
                    end
                else
                    middle(t1,tm,f1,fm,n+1,nil)
                    addCp(fm)
                    middle(tm,t2,fm,f2,n+1,nil)
                end
            else -- fm = nil
                if (f1 ~= nil) then middle(t1,tm,f1,fm,n+1,nil) else closeCp() end
                if (f2 ~= nil) then middle(tm,t2,fm,f2,n+1,nil) else closeCp() end
            end
        end        
    end
    --corps de la fonction parametric
    local prec = nil -- point précédent le point courant
    local tPrec = nil -- valeur de t précédente
    local z = nil -- point courant
    for _ = 1, nb do
        A = p(t)
        if (A == nil) or notDef(A.x) or notDef(A.y) or notDef(A.z) then A = nil end
        if (A ~= nil) then
            if  (prec ~= nil) or ( (prec == nil) and (t > dep) ) then
                middle(tPrec,t,prec,A,1,nil)
            end
            addCp(A)
        else -- A = nil
            if (prec ~= nil) then middle(tPrec,t,prec,A,1,nil) 
            else closeCp()
            end
        end
        tPrec = t
        prec = A
        t = t + pas
    end
    if (not lastJump) and (count > 1) then table.insert(curve, cp) end -- dernière composante
    if #curve > 0 then return curve end
end


 -- arc de cercle en courbe de Bézier
function ld.arc3db(B,A,C,R,sens,normal)
-- calcule 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é
-- renvoie un chemin en courbes de Bézier avec des point3d
    local u, AC = B-A, C-A
    u = pt3d.normalize(u)
    local N = normal or pt3d.prod(u,AC)
    N = pt3d.normalize(N)
    local v = pt3d.prod(N,u)
    local a, b = pt3d.dot(AC,u), pt3d.dot(AC,v)
    local S = ld.arcb(1,0,Z(a,b),R,sens) -- Z(a,b) est l'affixe de AC dans le repère (A,u,v)
    local chem = {}
    for _, P in ipairs(S) do
        if type(P) == "string" then table.insert(chem,P)
        else
            table.insert(chem, A+P.re*u+P.im*v)
        end
    end
    return chem -- renvoie un chemin en courbes de Bézier 3d
end

 -- arc de cercle en ligne polygonale
 function ld.arc3d(B,A,C,r,sens,normal)
-- renvoie la liste de points de l'arc BAC (ligne polygonale 3d)
-- r = rayon
-- sens = 1/-1 (sens trigo ou inverse)
-- normal : vecteur normal au plan de l'arc
    local u, v = B-A, C-A
    u, v = pt3d.normalize(u), pt3d.normalize(v)
    local n = normal
    if n == nil then n = pt3d.prod(u,v) end
    if (n == nil) or pt3d.isNul(n) then return end
    n = pt3d.normalize(n)
    local V = r*u --vecteur de depart
    local W = pt3d.prod(n,V)
    local alpha = ld.cpx.arg( ld.cpx.Z(pt3d.dot(v,V), pt3d.dot(v,W)) )--pt3d.angle3d(u,v,1e-10)
    local fin = alpha
    if ((sens > 0) and (fin < 0)) then fin = 2*math.pi+fin
    elseif ((sens < 0) and (fin > 0)) then fin = fin-2*math.pi --; W = -W
    end
    if alpha == 0 then fin = 2*math.pi end
    local L = ld.parametric3d( function(t) return A+math.cos(t)*V+math.sin(t)*W end, 0,fin, math.max(2,math.floor(20*fin/math.pi)) )
    return L
end

-- cercle en courbe de Bézier
function ld.circle3db(C,R,normal)
-- calcule un cercle de centre C de rayon R et normal au vecteur normal
-- renvoie un chemin en courbes de Bézier avec des point3d
    local N, u  = pt3d.normalize(normal)
    --if N.x ~= 0 then u = M(-(N.y+N.z)/N.x,1,1)
    --elseif N.y ~= 0 then u = M(1, -(N.x+N.z)/N.y,1)
    --else u = M(1,1,-(N.y+N.x)/N.z)
    --end
    u = pt3d.prod(N,pt3d.vecI)
    if pt3d.abs(u) <1e-12 then u = pt3d.prod(N,pt3d.vecJ) end 
    u = pt3d.normalize(u); v = pt3d.prod(N,u)
    local S = ld.circleb(0,R) -- dans le repère (C,u,v)
    if S ~= nil then
        local chem = {}
        for _, P in ipairs(S) do
            if type(P) == "string" then table.insert(chem,P)
            else
                table.insert(chem, C+P.re*u+P.im*v)
            end
        end
        return chem -- renvoie un chemin en courbes de Bézier
    end
end

-- cercle en ligne polygonale
function ld.circle3d(A,r,normal)
-- calcule un cercle de centre C de rayon R et normal au vecteur normal
-- renvoie une ligne polygonale
    local u = pt3d.prod(vecI,normal)
    if pt3d.isNul(u) then 
        u = pt3d.prod(vecJ,normal)
        if pt3d.isNul(u) then return end
    end
    return ld.arc3d(A+u,A,A+u,r,1,normal)
end

-- cercles de l'espace, ou sphères, circonscrits ou inscrits 
function ld.circumcircle3d(A,B,C)
    local n = pt3d.normalize(pt3d.prod(B-A,C-A))
    local u = pt3d.normalize(B-A)
    local v = pt3d.prod(n,u)
    -- now we can do 2d geometry with complex in the direct orthonormal frame (A,u,v)
    local a, b, c = 0, pt3d.abs(B-A), Z(pt3d.dot(C-A,u), pt3d.dot(C-A,v)) -- affixes of A, B and C
    local I = ld.interD( ld.med(a,b), ld.med(b,c) ) --intersection of the perpendicular bisectors of the sides
    local radius =cpx.abs(I)
    return A+I.re*u+I.im*v, radius, n
end

function ld.incircle3d(A,B,C) -- returns center, radius, normal
    local n = pt3d.normalize(pt3d.prod(B-A,C-A))
    local u = pt3d.normalize(B-A)
    local v = pt3d.prod(n,u)
    -- now we can do 2d geometry with complex in the direct orthonormal frame (A,u,v)
    local a, b, c = 0, pt3d.abs(B-A), Z(pt3d.dot(C-A,u), pt3d.dot(C-A,v)) -- affixes of A, B and C
    local I = ld.interD( ld.bissec(b,a,c), ld.bissec(a,b,c) ) --intersection of interior bisectors
    local radius = cpx.abs(I-ld.proj(I,{0,b}))
    return A+I.re*u+I.im*v, radius, n
end

function ld.circumsphere(A,B,C,D) -- circumsphere for a tetrahedron, returns center, radius
    local P1,P2,P3 = {(A+B)/2, B-A}, {(B+C)/2,B-C}, {(C+D)/2,C-D}
    local D = ld.interPP(P1,P2)
    if D ~= nil then
        local I = ld.interDP(D,P3)
        if I ~= nil then
            return I, pt3d.abs(I-A)
        end
    end
end

function ld.insphere(A,B,C,D) -- insphere for a tetrahedron, returns center, radius
    local hA = pt3d.abs(A-proj3d(A,{B,pt3d.prod(C-B,D-B)}))
    local hB = pt3d.abs(B-proj3d(B,{C,pt3d.prod(D-C,A-C)}))
    local hC = pt3d.abs(C-proj3d(C,{D,pt3d.prod(A-D,B-D)}))
    local hD = pt3d.abs(D-proj3d(D,{A,pt3d.prod(B-A,C-A)}))
    local S = 1/hA+1/hB+1/hC+1/hD
    local I = (A/hA+B/hB+C/hC+D/hD)/S
    local R = 1/S
    if (I ~= nil) and (R ~= nil)  then
            return I, R  -- center, radius
        end
end

function ld.tetra_len(ab,ac,ad,bc,bd,cd)  -- The arguments are the lengths of the 6 sides.
    local A, B = Origin, ab*vecI
    local alpha = math.acos((ab^2+ac^2-bc^2)/(ab*ac)/2)
    local c = ac*cpx.exp(cpx.I*alpha)
    local C = M(c.re, c.im,0)
    local xd = (ad^2-bd^2+ab^2)/ab/2
    local yd = (C.x^2+C.y^2-2*xd*C.x-cd^2+ad^2)/C.y/2
    local zd = math.sqrt(ad^2-xd^2-yd^2)
    local D = M(xd,yd,zd)
    return A,B,C,D -- returns 4 vertices of tetrahedron, A is at Origin, B is on x-axe, A,B and C are on xy-plane
end

-- 3d triangles

function ld.sss_triangle3d(ab,bc,ac)  -- returns 3 points A,B,C (table), the arguments are the lengths of the 3 sides.
    return map(toPoint3d, ld.sss_triangle(ab,bc,ac)) -- returns triangle with A=Origin and B=ab*vecI and C in xy-plane
end

function ld.sas_triangle3d(ab,gamma,ac)  -- returns 3 points A,B,C (table), the arguments are length, angle (AB,AC) (degrees), length
    return map(toPoint3d, ld.sas_triangle(ab,gamma,ac)) -- returns triangle with A=Origin and B=ab*vecI and C in xy-plane
end

function ld.asa_triangle3d(alpha,ab,beta) -- returns 3 points A,B,C (table), the arguments are a length, angles (AB,AC) and (BA,BC)
    return map(toPoint3d, ld.asa_triangle(alpha,ab,beta)) --- returns triangle with A=Origin and B=ab*vecI and C in xy-plane
end

---- intersections

function ld.interDP(d,P) -- intersection droite/plan
-- intersection droite d={A,u} avec plan P={B,n} dans l'espace
    return ld.proj3dO(d[1],P,d[2])
end

function ld.interPP(P1,P2) -- intersection plan/plan
-- intersection plan P1={A1,n1} avec plan P2={A2,n2} dans l'espace
    local A, u = table.unpack(P1)
    local B, v = table.unpack(P2)
    local w = pt3d.prod(u,v)
    if pt3d.isNul(w) then return end -- plans parallèles
    local M = A + pt3d.dot(B-A,v) / pt3d.dot(w,w)*pt3d.prod(w,u)
    return {M,w}
end

function ld.interDD(D1,D2,eps) -- intersection droite/droite
-- intersection droite D1={A1,u1} et D2={A2,u2}
    local A1, u1 = table.unpack(D1)
    local A2, u2 = table.unpack(D2)
    eps = eps or 1e-10
    local n = pt3d.prod(u1,u2)
    if  math.abs(pt3d.det(A2-A1, u1, u2)) <= eps then 
        return A1 + pt3d.det(A2-A1, u2, n)/pt3d.dot(n,n)*u1
    end
end

function ld.interPS(P,S) -- intersection of plane P={A,n} with sphere S={C,r}, returns nil or a circle
    local C, r = table.unpack(S)
    local I = ld.proj3d(C,P)
    local d, R, u = pt3d.abs(C-I)
    if d > r then return end -- no intersection
    if d < 1e-12 then --d quasi nul
        I = C; R = r --big circle
    else
        R = math.sqrt(r^2-d^2) -- radius
    end
    return I,R,P[2] -- center, radius, normal vector
end

function ld.interSS(S1,S2) -- intersection of sphere S1={C1,R1} and S2={C2,R2}, returns nil or a circle
    local C1,R1 = table.unpack(S1)
    local C2,R2 = table.unpack(S2)
    local d = pt3d.abs(C2-C1)
    if (d > R1+R2) or (d < math.abs(R2-R1)) then return end -- no intersection
    local t = (R2^2-R1^2)/(2*d^2)+1/2
    local I = C2+t*(C1-C2) -- center
    local R = math.sqrt(R1^2-pt3d.abs(I-C1)^2) -- radius
    return I,R,C2-C1 -- center, radius, normal vector
end

function ld.interDS(D,S) -- intersection of line D={A,u} with sphere S={C,r}, returns nil or one or two points
    local C, r = table.unpack(S)
    local I = ld.dproj3d(C,D)
    local d = pt3d.abs(C-I)
    if d > r then return end -- no intersection
    local A,u = table.unpack(D)
    u = pt3d.normalize(u)
    local delta = math.sqrt(r^2-d^2)
    local A1, A2 = I+delta*u, I-delta*u
    if A2 == A1 then return A1
    else return A1, A2
    end
end

function ld.interCS(C,S)
-- intersection of  the circle C={I1,r1,n1} and the sphère S = {O,R)
    local I1, r1, n1 = table.unpack(C)
    local  I2, r2, n2 = ld.interPS({I1,n1},S) -- intersection of the plane of the circle and the sphere
    if I2 == nil then return end -- empty intersection 
    -- in P plane P we calculate  the intersection of two circles
    local d = pt3d.abs(I2-I1)
    if (d > r1+r2) or (d < math.abs(r2-r1)) then return end
    local k = (d^2+r1^2-r2^2)/(r1*d*2)
    if k > 1 then k = 1 elseif k < -1 then k = -1 end
    local alpha = math.acos(k)*ld.rad
    local I3 = I1 + r1*(I2-I1)/d
    local A, B = ld.rotate3d(I3,alpha,{I1,n1}), ld.rotate3d(I3,-alpha,{I1,n1})
    if A == B then return A else return A, B end
end

function ld.interSSS(S1,S2,S3)-- intersection of 3 sphere S1={C1,R1}  S2={C2,R2} and S3= {C3,R3}
    local I1,r1,n1 = ld.interSS(S1,S2) -- nil or a circle
    if I1 == nil then return end
    return ld.interCS({I1,r1,n1},S3)
end


-- couper avec un plan (cut...) ou clipper avec un polyèdre convexe (clip...)

-- couper une ligne polygonale avec un plan
function ld.cutpolyline3d(Lg,plane,close)
-- Lg est une liste de points 3d ou une liste de listes de points 3d  (ligne polygonale 3d)
-- plane est un plan {A,n}
-- la fonction coupe Lg avec le plan, la fonction renvoie :
-- la partie de L située devant le plan (du côté de n), la partie située derrière le plan et les points d'intersection
-- close indique si Lg doit être refermée
    if (type(Lg) ~= "table") or (#Lg == 0) then return end
    local Dev, Der = {}, {}
    local traiter = function(l)
        --l est un liste de points3d et de 'jump" que l'on splitte en composantes connexes
        local res = {} -- résultat
        local cp = {} -- composante courante
        local count = 0 -- nb d'éléments dans cp
        for _,A in ipairs(l) do
            if A ~= "jump" then 
                table.insert(cp,A)
                count = count + 1
            elseif count > 1 then 
                table.insert(res,cp); count = 0; cp = {}
            else count = 0; cp = {}
            end
        end
        if count > 1 then table.insert(res,cp) end
        return ld.merge3d(res)
    end
    
    local A, n = table.unpack(plane)
    close = close or false
    local dev, der, inter = {}, {}, {}
    local AM, pscal, last, v
    local lastPos --0=out 1=on 2=in
    if isPoint3d(Lg[1]) then Lg = {Lg} end
    for _,L in ipairs(Lg) do
        dev, der, inter = {}, {}, {}
        last = nil
        if close then table.insert(L,L[1]) end
        for k,M in ipairs(L) do -- parcours par point
            AM = M-A; pscal = pt3d.dot(AM,n)
            if math.abs(pscal) < 1e-8 then pscal = 0 end -- M est dans le plan
            if pscal == 0 then table.insert(dev,M); table.insert(der,M); table.insert(inter,M); lastPos = "on"
            elseif pscal > 0 then -- M est du bon côté
                if lastPos == "out" then
                    v = last - M
                    res = ld.proj3dO(last,{A,n},v)
                    if res ~= nil then 
                        table.insert(dev,res); table.insert(der,res); table.insert(der,"jump"); table.insert(inter,res) 
                    end
                elseif lastPos == "on" then table.insert(der,"jump")
                end
                lastPos = "in"
                table.insert(dev,M)
            else -- M est du mauvais côté
                if lastPos == "in" then
                    v = last - M
                    res = ld.proj3dO(last,{A,n},v)
                    if res ~= nil then 
                        table.insert(dev,res); table.insert(dev,"jump");table.insert(der,res); table.insert(inter,res) 
                    end
                elseif lastPos == "on" then table.insert(dev,"jump")
                end
                lastPos = "out"
                table.insert(der,M)
            end
            last = M
        end
        if close then table.remove(L) end
        table.append(Dev,traiter(dev)); table.append(Der, traiter(der))
    end
    return Dev, Der, inter
end

--clipper une ligne polygonale avec un polyèdre convexe
function ld.clippolyline3d(L, poly, exterior, close)
-- clippe la ligne polygonale 3d L avec le polyèdre convexe poly (polyèdre)
-- exterior (true/false) indique si on conserve l'extérieur ou pas
-- close indique si L doit être refermée
    exterior = exterior or false
    close = close or false
    local A, B, C, u, L1
    if not exterior then -- on conserve l'intérieur
        for _,facet in ipairs(poly.facets) do
            A = poly.vertices[facet[1]]
            B = poly.vertices[facet[2]]
            C = poly.vertices[facet[3]]
            u = pt3d.prod(B-A,C-A)
            if u ~= nil then
                L = ld.cutpolyline3d(L,{A,-u},close)
            end
        end
        return L
    else -- on conserve l'extérieur
        local rep = {}
        for _,facet in ipairs(poly.facets) do
            A = poly.vertices[facet[1]]
            B = poly.vertices[facet[2]]
            C = poly.vertices[facet[3]]
            u = pt3d.prod(B-A,C-A)
            if u ~= nil then
                L1, L = ld.cutpolyline3d(L,{A,u},close)
                insert(rep,L1)
            end
        end
        return rep
    end
end    

--clipper une droite avec un polyèdre convexe
function ld.clipline3d(line, poly)
-- clippe la droite line avec le polyèdre convexe poly (polyèdre)
-- on renvoie la partie intérieure au polyèdre uniquement
    local A, u = table.unpack(line)
    u = pt3d.normalize(u)
    if u == nil then return end
    local x1,x2,y1,y2,z1,z2 = ld.getbounds3d(poly.vertices)
    local O = M(x1+x2,y1+y2,z1+z2)/2 -- centre de la boite contenant le polyèdre
    local delta = (math.abs(x2-x1)+math.abs(y2-y1)+math.abs(z2-z1))/2
    local O1 = ld.dproj3d(O,line)
    local d = pt3d.N1(O-O1)
    local t = 1+delta+d
    local A, B = O1-t*u, O1+t*u -- deux points de la droite mais hors de la boite
    local L = ld.clippolyline3d({A,B},poly)
    if L ~= nil and (#L == 1) then L = L[1] end
    return L
end

function ld.bezier3d(a,c1,c2,b,nbdots)
--renvoie les points de la courbe de Bézier allant de a à b ayant comme points de contrôle c1 et c2
-- a,c1,c2,b sont des points 3d.
    if (a == nil) or (b == nil) or (c1 == nil) or (c2 == nil) then return end
    local u, v, w = b-3*c2+3*c1-a, 3*a-6*c1+3*c2, 3*c1-3*a
    local p = function(t)
            return a+t*(w+t*(v+t*u))
        end
    return ld.parametric3d(p,0,1,nbdots or ld.bezier_nbdots)
end


function ld.path3d(chemin, nbdots)
-- renvoie les points constituant le chemin (ligne polygonale)
-- celui-ci est une table de points, valeurs et d'instructions ex: {-1,2+i,3,"l", 4, "m", -2*i,-3-3*i,"l","cl",...}
-- "m" pour moveto, "l" pour lineto, "b" pour bézier, "c" pour cercle, "ca" pour arc de cercle, "cl" pour close
    if (chemin == nil) or (type(chemin) ~= "table") or (#chemin < 3) then return end
    local res = {} -- résultat
    local crt = {} -- composante courante
    local aux = {} -- lecture en cours
    local last, first = nil, nil -- dernier lu et premier à venir
    
    local lineto = function() -- traitement du lineto
    -- on met les points dans la composante courante et on vide aux
            for _,z in ipairs(aux) do
                table.insert(crt,z)
            end
            first = last
            aux = {}
    end
    
    local moveto = function() -- traitement du moveto
    -- on démarre une nouvelle composante
            if #crt > 0 then table.insert(res,crt); crt = {} end -- on démarre une nouvelle composante avec le move
            first = nil
            aux = {last}
    end
    
    local close = function() -- traitement du closepath
        -- en principe il y a eu une instruction avant autre que move, aux doit être vide et pas crt
        if (#crt == 0) then return end
        table.insert(crt,crt[1]) -- on ferme la composante courante en ajoutant le premier point
        first = crt[1]
        aux = {}
    end
    
    local Bezier = function()
        -- aux contient une courbe de bézier
        if first ~= nil then 
            table.insert(aux,1,first); table.remove(crt)
        end
        local a,c1,c2,b = table.unpack(aux)
        local C = ld.bezier3d(a,c1,c2,b,nbdots) -- renvoie une liste de listes de complexes
        if C ~= nil then
            for _, z in ipairs(C[1]) do
                table.insert(crt,z)
            end
        end
        first = last
        aux = {}
    end
    
    local Circle = function()
    -- il faut aux = {a,c,v} (un point, le centre et un vecteur normal)
        if first ~= nil then 
            table.insert(aux,1,first); table.remove(crt)
        end
        local a, c, v = table.unpack(aux)
        local C = ld.circle3d(c,pt3d.abs(c-a),v)
        if C ~= nil then
            for _, z in ipairs(C[1]) do
                table.insert(crt,z)
            end
            first = crt[#crt]
        end
        aux = {}
    end
    
    local Arc = function()
        -- il faut aux = {b,a,c,r,sens,v}
        if first ~= nil then 
            table.insert(aux,1,first)
        end
        local C = ld.arc3d(table.unpack(aux))
        if C ~= nil then
            for _, z in ipairs(C[1]) do
                table.insert(crt,z)
            end
            first = crt[#crt]
        end
        aux = {}
    end
    
    local traiter = { ["s"]=Spline, ["l"]=lineto, ["m"]=moveto, ["cl"]=close, ["b"]=Bezier, ["c"]=Circle, ["ca"]=Arc} 
    for _, z in ipairs(chemin) do
        if (type(z) == "number") or isPoint3d(z) then table.insert(aux,z); last = z 
        else
            if type(z) == "string" then traiter[z]() end
        end
    end
    if #crt > 0 then table.insert(res, crt) end    
    if #res > 0 then return  res end
end


function ld.polyline2path3d(L,close) -- conversion list of 3d points or list of lists of 3d points (L) -> path
    if (L==nil) or (type(L) ~= "table") or (#L == 0) then return end
    if (type(L[1]) == "number") or isPoint3d(L[1]) then L = {L} end
    close = close or false
    local ret = {} 
    local aux
    for _, cp in ipairs(L) do
        aux = table.copy(cp)
        table.insert(aux,2,"m") -- move
        table.insert(aux,"l")  -- lineto
        if close then table.insert(aux,"cl") end 
        table.append(ret,aux)
    end
    return ret
end


function ld.split_points_by_visibility(L,visible_function)
--L is a list of 3d points, or a lits of list of 3d points (polygonal line)
-- visible_function is a function : visible_function(A) returns true is the 3d point A is visible.
-- the function retuns a sequence :  visible_points, hidden_points (two tables)
    if (L == nil) or (type(L) ~= "table") then return end
    local Visible, Hidden = {},{}
    local visible, hidden, state,I = {}, {}
    
    if isPoint3d(L[1]) then L = {L} end
    for _, cp in ipairs(L) do
        visible, hidden = {}, {}
        state = 0
        for _, A in ipairs(cp) do
            if visible_function(A) then -- A is visible
                table.insert(visible,A)
                if state == 2 then-- previous dot was not visible
                    I = (A+hidden[#hidden])/2
                    table.insert(visible,1,I)
                    table.insert(hidden,I)
                    table.insert(Hidden, hidden); hidden = {}
                end
                state = 1
            else -- A not visible
                table.insert(hidden,A)
                if state == 1 then -- previous dot was visible
                    I = (A+visible[#visible])/2
                    table.insert(hidden,1,I)
                    table.insert(visible,I)
                    table.insert(Visible, visible); visible = {}
                end
                state = 2
            end
        end
        if #visible > 0 then table.insert(Visible, visible) end
        if #hidden > 0 then  table.insert(Hidden, hidden) end
    end
    Visible = ld.merge3d(Visible); Hidden = ld.merge3d(Hidden)
    return Visible, Hidden
end

-- enveloppe convexe 3d

function ld.cvx_hull3dcoplanar(L,n)
-- cvx_hull3dcoplanar( liste de points 3D coplanaires, vecteur normal): renvoie l'enveloppe convexe plane de la liste de points}
    if (L == nil) or (type(L) ~= "table") or (n == nil) then return end
    local O, O1 = L[1], L[2]
    local I = pt3d.normalize(O1-O)
    local J = pt3d.normalize(pt3d.prod(n,I))
    L = map( function(A) return Z(pt3d.dot(A,I), pt3d.dot(A,J)) end, ld.shift3d(L,-O))
    return map(function(z) return O+z.re*I+z.im*J end, ld.cvx_hull2d(L))
end

local update_edges = function(edges,A1,A2,n)
-- edges = liste de {A1,A2,normal vector}
-- inserts {A1,A2,n} in the list edges without duplicates of A1 A2
    local ok,k,nb = false, 1, #edges
    local rep
    while (k<=nb) and (not ok)  do
        local A, B = edges[k][1], edges[k][2]
        ok = ( (pt3d.abs(A-A1)<1e-10) and (pt3d.abs(B-A2)<1e-10) ) or ( (pt3d.abs(B-A1)<1e-10) and (pt3d.abs(A-A2)<1e-10) )
        if ok then table.remove(edges,k) else k = k+1 end
    end
    if not ok then table.insert(edges,{A1,A2,n}) end
end

function ld.cvx_hull3d(L)
-- L is a list of 3d points
-- returns convex hull of L as a list of facets
    if (L == nil) or (type(L) ~= "table") or (#L < 2) then return end
    if #L == 2 then return {L} end
    local epsilon = 1e-10
    local G = pt3d.isobar3d(L)
    local P1 = L[1]
    local zmin = P1.z
    for _,A in ipairs(L) do
        if A.z < zmin then zmin = A.z; P1 = A end
    end -- P1 point le plus bas
    -- recherche de P2, point le plus proche (?) du plan z=P1.z, l'arête [P1,P2] est dans l'enveloppe
    local cosmin, P2, P3 = 1.001
    for _,A in ipairs(L) do
        local B = A-P1
        local d = pt3d.abs(B)
        if d > epsilon then
            local x = B.z/d
            if (x < cosmin) and (pt3d.abs(pt3d.prod(G-P1,G-A)) > epsilon) then cosmin = x; P2 = A end 
        end
    end
    if P2 == nil then return {L} end -- points alignés
    -- recherche d'une première facette triangulaire de l'enveloppe
    cosmin = 1.001; P3 = nil
    local n1 = pt3d.normalize(pt3d.prod(P2-G,P1-G))
    local k1 = pt3d.normalize(pt3d.prod(n1,P1-P2))
    if pt3d.dot(k1,G-P1) < 0 then k1 = -k1 end
    for _,A in ipairs(L) do
        local B = A-P2
        if pt3d.abs(pt3d.prod(B,A-P1)) > epsilon then -- A ne doit pas être sur (P1,P2)
            local s = pt3d.dot(B,k1)
            local x = s/math.sqrt(pt3d.dot(B,n1)^2+s^2)
            if x < cosmin then cosmin = x; P3 = A end
        end
    end
    if P3 == nil  then return {{P1,P2}} end
    -- recherche de l'enveloppe convexe de tous les points dans le plan de cette première facette
    n1 = pt3d.normalize(pt3d.prod(P2-P3,P1-P3))
    if pt3d.dot(n1,G-P1) > 0 then n1 = -n1 end
    local facette = {}
    for  _, A in ipairs(L) do
        local B = P2-A
        if math.abs(pt3d.dot(B,n1)) < epsilon then table.insert(facette,A) end
    end
    facette = ld.cvx_hull3dcoplanar(facette,n1)
    local rep = {}  --contiendra l'enveloppe convexe
    table.insert(rep, facette) --insertion première facette de l'enveloppe
    -- on initialise la liste des <bords> avec la première facette: { {A1,A2, vecteur normal},...]
    local B = facette[1]
    local nb = #facette
    local bords = {}
    for k = 1, nb do
        local A = B; B = facette[k%nb+1]
        table.insert(bords,{A,B,n1})
    end
    while #bords ~= 0 do
        local Z = bords[1] -- premier bord
        local P1, P2, n1 = table.unpack(Z)
        --recherche d'une facette triangulaire [P1,P2,P3] de l'enveloppe
        local cosmin, P3, k1 = 1.001, nil, pt3d.normalize(pt3d.prod(P1-P2,n1))
        for _, A in ipairs(L) do
            local B = A-P2
            if math.abs(pt3d.dot(B,n1)) > epsilon then --A ne doit pas être dans le plan de la facette
                local s = pt3d.dot(B,k1)
                local x = s/math.sqrt(pt3d.dot(B,n1)^2+s^2)
                if x < cosmin then cosmin = x; P3 = A end
            end
        end
        if P3 == nil  then return rep end
        --recherche de l'enveloppe convexe de tous les points dans le plan de cette facette
        local n2 = pt3d.normalize(pt3d.prod(P2-P3,P1-P3))
        facette = {}
        for  _, A in ipairs(L) do
            local B = P2-A
            if math.abs(pt3d.dot(B,n2)) < epsilon then table.insert(facette,A) end
        end
        facette = ld.cvx_hull3dcoplanar(facette,n2)
        table.insert(rep, facette) -- on ajoute cette facette à l'enveloppe
        -- mise à jour de la liste des bords
        B = facette[1]
        nb = #facette
        for k = 1, nb do
            local A = B; B = facette[k%nb+1]
            update_edges(bords,A,B,n2)
        end
    end
    return rep
end

function ld.curvilinear_param3d(L,close) -- curvilinear parametrization 3d
-- L is a list of 3d points
-- close=true/false, true if L must be closed
-- returns a function f:t -> f(t) with t in [0;1] and f(t) is a point on L, f(0) is the first point, f(1) the last point.
    if (L == nil) or (type(L) ~= "table") or (#L == 0) then return end
    if not isPoint3d(L[1])  then L = L[1] end -- liste de liste de points 3d, on prend la première composante
    close = close or false
    local a, b, n, p = nil, L[1], #L
    local L2, L1, s = {b}, {0}, 0 -- L2=points, L1 = length    
    if close then p = n+1 else p = n end
    for k = 2, p do
        a = b; b = L[(k-1)%n+1]
        s = s + pt3d.abs(b-a) -- curve length from beginning to b
        table.insert(L2,b); table.insert(L1,s)
    end
    local total_len = s
    local l = function(t) -- returns L(t) using linear interpolation with t in [0,1]
        local k = 1
        local n = math.min(#L1,#L2)
        if t == 0 then return L2[1] end -- first point of L
        if t == 1 then return L2[#L2] end -- last point
        t = t*total_len
        while (k<=n) and (L1[k] < t) do k = k+1 end
        if k <=n then
            local u = (t-L1[k-1])/(L1[k]-L1[k-1])
            return L2[k-1]+u*(L2[k]-L2[k-1])
        end
    end
    return l,total_len
end
