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