-- luadraw_curves.lua (chargé par luadraw__calc)
-- 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 Zp = cpx.Zp
local toComplex = cpx.toComplex
local isComplex = cpx.isComplex

ld.bezier_nbdots = 12 -- Minimum number of points calculated when converting a Bézier curve into a polygonal line.

function ld.parametric(p,t1,t2,nbdots,discont,nbdiv) --new version, experimantal
-- le paramétrage p est une fonction : t (réel) -> p(t) (cpx)
    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 = math.abs(pas)*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
    middle = function(t1,t2,f1,f2,n,d12) -- nouvelle fonction récursive middle, expérimentale
        if (n > niveau) then 
            if saut then closeCp() end -- fermer la composante connexe
        else
            local tm = (t1 + t2) / 2  -- dichotomie
            local fm = p(tm)
            if (fm ~= nil) then
                if (f1 ~= nil) and (f2 ~= nil) then
                    if d12 == nil then d12 = cpx.abs(f2-f1) end
                    local d1m, dm2 = cpx.abs(f1-fm), cpx.abs(f2-fm)
                    if (d12>seuil) or (d1m+dm2>1.0005*d12) -- d(f1,f2)>seuil ou d(f1,fm)+d(fm,2)>1.0005*d(f1,f2)
                    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
        z = ld.evalf(p,t)
        if (z == nil) or ld.notDef(z.re) or ld.notDef(z.im) then z = nil end
        if (z ~= nil) then
            if (prec ~= nil) or ( (prec == nil) and (t > dep) ) then -- and test(tPrec,t,prec,z)
                middle(tPrec,t,prec,z,1,nil)
            end
            addCp(z)
        else -- z = nil
            if (prec ~= nil) then middle(tPrec,t,prec,z,1,nil) 
            else closeCp()
            end
        end
        tPrec = t
        prec = z
        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


function ld.polar(rho,t1,t2,nbdots,discont,nbdiv) -- le paramétrage rho(t) renvoie un réel
    local p = function(t)
        return Zp(rho(t),t) 
    end
    return ld.parametric(p,t1,t2,nbdots,discont,nbdiv)
end   

function ld.cartesian(f,x1,x2,nbdots,discont,nbdiv)
-- f doit être une fonction x (réel) -> f(x) (réel)
    local p = function(x)
        return Z( x, f(x) ) 
    end
    return ld.parametric(p,x1,x2,nbdots,discont,nbdiv)
end

 
function ld.periodic(f,period,x1,x2,nbdots,discont,nbdiv)
-- courbe d'une fonction f périodique de période period={a,b}
-- f doit être une fonction x (réel) -> f(x) (réel)
    local p = function(x)
        return Z( x, f(x) )
    end
    local tmin, tmax = period[1], period[2]
    local T = tmax - tmin
    local C = ld.parametric(p,tmin,tmax,nbdots,discont,nbdiv)
    if C == nil then return end
    local k1, k2 = math.floor( (x1-tmin)/T ), math.floor( (x2-tmax)/T )+1
    local rep = {}
    for k = k1, k2 do
        for _,cp in ipairs(C) do
            local aux = {}
            for _,z in ipairs(cp) do
                table.insert(aux, z+k*T)
            end
            table.insert(rep, aux)
        end
    end
    rep = ld.cutpolyline(rep,{x1,-cpx.I}); rep = ld.cutpolyline(rep,{x2,cpx.I})
    return rep
end


function ld.affinebypiece(def,discont)
-- courbe d'une fonction affine par morceaux
-- def est une table permettant la définition de la fonction def = { {x1,x2,x3,...,xn}, {{a1,b1}, {a2,b2},...} }
--{x1,x2,...,xn} forme une subdivision de l'intervalle [x1,xn] où est calculée la courbe
-- sur l'intervalle [x1,x2] l'équation est y=a1*x+b1, sur [x2,x3] c'est y=a2*x+b2, etc
    if discont == nil then discont = true end -- true par défaut
    local sub, val = def[1], def[2]
    local n = #sub -1
    local res, crt, last = {}, {}, nil
    for k = 1, n do
        local x1, x2 = sub[k], sub[k+1]
        local a, b = table.unpack(val[k])
        local y1, y2 = a*x1+b, a*x2+b
        if last == y1 then -- continuité en x1 
            table.insert(crt, Z(x2,y2))
        else 
            if #crt > 0 then  
                if discont then table.insert(res,crt)
                else table.append(res,crt)
                end
                crt = {} 
            end
            table.insert(crt, Z(x1,y1))
            table.insert(crt, Z(x2,y2))
        end
        last = y2
    end
    if #crt > 0 then  
        if discont then table.insert(res,crt)
        else table.append(res,crt)
        end
    end    
    if #res > 0 then return res end
end

function ld.stepfunction(def,discont)
-- courbe d'une fonction en escalier
-- def est une table permettant la définition de la fonction def = { {x1,x2,x3,...,xn}, {c1,c2,...} }
--{x1,x2,...,xn} forme une subdivision de l'intervalle [x1,xn] où est calculée la courbe
-- sur l'intervalle [x1,x2] la fonction vaut c1, sur [x2,x3] c'est c2, etc
    if discont == nil then discont = true end -- true par défaut
    local sub, val, last = def[1], def[2], nil
    local n = #sub -1
    local res, crt = {}, {}
    for k = 1, n do
        local a, b, c = sub[k], sub[k+1], val[k] 
        if last == c then -- continuité en x1 
            table.insert(crt, Z(b,c))
        else 
            if #crt > 0 then  
                if discont then table.insert(res,crt)
                else table.append(res,crt)
                end
                crt = {} 
            end
            table.insert(crt, Z(a,c))
            table.insert(crt, Z(b,c))
        end
        last = c
    end
    if #crt > 0 then  
        if discont then table.insert(res,crt)
        else table.append(res,crt)
        end
    end    
    if #res > 0 then return res end
end

function ld.implicit(f,x1,x2,y1,y2,grid)
-- renvoie une liste de segments constituant la courbe implicite d'équation f(x,y)=0 dans le pavé [x1,x2]X[y1,y2]
-- grid = {n1, n2} ce qui signifie que l'intervalle [x1,x2] est découpé en n1 morceaux, et l'intervalle [y1,y2] en n2 morceaux.
    local tolerance = 1e-6
    
    local dist = function(a,b)
            return cpx.abs(b-a)
        end
    
    local dicho = function(a,b) -- a,b sont deux tables { Z(x,y),f(x,y)}  retourne "none" ou un point
            local z1, z2, f1, f2 = a[1], b[1], a[2], b[2]
            if f1*f2 <= 0 then
                for _ = 1, 10 do --while cpx.abs(z2-z1) >= tolerance do
                    local z3 = (z1+z2)/2
                    local fm = f(z3.re,z3.im)
                    if f1*fm <= 0 then  z2 = z3 else z1 = z3; f1=fm end
                end
                return (z1+z2)/2
            end
            return "none"
        end
    
    local  N, P = grid[2], grid[1]    
    local xpas, ypas = (x2-x1)/P, (y2-y1)/N
    local Grid = {} -- grille de points { Z(xi,yj), f(xi,yj) }
    local y = y1    
    for _ = 1, N+1 do
        local ligne = {}
        local x = x1
        for _ = 1, P+1 do
            table.insert(ligne, {Z(x,y), f(x,y)} )
            x = x+xpas
        end
        table.insert(Grid, ligne)
        y = y+ypas
    end
    local AretesV = {}
    for I = 1, N do
        local cp = {}
        for J = 1, P+1 do
            table.insert(cp, dicho( Grid[I][J], Grid[I+1][J]) )  -- arête verticale
        end
        table.insert(AretesV, cp)
    end
    local AretesH = {}
    for I = 1, N+1 do
        local cp = {}
        for J = 1, P do
            table.insert(cp, dicho( Grid[I][J], Grid[I][J+1]) )  -- arête horizontale
        end
        table.insert(AretesH, cp)        
    end
    local rep = {}
    for I = 1,N do
        for J = 1, P do
            local m1 = AretesV[I][J] -- point sur arête verticale gauche de la maille
            local m2 = AretesH[I][J] --point sur arête horizontale basse de la maille
            local m3 = AretesV[I][J+1] --point sur arête verticale droite de la maille
            local m4 = AretesH[I+1][J] --point sur arête horizontale haute de la maille
            local m5 = true --(Grid[I][J+1][2]*Grid[I+1][J][2] <=0 ) -- test changement de signe sur diag descendante
            local m6 = true --(Grid[I][J][2]*Grid[I+1][J+1][2] <= 0)
            if m1 ~= "none" then
                if (m2 ~= "none") and m6 then
                    table.insert( rep, {m1,m2} )
                else if (m3 ~= "none") and ((m5) or (m6)) then
                        table.insert( rep, {m1,m3} )
                    else if (m4 ~= "none") and m5 then
                            table.insert( rep, {m1,m4} )
                         end
                    end
                end
            end
            if m2 ~= "none" then
                if (m3 ~= "none") and m5 then
                    table.insert( rep, {m3,m2} )
                else    if (m4 ~= "none") and ((m5) or (m6)) then
                            table.insert( rep, {m4,m2} )
                        end
                end
            end
            if (m3 ~= "none") then
                if (m4 ~= "none") and m6 then
                   table.insert( rep, {m3,m4} )
                end
            end
        end
    end
    if #rep > 0 then return ld.merge(rep) end -- on renvoie le résultat en recollant les segments avec merge
end


function ld.odeRK4(f,t0,Y0,tmin,tmax,nbdots)
-- résolution dans l'intervalle [tmin,tmax] (contenant t0) de Y'(t)=f(t,Y(t)) où f: (t,Y) -> f(t,Y) dans R^n avec Y(t)={y1(t),...,yn(t)} (liste de réels)
--t0 et Y0 donnent les conditions initiales, nbdots le nombre de points calculés (50 par défaut)
-- la fonction renvoie une liste { {tmin,...,tmax}, {y1(tmin),..., y1(tmax)}, ..., {yn(tmin),..., yn(tmax)} }
-- méthode Runge Kutta 4
    local calcError = false
    local ode_f
    if type(Y0) == "number" then -- dimension 1
        ode_f = function(t,Y) 
            if Y == nil then calclError = true; return 
            else return {f(t,Y[1])} end
        end
        Y0 = {Y0}
    else
        ode_f = function(t,Y) 
            if Y == nil then calclError = true; return 
            else return f(t,Y) end
        end
    end
    local add = function(Y1,Y2) -- addition dans R^n
            if calcError then return end
            if (Y1 == nil) or (Y2 == nil) then calcError = true; return end
            local rep = {}
            for k,y in ipairs(Y1) do
                local r = y+Y2[k]
                if ld.notDef(r) then calcError = true; return end
                table.insert(rep,r)
            end
            return rep
        end
    local mul = function(k,Y) -- produit par un scalaire
            local rep = {}
            if calcError then return end
            if (Y == nil) then calcError = true; return end
            for _,y in ipairs(Y) do
                local r = k*y
                if ld.notDef(r) then calcError = true; return end
                table.insert(rep,r)
            end
            return rep
        end
    local h = (tmax-tmin)/(nbdots-1) 
    local t, Y, n, stop = t0, Y0, #Y0, (t0 >= tmax)
    local rep = { {t0} }
    for k = 1, n do
        table.insert(rep,{Y0[k]})
    end
    while not stop do
        local k1 = ode_f(t, Y)
        t = t + h/2 
        local k2 = ode_f(t, add(Y, mul(h/2,k1)) )
        local k3 = ode_f(t, add(Y, mul(h/2,k2)) ) 
        t = t + h/2
        local k4 = ode_f(t, add(Y, mul(h,k3)) ) 
        Y = add(Y, mul(h/6, add(k1, add( mul(2,k2), add( mul(2,k3),k4) ) ) ) )
        if not calcError then 
            table.insert( rep[1], t)
            for k = 1, n do
                table.insert(rep[k+1],Y[k])
            end
        end
        if (t < tmax) and (t+h > tmax) then h = tmax-t end
        stop = (t+h > tmax) or calcError
    end
    calcError = false
    t = t0; Y = Y0;  h = (tmin-tmax)/(nbdots-1); stop = (t0 <= tmin) 
    while not stop do
        local k1 = ode_f(t, Y)
        t = t + h/2 
        local k2 = ode_f(t, add(Y, mul(h/2,k1)) )
        local k3 = ode_f(t, add(Y, mul(h/2,k2)) ) 
        t = t + h/2
        local k4 = ode_f(t, add(Y, mul(h,k3)) ) 
        Y = add(Y, mul(h/6, add(k1, add( mul(2,k2), add( mul(2,k3),k4) ) ) ) ) 
        if not calcError then 
            table.insert(rep[1],1,t)
            for k = 1, n do
                table.insert(rep[k+1],1,Y[k])
            end
        end
        if (t>tmin) and (t+h < tmin) then h = tmin-t end
        stop = (t+h < tmin) or calcError
    end
    return rep
end

function ld.odeRkf45(f,t0,Y0,tmin,tmax,nbdots)
-- résolution dans l'intervalle [tmin,tmax] (contenant t0) de Y'(t)=f(t,Y(t)) où f: (t,Y) -> f(t,Y) dans R^n avec Y(t)={y1(t),...,yn(t)} (liste de réels)
--t0 et Y0 donnent les conditions initiales, nbdots le nombre de points calculés (50 par défaut)
-- la fonction renvoie une liste { {tmin,...,tmax}, {y1(tmin),..., y1(tmax)}, ..., {yn(tmin),..., yn(tmax)} }
-- méthode de Runge-Kutta-Fehlberg (pas variable)
    local calcError = false
    local ode_f
    if type(Y0) == "number" then -- dimension 1
        ode_f = function(t,Y) 
            if Y == nil then calclError = true; return 
            else return {f(t,Y[1])} end
        end
        Y0 = {Y0}
    else
        ode_f = function(t,Y) 
            if Y == nil then calclError = true; return 
            else return f(t,Y) end
        end
    end
    
    local add = function(Y1,Y2) -- addition dans R^n
            if calcError then return end
            if (Y1 == nil) or (Y2 == nil) then calcError = true; return end
            local rep = {}
            for k,y in ipairs(Y1) do
                local r = y+Y2[k]
                if ld.notDef(r) then calcError = true; return end
                table.insert(rep,r)
            end
            return rep
        end
    local mul = function(k,Y) -- produit par un scalaire
            local rep = {}
            if calcError then return end
            if (Y == nil) then calcError = true; return end
            for _,y in ipairs(Y) do
                local r = k*y
                if ld.notDef(r) then calcError = true; return end
                table.insert(rep,r)
            end
            return rep
        end
        
    local Nmax, hMax = 5*nbdots, (tmax-tmin)/(nbdots-1) 
    local hMin = (tmax-tmin)/(Nmax-1) 
    local t, Y, n, stop, err = t0, Y0, #Y0, (t0 >= tmax)
    local Nmax1, Nmax2 = math.floor((tmax-t0)/hMin), math.floor((t0-tmin)/hMin)
    local h, eps, N = hMax, 5e-7, 0
    local rep = { {t0} }
    for k = 1, n do
        table.insert(rep,{Y0[k]})
    end
    local nextY = function()
        local k1 = mul(h, ode_f(t, Y))
        local k2 = mul(h, ode_f(t+0.25*h, add(Y, mul(1/4,k1))))
        local k3 = mul(h, ode_f(t+0.375*h, add(Y, mul(3/32, add(k1,mul(3,k2))))))
        local k4 = mul(h, ode_f(t+12*h/13, add(Y, mul(1/2197, add( mul(1932,k1), add(mul(-7200,k2),mul(7296,k3)))))))         
        local k5 = mul(h, ode_f(t+h, add(Y, add(mul(439/216,k1), add(mul(-8,k2), add(mul(3680/513,k3),mul(-845/4104,k4)))))))
        local k6 = mul(h, ode_f(t+h/2, add(Y, add(mul(-8/27,k1), add(mul(2,k2), add(mul(-3544/2565,k3), add(mul(1859/4104,k4),mul(-11/40,k5))))))))
        Y = add(Y, add(mul(25/216,k1), add(mul(1408/2565,k3), add(mul(2197/4104,k4),mul(-1/5,k5)))))
        err = add(mul(1/360,k1), add(mul(-128/4275,k3), add(mul(-2197/75240,k4), add(mul(1/50,k5),mul(2/55,k6)))))
    end
    -- côté droit de t0
    while not stop do
        nextY()
        t = t+h; N = N+1
        if not calcError then 
            table.insert( rep[1], t)
            for k = 1, n do
                table.insert(rep[k+1],Y[k])
            end
            local d = 0
            for _,x in ipairs(err) do
                if math.abs(x) > d then d = math.abs(x) end
            end
            d = 2*d
            local s = 0
            if d>0 then s = (eps*h/d)^0.25 end
            if s < 0.1 then s = 0.1
            elseif s > 4 then s = 4
            end
            h = s*h
            if h < hMin then h = hMin 
            elseif h > hMax then h = hMax
            end
        end
        if (t < tmax) and (t+h > tmax) then h = tmax-t end
        stop = (t+h > tmax) or (N > Nmax1) or calcError
    end
    -- maintenant, côté gauche de t0
    calcError = false
    t = t0; Y = Y0;  h = -hMax; N = 0; stop = (t0 <= tmin)
       while not stop do
        nextY()
        t = t+h; N = N+1
        if not calcError then 
            table.insert( rep[1],1, t)
            for k = 1, n do
                table.insert(rep[k+1],1,Y[k])
            end
            local d = 0
            for _,x in ipairs(err) do
                if math.abs(x) > d then d = math.abs(x) end
            end
            d = 2*d
            local s = 0
            if d>0 then s = (-eps*h/d)^0.25 end
            if s < 0.1 then s = 0.1
            elseif s > 4 then s = 4
            end
            h = s*h
            if -h < hMin then h = -hMin 
            elseif -h > hMax then h = -hMax
            end
        end
        if (t>tmin) and (t+h < tmin) then h = tmin-t end
        stop = (t+h < tmin) or (N > Nmax2) or calcError
    end
    return rep
end

function ld.odesolve(f,t0,Y0,tmin,tmax,nbdots,method)
    if (method == nil) or (method == "rkf45") then 
        return ld.odeRkf45(f,t0,Y0,tmin,tmax,nbdots)
    elseif method == "rk4" then
        return ld.odeRK4(f,t0,Y0,tmin,tmax,nbdots)
    end
end

function ld.bezier(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 complexes.
    a = toComplex(a)
    c1 = toComplex(c1)
    c2 = toComplex(c2)
    b = toComplex(b)
    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.parametric(p,0,1,nbdots or ld.bezier_nbdots,false,2)
end

function ld.spline(liste,v1,v2)
-- liste : liste de points
-- v1 et v2 vecteurs tangents(complexes) aux extrémités ou bien nil pour une spline naturelle
-- renvoie un chemin à dessiner avec Dpath
    local error = false
    local n = #liste
    if n <= 1 then return end
    local sortie = {}
    n = 2*n-2
    local a, b, c, d = {},{},{},{}
    for k = 1, n-1 do
       table.insert(a,0); table.insert(c,0)
    end
    for k = 1, n do
        table.insert(b,0); table.insert(d,0)
    end
    b[1] = 1
    if v1 == nil then b[n] = 0 else b[n] = 1 end
    for k = 1, (n-2)//2 do 
       b[2*k] = 3; b[2*k+1] = 2 
    end
    if v2 == nil then c[1] = 3 else c [1] = 2 end
    for k = 1, (n-2)//2 do 
        c[2*k] = 1; c[2*k+1] = 5 
    end
    a[n-1] = 1
    for k = 1, (n-2)//2 do 
        a[2*k-1] = 5; a[2*k] = 1 
    end
    local d1, d2, d3 = nil, liste[1], liste[2]
    local k = n-1
    local res1, res2, res3
    if v1 == nil then d[n] = Z(0,0) 
    else
        res1 = d3-d2
        res2 = res1-v1
        if res2 ~= nil then d[n] = res2 else return end
    end
    for j = 3, #liste do
        d1 = d2; d2 = d3; d3 = liste[j]
        res1 = d3-d2
        res2 = d2-d1
        res3 = res1-res2
        if res3 ~= nil then
            d[k] = res3
            res3 = 3*res3
            d[k-1] = res3
        else
          return
        end
        k =k-2
    end
    if v2 == nil then d[1] = Z(0,0)
    else
        res1 = d3-d2 -- d2-d1
        res2 = v2-res1
        if res2 ~= nil then d[1] = res2 else return end
    end
    local u
    for k = 1, n-1 do
        u = a[k]/b[k]
        b[k+1] = b[k+1]-u*c[k]
        d[k+1] = d[k+1]-u*d[k]
    end
    d[n] = d[n] / b[n]
    for k =(n-1), 1, -1 do
        d[k] = (d[k]-c[k]*d[k+1])/b[k]
    end
    for k = n, 1, -1 do table.insert(sortie,d[k]) end
    local A1, A2, a1, a2, C1 = nil, liste[1]
    local res, first = {}, true
    for j = 2, #liste do
        A1 = A2;  A2 = liste[j]; a1 = sortie[2*j-3]; a2 = sortie[2*j-2]
        u = A2-A1-a2-a1
        C1 = u/3+A1
        table.insert(res,A1)
        if not first then table.insert(res,"b") else first = false end
        table.insert(res,C1)
        table.insert(res,a2/3+2*C1-A1)
    end
    table.insert(res,A2); table.insert(res,"b")
    return res
end

function ld.tcurve(L)
-- renvoie sous forme de chemin une courbe passant par des points donnés avec des tangentes imposées à gauche et à droite.
-- L = {pt1, {t1,a1,t2,a2}, pt2, {t1,a1,t2,a2}, ... }
-- t1 est la norme du vecteur tangent à gauche, a1 est l'angle en degré par rapport à l'horizontale du vecteur tangent à gauche
-- c'est la même chose pour le vecteur tangent à droite avec t2 et a2, mais ceux-ci sont facultatifs
-- lorsque t2 et a2 ne sont pas donnés, alors ils prennent les mêmes valeurs que t1 et a1
    local Vdir = function(t1,a1,t2,a2)
        local left 
        if t1 == nil then left = 0
        else
            left = t1*cpx.exp(cpx.I*a1*math.pi/180)-- vecteur tangent gauche
        end
        local right
        if t2 == nil then right = left
        else
            right = t2*cpx.exp(cpx.I*a2*math.pi/180)
        end
        return left,right -- vecteurs tangents gauche et droit
    end
    local a, val, var, b, vbl, vbr = nil, nil, nil, L[1], Vdir(table.unpack(L[2]))
    local res = {b} -- premier point
    for k = 2, #L/2 do
        a, val, var, b, vbl, vbr = b, vbl, vbr, L[2*k-1], Vdir(table.unpack(L[2*k]))
        table.append(res,{a+var/3,b-vbl/3,b,"b"}) -- courbe de Bézier de a à b respectant les contraintes
    end
    return res
end

function ld.curvilinear_param(L,close) -- curvilinear parametrization
-- L is a list of complex numbers
-- close=true/false, true if L must be closed
-- returns a function ld.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 (type(L[1]) == "table") and (not isComplex(L[1]))  then L = L[1] end -- liste de listes de complexes, on prend la première composante
    close = close or false
    local a, b, n, p = nil, toComplex(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 = toComplex(L[(k-1)%n+1])
        s = s + cpx.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

------------------------- csv -------------------------------------------

function ld.read_csv_file(file, options)
-- file is the name of the csv file: "<name>.csv"
-- options is a table with fields:
    -- header = true (indcates if the first line is the column names)
    -- dic = false (if true the output will be a list of dictionaries)
    -- sep = "," (separator)
    -- num = true (automatic conversion to numerical values)
    -- comment = "%%"

    options = options or {}
    local header = options.header 
    if header == nil then header = true end
    local dic = options.dic or false  
    if not header then dic = false end
    local sep = options.sep or "," 
    local comment = options.comment or "%%" 
    --% doit être doublé car c'est un caractère spécial de match et co
    local num = true  
    if options.num ~= nil then num = options.num end
    
    local trim = function(s) -- removes leading and trailing spaces
        return s:match("^%s*(.-)%s*$")
    end

    local data = {}
    local head_line
    if header then head_line = {} end
    local lg = 1
    for line in io.lines(file) do
        if line ~= "" and not line:match("^"..comment) then
            if (lg == 1) and header then
                for part in string.gmatch(line, "([^"..sep.."]+)") do
                    table.insert(head_line, trim(part))
                end
            else
                local result = {}
                for part in string.gmatch(line, "([^"..sep.."]+)") do
                    if num then
                        local x = tonumber(part)
                        if x ~= nil then table.insert(result, x)
                        else table.insert(result, trim(part))
                        end
                    else table.insert(result, trim(part))
                    end
                end
                if dic then
                    local res = {}
                    for k, key in ipairs(head_line) do
                        res[key] = result[k]
                    end
                    table.insert(data, res)
                else
                    table.insert(data, result)
                end
            end
            lg = lg + 1
        end
    end
  return data, head_line
end
