Code:
--[[
 Advanced animated mechanics for the 8860 set, extensively using the aniTools module.
 Copyright Roland Melkert 2017
 Free for non-commercial use.
 Version 2017-12-29
--]]
--==Options=============================================================================================================
--[[ local doLenStats=false --prints info about minimal needed animation length and estimates POV-Ray rendering time.
local frtA=35 --normal radiosity
local frtB=3.5*60 --HQ radiosity
local doLeftChairTiltDemo=true --left or right chair tilt demo
]]
--==Includes============================================================================================================
genTools=require('genTools')
aniTools=require('aniTools')
--==Sub actors==========================================================================================================
function initEngine(actor)
  --Engine table
  local result={}
  --Animation elements
  result.mainAxleAngle=actor:addAniElm(aniTools.aniElm(0))
  result.pistonRodPosOri={}
  result.pistonTopPosOri={}
  local m=ldc.matrix() --can use a single one as aniElm clones it.
  for i=1,4 do
    result.pistonRodPosOri[i]=actor:addAniElm(aniTools.aniElm(m))
    result.pistonTopPosOri[i]=actor:addAniElm(aniTools.aniElm(m))
  end
  result.coverVis=actor:addAniElm(aniTools.aniElm(true))
  result.coverPos=actor:addAniElm(aniTools.aniElm(0))
  --Joints
  local sf=ldc.subfile()
  local ZZ=ldc.vector(0, 0, -1) --negative to force positive angles for forward motion.
  local mainAxleRef=sf:getRef('axe_motor.ldr')
  actor:addJoint(aniTools.angleJoint(mainAxleRef, result.mainAxleAngle, ZZ))
  result.pistonRodJoint={}
  result.pistonTopJoint={}
  local c=mainAxleRef:getPos()
  for i=1,4 do
    --Use relative posOri joints for both pistons and tops.
    -- This simplifies the below maths as we can assume everything is relative to points along the main axle.
    local ref=sf:getRef('piston.ldr', i)
    c:setZ(ref:getPos():getZ())
    result.pistonRodJoint[i]=actor:addJoint(aniTools.posOriJoint(ref, result.pistonRodPosOri[i], c, false, ldc.matrix()))
    result.pistonTopJoint[i]=actor:addJoint(aniTools.posOriJoint(sf:getRef('2851.dat', i), result.pistonTopPosOri[i], c, false, ldc.matrix()))
  end
  --All axles are depended on de main engine axle, and thus normally would/could be handled in the engine apply function below.
  -- But the gearbox is depended on the resulting drive axle angle, so in order to animate that non static (gear shifts) this angle must be known/available during sequence processing.
  -- For this to be possible actions must be used, so lets define a custom one doing all axles in one go.
  result.depsAction=function(self)
    result=aniTools.depAction(0)
    result.engine=self
    result.calc=function(self, time)
    end
    return result
  end
  result.apply=function(self)
    --The pistons are always depended on a 'normal' animation element, so we can handle their final position in this apply funciton outside normal sequence processing.
    -- This allows for a slightly more efficient way of dealing with all of them in one go and removes the need to worry about it during sequencing.
    --We need to calculate their orientation changes resulting from the cylinder constrains.
    local mainAxleOri=ldc.matrix()
    mainAxleOri:setRotate(self.mainAxleAngle.value, 0, 0, -1)
    for i=1,4 do
      --All related joints maintain a center along the main axle.
      -- So we need the relative position of the piston (in rest) to that.
      local rpNeg=genTools.IF(i>2, 1, -1) --The rods are connect left and right in pairs.
      local bpNeg=genTools.IF((i%2)==0, 1, -1) --The heads go from left to right, odd being at the positive side of X
      --The rods are connected to the offcenter axle's
      local p1=ldc.vector(rpNeg*16, 0, 0)
      --Rotate it to its current position.
      p1:transform(mainAxleOri)
      --The head is normally located 60 units further away.
      local blockPos=ldc.vector(bpNeg*(60+16), 0, 0)
      --Now we get the pistons 'Y' direction in local space directly from its original reference.
      local yDir=self.pistonRodJoint[i].orgAbsPosOri:getOriYRow()
      --How far is the piston position from the line the piston moves on.
      -- getDistance also returns the (pos/neg) distance from the line's position to p1 when based upon the normalized line vector.
      local dist, mul=p1:getDistance(blockPos, yDir)
      --Use this to calculate the (shortest) position on the movement line in relation to the piston position.
      local pShort=blockPos+yDir*mul
      --These two points together with the unknown top position form a triangle.
      -- We use this to calculate the distance of the 'used' part of the movement line in relation to the nearest point on it.
      local s=math.acos(dist/60)
      s=math.sin(s)*60
      --And then use that distance to move the 'near' point along the line.
      -- which is the position of the top.
      local p2=pShort-yDir*s
      --Now we need to orientate the piston so the joint can place it correctly.
      -- Local Y goes from piston to top, note neg due to LDraw.
      local pistonY=p1-p2
      -- Local Z never changes as the axle is parallel to it, so get it from the original reference.
      local pistonZ=self.pistonRodJoint[i].orgAbsPosOri:getOriZRow()
      -- And having two, we can get the third as the crossproduct because everything needs to be perpendicular.
      local pistonX=pistonY:getCross(pistonZ)
      --Use this and the calculated position to construct a matrix.
      local posOri=ldc.matrix()
      posOri:setOriRows(pistonX, pistonY, pistonZ) --Do note we can't do it in the matrix constructor function as that expects 'columns', you could invert it later though.
      posOri:setPos(p1)
      --Apply to the aniElm
      self.pistonRodJoint[i].elm.value:set(posOri)
      --The top piston only needs its position changed, but as we are using relative joints we also need to apply the original top orientation.
      posOri:setOri(self.pistonTopJoint[i].orgAbsPosOri)
      posOri:setPos(p2)
      --Apply to the aniElm
      self.pistonTopJoint[i].elm.value:set(posOri)
    end
  end
  return result
end
--==Main actor==========================================================================================================
function carActor()
  --Start with a basic actor table.
  local result=aniTools.actor()
  --Split mechanics / control elements into logical sub tables.
  result.engine=initEngine(result) -- This handles the engine axles and pistons.
  --Override the apply function so we can do all the auto dependencies.
  -- This could also be done using custom dependency actions.
  -- But there is no real advantage to that method in a large animation like this one.
  --Buffer the default apply function as we are overriding it to do some auto dependencies.
  -- You could skip this if you don't use joints in an animation, but we do.
  result.orgApply=result.apply
  result.apply=function(self)
    self.engine:apply()
    -- The default function processes all joints so it must be called in our version too.
    self:orgApply()
  end
  return result
end
function camActor(story)
  --Initial/rest state 3rd person camera.
  local cam=ldc.camera(3, ldc.vector(46.4297, -27.8264, -53.1873),  2074.9246,  69.8029,  35.1886, 0)
  --Register it as an output camera, the returned cam object is a DIFFERENT one as it needs to be a 'linked' one.
  cam=story:addCamera(cam)
  --Create an actor for this output camera.
  result=story:addActor(aniTools.cameraActor(cam))
  --Easy to use spin action generator.
  result.spin=function(self, seq, ofs, len)
    seq:addAction(aniTools.diffAction(self.yaw, ofs, len, 360))
    return len
  end
  return result
end
--==Events==============================================================================================================
function onStart()
  movie=aniTools.story()
  local car=movie:addActor(carActor())
  print (car)
--  local cam=camActor(movie)
  --Add the main sequence to the story
  local mainSeq=movie:addSequence(aniTools.sequence());
  -- Main dependency actions.
  mainSeq:addAction(car.engine:depsAction())
  --Engine speed
  local driveSeq=aniTools.sequence()
  driveSeq:addAction(aniTools.accelAction(car.engine.mainAxleAngle, 0, 2, 0, 150))
  driveSeq:addAction(aniTools.speedAction(car.engine.mainAxleAngle, 2, nil, 150))
  driveSeq:addAction(aniTools.accelAction(car.engine.mainAxleAngle, 0, 2, 150, 0))
  driveSeq.finLen=function(self, len) self.actions[2].len=len-4  self.actions[3].ofs=len-2 end --The length of motion is set at the end of storyboarding.
  --Put together the story.
  local ofs=0
  local len=0
  local driveOfs=ofs
  mainSeq:addRef(driveSeq, ofs, nil)
  --Done setting up the story
  movie:prep() --If not here it will be automatically done in the first apply call, which might cause a 'too slow playback' error in complicated animations.
  if doLenStats then
    print('Length needed: ', ofs, ', est render time: ', genTools.round(ofs*25*frtA/60/60, 2), ' hours, HQ: ', genTools.round(ofs*25*frtB/60/60, 2), ' hours.')
  end
end
function onFrame()
  movie:apply(ldc.animation():getFrameTime())
end
--==Register============================================================================================================
function register()
  local ani=ldc.animation('Demo')
  ani:setLength(10)
  ani:setEvent('start', 'onStart')
  ani:setEvent('frame', 'onFrame')
end
register()