-- luadraw_povray.lua
-- date 2026/07/09
-- version 3.3
-- 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

-- This module adds a (very basic) export to POV-RAY

local ld = luadraw
local cpx, pt3d = ld.cpx, ld.pt3d
local Origin, vecI, vecJ, vecK = pt3d.Origin, pt3d.vecI, pt3d.vecJ, pt3d.vecK
local Z, M = cpx.Z, pt3d.M
local graph3d = ld.graph3d

local oldtostring = tostring
local tostring = function(x)
    if type(x) == "number" then
        return ld.strReal(x,6)
    else
        return x
    end
end
local Pov_preamble = {""}
local Pov_declarations = {""}
local Pov_renderings = {""}
local Pov_export = Pov_declarations
local Pov_object = 0
local Pov_default_cmd_win = "pvengine64.exe"
local Pov_default_cmd_unix = "povray"
local Pov_default_cmd_ext = "" -- added at the end of the command
local Pov_default_param = "-V +A +FN"
local Pov_default_param_ext = "/RENDER /EXIT" -- for windows
local Pov_default_includefiles = {} --list of strings (inc files)
local Pov_default_bg = ""
local Pov_default_scale = 1
local Pov_default_shadow = true
local Pov_default_arrowscale = {1,1}

local Pov_cmd_win = Pov_default_cmd_win
local Pov_cmd_unix = Pov_default_cmd_unix
local Pov_cmd_ext =  Pov_default_cmd_ext -- added at the end of the command
local Pov_param = Pov_default_param
local Pov_param_ext = Pov_default_param_ext
local Pov_includefiles = Pov_default_includefiles
local Pov_size = ""
local Pov_bg = Pov_default_bg
local Pov_scale = Pov_default_scale
local Pov_shadow = Pov_default_shadow
local Pov_arrowscale = Pov_default_arrowscale

function graph3d:Pov_clean()
    Pov_preamble = {""}
    Pov_declarations = {""}
    Pov_renderings = {""}
    Pov_export = Pov_declarations
    Pov_object = 0
end

function ld.povray_default(options)
-- options = {bg="" ,imagescale= 1, shadow=true, param="-V +A +FN", pov_cmd=default, win_param_ext=default, include={}, pov_cmd_ext=""}
    options = options or {}
    Pov_default_scale = options.imagescale or Pov_default_scale
    Pov_default_arrowscale = options.arrowscale or Pov_default_arrowscale
    Pov_default_bg = options.bg or Pov_default_bg
    local shadow = options.shadow
    if shadow ~= nil then Pov_default_shadow = shadow end
    Pov_default_includefiles = options.include or Pov_default_includefiles  -- "{file1.inc", "file2.inc", ...}
    Pov_default_cmd_ext = options.pov_cmd_ext or Pov_default_cmd_ext
    Pov_param = options.param or Pov_default_param
    if os.type == "windows" then
        Pov_default_cmd_win = options.pov_cmd or Pov_default_cmd_win
    else
        Pov_default_cmd_unix = options.pov_cmd or Pov_default_cmd_unix
    end
    Pov_default_param_ext = options.win_param_ext or Pov_default_param_ext
 end

function graph3d:Pov_new(options)
-- options = {bg="" ,imagescale= 1, shadow=true, param="-V +A +FN", pov_cmd=default, win_param_ext=default, include={}, pov_cmd_ext=""}
    options = options or {}
    Pov_scale = options.imagescale or Pov_default_scale
    Pov_arrowscale = options.arrowscale or Pov_default_arrowscale
    Pov_bg = options.bg or Pov_default_bg
    Pov_shadow = options.shadow
    if Pov_shadow == nil then Pov_shadow = Pov_default_shadow end
    Pov_includefiles = options.include or Pov_default_includefiles  -- "{file1.inc", "file2.inc", ...}
    if type(Pov_includefiles) == "string" then Pov_includefiles = {Pov_includefiles} end
    Pov_cmd_ext = options.pov_cmd_ext or Pov_default_cmd_ext
    Pov_param = options.param or Pov_default_param
    if os.type == "windows" then
        Pov_cmd_win = options.pov_cmd or Pov_default_cmd_win
    else
        Pov_cmd_unix = options.pov_cmd or Pov_default_cmd_unix
    end
    Pov_param_ext = options.win_param_ext or Pov_default_param_ext
    self:Pov_clean()
 end
 
function graph3d:Pov_include(...) -- "file1.inc", "file2.inc", ...
    for _, incfile in ipairs{...} do
        table.insert(Pov_includefiles, incfile)
    end
end

local full_name = function(filename)
    local lfs = require "lfs"
    local cwd = lfs.currentdir()
    local name = cwd..'/'..filename
    if os.type == "windows" then
        name = string.gsub(name, "/", "\\")
    end
    return name    
end

function graph3d:Pov_save(filename) -- filename without path, without extension
    if filename == nil then
        local n = #ld.graph_name
        filename = string.sub(ld.graph_name,1,n-3).."pov"
    else
        filename = ld.cachedir..filename..".pov"
    end
    local f = io.open(filename,"w")
    if f == nil then print("Can't open file "..filename..". Abort...")
    else
        self:Pov_do_preamble()
        local str = ld.concat(Pov_preamble, {"//declarations"},Pov_declarations,{"//renderings"},Pov_renderings)
        for _, lg in ipairs(str) do
            f:write(lg.."\n")
        end
        f:close()
    end
end

function graph3d:Pov_exec(filename) -- filename without path, without extension
    self:Pov_save(filename)
    if filename == nil then
        local n = #ld.graph_name
        filename = string.sub(ld.graph_name,1,n-3).."pov"
    else
        filename = ld.cachedir..filename..".pov"
    end
    local param = Pov_param
    local cmd
    if Pov_bg == "" then 
        param = param .. " +UA" --transparent background
    end  
    if os.type == "windows" then
        cmd = Pov_cmd_win.." "..Pov_size..param..' "'..full_name(filename)..'" '..Pov_param_ext..Pov_cmd_ext
    else
        cmd = Pov_cmd_unix.." "..Pov_size..param.." "..filename..Pov_cmd_ext
    end
    print("execute: "..cmd)
    os.execute(cmd)  -- this instruction needs a compilation with -shell-escape option
end

function graph3d:Pov_show(filename)
    if filename == nil then
        local n = #ld.graph_name
        filename = string.sub(ld.graph_name,1,n-3).."png"
    end
    self:Dimage(filename, Z(self.Xmin,self.Ymin), {pos="NE", graphics_options="width="..self:Graphwidth().."cm, height="..self:Graphheight().."cm"})
end


local get_num = function()
    Pov_object = Pov_object+1
    return Pov_object
end

local pov_write = function(str)
-- écrit la chaîne str sans retour à la ligne
    if str ~= "" then
        local n = #Pov_export
        Pov_export[n] = Pov_export[n]..str
    end
end

local pov_writeln = function(str)
--  écrit la chaîne str avec retour à la ligne
    if str ~= nil then pov_write(str) end
    table.insert(Pov_export,"") -- on démarre une nouvelle ligne   
end

function graph3d:Pov_write(str)
    pov_write(str)
end

function graph3d:Pov_writeln(str)
    pov_writeln(str)
end

local strdot = function(A) 
    return "<"..tostring(-A.x)..","..tostring(A.y)..","..tostring(A.z)..">"
end

local strfacet = function(f) 
    return "<"..tostring(f[1]-1)..","..tostring(f[2]-1)..","..tostring(f[3]-1)..">"
end

local strdot2 = function(A) -- without changing x-coordinate
    return "<"..tostring(A.x)..","..tostring(A.y)..","..tostring(A.z)..">"
end

local define_options = function(self,options) -- common options
    options = options or {}
    if options.render == nil then options.render = true end
    options.name = options.name or "object"..get_num()
    options.color = options.color or ld.White
    options.opacity = options.opacity or 1
    options.ambient = options.ambient or 0.35
    options.diffuse = options.diffuse or 0.8
    options.phong = options.phong or 0.5
    if options.shadow == nil then options.shadow = true end
    if options.matrix == nil then options.matrix = self.matrix3d
    else
        options.matrix = ld.composematrix3d(self.matrix3d,options.matrix)
    end
    return options
end


local write_matrix2 = function(matrix) -- with changing of x-coordinate
        local V4, V1, V2, V3 = matrix[1], matrix[2], matrix[3], matrix[4]
        pov_writeln("    matrix <"..tostring(-V1.x)..","..tostring(V1.y)..","..tostring(V1.z)..",")
        pov_writeln("            "..tostring(-V2.x)..","..tostring(V2.y)..","..tostring(V2.z)..",")
        pov_writeln("            "..tostring(-V3.x)..","..tostring(V3.y)..","..tostring(V3.z)..",")
        pov_writeln("            "..tostring(-V4.x)..","..tostring(V4.y)..","..tostring(V4.z)..">")
end

local write_matrix = function(matrix) -- without changing of x-coordinate
    if not ld.isID3d(matrix) then
        local V4, V1, V2, V3 = matrix[1], matrix[2], matrix[3], matrix[4]
        pov_writeln("    matrix <"..tostring(V1.x)..","..tostring(-V1.y)..","..tostring(-V1.z)..",")
        pov_writeln("            "..tostring(-V2.x)..","..tostring(V2.y)..","..tostring(V2.z)..",")
        pov_writeln("            "..tostring(-V3.x)..","..tostring(V3.y)..","..tostring(V3.z)..",")
        pov_writeln("            "..tostring(-V4.x)..","..tostring(V4.y)..","..tostring(V4.z)..">")
    end
end

local write_clip = function(cliplist,box)
-- box = true or false, 
-- if true then data = {M(x1,y1,z1), M(x2,y2,z2)} is a box, else data = {A,n} is  a plane
    if cliplist == nil then return end
    if (type(cliplist) ~= "table") or pt3d.isPoint3d(cliplist[1]) then cliplist = {cliplist} end
    for _, data in ipairs(cliplist) do
        if type(data) == "string" then -- data must the name of an existing object
            pov_writeln("    clipped_by{ object{ "..data.."} }")
        else
            local f = strdot
            local M1, M2 = table.unpack(data)
            if box == nil then box = true end
            if box then -- box or sphere
               if pt3d.isPoint3d(M2) then 
                    pov_writeln("    clipped_by{ box{ ".. f(M1).." ".. f(M2).."} }")
                else
                 pov_writeln("    clipped_by{ sphere{ ".. f(M1)..", ".. tostring(M2).."} }")
                end 
            else -- plane
                local O = ld.proj3d(Origin, data)
                M2 = pt3d.normalize(M2)
                local d = pt3d.abs(O)
                --M2 = -M2
                if pt3d.dot(O,M2) > 0 then d = -d end -- distance à l'origine
                pov_writeln("  clipped_by { plane{ "..f(-M2)..", "..tostring(d).." } }")
            end
        end
    end
end

local color_map = function(pal, opacity)
    opacity = opacity or 1
    local cmap, n = "", #pal
    for k = 0, n-1 do
        local col = pal[k+1]
        cmap = cmap.."["..(k/(n-1)).. " rgbt<"..col[1]..","..col[2]..","..col[3]..","..(1-opacity)..">]"
    end
    return "color_map{"..cmap.."}"
end

local pattern_type = function(func,minmax)
    minmax = minmax or {}
    local x1, x2 = table.unpack(minmax)
    if (x1 == nil) or (x2 == nil) then x1, x2 = 0, 1 end
    if (func == "x")  or (func == "y") or (func == "z")  then 
        return "function{ ("..func.."-("..tostring(x1).."))/("..tostring(x2-x1)..") }"
    else
        return func
    end
end

local write_rendering = function(options) --function(name,color,opacity,ambient,diffuse,phong,shadow,mytexture)
     local name, color, opacity, ambient, diffuse, phong, shadow, mytexture, usepalette = options.name, options.color, options.opacity, options.ambient, options.diffuse, options.phong, options.shadow, options.mytexture, options.usepalette
    Pov_export = Pov_renderings
    pov_writeln("\nobject{ "..name)
    if mytexture ~= nil then
        pov_writeln("  "..mytexture)
    else
        pov_writeln("  texture{ ")
        if usepalette ~= nil then --usepalette = {palette, function, minmax}
            local pal, func, minmax = table.unpack(usepalette)
             pov_writeln("         pigment{ "..pattern_type(func,minmax).." "..color_map(pal,opacity).."}")
        elseif type(color) == "string" then -- pigment can come from a file as textures.inc
            pov_writeln("         pigment{ "..color.."}") -- transmit "..tostring(1-opacity).."}")
        else
            pov_writeln("         pigment{ color rgbt<"..tostring(color[1])..","..tostring(color[2])..","..tostring(color[3])..","..tostring(1-opacity)..">}")
        end
        pov_writeln("         finish{ ambient "..tostring(ambient).." diffuse "..tostring(diffuse).." phong "..tostring(phong).."} }")
    end
    if not shadow then pov_writeln("  no_shadow}") 
    else
        pov_writeln("      }")
    end
    Pov_export = Pov_declarations
end

local write_modifiers_rendering = function(options) --function(name,color,opacity,ambient,diffuse,phong,shadow,mytexture)
        --modifiers
    if options.clipbox ~= nil then  write_clip(options.clipbox, true) end
    if options.clipplane ~= nil then  write_clip(options.clipplane,false) end
    if options.render then -- rendering
        write_rendering(options)
    end
end

--------------------- preamble -----------------

function graph3d:Pov_do_preamble()
    Pov_export = Pov_preamble
    local param = Pov_param
    if Pov_bg == "" then 
        param = param .. " +UA" --transparent background
    end
    local wd, ht, H, W = self:Graphwidth(), self:Graphheight()
    if ht > wd then 
        H = math.floor(640*Pov_scale); W = math.floor(640*wd/ht*Pov_scale)
    else 
        W = math.floor(640*Pov_scale); H = math.floor(640*ht/wd*Pov_scale)
    end
    local rep = {}
    Pov_size = " +W"..tostring(W).." +H"..tostring(H).." "
    pov_writeln("//luadraw "..ld.version)
    pov_writeln("//options:"..Pov_size..param)
    pov_writeln("#version 3.7;")
    for _,incfile in ipairs(Pov_includefiles) do
        pov_writeln('#include "'..incfile..'"')
    end
    pov_writeln("global_settings { charset utf8\n     ambient_light rgb 1.5\n     assumed_gamma 1.8}")
    if Pov_bg == "" then 
        pov_writeln("background{ color srgbt<0,0,0,1> }")
    elseif type(Pov_bg) == "string" then
        pov_writeln("background{ "..Pov_bg.." }")
    else
        local r,g,b = table.unpack(Pov_bg)
        pov_writeln("background{ color rgb<"..tostring(r)..","..tostring(g)..","..tostring(b).."> }")
    end
    local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d)
    local dist, N = pt3d.abs(M(x2-x1,y2-y1,z2-z1)), self.Normal
    pov_writeln("camera{ orthographic")
    pov_writeln("     location "..tostring(1000*dist).."*<"..tostring(-N.x)..","..tostring(N.y)..","..tostring(N.z)..">")
    local SkyY = self:ScreenY()
    local SkyX = self:ScreenX()
    local centerBox = (self.Xmax+self.Xmin)/2*SkyX + (self.Ymax+self.Ymin)/2*SkyY
    pov_writeln("     sky <"..tostring(self.cosPhi*self.cosTheta)..","..tostring(-self.cosPhi*self.sinTheta)..","..tostring(self.sinPhi)..">")
    pov_writeln("     up "..tostring(self.Ymax-self.Ymin).."*y")
    pov_writeln("     right ".. tostring(self.Xmax-self.Xmin).."*x")
    pov_writeln("     look_at <0,0,0>")
    pov_writeln("     translate <"..tostring(-centerBox.x)..","..tostring(centerBox.y)..","..tostring(centerBox.z)..">}")
    local light = pt3d.normalize( M(-self.cosPhi*self.cosTheta/2+self.sinTheta/3-N.x, -self.cosPhi*self.sinTheta/2-self.cosTheta/3+N.y, self.sinPhi/2+N.z))
    local str_light = "light_source { "..tostring(1000*dist).."*<"..tostring(light.x)..","..tostring(light.y)..","..tostring(light.z).."> color rgb<1,1,1>"
    if Pov_shadow == false then str_light =  str_light.." shadowless" end 
    pov_writeln( str_light.."}")
    Pov_export = Pov_declarations
end

function graph3d:Pov_strdot(A)
    return strdot(A)
end

local writedotlist = function(dotlist)
    local nbcol = 3
    local nb = #dotlist
    local nblg = nb//nbcol
    local sep = ""
    for k = 1, nblg do
        local dep = nbcol*(k-1)+1
        local str = "      "
        for j = 0, nbcol-1 do
            str = str..sep..strdot(dotlist[dep+j]); sep= ", "
        end
        pov_writeln( str )
    end
    if nb%nbcol ~= 0 then
        pov_write("      ")
        for k = nbcol*nblg+1, nb do
            pov_write( sep..strdot(dotlist[k]))
        end
        pov_writeln()
    end
end

local writefacetlist = function(facetlist)
    local nbcol = 6
    local nb = #facetlist
    local nblg = nb//nbcol
    local sep = ""
    for k = 1, nblg do
        local dep = nbcol*(k-1)+1
        local str = "      "
        for j = 0, nbcol-1 do
            str = str..sep..strfacet(facetlist[dep+j]); sep= ", "
        end
        pov_writeln( str )
    end
    if nb%nbcol ~= 0 then
        pov_write("      ")
        for k = nbcol*nblg+1, nb do
            pov_write( sep..strfacet(facetlist[k]))
        end
        pov_writeln()
    end
end
------------------------------- implicit surf f(x,y,z)=0 ------------------------------

function graph3d:Pov_implicit( povfunc, luafunc, options)
-- luafunc : lua function of (x,y,z), to draw luafunc(x,y,z)=0
-- povfunc : povray version of the function (string)
-- options = {clipbox= default, clipplane, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil,containedby = default win 3d }
    options = define_options(self,options)
     local containedby = options.containedby
    if containedby == nil then 
        local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d)
        containedby = {M(x1,y1,z1), M(x2,y2,z2)}
    end
    
    local max_grad = function(f, box)
        local M1, M2 
        if type(box[2]) == "number" then 
            local C, r = table.unpack(box)
            M1, M2 = M(C.x-r,C.y-r,C.z-r), M(C.x+r,C.y+r,C.z+r)
        else M1, M2 = table.unpack(box)
        end
        local x1,x2,y1,y2,z1,z2 = M1.x, M2.x, M1.y, M2.y, M1.z, M2.z
        local nb = 20
        local pasx, pasy, pasz = (x2-x1)/(nb-1), (y2-y1)/(nb-1), (z2-z1)/(nb-1)
        local min, max = math.huge, -math.huge
        for x = x1, x2, pasx do
                for y = y1, y2, pasy do
                    for z = z1, z2, pasz do
                            local m = math.abs(f(x,y,z))
                            if max <m then max = m end
                            if min > m then min = m end
                    end
                end
        end
        return tostring((max+min)/3)
    end
    
    local M1, M2 = table.unpack( containedby )
    pov_writeln("\n#declare "..options.name.." =  isosurface{ ")
    pov_writeln("   function{"..povfunc.."}")
    if pt3d.isPoint3d(M2) then 
        pov_writeln("    contained_by{ box{ ".. strdot2(M1).." "..strdot2(M2).."} }")
    else
         pov_writeln("    contained_by{ sphere{ ".. strdot2(M1)..", ".. tostring(M2).."} }")
    end
    pov_writeln("    open") 
    pov_writeln("    evaluate "..max_grad(luafunc,containedby)..", 1.5, 0.7") 
    write_matrix2(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end


--------------- parametric surface ----------------------------

function graph3d:Pov_surface1( xfunc, yfunc, zfunc, u1, u2, v1, v2, options)
-- xfunc = string representing x(u,v), yfunc = string representing y(u,v), zfunc = string representing z(u,v)
-- options = {clipbox= default, clipplane, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, max_grad=default, containedby=default win3d}
    options = define_options(self,options)
    local containedby = options.containedby
    if containedby == nil then 
        local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d)
        containedby = {M(x1,y1,z1), M(x2,y2,z2)}
    end
    local M1, M2 = table.unpack( containedby )
    pov_writeln("\n#declare "..options.name.." = parametric{")
    pov_writeln("    function{"..xfunc.."},")
    pov_writeln("    function{"..yfunc.."},")
    pov_writeln("    function{"..zfunc.."}")
    pov_writeln("    <"..tostring(u1)..","..tostring(v1)..">, <"..tostring(u2)..','..tostring(v2)..">")
    if pt3d.isPoint3d(M2) then 
        pov_writeln("    contained_by{ box{ ".. strdot2(M1).." ".. strdot2(M2).."} }")
    else
         pov_writeln("    contained_by{ sphere{ ".. strdot2(M1)..", ".. tostring(M2).."} }")
    end
    if max_grad ~= nil then
        pov_writeln("    max_gradient "..tostring(max_grad)) 
    end
    write_matrix2(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_surface2(f,u1,u2,v1,v2,options)
-- draw the surface (u,v) -> f(u,v) dans R^3 using normals
-- u1 et u2 sont les bornes pour u, et v1, v2 pour v
-- options = {grid={25,25}, clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, clip=false}
    options = define_options(self,options)
    local grid = options.grid or {25,25}
    local F = ld.obj_surface(f,u1,u2,v1,v2,grid)
    if F == nil then return end
    local clip = options.clip or false
    if clip then 
        local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d)
        options.clipbox = {M(x1,y1,z1), M(x2,y2,z2)}
    end
    pov_writeln("\n#declare "..options.name.." = mesh2{")
    pov_writeln("         vertex_vectors{ "..tostring(#F.vertices)..",")
    writedotlist(F.vertices)
    pov_writeln("                       }")
    pov_writeln("         normal_vectors{ "..tostring(#F.normals)..",")
    writedotlist(F.normals)
    pov_writeln("                       }")
    pov_writeln("         face_indices{ "..tostring(#F.facets)..",")
    writefacetlist(F.facets)
    pov_writeln("                       }")
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_surface( xfunc, yfunc, zfunc, u1, u2, v1, v2, options)
    if type(yfunc) == "string" then
        self:Pov_surface1( xfunc, yfunc, zfunc, u1, u2, v1, v2, options)
    else
        self:Pov_surface2( xfunc, yfunc, zfunc, u1, u2, v1) -- ici f=xfunc, u1=yfunc, u2=zfunc, v1=u1, v2=u2 et options=v1
    end
end

------------------- plane ------------------------------------------
function graph3d:Pov_plane(P, options) -- plane as facet
-- P = {A, N} A = dot on plane, N = normal vector
-- options = -- Pov_facet options, plus scale=1
    if P == nil then return  end
    options = options or {}
    options.scale = options.scale or 1
    local matrix = options.matrix or ld.ID3d
    matrix = ld.composematrix3d(self.matrix3d,matrix)
    local oldmatrix = self.matrix3d
    self.matrix3d = ld.ID3d
    local A, n = table.unpack(P)
    if not ld.isID3d(matrix) then
        A = ld.mtransform3d(A,matrix)
        n = ld.mLtransform3d(n,matrix)
    end
    options.matrix = nil
    local face = self:Plane2facet({A,n},options.scale)
    if face ~= nil then 
        self:Pov_facet(face, options)
    end
    self.matrix3d = oldmatrix
end

function graph3d:Pov_plane2( P, options)
-- P = {A, N} A = dot on plane, N = normal vector
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil}
    options = define_options(self,options)
    local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d)
    if options.clipbox == nil then 
        options.clipbox = {M(x1,y1,z1), M(x2,y2,z2)}
    else
        table.insert(options.clipbox,1,{M(x1,y1,z1), M(x2,y2,z2)})
    end
    local A, N = table.unpack( P )
    N = pt3d.normalize(N)
    local O = ld.proj3d(Origin, P)
    local d = pt3d.abs(O)
    if pt3d.dot(O,N) < 0 then d = -d end -- distance à l'origine
    pov_writeln("\n#declare "..options.name.." = plane{")
    pov_writeln("    "..strdot(N)..", "..tostring(d))    
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")    
end


-------------------------- facets -----------------------------------

function graph3d:Pov_facet(F,options)
-- F: polyhedron or list of facets
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, edge=false, edgecolor=default, edgewidth=default, hidden=default, hiddenstyle=default}
    options = define_options(self,options)
    if F == nil then return end
    if pt3d.isPoint3d(F[1]) then F = {F} end
    local render = options.render
    local edge = options.edge or false
    local edgecolor = options.edgecolor or ld.Black
    local edgewidth = options.edgewidth or self.param.linewidth
    local hidden = options.hidden or ld.Hiddenlines
    local hiddenscale = options.hiddenscale or ld.Hiddenlinescale
    local hiddenstyle = options.hiddenstyle or ld.Hiddenlinestyle
    local triangulate = function(F) --triangulate facets or a polyhedron, returns polyhedron
        if F == nil then return end
        local rep = {}
        if F.vertices ~= nil then --polyhedron
            --rep.vertices = table.copy(F.vertices)
            --rep.facets = {}
            for _,f in ipairs(F.facets) do
                if #f == 3 then table.insert(rep,f)
                else
                    local a, c, b = f[1], f[2]
                    for k = 3, #f do
                        b = c; c = f[k]
                        table.insert(rep,{a,b,c})
                    end
                end
            end
        else  -- facets
            for _,f in ipairs(F) do
                local n= #f
                if n == 3 then table.insert(rep, f) --triangle
                elseif n == 4 then -- quad
                    local a, b, c, d = f[1], f[2], f[3], f[4]
                    if pt3d.abs(c-a) < pt3d.abs(d-b) then
                        table.insert(rep, {a,c,d}); table.insert(rep, {c,a,b});
                    else
                        table.insert(rep, {b,d,a}); table.insert(rep, {d,b,c});
                    end
                else --more than 4 vertices
                    local a, c, b = f[1], f[2]
                    for k = 3, #f do
                        b = c; c = f[k]
                        table.insert(rep,{a,b,c})
                    end
                end
            end
        end
        return rep
    end
    
    local F1 = triangulate(F)
    if F.vertices ~= nil then -- polyhedron
        pov_writeln("\n#declare "..options.name.." = mesh2{")
        pov_writeln("         vertex_vectors{ "..tostring(#F.vertices)..",")
        writedotlist(F.vertices)
        pov_writeln("                       }")
        if F.normals ~= nil then
            pov_writeln("         normal_vectors{ "..tostring(#F.normals)..",")
            writedotlist(F.normals)
            pov_writeln("                       }")
        end
        pov_writeln("         face_indices{ "..tostring(#F1)..",")
        writefacetlist(F1)
        pov_writeln("                       }")
    else
        pov_writeln("\n#declare "..options.name.." = union{")
        for _, f in ipairs(F1) do
            pov_writeln("           triangle{ "..strdot(f[1]).." , "..strdot(f[2]).." , "..strdot(f[3]).." }")
        end
    end
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
    if edge then
        local L = ld.facetedges(F)
        self:Pov_polyline(L, {color=edgecolor, style=edgestyle, hidden=hidden, hiddenstyle=hiddenstyle, hiddenscale=hiddenscale, matrix=options.matrix, clipbox = options.clipbox, clipplane=options.clipplane, shadow=options.shadow})
    end
end

------------------------- geometric forms ----------------------------
function graph3d:Pov_box(A,B,options)
-- A = M(xmin,ymin,zmin), B = M(xmax,ymax,zmax)
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil}
    options = define_options(self,options)
    pov_writeln("\n#declare "..options.name.." = box{")
    pov_writeln("    "..strdot(A).." "..strdot(B))
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_torus( A,R,r,N,options)
-- A = center, R = big radius, r = small radius, N = normal vector
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil}
    options = define_options(self,options)
     local V, mat = pt3d.normalize(N) -- mat = matrix to correctly orient the torus
    if math.abs(V.y) == 1 then 
        mat = {A,vecI,vecJ,vecK} 
    else
        mat = ld.matrix3dof( function(X) return A+ld.rotate3d(X, math.acos(V.y)*ld.rad,{Origin, pt3d.prod(vecJ,V)}) end)
    end
    options.matrix = ld.composematrix3d(options.matrix, mat)
    pov_writeln("\n#declare "..options.name.." = torus{")
    pov_writeln("    ".. tostring(R)..", "..tostring(r))
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")    
end

function graph3d:Pov_sphere(A,R,options)
-- A = center, R = big radius, r = small radius, N = normal vector
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil}
    options = define_options(self,options)
    pov_writeln("\n#declare "..options.name.." = sphere{")
    pov_writeln("    "..strdot(A).." "..tostring(R))
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end


function graph3d:Pov_cylinder(A,R,B, options)
-- A = center, R = big radius, r = small radius
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, hollow=false}
    options = define_options(self,options)
    local hollow = options.hollow or false
    pov_writeln("\n#declare "..options.name.." = cylinder{")
    pov_writeln("    "..strdot(A).." "..strdot(B).." "..tostring(R))
    if hollow then pov_writeln("    open") end
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_cone(A,R,B,r,options)
-- A = center, R = big radius, r = small radius
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, hollow=false}
    if type(r) == "table" then options = r; r = 0 end
    options = define_options(self,options)
    local hollow = options.hollow or false
    pov_writeln("\n#declare "..options.name.." = cone{")
    pov_writeln("    "..strdot(A).." "..tostring(R).." "..strdot(B).." "..tostring(r))
    if hollow then pov_writeln("    open") end
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_circle(A,R,N,options)
-- A = center, R = radius, N = normal vector
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, width=8}
    options = options or {}
    local width = options.width or self.param.linewidth
    options.color = options.color or ld.Black
    local r = width*ld.pt/10
    self:Pov_torus(A,R,r,N,options)
end


----------------------- polyline ---------------------------------------
function graph3d:Pov_polyline(L, options)
-- L list of 3D points or list of lists of 3D points
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, width=8, close=false, arrows=0, hidden=default,hiddenstyle=defaut}
    if L == nil then return end
    options = options or {}
    options.color = options.color or ld.Black
    options = define_options(self,options)
    options.width = options.width or self.param.linewidth
    local width = options.width*ld.pt/10/self:Abs(1)*0.667
    local arrows = options.arrows or 0
    local arrowscale = options.arrowscale or Pov_arrowscale
    if type(arrowscale) == "number" then arrowscale = {arrowscale,arrowscale} end
    local close = options.close or false
    local style = options.style or self.param.linestyle
    if style == "noline" then return end
    local hidden = options.hidden or ld.Hiddenlines
    local hiddenstyle = options.hiddenstyle or ld.Hiddenlinestyle
    local hiddenscale = options.hiddenscale or ld.Hiddenlinescale
    
    if pt3d.isPoint3d(L[1]) then L = {L} end
    if not ld.isID3d(options.matrix) then L = ld.mtransform3d(L,options.matrix) end
    local listarrows, listdots
    local arrows_radius = (options.width*ld.pt/10+0.05)/self:Abs(1)
    local arrows_height = arrowscale[2]*arrows_radius*4 --0.25/self:Abs(1)
    arrows_radius = arrowscale[1]*arrows_radius
    
    local prepare_cp = function(cp)
        local aux = table.copy(cp)
        if close and (aux[1] ~= aux[#aux]) then table.insert(aux,aux[1]) end
        local A, B, h, d
        listarrows = {}
        local n = #aux
        if arrows >= 1 then
            A = aux[n-1]; B = table.remove(aux)
            d = pt3d.abs(B-A); h = arrows_height*pt3d.normalize(A-B)
            if d >= arrows_height then table.insert(aux,B+h) end
            table.insert(listarrows,{B+h,arrows_radius,B})
        end
        if arrows == 2 then
            A = table.remove(aux,1); B = aux[1]
            d = pt3d.abs(B-A); h = arrows_height*pt3d.normalize(B-A)
            if d >= arrows_height then table.insert(aux,A+h) end
            table.insert(listarrows,{A+h,arrows_radius,A})
        end
        listdots = aux
    end
    
    if options.clippbox ~= nil then 
        local M1,M2 = table.unpack( options.clippbox )
        local poly = ld.parallelep(M1, (M2.x-M1.x)*vecI,(M2.y-M1.y)*vecJ,(M2.z-M1.z)*vecK)
        L = clippolyline3d(L, poly, false, close)
    end
    if options.clipplane ~= nil then 
        L = ld.cutpolyline3d(L,options.clipplane,close)
    end
    pov_writeln("\n#declare "..options.name.." =  union{")
    for _, cp in ipairs(L) do
        prepare_cp(cp)
        for _,arr in ipairs(listarrows) do
            pov_writeln("    cone{ "..strdot(arr[1]).." "..tostring(arr[2]))
            pov_writeln("           "..strdot(arr[3]).." 0 }")
        end
        local B, A = listdots[1]
        if style == "solid" then
            for k = 2, #listdots do
                A = B; B = listdots[k]
                if pt3d.abs(B-A) > 1e-10 then
                    pov_writeln("    cylinder{"..strdot(A)..","..strdot(B).." "..tostring(width).."}")
                else B = A
                end
            end
        elseif style == "dotted" then
            local d = 8*width
            for k = 2, #listdots do
                A = B; B = listdots[k]
                local norm = pt3d.abs(B-A)
                if pt3d.abs(B-A) > 1e-10 then
                    local u = d*(B-A)/norm
                    local N = math.floor(norm/d)+1
                    local dep = A
                    for p = 1, N do
                        pov_writeln("    sphere{"..strdot(dep).." "..tostring(width).."}")
                        dep = dep+u
                    end
                else B = A
                end
            end
        elseif style == "dashed" then
            local d = 4*width
            for k = 2, #listdots do
                A = B; B = listdots[k]
                local norm = pt3d.abs(B-A)
                if norm > d then
                    local u = d*(B-A)/norm
                    local N = math.floor(pt3d.abs(B-A-4*u)/d/2)+1
                    local dep, fin = A, A+u/2
                    pov_writeln("    cylinder{"..strdot(dep)..","..strdot(fin).." "..tostring(width).."}")
                    dep = A+1.5*u; fin = dep+u
                    for p = 0, N do
                        pov_writeln("    cylinder{"..strdot(dep)..","..strdot(fin).." "..tostring(width).."}")
                        dep = dep+2*u; fin = fin+2*u
                    end
                    dep = B-u/2; fin = B
                    pov_writeln("    cylinder{"..strdot(dep)..","..strdot(fin).." "..tostring(width).."}")
                else B = A
                end
            end
        end
    end
    if options.render then write_rendering(options) end
    pov_writeln("  }")
    if hidden then
        local args = {}
        args.hidden = false 
        args.style = hiddenstyle; 
        args.width = options.width*hiddenscale
        args.arrows = 0
        args.name = options.name..'_hidden'
        args.color = options.color
        args.close = close
        self:Pov_polyline( ld.shift3d(L,500*self.Normal), args)
    end
end

------------------- axes -----------------------------------------------
function graph3d:Pov_axes(O, options)
    local x0,y0,z0 = O.x, O.y, O.z
    local rep = {}
    local x1,x2,y1,y2,z1,z2 = table.unpack(self.param.viewport3d)
    self:Pov_polyline({{M(x1,y0,z0),M(x2,y0,z0)}, {M(x0,y1,z0),M(x0,y2,z0)},{M(x0,y0,z1),M(x0,y0,z2)}}, options)
end

------------------ dots ------------------------------------------------
function graph3d:Pov_dots(L, options)
-- L = list of 3d points
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil,style="ball" or "box", dotscale=1}
    if L == nil then return end
    if pt3d.isPoint3d(L) then L = {L} end
    options = options or {}
    options.color = options.color or ld.Black
    options = define_options(self,options)
    local style = options.style or "ball"
    local dotscale = options.dotscale or 1
    local opacity = options.opacity or self.param.lineopacity
    if not ld.isID3d(options.matrix) then L = ld.mtransform3d(L,options.matrix) end
    local deb, fin = "", ""
    if #L > 1 then deb = "union{"; fin = "}" end
    pov_writeln("\n#declare "..options.name.." =  "..deb)
    if style == "ball" then
        local r = dotscale*0.075
        for _, A in ipairs(L) do 
            pov_writeln("    sphere{"..strdot(A).." "..tostring(r)..fin)
        end
    elseif style == "box" then
        local d = dotscale*0.15/2
        for _, A in ipairs(L) do 
            pov_writeln("    box{"..strdot(M(A.x-d,A.y-d,A.z-d)).." "..strdot(M(A.x+d,A.y+d,A.z+d))..fin)
        end
    end
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

----------------- CSG : union, intersection, difference, merge ---------
function graph3d:Pov_union(list, options)
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil}
    options = define_options(self,options)
    pov_writeln("\n#declare "..options.name.." = union{")
    for _,obj in ipairs(list) do
        pov_writeln("    object { "..obj.." }")
    end
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_intersection(list, options)
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, hollow=false}
    options = define_options(self,options)
    local render = options.render
     pov_writeln("\n#declare "..options.name.." = intersection{")
    for _,obj in ipairs(list) do
        pov_writeln("    object { "..obj.." }")
    end
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_merge(list, options)
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, hollow=false}
    options = define_options(self,options)
    pov_writeln("\n#declare "..options.name.." = merge{")
    for _,obj in ipairs(list) do
        pov_writeln("    object { "..obj.." }")
    end
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

function graph3d:Pov_difference(list, options)
-- options = {clipbox= nil, clipplane=nil, render=1, name=default, matrix=nil, color=White (rgb table), opacity=1, ambient=0.35, diffuse=0.8, phong=0.5, shadow=true, mytexture=nil, hollow=false}
    options = define_options(self,options)
    pov_writeln("\n#declare "..options.name.." = difference{")
    pov_writeln("    object { "..list[1].." }")
    pov_writeln("    object { "..list[2].." }")
    write_matrix(options.matrix)
    write_modifiers_rendering(options)
    pov_writeln("  }")
end

------------------- user ------------------------------------------
function graph3d:Pov_comment(comment) -- writes comment in pov source
    pov_writeln("//"..comment)
end    

function graph3d:Pov_special(user_content) -- writes directly into the source code.
    pov_writeln(user_content)
end
