Skip to content

Instantly share code, notes, and snippets.

@loopspace loopspace/1aTabOrder
Created May 20, 2013

Embed
What would you like to do?
Library Miscellaneous Release v2 -A library of miscellaneous classes and functions.
Library Miscellaneous Tab Order Version: 2
------------------------------
This file should not be included in the Codea project.
#ChangeLog
#Main
#EuclideanPlane
#ComplexPlane
#Shape
#ShapeElements
#Shapes
#Tracks
--[[
ChangeLog
=========
v2.0 Split into separate libraries: "Library Miscellaneous" is for
things that don't fit elsewhere.
v1.0 Converted to use toadkick's cmodule for importing
and Briarfox's AutoGist for versionning.
It needs toadkick's code from
https://gist.github.com/apendley/5411561
tested with version 0.0.8
To use without AutoGist, comment out the lines in setup.
--]]
--[==[
-- Complex plane class
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
local Colour = unpack(cimport "Colour",nil)
local Complex = cimport "Complex"
cimport "ColourNames"
cimport "Menu"
cimport "Slider"
cimport "NumberSpinner"
cimport "Utilities"
local ComplexPlane = class()
function ComplexPlane:init(f,ui,t)
Complex.precision = 1
self.o = vec2(WIDTH/2,HEIGHT/2)
self.font = f
self.points = {}
self.bgcolour = Colour.readData("local", "bgcolour", Colour.x11.Bisque4)
self.axescolour = Colour.readData("local", "axescolour",Colour.svg.Black)
self.controlcolour = Colour.readData("local", "controlcolour", Colour.svg.SlateBlue)
self.ptcolour = Colour.readData("local", "ptcolour", Colour.svg.Fuchsia)
self.cptcolour = Colour.readData("local", "cptcolour", Colour.svg.Thistle)
self.textcolour = Colour.readData("local", "textcolour", Colour.svg.Black)
self.font:setColour(self.textcolour)
self.scale = readLocalData("scale",HEIGHT/4)
self.lh = f:lineheight()
self.showvalue = Boolean.readData("local","showvalue",true)
self.polar = Boolean.readData("local","polar",false)
local m = ui:addMenu({title = "Complex Number", attach = true})
local om = ui:addMenu()
local cm = ui:addMenu()
local ctm = ui:addMenu()
ctm:isChildOf(m)
om:isChildOf(m)
cm:isChildOf(m)
m:addItem({
title = "Operation",
action = function(x,y)
om.active = not om.active
om.x = x
om.y = y
end,
highlight = function()
return om.active
end,
deselect = function()
om:deactivateDown()
end
})
m:addItem({
title = "Scale",
action = function()
ui:getParameter(self.scale,
1,
HEIGHT,
function(t)
saveLocalData("scale",t)
self.scale = t
end
)
return true
end
})
m:addItem({
title = "Single Point",
action = function()
self.singleton = not self.singleton
Boolean.saveData("local", "singleton", self.singleton)
return true
end,
highlight = function()
return self.singleton
end
})
m:addItem({
title = "Constrain Control Point",
action = function(x,y)
ctm.active = not ctm.active
ctm.x = x
ctm.y = y
end,
highlight = function()
return ctm.active
end,
deselect = function()
ctm:deactivateDown()
end
})
m:addItem({
title = "Clear",
action = function()
self:clear()
return true
end
})
m:addItem({
title = "Show Values",
action = function()
self.showvalue = not self.showvalue
Boolean.saveData("local","showvalue",self.showvalue)
return true
end,
highlight = function()
return self.showvalue
end
})
m:addItem({
title = "Customise",
action = function(x,y)
cm.active = not cm.active
cm.x = x
cm.y = y
end,
highlight = function()
return cm.active
end,
deselect = function()
cm:deactivateDown()
end
})
for k,v in ipairs({
"Addition",
"Multiplication",
"Reciprocal",
"Conjugate",
"Minus",
"Exponential"
}) do
om:addItem({
title = v,
action = function()
self.op = v
return true
end,
highlight = function()
return self.op == v
end
})
end
om:addItem({
title = "Roots",
action = function()
self.op = "Roots"
ui:getNumberSpinner({
action = function(t)
t = tonumber(t)
self.root = math.floor(math.abs(t))
return true
end,
maxdigits = 1,
maxdecs = 0,
allowSignChange = false
}
)
return true
end,
highlight = function()
return self.op == "Roots"
end
})
cm:addItem({
title = "Point Size",
action = function()
ui:getParameter(self.radius,
1,
50,
function(t)
saveLocalData("radius",t)
self.radius = t
end
)
return true
end
})
cm:addItem({
title = "Polar Form",
action = function()
self.polar = not self.polar
Boolean.saveData("local","polar",self.polar)
return true
end,
highlight = function()
return self.polar
end
})
for k,v in ipairs({
{"Background Colour", "bgcolour"},
{"Axes Colour", "axescolour"},
{"Control Point Colour", "controlcolour"},
{"Point Colour", "ptcolour"},
{"Computed Point Colour", "cptcolour"}
}) do
cm:addItem({
title = v[1],
action = function()
ui:getColour("svg",
function(c)
self[v[2]]= c
Colour.saveData("local",v[2],c)
return true
end
)
return true
end
})
end
cm:addItem({
title = "Text Colour",
action = function()
ui:getColour("svg",
function(c)
self.textcolour= c
Colour.saveData("local","textcolour",c)
self.font:setColour(self.textcolour)
return true
end
)
return true
end
})
ctm:addItem({
title = "None",
action = function()
self.ctrlcond = function(z)
return z
end
self.ctrlname = "None"
return true
end,
highlight = function()
return self.ctrlname == "None"
end
})
ctm:addItem({
title = "Real Axis",
action = function()
self.ctrlcond = function(z)
z.z.y = 0
return z
end
self.ctrlname = "Real"
return true
end,
highlight = function()
return self.ctrlname == "Real"
end
})
ctm:addItem({
title = "Imaginary Axis",
action = function()
self.ctrlcond = function(z)
z.z.x = 0
return z
end
self.ctrlname = "Imaginary"
return true
end,
highlight = function()
return self.ctrlname == "Imaginary"
end
})
ctm:addItem({
title = "Unit Circle",
action = function()
self.ctrlcond = function(z)
if z:is_zero() then
return Complex(1,0)
else
return z:normalise()
end
end
self.ctrlname = "Unit"
return true
end,
highlight = function()
return self.ctrlname == "Unit"
end
})
ctm:addItem({
title = "Gaussian Integer",
action = function()
self.ctrlcond = function(z)
z.z.x = math.floor(z.z.x + .5)
z.z.y = math.floor(z.z.y + .5)
return z
end
self.ctrlname = "Gauss"
return true
end,
highlight = function()
return self.ctrlname == "Gauss"
end
})
self.control = false
self.controlpt = Complex(0,0)
self.ctrlcond = function(z) return z end
self.ctrlname = "None"
self.radius = readLocalData("radius",10)
self.controlr = readLocalData("controlr",20)
self.root = 4
self.singleton = Boolean.readData("local", "singleton", false)
t:pushHandler(self)
ui:addHelp({
title = "Complex Plane",
text = "Touch the screen to draw points, then select an operation from the menu to see the effect. " ..
"For operations that require two numbers (such as addition), the second point is the larger control point. " ..
"This can be moved by dragging it around the screen. " ..
"Various options are available in the menus: " ..
"It is possible to constrain the movement of the control point, " ..
"to draw only one point rather than adding new ones (useful for roots), " ..
"to scale the drawing area, " ..
"and whether or not to show the values of the points (only those for the last point are shown)."
}
)
end
function ComplexPlane:draw()
local o = self.o
local x
local s = self.scale
background(self.bgcolour)
stroke(self.axescolour)
strokeWidth(8)
line(o.x,0,o.x,HEIGHT)
line(0,o.y,WIDTH,o.y)
strokeWidth(3)
noFill()
ellipseMode(RADIUS)
ellipse(o.x,o.y,s)
self.font:write("1",o.x+s,o.y+5)
self.font:write("i",o.x+5,o.y + s)
self.font:write("-1",o.x-s-24,o.y+5)
self.font:write("-i",o.x+5,o.y - s - self.lh/2)
fill(self.ptcolour)
strokeWidth(-1)
local r = self.radius
local n
for k,v in ipairs(self.points) do
x = s * v.z + o
ellipse(x.x,x.y,r)
n = k
end
if self.op then
fill(self.cptcolour)
local t
for k,v in ipairs(self.points) do
t = self:operation(self.op,v)
for l,w in ipairs(t) do
x = s * w.z + o
ellipse(x.x,x.y,r)
end
end
end
if self.control then
fill(self.controlcolour)
x = s * self.controlpt.z + o
ellipse(x.x,x.y,self.controlr)
end
if self.showvalue and n then
local z = self.points[n]
x = s * z.z + o
local st
if self.polar then
st = z:topolarstring()
else
st = z:tostring()
end
self.font:write(st,x.x,x.y)
if self.op then
local t = self:operation(self.op,z)
for l,w in ipairs(t) do
x = s * w.z + o
if self.polar then
st = w:topolarstring()
else
st = w:tostring()
end
self.font:write(st,x.x,x.y)
end
end
if self.control then
x = s * self.controlpt.z + o
local st
if self.polar then
st = self.controlpt:topolarstring()
else
st = self.controlpt:tostring()
end
self.font:write(st,x.x,x.y)
end
end
end
function ComplexPlane:fullscreen(od,d)
if d == STANDARD then
self.o = vec2(375,HEIGHT/2)
else
self.o = vec2(512,HEIGHT/2)
end
end
function ComplexPlane:clear()
self.points = {}
end
function ComplexPlane:operation(t,v)
if t == "Addition" then
self.control = true
return {v + self.controlpt}
elseif t == "Multiplication" then
self.control = true
return {v * self.controlpt}
elseif t == "Reciprocal" then
self.control = false
if not v:is_zero() then
return {v:reciprocal()}
else
return {}
end
elseif t == "Minus" then
self.control = false
return {-v}
elseif t == "Conjugate" then
self.control = false
return {v^""}
elseif t == "Exponential" then
self.control = true
if not v:is_zero() then
return {v^self.controlpt}
else
return {}
end
elseif t == "Roots" then
self.control = false
local s = {}
local n = 1/self.root
for i = 1,self.root do
table.insert(s,v:power(n,i))
end
return s
end
end
function ComplexPlane:isTouchedBy(touch)
return true
end
function ComplexPlane:processTouches(g)
if g.updated then
local z
local o = self.o
local s = self.scale
local rr = (self.controlr/s)^2
for k,t in ipairs(g.touchesArr) do
z = Complex((t.touch.x - o.x)/s,(t.touch.y - o.y)/s)
if t.touch.state == BEGAN
and not self.ctrltouch
and self.controlpt:distSqr(z) < rr
then
self.ctrltouch = t.id
end
if t.id == self.ctrltouch then
self.controlpt = self.ctrlcond(z)
if t.touch.state == ENDED then
self.ctrltouch = nil
end
elseif t.updated then
if self.singleton then
self.points = {z}
else
table.insert(self.points,z)
end
end
end
g:noted()
end
if g.type.ended then
g:reset()
end
end
function ComplexPlane:saveStyle(t)
t = t or {}
for k,v in ipairs {
"bgcolour",
"axescolour",
"controlcolour",
"ptcolour",
"cptcolour",
"textcolour",
"scale",
"showvalue",
"polar",
"singleton"
} do
t[v] = self[v]
end
return t
end
function ComplexPlane:restoreStyle(t)
t = t or {}
for k,v in ipairs {
"bgcolour",
"axescolour",
"controlcolour",
"ptcolour",
"cptcolour",
"textcolour",
"scale",
"showvalue",
"polar",
"singleton"
} do
self[v] = t[v] or self[v]
end
self.font:setColour(self.textcolour)
end
function ComplexPlane:getPoints(t)
t = t or {}
for k,v in ipairs(self.points) do
table.insert(t,v)
end
return t
end
function ComplexPlane:setPoints(t)
self.points = {}
for k,v in ipairs(t) do
table.insert(self.points,v)
end
end
return ComplexPlane
--]==]
--[==[
-- EuclideanPlane
local EuclideanPlane = class()
local Font,Sentence = unpack(cimport "Font",nil)
local Colour = unpack(cimport "Colour",nil)
cimport "Matrix"
function EuclideanPlane:init(t)
t = t or {}
local x = t.x or WIDTH/2
local y = t.y or HEIGHT/2
self.o = vec2(x,y)
local width = t.width or WIDTH
local height = t.height or HEIGHT
self.matrix = t.matrix or Matrix({{1,0},{0,1}})
self.size = t.size or math.min(width,height)/2
self.scale = t.scale or 2
self.sf = self.size/self.scale
self.colour = t.colour or Colour.svg.LightBlue
self.acolour = t.axesColour or Colour.svg.Black
self.pcolour = t.pointColour or Colour.svg.LightPink
local fcolour = t.fontColour or Colour.svg.DarkSlateBlue
local fontname = t.fontname or "HoeflerText-Italic"
local fontsize = t.fontsize or 20
self.font = Font({name = fontname, size = fontsize})
self.font:setColour(fcolour)
self.points = {}
self.cpoints = {}
self.lines = {}
self.clines = {}
self.npoints = 0
self.nlines = 0
self.lastpoint = nil
self.lastline = {}
self.psize = t.pointSize or 10
self.name = t.name
self.xlabel = Sentence(self.font,"x")
self.xlabel:setAnchor("south east")
self.xlabel:setColour(fcolour)
self.ylabel = Sentence(self.font,"y")
self.ylabel:setAnchor("north west")
self.ylabel:setColour(fcolour)
if t.touchHandler then
t.touchHandler:pushHandler(self)
end
self.active = true
end
function EuclideanPlane:draw()
if not self.active then
return
end
local w = self.size
local r = self.psize
local o = self.o
local d = self.scale
pushStyle()
pushMatrix()
resetStyle()
resetMatrix()
ortho(0,WIDTH,0,HEIGHT,2*r,-2*r)
smooth()
translate(o.x,o.y)
noStroke()
fill(self.colour)
rectMode(RADIUS)
rect(0,0,w,w)
strokeWidth(4)
stroke(self.acolour)
line(0,-w,0,w)
line(-w,0,w,0)
self.xlabel:draw(w,0)
self.ylabel:draw(0,w)
--modelMatrix(self.matrix*modelMatrix())
fill(self.pcolour)
noStroke()
clip(o.x - w,o.y - w,2 * w, 2 * w)
local u
for k,v in ipairs(self.cpoints) do
ellipse(v.x,v.y,r)
end
strokeWidth(4)
stroke(self.pcolour)
lineCapMode(PROJECT)
for k,v in ipairs(self.clines) do
line(v[1].x,v[1].y,v[2].x,v[2].y)
end
noClip()
popStyle()
popMatrix()
end
function EuclideanPlane:setMatrix(m)
self.matrix = m
self:recomputePoints()
end
function EuclideanPlane:setScaleFactor(s)
self.sf = s
self:recomputePoints()
end
function EuclideanPlane:isTouchedBy(touch)
if not self.active then
return false
end
if touch.x < self.o.x - self.size then
return false
end
if touch.x > self.o.x + self.size then
return false
end
if touch.y < self.o.y - self.size then
return false
end
if touch.y > self.o.y + self.size then
return false
end
return true
end
function EuclideanPlane:processTouches(g)
if g.updated then
if g.num == 1 then
local z
local o = self.o
local s = self.sf
local m = s*self.matrix
local size = self.size
local t = g.touchesArr[1]
if t.updated then
if not self.singleton
and t.touch.state == MOVING
and t.laststate == BEGAN then
if t.touch.x > o.x - size
and t.touch.x < o.x + size
and t.touch.y > o.y - size
and t.touch.y < o.y + size
then
z = Vector((t.touch.x - o.x)/s,(t.touch.y - o.y)/s)
table.insert(self.points,z)
table.insert(self.cpoints,m*z)
self.npoints = self.npoints + 1
end
end
if t.touch.state ~= BEGAN then
if t.touch.x > o.x - size
and t.touch.x < o.x + size
and t.touch.y > o.y - size
and t.touch.y < o.y + size
then
z = Vector((t.touch.x - o.x)/s,(t.touch.y - o.y)/s)
if self.singleton then
self.points = {z}
self.cpoints = {m*z}
self.npoints = 1
else
table.insert(self.points,z)
table.insert(self.cpoints,m*z)
self.npoints = self.npoints + 1
end
self.lastpoint = z
end
end
end
elseif g.num == 2 then
local za,zb
local o = self.o
local s = self.sf
local m = s*self.matrix
local ta = g.touchesArr[1]
local tb = g.touchesArr[2]
za = Vector((ta.touch.x - o.x)/s,(ta.touch.y - o.y)/s)
zb = Vector((tb.touch.x - o.x)/s,(tb.touch.y - o.y)/s)
if ta.updated or tb.updated then
if self.singleton then
self.lines = {{za,zb}}
self.clines = {{m*za,m*zb}}
else
table.insert(self.lines,{za,zb})
table.insert(self.clines,{m*za,m*zb})
end
end
self.lastline = {za,zb}
elseif g.num == 3 then
if g.type.ended and g.type.tap then
self:clear()
end
end
g:noted()
end
if (g.type.ended and not g.type.tap) or g.type.finished then
g:reset()
end
end
function EuclideanPlane:clear()
self.points = {}
self.cpoints = {}
self.npoints = 0
self.clines = {}
self.lines = {}
self.nlines = 0
self.lastpoint = nil
self.lastline = {}
end
function EuclideanPlane:activate()
self.active = true
self:clear()
end
function EuclideanPlane:deactivate()
self.active = false
self:clear()
end
function EuclideanPlane:addPoint(z)
local m = self.sf * self.matrix
if z:linfty() <= self.scale then
if self.singleton then
self.points = {z}
self.cpoints = {m*z}
self.npoints = 1
else
table.insert(self.points,z)
table.insert(self.cpoints,m*z)
self.npoints = self.npoints + 1
end
self.lastpoint = z
else
self.lastpoint = nil
end
end
function EuclideanPlane:recomputePoints()
local t = {}
local m = self.sf*self.matrix
for k,v in ipairs(self.points) do
table.insert(t,m*v)
end
self.cpoints = t
t = {}
for k,v in ipairs(self.lines) do
table.insert(t,{m*v[1],m*v[2]})
end
self.clines = t
end
return EuclideanPlane
--]==]
VERSION = 2.0
clearProjectData()
-- DEBUG = true
-- Use this function to perform your initial setup
function setup()
autogist = AutoGist("Library Miscellaneous","A library of miscellaneous classes and functions.",VERSION)
autogist:backup(true)
--displayMode(FULLSCREEN_NO_BUTTONS)
cmodule "Library CModule"
cimport "TestSuite"
local Touches = cimport "Touch"
local UI = cimport "UI"
local Debug = cimport "Debug"
local Playlist = cimport "Playlist"
local Colour = unpack(cimport "Colour",nil)
cimport "ColourNames"
cimport "PictureBrowser"
cimport "Menu"
cimport "Keypad"
cimport "Keyboard"
local TextNode = cimport "TextNode"
if cmodule.loaded("Menu") then
print("Menu loaded")
else
print("Menu not loaded")
end
local View = cimport "View"
local Quaternion = cimport "Quaternion"
local Explosion = cimport "Explosion"
--print(Quaternion(1,.56789,0,0))
touches = Touches()
ui = UI(touches)
debug = Debug({ui = ui})
ui:systemmenu()
ui:helpmenu()
ui:addMessage("This is a system message, hello everyone.")
playlist = Playlist({ ui = ui})
testsuite.initialise({ui = ui})
--[[
colfn = function(col)
print(col)
ui:getColour(col,colfn)
return true
end
colfn(Colour.svg.Black)
--]]
debug:log({
name = "Screen north west",
message = function() local x,y = RectAnchorOf(Screen,"north west") return x .. ", " .. y end
})
--debug:activate()
ui:setPictureList({directory = "Documents", camera = true, filter = function(n,w,h) return math.min(w,h) > 500 end})
--ui:getPicture(function(i) img = i return true end)
--print(USRotateCCW(vec2(1,0)))
ui:declareKeyboard({name = "ArialMT", type = "fullqwerty"})
ui:useKeyboard("fullqwerty",
function(k) print(k) return false end)
print("Textnode")
tn = TextNode({
pos = function() return WIDTH/2,800 end,
anchor = "centre",
--angle = 30,
ui = ui,
fit = true,
maxHeight = HEIGHT,
})
watch("tn.numlines")
watch("dbug")
touches:pushHandler(tn)
--
print(PORTRAIT)
print(PORTRAIT_UPSIDE_DOWN)
print(PORTRAIT_ANY)
print(LANDSCAPE_LEFT)
print(LANDSCAPE_RIGHT)
print(LANDSCAPE_ANY)
print(ANY)
--ui:setOrientation(PORTRAIT_UPSIDE_DOWN)
-- ui:supportedOrientations(PORTRAIT_UPSIDE_DOWN)
view = View(ui,touches)
parameter.watch("view.baseRotation")
parameter.watch("stuff[1]")
parameter.watch("stack")
shape = mesh()
shape.texture = "Documents:Daniel at barnehage"
local x,y,z = 1,1,1
shape.vertices = {
vec3(x,0,0),
vec3(0,y,0),
vec3(0,0,z),
vec3(0,0,0),
vec3(0,y,0),
vec3(0,0,z),
vec3(x,0,0),
vec3(0,0,0),
vec3(0,0,z),
vec3(x,0,0),
vec3(0,y,0),
vec3(0,0,0)
}
shape.colors = {
Colour.svg.Red,
Colour.svg.Green,
Colour.svg.Blue,
Colour.svg.White,
Colour.svg.Green,
Colour.svg.Blue,
Colour.svg.Red,
Colour.svg.White,
Colour.svg.Blue,
Colour.svg.Red,
Colour.svg.Green,
Colour.svg.White
}
view.eye = vec3(5,0,0)
view.range = .25
perspective(40,WIDTH/HEIGHT)
print(projectionMatrix())
camera(0,0,15,0,0,0,0,1,0)
print(viewMatrix())
camera(0,0,-15,0,0,0,0,1,0)
print(viewMatrix())
--watch("dbg")
--ui:getCurve(vec4(0,0,3,-2),function(v) print(v) return true end)
--ui:getColour(function(c) print(c) return true end)
tw = {t = 0,s=0}
ui:setTimer(5,function() tween(5,tw,{t = 1,s = 0}) return true end)
ui:setTimer(12,function() tween(5,tw,{t = 1,s = 1}) return true end)
ui:setTimer(18,function() tw = {t = 0,s = 1} return true end)
ui:setTimer(19,function() tween(5,tw,{t = 1,s = 0}) return true end)
print(Quaternion.Rotation(1,3,4,5))
parameter.watch("vm")
parameter.watch("qt")
qslp = Quaternion.unit():make_slerp(Quaternion(1,0,0,0))
qlp = Quaternion.unit():make_lerp(Quaternion(-1,0,0,0))
explosion = Explosion({
image = "Cargo Bot:Codea Icon",
trails = true,
centre = vec2(WIDTH/2,HEIGHT/2)
})
--explosion:activate(1,5)
--ui:addNotice({text = "Watch carefully"})
ui:getNumber(function(n) print(n) end)
end
-- This function gets called once every frame
function draw()
-- process touches and taps
touches:draw()
background(34, 47, 53, 255)
--testsuite.draw()
--playlist:draw()
--[[
fill(255, 255, 255, 255)
pushMatrix()
translate(WIDTH/2,HEIGHT/2)
rotate(45)
text("hello",0,0)
popMatrix()
--]]
if img then
local w,h = img.width,img.height
local asp = math.min(WIDTH/w,HEIGHT/h,1)
--sprite(img,WIDTH/2,HEIGHT/2,w*asp,h*asp)
end
tn:draw()
pushMatrix()
view:draw()
vm = viewMatrix()
qt = qslp(tw.t)
qt = qt*qlp(tw.s)
--modelMatrix(qt:tomatrix())
--perspective(40,WIDTH/HEIGHT)
--camera(10,10,10,0,0,0,0,1,0)
shape:draw()
popMatrix()
resetMatrix()
viewMatrix(matrix())
ortho()
explosion:draw()
ui:draw()
debug:draw()
touches:show()
end
function touched(touch)
touches:addTouch(touch)
end
function orientationChanged(o)
if ui then
ui:orientationChanged(o)
end
end
function fullscreen()
end
function reset()
end
--[==[
-- 3D Shapes
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
A "Shape" is basically a list of vertices and edges in 3-space which
is projected onto the iPad screen.
In actual fact, it is slightly more complicated than that as there are
various supplementary pieces that we need such as default properties
for the edges and vertices.
Perhaps the key extra piece of information is whether to use the more
accurate method for determining which edge or vertex is on top and
redrawing the one on top using an appropriate clip if required. This
solves the problem that ordering edges according to which is "on top"
(as seen from the eye) is not a transitive relation. However, it is
slower than a crude ordering which ignores this subtlety.
--]]
local Shape = class()
View = cimport "View"
Vec3 = cimport "Vec3"
--[[
This is our initialiser function. As well as setting up a few
variables, we define a menu for changing various properties of our
object. Our arguments are the surrounding space and the touch handler.
--]]
function Shape:init(ui,t)
self.vertices = {} -- vertices
self.edges = {} -- edges
self.faces = {}
self.triangles = {}
self.facemesh = mesh()
self.surface = false
self.textures = {}
self.edgesBetween = {} -- edges specified by listing the endpoints
t:pushHandler(self)
self.space = View(ui,t)
self.defaults = {}
self.defaults.vertex = {radius = .1, colour = Colour.svg.Red}
self.defaults.edge = {thickness = 10, colour = Colour.svg.Yellow}
self.defaults.face = {colour = Colour.svg.Red}
self.actives = {}
self.edit = false
self.ClipSize = 10
self.checkIntersections = false
self.shapeMenu = self.space.ui:addMenu()
for k,v in ipairs(preDefinedShapes) do
self.shapeMenu:addItem({
title = v,
action = function()
self:loadShape(v)
return true
end,
highlight = function()
return v == self.shapeName
end
})
end
self.savedShapes = readProjectData("Shapes") or ""
local a = self.savedShapes
while a ~= "" do
local i,j = string.find(a,";")
local v = string.sub(a,1,i - 1)
a = string.sub(a,i + 1,-1)
self.shapeMenu:addItem({
title = v,
action = function()
self:loadShape(v)
return true
end,
highlight = function()
return v == self.shapeName
end
})
end
self.shapeMenu:addItem({
title = "Saved Shape",
action = function()
self:loadShape()
return true
end
})
local m
m = self.space.ui:addMenu({title = "Shape", attach = true})
self.shapeMenu.parent = m
m:addItem({
title = "Apply Spatial Transformation",
action = function()
self:applySpatial()
self.space:restoreInitials()
return true
end
})
m:addItem({
title = "Apply Default Properties",
action = function()
self:applyDefaults()
return true
end
})
m:addItem({
title = "Load Shape",
action = function(x,y)
self.clearOnLoad = true
if self.shapeMenu.active then
self.shapeMenu:deactivateDown()
else
self.shapeMenu:activate()
self.shapeMenu.x = x
self.shapeMenu.y = y
end
end,
deselect = function()
self.shapeMenu:deactivateDown()
end,
highlight = function()
return self.shapeMenu.active and self.clearOnLoad
end
})
m:addItem({
title = "Save Shape",
action = function()
self.space.ui:getText(
function(t)
local s
s = self:toString()
self.savedShapes = self.savedShapes .. t .. ";"
saveProjectData("Shapes",self.savedShapes)
saveProjectData("Shape " .. t,s)
self.space.ui:addMessage("Shape " .. t .. " saved")
self.shapeMenu:addItem({
title = t,
action = function()
self:loadShape(t)
return true
end,
highlight = function()
return t == self.shapeName
end
})
return true
end
)
return true
end
})
m:addItem({
title = "Add Shape",
action = function(x,y)
self.clearOnLoad = false
if self.shapeMenu.active then
self.shapeMenu:deactivateDown()
else
self.shapeMenu:activate()
self.shapeMenu.x = x
self.shapeMenu.y = y
end
end,
deselect = function()
self.shapeMenu:deactivateDown()
end,
highlight = function()
return self.shapeMenu.active and not self.clearOnLoad
end
})
m:addItem({
title = "Edit Shape",
action = function()
self.edit = not self.edit
return true
end,
highlight = function()
return self.edit
end})
m:addItem({
title = "Delete selected vertices",
action = function()
self:delActiveVertices()
return true
end
})
m:addItem({
title = "Colour Vertices",
action = function()
self.space.ui:getColour("svg",
function(c)
self:setParameter("vertex","colour",c)
return true
end
)
return true
end
})
m:addItem({
title = "Vertex Radii",
action = function()
self.space.ui:getParameter(
self.defaults.vertex.radius,
.05,
.3,
function(t)
self:setParameter("vertex","radius",t)
return true
end,
function(t)
self:setParameter("vertex","radius",t)
self:edgesRecalc()
return true
end
)
return true
end
})
m:addItem({
title = "Colour Edges",
action = function()
self.space.ui:getColour("svg",
function(c)
self:setParameter("edge","colour",c)
return true
end
)
return true
end
})
m:addItem({
title = "Edge Thickness",
action = function()
self.space.ui:getParameter(
self.defaults.edge.thickness,
5,
15,
function(t)
self:setParameter("edge","thickness",t)
return true
end,
function(t)
self:setParameter("edge","thickness",t)
return true
end
)
return true
end
})
m:addItem({
title = "Use Intersection algorithm",
action = function()
self.checkIntersections = not self.checkIntersections
return true
end,
highlight = function()
return self.checkIntersections
end})
ui:addHelp({
title = "Shape",
text = "The shape refers to the object actually shown. There are two types of shape: ball-and-stick models and surfaces. The Shape menu gives you control over various aspects of the shapes, though not all options make sense for both types of shape."
})
end
function Shape:setTextureByName(n)
if self.textures[n] then
self.facemesh.texture = self.textures[n]
self.hasTexture = false
return true
else
return false
end
end
function Shape:setTexture(i,n)
if n then
self.textures[n] = i
end
self.facemesh.texture = i
self.hasTexture = true
end
function Shape:unsetTexture()
self.facemesh.texture = nil
end
--[[
Clear the shape.
--]]
function Shape:clear()
self.vertices = {} -- vertices
self.edges = {} -- edges
self.faces = {}
self.triangles = {}
self.facemesh = mesh()
self.surface = false
self.faceted = false
self.hasTexture = false
self.edgesBetween = {} -- edges specified by listing the endpoints
self.actives = {}
self.edit = false
self.checkIntersections = false
end
--[[
Load a shape from either a function or the project data, possibly
clearing the current shape first.
--]]
function Shape:loadShape(t)
local s,a
if self.clearOnLoad then
a = " loaded"
else
a = " added"
end
if Shape[t] then
if self.clearOnLoad then
self:clear()
end
Shape[t](self)
self.space.ui:addMessage("Shape " .. t .. a)
elseif t then
s = readProjectData("Shape " .. t)
if s then
if self.clearOnLoad then
self:clear()
end
self:loadShapeFromData(s)
self.space.ui:addMessage("Shape " .. t .. a)
end
else
self.space.ui:getText(
function(str)
self:loadShape(str)
return true
end
)
end
end
--[[
Set the default properties for the vertices.
--]]
function Shape:setVertexDefaults(r,c,v)
self.defaults.vertex.radius = r
self.defaults.vertex.colour = c
self.defaults.vertex.valency = v
self.ClipSize = math.max(self.ClipSize,2 * r * self.space.intScale * self.space.extScale)
end
--[[
Set the default properties for the edges.
--]]
function Shape:setEdgeDefaults(t,c)
self.defaults.edge.thickness = t
self.defaults.edge.colour = c
self.ClipSize = math.max(self.ClipSize,2 * t)
end
--[[
This applies the default properties to the vertices and edges, meaning
that changing the defaults will not change the appearance of the
vertices and edges. This is particularly useful when merging shapes.
--]]
function Shape:applyDefaults()
for k,v in pairs(self.vertices) do
if not v.radius then
v.radius = self.defaults.vertex.radius
end
if not v.colour then
v.colour = self.defaults.vertex.colour
end
if not v.valency then
v.valency = self.defaults.vertex.valency
end
end
for k,v in pairs(self.edges) do
if not v.thickness then
v.thickness = self.defaults.edge.thickness
end
if not v.colour then
v.colour = self.defaults.edge.colour
end
end
end
--[[
This sets a parameter (such as colour) for all the selected vertices
or edges (and edge is selected if its two end vertices are selected).
If none are selected then it changes the defaults.
--]]
function Shape:setParameter(r,s,t)
local a = {}
local b = false
for k,v in ipairs(self.vertices) do
if v.active then
table.insert(a,v)
b = true
end
end
if b then
if r == "vertex" then
for k,v in ipairs(a) do
v[s] = t
end
elseif r == "edge" then
for k,e in ipairs(self.edges) do
if e.head.active and e.tail.active then
e[s] = t
end
end
end
else
self.defaults[r][s] = t
end
end
--[[
The following functions apply transformations to the shape so that it
updates the actual positions of the pieces. There are various
transformations that can be applies: scaling, translation, and
rotation, and various functions for applying one or other of them.
The last one applies the current spatial transformation.
--]]
function Shape:applyTransformation(t)
if type(t) == "number" then
self:applyScale(t)
elseif type(t) == "table" then
if t:is_a(Vec3) then
self:applyTranslation(t)
elseif t:is_a(Quaternion) then
self:applyRotation(t)
end
end
end
function Shape:applyScale(s)
for k,v in pairs(self.vertices) do
v.coordinate = s * v.coordinate
if v.radius then
v.radius = s * v.radius
end
end
self:edgesRecalc()
end
function Shape:applyRotation(q)
for k,v in pairs(self.vertices) do
v.coordinate = v.coordinate^q
end
for k,v in pairs(self.edges) do
v.headpt = v.headpt^q
v.tailpt = v.tailpt^q
end
end
function Shape:applyTranslation(u)
for k,v in pairs(self.vertices) do
v.coordinate = v.coordinate + u
end
for k,v in pairs(self.edges) do
v.headpt = v.headpt + u
v.tailpt = v.tailpt + u
end
end
function Shape:applySRT(s,q,u)
for k,v in pairs(self.vertices) do
v.coordinate = s * v.coordinate^q + u
end
self:edgesRecalc()
end
function Shape:applySpatial()
for k,v in pairs(self.vertices) do
v.coordinate = self.space:applyIntTransformation(v.coordinate)
end
self:edgesRecalc()
end
--[[
This sets our set of vertices from a table of vertices.
--]]
function Shape:setVertices(vv)
self.vertices = vv
for k,v in pairs(self.vertices) do
v.shape = self
end
end
--[[
It is allowed to specify the edges by saying which vertices they go between.
--]]
function Shape:setEdgesBetween(e)
self.edgesBetween = e
end
--[[
This is for adding a vertex to the current shape.
--]]
function Shape:addVertex(v)
-- check if v is a Vec3 or not
table.insert(self.vertices,v)
v.shape = self
end
--[[
This is for adding an edge to the current shape.
--]]
function Shape:addEdge(e)
-- check if e is an edge or not
table.insert(self.edges,e)
e.shape = self
end
--[[
This is for adding an edge by specifying which vertices it goes between.
--]]
function Shape:addEdgeBetween(a,b)
table.insert(self.edgesBetween,{a,b})
end
--[[
It is possible to define a shape by specifying a set of vertices with
valencies. This function computes the edges according to those
valencies by iterating over the set of vertices and choosing, for
each, the n closest other vertices with unfulfilled valencies, where n
is the number of unfulfilled valencies of the current vertex.
--]]
function Shape:addEdgesByValency()
for k,v in ipairs(self.vertices) do
if not v.valency then
v.valency = self.defaults.vertex.valency
for l,e in pairs(v.edges) do
v.valency = v.valency - 1
end
end
end
for k,v in ipairs(self.vertices) do
if v.valency > 0 then
local vv = {}
for l,u in ipairs(self.vertices) do
if l > k then
table.insert(vv,{u,u.coordinate:subtract(v.coordinate):lenSqr()})
end
end
table.sort(vv, function(a,b) return a[2] < b[2] end)
local i = 1
repeat
if not vv[i] then
break
end
if vv[i][1].valency > 0 then
local e = Edge(v,vv[i][1],self)
e:Shorten()
table.insert(self.edges,e)
i = i + 1
end
until v.valency == 0
end
end
end
--[[
This function generates the family of edges from the "edges between" data.
--]]
function Shape:GenerateEdges()
local u,w,e
for k,v in pairs(self.edgesBetween) do
u = self.vertices[v[1] + 1]
w = self.vertices[v[2] + 1]
if v[3] then
e = Edge(u,w,self,unpack(v[3]))
else
e = Edge(u,w,self)
end
e:Shorten()
table.insert(self.edges,e)
end
end
--[[
When vertices are moved, or their radii changed, it is necessary to
recompute the end points of the edges. This function does that.
--]]
function Shape:edgesRecalc()
for k,v in pairs(self.edges) do
v:Shorten()
end
end
--[[
This adds a face to the shape, checking if the verties are already there.
--]]
function Shape:addFace(f)
table.insert(self.faces,f)
for k,v in ipairs (f.vertices) do
if not v.shape then
self:addVertex(v)
end
end
for k,v in ipairs(f.triangles) do
table.insert(self.triangles,v)
end
end
--[[
This is our draw function. First, we project the vertices and edges
onto the screen. Then we order them by distance from the eye (for
edges, this is the furthest point from the eye). This gives a
reasonable ordering and we draw them in that order. If the
"checkIntersections" flag is set then when each piece is rendered, we
check to see if any of the edges underneath it should really have been
drawn on top. If so, we redraw the underneath edge clipped to near
the intersection.
--]]
function Shape:draw()
self.space:draw()
local d = {}
ellipseMode(RADIUS)
for k,v in pairs(self.vertices) do
v:Project()
table.insert(d,v)
if not self.edit then
v.active = false
end
end
if self.surface then
local f = {}
for k,v in pairs(self.faces) do
v:Project()
end
for k,v in pairs(self.triangles) do
table.insert(f,v)
end
table.sort(f,Shape.byLevel)
local tv = {}
local tt = {}
local ft = {}
local fc = {}
local ftex = {}
local n,c,nv
for k,v in ipairs(f) do
tv = v:getVertices()
if self.hasTexture then
tt = v:getTexCoords()
end
n = self.space:applyIntDirTransformation(v.normal)
for i = 1,3 do
table.insert(ft,tv[i].projection)
if self.hasTexture then
table.insert(ftex,tt[i])
end
if self.faceted then
nv = n
else
nv = tv[i].pnormal or n
end
if nv:is_zero() then
table.insert(fc,Colour.transparent)
else
nv = nv:normalise()
c = 40*nv:dot(self.space.light) + 50
if v.colour then
c = Colour.shade(v.colour,c)
else
c = Colour.shade(self.defaults.face.colour,c)
end
table.insert(fc,c)
end
end
end
if self.hasTexture then
self.facemesh.texCoords = ftex
end
self.facemesh.vertices = ft
self.facemesh.colors = fc
--pushStyle()
--fill(Colour.svg.White)
self.facemesh:draw()
--popStyle()
else
for k,v in pairs(self.edges) do
v:Project()
table.insert(d,v)
end
table.sort(d,Shape.byLevel)
local de = {}
local ne = {}
for k,v in ipairs(d) do
v:draw()
if self.checkIntersections then
ne = {}
for kk,vv in ipairs(de) do
if vv.mlevel > v.level then
if vv:isOver(v) then
local z = vv:Intersect(v) + self.space.origin
-- This is for debugging the overlaps
if DEBUG then
pushMatrix()
pushStyle()
resetMatrix()
resetStyle()
noStroke()
ellipse(z.x,z.y,20)
popStyle()
popMatrix()
end
clip(z.x - self.ClipSize/2,
z.y - self.ClipSize/2,
self.ClipSize,
self.ClipSize)
vv:draw()
noClip()
end
table.insert(ne,vv)
end
end
de = ne
if v:is_a(Edge) then
table.insert(de,v)
end
end
end
end
end
--[[
This is an auxilliary function for sorting vertices and edges.
--]]
function Shape.byLevel(a,b)
return a.level < b.level
end
--[[
The shape is touched if we are in edit mode. First we check if one of
the vertices has been touched, if not the shape itself claims the
touch.
--]]
function Shape:isTouchedBy(touch)
self.touched = nil
if self.edit then
for k,v in pairs(self.vertices) do
if v:isTouchedBy(touch) then
self.touched = v
return true
end
end
return true
end
end
--[[
We let all vertices see the touch, even if they have not been touched
themselves, as all active vertices react to touches. If none have
been touched, the shape adds a new vertex at the touch point.
--]]
function Shape:processTouches(g)
if self.touched then
for k,v in pairs(self.vertices) do
v:processTouches(g)
end
else
if g.updated then
self:addDataFrom(g)
end
end
if g.type.finished then
g:reset()
else
g:noted()
end
end
--[[
This is the function that adds the vertex from the data given by the
gesture.
--]]
function Shape:addDataFrom(g)
if g.type.tap and g.type.ended then
local v,q,t
t = g.touchesArr[1]
v = vec2(t.touch.x,t.touch.y)
v = v - self.space.origin - self.space.extTranslate
v = Vec3(v.x,v.y,0)
v = v / (self.space.intScale * self.space.extScale)
v = v - self.space.intTranslate
q = self.space.rotation
q = q^""
v = v^q
self:addVertex(Vertex(v))
end
end
--[[
This deletes all active vertices and their edges.
--]]
function Shape:delActiveVertices()
for k,e in pairs(self.edges) do
if e.head.active or e.tail.active then
self.edges[k] = nil
e:destroy()
end
end
for k,v in pairs(self.vertices) do
if v.active then
self.vertices[k] = nil
end
end
end
--[[
This toggles edges between active vertices.
--]]
function Shape:toggleVertices(v)
local a = {}
local r = {}
for k,u in pairs(self.vertices) do
if u.active and u ~= v then
a[u] = true
end
end
for k,e in pairs(v.edges) do
w = e:getVertex(v)
if w.active then
r[e] = true
a[w] = nil
end
end
for k,e in pairs(self.edges) do
if r[e] then
self.edges[k] = nil
e:destroy()
end
end
for k,u in pairs(a) do
if u then
e = Edge(k,v,self)
e:Shorten()
table.insert(self.edges,e)
end
end
end
--[[
This renders the shape data to a string which can be saved as project
data and reconstructed later if so desired.
--]]
function Shape:toString()
local s,t,n
s = ""
t = {}
n = 0
for k,v in ipairs(self.vertices) do
s = s .. "Vertex:" .. v:toString() .. ";"
n = n + 1
t[v] = n
end
for k,e in pairs(self.edges) do
s = s .. "Edge:" .. t[e.head] .. "," .. t[e.tail] .. "," .. e:toString() .. ";"
end
return s
end
--[[
This reconstructs a shape from its serialised data.
--]]
function Shape:loadShapeFromData(s)
local v = {}
local e = {}
local i,j,a,b,c,p,q
while s ~= "" do
i,j = string.find(s,";")
a = string.sub(s,1,i-1)
s = string.sub(s,i+1,-1)
i,j = string.find(a,":")
b = string.sub(a,1,i-1)
a = string.sub(a,i+1,-1)
if b == "Vertex" then
c = Vertex.fromString(a)
table.insert(v,c)
elseif b == "Edge" then
i,j = string.find(a,",")
p = string.sub(a,1,i-1)
a = string.sub(a,i+1,-1)
p = tonumber(p)
i,j = string.find(a,",")
q = string.sub(a,1,i-1)
a = string.sub(a,i+1,-1)
q = tonumber(q)
c = Edge(v[p],v[q])
c:setProperties(a)
table.insert(e,c)
end
end
for k,u in ipairs(v) do
self:addVertex(u)
end
for k,u in ipairs(e) do
self:addEdge(u)
end
self:edgesRecalc()
end
return Shape
--]==]
--[==[
-- Shape elements
--[[
The Vertex class is for a vertex of a shape. Our initial data is our
coordinate (either a Vec3 object or a triple of numbers) and
optionally a radius and colour.
--]]
local Vertex = class()
local Vec3 = cimport "Vec3"
function Vertex:init(...)
local a
-- coordinate
-- colour
-- radius
-- projected coordinate
-- scale factor for radius
-- distance from eye
-- surrounding space
-- edges ending at this vertex
-- valency
if type(arg[1]) == "table" then
if arg[1]:is_a(Vec3) then
self.coordinate = arg[1]
else
print("Expected a Vec3")
return false
end
arg[1] = nil
elseif type(arg[1]) == "userdata" then
if arg[1].x then
-- hopefully a vec3
self.coordinate = Vec3(arg[1])
end
arg[1] = nil
elseif type(arg[1]) == "number" then
self.coordinate = Vec3(arg[1],arg[2],arg[3])
arg[1] = nil
arg[2] = nil
arg[3] = nil
end
for k,v in pairs(arg) do
if type(k) == "number" then
if type(v) == "number" then
self.radius = v
elseif type(v) == "table" then
if v:is_a(Space) then
self.space = v
else
print("Expecting a colour")
return false
end
elseif type(v) == "userdata" then
if v.r then
self.colour = v
else
print("Expecting a colour")
return false
end
end
end
end
self.edges = {}
self.active = false
return self
end
--[[
This gets our projected coordinate on the iPad screen.
--]]
function Vertex:Project()
local c
c = self.shape.space:Project(self.coordinate)
if c[4] then
self.projection = c[1]
self.rScale = c[2]
self.level = c[3]
else
self.projection = nil
self.level = 0
end
if self.normal then
self.pnormal = self.shape.space:ProjectDirection(self.normal)
end
end
--[[
Vertices know which edges end at themselves.
--]]
function Vertex:addEdge(e)
table.insert(self.edges,e)
if self.valency then
self.valency = self.valency - 1
end
end
--[[
This is our draw routine. We draw an ellipse, suitably scaled, with a
halo of the background colour as a "shadow".
--]]
function Vertex:draw()
local r,c
if self.projection then
if self.radius then
r = self.radius
else
r = self.shape.defaults.vertex.radius
end
self.pRadius = r * self.rScale * self.shape.space.intScale * self.shape.space.extScale
if self.colour then
c = self.colour
else
c = self.shape.defaults.vertex.colour
end
if self.active then
c = Colour.tint(c,50)
end
stroke(self.shape.space.bgColour)
strokeWidth(2) -- make configurable
fill(c)
ellipse(self.projection.x, self.projection.y, self.pRadius )
end
end
--[[
See if it was us that was touched.
--]]
function Vertex:isTouchedBy(touch)
local v = vec2(touch.x - WIDTH/2,touch.y - HEIGHT/2) - self.projection
local r = self.pRadius or 20
if v:len() < r then
return true
else
return false
end
end
--[[
If we were touched then if it was a tap we toggle our active status,
otherwise we move in a plane parallel to the current iPad screen.
--]]
function Vertex:processTouches(g)
if g.num == 1 then
if g.type.tap
and self.shape.touched == self
then
if g.type.finished then
self.active = not self.active
end
elseif self.active
or self.shape.touched == self
then
self:move(g.touchesArr[1])
end
elseif g.num == 2 then
if g.type.tap
and g.type.ended
and g.updated
and self.shape.touched == self
then
self.shape:toggleVertices(self)
end
end
end
--[[
This is the function that moves us.
--]]
function Vertex:move(t)
local v,q
if t.updated then
v = self.projection + vec2(t.touch.deltaX,t.touch.deltaY)
self.coordinate = self.shape.space:invProject(v,self.coordinate)
for k,e in pairs(self.edges) do
e:Shorten()
end
end
end
--[[
Delete an edge.
--]]
function Vertex:deleteEdge(e)
for k,v in pairs(self.edges) do
if v == e then
self.edges[k] = nil
end
end
end
--[[
Convert our data to a string for serialisation.
--]]
function Vertex:toString()
local s
s = self.coordinate:tostring()
s = s .. ","
if self.radius then
s = s .. self.radius
end
s = s .. ","
if self.colour then
s = s .. tostring(self.colour)
end
return s
end
--[[
(Technically not a class function) define a new vertex from serialised
data.
--]]
function Vertex.fromString(s)
local i,j,a,x,y,z
i,j = string.find(s,")")
a = string.sub(s,2,i-1)
s = string.sub(s,i+2,-1)
i,j = string.find(a,",")
x = string.sub(a,1,i-1)
a = string.sub(a,i + 1,-1)
i,j = string.find(a,",")
y = string.sub(a,1,i-1)
z = string.sub(a,i + 1,-1)
x = tonumber(x)
y = tonumber(y)
z = tonumber(z)
v = Vertex(x,y,z)
i,j = string.find(s,",")
a = string.sub(s,1,i-1)
s = string.sub(s,i + 2,-2)
if a ~= "" then
a = tonumber(a)
v.radius = a
end
if s ~= "" then
i,j = string.find(s,",")
x = string.sub(s,1,i-1)
s = string.sub(s,i + 1,-1)
i,j = string.find(s,",")
y = string.sub(s,1,i-1)
s = string.sub(s,i + 1,-1)
i,j = string.find(s,",")
z = string.sub(s,1,i-1)
a = string.sub(s,i + 1,-1)
x = tonumber(x)
y = tonumber(y)
z = tonumber(z)
a = tonumber(a)
v.colour = color(x,y,z,a)
end
return v
end
--[[
The Edge class represents an edge between two vertices. We get two
vertices, and optionally a thickness and a colour.
--]]
local Edge = class()
function Edge:init(u,v,...)
-- vertices at end points (Data3dVertex objects)
-- colour
-- thickness
-- projected coordinates
-- distance from eye
self.head = u
self.tail = v
u:addEdge(self)
v:addEdge(self)
for k,v in ipairs(arg) do
if type(v) == "number" then
self.thickness = v
elseif type(v) == "table" then
if v:is_a(Shape) then
self.shape = v
else
print("Expecting a colour")
return false
end
elseif type(v) == "userdata" then
if v.r then
self.colour = v
else
print("Expecting a colour")
return false
end
end
end
return self
end
--[[
To simulate the 3d nature of the vertices, the edges "stop" at the
apparent edge of the vertex sphere. This is done by shortening the
edges by the appropriate amount.
--]]
function Edge:Shorten()
local u,v,w
u = self.head.coordinate
v = self.tail.coordinate
w = u - v
w = w:normalise()
if self.head.radius then
u = u - self.head.radius * w
else
u = u - self.shape.defaults.vertex.radius * w
end
if self.tail.radius then
v = v + self.tail.radius * w
else
v = v + self.shape.defaults.vertex.radius * w
end
self.headpt = u
self.tailpt = v
end
--[[
This projects us onto the iPad screen.
--]]
function Edge:Project()
local u,w,uu,ww
if not self.headpt then
self:Shorten()
end
u = self.shape.space:Project(self.headpt)
w = self.shape.space:Project(self.tailpt)
if u[4] or w[4] then
self.level = math.min(u[3],w[3])
self.mlevel = math.max(u[3],w[3])
uu = u[1]
ww = w[1]
if not u[4] then
uu = ww - uu
uu = uu:normalize()
uu = self.shape.space.boundary * uu
uu = ww + uu
end
if not w[4] then
ww = uu - ww
ww = ww:normalize()
ww = self.shape.space.boundary * ww
ww = ww + uu
end
self.projHead = uu
self.projTail = ww
end
end
--[[
Given one vertex, return the one at the other end.
--]]
function Edge:getVertex(v)
if self.head == v then
return self.tail
elseif self.tail == v then
return self.head
else
return false
end
end
--[[
Do our best to get rid of ourselves.
--]]
function Edge:destroy()
self.head:deleteEdge(self)
self.tail:deleteEdge(self)
end
--[[
This is the draw function.
--]]
function Edge:draw()
local c,t,b
if self.colour then
c = self.colour
else
c = self.shape.defaults.edge.colour
end
if self.head.active and self.tail.active then
c = Colour.tint(c,50)
end
if self.thickness then
t = self.thickness
else
t = self.shape.defaults.edge.thickness
end
b = t + 2
stroke(self.shape.space.bgColour)
strokeWidth(b)
line(self.projHead.x, self.projHead.y, self.projTail.x, self.projTail.y)
stroke(c)
strokeWidth(t)
line(self.projHead.x, self.projHead.y, self.projTail.x, self.projTail.y)
end
--[[
This function computes whether or not we are over some other edge or vertex.
--]]
function Edge:isOver(e)
if e:is_a(Edge) then
return Vec3.isOverLine(
self.shape.space:applyIntTransformation(self.headpt),
self.shape.space:applyIntTransformation(self.tailpt),
self.shape.space:applyIntTransformation(e.headpt),
self.shape.space:applyIntTransformation(e.tailpt),
self.shape.space.eye)
else
local r
if self.thickness then
r = self.thickness
else
r = self.shape.defaults.edge.thickness
end
r = r/(self.shape.space.intScale * self.shape.space.extScale)
if e.radius then
r = r + e.radius
else
r = r + self.shape.defaults.vertex.radius
end
return Vec3.isOverPoint(
self.shape.space:applyIntTransformation(self.head.coordinate),
self.shape.space:applyIntTransformation(self.tail.coordinate),
self.shape.space:applyIntTransformation(e.coordinate),
r,
self.shape.space.eye)
end
end
--[[
Assuming that we are over an edge or vertex, this returns the
intersection point (on the iPad screen) for clipping.
--]]
function Edge:Intersect(v)
if v:is_a(Edge) then
local a,b,c,d
a = self.projHead - self.projTail
b = v.projHead - v.projTail
c = a:cross(b)
return self.projHead:cross(self.projTail)/c*b - v.projHead:cross(v.projTail)/c*a
else
return v.projection
end
end
--[[
This serialises our properties.
--]]
function Edge:toString()
local s = ""
if self.thickness then
s = s .. self.thickness
end
s = s .. ","
if self.colour then
s = s .. tostring(self.colour)
end
return s
end
--[[
This sets our properties from a string.
--]]
function Edge:setProperties(s)
local i,j,r
if s and s ~= "," and s ~= "" then
i,j = string.find(s,",")
r = string.sub(s,1,i-1)
s = string.sub(s,i + 2,-2)
if r ~= "" then
self.thickness = r
end
i,j = string.find(s,",")
x = string.sub(s,1,i-1)
s = string.sub(s,i + 1,-1)
i,j = string.find(s,",")
y = string.sub(s,1,i-1)
s = string.sub(s,i + 1,-1)
i,j = string.find(s,",")
z = string.sub(s,1,i-1)
a = string.sub(s,i + 1,-1)
x = tonumber(x)
y = tonumber(y)
z = tonumber(z)
a = tonumber(a)
self.colour = color(x,y,z,a)
end
end
local Triangle = class()
function Triangle:init(t,col,imgc,f)
self.vertices = t
local a = self.vertices[1].coordinate
local b = self.vertices[2].coordinate
local c = self.vertices[3].coordinate
b = b - a
c = c - a
b = b:cross(c)
if b:is_zero() then
self.normal = Vec3(0,0,0)
else
self.normal = b:normalise()
end
self.colour = col
self.imgcoords = {}
imgc = imgc or {}
for i = 1,3 do
self.imgcoords[i] = imgc[i] or vec2(0,0)
end
self.face = f
end
function Triangle:getProjectedVertices()
return {self.vertices[1].projection,
self.vertices[2].projection,
self.vertices[3].projection}
end
function Triangle:getVertices()
return {self.vertices[1],
self.vertices[2],
self.vertices[3]}
end
function Triangle:getTexCoords()
return {self.imgcoords[1],
self.imgcoords[2],
self.imgcoords[3]}
end
function Triangle:Project()
self.level = math.min(self.vertices[1].level,
self.vertices[2].level,
self.vertices[3].level)
end
local Face = class()
function Face:init(t,c,imgcoords)
self.triangles = {}
self.vertices = t
self.colour = c
if imgcoords then
self.imgcoords = imgcoords
end
self:triangulate()
end
function Face:triangulate()
-- assume for now that our face is convex and planar
local v = {}
local n = 0
local t = {}
for k,u in ipairs(self.vertices) do
table.insert(v,u)
n = n + 1
end
local c = {}
if self.imgcoords then
for k,u in ipairs(self.imgcoords) do
table.insert(c,u)
end
end
local a,b,d
for i = 1,n - 2 do
if i%2 == 1 then
a = table.remove(v)
else
a = table.remove(v,1)
end
n = n - 1
if self.imgcoords then
if i%2 == 1 then
b = table.remove(c)
else
b = table.remove(c,1)
end
d = {b,c[1],c[n]}
else
d = nil
end
table.insert(t,Triangle({a,v[1],v[n]},self.colour,d,self))
end
self.triangles = t
end
function Face:Project()
local l = 0
local n = 0
for k,v in ipairs(self.vertices) do
l = l + v.level
n = n + 1
end
l = l/n
for k,v in ipairs(self.triangles) do
v.level = l
end
end
return {Vertex, Edge, Face, Triangle}
--]==]
--[==[
-- Predefined Shapes
--[[
Define some common shapes.
--]]
local Shape = cimport "Shape"
local Vertex, Edge, Face, Triangle = unpack(cimport "Shapes",nil)
local Vec3 = cimport "Vec3"
local preDefinedShapes = {
"Cube",
"Chair",
"Borromean",
"Sticks",
"TwoSticks",
"Buckyball",
"Octohedron",
"CubeFaceted",
"Torus",
"Sphere",
"Die",
"Globe"
}
--[[
Cube.
--]]
function Shape.Cube(s)
for n = 0,7 do
i = 2*(n%2) - 1
j = 2*(math.floor(n/2)%2) -1
k = 2*(math.floor(n/4)%2) -1
s:addVertex(Vertex(i,j,k))
end
s:setVertexDefaults(.2,color(255,0,0,255),3)
s:setEdgeDefaults(10,color(255,255,0,255))
s:addEdgesByValency()
end
--[[
Chair: this is one form of cyclic hexane.
--]]
function Shape.Chair(s)
s:addVertex(Vertex( -0.0435, -1.4573, -0.2258))
s:addVertex(Vertex( 1.2404, -0.7663, 0.2258))
s:addVertex(Vertex( -1.2838, -0.6910, 0.2258))
s:addVertex(Vertex( 1.2838, 0.6911, -0.2257))
s:addVertex(Vertex( -1.2404, 0.7662, -0.2258))
s:addVertex(Vertex( 0.0435, 1.4573, 0.2258))
s:setVertexDefaults(.2,color(255,0,0,255),2)
s:setEdgeDefaults(10,color(255,255,0,255))
s:addEdgesByValency()
end
--[[
Rectangular version of the borromean rings.
--]]
function Shape.Borromean(s)
local edges = {}
local e = {}
local r = Colour.svg.Red
local b = Colour.svg.Blue
local g = Colour.svg.Green
s:addVertex(Vertex(2,1,0,r))
s:addVertex(Vertex(2,-1,0,r))
s:addVertex(Vertex(-2,-1,0,r))
s:addVertex(Vertex(-2,1,0,r))
e[1] = {{0,1,{r}},{1,2,{r}},{2,3,{r}},{3,0,{r}}}
s:addVertex(Vertex(0,2,1,b))
s:addVertex(Vertex(0,2,-1,b))
s:addVertex(Vertex(0,-2,-1,b))
s:addVertex(Vertex(0,-2,1,b))
e[2] = {{4,5,{b}},{5,6,{b}},{6,7,{b}},{7,4,{b}}}
s:addVertex(Vertex(1,0,2,g))
s:addVertex(Vertex(-1,0,2,g))
s:addVertex(Vertex(-1,0,-2,g))
s:addVertex(Vertex(1,0,-2,g))
e[3] = {{8,9,{g}},{9,10,{g}},{10,11,{g}},{11,8,{g}}}
for k,v in pairs(e) do
for j,u in pairs(v) do
table.insert(edges,u)
end
end
s:setEdgesBetween(edges)
s:setVertexDefaults(.1,color(255,0,0,255))
s:setEdgeDefaults(10,color(255,255,0,255))
s:GenerateEdges()
s.checkIntersections = true
end
--[[
Four overlapping sticks, showing that "is over" is not a transitive
relation.
--]]
function Shape.Sticks(s)
local ver = {
Vertex(1,2,1),
Vertex(1,-2,-1),
Vertex(-1,-2,1),
Vertex(-1,2,-1),
Vertex(2,1,-1),
Vertex(-2,1,1),
Vertex(-2,-1,-1),
Vertex(2,-1,1)
}
for k,v in ipairs(ver) do
s:addVertex(v)
end
local ed = {{0,1},{2,3},{4,5},{6,7}}
for k,v in ipairs(ed) do
s:addEdge(Edge(ver[v[1] + 1], ver[v[2] + 1]))
end
s:setVertexDefaults(.2,color(255,0,0,255))
s:setEdgeDefaults(10,color(255,255,0,255))
s:edgesRecalc()
s.checkIntersections = true
end
--[[
Two sticks: used for debugging the four sticks.
--]]
function Shape.TwoSticks(s)
local ver = {
Vertex(1,2,1),
Vertex(1,-2,-1),
Vertex(2,1,-1),
Vertex(-2,1,1)
}
for k,v in ipairs(ver) do
s:addVertex(v)
end
local ed = {{0,1},{2,3}}
for k,v in ipairs(ed) do
s:addEdge(Edge(ver[v[1] + 1], ver[v[2] + 1]))
end
s:setVertexDefaults(.2,color(255,0,0,255))
s:setEdgeDefaults(10,color(255,255,0,255))
s:edgesRecalc()
s.checkIntersections = true
end
--[[
Buckminster fullerene (aka a football).
--]]
function Shape.Buckyball(s)
local phi, qa,v
phi = (math.sqrt(5) + 1)/2
for l = 0,2 do
qa= Quaternion.Rotation(2*l*math.pi/3,1,1,1)
for n = 0,7 do
i = 2*(n%2) - 1
j = 2*(math.floor(n/2)%2) -1
k = 2*(math.floor(n/4)%2) -1
v = Vec3(i*2,j*(1+2*phi),k*phi)
v = v^qa
s:addVertex(Vertex(v))
v = Vec3(i,j*(2+phi),k*2*phi)
v = v^qa
s:addVertex(Vertex(v))
end
for n = 0,3 do
i = 2*(n%2) - 1
j = 2*(math.floor(n/2)%2) -1
v = Vec3(0,i,j*3*phi)
v = v^qa
s:addVertex(Vertex(v))
end
end
s:setVertexDefaults(.1,color(255,0,0,255),3)
s:setEdgeDefaults(5,color(255,255,0,255))
s:addEdgesByValency()
end
--[[
An octohedron.
--]]
function Shape.Octohedron(s)
local q, v
for j = 0,2 do
q = Quaternion.Rotation(2*j*math.pi/3,1,1,1)
for i = -1,1,2 do
v = i*2*Vec3.e1^q
s:addVertex(Vertex(v))
end
end
s:setVertexDefaults(.2,color(255,0,0,255),4)
s:setEdgeDefaults(10,color(255,255,0,255))
s:addEdgesByValency()
end
--[[
A Lattice.
--]]
function Shape.Lattice(s)
local q, v, n
local sep = 2
local m = 2
for k = -m,m do
for j = -m,m do
for i = -m,m do
n = 6
if math.abs(k) == m then
n = n - 1
end
if math.abs(j) == m then
n = n - 1
end
if math.abs(i) == m then
n = n - 1
end
v = Vertex(sep * Vec3(i,j,k))
v.valency = n
s:addVertex(v)
end
end
end
s:setVertexDefaults(.1,color(255,0,0,255))
s:setEdgeDefaults(10,color(255,255,0,255))
s:addEdgesByValency()
end
--[[
Cube with faces.
--]]
function Shape.CubeFaceted(s)
local v = {}
for n = 0,7 do
i = 2*(n%2) - 1
j = 2*(math.floor(n/2)%2) -1
k = 2*(math.floor(n/4)%2) -1
table.insert(v,Vertex(i,j,k))
end
s.surface = true
s:addFace(Face({v[1],v[2],v[6],v[5]},Colour.svg.Red))
s:addFace(Face({v[3],v[4],v[2],v[1]},Colour.svg.Blue))
s:addFace(Face({v[5],v[6],v[8],v[7]},Colour.svg.Green))
s:addFace(Face({v[7],v[8],v[4],v[3]},Colour.svg.Yellow))
s:addFace(Face({v[5],v[7],v[3],v[1]},Colour.svg.Orange))
s:addFace(Face({v[2],v[4],v[8],v[6]},Colour.svg.Purple))
end
--[[
Cube with pictures.
--]]
function Shape.Die(s)
local v = {}
for n = 0,7 do
i = 2*(n%2) - 1
j = 2*(math.floor(n/2)%2) -1
k = 2*(math.floor(n/4)%2) -1
table.insert(v,Vertex(i,j,k))
end
s.surface = true
if not s:setTexture("Die") then
pushStyle()
local r = 100
local rl = 26
local rr = r - rl
local rd = r/10
local die = image(r,8*r)
setContext(die)
fill(255, 255, 255, 255)
strokeWidth(0)
noSmooth()
rect(0,0,r,6*r)
smooth()
noFill()
strokeWidth(3)
stroke(0, 56, 255, 255)
rectMode(CORNERS)
for i = 1,6 do
rect(0,(i-1)*r,r,i*r)
end
strokeWidth(0)
fill(0, 0, 0, 255)
ellipseMode(RADIUS)
for i = 1,3 do
ellipse(r/2, i*2*r- 3*r/2,rd)
ellipse(rr, i*r + 2*r + rr,rd)
ellipse(rl, i*r + 2*r + rl,rd)
end
for i = 1,5 do
ellipse(rl, i*r + rr,rd)
ellipse(rr, i*r + rl,rd)
end
ellipse(rl,11*r/2,rd)
ellipse(rr,11*r/2,rd)
setContext()
popStyle()
s:setTexture(die,"Die")
end
s:addFace(Face(
{v[1],v[2],v[6],v[5]},
Colour.svg.White,
{vec2(0,0),vec2(0,.125),vec2(1,.125),vec2(1,0)}
))
s:addFace(Face(
{v[3],v[4],v[2],v[1]},
Colour.svg.White,
{vec2(0,.125),vec2(0,.25),vec2(1,.25),vec2(1,.125)}
))
s:addFace(Face(
{v[5],v[6],v[8],v[7]},
Colour.svg.White,
{vec2(0,.25),vec2(0,.375),vec2(1,.375),vec2(1,.25)}
))
s:addFace(Face(
{v[7],v[8],v[4],v[3]},
Colour.svg.White,
{vec2(0,.375),vec2(0,.5),vec2(1,.5),vec2(1,.375)}
))
s:addFace(Face(
{v[5],v[7],v[3],v[1]},
Colour.svg.White,
{vec2(0,.5),vec2(0,.625),vec2(1,.625),vec2(1,.5)}
))
s:addFace(Face(
{v[2],v[4],v[8],v[6]},
Colour.svg.White,
{vec2(0,.625),vec2(0,.75),vec2(1,.75),vec2(1,.625)}
))
end
function Shape.Torus(s,t)
t = t or {}
local innerR = t.innerRadius or 1
local outerR = t.outerRadius or 2
local innerA = t.innerAxis or Vec3.e3
local outerA = t.outerAxis or Vec3.e2
local nipts,nopts
if t.innerPoints then
nipts = t.innerPoints
elseif t.maxInnerRectangle then
nipts = math.ceil(2*math.pi * innerR / t.maxInnerRectangle)
elseif t.points then
nipts = t.points
elseif t.maxRectangle then
nipts = math.ceil(2*math.pi * innerR/t.maxRectangle)
else
nipts = 10
end
if t.outerPoints then
nopts = t.outerPoints
elseif t.maxOuterRectangle then
nopts = math.ceil(2*math.pi * (innerR + outerR) / t.maxOuterRectangle)
elseif t.points then
nopts = t.points
elseif t.maxRectangle then
nopts = math.ceil(2*math.pi * (innerR + outerR) / t.maxRectangle)
else
nopts = 20
end
local faces = {}
local e1 = Vec3.e1
local e2 = Vec3.e2
local ipts,qi,qj,w,n
local pts = {}
local istep = 360/nipts
local ostep = 360/nopts
local iang = 180/istep
local oang = 180/ostep
for i = 1,nopts do
qi = Quaternion.Rotation(math.rad(i*ostep+oang),outerA)
ipts = {}
for j = 1,nipts do
qj = Quaternion.Rotation(math.rad(j*istep+iang),innerA)
n = e2^(qi * qj)
w = Vertex(outerR * e1^qi + innerR * n)
w.normal = n
table.insert(ipts,w)
table.insert(faces,{
{i,j},
{i%nopts+1,j},
{i%nopts+1,j%nipts+1},
{i,j%nipts + 1}
})
end
table.insert(pts,ipts)
end
for k,v in ipairs(faces) do
s:addFace(Face({pts[v[1][1]][v[1][2]],
pts[v[2][1]][v[2][2]],
pts[v[3][1]][v[3][2]],
pts[v[4][1]][v[4][2]]}))
end
s.surface = true
end
function Shape.Sphere(s,t)
t = t or {}
local radius = t.radius or 1.5
local npts
if t.points then
npts = t.points
elseif t.maxRectangle then
npts = math.ceil(2*math.pi * radius/t.maxRectangle)
else
npts = 10
end
local nipts = npts
local nopts = 2*npts
local faces = {}
local e1 = Vec3.e1
local ipts,qi,qj,w,n
local pts = {}
local istep = 180/nipts
local ostep = 360/nopts
for i = 1,nopts do
qi = Quaternion.Rotation(math.rad(i*ostep),Vec3.e2)
ipts = {}
for j = 1,nipts-1 do
qj = Quaternion.Rotation(math.rad(j*istep-90),Vec3.e3)
n = e1^(qi * qj)
w = Vertex(radius * n)
w.normal = n
table.insert(ipts,w)
if j ~= nipts-1 then
table.insert(faces,{
{i,j},
{i%nopts+1,j},
{i%nopts+1,j+1},
{i,j + 1}
})
end
end
table.insert(pts,ipts)
end
for k,v in ipairs(faces) do
s:addFace(Face({pts[v[1][1]][v[1][2]],
pts[v[2][1]][v[2][2]],
pts[v[3][1]][v[3][2]],
pts[v[4][1]][v[4][2]]}))
end
local top = Vertex(radius * Vec3.e2)
top.normal = Vec3.e2
local bot = Vertex(-radius * Vec3.e2)
bot.normal = -Vec3.e2
for i = 1,nopts do
s:addFace(Face({pts[i][1],bot,pts[i%nopts + 1][1]}))
s:addFace(Face({pts[i][nipts-1],pts[i%nopts + 1][nipts-1],top}))
end
s.surface = true
s.faceted = true
end
function Shape.Globe(s,t)
t = t or {}
local radius = t.radius or 1.5
local npts
if t.points then
npts = t.points
elseif t.maxRectangle then
npts = math.ceil(2*math.pi * radius/t.maxRectangle)
else
npts = 10
end
local nipts = npts
local nopts = 2*npts
local faces = {}
local e1 = Vec3.e1
local ipts,qi,qj,w,n
local pts = {}
local istep = 180/nipts
local ostep = 360/nopts
for i = 1,nopts do
qi = Quaternion.Rotation(math.rad(i*ostep),Vec3.e2)
ipts = {}
for j = 1,nipts-1 do
qj = Quaternion.Rotation(math.rad(j*istep-90),Vec3.e3)
n = e1^(qi * qj)
w = Vertex(radius * n)
w.normal = n
table.insert(ipts,w)
--[[
if j ~= nipts-1 then
table.insert(faces,{
{i,j},
{i%nopts+1,j},
{i%nopts+1,j+1},
{i,j + 1}
})
end
--]]
end
table.insert(pts,ipts)
end
for i = 1,nopts do
for j = 1,nipts-2 do
s:addFace(Face({pts[i][j],
pts[i%nopts+1][j],
pts[i%nopts+1][j+1],
pts[i][j+1]},
Colour.svg.White,
{
vec2((i-1)/nopts,j/nipts),
vec2(i/nopts,j/nipts),
vec2(i/nopts,(j+1)/nipts),
vec2((i-1)/nopts,(j+1)/nipts)
}))
end
end
--[[
--]]
local top = Vertex(radius * Vec3.e2)
top.normal = Vec3.e2
local bot = Vertex(-radius * Vec3.e2)
bot.normal = -Vec3.e2
for i = 1,nopts do
s:addFace(Face({
pts[i][1],bot,pts[i%nopts + 1][1]
},
Colour.svg.White,
{
vec2((i-1)/nopts,1/nipts),
vec2(i/nopts,0),
vec2(i/nopts,1/nipts)
}
))
s:addFace(Face({
pts[i][nipts-1],pts[i%nopts + 1][nipts-1],top
},
Colour.svg.White,
{
vec2((i-1)/nopts,1-1/nipts),
vec2(i/nopts,1-1/nipts),
vec2((i-.5)/nopts,1)
}
))
end
s.surface = true
--s.faceted = true
if not s:setTextureByName("Globe") then
s:setTexture(globe,"Globe")
end
end
local function buildAffine(f)
local o = f(Vec3.origin)
local e1 = f(Vec3.e1) - o
local e2 = f(Vec3.e2) - o
local e3 = f(Vec3.e3) - o
return function(u)
return Vec3(
e1.x * u.x + e2.x * u.y + e3.x * u.z + o.x,
e1.y * u.x + e2.y * u.y + e3.y * u.z + o.y,
e1.z * u.x + e2.z * u.y + e3.z * u.z + o.z
)
end
end
local function buildLinear(f)
local e1 = f(Vec3.e1)
local e2 = f(Vec3.e2)
local e3 = f(Vec3.e3)
return function(u)
return Vec3(
e1.x * u.x + e2.x * u.y + e3.x * u.z,
e1.y * u.x + e2.y * u.y + e3.y * u.z,
e1.z * u.x + e2.z * u.y + e3.z * u.z
)
end
end
--]==]
--[==[
--[[
Track definitions.
--]]
function TrackPoints(a)
a = a or {}
local pts = a.points or {}
local t = a.start or 0
local r = a.step or .1
r = r*r
local s = a.delta or .1
local f = a.pathFunction or function(q) return q*vec3(1,0,0) end
local nf = a.normalFunction or function(q) return vec3(0,1,0) end
local b = a.finish or 1
local tpt = f(t)
table.insert(pts,{tpt,
tangent({delta = s, pathFunction = f, time = t}),
nf(t),t})
local dis
local p
while t < b do
dis = 0
while dis < r do
t = t + s
p = f(t)
dis = dis + p:distSqr(tpt)
tpt = p
end
if t > b then
t = b
p = f(b)
end
table.insert(pts,{p,
tangent({delta = s, pathFunction = f, time = t}),
nf(t),t})
tpt = p
end
return pts
end
function tangent(a)
local s = a.delta/2 or .1
local f = a.pathFunction or function(q) return q*vec3(1,0,0) end
local t = a.time or 0
local u = f(t-s)
local v = f(t+s)
return (v-u)/(2*s)
end
local Tracks = {}
function Tracks.torus(p,q)
local innerRa = 10
local innerRb = 10
local outerR = 30
local trackFunction = function(t)
local it = p*t*2*math.pi
local ot = q*t*2*math.pi
return vec3(
(outerR + innerRb*math.cos(it))*math.cos(ot),
innerRa*math.sin(it),
(outerR + innerRb*math.cos(it))*math.sin(ot)
)
end
local trackNormal = function(t)
local it = p*t*2*math.pi
local ot = q*t*2*math.pi
return vec3(
innerRa*math.cos(it)*math.cos(ot),
innerRb*math.sin(it),
innerRa*math.cos(it)*math.sin(ot)
)
end
local coreFunction = function(t)
local ot = q*t*2*math.pi
return vec3(
outerR*math.cos(ot),
0,
outerR*math.sin(ot)
)
end
local maxHeight = innerRa
local minHeight = -innerRa
return trackFunction, trackNormal, maxHeight, minHeight
end
function Tracks.mobius()
local r = 30
local trackFunction = function(t)
local a = 2*math.pi*t
return vec3(r*math.cos(a),0,r*math.sin(a))
end
local trackNormal = function(t)
local a = math.pi*t
return vec3(
math.sin(a)*math.cos(2*a),
math.cos(a),
math.sin(a)*math.sin(2*a))
end
return trackFunction,trackNormal,0.5,-0.5
end
function Tracks.loop(p,q,r,h)
local r = r or 30
local h = h or 10
local w = vec3(0,50,0)
local trackFunction = function(t)
local a = 2*math.pi*t
return vec3(
r*math.cos(a),
h*math.sin(p*a),
r*math.sin(q*a))
end
local trackNormal = function(t)
return w - trackFunction(t)
end
return trackFunction,trackNormal,h,-h
end
cmodule.gexport {
tangent = tangent,
Tracks = Tracks,
TrackPoints = TrackPoints
}
--]==]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.