% tkz-grapheur-lua.tex
% Copyright 2026 Cédric Pierquet
% Released under the LaTeX Project Public License v1.3c or later, see http://www.latex-project.org/lppl.txt
% macros base [lua] de tkz-grapheur

%====COURBES IMPLICITES
\newlength\pflthickimpl
\tikzset{pflcourbeimplicit/.style={only marks,mark=*}}

\defKV[GraphiqueTikzImplicite]{%
  CoeffPasAuto=\def\pflcoeffpasimpl{#1},step stretch=\def\pflcoeffpasimpl{#1},%
  Pas=\def\pflpasimpl{#1},step=\def\pflpasimpl{#1},%
  Couleur=\def\pflcouleurbimpl{#1},color=\def\pflcouleurbimpl{#1},%
  Epaisseur=\setlength\pflthickimpl{#1},thick=\setlength\pflthickimpl{#1},%
  Precision=\def\pflprecisiimpl{#1},prec=\def\pflprecisiimpl{#1},%
  Tolerance=\def\pfltoleranceimplic{#1},tolerance=\def\pfltoleranceimplic{#1},%
  xa=\def\pflimplxa{#1},%
  xb=\def\pflimplxb{#1},%
  ya=\def\pflimplya{#1},%
  yb=\def\pflimplyb{#1}%
}
\setKVdefault[GraphiqueTikzImplicite]{%
  CoeffPasAuto=1,step stretch=1,%
  Pas=auto,step=auto,%
  Couleur=black,color=black,%
  Epaisseur=0.2875pt,thick=0.2875pt,%
  Precision=8,precision=8,%
  Tolerance=1e-10,tolerance=1e-10,%
  lua=true,%
  xa={},xb={},ya={},yb={}%
}

\NewDocumentCommand\TracerCourbeImplicite{ O{} m }{%with jfbu part of code for xint version !
  % #1 = clés (Pas, Couleur, etc)
  % #2 = expression f(x,y) (= 0 implicite)
  \restoreKV[GraphiqueTikzImplicite]%
  \setKV[GraphiqueTikzImplicite]{#1}%
  % résolution des bornes (défaut = fenêtre)
  \IfStrEq{\pflimplxa}{}{\xdef\pflimplxa{\pflxmin}}{}%
  \IfStrEq{\pflimplxb}{}{\xdef\pflimplxb{\pflxmax}}{}%
  \IfStrEq{\pflimplya}{}{\xdef\pflimplya{\pflymin}}{}%
  \IfStrEq{\pflimplyb}{}{\xdef\pflimplyb{\pflymax}}{}%
  \ifboolKV[GraphiqueTikzImplicite]{lua}%
  {%
    \ifluatex
    \IfStrEq{\pflpasimpl}{auto}%
    {%
      \pgfmathsetmacro{\pfl@pasx}{\pgfmath@tonumber{\pflthickimpl} * 0.5 * (\pflcoeffpasimpl) / \pgfmath@tonumber{\pgf@xx}}%
      \pgfmathsetmacro{\pfl@pasy}{\pgfmath@tonumber{\pflthickimpl} * 0.5 * (\pflcoeffpasimpl) / \pgfmath@tonumber{\pgf@yy}}%
    }%
    {%
      \def\pfl@pasx{\pflpasimpl}%
      \def\pfl@pasy{\pflpasimpl}%
    }%
    \edef\tmplistepts{\directlua{tex.sprint(pfl.marching_squares("\luaescapestring{#2}",\pflimplxa,\pflimplxb,\pflimplya,\pflimplyb,\pfl@pasx,\pfl@pasy,\pfltoleranceimplic))}}%
    \begin{scope}
        \clip (\pflxmin,\pflymin) rectangle (\pflxmax,\pflymax);
        \draw[\pflcouleurbimpl] plot[mark size=\pflthickimpl,pflcourbeimplicit] coordinates {\tmplistepts};
    \end{scope}%
    \else
      \PackageWarning{tkz-grapheur}{.: LuaLaTeX not detected, \TracerCourbeImplicite\space with [lua=false] key only available :.}
    \fi
  }%
  {%
  % open a scope limiting group
  \begingroup
  % configure xint to work with less digits
  \xintSetDigits*{\pflprecisiimpl}%
  % définition fonction xint à 2 variables
  \xintdeffloatfunc pflimplicitefct(x,y) = #2 ;%
  %calcul auto du pas
  \IfStrEq{\pflpasimpl}{auto}%
  {%
    \pgfmathsetmacro{\pflpasximplcalc}{\pgfmath@tonumber{\pflthickimpl} * \pflcoeffpasimpl * 1.414 / \pgfmath@tonumber{\pgf@xx}}%
    \pgfmathsetmacro{\pflpasyimplcalc}{\pgfmath@tonumber{\pflthickimpl} * \pflcoeffpasimpl * 1.414 / \pgfmath@tonumber{\pgf@yy}}%
  }%
  {%
    \def\pflpasximplcalc{\pflpasimpl}%
    \def\pflpasyimplcalc{\pflpasimpl}%
  }%
  \def\tmplisteptsh{}%
  \def\tmplisteptsv{}%
  \def\tmplisteptsp{}%
  % double boucle marching squares
  \edef\tmpxcoords{\xintfloateval{\pflxmin..[\pflpasximplcalc]..\pflxmax}}%
  \edef\tmpycoords{\xintfloateval{\pflymin..[\pflpasyimplcalc]..\pflymax}}%
  % attention ensemble de définition ??...
  \def\tmpj{0}%
  % precompute values of function on left vertical border
  \xintFor ##2 in {\tmpycoords}\do{%
    \expandafter\edef\csname tmp_value_\tmpj\endcsname{\xintfloateval{pflimplicitefct(\pflxmin,##2)}}%
    \edef\tmpj{\the\numexpr\tmpj+1}%
    \xintifForLast{% one more
      \expandafter\edef\csname tmp_value_\tmpj\endcsname{\xintfloateval{pflimplicitefct(\pflxmin,##2+\pflpasyimplcalc)}}%
    }{}%
  }%
  \xintFor ##1 in {\tmpxcoords}\do{%
    \edef\tmpC{\csname tmp_value_0\endcsname}%
    \def\tmpj{0}%
    \xintFor ##2 in {\tmpycoords}\do{%
      % évaluer les 3 coins et préparer en même temps la prochaine colonne pré-calculée
      \let\tmpA\tmpC
      \edef\tmpB{\xintfloateval{pflimplicitefct(##1+\pflpasximplcalc,##2)}}%
      % mettre à jour pour la prochaine fois, oui c'est rusé
      \expandafter\let\csname tmp_value_\tmpj\endcsname\tmpB
      \edef\tmpj{\the\numexpr\tmpj+1}%
      % là on est encore avec la colonne en cours, déjà calculée
      \edef\tmpC{\csname tmp_value_\tmpj\endcsname}%
      % changement de signe horizontal
      \xintifboolexpr{sgn(\tmpA)*sgn(\tmpB) < 0}%
        {%
          \xdef\tmplisteptsh{\tmplisteptsh (\xintfloateval{##1-(\tmpA)*\pflpasximplcalc/(\tmpB-\tmpA)},##2)}%
        }{}%
      % changement de signe vertical
      \xintifboolexpr{sgn(\tmpA)*sgn(\tmpC) < 0}%
        {%
          \xdef\tmplisteptsv{\tmplisteptsv (##1,\xintfloateval{##2-(\tmpA)*\pflpasyimplcalc/(\tmpC-\tmpA)})}%
        }{}%
      % si pas de changement de signe détecté → test zéro exact
      \xintifboolexpr{(sgn(\tmpA)*sgn(\tmpB) >= 0) && (sgn(\tmpA)*sgn(\tmpC) >= 0) && (abs(\tmpA) < \pfltoleranceimplic)}%
        {%
          \xdef\tmplisteptsp{\tmplisteptsp (##1,##2)}%
        }{}%
      % le dernier coup il faut en calculer un de plus
      \xintifForLast{%
        \expandafter\edef\csname tmp_value_\tmpj\endcsname{\xintfloateval{pflimplicitefct(##1+\pflpasximplcalc,##2+\pflpasyimplcalc)}}%
      }{}%
    }%
  }%
  \begin{scope}
    \clip (\pflxmin,\pflymin) rectangle (\pflxmax,\pflymax) ;
    \IfEq{\tmplisteptsp}{}{}%
      {%
        \draw[\pflcouleurbimpl] plot[pflcourbeimplicit] coordinates {\tmplisteptsp} ;%
      }%
    \IfEq{\tmplisteptsh}{}{}%
      {%
        \draw[\pflcouleurbimpl] plot[pflcourbeimplicit] coordinates {\tmplisteptsh} ;%
      }%
    \IfEq{\tmplisteptsv}{}{}%
      {%
        \draw[\pflcouleurbimpl] plot[pflcourbeimplicit] coordinates {\tmplisteptsv} ;%
      }%
  \end{scope}%
  \endgroup%
  }%
}
\NewCommandCopy\PlotImplicitCurve\TracerCourbeImplicite

\defKV[GraphiqueTikzNiveau]{%
  Couleur=\def\pflniveaucouleur{#1},%
  Couleurs=\def\pflniveaucouleurs{#1},%
  Pas=\def\pflniveaupas{#1},%
  Epaisseur=\setlength\pflthickimpl{#1}%
}
\setKVdefault[GraphiqueTikzNiveau]{%
  Couleur=black,%
  Couleurs={},%
  Pas=auto,%
  Epaisseur=0.2875pt%
}

% \NewDocumentCommand\TracerLignesNiveau{ O{} m m }{%
  % % #1 = clés
  % % #2 = expression f(x,y)
  % \restoreKV[GraphiqueTikzNiveau]%
  % \setKV[GraphiqueTikzNiveau]{#1}%
  % % Boucle sur les valeurs
  % \foreach \elt [count=\i from 1] in {#3}{%
    % \IfStrEq{\pflniveaucouleurs}{}%
      % {%
        % \edef\pflcoulcourante{\pflniveaucouleur}%
      % }%
      % {%
        % \setsepchar{,}%
        % \readlist*\pfllistecouleurs{\pflniveaucouleurs}%
        % %Couleur cyclique si moins de couleurs que de niveaux
        % \pgfmathtruncatemacro{\pflcolidx}{mod(\i-1,\pfllistecouleurslen)+1}%
        % \itemtomacro\pfllistecouleurs[\pflcolidx]{\pflcoulcourante}%
      % }%
    % \TracerCourbeImplicite[lua,Couleur=\pflcoulcourante,Pas=\pflniveaupas,Epaisseur=\pflthickimpl]{#2 - (\elt)}%
  % }
% }
% \NewCommandCopy\PlotLevelCurves\TracerLignesNiveau

\NewDocumentCommand\TracerLignesNiveau{ O{} m m }{%
  % #1 = clés
  % #2 = expression f(x,y)
  % #3 = liste des valeurs k
  \restoreKV[GraphiqueTikzNiveau]%
  \setKV[GraphiqueTikzNiveau]{#1}%
  % calcul du pas
  \IfStrEq{\pflniveaupas}{auto}{%
    \pgfmathsetmacro{\pfl@pasx}{\pgfmath@tonumber{\pflthickimpl}*0.5/\pgfmath@tonumber{\pgf@xx}}%
    \pgfmathsetmacro{\pfl@pasy}{\pgfmath@tonumber{\pflthickimpl}*0.5/\pgfmath@tonumber{\pgf@yy}}%
  }{%
    \pgfmathsetmacro{\pfl@pasx}{\pflniveaupas}%
    \pgfmathsetmacro{\pfl@pasy}{\pflniveaupas}%
  }%
  % Appel Lua unique → grille calculée une seule fois !
  \directlua{
    pfl._niveau_pts = pfl.level_curves(
      "\luaescapestring{#2}",
      "\luaescapestring{#3}",
      \pflxmin, \pflxmax, \pflymin, \pflymax,
      \pfl@pasx, \pfl@pasy
    )
  }%
  % Boucle tracé uniquement
  \foreach \elt [count=\i from 1] in {#3}{%
    \IfStrEq{\pflniveaucouleurs}{}%
      {\edef\pflcoulcourante{\pflniveaucouleur}}%
      {%
        \setsepchar{,}%
        \readlist*\pfllistecouleurs{\pflniveaucouleurs}%
        \itemcycltomacro\pfllistecouleurs[\i]{\pflcoulcourante}%
		%\pgfmathtruncatemacro{\pflcolidx}{mod(\i-1,\pfllistecouleurslen)+1}%
        %\itemtomacro\pfllistecouleurs[\pflcolidx]{\pflcoulcourante}%
      }%
    \edef\pflkpts{\directlua{tex.sprint(pfl._niveau_pts[\i] or "")}}%
    \begin{scope}
      \clip (\pflxmin,\pflymin) rectangle (\pflxmax,\pflymax);
      \draw[\pflcoulcourante]
        plot[mark size=\pflthickimpl,pflcourbeimplicit]
        coordinates {\pflkpts};
    \end{scope}%
  }%
}%
\NewCommandCopy\PlotLevelCurves\TracerLignesNiveau

\ifluatex
\RequirePackage{luacode}
% \begin{luacode*}
% pfl = pfl or {}

% function pfl.expr_to_func(expr_str)
    % -- Traduction expression LaTeX → Lua
	% -- texio.write_nl("=== expr reçue : " .. expr_str)
    % local e = expr_str
    % e = e:gsub("%^",    "^")
    % e = e:gsub("sin%(", "math.sin(")
    % e = e:gsub("cos%(", "math.cos(")
    % e = e:gsub("tan%(", "math.tan(")
    % e = e:gsub("exp%(", "math.exp(")
    % e = e:gsub("sqrt%(","math.sqrt(")
    % e = e:gsub("log%(", "math.log(")
    % e = e:gsub("abs%(", "math.abs(")
    % e = e:gsub("pi",    tostring(math.pi))
    % e = e:gsub("%*%*",  "^")
    % -- Compilation dynamique
	% -- texio.write_nl("=== expr traduite : " .. e)
    % local fstr = "local x,y = ...; return " .. e
	% -- texio.write_nl("=== fstr : " .. fstr)
    % local f, err = load(fstr)
    % if not f then
        % texio.write_nl("! pfl.expr_to_func : " .. tostring(err))
        % return nil
    % end
    % return f
% end

% function pfl.marching_squares(expr_str, xmin, xmax, ymin, ymax, pasx, pasy, eps)
    % eps = eps or 1e-12
    % local f = pfl.expr_to_func(expr_str)
    % if not f then return "" end

    % local ni = math.floor((xmax - xmin) / pasx)
    % local nj = math.floor((ymax - ymin) / pasy)

    % -- Phase 1 : tableau complet F[i][j]
    % local F = {}
    % local xs = {}
    % local ys = {}
    % for i = 0, ni do
        % xs[i] = xmin + i * pasx
        % F[i]  = {}
    % end
    % for j = 0, nj do
        % ys[j] = ymin + j * pasy
    % end
    % for i = 0, ni do
        % for j = 0, nj do
            % F[i][j] = f(xs[i], ys[j])
        % end
    % end

    % -- Phase 2 : marching squares
    % local pts = {}
    % for i = 0, ni - 1 do
        % for j = 0, nj - 1 do
            % local A = F[i  ][j  ]
            % local B = F[i+1][j  ]
            % local C = F[i  ][j+1]
            % local D = F[i+1][j+1]
            % local xi  = xs[i]
            % local xip = xs[i+1]
            % local yj  = ys[j]
            % local yjp = ys[j+1]
            % -- Arête A→B
            % if A * B < 0 then
                % pts[#pts+1] = string.format("(%.3f,%.3g)",
                    % xi - A * pasx / (B - A), yj)
            % end
            % -- Arête A→C
            % if A * C < 0 then
                % pts[#pts+1] = string.format("(%.3f,%.3g)",
                    % xi, yj - A * pasy / (C - A))
            % end
            % -- Arête C→D
            % -- if C * D < 0 then
            % --    pts[#pts+1] = string.format("(%.6f,%.6g)",
            % --        xi - C * pasx / (D - C), yjp)
            % -- end
            % -- Arête B→D
            % -- if B * D < 0 then
            % --     pts[#pts+1] = string.format("(%.6f,%.6g)",
            % --         xip, yj - B * pasy / (D - B))
            % -- end
            % -- Zéro exact
            % if A*B >= 0 and A*C >= 0 and math.abs(A) < eps then
                % pts[#pts+1] = string.format("(%.6f,%.6g)", xi, yj)
            % end
        % end
    % end
    % local result = table.concat(pts, " ")
    % if result == "" then
        % return string.format("(%.6f,%.6f)", xmin - 1, ymin - 1)
    % end
    % return result
% end
% \end{luacode*}
\begin{luacode*}
pfl = pfl or {}

function pfl.expr_to_func(expr_str)
    local e = expr_str
    e = e:gsub("%^",    "^")
    e = e:gsub("sin%(", "math.sin(")
    e = e:gsub("cos%(", "math.cos(")
    e = e:gsub("tan%(", "math.tan(")
    e = e:gsub("exp%(", "math.exp(")
    e = e:gsub("sqrt%(","math.sqrt(")
    e = e:gsub("log%(", "math.log(")
    e = e:gsub("ln%(", "math.log(")
    e = e:gsub("abs%(", "math.abs(")
    e = e:gsub("pi",    tostring(math.pi))
    e = e:gsub("%*%*",  "^")
    local fstr = "local x,y = ...; return " .. e
    local f, err = load(fstr)
    if not f then
        texio.write_nl("! pfl.expr_to_func : " .. tostring(err))
        return nil
    end
    return f
end

function pfl.marching_squares(expr_str, xmin, xmax, ymin, ymax, pasx, pasy, eps)
    eps = eps or 1e-12
    local f = pfl.expr_to_func(expr_str)
    if not f then return "" end
    local ni = math.floor((xmax - xmin) / pasx)
    local nj = math.floor((ymax - ymin) / pasy)
    local F, xs, ys = {}, {}, {}
    for i = 0, ni do xs[i] = xmin + i*pasx ; F[i] = {} end
    for j = 0, nj do ys[j] = ymin + j*pasy end
    for i = 0, ni do
        for j = 0, nj do F[i][j] = f(xs[i], ys[j]) end
    end
    local pts = {}
    for i = 0, ni-1 do
        for j = 0, nj-1 do
            local A,B,C = F[i][j],F[i+1][j],F[i][j+1]
            local xi,yj = xs[i],ys[j]
            if A*B < 0 then
                pts[#pts+1] = string.format("(%.3f,%.3g)",
                    xi-A*pasx/(B-A), yj)
            end
            if A*C < 0 then
                pts[#pts+1] = string.format("(%.3f,%.3g)",
                    xi, yj-A*pasy/(C-A))
            end
            if A*B >= 0 and A*C >= 0 and math.abs(A) < eps then
                pts[#pts+1] = string.format("(%.6f,%.6g)", xi, yj)
            end
        end
    end
    local result = table.concat(pts, " ")
    if result == "" then
        return string.format("(%.6f,%.6f)", xmin-1, ymin-1)
    end
    return result
end

function pfl.level_curves(expr_str, valeurs_str, xmin, xmax, ymin, ymax, pasx, pasy, eps)
    eps = eps or 1e-12
    local f = pfl.expr_to_func(expr_str)
    if not f then return {} end
    local ni = math.floor((xmax - xmin) / pasx)
    local nj = math.floor((ymax - ymin) / pasy)
    local F, xs, ys = {}, {}, {}
    for i = 0, ni do xs[i] = xmin + i*pasx ; F[i] = {} end
    for j = 0, nj do ys[j] = ymin + j*pasy end
    for i = 0, ni do
        for j = 0, nj do F[i][j] = f(xs[i], ys[j]) end
    end
    local valeurs = {}
    for k in valeurs_str:gmatch("[^,]+") do
        valeurs[#valeurs+1] = tonumber(k)
    end
    local resultats = {}
    for _, k in ipairs(valeurs) do
        local pts = {}
        for i = 0, ni-1 do
            for j = 0, nj-1 do
                local A,B,C = F[i][j]-k,F[i+1][j]-k,F[i][j+1]-k
                local xi,yj = xs[i],ys[j]
                if A*B < 0 then
                    pts[#pts+1] = string.format("(%.3f,%.3g)",
                        xi-A*pasx/(B-A), yj)
                end
                if A*C < 0 then
                    pts[#pts+1] = string.format("(%.3f,%.3g)",
                        xi, yj-A*pasy/(C-A))
                end
                if A*B >= 0 and A*C >= 0 and math.abs(A) < eps then
                    pts[#pts+1] = string.format("(%.6f,%.6g)", xi, yj)
                end
            end
        end
        local result = table.concat(pts, " ")
        if result == "" then
            result = string.format("(%.6f,%.6f)", xmin-1, ymin-1)
        end
        resultats[#resultats+1] = result
    end
    return resultats
end
\end{luacode*}
\fi

\endinput