Animate on a timeline
A driver is the input a person moves. Some machines also have a motion
that is simply a function of time: a clock’s hands, an engine on a
stand turning at a steady speed, a demonstration that plays by itself.
That is the animation timeline, and self.time is its driver.
self.time
On an assembly, simulate() may read self.time. With no time base
declared it is a number between 0 and 1 that loops in the viewer, and
symbolic in the build, so the expression travels to the browser
unevaluated:
class SimpleClock(AssemblyNode):
base = ClockBase()
pointer = Pointer()
def simulate(self):
self.pointer.rotate(-360 * self.time, [0, 0, 1])
The angle is negative because a positive rotation about Z is counter-clockwise, and clocks run clockwise. Scrub the viewer to 0.25 and the pointer has turned a quarter turn.
time is one driver among the others, and one expression may mix it
with a declared driver. Under a stepped simulation the same
self.time reads the simulation clock in seconds
(Three ways a machine runs).
Give the loop a duration
The timeline says nothing about how long one turn of it is. A machine modelled in real time declares that on its root:
from machinome.motion.ports import Time
class WallClock(AssemblyNode):
time = Time(loop=12 * 3600)
def simulate(self):
self.movement.seconds = self.time
loop is the span of machine time, in seconds, that one turn of the
timeline covers. From then on self.time reads seconds everywhere: in
simulate(), in every assembly below the root, in tests and in
snapshots. The published document carries the loop, and the viewer plays
it at real time with a speed control; a twelve-hour clock at ×720 turns
its hour hand once a minute.
Declare Time only as time, only on the root: descendants read the
root’s base, and a declaration on a linked descendant is refused. The
other two spellings, Time.running() and Time.elapsed(), select
the other execution models and are not timelines; under
Time.running() the clock may drive a relation directly,
time.drives(shaft.turn, ratio=6), which is retained motion rather than
a pose over a timeline (Running mechanics).
Motion without a branch
In the viewer there is no value to branch on, so a law over time is
arithmetic, and machinome.math carries the arithmetic that expresses
a mechanism without an if. Every function there has three faces: it
computes on plain numbers, defers as an expression when a value is
symbolic, and checks dimensions when a value is a declared parameter.
Python’s math would fail on the symbolic face and works in radians;
all angles in Machinome are degrees.
sin,cos,tan,asin,acos,atan,atan2,sqrt: OpenSCAD’s degree-based trigonometry.abs,floor,ceil,sign,min,max: the builtins OpenSCAD and JavaScript agree on.clamp(x, low, high)andclamp01(x): a value held within bounds, which is how a part stops at a stop.ramp(x, start, end): 0 beforestart, 1 afterend, straight between, so a stage of a timeline is one term.lerp(a, b, u):aat 0,bat 1, unclamped.wrap(angle): an angle folded into (-180, 180];wrap(value, period)for anything else that repeats.piecewise(x, points): linear interpolation through measured waypoints, held flat past each end.bump(u): a smooth 0-1-0 pulse overuin [0, 1].
A cam that dwells and then lifts, over a measured profile:
from machinome.math import piecewise
PROFILE = [(0.0, 0.0), (90.0, 0.0), (150.0, 12.0), (210.0, 0.0)]
class Valve(AssemblyNode):
def simulate(self):
angle = 360 * self.time
self.stem.translate([0, 0, piecewise(angle, PROFILE)])
There is deliberately no round and no mod: OpenSCAD, JavaScript
and Python round halves three different ways, and OpenSCAD spells modulo
as an operator with a different sign rule. floor(x + 0.5) is the
half-up every runtime agrees on, and wrap is built on ceil.
Points, not just numbers: polar(radius, angle), turn(point, angle,
about=...) and rotate_x, rotate_y, rotate_z turn points with
the same three faces, so a point turned by a driver-derived angle
survives the viewer. A rotation’s axis is a constant and cannot carry a
symbol; a rod leaning about a direction the effector’s position decides
is two rotations about constant axes, which is what the delta helper in
Machinome Mechanics
returns.
Freezing an instant
set_keyframe(t) pins an assembly and everything below it to one
instant: self.time becomes the number t and meshes resolve
numerically. That is what tests and single-instant renders do.
clear_keyframe() releases it back to symbolic time. Nothing
accumulates however often you freeze and release, and rest placement is
untouched.
clock.set_keyframe(0.25)
pose = clock.pointer.mesh
clock.clear_keyframe()
machinome export never freezes, so an export always carries the
animation. To show one instant of an exported model, use the page’s
?t= and ?autoplay=0 options rather than publishing a frozen
document.
Testing along the timeline
@testing_steps(n) runs a test at n instants of the timeline and
@testing_instant(t) at one, so a clearance that must hold through a
turn is one decorated assertion:
from machinome.test import TestCase, testing_steps
class SimpleClockTest(TestCase):
@testing_steps(16)
def test_pin_runs_free_in_pointer(self):
self.assertNotIntersecting(self.node.pointer, self.node.pin)
Under a declared loop the decorators take seconds; their defaults do not follow the declaration, so a sweep over a declared root states the span it covers. A machine driven by inputs rather than by time is swept with a scenario instead (7. Step it through a move).