Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 18

Radio towers

The radio towers are the tallest things beside the road: lattices of thin rods, 80 metres high, standing well back from it. One stands past the end of every segment, off to the right, and a long segment has one more halfway along, off to the left.

Step 1,529 of the ride: the road and its rails, with the radio towers standing back from it.

A tower raises two problems of its own. Far off, it should sink below the horizon from the bottom up, as the ground curves away under it. Up close, it can stand partly behind the camera. The chapter takes them in that order, after the tower's place by the road.

Where a tower stands §

A tower is two facts: its yaw, which way its square footprint is turned, and its beacon, the light at its top:

Tower.roc, in Tower
## A tower in a frame: how it is turned, and its beacon as it shines.
Tower :: { yaw : Angle.Turn, beacon : Beacon }.{
# ...

Trees get their places once, when the world is built. A tower's beacon blinks with the clock, so its sighting builds the tower afresh each frame, from its segment and the clock. Here's the tower past a segment's end:

TowerSighting.roc
## The tower past the end of a segment in view, if it is in front of the
## camera and above the horizon.
end_tower : RoadAhead.RoadView,
{
	vantage : Geom.Vantage,
	in_view : RoadAhead.SegmentInView,
	clock : Clock,
} -> Try(Pipeline.Sighted(Tower), [OutOfSight])
end_tower = |road, { vantage, in_view, clock }| {
	beacon = Beacon.at({ segment: in_view.index, place: AtEnd }, clock)
	tower_if_ahead(
		road,
		{
			vantage,
			from_rider: in_view.from_rider,
			road_point: {
				along: Along.at(in_view.segment.length.plus(end_tower_beyond_end)),
				across: Across.right_of_middle(end_tower_right_of_middle),
			},
			tower: Tower.standing({ yaw: end_tower_yaw, beacon }),
		},
	)
}

end_tower turns the tower 30 degrees from square to the road. tower_if_ahead stands the tower at its point by the road, and returns its sighting if it's in front of the camera and some of it shows above the horizon. The tower halfway along is the same, with its footprint square to the road, a Midway beacon, and one more way to fail: mid_tower_point gives Err(NoMidTower) for a segment of a kilometre or less. TowerSighting is the one home for where towers stand.

Over the horizon §

The ride's ground curves away on its 100 km radius, and a tower stands on it. From the rider's eye, 1.2 metres up, the ground curves out of sight 490 metres out. Past that horizon, the lowest thing the rider can see lies on the line from the eye that grazes the ground there. One function finds that lowest height at any point on the ground:

EarthCurve.roc
lowest_visible_height : RiderView.RiderPoint -> Distance

The ground hides whatever part of a tower is under that line, so as a tower gets further away, it sinks from the bottom up. An 80-metre tower still shows its tip about 4.5 km off. Once all of it is under the line, Pipeline.sighting_above_horizon_of refuses it as OutOfSight, as in the “Errors as tag unions” chapter. So a sighted tower always has something to paint.

Rods §

Tower.tower_shapes returns a tower as a few dozen rods, and a disc for its beacon when the beacon is bright enough and big enough to show. It starts by checking that some of the tower shows above the horizon:

Tower.roc, in tower_shapes
# ...
if hidden_on_tower(geometry) >= tower_height {
	crash "Tower.tower_shapes: a tower hidden wholly below the horizon is refused at its sighting"
	# ...

Its sighting has already refused a tower the horizon hides wholly. We enforce invariants with crash, and the invariant here is that every tower this function gets shows some part above the horizon. Otherwise come the legs, the rings and the braces, and the beacon's disc, when it shows, last, at the top.

A tower's footprint is a square, and four tags name its corners. Going round the square is a match:

Tower.roc
## The corner after this one, going round the square.
next_corner : Corner -> Corner
next_corner = |corner|
	match corner {
		BackLeft => BackRight
		BackRight => FrontRight
		FrontRight => FrontLeft
		FrontLeft => BackLeft
	}

Four legs run from the corners up to the top, each starting at the height the horizon hides. Every 20 metres up, a ring of four rods joins each corner to the next one round, and crossed braces fill the lowest two storeys.

Each rod is a line between two points seen from the rider. A near tower is wide, so its foot can stand ahead of the camera while a corner of its footprint stands beside or behind it. So each rod goes through this function:

RiderView.roc
## The part of the line from `from` to `to` at or past the near plane: the
## whole line, or the line cut where it crosses the near plane. A line
## with both ends behind the near plane has no such part.
line_ahead : { from : RiderView.PointAtHeight, to : RiderView.PointAtHeight } -> Try(
	{ from : RiderView.PointAtHeightAhead, to : RiderView.PointAtHeightAhead },
	[BehindCamera],
)
line_ahead = |{ from, to }|
	match (RiderView.point_at_height_ahead(from), RiderView.point_at_height_ahead(to)) {
		(Ok(start), Ok(end)) => Ok({ from: start, to: end })
		(Ok(start), Err(BehindCamera)) =>
			Ok({ from: start, to: near_plane_crossing({ from, to }) })
		(Err(BehindCamera), Ok(end)) =>
			Ok({ from: near_plane_crossing({ from, to }), to: end })
		(Err(BehindCamera), Err(BehindCamera)) => Err(BehindCamera)
	}

The match is on a tuple, (start, end): two values side by side, in parentheses. So it covers all four combinations of its two Trys. A rod with one end short of the near plane, 0.4 metres ahead of the camera, is cut where it crosses that plane. A rod with both ends behind it is refused.

Each frame derives a tower afresh, its rods and its disc, from its segment, the clock and the camera. The disc on top blinks with the clock, which starts running in the second half of the book, in the “The beacons” chapter. The last things to stand in the frame are the herds.