lpdf-u3d.lua /size: 16 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['lpdf-u3d'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- The following code is based on a working prototype provided
10-- by Michael Vidiassov. It is rewritten using the lpdf library
11-- and different checking is used. The macro calls are adapted
12-- (and will eventually be removed). The user interface needs
13-- an overhaul. There are some messy leftovers that will be
14-- removed in future versions.
15
16-- For some reason no one really tested this code so at some
17-- point we will end up with a reimplementation. For instance
18-- it makes sense to add the same activation code as with swf.
19
20local tonumber = tonumber
21local formatters, find = string.formatters, string.find
22local cos, sin, sqrt, pi, atan2, abs = math.cos, math.sin, math.sqrt, math.pi, math.atan2, math.abs
23
24local backends, lpdf = backends, lpdf
25
26local nodeinjections           = backends.pdf.nodeinjections
27
28local pdfconstant              = lpdf.constant
29local pdfboolean               = lpdf.boolean
30local pdfunicode               = lpdf.unicode
31local pdfdictionary            = lpdf.dictionary
32local pdfarray                 = lpdf.array
33local pdfnull                  = lpdf.null
34local pdfreference             = lpdf.reference
35local pdfflushstreamobject     = lpdf.flushstreamobject
36local pdfflushstreamfileobject = lpdf.flushstreamfileobject
37
38local checkedkey               = lpdf.checkedkey
39local limited                  = lpdf.limited
40
41local embedimage               = images.embed
42
43local schemes = table.tohash {
44    "Artwork", "None", "White", "Day", "Night", "Hard",
45    "Primary", "Blue", "Red", "Cube", "CAD", "Headlamp",
46}
47
48local modes = table.tohash {
49    "Solid", "SolidWireframe", "Transparent", "TransparentWireframe", "BoundingBox",
50    "TransparentBoundingBox", "TransparentBoundingBoxOutline", "Wireframe",
51    "ShadedWireframe", "HiddenWireframe", "Vertices", "ShadedVertices", "Illustration",
52    "SolidOutline", "ShadedIllustration",
53}
54
55local function normalize(x, y, z)
56    local modulo = sqrt(x*x + y*y + z*z);
57    if modulo ~= 0 then
58        return x/modulo, y/modulo, z/modulo
59    else
60        return x, y, z
61    end
62end
63
64local function rotate(vect_x,vect_y,vect_z, tet, axis_x,axis_y,axis_z)
65    -- rotate vect by tet about axis counterclockwise
66    local c, s = cos(tet*pi/180), sin(tet*pi/180)
67    local r = 1 - c
68    local n = sqrt(axis_x*axis_x+axis_y*axis_y+axis_z*axis_z)
69    axis_x, axis_y, axis_z = axis_x/n, axis_y/n, axis_z/n
70    return
71        (axis_x*axis_x*r+c       )*vect_x + (axis_x*axis_y*r-axis_z*s)*vect_y + (axis_x*axis_z*r+axis_y*s)*vect_z,
72        (axis_x*axis_y*r+axis_z*s)*vect_x + (axis_y*axis_y*r+c       )*vect_y + (axis_y*axis_z*r-axis_x*s)*vect_z,
73        (axis_x*axis_z*r-axis_y*s)*vect_x + (axis_y*axis_z*r+axis_x*s)*vect_y + (axis_z*axis_z*r+c       )*vect_z
74end
75
76local function make3dview(view)
77
78    local name = view.name
79    local name = pdfunicode(name ~= "" and name or "unknown view")
80
81    local viewdict = pdfdictionary {
82        Type = pdfconstant("3DView"),
83        XN   = name,
84        IN   = name,
85        NR   = true,
86    }
87
88    local bg = checkedkey(view,"bg","table")
89    if bg then
90        viewdict.BG = pdfdictionary {
91            Type = pdfconstant("3DBG"),
92            C    = pdfarray { limited(bg[1],1,1,1), limited(bg[2],1,1,1), limited(bg[3],1,1,1) },
93        }
94    end
95
96    local lights = checkedkey(view,"lights","string")
97    if lights and schemes[lights] then
98        viewdict.LS =  pdfdictionary {
99            Type    = pdfconstant("3DLightingScheme"),
100            Subtype = pdfconstant(lights),
101        }
102    end
103
104    -- camera position is taken from 3d model
105
106    local u3dview = checkedkey(view, "u3dview", "string")
107    if u3dview then
108        viewdict.MS      = pdfconstant("U3D")
109        viewdict.U3DPath = u3dview
110    end
111
112    -- position the camera as given
113
114    local c2c      = checkedkey(view, "c2c", "table")
115    local coo      = checkedkey(view, "coo", "table")
116    local roo      = checkedkey(view, "roo", "number")
117    local azimuth  = checkedkey(view, "azimuth", "number")
118    local altitude = checkedkey(view, "altitude", "number")
119
120    if c2c or coo or roo or azimuth or altitude then
121
122        local pos  = checkedkey(view, "pos", "table")
123        local dir  = checkedkey(view, "dir", "table")
124        local upv  = checkedkey(view, "upv", "table")
125        local roll = checkedkey(view, "roll", "table")
126
127        local coo_x, coo_y, coo_z       = 0, 0, 0
128        local dir_x, dir_y, dir_z       = 0, 0, 0
129        local trans_x, trans_y, trans_z = 0, 0, 0
130        local left_x, left_y, left_z    = 0, 0, 0
131        local up_x, up_y, up_z          = 0, 0, 0
132
133        -- point camera is aimed at
134
135        if coo then
136            coo_x, coo_y, coo_z = tonumber(coo[1]) or 0, tonumber(coo[2]) or 0, tonumber(coo[3]) or 0
137        end
138
139        -- distance from camera to target
140
141        if roo then
142           roo = abs(roo)
143        end
144        if not roo or roo == 0 then
145            roo = 0.000000000000000001
146        end
147
148        -- set it via camera position
149
150        if pos then
151            dir_x = coo_x - (tonumber(pos[1]) or 0)
152            dir_y = coo_y - (tonumber(pos[2]) or 0)
153            dir_z = coo_z - (tonumber(pos[3]) or 0)
154            if not roo then
155                roo = sqrt(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z)
156            end
157            if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end
158            dir_x, dir_y, dir_z = normalize(dir_x,dir_y,dir_z)
159        end
160
161        -- set it directly
162
163        if dir then
164            dir_x, dir_y, dir_z = tonumber(dir[1] or 0), tonumber(dir[2] or 0), tonumber(dir[3] or 0)
165            if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end
166            dir_x, dir_y, dir_z = normalize(dir_x,dir_y,dir_z)
167        end
168
169        -- set it movie15 style with vector from target to camera
170
171        if c2c then
172            dir_x, dir_y, dir_z = - tonumber(c2c[1] or 0), - tonumber(c2c[2] or 0), - tonumber(c2c[3] or 0)
173            if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end
174            dir_x, dir_y, dir_z = normalize(dir_x,dir_y,dir_z)
175        end
176
177        -- set it with azimuth and altitutde
178
179        if altitude or azimuth then
180            dir_x, dir_y, dir_z = -1, 0, 0
181            if altitude then  dir_x, dir_y, dir_z = rotate(dir_x,dir_y,dir_z, -altitude, 0,1,0) end
182            if azimuth  then  dir_x, dir_y, dir_z = rotate(dir_x,dir_y,dir_z,  azimuth,  0,0,1) end
183        end
184
185        -- set it with rotation like in MathGL
186
187        if rot then
188            if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_z = -1 end
189            dir_x,dir_y,dir_z = rotate(dir_x,dir_y,dir_z, tonumber(rot[1]) or 0, 1,0,0)
190            dir_x,dir_y,dir_z = rotate(dir_x,dir_y,dir_z, tonumber(rot[2]) or 0, 0,1,0)
191            dir_x,dir_y,dir_z = rotate(dir_x,dir_y,dir_z, tonumber(rot[3]) or 0, 0,0,1)
192        end
193
194        -- set it with default movie15 orientation looking up y axis
195
196        if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end
197
198        -- left-vector
199        -- up-vector
200
201        if upv then
202            up_x, up_y, up_z = tonumber(upv[1]) or 0, tonumber(upv[2]) or 0, tonumber(upv[3]) or 0
203        else
204            -- set default up-vector
205            if abs(dir_x) == 0 and abs(dir_y) == 0 then
206                if dir_z < 0 then
207                    up_y =  1 -- top view
208                else
209                    up_y = -1 -- bottom view
210                end
211            else
212                -- other camera positions than top and bottom, up-vector = up_world - (up_world dot dir) dir
213                up_x, up_y, up_z = - dir_z*dir_x, - dir_z*dir_y, - dir_z*dir_z + 1
214            end
215        end
216
217        -- normalize up-vector
218
219        up_x, up_y, up_z = normalize(up_x,up_y,up_z)
220
221        -- left vector = up x dir
222
223        left_x, left_y, left_z = dir_z*up_y - dir_y*up_z, dir_x*up_z - dir_z*up_x, dir_y*up_x - dir_x*up_y
224
225        -- normalize left vector
226
227        left_x, left_y, left_z = normalize(left_x,left_y,left_z)
228
229        -- apply camera roll
230
231        if roll then
232            local sinroll = sin((roll/180.0)*pi)
233            local cosroll = cos((roll/180.0)*pi)
234            left_x = left_x*cosroll + up_x*sinroll
235            left_y = left_y*cosroll + up_y*sinroll
236            left_z = left_z*cosroll + up_z*sinroll
237            up_x = up_x*cosroll + left_x*sinroll
238            up_y = up_y*cosroll + left_y*sinroll
239            up_z = up_z*cosroll + left_z*sinroll
240        end
241
242        -- translation vector
243
244        trans_x, trans_y, trans_z = coo_x - roo*dir_x, coo_y - roo*dir_y, coo_z - roo*dir_z
245
246        viewdict.MS  = pdfconstant("M")
247        viewdict.CO  = roo
248        viewdict.C2W = pdfarray {
249             left_x, left_y, left_z,
250             up_x, up_y, up_z,
251             dir_x, dir_y,  dir_z,
252             trans_x, trans_y, trans_z,
253        }
254
255    end
256
257    local aac = tonumber(view.aac) -- perspective projection
258    local mag = tonumber(view.mag) -- ortho projection
259
260    if aac and aac > 0 and aac < 180 then
261        viewdict.P = pdfdictionary {
262            Subtype = pdfconstant("P"),
263            PS      = pdfconstant("Min"),
264            FOV     = aac,
265        }
266    elseif mag and mag > 0 then
267        viewdict.P = pdfdictionary {
268            Subtype = pdfconstant("O"),
269            OS      = mag,
270        }
271    end
272
273    local mode = modes[view.rendermode]
274    if mode then
275        pdfdictionary {
276            Type    = pdfconstant("3DRenderMode"),
277            Subtype = pdfconstant(mode),
278        }
279    end
280
281    -- crosssection
282
283    local crosssection = checkedkey(view,"crosssection","table")
284    if crosssection then
285        local crossdict = pdfdictionary {
286            Type = pdfconstant("3DCrossSection")
287        }
288
289        local c = checkedkey(crosssection,"point","table") or checkedkey(crosssection,"center","table")
290        if c then
291            crossdict.C = pdfarray { tonumber(c[1]) or 0, tonumber(c[2]) or 0, tonumber(c[3]) or 0 }
292        end
293
294        local normal = checkedkey(crosssection,"normal","table")
295        if normal then
296            local x, y, z = tonumber(normal[1] or 0), tonumber(normal[2] or 0), tonumber(normal[3] or 0)
297            if sqrt(x*x + y*y + z*z) == 0 then
298                x, y, z = 1, 0, 0
299            end
300            crossdict.O = pdfarray {
301                pdfnull,
302                atan2(-z,sqrt(x*x + y*y))*180/pi,
303                atan2(y,x)*180/pi,
304            }
305        end
306
307        local orient = checkedkey(crosssection,"orient","table")
308        if orient then
309            crossdict.O = pdfarray {
310                tonumber(orient[1]) or 1,
311                tonumber(orient[2]) or 0,
312                tonumber(orient[3]) or 0,
313            }
314        end
315
316        crossdict.IV = cross.intersection or false
317        crossdict.ST = cross.transparent or false
318
319        viewdict.SA = next(crossdict) and pdfarray { crossdict } -- maybe test if # > 1
320    end
321
322    local nodes = checkedkey(view,"nodes","table")
323    if nodes then
324        local nodelist = pdfarray()
325        for i=1,#nodes do
326            local node = checkedkey(nodes,i,"table")
327            if node then
328                local position = checkedkey(node,"position","table")
329                nodelist[#nodelist+1] = pdfdictionary {
330                    Type = pdfconstant("3DNode"),
331                    N    = node.name or ("node_" .. i), -- pdfunicode ?
332                    M    = position and #position == 12 and pdfarray(position),
333                    V    = node.visible or true,
334                    O    = node.opacity or 0,
335                    RM   = pdfdictionary {
336                        Type    = pdfconstant("3DRenderMode"),
337                        Subtype = pdfconstant(node.rendermode or "Solid"),
338                    },
339                }
340            end
341      end
342      viewdict.NA = nodelist
343    end
344
345   return viewdict
346
347end
348
349local stored_js, stored_3d, stored_pr, streams = { }, { }, { }, { }
350
351local f_image = formatters["q /GS gs %.6N 0 0 %.6N 0 0 cm /IM Do Q"]
352
353local function insert3d(spec) -- width, height, factor, display, controls, label, foundname
354
355    local width, height, factor = spec.width, spec.height, spec.factor or number.dimenfactors.bp
356    local display, controls, label, foundname = spec.display, spec.controls, spec.label, spec.foundname
357
358    local param       = (display  and parametersets[display])  or { }
359    local streamparam = (controls and parametersets[controls]) or { }
360    local name        = "3D Artwork " .. (param.name or label or "Unknown")
361
362    local activationdict = pdfdictionary {
363       TB = pdfboolean(param.toolbar,true),
364       NP = pdfboolean(param.tree,false),
365    }
366
367    local stream = streams[label]
368    if not stream then
369
370        local subtype, subdata = "U3D", io.loaddata(foundname) or ""
371        if find(subdata,"^PRC") then
372            subtype = "PRC"
373        elseif find(subdata,"^U3D") then
374            subtype = "U3D"
375        elseif file.suffix(foundname) == "prc" then
376            subtype = "PRC"
377        end
378
379        local attr = pdfdictionary {
380            Type    = pdfconstant("3D"),
381            Subtype = pdfconstant(subtype),
382        }
383        local streamviews = checkedkey(streamparam, "views", "table")
384        if streamviews then
385            local list = pdfarray()
386            for i=1,#streamviews do
387                local v = checkedkey(streamviews, i, "table")
388                if v then
389                    list[#list+1] = make3dview(v)
390                end
391            end
392            attr.VA = list
393        end
394        if checkedkey(streamparam, "view", "table") then
395            attr.DV = make3dview(streamparam.view)
396        elseif checkedkey(streamparam, "view", "string") then
397            attr.DV = streamparam.view
398        end
399        local js = checkedkey(streamparam, "js", "string")
400        if js then
401            local jsref = stored_js[js]
402            if not jsref then
403                jsref = pdfflushstreamfileobject(js)
404                stored_js[js] = jsref
405            end
406            attr.OnInstantiate = pdfreference(jsref)
407        end
408        stored_3d[label] = pdfflushstreamfileobject(foundname,attr)
409        stream = 1
410    else
411       stream = stream + 1
412    end
413    streams[label] = stream
414
415    local name = pdfunicode(name)
416
417    local annot  = pdfdictionary {
418        Subtype  = pdfconstant("3D"),
419        T        = name,
420        Contents = name,
421        NM       = name,
422        ["3DD"]  = pdfreference(stored_3d[label]),
423        ["3DA"]  = activationdict,
424    }
425    if checkedkey(param,"view","table") then
426        annot["3DV"] = make3dview(param.view)
427    elseif checkedkey(param,"view","string") then
428        annot["3DV"] = param.view
429    end
430
431    local preview = checkedkey(param,"preview","string")
432    if preview then
433        activationdict.A = pdfconstant("XA")
434        local tag = formatters["%s:%s:%s"](label,stream,preview)
435        local ref = stored_pr[tag]
436        if not ref then
437            local figure = embedimage {
438                filename = preview,
439                width    = width,
440                height   = height
441            }
442            ref = figure.objnum
443            stored_pr[tag] = ref
444        end
445        if ref then -- see back-pdf ** .. here we have a local /IM !
446            local pw   = pdfdictionary {
447                Type      = pdfconstant("XObject"),
448                Subtype   = pdfconstant("Form"),
449                FormType  = 1,
450                BBox      = pdfarray { 0, 0, pdfnumber(factor*width), pdfnumber(factor*height) },
451                Matrix    = pdfarray { 1, 0, 0, 1, 0, 0 },
452                ProcSet   = lpdf.procset(),
453                Resources = pdfdictionary {
454                                XObject = pdfdictionary {
455                                    IM = pdfreference(ref)
456                                }
457                            },
458                ExtGState = pdfdictionary {
459                                GS = pdfdictionary {
460                                    Type = pdfconstant("ExtGState"),
461                                    CA   = 1,
462                                    ca   = 1,
463                                }
464                            },
465            }
466            local pwd = pdfflushstreamobject(f_image(factor*width,factor*height),pw)
467            annot.AP = pdfdictionary {
468                N = pdfreference(pwd)
469            }
470        end
471        return annot, figure, ref
472    else
473        activationdict.A = pdfconstant("PV")
474        return annot, nil, nil
475    end
476end
477
478function nodeinjections.insertu3d(spec)
479    local annotation, preview, ref = insert3d { -- just spec
480        foundname = spec.foundname,
481        width     = spec.width,
482        height    = spec.height,
483        factor    = spec.factor,
484        display   = spec.display,
485        controls  = spec.controls,
486        label     = spec.label,
487    }
488    node.write(nodeinjections.annotation(spec.width,spec.height,0,annotation()))
489end
490