-- luadraw_build3d.lua (chargé par luadraw__graph3d)
-- date 2026/06/13
-- version 3.2
-- 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

-- construction d'objets 3d

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


function ld.plane(A,B,C)
-- renvoie leplan passant par A, B et C
    local n = pt3d.prod(B-A,C-A)
    if pt3d.N1(n) > 1e-12 then 
        return {A,n}
    end
end

function ld.orthoframe(P, u) -- returns an orthonormal frame of the plane P={A,n}
    local A, n = table.unpack(P)
    n = pt3d.normalize(n)
    if u ~= nil then u = ld.proj3d(u,{Origin,n}) end
    if (u == nil) or (pt3d.N1(u) < 1e-12) then u = pt3d.prod(n,vecI) end
    if pt3d.N1(u) < 1e-12 then -- u = 0
        u = pt3d.prod(n,vecJ)
    end
    u = pt3d.normalize(u)
    local v = pt3d.prod(n,u)
    return A, u, v
end

function ld.plane2ABC(P) -- returns 3 points of plane P={A,n}
    local A,u,v = ld.orthoframe(P)
    return A, A+u, A+v
end

function ld.planeEq(a,b,c,d)
-- renvoie le plan d'équation ax+by+cz+d=0
    local A, n
    n = M(a,b,c)
    if a ~= 0 then 
        A = M(-d/a,0,0)
    elseif b ~= 0 then
        A = M(0,-d/b,0)
    elseif c ~= 0 then
        A = M(0,0,-d/c)
    else return
    end
    return {A,n}
end

function ld.planeEqn(a,b,c,d) -- old version
    return ld.planeEq(a,b,c,d)
end    

function ld.classify3d(L, n)
-- classe les points 3d de la liste L (supposés coplanaires) pour former une facette orientée par n
-- cette facette est supposée CONVEXE
    if (L == nil) or (type(L) ~= "table")  or (#L == 0) then return end
    local G = pt3d.isobar3d(L)
    local u1 = L[1]
    u1 = pt3d.normalize(u1-G)
    local v = pt3d.normalize(n)
    local u2 = pt3d.prod(v,u1)
    local rep = {}
    for k,A in ipairs(L) do
        local pos = A-G
        local zarg = cpx.arg( Z(pt3d.dot(pos,u1), pt3d.dot(pos,u2)) )
        if zarg ~= nil then table.insert(rep, {zarg, k} ) end
    end
    table.sort(rep, function(e1,e2) return e1[1] < e2[1] end)
    local res = {}
    for _,cp in ipairs(rep) do
        table.insert(res, L[cp[2]])
    end
    return res
end

function ld.getfacet(P,L)
-- renvoie la liste des facettes du polyèdre P = { vertices={sommets}, facets = {facettes avec n° de sommets} }, dont le numéro est dans la liste L
-- si L est un entier alors on renvoie la facette numéro L
-- si L vaut nil, on renvoie toutes les facettes
-- les facettes renvoyées ont des coordonnées 3d
    local n = #P.facets
    if L == nil then L = ld.range(1,n) end
    local rep = {}
    if (type(L) == "number") and (L <= n) then 
        for _,k in ipairs(P.facets[L]) do
            table.insert(rep, P.vertices[k])
        end
        return rep
    end
    for _,i in ipairs(L) do
        if i <= n then
            cp = {}
            for _,k in ipairs(P.facets[i]) do
                table.insert(cp, P.vertices[k])
            end
            table.insert(rep, cp)
        end
    end
    if #rep > 0 then return rep end
end

function ld.facet2plane(L)
-- L est une facette ou une liste de facettes
-- renvoie le plan contenant chaque facette
    local rep
    local afacet = function(f)
        local A, B, C = f[1], f[2], f[3]
        return {A,pt3d.prod(B-A,C-A)}
    end
    if isPoint3d(L[1]) then rep = afacet(L) 
    else
        rep = {}
        for _,f in ipairs(L) do
            table.insert(rep, afacet(f))
        end
    end
    return rep
end

function ld.plane2rectangle(P,V,L1,L2) -- or plane2rectangle(P,L1,L2)
-- P = {A, u} is a plane
-- V is a vector in the plane P
-- L1 and L2 are length
-- returns the plane as a rectangular facet
    local A, u = table.unpack(P)
    if (V ~= nil) and (type(V) ~= "number") then
        u = pt3d.normalize(u)
        V = pt3d.normalize(V)
        local W = pt3d.prod(u,V)
        V = L1*V
        W = L2*W
        local Dep = A-W/2-V/2
        return {Dep, Dep+V, Dep+V+W, Dep+W}
    else 
        L2 = L1; L1 = V
        if L1 == nil then L1 = 5 end
        if L2 == nil then L2 = L1 end
        local F = {M(0,-L1/2,-L2/2), M(0,L1/2,-L2/2), M(0,L1/2,L2/2), M(0,-L1/2,L2/2)}
        return ld.shift3d( ld.rotateaxe3d( F, M(1,0,0), u), A)
    end
end
    
function ld.poly2facet(P) -- conversion polyèdre -> liste facettes 3d
-- cette commande prend en entrée un polyèdre P = { vertices={sommets}, facets = {facettes avec n° de sommets} }
-- La fonction renvoie une liste de facettes avec coordonnées 3d
-- les facettes sont orientées par l'ordre d'apparition des sommets.
    local rep = {}
    local F
    for _,facet in ipairs(P.facets) do
        F = {}
        for _,k in ipairs(facet) do
            table.insert(F, P.vertices[k])
        end
        table.insert(rep,F)
    end
    return rep
end

function ld.facet2poly(facetlist,eps)
-- facetlist est une liste de facettes avec coordonnées 3d
-- la fonction renvoie un polyèdre P = { vertices={sommets}, facets = {facettes avec n° de sommets} }
    local poly = {}
    eps = eps or 1e-8
    poly.vertices = {}; poly.facets = {}
    for _, facet in ipairs(facetlist) do
        local aux = {}
        for _, A in ipairs(facet) do
            table.insert(aux, pt3d.insert3d(poly.vertices,A,eps))
        end
        table.insert(poly.facets,aux)
    end
    return poly
end

function ld.reverse_face_orientation(F)
-- F est une facette, une liste de facettes, ou un polyèdre
    if (F == nil) or (type(F) ~= "table") then return end
    local rep = {}
    if isPoint3d(F[1]) then -- une facette
        return ld.reverse(F)
    elseif (F.vertices == nil) then -- liste de facettes
        for _,f in ipairs(F) do 
            table.insert(rep, ld.reverse(f))
        end
        return rep
    else -- polyèdre
        rep.vertices = F.vertices
        rep.facets = map(ld.reverse,F.facets)
        return rep
    end    
end

function ld.splitseg(F,plane)
-- cette fonction coupe le segment F (points 3d) avec le plan plane = {A,n}
-- la fonction renvoie la partie devant (segment), la partie derrière (segment),
    local S,n = table.unpack(plane)
    local dev, der = {}, {}
    local A, B = table.unpack(F)
    local p1, p2, I = pt3d.dot(A-S,n), pt3d.dot(B-S,n)
    if math.abs(p2)<1e-8 then p2 = 0 end
    if math.abs(p1)<1e-8 then -- A sur la facette
        if (p2 == 0) then -- B sur la facette
            dev = F
        elseif p2 > 0 then -- B du bon coté
            dev = F
        else -- B du mauvais côté
            der = F
        end
    elseif p1 > 0 then -- A du bon côté
        if (p2 == 0) then --  B sur la facette
            dev = F
        elseif p2 > 0 then -- B du bon coté
            dev = F
        else -- B du mauvais côté
            I = ld.proj3dO(A,plane,B-A)
            der = {I,B}
            dev = {A,I}
        end
    else -- A du mauvais côté
        if (p2 == 0) then --  B sur la facette
            der = F
        elseif p2 < 0 then -- B du mauvais coté
            der = F
        else -- B du bon côté
            I = ld.proj3dO(A,plane,B-A)
            dev = {I,B}
            der = {A,I}
        end
    end
    return dev, der --sec -- on renvoie le 
end

function ld.splitfacet(F,plane)
-- cette fonction coupe la facette F (points 3d) avec le plan plane = {S,n}
-- la fonction renvoie la partie devant (facette), la partie derrière (facette)
    local S,n = table.unpack(plane)
    local nb, dev, der = #F, {}, {}
    local A1, B1 = nil, F[1]
    local p1, p2, I = nil, pt3d.dot(B1-S,n)
    if math.abs(p2) < 1e-8 then p2 = 0 end
    for k = 2, nb+1 do
        if k == nb+1 then k = 1 end -- on ferme la facette
        A1 = B1; p1 = p2; B1 = F[k]; p2 = pt3d.dot(B1-S,n)
        if math.abs(p2) < 1e-8 then p2 = 0 end
        if (p1*p2 < 0) or (p2 == 0) then
            if p2 == 0 then I = B1 else I = ld.proj3dO(A1,plane,B1-A1) end
            if I ~= nil then 
                table.insert(dev,I) ; table.insert(der,I)
            end
        end
        if (p2 > 0) and (p1 ~= nil) then table.insert(dev,B1) end
        if (p2 < 0) and (p1 ~= nil) then table.insert(der,B1) end
    end
    --local sec = classify3d(coupe,-n)
    if #der < 3 then der = {} end
    if #dev < 3 then dev = {} end
    return dev, der --sec -- on renvoie le devant et le derrière
end

function ld.clipplane(plane,P)
--cette fonction clippe le plan avec le polyèdre P et renvoie la facette obtenue
    local S,n = table.unpack(plane)
    if (S == nil) or (n == nil) then return end
    local coupe = {} 
    for _,F in ipairs(P.facets) do
        local nb = #F
        local A1, B1 = nil, P.vertices[F[1]]
        local p1, p2, I = nil, pt3d.dot(B1-S,n)
        if math.abs(p2)<1e-8 then p2 = 0 end
        for k = 2, nb+1 do
            if k == nb+1 then k = 1 end -- on ferme la facette
            A1 = B1; p1 = p2; B1 = P.vertices[F[k]]; p2 = pt3d.dot(B1-S,n)
            if math.abs(p2) < 1e-8 then p2 = 0 end
            if (p1*p2 < 0) or (p2 == 0) then
                if p2 == 0 then I = B1 else I = ld.proj3dO(A1,plane,B1-A1) end
                if I ~= nil then 
                    pt3d.insert3d(coupe,I,1e-10)
                end
            end
        end
    end
    return ld.classify3d(coupe,n) -- on renvoie la section (facette orientée par n) 
end

function ld.cutpoly(P,plane,close)
-- cette fonction coupe le polyèdre P = { vertices={sommets}, facets = {facettes avec n° de sommets} }
-- avec le plan plane = {A,n}
-- la partie contenant n est conservée 
-- si close vaut true le polyèdre est refermé avec la section, sinon il est ouvert
-- la fonction renvoie la partie conservée (polyèdre), la partie non conservée (polyèdre), et la section (liste de point3d orientée par -n)
    local S,n = table.unpack(plane)
    close = close or false
    local res, res2, coupe = {}, {}, {} -- res = sortie, res2 = autre partie du polyèdre coupé
    for _,F in ipairs(P.facets) do
        local nb, aux, aux2 = #F, {}, {}
        local A1, B1 = nil, P.vertices[F[1]]
        local p1, p2, I = nil, pt3d.dot(B1-S,n)
        if math.abs(p2)<1e-8 then p2 = 0 end
        for k = 2, nb+1 do
            if k == nb+1 then k = 1 end -- on ferme la facette
            A1 = B1; p1 = p2; B1 = P.vertices[F[k]]; p2 = pt3d.dot(B1-S,n)
            if math.abs(p2) < 1e-8 then p2 = 0 end
            if (p1*p2 < 0) or (p2 == 0) then
                if p2 == 0 then I = B1 else I = ld.proj3dO(A1,plane,B1-A1) end
                if I ~= nil then 
                    table.insert(aux,I) ; table.insert(aux2,I)
                    pt3d.insert3d(coupe,I,1e-10)
                end
            end
            if (p2 > 0) and (p1 ~= nil) then table.insert(aux,B1) end
            if (p2 < 0) and (p1 ~= nil) then table.insert(aux2,B1) end
        end
        if #aux>2 then table.insert(res,aux) end
        if #aux2>2 then table.insert(res2,aux2) end
    end
    local sec = ld.classify3d(coupe,-n)
    if (#coupe > 2) and close then
        table.insert(res,sec)
        table.insert(res2,ld.reverse(sec))
    end
    return ld.facet2poly(res), ld.facet2poly(res2), sec -- on renvoie des polyèdres et la section (facette orientée par -n) 
end

function ld.cutfacet(L,plane,close)
-- cette fonction coupe les facettes de L (L est une facette ou une liste de facettes)
-- avec le plan plane = {A,n}
-- la partie contenant n est conservée 
-- si close vaut true la section est ajoutée, sinon il est ouvert
-- la fonction renvoie la partie conservée (facettes), la partie non conservée (facettes), et la section (liste de point3d orientée par -n)
    if isPoint3d(L[1]) then L = {L} end -- pour avoir une liste de facettes
    if L.vertices ~= nil then -- polyèdre
        L = ld.poly2facet(L)
    end
    local S,n = table.unpack(plane)
    close = close or false
    local res, res2, coupe = {}, {}, {} -- res = sortie, res2 = autre partie du polyèdre coupé
    for _,F in ipairs(L) do
        local nb, aux, aux2 = #F, {}, {}
        local A1, B1 = nil, F[1]
        local p1, p2, I = nil, pt3d.dot(B1-S,n)
        if math.abs(p2)<1e-8 then p2 = 0 end
        for k = 2, nb+1 do
            if k == nb+1 then k = 1 end -- on ferme la facette
            A1 = B1; p1 = p2; B1 = F[k]; p2 = pt3d.dot(B1-S,n)
            if math.abs(p2) < 1e-8 then p2 = 0 end
            if (p1*p2 < 0) or (p2 == 0) then
                if p2 == 0 then I = B1 else I = ld.proj3dO(A1,plane,B1-A1) end
                if I ~= nil then 
                    table.insert(aux,I) ; table.insert(aux2,I)
                    pt3d.insert3d(coupe,I,1e-10)
                end
            end
            if (p2 > 0) and (p1 ~= nil) then table.insert(aux,B1) end
            if (p2 < 0) and (p1 ~= nil) then table.insert(aux2,B1) end
        end
        if #aux>2 then table.insert(res,aux) end
        if #aux2>2 then table.insert(res2,aux2) end
    end
    local sec = ld.classify3d(coupe,-n)
    if (#coupe > 2) and close then
        table.insert(res,sec)
        table.insert(res2,ld.reverse(sec))
    end
    return res, res2, sec -- on renvoie des listes de facettes et la section (facette orientée par -n) 
end

local clipfacetfacet = function(S, poly, exterior)
-- clippe la liste de facettes S avec le polyèdre convexe poly (sous forme d'une liste de facettes)
-- exterior (true/false) indique si on conserve l'extérieur ou pas
    exterior = exterior or false
    local A, B, C, u, S1
    if not exterior then -- on conserve l'intérieur
        for _,facet in ipairs(poly) do
            A = facet[1]
            B = facet[2]
            C = facet[3]
            u = pt3d.prod(B-A,C-A)
            if u ~= nil then
                S = ld.cutfacet(S,{A,-u})
            end
        end
        return S
    else -- on conserve l'extérieur
        local rep = {}
        for _,facet in ipairs(poly) do
            A = facet[1]
            B = facet[2]
            C = facet[3]
            u = pt3d.prod(B-A,C-A)
            if u ~= nil then
                S1, S = ld.cutfacet(S,{A,u})
                insert(rep,S1)
            end
        end
        return rep
    end
end    

function ld.clip3d(S, poly, exterior)
-- clippe la liste de facettes S avec le polyèdre convexe poly (polyèdre)
-- exterior (true/false) indique si on conserve l'extérieur ou pas
-- renvoie une liste de facettes
    if S.vertices ~= nil then --S est sous forme de polyèdre
        S = ld.poly2facet(S)
    end
    if poly.vertices == nil then -- poly est une liste de facettes
        return clipfacetfacet(S,poly,exterior)
    end
    exterior = exterior or false
    local A, B, C, u, S1
    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
                S = ld.cutfacet(S,{A,-u})
            end
        end
        return S
    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
                S1, S = ld.cutfacet(S,{A,u})
                ld.insert(rep,S1)
            end
        end
        return rep
    end
end    

function ld.facetedges(F)
-- F est une liste de facettes avec point3d ou bien un polyedre
-- la fonction renvoie la liste des arêtes (ligne polygonale 3d)
    if (F == nil) or (type(F) ~= "table") then return end
    if pt3d.isPoint3d(F[1]) then F = {F} end
    local P = F
    if P.vertices == nil then P = ld.facet2poly(F) end
    local rep = {}
    local a, b, ab, n
    for _,facet in ipairs(P.facets) do -- parcours du polyèdre par facette
        b = facet[1]; n = #facet
        for k = 2, n+1 do
            if k == n+1 then k = 1 end
            a = b; b = facet[k]
            if a > b then 
                ab = a..";"..b -- pour servir de clé
            else ab = b..";"..a
            end
            rep[ab] = {a,b}
        end
    end
    local aux = {}
    for _,aret in pairs(rep) do
        table.insert(aux,{P.vertices[aret[1]], P.vertices[aret[2]]})
    end
    return aux  -- liste de segments 3d
end

function ld.facetvertices(F)
-- F est une liste de facettes avec point3d ou bien un polyèdre
-- la fonction renvoie la liste des sommets (liste de points 3d)
    if (F == nil) or (type(F) ~= "table") or (#F == 0) then return end
    if F.vertices ~= nil then return table.copy(F.vertices) end
    if isPoint3d(F[1]) then F = {F} end
    local S = {}
    eps = eps or 1e-8
    for _, facet in ipairs(F) do
        for _, A in ipairs(facet) do
            pt3d.insert3d(S,A,eps) -- insertion sans répétition
        end
    end
    return S
end

function ld.border(P)
-- P est un polyèdre ou une liste de facettes
-- la fonction renvoie une ligne polygonale 3d
-- qui correspond au bord de P, c'est à dire les arêtes appartenant à une seule face.
    if (P == nil) or (type(P) ~= "table") then return end
    if P.vertices == nil then P = ld.facet2poly(P) end -- si P est une liste de facettes
    local rep = {}
    local inserer = function(a,b) -- arete = {a,b} deux entiers n° de sommets
        if a > b then a, b = b, a end
        local ab = a..";"..b -- pour servir de clé
        if rep[ab] ~= nil then rep[ab][2] = rep[ab][2] + 1
        else
            rep[ab] = {{a,b},1} 
        end
    end
    local facet, a, b
    for _,facet in ipairs(P.facets) do -- parcours du polyèdre par facette
        b = facet[1]
        for k = 2, #facet do
            a = b
            b = facet[k]
            inserer(a,b)
        end
        inserer(b,facet[1])
    end
    local aux = {}
    for _,aret in pairs(rep) do
        a, b = table.unpack(aret[1]) -- n° des sommets formant l'arête
        if aret[2] == 1 then -- arête du bord
            table.insert(aux,{a,b})
        end
    end
    aux = ld.merge(aux)
    rep = {}
    for _,F in ipairs(aux) do
        local cp = {}
        for _, k in ipairs(F) do
            table.insert(cp, P.vertices[k]) -- conversion en point 3d
        end
        table.insert(rep,cp)
    end
    return rep  -- ligne polygonale 3d
end


-- fonctions revoyant un polyèdre

function ld.tetra(S,v1,v2,v3)
-- construit un tétraèdre de sommet S et des trois vecteurs v1, v2, v3 supposés dans le sens direct
-- la fonction renvoie une liste de sommets (point3d) suivie d'une liste de facettes avec les numéros des sommets
   return { ["vertices"] = {S,S+v1,S+v2,S+v3}, ["facets"] = {{1,3,2},{1,2,4},{2,3,4},{1,4,3}} }
end   

function ld.parallelep(A,v1,v2,v3)
-- construit un parallélépipède à partir d'un sommet A et de 3 vecteurs, supposés dans le sens direct
    local B, C, D, E, F, G, H 
    B = A+v1; C = B+v2; D = A+v2; E = A+v3; F = E+v1; G = F+v2; H = E+v2
    return { ["vertices"]={A,B,C,D,E,F,G,H}, ["facets"]={{1,4,3,2},{5,6,7,8},{1,2,6,5},{8,7,3,4},{1,5,8,4},{6,2,3,7}} }
end

function ld.prism(base, vector, open)
-- construit un prisme, base est liste de point3d, vector est un vecteur 3d de translation
-- open est un booléen indiquant si le prisme est ouvert ou non, false par défaut
-- la base doit être orientée par le vector
    open = open or false
    local n = #base -- nombre de sommets
    local P = {}
    P.vertices = table.copy(base)
    for k = 1, n do
        table.insert(P.vertices,base[k]+vector)
    end
    P.facets = {}
    local aux
    if not open then
        aux = {}
        for k = n, 1, -1 do table.insert(aux,k) end
        table.insert(P.facets, aux)
        aux = {}
        for k = 1, n do table.insert(aux, n+k) end
        table.insert(P.facets, aux)
    end
    aux = {}
    for k = 1, n-1 do 
        table.insert(P.facets,{k,k+1,k+n+1,k+n})
    end
    table.insert(P.facets,{n,1,n+1,2*n})
    return P
end

function ld.pyramid(base,vertex,open)
-- construit une pyramide, base est liste de point3d, vertex est le sommet (point3d)
-- open est un booléen indiquant si la base est ouverte ou non, false par défaut
-- la base doit être orientée par le sommet
    local n = #base
    open = open or false
    local P = {}
    local aux
    P.vertices = table.copy(base); table.insert(P.vertices, vertex)
    P.facets = {}
    if not open then
        aux = {}
        for k = n, 1, -1 do table.insert(aux,k) end
        table.insert(P.facets,aux)
    end 
    for k = 1, n-1 do 
        table.insert(P.facets,{k,k+1,n+1})
    end
    table.insert(P.facets,{n,1,n+1})
    return P
end

function ld.truncated_pyramid(base,vertex,height,open)
-- construit une pyramide tronquée, base est liste de point3d, vertex est le sommet (point3d)
-- height indique la hauteur partant de la base
-- open est un booléen indiquant si la base est ouverte ou non, false par défaut
-- la base doit être orientée par le sommet
    local Pyr = ld.pyramid(base,vertex,open)
    local Pb = ld.facet2plane(base)
    local n = pt3d.normalize(Pb[2])
    local A = ld.proj3d(vertex,Pb)
    local B = A+height*n
    return ld.cutpoly(Pyr,{B,-n},not open)
end

function ld.regular_pyramid(n,side,height,open,center,axe)
-- pyramide régulière
-- n = nombre de côtés, side = longueur d'un côté, center= centre de la base, axe = vecteur directeur de l'axe
-- open est un booléen indiquant si la base est ouverte ou non, false par défaut
    open = open or false
    center = center or Origin
    axe = axe or vecK
    axe = pt3d.normalize(axe)
    local X = side/(2*math.sin(math.pi/n))
    local base = ld.polyreg(0,X,n) -- regular n-sided 2d polygon 
    local A, u, v = center, vecI, vecJ
    if axe ~= vecK then 
        A, u, v = ld.orthoframe({center,axe})
    end
    base = map( function(z) return A+z.re*u+z.im*v end, base) -- conversion 2d -> 3d 
    local S = height*axe
    if (base ~= nil) and (S ~= nil) then return ld.pyramid(base,S,open) end
end

function ld.cylinder(A,V,R,nbfacet,open,aux) -- ou cylinder(C,R,A,nbfacet,open) ou cylinder(C,R,V,A,nbfacet,open)
-- construit un cylindre de rayon R, d'axe {A,V} (V vecteur 3d non nul)
-- nbfacet vaut 35 par défaut
-- open=true/false vaut false par défaut
    local B
    if type(V) == "number" then -- format 2 ou format 3
        if (nbfacet == nil) or (type(nbfacet) == "number") then -- format 2
            local r = V -- radius
            V = A-R -- sommet - centre
            A = R; R = r
            B = A+V
        else -- format 3
            local r = V -- radius
            B = A -- center
            V = R -- vecteur normal à la base
            A = nbfacet -- sommet
            if pt3d.dot(V,B-A) < 0 then V = -V end
            nbfacet = open
            open = aux
            R = r
        end
    else B = A+V
    end
    local vect = pt3d.normalize(V)
    nbfacet = nbfacet or 35
    open = open or false
    local P = {}
    P. vertices= {}; P.facets = {}
    local u, pas = 0, 2*math.pi/nbfacet
    local v1 = pt3d.prod(vecK,vect)
    if pt3d.isNul(v1) then v1 = R*vecI else v1 = R*pt3d.normalize(v1) end
    local v2 = pt3d.prod(vect,v1)
    for k1 = 1, nbfacet do
        table.insert(P.vertices,A+math.cos(u)*v1+math.sin(u)*v2); u = u+pas
    end
    u = 0
    for k1 = 1, nbfacet do
        table.insert(P.vertices, B+math.cos(u)*v1+math.sin(u)*v2); u = u+pas
    end
    if not open then
        local aux = {}
        for k2 = nbfacet, 1, -1 do table.insert(aux, k2) end; table.insert(P.facets,aux)
        aux = {}
        for k2 = nbfacet+1, 2*nbfacet do table.insert(aux, k2) end; table.insert(P.facets,aux)
    end
    for k2 = 1, nbfacet-1 do table.insert(P.facets, {k2,k2+1,k2+1+nbfacet,k2+nbfacet}) end
    table.insert(P.facets, {nbfacet,1,1+nbfacet,2*nbfacet})
    return P
end


function ld.cone(A,V,R,nbfacet,open,aux) -- ou cone(C,R,A,nbfacet,open) ou cone(C,R,V,A,nbfacet,open)
-- construit un cône de rayon A, base en A+V et de rayon R à la base (V vecteur 3d non nul)
-- nbfacet vaut 35 par défaut
-- open=true/false vaut false par défaut
    local B
    if type(V) == "number" then -- format 2 ou format 3
        if (nbfacet == nil) or (type(nbfacet) == "number") then -- format 2
            local r = V -- radius
            V = A-R -- sommet - centre
            A = R; R = r
            B = A+V
        else -- format 3
            local r = V -- radius
            B = A -- center
            V = R -- vecteur normal à la base
            A = nbfacet -- sommet
            if pt3d.dot(V,B-A) < 0 then V = -V end
            nbfacet = open
            open = aux
            R = r
        end
    else B = A+V
    end
    local vect = pt3d.normalize(V)
    nbfacet = nbfacet or 35
    nbfacet = nbfacet+1
    open = open or false
    local P = {}
    P.vertices= {A}; P.facets = {}
    local u, pas = 0, 2*math.pi/(nbfacet-1)
    local v1 = pt3d.prod(vecK,vect)
    if pt3d.isNul(v1) then v1 = R*vecI else v1 = R*pt3d.normalize(v1) end
    local v2 = pt3d.prod(vect,v1)
    for k1 = 1, nbfacet-1 do
        table.insert(P.vertices,B+math.cos(u)*v1+math.sin(u)*v2); u = u+pas
    end
    if not open then
        local aux = {}
        for k2 = 2, nbfacet do table.insert(aux, k2) end; table.insert(P.facets,aux)
    end
    for k2 = 2, nbfacet-1 do table.insert(P.facets, {k2+1,k2,1}) end
    table.insert(P.facets, {2,nbfacet,1})
    return P    
end

function ld.frustum(C,R,r,V,A,nb,open) -- ou frustum(C,R,r,V,nb,open), frustum build with facets (tronc de cône droit ou penché)
    if type(A) == "number" then -- syntaxe C,V,R,nb,open
        open = nb; nb = A; A = nil
    elseif isPoint3d(A) then V = ld.dproj3d(A,{C,V}) - C -- frustum penché
    end
    nb = nb or 35
    open = open or false
    if R == r then -- cylinder
        if A == nil then return ld.cylinder(C,V,R,nb,open)
        else return ld.cylinder(C,V,R,A,nb,open)
        end
    end
    local k = R/(R-r)
    local H = k*V
    local Co
    if A == nil then Co = ld.cone(C,R,C+H,nb,open) --cone(C+H,-H,R,nb,open)
    else 
        local S = k*(A-r/R*C)
        Co = ld.cone(C,R,V,S,nb,open)
    end
    local P = {C+V,-V}
    local rep = ld.cutpoly(Co, P, not open)
    return rep
end

function ld.sphere(A,R,nbu,nbv)
-- construit une sphère de rayon A de rayonR
--  nbu est le nb de fuseaux, et nbv le nb de tranches
    nbu = nbu or 36
    nbv = nbv or 20
    nbu = nbu+1; nbv = nbv+1
    local pasU, pasV = 2*math.pi/(nbu-1), math.pi/(nbv-1)
    local rep = {}
    rep.vertices, rep.facets = {A+R*vecK}, {}
    -- calcul des sommets
    local u, v, a, b
    v = pasV
    for k1 = 1, nbv-2 do
        u = 0; a = R*math.cos(v); b = R*math.sin(v)
        for k2 = 1, nbu do
            table.insert(rep.vertices, A+M(b*math.cos(u),b*math.sin(u),a))
            u = u + pasU
        end
        v = v + pasV
    end
    table.insert(rep.vertices,A-R*vecK)
    -- calcul des faces
    local dep, last
    for k2 = 2, nbu do 
        table.insert(rep.facets,{k2,k2+1,1})
    end
    for k2 = 2, nbv-2 do
        dep = 2+nbu*(k2-1)
        for k1 = dep, dep+nbu-2 do
            table.insert(rep.facets,{k1,k1+1,k1+1-nbu,k1-nbu})
        end
    end
    dep = 2+nbu*(nbv-3); last = nbu*(nbv-2)+2
    for k2 = dep, dep+nbu-2 do 
        table.insert(rep.facets, {k2,last,k2+1})
    end
    return rep
end    


-- fonctions renvoyant une liste de facettes
local eval_uvmesh = function(F, uvmesh)
-- F =(u,v) -> F(u,v) in R^3
-- uvmesh = {{u1,u2,...u,N}, {v1,v2,...,vM}}
    local S = {}
    for _,u in ipairs(uvmesh[1]) do
        local aux = {}
        for _,v in ipairs(uvmesh[2]) do
            table.insert(aux,F(u,v))
        end
        table.insert(S,aux)
    end
    return S
end

function ld.surface(f,u1,u2,v1,v2,grid) -- or surface(f, uvmesh) with uvmesh = {uvalues, vvalues}
-- renvoie les facettes (point 3d) représentant la surface paramétrée par (u,v) -> f(u,v) dans R^3
-- u1 et u2 sont les bornes pour u, et v1, v2 pour v
-- grid={nbu,nbv} donne le nombre de points suivant u et suivant v
    local F = function(u,v)
        local R = ld.evalf(f,u,v) -- protected evaluation
        if (R == nil) then return cpx.Jump
        else return R
        end
    end
    local different = function(A,B)
        return pt3d.N1(B-A)>1e-10
    end
    local uvmesh, nbu, nbv
    if type(u1) ~= "number" then 
        uvmesh = u1
        nbu, nbv = #uvmesh[1], #uvmesh[2]
    else
        grid = grid or {25,25}
        nbu, nbv = table.unpack(grid)
        uvmesh = {ld.linspace(u1,u2,nbu), ld.linspace(v1,v2,nbv)}
    end
    local S = eval_uvmesh(F, uvmesh)
    local rep = {}
    local A, last
    for i = 1, nbu-1 do
        for j = 1, nbv-1 do
            aux = {}
            A = S[i][j]; first = A; last = A
            if A ~= cpx.Jump then table.insert(aux,A) end
            A = S[i+1][j]
            if (A ~= cpx.Jump) and ((last == cpx.Jump) or different(A,last)) then table.insert(aux,A); last = A end
            A = S[i+1][j+1]
            if (A ~= cpx.Jump) and ((last == cpx.Jump) or different(A,last)) then table.insert(aux,A); last = A end
            A = S[i][j+1]
            if (A ~= cpx.Jump) and ((last == cpx.Jump) or different(A,last)) and ((first == cpx.Jump) or different(A,first)) then table.insert(aux,A) end
            if #aux > 2 then table.insert(rep,aux) end
        end
    end
    return rep
end

function ld.cartesian3d(f,x1,x2,y1,y2,grid,addWall)
-- cartesian surface z=f(x,y) with (x,y) in [x1,x2]x[y1,y2]
-- default grid = {25,25} 
-- addWall = 0 (none), "x", "y", "xy" (partition walls for Dscene3d)
    grid = grid or {25,25}
    addWall = addWall or 0
    local F = function(u,v)
        return M(u,v,f(u,v))
    end
    local rep = ld.surface(F,x1,x2,y1,y2,grid)
    local u1,u2,v1,v2,w1,w2, cube 
    if addWall ~= 0 then
        u1,u2,v1,v2,w1,w2 = ld.getbounds3d(rep)
        cube = ld.parallelep(M(u1,v1,w1),(u2-u1)*vecI,(v2-v1)*vecJ,(w2-w1)*vecK)
    end
    local wall = {}
    if string.find(addWall,"x") ~= nil then
        local x, xpas = x1, (x2-x1)/(grid[1]-1)
        for k = 1, grid[1] do
            local clp = ld.clipplane({x*vecI,vecI},cube)
            if clp ~= nil then table.insert(wall, clp) end 
            x = x+xpas
        end
    end
    if string.find(addWall,"y") ~= nil then
        local y, ypas = y1, (y2-y1)/(grid[2]-1)
        for k = 1, grid[2] do
            local clp = ld.clipplane({y*vecJ,vecJ},cube)
            if clp ~= nil then table.insert(wall, clp) end
            y = y+ypas
        end
    end
    return rep, wall
end

function ld.cylindrical_surface(r,z,u1,u2,v1,v2,grid,addWall)
-- functions r:(u,v) -> r(u,v) and z:(u,v) -> z(u,v)
-- cylindrical surface parametrized by Mc(r(u,v),v,z(u,v)) with (u,v) in [u1,u2]x[v1,v2]
-- default grid = {25,25} 
-- addWall = 0 (none), "v" (planes containing vecK and cos(v)*vecI+sin(v)*vecJ, partition walls for Dscene3d), or "z" (planes z=cte, useful when z does not depend on v), or "vz"
    grid = grid or {25,25}
    addWall = addWall or 0
    local F = function(u,v)
        return Mc(r(v,u),u,z(v,u))
    end
    local rep = ld.surface(F,v1,v2,u1,u2,ld.reverse(grid))
    local wall = {}
    if addWall ~= 0 then
        local x1,x2,y1,y2,z1,z2 = ld.getbounds3d(rep)
        local cube = ld.parallelep(M(x1,y1,z1),(x2-x1)*vecI,(y2-y1)*vecJ,(z2-z1)*vecK)
        if string.find(addWall,"z") ~= nil then
            local u, upas = u1, (u2-u1)/(grid[1]-1)
            for k = 1, grid[1] do
                local clp = ld.clipplane({z(u,v1)*vecK,vecK},cube)
                if clp ~= nil then table.insert(wall, clp) end
                u = u+upas
            end
        end        
        if string.find(addWall,"v") ~= nil then
            local v, vpas = v1, (v2-v1)/(grid[2]-1)
            --for k = 1, grid[2] do
            while v < math.min(v2,v1+math.pi) do
                local clp = ld.clipplane({Origin,-math.sin(v)*vecI+math.cos(v)*vecJ},cube)
                if clp ~= nil then table.insert(wall, clp) end
                v = v+vpas
            end

        end
    end
    return rep, wall
end


function ld.curve2cone(f,t1,t2,S,args)
-- construit un cône de sommet S sur une courbe paramétrée par f sur l'intervalle [t1,t2]
-- args est une table à 4 champs: { nbdots = 15, nbdiv = 0, ratio = 0, obj=false }
-- nbdots est le nombre de points minimal.
-- ratio est un nombre qui est le ratio d'homothétie pour construire l'autre partie du cône
-- la fonction renvoie une liste de facettes et les bords du cône (ligne polygonale 3d)
    args = args or {}
    local nbdots = args.nbdots or 15
    local ratio = args.ratio or 0
    local nbdiv = args.nbdiv or 0
    local obj = args.obj or false
    local base = ld.parametric3d(f,t1,t2,nbdots,false,nbdiv)[1] -- première composante connexe de la courbe
    local nb = #base -- nombre de points
    local cone = {}
    local bords = {}
    table.insert(bords,table.copy(base))
    if ratio ~= 0 then -- il y a une autre partie
        ld.insert(base,ld.concat({S}, ld.scale3d(base,ratio,S))) -- on ajoute le sommet S et les images par l'homothétie de centre S
        table.insert(bords, ld.scale3d(bords[1],ratio,S))
    else
        table.insert(base,S)
    end
    cone.vertices = base
    cone.facets = {}
    for k = 1, nb-1 do 
        table.insert(cone.facets, {k,k+1,nb+1})
    end
    if ratio ~= 0 then
        for k = nb+2, 2*nb do 
            table.insert(cone.facets,{k+1,k,nb+1})
        end
    end
    if obj then
        return ld.facet2obj(cone), bords
    else
        return ld.poly2facet(cone), bords
    end
end


function ld.curve2cylinder(f,t1,t2,V,args)
-- construit un cylindre sur une courbe paramétrée par f sur l'intervalle [t1,t2] translatée avec V
-- args est une table à 3 champs: { nbdots = 15, nbdiv = 0, obj=false}
-- nbdots est le nombre de points minimal.
-- la fonction renvoie une liste de facettes et les bords du cylindre (ligne polygonale 3d)
    args = args or {}
    local nbdots = args.nbdots or 15
    local nbdiv = args.nbdiv or 0
    local obj = args.obj or false
    local base = ld.parametric3d(f,t1,t2,nbdots,false,nbdiv)[1] -- première composante connexe de la courbe
    local nb = #base -- nombre de points
    local cyl = {}
    local bords = {}
    table.insert(bords,table.copy(base))
    ld.insert(base,ld.shift3d(base,V)) -- on ajoute les images par la translation de vecteur V
    table.insert(bords,ld.shift3d(bords[1],V))
    cyl.vertices = base
    cyl.facets = {}
    for k = 1, nb-1 do 
        table.insert(cyl.facets, {k,k+1,k+nb+1,k+nb})
    end
    if obj then
        return ld.facet2obj(cyl), bords
    else
        return ld.poly2facet(cyl), bords
    end
end


function ld.section2tube(section,L,args)
-- section est une facette 3d qui doit être centrée sur le premier point de L
-- L est une liste de points 3d 
-- la fonction renvoie un "tube" centré sur L 
-- args est une table à 4 champs:
-- close=true/false indique si la ligne doit être refermée
-- hollow=true/false indique si le tube a ses extrémités ouvertes (true) ou fermées
-- obj = true/false format obj ou  format facettes
-- addwall=0 (ou 1) permet d'ajouter (pour Dscene3d) des séparations (murs) entre chaque "tronçon" du tube
    if (L == nil) or (type(L) ~= "table") then return end
    
    args = args or {}
    local nbfacet = #section
    local close = args.close or false
    local hollow = args.hollow or false
    local obj = args.obj or false
    if close then hollow = true end
    local addwall = args.addwall or 0
    local poly, sep = {}, {}
    poly.vertices = {}
    poly.facets = {}
    -- orientation de la section
    local a, b, c = L[1], L[2]
    local v = pt3d.prod(section[2]-section[1],section[3]-section[1])
    if pt3d.dot(b-a,v) < 0 then section = ld.reverse(section) end
    
    local num = function(num_section, index)
        return (num_section-1)*nbfacet + (index-1)%nbfacet+1
    end
    -- construction des sections
    local cp = table.copy(L)-- cp is modified
    local nb_sections = 0
    if #cp > 1 then
        local last, crt_section, aux_section, P
        if pt3d.abs(cp[1]-cp[#cp])<1e-8 then table.remove(cp); close = true end
        if close then last = cp[#cp]; table.insert(cp,cp[1]) end
        a = nil; b = nil; c = nil
        for _,m in ipairs(cp) do
            a = b; b = c; c = m
            if a == nil then 
                if b ~= nil then -- première section au début de cp
                    crt_section = section
                    if close then
                        P = {b, pt3d.normalize(last-b)-pt3d.normalize(c-b)} --plan bissecteur last/b/c en b
                        aux_section = ld.proj3dO(crt_section,P,b-c)
                        if aux_section ~= nil then 
                            crt_section = aux_section
                        end
                    end
                    if (not close) and (addwall == 1) then table.insert(sep,crt_section) end
                    ld.insert(poly.vertices, crt_section); nb_sections= nb_sections+1
                    if not hollow then 
                        table.insert(poly.facets, ld.range(nbfacet,1,-1))
                    end
                end
            else -- on a trois points consécutifs
                P = {b, pt3d.normalize(a-b)-pt3d.normalize(c-b)} --plan bissecteur
                aux_section = ld.proj3dO(crt_section,P,b-a) --projection de la liste courante sur le plan P
                if aux_section == nil then 
                    aux_section = ld.shift3d(crt_section,b-a)
                end
                ld.insert(poly.vertices, aux_section); nb_sections = nb_sections+1
                crt_section = aux_section -- list actuelle
                if addwall == 1 then table.insert(sep,aux_section) end
            end
        end
        if not close then --dernière section
            P = {c,b-c} -- plan de la dernière section
            aux_section = ld.proj3dO(crt_section,P,b-c)
            if aux_section == nil then 
                aux_section = ld.shift3d(crt_section,c-b)
            end
            crt_section = aux_section
            ld.insert(poly.vertices, crt_section); nb_sections= nb_sections+1
            if addwall == 1 then table.insert(sep,crt_section) end -- facette séparatrice
        end
    end
    -- construction des facettes
    for s = 1, nb_sections-1 do
        for k = 1, nbfacet do
            table.insert(poly.facets,  {num(s,k), num(s,k+1), num(s+1,k+1), num(s+1,k)})
        end
    end
    if close then
        for k = 1, nbfacet do
            table.insert(poly.facets, {num(nb_sections,k), num(nb_sections,k+1), num(1,k+1), num(1,k)})
        end
    end
    if not hollow then
       table.insert(poly.facets,  map(function(k) return num(nb_sections,k) end, ld.range(1,nbfacet)))
    end
    if #sep == 0 then sep = nil end
    if obj then
        return ld.facet2obj(poly), sep
    else
        return ld.poly2facet(poly), sep
    end 
end    


function ld.line2tube(L,r,args)
-- L est une liste de points 3d 
-- la fonction renvoie un tube centré sur L (liste de facettes)
-- args est une table à 4 champs:
-- nbfacet = 4 par défaut
-- close = true/false indique si la ligne doit être refermée
-- hollow = true/false indique si le tube a ses extrémités ouvertes (true) ou fermées
-- obj = true/false format obj ou  non
-- addwall = 0 (ou 1) permet d'ajouter (pour Dscene3d) des séparations (murs) entre chaque "tronçon" du tube
    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
    args = args or {}
    local nbfacet = args.nbfacet or 3
    local close = args.close or false
    local hollow = args.hollow or false
    if close then hollow = true end
    local obj = args.obj or false
    local addwall = args.addwall or 0
   
    local cp2section = function(cp) -- traitement d'une composante connexe
        local b, c = cp[1], cp[2]
        local u = pt3d.prod(b-c,vecI)
        if pt3d.isNul(u) then u = pt3d.prod(b-c,vecJ) end
        u = r*pt3d.normalize(u)
        local b1, v, theta = b+u, c-b, 2*math.pi/nbfacet*ld.rad
        local list = {b1}
        for k = 1, nbfacet-1 do
            table.insert(list, ld.rotate3d(b1,k*theta,{b,v}))
        end
        return list
    end
    
    if #L > 1 then
        return ld.section2tube( cp2section(L), L, {hollow = hollow, close = close, addwall = addwall, obj = obj})
    end
end

function ld.rotcurve(p,t1,t2,axe,angle1,angle2,args)
-- renvoie la surface (liste de facettes) balayée par la courbe de la 
-- fonction t ->p(t) sur l'intervalle [t1,t2] en la faisant tourner autour de axe = {point3d, vecteur 3d}
-- d'un angle allant de angle1 (degrés) à angle2
-- args est une table à 3 champs :
-- grid = {25,25} : subdivision pour t et pour l'angle.
-- obj = true/false sortie au format obj ou non
--addwall=0/1/2 permet d'ajouter (pour Dscene3d) des séparations (murs) entre chaque "couche" de facettes (valeur 1) ou chaque "tranche" de rotatation (valeur 2, lorsque L est dans un même plan que l'axe de rotation)
    angle1 = angle1 or -180
    angle2 = angle2 or 180
    args = args or {}
    local grid = args.grid or {25,25}
    local obj = args.obj or false
    local addwall = args.addwall or 0
    --axe[2] = -axe[2]
    local f = function(u,v)
        return ld.rotate3d(p(u),v,axe)
    end
    local sep
    if addwall == 1 then 
        sep = {}
        local t, nb, V = t1, grid[1], axe[2]
        local pas = (t2-t1)/(nb-1)
        for k = 1, nb do
            table.insert(sep, {p(t),V})
            t = t+pas
        end
    elseif addwall == 2 then
        sep = {}
        local A, B, nb = axe[1], p((t1+t2)/2), grid[2]
        local t, dt = angle1, (angle2-angle1)/(nb-1)
        local B1 = ld.rotate3d(B,t,axe)
        for k = 1, nb do
            table.insert(sep, {A,A+axe[2],B1}) -- facette séparatrice
            B1 = ld.rotate3d(B,t,axe)
            t = t+dt
        end
    end
    if obj then 
        return ld.obj_surface(f,t1,t2,angle1,angle2,grid), sep
    else 
        return ld.surface(f,t1,t2,angle1,angle2,grid), sep
    end
end

function ld.rotline(L,axe,angle1,angle2,args)
-- renvoie la surface (liste de facettes) balayée par la liste de points 3d L
-- en la faisant tourner autour de axe = {point3d, vecteur 3d}
-- d'un angle allant de angle1 (degrés) à angle2
-- agrs est une table à 3 champs :
-- nbdots est le nombre de subdivisions pour l'angle
-- close qui indique si la ligne doit être refermée
-- obj = true/false sortie au format obj ou non
--addwall=0/1/2 permet d'ajouter (pour Dscene3d) des séparations (murs) entre chaque "couche" de facettes (valeur 1) ou chaque "tranche" de rotatation (valeur 2, lorsque L est dans un même plan que l'axe de rotation)
    if (L == nil) or (type(L) ~= "table") or (not isPoint3d(L[1])) then return end
    args = args or {}
    local nb = args.nbdots or 25
    --axe[2] = -axe[2]
    angle1 = angle1 or -180
    angle2 = angle2 or 180
    local close = args.close or false
    local obj = args.obj or false
    local addwall = args.addwall or 0
    local L1 = table.copy(L)
    if close then table.insert(L1,L[1]) end

    local f = function(u,v)
        return ld.rotate3d(L1[u],v,axe)
    end
    local n = #L1
    local sep
    if addwall == 1 then 
        sep = {}
        local V = axe[2]
        for k = 1, n do
            table.insert(sep, {L1[k],V})
        end
    elseif addwall == 2 then
        sep = {}
        local A, B = axe[1], L[2]
        local t, dt = angle1, (angle2-angle1)/(nb-1)
        local B1 = ld.rotate3d(B,t,axe)
        for k = 1, nb do
            table.insert(sep, {A,A+axe[2],B1}) -- facette séparatrice
            B1 = ld.rotate3d(B,t,axe)
            t = t+dt
        end
    end
    if obj then 
        return ld.obj_surface(f,1,n,angle1,angle2,{n,nb}), sep
    else 
        return ld.surface(f,1,n,angle1,angle2,{n,nb}), sep
    end    
end


 --------------- obj -------------------------
 
function ld.read_obj_file(file, triangulate) -- Contribution de Christophe BAL 2025/09/02
-- prototype::
--    file : le chemin d'un fichier ext::''OBJ'' au format \wavefront
--           simplifié (non gestion des textures, ni des normales).
--
--    :return: ''polyhedron,{xmin, xmax, ymin, ymax, zmin, zmax}'' où
--             les valeurs extrémales correspondent à celles obtenues
--             en analysant le fichier, et le polyèdre est une version
--             \luadraw du modèle 3D indiqué par le fichier.
------
    triangulate = triangulate or false

    local update_extreme_vals = function (a, a_min, a_max)
    -- sert à déterminer la bounding box du polyèdre
        return math.min(a, a_min), math.max(a, a_max)
    end

    local polyhedron = {}
    local vertices   = {}
    local facets     = {}
    local normals    = {}
    local xmin, xmax = math.huge, -math.huge
    local ymin, ymax = math.huge, -math.huge
    local zmin, zmax = math.huge, -math.huge

    local pattern_decimal = "([%-%d%.e%+%-]+)"
    local pattern_vertex  = "^v%s+" .. pattern_decimal ..
                              "%s+" .. pattern_decimal .. 
                              "%s+" .. pattern_decimal

    -- Dans les \regexs \lua, ''%'' est le caractère d'échappement.
    for line in io.lines(file) do
        -- Nettoyage des espaces finaux et initiaux : en \lua, ''-''
        -- est un caractère spécial pour une recherche non gourmande.
        line = line:match("^%s*(.-)%s*$")
        -- On ignore les lignes vides et les commentaires.
        if line ~= "" and not line:match("^#") then
        -- Cas d'un sommet.
            if line:match("^v%s") then
                -- La \regex suivante est très fragile, mais nous faisons 
                -- confiance aux fichiers ext::''obj'' utilisés.
                local x, y, z = line:match(pattern_vertex)
                --local x, y, z = line:match("^v%s+([%-%d%.]+)%s+([%-%d%.]+)%s+([%-%d%.]+)")
                if x and y and z then
                  x, y, z = tonumber(x), tonumber(y), tonumber(z)
                -- Gestion des valeurs extrémales.
                  xmin, xmax = update_extreme_vals(x, xmin, xmax)
                  ymin, ymax = update_extreme_vals(y, ymin, ymax)
                  zmin, zmax = update_extreme_vals(z, zmin, zmax)
                -- Un nouveau sommet.
                  table.insert(vertices, M(x, y, z))
                end
            -- Cas d'une face.
            elseif line:match("^f%s") then
                local face = {}
                for idx in line:gmatch("(%d+)[^ ]*") do
                  table.insert(face, tonumber(idx))
                end
                if #face > 0 then
                  table.insert(facets, face)
                end
            end
        end
    end
    polyhedron.vertices = vertices
    if triangulate then -- triangulate facets and calculate normals
        local Tfacets = {}
        for _, f in ipairs(facets) do --
            if #f == 3 then table.insert(Tfacets,f)
            else
                local a, c, b = f[1], f[2]
                for k = 3, #f do
                    b = c; c = f[k]
                    table.insert(Tfacets,{a,b,c})
                end
            end
        end
        for k = 1, #vertices do table.insert(normals,Origin) end
        for _,f in ipairs(Tfacets) do
            local a, b, c = table.unpack(f)
            local A,B,C = vertices[a], vertices[b], vertices[c]
            local N = pt3d.normalize( pt3d.prod(B-A,C-A) )
            normals[a] = normals[a] + N; normals[b] = normals[b] + N; normals[c] = normals[c] + N
        end
        for k = 1, #normals do
            normals[k] = pt3d.normalize( normals[k] )
        end
        polyhedron.normals = normals
        polyhedron.facets   = Tfacets
    else
        polyhedron.facets   = facets
    end
    return polyhedron, {xmin, xmax, ymin, ymax, zmin, zmax}  -- polyhedron first
end

function ld.facet2obj(F)
    -- F : list of facets (with 3D points) or polyhedron
    -- returns { vertices={}, facets={}, normals={} }
    if F.vertices == nil then F = facet2poly(F) end
    local res = {}
    res.vertices = table.copy(F.vertices)
    local Tfacets = {}
    local normals = {}
    for _, f in ipairs(F.facets) do --
        if #f == 3 then table.insert(Tfacets,f)
        else
            local a, c, b = f[1], f[2]
            for k = 3, #f do
                b = c; c = f[k]
                table.insert(Tfacets,{a,b,c})
            end
        end
    end
    for k = 1, #F.vertices do table.insert(normals,Origin) end
    for _,f in ipairs(Tfacets) do
        local a, b, c = table.unpack(f)
        local A,B,C = F.vertices[a], F.vertices[b], F.vertices[c]
        local N = pt3d.normalize( pt3d.prod(B-A,C-A) )
        normals[a] = normals[a] + N; normals[b] = normals[b] + N; normals[c] = normals[c] + N
    end
    for k = 1, #normals do
        normals[k] = pt3d.normalize( normals[k] )
    end
    res.normals = normals
    res.facets = Tfacets
    return res
end    


function ld.obj_surface(f,u1,u2,v1,v2,grid) -- or obj_surface(f, uvmesh) with uvmesh= {uvalues, vvalues}
-- surface paramétrée par (u,v) -> f(u,v) dans R^3,  à facettes triangulaires au format obj
-- u1 et u2 sont les bornes pour u, et v1, v2 pour v
-- grid={nbu,nbv} donne le nombre de points suivant u et suivant v
-- renvoie une table à 3champs {vertices = {sommets (3D}, normals = {vecteurs (3D)}, facets = { {1,2,3},...} }
-- les facettes sont triangulaires.
    --grid = grid or {25,25}
    local F = function(u,v)
        local R = ld.evalf(f,u,v) -- protected evaluation
        if (R == nil) then return cpx.Jump
        else return R
        end
    end

    local different = function(A,B)
        return pt3d.N1(B-A)>1e-10
    end
    
    local S, vertices, normals, posvertices = {}, {}, {}, {}
    local posvertex = function(i,j)
        local rep
        if posvertices[i][j] == 0 then
            table.insert(vertices, S[i][j]); rep = #vertices
            table.insert(normals, Origin)
            posvertices[i][j] = rep
        else rep = posvertices[i][j]
        end
        return rep
    end
    local uvmesh, nbu, nbv
    if type(u1) ~= "number" then 
        uvmesh = u1
        nbu, nbv = #uvmesh[1], #uvmesh[2]
    else
        grid = grid or {25,25}
        nbu, nbv = table.unpack(grid)
        uvmesh = {ld.linspace(u1,u2,nbu), ld.linspace(v1,v2,nbv)}
    end
    for _,u in ipairs(uvmesh[1]) do
        local aux, auxN = {}, {}
        for _,v in ipairs(uvmesh[2]) do
            table.insert(aux,F(u,v))
            table.insert(auxN, 0)
        end
        table.insert(S,aux); table.insert(posvertices,auxN)
    end
    local rep = {}
    local A, last
    for i = 1, nbu-1 do
        for j = 1, nbv-1 do
            aux = {}
            A = S[i][j]; first = A; last = A
            if A ~= cpx.Jump then table.insert(aux,A); table.insert(aux,posvertex(i,j)) end
            A = S[i+1][j]; 
            if (A ~= cpx.Jump) and ((last == cpx.Jump) or different(A,last)) then 
                table.insert(aux,A); table.insert(aux,posvertex(i+1,j)); last = A 
            end
            A = S[i+1][j+1]
            if (A ~= cpx.Jump) and ((last == cpx.Jump) or different(A,last)) then 
                table.insert(aux,A); table.insert(aux,posvertex(i+1,j+1)); last = A 
            end
            A = S[i][j+1]
            if (A ~= cpx.Jump) and ((last == cpx.Jump) or different(A,last)) and ((first == cpx.Jump) or different(A,first)) then table.insert(aux,A) table.insert(aux,posvertex(i,j+1)) end
            local n = #aux
            if n > 5 then -- ajout + triangulation
                if n == 6 then table.insert(rep,aux) -- triangle 
                else --quad {a,b,c,d}
                    table.insert(rep,{aux[1],aux[2],aux[3],aux[4],aux[5],aux[6]}) -- triangle {a,b,c}
                    table.insert(rep,{aux[1],aux[2],aux[5],aux[6],aux[7],aux[8]}) -- triangle {a,c,d}
                end
            end
        end
    end
    -- calculs des normales
    for _,f in ipairs(rep) do
        local N = pt3d.normalize( pt3d.prod(f[3]-f[1], f[5]-f[1]) )
        for k = 2,6,2 do
            local pos = f[k]
            normals[pos] = normals[pos]  + N
        end
    end
    -- normalisation
    local result, facet = {}
    result.vertices = vertices; result.facets = {}
    for _,f in ipairs(rep) do
        facet = {}
        for k = 2,6,2 do
            local pos = f[k]
            normals[pos] = pt3d.normalize( normals[pos] )
            table.insert(facet, pos)
        end
        table.insert(result.facets, facet)
    end
    result.normals = normals
    return result
end
