"""Tutorial - Signed distance fields

This script demonstrates how to:
* Build a signed distance field from a closed triangle mesh and use it as a collision shape.
* Query the field for the signed distance and gradient at a point.
* Collide signed distance fields against each other and against the primitive shapes.
* Simulate tight, non-convex contacts: a hanging chain, and a nut that turns down a bolt.

Background:
agxCollide.SignedDistanceField is a rigid shape represented by a sampled signed distance field,
the distance to the surface, negative inside. The field is held by an
agxCollide.SignedDistanceFieldGrid, which is built once from a closed triangle mesh. A grid can be
shared by many shapes and saved to file with write() and loaded with read().

A field answers how deep a point is in constant time, however complex the mesh is. This suits
non-convex shapes where a convex decomposition is a poor fit: threads, gears, chains and buckets.
The field also keeps the triangles it was built from, for contact generation and for rendering.

Supported shape pairs: SignedDistanceField, Box, Sphere, Capsule, Cylinder, Convex, Trimesh,
HeightField, Plane and Line.

Scenes:
1. Rings dropped on a peg: building fields, querying them.
2. Primitives dropped in a bowl.
3. A chain of links, held only by contacts between signed distance fields.
4. A nut that turns down a bolt under its own weight, signed distance fields.
5. The same nut and bolt, starting as triangle meshes.
6. Signed distance fields dropped on a plane, a box, cylinders, capsules and a convex.
7. Many rings in a bowl.
8. What a field represents: the surface of the field of a nut at four voxel sizes, colored by its
   distance to the source mesh, next to the source mesh.

In every scene, the key k switches the signed distance fields to triangle meshes of the same
triangles and back, for comparison, with the collision detection and step times shown. For a
benchmark without graphics, see data/python/signed_distance_field_benchmark.py.

agxOSG.createVisual draws the triangles a field was built from. To see the field itself, use
agxOSG.createSignedDistanceFieldVisual, as in scene 8.
"""

import math
import time

# AGX Dynamics imports
import agx
import agxCollide
import agxIO
import agxOSG
import agxRender
import agxSDK

# Import useful utilities to access the current simulation, graphics root and application
from agxPythonModules.utils.environment import application, init_app, root, simulation
from tutorial_utils import createHelpText

# Shapes that Python does not own, held by references (see addRepresentationSwitch).
_meshReferences = []


def readMesh(fileName, scale):
    """Read a mesh file and scale it: (vertices, indices)."""
    reader = agxIO.MeshReader()
    assert reader.readFile(fileName), fileName
    vertices = agx.Vec3Vector()
    for vertex in reader.getVertices():
        vertices.append(vertex * scale)
    # Copy: getIndices() refers into the reader, which is deleted on return.
    return vertices, agx.UInt32Vector(reader.getIndices())


def createField(vertices, indices, voxelSize, name):
    """Build a signed distance field from a closed triangle mesh."""
    # The voxel size is the resolution of the field near the surface. Features smaller than a
    # few voxels are rounded off, so pick it from the smallest feature that matters.
    options = agxCollide.SignedDistanceFieldGridOptions()
    options.voxelSize = voxelSize
    start = time.perf_counter()
    grid = agxCollide.SignedDistanceFieldGrid.createFromMesh(vertices, indices, options)
    print(
        f"{name}: {len(indices) // 3} triangles, {voxelSize * 1000:.1f} mm voxels, built in "
        f"{time.perf_counter() - start:.2f} s, {grid.getMemoryUsage() / 1e6:.2f} MB"
    )
    # A field takes time to build: an application can build it once, save it with
    # grid.write("name.sdf") and load it with agxCollide.SignedDistanceFieldGrid.read("name.sdf").
    return grid


def addBody(shape, position, rotation=None, material=None, color=None):
    """Create a dynamic body with one shape and add it, with graphics, to the simulation."""
    geometry = agxCollide.Geometry(shape)
    if material is not None:
        geometry.setMaterial(material)
    body = agx.RigidBody(geometry)
    body.setPosition(position)
    if rotation is not None:
        body.setRotation(rotation)
    simulation().add(body)
    node = agxOSG.createVisual(body, root())
    if color is not None:
        agxOSG.setDiffuseColor(node, color)
    return body


def addStatic(shape, position=agx.Vec3(), rotation=None, material=None, color=None):
    """Create a static geometry and add it, with graphics, to the simulation."""
    geometry = agxCollide.Geometry(shape)
    if material is not None:
        geometry.setMaterial(material)
    geometry.setPosition(position)
    if rotation is not None:
        geometry.setRotation(rotation)
    simulation().add(geometry)
    node = agxOSG.createVisual(geometry, root())
    if color is not None:
        agxOSG.setDiffuseColor(node, color)
    return geometry


def addFloor(halfSize, z=0.0):
    addStatic(
        agxCollide.Box(agx.Vec3(halfSize, halfSize, 0.05)),
        agx.Vec3(0, 0, z - 0.05),
        color=agxRender.Color.LightGray(),
    )


def buildRings():
    """Rings dropped on a peg."""
    print("== running scene buildRings ==")
    createHelpText(
        simulation(),
        application(),
        ["Rings (signed distance fields) dropped on a peg (cylinder)"],
    )

    # Contacts come from how deep one shape is in the other. If a body moves through a large part
    # of a thin feature in one step, here a 0.05 m thick ring, the direction out is ambiguous and
    # the shapes can be pushed the wrong way or pass through. Keep the motion per step well below
    # the thinnest feature, which here takes a smaller time step than the default 1/60 s.
    simulation().setTimeStep(1.0 / 250.0)

    # One field for all rings: the grid is shared by the shapes.
    # The torus is 0.25 m across with a 0.05 m thick ring, its axis along y.
    vertices, indices = readMesh("models/torusRound.obj", 0.1)
    grid = createField(vertices, indices, 0.002, "torus")

    # A field answers distance queries in its own frame: negative inside, with the gradient
    # pointing out of the surface. It is accurate in a narrow band around the surface, and
    # coarser further away. The ring is 0.025 m thick around a circle of radius 0.1 m.
    gradient = agx.Vec3()
    for point in [agx.Vec3(0.128, 0, 0), agx.Vec3(0.1, 0.02, 0), agx.Vec3(0, 0, 0.072)]:
        distance = grid.evaluate(point, gradient)
        print(f"  distance at {point}: {distance * 1000:6.1f} mm, gradient {gradient}")

    addFloor(1.0)
    addStatic(
        agxCollide.Cylinder(0.04, 0.6),
        agx.Vec3(0, 0, 0.3),
        agx.Quat(math.pi / 2, agx.Vec3.X_AXIS()),
        color=agxRender.Color.SteelBlue(),
    )

    # Turn the rings so their axis is vertical, tilt them a little and drop them on the peg.
    colors = [
        agxRender.Color.Gold(),
        agxRender.Color.OrangeRed(),
        agxRender.Color.YellowGreen(),
    ]
    for i in range(6):
        rotation = agx.Quat(math.pi / 2 + 0.3 * (i % 2 - 0.5), agx.Vec3.X_AXIS())
        offset = 0.03 * (i % 3 - 1)
        addBody(
            agxCollide.SignedDistanceField(grid),
            agx.Vec3(offset, -offset, 0.75 + 0.15 * i),
            rotation,
            color=colors[i % len(colors)],
        )

    addRepresentationSwitch()

    application().setCameraHome(
        agx.Vec3(1.2, -1.6, 1.1), agx.Vec3(0, 0, 0.35), agx.Vec3.Z_AXIS()
    )


def buildPrimitivesInBowl():
    """Primitive shapes and triangle meshes dropped in a bowl."""
    print("== running scene buildPrimitivesInBowl ==")
    createHelpText(
        simulation(),
        application(),
        ["Shapes dropped in a static bowl (signed distance field)"],
    )
    # Small shapes falling fast: a small time step, see buildRings.
    simulation().setTimeStep(1.0 / 240.0)

    # The bowl is about 1.1 m across and 0.45 m high.
    vertices, indices = readMesh("models/bowl.obj", 0.15)
    addStatic(
        agxCollide.SignedDistanceField(createField(vertices, indices, 0.005, "bowl")),
        color=agxRender.Color.SteelBlue(),
    )
    addFloor(1.5)

    torusVertices, torusIndices = readMesh("models/torusRound.obj", 0.04)
    sphereVertices, sphereIndices = readMesh("models/icosphere2.obj", 0.06)
    shapes = [
        lambda: agxCollide.Box(agx.Vec3(0.05, 0.04, 0.03)),
        lambda: agxCollide.Sphere(0.05),
        lambda: agxCollide.Capsule(0.035, 0.08),
        lambda: agxCollide.Cylinder(0.045, 0.06),
        lambda: agxCollide.Convex(sphereVertices, sphereIndices, "convex"),
        lambda: agxCollide.Trimesh(torusVertices, torusIndices, "torus"),
    ]
    colors = [
        agxRender.Color.Gold(),
        agxRender.Color.OrangeRed(),
        agxRender.Color.YellowGreen(),
        agxRender.Color.Violet(),
        agxRender.Color.Cyan(),
        agxRender.Color.Coral(),
    ]
    for layer in range(3):
        for k, createShape in enumerate(shapes):
            angle = 2 * math.pi * (k + 0.5 * layer) / len(shapes)
            position = agx.Vec3(
                0.25 * math.cos(angle), 0.25 * math.sin(angle), 0.65 + 0.15 * layer
            )
            addBody(
                createShape(),
                position,
                agx.Quat(0.4 * k + layer, agx.Vec3(1, 1, 0)),
                color=colors[k],
            )

    addRepresentationSwitch()

    application().setCameraHome(
        agx.Vec3(1.2, -1.6, 1.4), agx.Vec3(0, 0, 0.2), agx.Vec3.Z_AXIS()
    )


def createWedge():
    """A convex wedge, 0.4 m wide, 0.5 m long and 0.15 m high, with the ridge along y."""
    vertices = agx.Vec3Vector()
    for point in [
        (-0.2, -0.25, 0),
        (-0.2, 0.25, 0),
        (0.2, -0.25, 0),
        (0.2, 0.25, 0),
        (0, -0.25, 0.15),
        (0, 0.25, 0.15),
    ]:
        vertices.append(agx.Vec3(*point))
    indices = agx.UInt32Vector()
    for triangle in [
        (0, 1, 3),
        (0, 3, 2),
        (0, 4, 5),
        (0, 5, 1),
        (2, 5, 4),
        (2, 3, 5),
        (0, 2, 4),
        (1, 5, 3),
    ]:
        for index in triangle:
            indices.append(index)
    return agxCollide.Convex(vertices, indices, "wedge")


def buildFieldsOnPrimitives():
    """Signed distance fields dropped on static primitives."""
    print("== running scene buildFieldsOnPrimitives ==")
    createHelpText(
        simulation(),
        application(),
        [
            "Signed distance fields (rings, links, spheres) dropped on a plane, a box,",
            "cylinders, capsules and a convex wedge",
        ],
    )
    # Thin rings and links: a small time step, see buildRings.
    simulation().setTimeStep(1.0 / 240.0)

    addStatic(agxCollide.Plane(agx.Vec3.Z_AXIS(), 0.0))
    # agxOSG does not draw planes: a thin box under it, only for the graphics.
    floor = addStatic(
        agxCollide.Box(agx.Vec3(1.5, 1.5, 0.01)),
        agx.Vec3(0, 0, -0.01),
        color=agxRender.Color.LightGray(),
    )
    floor.setEnableCollisions(False)
    # A ramp, horizontal cylinders and capsules to bounce off, and a convex wedge.
    addStatic(
        agxCollide.Box(agx.Vec3(0.4, 0.3, 0.02)),
        agx.Vec3(-0.5, 0, 0.35),
        agx.Quat(0.35, agx.Vec3.Y_AXIS()),
        color=agxRender.Color.SteelBlue(),
    )
    for k in range(3):
        addStatic(
            agxCollide.Cylinder(0.03, 0.8),
            agx.Vec3(0.1 + 0.18 * k, 0, 0.25 + 0.06 * (k % 2)),
            color=agxRender.Color.LightSlateGray(),
        )
        addStatic(
            agxCollide.Capsule(0.025, 0.6),
            agx.Vec3(0.55, -0.3 + 0.3 * k, 0.12),
            agx.Quat(math.pi / 2, agx.Vec3.Z_AXIS()),
            color=agxRender.Color.CadetBlue(),
        )
    addStatic(createWedge(), agx.Vec3(0.2, 0.6, 0), color=agxRender.Color.SlateBlue())

    torusVertices, torusIndices = readMesh("models/torusRound.obj", 0.05)
    linkVertices, linkIndices = readMesh("models/Link_v2.obj", 0.0002)
    sphereVertices, sphereIndices = readMesh("models/icosphere2.obj", 0.06)
    grids = [
        createField(torusVertices, torusIndices, 0.002, "torus"),
        createField(linkVertices, linkIndices, 0.001, "link"),
        createField(sphereVertices, sphereIndices, 0.002, "sphere"),
    ]
    colors = [
        agxRender.Color.Gold(),
        agxRender.Color.OrangeRed(),
        agxRender.Color.YellowGreen(),
    ]
    for i in range(18):
        position = agx.Vec3(
            -0.6 + 0.26 * (i % 6), -0.25 + 0.4 * (i // 6), 0.9 + 0.1 * (i % 3)
        )
        addBody(
            agxCollide.SignedDistanceField(grids[i % 3]),
            position,
            agx.Quat(0.9 * i, agx.Vec3(1, 1, 0)),
            color=colors[i % 3],
        )

    addRepresentationSwitch()

    application().setCameraHome(
        agx.Vec3(1.0, -2.0, 1.2), agx.Vec3(0, 0, 0.25), agx.Vec3.Z_AXIS()
    )


class StepTimer(agxSDK.StepEventListener):
    """Wall time of collision detection (pre-collide to pre-step: broad and narrow phase) and of
    the whole step, as means over the last 120 steps."""

    def __init__(self):
        super().__init__(
            agxSDK.StepEventListener.PRE_COLLIDE
            | agxSDK.StepEventListener.PRE_STEP
            | agxSDK.StepEventListener.POST_STEP
        )
        self.clear()

    def clear(self):
        self.collision = []
        self.step = []

    def preCollide(self, t):
        self.start = time.perf_counter()

    def pre(self, t):
        self.collision = (self.collision + [(time.perf_counter() - self.start) * 1000])[
            -120:
        ]

    def post(self, t):
        self.step = (self.step + [(time.perf_counter() - self.start) * 1000])[-120:]


class SwitchRepresentation(agxSDK.GuiEventListener):
    """Switches every shape between a signed distance field and a triangle mesh with the k key, and
    shows the time spent in collision detection and in the step."""

    KEY = "k"

    def __init__(self, geometries, timer, useField=True, firstRow=0):
        super().__init__(
            agxSDK.GuiEventListener.KEYBOARD | agxSDK.GuiEventListener.UPDATE
        )
        # (geometry, field shape, mesh shape); the field shapes are in use from start if useField,
        # else the mesh shapes. The texts are on three rows from firstRow.
        self.geometries = geometries
        self.timer = timer
        self.useField = useField
        self.firstRow = firstRow

    def switch(self):
        self.useField = not self.useField
        for geometry, field, mesh in self.geometries:
            geometry.remove(mesh if self.useField else field)
            geometry.add(field if self.useField else mesh)
        self.timer.clear()

    def keyboard(self, key, x, y, alt, down):
        if down and key == ord(self.KEY):
            self.switch()
            return True
        return False

    def update(self, x, y):
        contacts = self.getSimulation().getSpace().getGeometryContacts()
        points = sum(len(contact.points()) for contact in contacts)
        decorator = application().getSceneDecorator()
        kind = "signed distance fields" if self.useField else "triangle meshes"
        decorator.setText(
            self.firstRow,
            f"Shapes: {kind}. Press {self.KEY} to switch.",
            agxRender.Color.White(),
        )
        if self.timer.step:
            collision = sum(self.timer.collision) / len(self.timer.collision)
            step = sum(self.timer.step) / len(self.timer.step)
            decorator.setText(
                self.firstRow + 1,
                f"Collision detection {collision:6.2f} ms, step {step:6.2f} ms (mean of the last {len(self.timer.step)} steps)",
                agxRender.Color.White(),
            )
        decorator.setText(
            self.firstRow + 2,
            f"{len(contacts)} geometry contacts, {points} contact points",
            agxRender.Color.White(),
        )


def addRepresentationSwitch(firstRow=0):
    """Let the key k switch every signed distance field in the scene to a triangle mesh of the same
    triangles, and back."""
    geometries = []
    for geometry in simulation().getGeometries():
        geometry = geometry.get() if hasattr(geometry, "get") else geometry
        shapes = [shape.get() if hasattr(shape, "get") else shape for shape in geometry.getShapes()]
        if len(shapes) != 1 or shapes[0].getType() != agxCollide.Shape.SIGNED_DISTANCE_FIELD:
            continue
        field = shapes[0].asSignedDistanceField()
        # A field is also a triangle mesh: a shallow copy is a plain Trimesh of the same triangles,
        # sharing them. Python does not own the copy, or the field once it is removed from the
        # geometry: hold them by references.
        mesh = agxCollide.TrimeshRef(field.shallowCopy())
        _meshReferences.extend([mesh, agxCollide.SignedDistanceFieldRef(field)])
        geometries.append((geometry, field, mesh.get()))
        # Keep the mass properties when the shape is switched: the field and the mesh compute
        # them slightly differently.
        body = geometry.getRigidBody()
        if body is not None:
            body.getMassProperties().setAutoGenerateMask(
                agx.MassProperties.AUTO_GENERATE_NONE
            )

    timer = StepTimer()
    simulation().add(timer)
    simulation().add(SwitchRepresentation(geometries, timer, firstRow=firstRow))


def buildSwitchRepresentation():
    """Tori in a bowl, switch between signed distance fields and triangle meshes with a key."""
    print("== running scene buildSwitchRepresentation ==")
    # Thin rings: a small time step, see buildRings.
    simulation().setTimeStep(1.0 / 240.0)

    bowlVertices, bowlIndices = readMesh("models/bowl.obj", 0.15)
    addStatic(
        agxCollide.SignedDistanceField(createField(bowlVertices, bowlIndices, 0.005, "bowl")),
        color=agxRender.Color.SteelBlue(),
    )

    # One grid for all rings.
    torusVertices, torusIndices = readMesh("models/torusRound.obj", 0.05)
    torusGrid = createField(torusVertices, torusIndices, 0.002, "torus")
    colors = [
        agxRender.Color.Gold(),
        agxRender.Color.OrangeRed(),
        agxRender.Color.YellowGreen(),
    ]
    for i in range(36):
        layer, k = divmod(i, 6)
        angle = 2 * math.pi * (k + 0.5 * layer) / 6
        addBody(
            agxCollide.SignedDistanceField(torusGrid),
            agx.Vec3(
                0.22 * math.cos(angle), 0.22 * math.sin(angle), 0.6 + 0.12 * layer
            ),
            agx.Quat(0.7 * i, agx.Vec3(1, 1, 0)),
            color=colors[i % len(colors)],
        )

    addRepresentationSwitch()
    application().setCameraHome(
        agx.Vec3(0.75, -1.0, 0.85), agx.Vec3(0, 0, 0.05), agx.Vec3.Z_AXIS()
    )


def buildChain():
    """A chain of links hanging from a static link."""
    print("== running scene buildChain ==")
    createHelpText(
        simulation(),
        application(),
        ["A chain held together only by contacts between signed distance fields"],
    )

    # The link is 0.21 m long with 0.02 m thick wire, its long axis along x.
    vertices, indices = readMesh("models/Link_v2.obj", 0.0002)
    grid = createField(vertices, indices, 0.001, "link")

    # 0.02 m thick wire: a small time step, see buildRings.
    simulation().setTimeStep(1.0 / 240.0)

    # Hang the links along z, every other link turned a quarter turn about its long axis. The
    # links interlock up to 0.17 m apart: start them just short of that.
    material = agx.Material("chain")
    simulation().getMaterialManager().getOrCreateContactMaterial(
        material, material
    ).setFrictionCoefficient(0.2)
    alongZ = agx.Quat(math.pi / 2, agx.Vec3.Y_AXIS())
    pitch = 0.165
    top = 1.6
    addStatic(
        agxCollide.SignedDistanceField(grid),
        agx.Vec3(0, 0, top),
        alongZ,
        material,
        agxRender.Color.SteelBlue(),
    )
    for i in range(1, 9):
        rotation = alongZ * agx.Quat(math.pi / 2 * (i % 2), agx.Vec3.Z_AXIS())
        link = addBody(
            agxCollide.SignedDistanceField(grid),
            agx.Vec3(0, 0, top - pitch * i),
            rotation,
            material,
            agxRender.Color.Gold() if i % 2 else agxRender.Color.OrangeRed(),
        )
        if i == 8:
            # Swing the chain.
            link.setVelocity(agx.Vec3(1.0, 0.5, 0))

    addFloor(1.5)
    addRepresentationSwitch()

    application().setCameraHome(
        agx.Vec3(1.8, -2.2, 1.2), agx.Vec3(0, 0, 0.9), agx.Vec3.Z_AXIS()
    )


# Nut and bolt, models/bolt.obj and models/nut.obj: a right hand thread of 53 mm diameter and
# 13.3 mm pitch with 0.7 mm radial clearance. The lead angle is about 5 degrees, so the thread is
# self-locking with friction above about 0.08.
THREAD_FRICTION = 0.03
# The bolt stands on its head with the thread up. In the model frame (axis z, head at +z) the
# tip of the bolt is at z = -69.73 mm. The nut, with zero rotation, is in phase with the thread
# when its center is at the tip plus a whole number of pitches.
BOLT_HEAD_TOP = 0.06953
BOLT_TIP = -0.06973


def readCenteredMesh(fileName, centerZ):
    """Read a mesh and move it so that its axis is the z axis, and its center to z = 0 if centerZ."""
    vertices, indices = readMesh(fileName, 1.0)
    bound = agxCollide.BoundingAABB()
    for vertex in vertices:
        bound.expand(vertex)
    center = (bound.min() + bound.max()) * 0.5
    offset = agx.Vec3(center.x(), center.y(), center.z() if centerZ else 0.0)
    centered = agx.Vec3Vector()
    for vertex in vertices:
        centered.append(vertex - offset)
    return centered, indices


class NutProgress(agxSDK.StepEventListener):
    """Shows how far the nut has turned down the thread."""

    def __init__(self, nut):
        super().__init__(agxSDK.StepEventListener.POST_STEP)
        self.nut = nut
        self.startHeight = nut.getPosition().z()
        self.turns = 0.0

    def post(self, t):
        self.turns += (
            self.nut.getAngularVelocity().z()
            * self.getSimulation().getTimeStep()
            / (2 * math.pi)
        )
        drop = (self.startHeight - self.nut.getPosition().z()) * 1000
        application().getSceneDecorator().setText(
            0,
            f"Nut down {drop:5.1f} mm, turned {self.turns:5.2f}",
            agxRender.Color.White(),
        )


def buildNutAndBolt(useField=True):
    """A static bolt and a nut put on its tip, which turns down the thread under its own weight."""
    kind = "signed distance fields" if useField else "triangle meshes"
    print(f"== running scene buildNutAndBolt, {kind} ==")
    createHelpText(simulation(), application(), [f"A nut turning down a bolt, {kind}"])

    # A small time step: the nut rattles in the clearance of the thread, which loses energy at
    # larger time steps.
    simulation().setTimeStep(1.0 / 1000.0)

    steel = agx.Material("steel")
    steel.getBulkMaterial().setDensity(7850)
    contact = simulation().getMaterialManager().getOrCreateContactMaterial(steel, steel)
    contact.setFrictionCoefficient(THREAD_FRICTION)
    contact.setRestitution(0)
    contact.setYoungsModulus(1e10)

    # Both representations of each part; the key k switches between them.
    geometries = []

    def createShape(vertices, indices, name):
        field = agxCollide.SignedDistanceField(createField(vertices, indices, 0.0005, name))
        mesh = agxCollide.Trimesh(vertices, indices, name)
        shape = field if useField else mesh
        geometries.append((None, field, mesh))
        return shape

    def registerGeometry(geometry):
        _, field, mesh = geometries[-1]
        geometries[-1] = (geometry, field, mesh)

    # Both parts are turned upside down, half a turn about x, which keeps them in phase.
    flip = agx.Quat(math.pi, agx.Vec3.X_AXIS())
    boltVertices, boltIndices = readCenteredMesh("models/bolt.obj", centerZ=False)
    bolt = addStatic(
        createShape(boltVertices, boltIndices, "bolt"),
        agx.Vec3(0, 0, BOLT_HEAD_TOP),
        flip,
        steel,
        agxRender.Color.LightSlateGray(),
    )
    registerGeometry(bolt)

    # Put the nut half way on the tip of the bolt.
    nutVertices, nutIndices = readCenteredMesh("models/nut.obj", centerZ=True)
    nut = addBody(
        createShape(nutVertices, nutIndices, "nut"),
        agx.Vec3(0, 0, BOLT_HEAD_TOP - BOLT_TIP),
        flip,
        steel,
        agxRender.Color.Goldenrod(),
    )
    registerGeometry(nut.getGeometries()[0].get() if hasattr(nut.getGeometries()[0], "get") else nut.getGeometries()[0])
    nut.getMassProperties().setAutoGenerateMask(agx.MassProperties.AUTO_GENERATE_NONE)
    print(
        f"nut mass {nut.getMassProperties().getMass():.3f} kg, thread friction {THREAD_FRICTION}"
    )
    simulation().add(NutProgress(nut))

    # The nut progress is on text row 0.
    timer = StepTimer()
    simulation().add(timer)
    simulation().add(SwitchRepresentation(geometries, timer, useField=useField, firstRow=1))

    application().setCameraHome(
        agx.Vec3(0.25, -0.3, 0.25), agx.Vec3(0, 0, 0.1), agx.Vec3.Z_AXIS()
    )


def buildNutAndBoltSignedDistanceFields():
    buildNutAndBolt(useField=True)


def buildNutAndBoltTriangleMeshes():
    buildNutAndBolt(useField=False)


def buildFieldResolution():
    """The surface of the field of a nut at four voxel sizes, next to its source mesh."""
    print("== running scene buildFieldResolution ==")
    createHelpText(
        simulation(),
        application(),
        [
            "The field of a nut at 0.5, 1, 2 and 4 mm voxels, right of the source mesh",
            "Gray: the field surface is on the mesh, red: outside it, blue: inside it (saturated at 0.5 mm)",
            "Magenta: source triangles the field does not resolve (thin walls, narrow gaps)",
        ],
    )
    nutVertices, nutIndices = readCenteredMesh("models/nut.obj", centerZ=True)
    # The nut lies with its axis along y, facing the camera.
    rotation = agx.Quat(math.pi / 2, agx.Vec3.X_AXIS())

    source = agxCollide.Geometry(agxCollide.Trimesh(nutVertices, nutIndices, "nut"))
    source.setPosition(agx.Vec3(-0.1, 0, 0))
    source.setRotation(rotation)
    simulation().add(source)
    agxOSG.setDiffuseColor(agxOSG.createVisual(source, root()), agxRender.Color.Goldenrod())

    for i, voxelSize in enumerate((0.0005, 0.001, 0.002, 0.004)):
        grid = createField(nutVertices, nutIndices, voxelSize, f"nut {voxelSize * 1000:g} mm")
        geometry = agxCollide.Geometry(agxCollide.SignedDistanceField(grid))
        geometry.setPosition(agx.Vec3(0.1 * i, 0, 0))
        geometry.setRotation(rotation)
        simulation().add(geometry)
        # The surface of the field, extracted from it, instead of the source triangles. Add
        # agxOSG.SDF_SOURCE_WIREFRAME to the mode to also draw the source mesh as lines.
        # The colors saturate at 0.5 mm for all four, to compare them.
        agxOSG.createSignedDistanceFieldVisual(geometry, root(), agxOSG.SDF_DEFAULT_VISUAL, 0.0005)
        unresolved = grid.findUnresolvedTriangles()
        print(
            f"   {len(unresolved)} of {len(grid.getMeshTriangles()) // 3} source triangles unresolved"
        )

    application().setCameraHome(
        agx.Vec3(0.1, -0.42, 0.22), agx.Vec3(0.1, 0, 0), agx.Vec3.Z_AXIS()
    )


def addRemainingScenes(app):
    """Add more scenes so that they can be started using the keyboard."""
    scriptFileName = application().getArguments().getArgumentName(1)
    scriptFileName = scriptFileName.replace("agxscene:", "")

    def addScene(name):
        sceneKey = application().getNumScenes() + 1  # Only works until tutorial 9.
        application().addScene(scriptFileName, name, ord(ascii(sceneKey)), True)

    addScene("buildPrimitivesInBowl")
    addScene("buildChain")
    addScene("buildNutAndBoltSignedDistanceFields")
    addScene("buildNutAndBoltTriangleMeshes")
    addScene("buildFieldsOnPrimitives")
    addScene("buildSwitchRepresentation")
    addScene("buildFieldResolution")


def buildScene():
    """Entry point when running this script using agxViewer."""
    # Add the other scenes to ExampleApplication from agxViewer.
    if application().getNumScenes() == 1:
        addRemainingScenes(application())

    buildRings()


# Entry point when this script is started with python executable
init = init_app(
    name=__name__,
    scenes=[
        (buildScene, "1"),
        (buildPrimitivesInBowl, "2"),
        (buildChain, "3"),
        (buildNutAndBoltSignedDistanceFields, "4"),
        (buildNutAndBoltTriangleMeshes, "5"),
        (buildFieldsOnPrimitives, "6"),
        (buildSwitchRepresentation, "7"),
        (buildFieldResolution, "8"),
    ],
    autoStepping=True,  # Default: False
)
