diff options
author | Karl Berry <karl@freefriends.org> | 2024-12-09 21:18:16 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2024-12-09 21:18:16 +0000 |
commit | bd01b8f1508b38fabf50da4ad8fbac6497697ba5 (patch) | |
tree | 515068e80d87595ecb606d7dbb3d4003b00407ed /Master/texmf-dist/tex/latex/luatikz/luatikz.lua | |
parent | e4c71587aa7865ccd8d4c83a4daf3d1b04c6da6c (diff) |
luatikz (9dec24)
git-svn-id: svn://tug.org/texlive/trunk@73087 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/latex/luatikz/luatikz.lua')
-rw-r--r-- | Master/texmf-dist/tex/latex/luatikz/luatikz.lua | 1999 |
1 files changed, 1999 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/latex/luatikz/luatikz.lua b/Master/texmf-dist/tex/latex/luatikz/luatikz.lua new file mode 100644 index 00000000000..1c18f2f6278 --- /dev/null +++ b/Master/texmf-dist/tex/latex/luatikz/luatikz.lua @@ -0,0 +1,1999 @@ +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 |