Page MenuHome

asymptote_export.py

File Metadata

Author
Scott Pakin (pakin)
Created
Nov 13 2013, 2:28 PM

asymptote_export.py

#!BPY
"""
Name: 'Asymptote (.asy)...'
Blender: 246
Group: 'Export'
Tooltip: 'Export scene to the Asymptote vector graphics language (.asy)'
"""
__author__ = ("Scott Pakin")
__url__ = ["Author's site, http://www.pakin.org/~scott"]
__email__ = ["scott.blend@pakin.org"]
__version__ = "1.0"
__bpydoc__ = """\
This script exports an Asymptote source file.
Usage:
Run this script from "File->Export" menu.
"""
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
# USA.
#
# ***** END GPL LICENCE BLOCK *****
import Blender
import BPyObject
import BPyMesh
import bpy
import math
import os
# Specify the approximate size for the output image in PostScript
# units (i.e., units of 0.13889" or 0.35278 mm).
IMAGESIZE = 500
def objAbbrev(obj):
"Return an object's two-letter abbreviation."
abbrevExceptions = {"Surf": "CU",
"Text": "CU",
"Lattice": "LT"}
try:
return abbrevExceptions[obj.type]
except KeyError:
return str.upper(obj.type[0:2])
def rotateVector(direction, rotation):
"Rotate a 3-D vector in three dimensions."
dx, dy, dz = direction
rx, ry, rz = rotation
dy, dz = \
dy*math.cos(rx) - dz*math.sin(rx), \
dy*math.sin(rx) + dz*math.cos(rx)
dx, dz = \
dx*math.cos(ry) + dz*math.sin(ry), \
dx*math.sin(ry) + dz*math.cos(ry)
dx, dy = \
dx*math.cos(rz) - dy*math.sin(rz), \
dx*math.sin(rz) + dy*math.cos(rz)
return (dx, dy, dz)
def writeBoilerplate(outfile, curScene):
"Write some Asymptote boilerplate at the top of the output file."
filebase = Blender.Get("filename")
if filebase == "":
filebase = "Untitled"
else:
filebase = os.path.basename(filebase)
outfile.write("/*\n")
outfile.write(" * Asymptote export of %s, SCE:%s\n" % (filebase, curScene.name))
outfile.write(" * produced using asymptote_export.py %s\n" % __version__)
outfile.write(" * by %s, %s\n" % (__author__, __email__[0]))
outfile.write(" */\n")
outfile.write("""
import three;
size(%.5g);
""" % IMAGESIZE)
def writeCamera(outfile, cameraObj):
"Write the camera's type and position as Asymptote source code."
cameraData = cameraObj.getData()
outfile.write("// CA:%s, OB:%s\n" % (cameraData.name, cameraObj.name))
if cameraData.type == "ortho":
view = "orthographic"
else:
view = "perspective"
# The challenge here is that Blender stores the camera's X, Y, and
# Z rotation while Asymptote wants a target point that at which
# the camera is pointing and an "up" vector. Linear algebra to
# the rescue!
rotx, roty, rotz = cameraObj.RotX, cameraObj.RotY, cameraObj.RotZ
if view == "perspective":
tx, ty, tz = rotateVector((0, 0, -cameraData.clipEnd), (rotx, roty, rotz))
tx, ty, tz = tx + cameraObj.LocX, ty + cameraObj.LocY, tz + cameraObj.LocZ
ux, uy, uz = rotateVector((0, 1, 0), (rotx, roty, rotz))
# Set the current projection to the current camera.
cpstr = "currentprojection = %s" % view
cpstr_space = " " * len(cpstr)
outfile.write("%s(camera = (%.5g, %.5g, %.5g),\n" % \
(cpstr, cameraObj.LocX, cameraObj.LocY, cameraObj.LocZ))
if view == "perspective":
outfile.write("%s target = (%.5g, %.5g, %.5g),\n" % (cpstr_space, tx, ty, tz))
outfile.write("%s autoadjust = false,\n" % cpstr_space)
outfile.write("%s showtarget = false,\n" % cpstr_space)
outfile.write("%s up = (%.5g, %.5g, %.5g));\n" % (cpstr_space, ux, uy, uz))
def writeLampsAndWorld(outfile, allLamps, world):
"Write all lamps and the world (if any) as Asymptote source code."
# Get the ambient and background colors from the world.
if world != None:
# Asymptote accepts only a single background color, not a
# gradient. We therefore average the horizon and zenith
# colors to produce a single background color for Asymptote.
hor = world.getHor()
zen = world.getZen()
wr = (hor[0] + zen[0]) / 2.0
wg = (hor[1] + zen[1]) / 2.0
wb = (hor[2] + zen[2]) / 2.0
haveAmbient = world.amb[0] != 0 or world.amb[1] != 0 or world.amb[2] != 0
ambientStr = "rgb(%.5g, %.5g, %.5g)" % (world.amb[0], world.amb[1], world.amb[2])
backgroundStr = "rgb(%.5g, %.5g, %.5g)" % (wr, wg, wb)
# Handle the no-lamps case.
if allLamps == []:
if world == None:
# No lamps and no world.
outfile.write("// The input scene doesn't contain any lamps. We therefore\n")
outfile.write("// draw all objects shadeless.\n")
outfile.write("currentlight = nolight;\n")
else:
# No lamps but a world.
outfile.write("// WO:%s\n" % world.name)
outfile.write("currentlight = light(background = %s" % backgroundStr)
if haveAmbient:
outfile.write(",\n ambient = %s" % ambientStr)
outfile.write(");\n")
return
# Determine the characteristics of all lamps in the scene.
comments = []
positions = []
diffuse = []
specular = []
for lampObj in allLamps:
# The challenge here is that Blender stores the lamp's X, Y,
# and Z rotation while Asymptote wants three points that
# define a plane. Linear algebra to the rescue again!
lampData = lampObj.getData()
comments.append("LA:%s, OB:%s" % (lampData.name, lampObj.name))
positions.append((lampObj.LocX, lampObj.LocY, lampObj.LocZ))
if lampData.mode & lampData.Modes["OnlyShadow"] != 0:
# The lamp doesn't emit any light. In Asymptote we
# represent this by having it emit black. (We currently
# have no control over shadows in Asymptote.)
diffuse.append((0, 0, 0))
specular.append((0, 0, 0))
else:
# The lamp emits light. We convert its diffuse and
# specular colors to Asymptote. Unlike Asymptote, Blender
# does not associcate an ambient color with a lamp (as far
# I know) so we leave the ambient field blank.
if lampData.mode & lampData.Modes["NoDiffuse"] == 0:
diffuse.append(tuple(lampData.col))
else:
diffuse.append((0, 0, 0))
if lampData.mode & lampData.Modes["NoSpecular"] == 0:
specular.append(tuple(lampData.col))
else:
specular.append((0, 0, 0))
# Output a single Asymptote light that represents all Blender lamps.
for cStr in comments:
outfile.write("// %s\n" % cStr)
if world != None:
outfile.write("// WO:%s\n" % world.name)
diffuseStr = str.join(", ", ["rgb(%.5g, %.5g, %.5g)" % dif for dif in diffuse])
specularStr = str.join(", ", ["rgb(%.5g, %.5g, %.5g)" % spec for spec in specular])
positionStr = str.join(", ", ["(%.5g, %.5g, %.5g)" % pos for pos in positions])
if len(allLamps) == 1:
# Single lamp -- don't bother creating arrays.
outfile.write("currentlight = light(diffuse = %s,\n" % diffuseStr)
outfile.write(" specular = %s,\n" % specularStr)
if world != None:
if haveAmbient:
outfile.write(" ambient = %s,\n" % ambientStr)
outfile.write(" background = %s,\n" % backgroundStr)
outfile.write(" %s);\n" % positionStr[1:-1])
else:
# Multiple lamps -- create arrays.
outfile.write("currentlight = light(diffuse = new pen[] {%s},\n" % diffuseStr)
outfile.write(" specular = new pen[] {%s},\n" % specularStr)
if world != None:
if haveAmbient:
outfile.write(" ambient = array(%d, %s),\n" % (len(allLamps), ambientStr))
outfile.write(" background = %s,\n" % backgroundStr)
outfile.write(" position = new triple[] {%s});\n" % positionStr)
def writeMesh(outfile, meshObj):
"""
Write a single mesh as Asymptote source code. Non-meshes are
converted to meshes.
"""
# If we were given a non-mesh, convert it to a mesh.
origData = meshObj.getData(mesh=True)
meshName = origData.name
if meshObj.type == "Mesh":
meshData = origData
else:
meshData = bpy.data.meshes.new("Converted " + meshName)
meshData.getFromObject(meshObj)
# Remap all vertices to global coordinates.
origVerts = meshData.verts[:]
meshData.transform(meshObj.matrixWorld)
# Acquire the list of materials used by this mesh.
try:
# Most objects provide a list of materials.
materials = origData.materials
except AttributeError:
# I don't know how to get a Text object's materials in Blender 2.46
# so Text objects are left uncolored. Sorry.
materials = []
colbits = meshObj.colbits
if colbits > 0:
objMats = meshObj.getMaterials(1)
for i in range(0, 16):
if colbits & (1<<i) == 1:
materials[i] = objMats[i]
# Write each face in turn.
outfile.write("// %s:%s, OB:%s\n" % (objAbbrev(meshObj), meshName, meshObj.name))
for face in meshData.faces:
# Determine how to color the face -- either per-vertex colors,
# a single color for the entire face, or no specified color.
surfaceOpts = ""
drawOpts = ""
if meshData.vertexColors:
# Each vertex has its own color.
surfaceOpts = ",\n new pen[] {"
surfaceOpts += str.join(", ", ["rgb(%.5g, %.5g, %.5g)" % (vcol.r/255.0, vcol.g/255.0, vcol.b/255.0) for vcol in face.col])
surfaceOpts += "}"
elif 0 <= face.mat < len(materials) and materials[face.mat] != None:
# The whole face has a single color.
drawOpts = ",\n rgb(%.5g, %.5g, %.5g)" % tuple(materials[face.mat].rgbCol)
else:
# The default material is 80% gray.
drawOpts = ",\n gray(0.8)"
# Construct a surface from the face's vertices.
path3 = []
for vert in face.verts:
path3.append("(%.5g, %.5g, %.5g)" % (vert.co.x, vert.co.y, vert.co.z))
surfaceStr = "surface(%s--cycle%s)" % (str.join("--", path3), surfaceOpts)
# Tell Asymptote to draw the face.
outfile.write("draw(%s%s);\n" % (surfaceStr, drawOpts))
# Remap all vertices back to local coordinates.
meshData.verts = origVerts
def writeOtherObjects(outfile, otherObjs):
"Write a comment that lists all of the objects we're ignoring."
if otherObjs == []:
return
outfile.write("/*\n")
outfile.write(" * Other objects encountered in the scene:\n")
otherText = []
for obj in otherObjs:
data = obj.getData()
if data:
otherText.append("%s:%s, OB:%s" % \
(objAbbrev(obj), data.name, obj.name))
else:
otherText.append("OB:%s" % obj.name)
otherText.sort()
for oText in otherText:
outfile.write(" * - %s\n" % oText)
outfile.write(" */\n")
def writeASY(filename):
"Write the camera and all meshes as Asymptote source code."
# Open the output file.
if Blender.sys.exists(filename):
if Blender.Draw.PupMenu("Save over%t|" + filename) == -1:
return
outfile = file(filename, "w")
# Acquire lists of all the "stuff" in the scene.
curScene = bpy.data.scenes.active
curCamera = curScene.objects.camera
curWorld = curScene.world
meshes = [] # Meshes and objects that can be converted to meshes
lamps = []
otherObjs = []
for obj in curScene.objects:
if filter(lambda e: e in obj.layers, curScene.layers) == []:
# Ignore objects that do not appear in any active layer.
otherObjs.append(obj)
elif obj.type in ["Mesh", "Curve", "Surf", "MBall", "Text"]:
meshes.append(obj)
elif obj.type == "Lamp":
lamps.append(obj)
elif obj.type == "Camera" and obj == curCamera:
pass
else:
otherObjs.append(obj)
# Convert all objects we can to Asymptote format.
writeBoilerplate(outfile, curScene)
outfile.write("\n")
writeCamera(outfile, curCamera)
outfile.write("\n")
writeLampsAndWorld(outfile, lamps, curWorld)
outfile.write("\n")
for meshObj in meshes:
writeMesh(outfile, meshObj)
outfile.write("\n")
writeOtherObjects(outfile, otherObjs)
outfile.close()
if __name__ == '__main__':
# Let the user select the output file.
Blender.Window.FileSelector(writeASY, "Export ASY", Blender.sys.makename(ext=".asy"))

Event Timeline