local _version = '2.12.0' if tikz then if tikz.version ~= _version then error( 'version chaos' ) end return end tikz = { version = _version } --==========================================================================-- -- DEFINES --==========================================================================-- -- directions tikz.north = 'north' tikz.north_east = 'north east' tikz.east = 'east' tikz.south_east = 'south east' tikz.south = 'south' tikz.south_west = 'south west' tikz.west = 'west' tikz.north_west = 'north west' -- colors tikz.black = 'black' tikz.blue = 'blue' tikz.brown = 'brown' tikz.cyan = 'cyan' tikz.darkgray = 'darkgray' tikz.gray = 'gray' tikz.green = 'green' tikz.lightgray = 'lightgray' tikz.lime = 'lime' tikz.magenta = 'magenta' tikz.olive = 'olive' tikz.orange = 'orange' tikz.pink = 'pink' tikz.purple = 'purple' tikz.red = 'red' tikz.teal = 'teal' tikz.violet = 'violet' tikz.yellow = 'yellow' tikz.white = 'white' -- align tikz.center = 'center' tikz.left = 'left' tikz.right = 'right' tikz.none = 'none' -- line caps tikz.round = 'round' --==========================================================================-- -- HELPERS -- --==========================================================================-- -- if defined writes output here local outfile -- -- Prints arguments to tex and to stdout. -- local function tprint( ... ) local args = {...} print( table.concat( args ) ) if tex then tex.print( table.concat( args ) ) end if outfile then outfile:write( table.concat( args ) ) outfile:write( '\n%%% DO NOT EDIT THIS FILE %%%\n' ) end end tikz.tprint = tprint -- -- Flattens an object stack. -- -- The content of all numerated unclassed objects -- are put into a copy of obj. -- local function flatten( o ) local r = { } for _, v in ipairs( o ) do if type( v ) == 'table' and getmetatable( v ) == nil then local fv = flatten( v ) for _, fvv in ipairs( fv ) do table.insert( r, fvv ) end for k, vv in pairs( fv ) do if type( k ) ~= 'number' then r[ k ] = vv end end else table.insert( r, v ) end end -- copies (and overwrites) all named attributes for k, v in pairs( o ) do if type( k ) ~= 'number' then r[ k ] = v end end return r end -- -- A key marking an object having a zString function. -- local _zString = { } -- -- Converts everything to a tikz string. -- local function zString( v ) if type( v ) == 'table' and v[ _zString ] then return v[ _zString ]( v ) end return tostring( v ) end -- -- Creates an immutable class. -- local function immutable( definer ) local def = { } local lazy = { } local proto = { } def.lazy = lazy def.proto = proto definer( def ) -- the callee fills in data local k = { } -- key the hidden table local mt = { } mt.__index = function( self, key ) if key == 'id' then return def.id end -- first check if native value local v = self[ k ][ key ] if v then return v end -- then if a lazy function local f = lazy[ key ] if f then local v = f( self ) self[ k ][ key ] = v -- cache return v end -- then if a proto value v = proto[ key ] if v then if type( v ) == 'function' then return( function(...) return v( self, table.unpack( {...} ) ) end ) else return v end end -- otherwise -- error( 'invalid property' ) end mt.__newindex = function( ) error( 'this is an immutable' ) end if def.add then mt.__add = def.add end if def.concat then mt.__concat = def.concat end if def.div then mt.__div = def.div end if def.mul then mt.__mul = def.mul end if def.sub then mt.__sub = def.sub end if def.tostring then mt.__tostring = def.tostring end return function(...) local args = {...} local nt = def.constructor( table.unpack( args ) ) local o = { [ k ] = nt, [ _zString ] = def.zString } setmetatable( o, mt ) return o end end tikz.immutable = immutable -- -- writes out direct options -- local function _opts( s, _opts, haveOpts ) if not _opts then return haveOpts end for k, v in pairs( _opts ) do if not haveOpts then table.insert( s, '[' ) else table.insert( s, ',' ) end haveOpts = true if type( k ) == 'number' then table.insert( s, v ) else table.insert( s, k..'='..v ) end end return haveOpts end ------------------------------------------------------------------------------- -- Point. ------------------------------------------------------------------------------- tikz.p = immutable( function( def ) def.id = 'point' def.add = function( left, right ) if right.id == 'line' or right.id == 'bezier3' then return right + left end return tikz.p{ left.x + right.x, left.y + right.y } end def.concat = function( left, right ) return zString( left )..zString( right ) end -- constructor def.constructor = function( args ) return { x = args[ 1 ], y = args[ 2 ] } end -- divisor def.div = function( left, right ) return tikz.p{ left.x / right, left.y / right } end def.mul = function( left, right ) if type( right ) == 'number' then return tikz.p{ left.x * right, left.y * right } elseif type( left ) == 'number' then return tikz.p{ left * right.x, left * right.y } elseif type( right ) == 'table' then if right.id == 'point' then return tikz.p{ left.x * right.x, left.y * right.y } end end error( 'invalid operation' ) end def.sub = function( left, right ) return tikz.p{ left.x - right.x, left.y - right.y } end def.tostring = function( self ) return 'p{ '..self.x..', '..self.y..' }' end -- converts to a tikz string. def.zString = function( self ) return '('..self.x..','..self.y..')' end end ) ------------------------------------------------------------------------------- -- A full circle. ------------------------------------------------------------------------------- tikz.circle = immutable( function( def ) def.id = 'circle' -- constructor def.constructor = function( args ) return { at = args.at, radius = args.radius, } end -- converts to a tikz string. def.zString = function( self ) return zString( self.at )..' circle ('.. self.radius .. ')' end end ) ------------------------------------------------------------------------------- -- A quadratic bezier curve. ------------------------------------------------------------------------------- tikz.bezier2 = immutable( function( def ) def.id = 'bezier2' -- constructor def.constructor = function( args ) local p1 = args.p1 local pc = args.pc local p2 = args.p2 return { p1 = p1, pc = pc, p2 = p2, } end -- draws helping information def.proto.drawHelpers = function( self, prefix ) tikz.draw{ draw = 'red', tikz.line{ self.p1, self.pc } } tikz.draw{ draw = 'blue', tikz.line{ self.pc, self.p2 } } if prefix ~= nil then tikz.put{ tikz.node{ at = self.p1, anchor = 'north west', text = prefix .. '1', } } tikz.put{ tikz.node{ at = self.pc, anchor = 'north west', text = prefix .. 'c', } } tikz.put{ tikz.node{ at = self.p2, anchor = 'north west', text = prefix .. '2', } } end end -- converts to a tikz string. def.zString = function( self ) local s = { } table.insert( s, zString( self.p1 ) ) table.insert( s, '.. controls' ) table.insert( s, zString( self.pc ) ) table.insert( s, '..' ) table.insert( s, zString( self.p2 ) ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A cubic bezier curve. ------------------------------------------------------------------------------- tikz.bezier3 = immutable( function( def ) def.id = 'bezier3' def.add = function( left, right ) if right.id == 'point' then return tikz.bezier3{ p1 = left.p1 + right, pc1 = left.pc1 + right, pc2 = left.pc2 + right, p2 = left.p2 + right, } end error( 'unknown operation' ) end -- constructor def.constructor = function( args ) local p1 = args.p1 local pc1 = args.pc1 local pc2 = args.pc2 local p2 = args.p2 return { p1 = p1, pc1 = pc1, pc2 = pc2, p2 = p2, } end -- draws helping information def.proto.drawHelpers = function( self, prefix ) tikz.draw{ draw = 'red', tikz.line{ self.p1, self.pc1 } } tikz.draw{ draw = 'yellow', tikz.line{ self.pc1, self.pc2 } } tikz.draw{ draw = 'blue', tikz.line{ self.pc2, self.p2 } } if prefix ~= nil then tikz.put{ tikz.node{ at = self.p1, anchor = 'north west', text = prefix .. '1', } } tikz.put{ tikz.node{ at = self.pc1, anchor = 'north west', text = prefix .. 'c1', } } tikz.put{ tikz.node{ at = self.pc2, anchor = 'north west', text = prefix .. 'c2', } } tikz.put{ tikz.node{ at = self.p2, anchor = 'north west', text = prefix .. '2', } } end end def.mul = function( left, right ) if type( right ) == 'number' then return tikz.bezier3 { p1 = left.p1 * right, pc1 = left.pc1 * right, pc2 = left.pc2 * right, p2 = left.p2 * right, } elseif type( left ) == 'number' then return tikz.bezier3 { p1 = right.p1 * left, pc1 = right.pc1 * left, pc2 = right.pc2 * left, p2 = right.p2 * left, } else error( 'invalid operation' ) end end -- point in center def.lazy.pc = function( self ) return self.pt( 0.5 ) end -- gets angle at t (0-1) def.proto.phit = function( self, t ) local p1 = self.p1 local pc1 = self.pc1 local pc2 = self.pc2 local p2 = self.p2 local u = 1 - t local uu3 = 3 * u * u local ut6 = 6 * u * t local tt3 = 3 * t * t return math.atan2( -uu3 * p1.y + uu3 * pc1.y - ut6 * pc1.y + ut6 * pc2.y - tt3 * pc2.y + tt3 * p2.y, -uu3 * p1.x + uu3 * pc1.x - ut6 * pc1.x + ut6 * pc2.x - tt3 * pc2.x + tt3 * p2.x ) end -- reverts p1 and p2 and pc1 and pc2 respectively def.lazy.revert = function( self ) return tikz.bezier3{ p1 = self.p2, pc1 = self.pc2, pc2 = self.pc1, p2 = self.p1, } end -- gets point at t (0-1) def.proto.pt = function( self, t ) if type( t ) ~= 'number' or t < 0 or t > 1 then error( 'invalid pt' ) end local p1 = self.p1 local pc1 = self.pc1 local pc2 = self.pc2 local p2 = self.p2 local t1 = 1 - t return( tikz.p{ t1^3*p1.x + 3*t*t1^2*pc1.x + 3 *t^2*t1*pc2.x + t^3*p2.x, t1^3*p1.y + 3*t*t1^2*pc1.y + 3 *t^2*t1*pc2.y + t^3*p2.y } ) end def.sub = function( left, right ) if right.id == 'point' then return tikz.bezier3{ p1 = left.p1 - right, pc1 = left.pc1 - right, pc2 = left.pc2 - right, p2 = left.p2 - right, } end error( 'unknown operation' ) end -- converts to a tikz string. def.zString = function( self ) local s = { } table.insert( s, zString( self.p1 ) ) table.insert( s, '.. controls' ) table.insert( s, zString( self.pc1 ) ) table.insert( s, 'and' ) table.insert( s, zString( self.pc2 ) ) table.insert( s, '..' ) table.insert( s, zString( self.p2 ) ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A bend line ------------------------------------------------------------------------------- tikz.bline = immutable( function( def ) def.id = 'bline' -- moves the line. def.add = function( left, right ) if right.id == 'point' then return tikz.line{ p1 = left.p1 + right, p2 = left.p2 + right } end error( 'unknown operation' ) end -- constructor -- -- either -- { p1, p2 }, (a list ) -- { p1 = ..., p2 = ... }, -- { pc = ..., phi = ..., length = ... }, def.constructor = function( args ) local p1 local p2 if #args > 1 then -- list p1 = args[ 1 ] p2 = args[ 2 ] elseif type( args.p1 ) ~= 'nil' and type( args.p2 ) ~= 'nil' then p1 = args.p1 p2 = args.p2 elseif args.phi ~= nil then local phi = args.phi if args.pc ~= nil then if args.p1 ~= nil or args.p2 ~= nil then error( 'invalid line options', 2 ) end local pc = args.pc local len05 = args.length / 2 p1 = pc + tikz.p{ math.cos( phi ), math.sin( phi ) } * len05 p2 = pc - tikz.p{ math.cos( phi ), math.sin( phi ) } * len05 elseif type( args.p1 ) ~= 'nil' then if args.pc ~= nil or args.p2 ~= nil then error( 'invalid line options', 2 ) end p1 = args.p1 p2 = p1 + tikz.p{ math.cos( phi ), math.sin( phi ) } * args.length else error( 'invalid line options', 2 ) end else error( 'invalid line options', 2 ) end return { bend_left = args.bend_left, bend_right = args.bend_right, p1 = p1, p2 = p2, } end -- converts to a cubic bezier (that should look the same) def.lazy.bezier3 = function( self ) local phi if self.bend_right ~= nil then phi = self.bend_right * math.pi / 180 else phi = -self.bend_left * math.pi / 180 end local line = self.line local phil = line.phi local len = line.length * 0.3915 local lt1 = tikz.line{ p1 = line.p1, phi = phil-phi, length = len } local lt2 = tikz.line{ p1 = line.p2, phi = phil+phi+math.pi, length = len } return tikz.bezier3{ p1 = line.p1, pc1 = lt1.p2, pc2 = lt2.p2, p2 = line.p2, } end -- converts to a straight line. def.lazy.line = function( self ) return tikz.line{ p1 = self.p1, p2 = self.p2, } end -- point in center def.lazy.pc = function( self ) return self.bezier3.pc end -- gets point at t (0-1). def.proto.pt = function( self, t ) return self.bezier3.pt( t ) end -- converts to a tikz string. def.zString = function( self ) local s = { } table.insert( s, zString( self.p1 ) ) table.insert( s, 'to' ) if self.bend_left ~= nil then table.insert( s, '[bend left='..self.bend_left..']' ) end if self.bend_right ~= nil then table.insert( s, '[bend right='..self.bend_right..']' ) end table.insert( s, zString( self.p2 ) ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A curve is defined by a list of points. ------------------------------------------------------------------------------- curve = immutable( function( def ) def.id = 'curve' -- constructor def.constructor = function( args ) return { points = args.points, tension = args.tension, cycle = args.cycle, } end -- draws helping information def.proto.drawHelpers = function( self, prefix ) local points = self.points for i, pi in ipairs( points ) do tikz.draw{ fill = 'black', tikz.circle{ at = pi, radius = 0.05, } } if prefix ~= nil then tikz.put{ tikz.node{ at = pi, anchor = 'north west', text = prefix .. i, } } end end end -- converts to a tikz string. def.zString = function( self ) local s = { 'plot' } table.insert( s, '[' ) if self.cycle then table.insert( s, 'smooth cycle' ) else table.insert( s, 'smooth' ) end if self.tension then table.insert( s, ',tension='..self.tension ) end table.insert( s, ']' ) local points = self.points table.insert( s, 'coordinates' ) table.insert( s, '{' ) for _, p in ipairs( points ) do table.insert( s, zString( p ) ) end table.insert( s, '}' ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A full ellipse. ------------------------------------------------------------------------------- tikz.ellipse = immutable( function( def ) def.id = 'ellipse' -- constructor def.constructor = function( args ) local at = args.at local xradius = args.xradius local yradius = args.yradius return { at = at, xradius = xradius, yradius = yradius, } end -- intersect with a line. def.proto.intersectLine = function( self, line ) local at = self.at local rx = self.xradius local ry = self.yradius local p1 = line.p1 local p2 = line.p2 local x0 = p1.x local y0 = p1.y local x1 = p2.x local y1 = p2.y local cx = at.x local cy = at.y x0 = x0 - cx y0 = y0 - cy x1 = x1 - cx y1 = y1 - cy local A = ((x1 - x0) * (x1 - x0)) / rx / rx + ((y1 - y0) * (y1 - y0)) / ry / ry local B = (2 * x0 * (x1 - x0)) / rx / rx + (2 * y0 * (y1 - y0)) / ry / ry local C = (x0 * x0) / rx / rx + (y0 * y0) / ry / ry - 1 local D = B * B - 4 * A * C local tv = { } if D == 0 then table.insert( tv, -B / 2 / A ) else table.insert( tv, (-B + math.sqrt(D)) / 2 / A ) table.insert( tv, (-B - math.sqrt(D)) / 2 / A ) end local r = { } for _, t in ipairs( tv ) do if t >= 0 and t <= 1 then table.insert( r, p1 + tikz.p{ t * line.length * math.cos( line.phi ), t * line.length * math.sin( line.phi ) } ) end end if #r == 0 then return elseif #r == 1 then return r[ 1 ] else return r end end -- converts to a tikz string. def.zString = function( self ) s = { } table.insert( s, zString( self.at ) ) table.insert( s, 'ellipse' ) table.insert( s, '[' ) table.insert( s, 'x radius =' ) table.insert( s, zString( self.xradius ) ) table.insert( s, ', y radius =' ) table.insert( s, zString( self.yradius ) ) table.insert( s, ']' ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- An ellipse part. ------------------------------------------------------------------------------- tikz.ellipseArc = immutable( function( def ) def.id = 'ellipseArc' -- constructor def.constructor = function( args ) return { at = args.at, from = args.from, to = args.to, xradius = args.xradius, yradius = args.yradius, } end -- converts to a tikz string. def.zString = function( self ) s = { } table.insert( s, 'plot' ) table.insert( s, '[domain =' ) table.insert( s, self.from ) table.insert( s, ':' ) table.insert( s, self.to ) table.insert( s, ',variable = \\phi,smooth]' ) table.insert( s, '({ '..zString(self.at.x)..'+'..zString(self.xradius)..'*cos(\\phi)},' ..'{ '..zString(self.at.y)..'+'..zString(self.yradius)..'*sin(\\phi)})' ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- Single line (segment). ------------------------------------------------------------------------------- tikz.line = immutable( function( def ) def.id = 'line' -- moves the line. def.add = function( left, right ) if right.id == 'point' then return tikz.line{ p1 = left.p1 + right, p2 = left.p2 + right, } end error( 'unknown operation' ) end -- multiplicator def.mul = function( left, right ) if type( left ) == 'number' then return tikz.line{ p1 = right.p1 * left, p2 = right.p2 * left, } elseif type( right ) == 'number' then return tikz.line{ p1 = left.p1 * right, p2 = left.p2 * right, } end error( 'unknown operation' ) end -- constructor -- -- either -- { p1, p2 }, (a list ) -- { p1 = ..., p2 = ... }, -- { pc = ..., phi = ..., length = ... }, def.constructor = function( args ) local p1 local p2 if #args > 1 then -- list p1 = args[ 1 ] p2 = args[ 2 ] elseif type( args.p1 ) ~= 'nil' and type( args.p2 ) ~= 'nil' then p1 = args.p1 p2 = args.p2 elseif args.phi ~= nil then local phi = args.phi if args.pc ~= nil then if args.p1 ~= nil or args.p2 ~= nil then error( 'invalid line options', 2 ) end local pc = args.pc local len05 = args.length / 2 p1 = pc + tikz.p{ math.cos( phi ), math.sin( phi ) } * len05 p2 = pc - tikz.p{ math.cos( phi ), math.sin( phi ) } * len05 elseif type( args.p1 ) ~= 'nil' then if args.pc ~= nil or args.p2 ~= nil then error( 'invalid line options', 2 ) end p1 = args.p1 p2 = p1 + tikz.p{ math.cos( phi ), math.sin( phi ) } * args.length else error( 'invalid line options', 2 ) end else error( 'invalid line options', 2 ) end return { p1 = p1, p2 = p2, } end -- divisor def.div = function( left, right ) if type( right ) == 'number' then return tikz.line{ p1 = left.p1 / right, p2 = left.p2 / right, } end error( 'unknown operation' ) end -- length of line. def.lazy.length = function( self ) local p1 = self.p1 local p2 = self.p2 local dx = p2.x - p1.x local dy = p2.y - p1.y local result = math.sqrt( dx*dx + dy*dy ) return result end -- center of line. def.lazy.pc = function( self ) local p1 = self.p1 local p2 = self.p2 local result = tikz.p{ ( p1.x + p2.x ) / 2, ( p1.y + p2.y ) / 2 } return result end -- angle of line. def.lazy.phi = function( self ) local p1 = self.p1 local p2 = self.p2 local result = math.atan2( p2.y - p1.y, p2.x - p1.x ) return result end -- gets point at t (0-1). def.proto.pt = function( self, t ) if type( t ) ~= 'number' or t < 0 or t > 1 then error( 'invalid pt' ) end local p1 = self.p1 local p2 = self.p2 local phi = self.phi local lt = self.length * t return( tikz.p{ p1.x + lt * math.cos( phi ), p1.y + lt * math.sin( phi ), } ) end -- intersects the line with another. def.proto.intersectLine = function( self, line ) local p1 = self.p1 local p2 = self.p2 local p3 = line.p1 local p4 = line.p2 if p1.x == p3.x and p1.y == p3.y then return p1 end if p1.x == p4.x and p1.y == p4.y then return p1 end if p2.x == p3.x and p2.y == p3.y then return p2 end if p2.x == p4.x and p2.y == p4.y then return p2 end local den = ( p1.x - p2.x )*( p3.y - p4.y ) - ( p1.y - p2.y )*( p3.x - p4.x ); if den == 0 then -- doesn't intersect return end local a = p1.x*p2.y - p1.y*p2.x; local b = p3.x*p4.y - p3.y*p4.x; local x = ( a*( p3.x - p4.x ) - ( p1.x - p2.x )*b ) / den; local y = ( a*( p3.y - p4.y ) - ( p1.y - p2.y )*b ) / den; if ( ( x >= p1.x and x <= p2.x or x <= p1.x and x >= p2.x ) and ( x >= p3.x and x <= p4.x or x <= p3.x and x >= p4.x ) ) or ( ( y >= p1.y and y <= p2.y or y <= p1.y and y >= p2.y ) and ( y >= p3.y and y <= p4.y or y <= p3.y and y >= p4.y ) ) then return tikz.p{ x, y } end end -- converts to a tikz string. def.zString = function( self ) local s = { } table.insert( s, zString( self.p1 ) ) table.insert( s, '--' ) table.insert( s, zString( self.p2 ) ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- Function plot. ------------------------------------------------------------------------------- tikz.plot = immutable( function( def ) def.id = 'plot' -- constructor def.constructor = function( args ) return { at = args.at, from = args.from, to = args.to, step = args.step, tension = args.tension, func = args.func, } end def.lazy.points = function( self ) local from = self.from local to = self.to local step = self.step if step == nil then step = ( to - from ) / 100 end local points = { } local d local at = self.at or tikz.p0 for d = from, to, step do table.insert( points, at + self.func( d ) ) end return points end -- converts to a tikz string. def.zString = function( self ) local s = { 'plot' } table.insert( s, '[' ) table.insert( s, 'smooth' ) if self.tension then table.insert( s, ',tension='..self.tension ) end table.insert( s, ']' ) local points = self.points table.insert( s, 'coordinates' ) table.insert( s, '{' ) for _, p in ipairs( points ) do table.insert( s, zString( p ) ) end table.insert( s, '}' ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A superEllipse ------------------------------------------------------------------------------- tikz.superEllipse = immutable( function( def ) def.id = 'superEllipse' -- constructor def.constructor = function( args ) local at = args.at local n = args.n local xradius = args.xradius local yradius = args.yradius return { at = at, n = n, xradius = xradius, yradius = yradius, } end -- returns p on degree (0-360) def.proto.pdeg = function( self, t ) if type( t ) ~= 'number' or t < 0 or t > 360 then error( 'invalid pdeg' ) end local at = self.at local n = self.n local xo = self.xradius * math.abs( math.cos( t * math.pi / 180 ) )^(2/n) local yo = self.yradius * math.abs( math.sin( t * math.pi / 180 ) )^(2/n) if t <= 90 then return tikz.p{ at.x + xo, at.y + yo } elseif t <= 180 then return tikz.p{ at.x - xo, at.y + yo } elseif t <= 270 then return tikz.p{ at.x - xo, at.y - yo } else return tikz.p{ at.x + xo, at.y - yo } end end -- converts to a tikz string. def.zString = function( self ) local s = { } local at = self.at local xa = at.x local ya = at.y local n = self.n local xr = self.xradius local yr = self.yradius table.insert( s, 'plot[domain=0:90,variable=\\t,smooth]' ) table.insert( s, '({'..xr..'*cos(\\t)^(2/'..n..')+'..xa..'},{'..yr..'*sin(\\t)^(2/'..n..')+'..ya..'})' ) table.insert( s, '--' ) table.insert( s, 'plot[domain=90:0,variable=\\t,smooth]' ) table.insert( s, '({-'..xr..'*cos(\\t)^(2/'..n..')+'..xa..'},{'..yr..'*sin(\\t)^(2/'..n..')+'..ya..'})' ) table.insert( s, '--' ) table.insert( s, 'plot[domain=0:90,variable=\\t,smooth]' ) table.insert( s, '({-'..xr..'*cos(\\t)^(2/'..n..')+'..xa..'},{-'..yr..'*sin(\\t)^(2/'..n..')+'..ya..'})' ) table.insert( s, '--' ) table.insert( s, 'plot[domain=90:0,variable=\\t,smooth]' ) table.insert( s, '({'..xr..'*cos(\\t)^(2/'..n..')+'..xa..'},{-'..yr..'*sin(\\t)^(2/'..n..')+'..ya..'})' ) table.insert( s, '-- cycle' ) return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A reference to a named node. ------------------------------------------------------------------------------- local optionsNamed = { ref = false, xshift = 'xshift', yshift = 'yshift', } tikz.named = immutable( function( def ) def.id = 'named' -- constructor def.constructor = function( args ) local s = { } _doOptions( s, args, optionsNamed, false, 2 ) return { opts = table.concat( s, ' ' ), ref = args.ref, } end -- converts to a tikz string. def.zString = function( self ) local s = { } table.insert( s, '(' ) if self.opts then table.insert( s, self.opts ) end table.insert( s, self.ref ) table.insert( s, ')' ) return table.concat( s ) end end ) ------------------------------------------------------------------------------- -- Multiple connected lines. ------------------------------------------------------------------------------- tikz.polyline = immutable( function( def ) def.id = 'polyline' -- constructor def.constructor = function( args ) local o = { } for _, v in ipairs( args ) do table.insert( o, v ) end return o end -- converts to a tikz string. def.zString = function( self ) local s = { '' } for k, v in ipairs( self ) do if k > 1 then table.insert( s, '--' ) end table.insert( s, zString( v ) ) end return table.concat( s, ' ' ) end end ) ------------------------------------------------------------------------------- -- A rectangle ------------------------------------------------------------------------------- tikz.rect = immutable( function( def ) def.id = 'rect' -- constructor def.constructor = function( args ) local psw local pne if args.pnw ~= nil then if args.pse ~= nil then psw = tikz.p{ args.pnw.x, args.pse.y } pne = tikz.p{ args.pse.x, args.pnw.y } elseif args.size ~= nil then psw = tikz.p{ args.pnw.x, args.pnw.y - args.size.y } pne = tikz.p{ args.pnw.x + args.size.x, args.pnw.y } else error( 'invalid rect options', 2 ) end elseif args.psw ~= nil then psw = args.psw if args.pne ~= nil then pne = args.pne elseif args.size ~= nil then pne = psw + args.size else error( 'invalid rect options', 2 ) end elseif args.pse ~= nil then if args.size ~= nil then psw = tikz.p{ args.pse.x - args.size.x, args.pse.y } pne = tikz.p{ args.pse.x, args.pse.y + args.size.y } else error( 'invalid rect options', 2 ) end elseif args.pc ~= nil then if args.size ~= nil then psw = tikz.p{ args.pc.x - args.size.x / 2, args.pc.y - args.size.y / 2 } pne = tikz.p{ args.pc.x + args.size.x / 2, args.pc.y + args.size.y / 2 } else error( 'invalid rect options', 2 ) end else error( 'invalid rect options', 2 ) end return { psw = psw, pne = pne, } end def.zString = function( self ) local s = { } table.insert( s, zString( self.psw ) ) table.insert( s, 'rectangle' ) table.insert( s, zString( self.pne ) ) return table.concat( s, ' ' ) end def.lazy.e = function( self ) return self.pne.x end def.lazy.height = function( self ) return self.pne.y - self.psw.y end def.lazy.n = function( self ) return self.pne.y end def.lazy.pc = function( self ) return tikz.p{ self.psw.x + self.width / 2, self.psw.y + self.height / 2 } end def.lazy.pe = function( self ) return tikz.p{ self.pne.x, self.psw.y + self.height / 2 } end def.lazy.pn = function( self ) return tikz.p{ self.psw.x + self.width / 2, self.pne.y } end def.lazy.pnw = function( self ) return tikz.p{ self.psw.x, self.pne.y } end def.lazy.ps = function( self ) return tikz.p{ self.psw.x + self.width / 2, self.psw.y } end def.lazy.pse = function( self ) return tikz.p{ self.pne.x, self.psw.y } end def.lazy.pw = function( self ) return tikz.p{ self.psw.x, self.psw.y + self.height / 2 } end def.lazy.s = function( self ) return self.psw.y end def.lazy.size = function( self ) return tikz.p{ self.width, self.height } end def.lazy.w = function( self ) return self.psw.x end def.lazy.width = function( self ) return self.pne.x - self.psw.x end end ) --==========================================================================-- -- STYLES -- --==========================================================================-- tikz.style = immutable( function( def ) def.id = 'style' -- constructor def.constructor = function( name ) return { name=name } end end ) tikz.above = tikz.style( 'above' ) tikz.arrow = tikz.style( '-{Latex}' ) tikz.below = tikz.style( 'below' ) tikz.dashed = tikz.style( 'dashed' ) tikz.dotted = tikz.style( 'dotted' ) tikz.double_arrow = tikz.style( '{Latex}-{Latex}' ) tikz.even_odd_rule = tikz.style( 'even odd rule' ) tikz.midway = tikz.style( 'midway' ) --==========================================================================-- -- OUTPUT --- --==========================================================================-- -- -- Writes out a named option. -- -- ~s: string to build -- ~args: arguments -- ~name: name of option -- ~haveOpts: true if had options -- ~rename: if defined rename options on output -- local function optionNamed( s, args, name, haveOpts, rename ) local v = args[ name ] if not v then return haveOpts end if not rename then return haveOpts end if not haveOpts then table.insert( s, '[' ) else table.insert( s, ',' ) end if v == true then table.insert( s, rename ) else table.insert( s, rename..'='..v ) end return true end -- -- Writes out an unnamed option. -- -- ~s: string to build -- ~name: name of option -- ~haveOpts: true if had options -- local function unnamedOption( s, name, haveOpts ) if not haveOpts then table.insert( s, '[' ) else table.insert( s, ',' ) end table.insert( s, name ) return true end -- -- Custom key compare. -- -- keys to put on top local topKeys = { node_distance = true, } local function kcompare( ka, kb ) local ta = topKeys[ ka ] local tb = topKeys[ kb ] if ta and not tb then return true end if not ta and tb then return false end return ka < kb end -- -- A helper to parse arguments to pass as drawing options to hand to tikz. -- -- s: string to build -- args: the arguments to parse. -- opts: named options to pass on -- haveOpts: true [ has already been built -- level: call level for error throwing -- local function _doOptions( s, args, opts, haveOpts, level ) args = flatten( args ) for _, v in ipairs( args ) do if v.id == 'style' then haveOpts = unnamedOption( s, v.name, haveOpts ) end end local nkeys = { } for key in pairs( args ) do if type( key ) ~= 'number' then table.insert( nkeys, key ) end end table.sort( nkeys, kcompare ) for _, k in ipairs( nkeys ) do local ok = opts[ k ] if ok then haveOpts = optionNamed( s, args, k, haveOpts, ok ) elseif ok == nil then error( 'unknown option '..k, level + 1 ) end end haveOpts = _opts( s, args._opts, haveOpts ) if haveOpts then table.insert( s, ']' ) end end ------------------------------------------------------------------------------- -- Drawing shapes. ------------------------------------------------------------------------------- local optionsDraw = { decorate = 'decorate', decoration = 'decoration', dash_pattern = 'dash pattern', color = 'color', draw = 'draw', fill = 'fill', line_cap = 'line cap', line_width = 'line width', opacity = 'opacity', pattern = 'pattern', rotate = 'rotate', transform_canvas = 'transform canvas', xshift = 'xshift', yshift = 'yshift', _butt_cap = '-Butt Cap', butt_cap_ = 'Butt Cap-', _round_cap = '-{Round Cap[]}', round_cap_ = '{Round Cap[]}-', _round_cap_ = '{Round Cap[]}-{Round Cap[]}', } -- -- Puts out a bounding box command -- tikz.boundingbox = function( args ) tprint( '\\pgfresetboundingbox' ) local s = { '\\path [use as bounding box] ' } for _, v in ipairs( args ) do if v.id ~= 'style' then table.insert( s, zString( v ) ) end end table.insert( s, ';' ) tprint( table.unpack( s ) ) end -- -- Puts out a draw command. -- tikz.draw = function( args ) local s = { '\\draw ' } _doOptions( s, args, optionsDraw, false, 2 ) for _, v in ipairs( args ) do if v.id ~= 'style' then table.insert( s, zString( v ) ) end end table.insert( s, ';' ) tprint( table.unpack( s ) ) end -- -- Puts out a path command. -- tikz.path = function( args ) local s = { '\\path' } _doOptions( s, args, optionsDraw, false, 2 ) for _, v in ipairs( args ) do if v.id ~= 'style' then table.insert( s, zString( v ) ) end end table.insert( s, ';' ) tprint( table.unpack( s ) ) end -- -- Puts out a bounding box. -- tikz.clip = function( args ) local s = { '\\begin{pgfinterruptboundingbox}', '\\clip' } _doOptions( s, args, optionsDraw, false, 2 ) for _, v in ipairs( args ) do if v.id ~= 'style' then table.insert( s, zString( v ) ) end end table.insert( s, ';' ) table.insert( s, '\\end{pgfinterruptboundingbox}' ) tprint( table.unpack( s ) ) end local shadeOptions = { ball_color = 'ball color', left_color = 'left color', lower_left_color = 'lower left', lower_right_color = 'lower right', opacity = 'opacity', right_color = 'right color', shading = 'shading', upper_left_color = 'upper left', upper_right_color = 'upper right', } -- -- Puts out a shade command. -- tikz.shade = function( args ) local s = { '\\shade' } _doOptions( s, args, shadeOptions, false, 2 ) for _, v in ipairs( args ) do if v.id ~= 'style' then table.insert( s, zString( v ) ) end end table.insert( s, ';' ) tprint( table.unpack( s ) ) end -- -- Puts out z stuff. -- tikz.put = function( args ) for _, v in ipairs( args ) do local s = { } if v.id == 'node' then table.insert( s, '\\' ) table.insert( s, zString( v ) ) table.insert( s, ';' ) else error( 'unknown: ' .. v.id, 1 ) end tprint( table.unpack( s ) ) end end tikz.declarehorizonalshading = function( args ) local steps = args.steps local name = args.name local s = { '\\pgfdeclarehorizontalshading{' .. name .. '}{100bp}{' } local keys = { } for k, _ in pairs( steps ) do table.insert( keys, k ) end table.sort( keys ) for _, k in ipairs( keys ) do if _ > 1 then table.insert( s, ';' ) end table.insert( s, 'color(' .. k .. 'bp)=(' .. steps[ k ] .. ')' ) end table.insert( s, '}' ) tprint( table.unpack( s ) ) end ------------------------------------------------------------------------------- -- A node to be defined to tikZ ------------------------------------------------------------------------------- local optionsNode = { above = 'above', anchor = 'anchor', align = 'align', at = false, below = 'below', color = 'color', draw = 'draw', left = 'left', minimum_height = 'minimum height', node_distance = 'node distance', name = false, right = 'right', rotate = 'rotate', text = false, text_width = 'text width', } tikz.node = immutable( function( def ) def.id = 'node'; def.zString = function( self ) local s = { 'node' } table.insert( s, self.opts ) if self.name then table.insert( s, ' (' ) table.insert( s, self.name ) table.insert( s, ')' ) end if self.at then table.insert( s, ' at ' ) table.insert( s, zString( self.at ) ) end if self.text then table.insert( s, ' {' ) local text = string.gsub( self.text, '\n', '' ) table.insert( s, text ) table.insert( s, '}' ) end return table.concat( s ) end -- constructor def.constructor = function( args ) local s = { } _doOptions( s, args, optionsNode, false, 2 ) return { at = args.at, name = args.name, opts = table.concat( s, ' ' ), text = args.text } end end ) tikz.outfile = function( filename ) outfile = io.open( filename, 'w' ) outfile:write( '%%% DO NOT EDIT THIS FILE %%%\n' ) end -- -- Hacking help. -- function _direct( args ) tprint( table.unpack( args ) ) end -- -- Checks version metch. -- tikz.checkVersion = function( v ) if v ~= tikz.version then error( 'version mismatch', 1 ) end end -- -- Creates a color. -- tikz.colorRGB = function( red, green, blue ) return '{rgb,255:red,'..red..'; green,'..green..'; blue,'..blue..'}' end -- -- Makes a tikz scope. -- Argument must be a function to call. -- tikz.scope = function( args ) args = flatten( args ) if #args ~= 1 or type( args[ 1 ] ) ~= 'function' then error( 'scope needs a function parameter' ) end _direct{ [[\begin{scope}]] } args[ 1 ]( ) _direct{ [[\end{scope}]] } end --==========================================================================-- -- SHORTCUTS --==========================================================================-- tikz.p0 = tikz.p{ 0, 0 } --==========================================================================-- -- Running code within tikz environment. --==========================================================================-- local env_nesting = 0 local org_env tikz.within = function( version ) if version ~= '*' and version ~= tikz.version then error( 'version mismatch', 1 ) end env_nesting = env_nesting + 1 if env_nesting > 1 then return end org_env = _ENV local nenv = { } local mt = { } mt.__index = function( _, key ) local zk = tikz[ key ] if zk ~= nil then return zk else return org_env[ key ] end end setmetatable( nenv, mt ) local func = debug.getinfo( 2 ).func debug.setupvalue( func, 1, nenv ) end tikz.without = function( ) env_nesting = env_nesting - 1 if env_nesting > 0 then return end local func = debug.getinfo( 2 ).func debug.setupvalue( func, 1, org_env ) end