Skip to content

Engine & Projector

IsometricEngine is the core rendering engine that projects 3D geometry to 2D screen space. It implements the SceneProjector interface, which decouples rendering from the concrete engine to enable testing and custom projections.

class IsometricEngine(
angle: Double = PI / 6, // 30 degrees
scale: Double = 70.0, // pixels per world unit
colorDifference: Double = 0.20,// lighting contrast
lightColor: IsoColor = IsoColor.WHITE
) : SceneProjector
ParameterTypeDefaultDescription
angleDoublePI / 6 (30 degrees)Isometric projection angle in radians. Must be finite.
scaleDouble70.0Pixels per world unit. Must be positive and finite.
colorDifferenceDouble0.20Strength of directional lighting on faces. Higher values produce more contrast between lit and shadowed faces. Must be non-negative.
lightColorIsoColorIsoColor.WHITEThe color of the light source. Tint this to simulate warm or cool lighting.

Both angle and scale are mutable properties. Changing them at runtime rebuilds the internal projection matrix and increments projectionVersion.

Converts a 3D world point to 2D screen coordinates.

fun worldToScreen(
point: Point,
viewportWidth: Int,
viewportHeight: Int
): Point2D

The origin is placed at the horizontal center (width / 2) and near the bottom (height * 0.9) of the viewport.

Unprojects a 2D screen point back to 3D world coordinates on a given Z plane. Because a screen point maps to a line in 3D, this returns the intersection with the horizontal plane at height z.

fun screenToWorld(
screenPoint: Point2D,
viewportWidth: Int,
viewportHeight: Int,
z: Double = 0.0
): Point

Adds geometry to the internal scene graph. Two overloads:

// Add all faces of a Shape
fun add(shape: Shape, color: IsoColor)
// Add a single Path with optional metadata
fun add(
path: Path,
color: IsoColor,
originalShape: Shape? = null,
id: String? = null,
ownerNodeId: String? = null
)

Removes all items from the scene graph.

fun clear()

Projects the 3D scene to 2D, applies culling, lighting, and depth sorting, then returns a PreparedScene containing sorted RenderCommand entries.

fun projectScene(
width: Int,
height: Int,
renderOptions: RenderOptions = RenderOptions.Default,
lightDirection: Vector = DEFAULT_LIGHT_DIRECTION.normalize()
): PreparedScene

Hit tests a prepared scene at given screen coordinates. Returns the matching RenderCommand or null.

fun findItemAt(
preparedScene: PreparedScene,
x: Double,
y: Double,
order: HitOrder = HitOrder.FRONT_TO_BACK,
touchRadius: Double = 0.0
): RenderCommand?

Extension functions that bridge continuous 3D world coordinates and the discrete tile grid system.

Extension function on IsometricEngine. Converts a screen tap point to the TileCoordinate of the tile cell that contains it. Chains screenToWorld with Point.toTileCoordinate internally.

fun IsometricEngine.screenToTile(
screenX: Double,
screenY: Double,
viewportWidth: Int,
viewportHeight: Int,
tileSize: Double = 1.0,
elevation: Double = 0.0,
originOffset: Point = Point.ORIGIN
): TileCoordinate
ParameterTypeDefaultDescription
screenXDoubleScreen x-coordinate of the tap (pixels).
screenYDoubleScreen y-coordinate of the tap (pixels).
viewportWidthIntScene viewport width (pixels).
viewportHeightIntScene viewport height (pixels).
tileSizeDouble1.0World units per tile side. Must match TileGridConfig.tileSize.
elevationDouble0.0Z-plane to intersect during inverse projection. Use the surface z of the tile layer.
originOffsetPointPoint.ORIGINWorld position of the grid’s (0, 0) corner. Must match TileGridConfig.originOffset.

For terrain where elevation varies per tile, use screenToWorld directly and call Point.toTileCoordinate after determining the correct layer.

Extension function on Point. Converts a continuous world point to the TileCoordinate of the cell that contains it. Uses floor() division so negative coordinates map correctly.

fun Point.toTileCoordinate(
tileSize: Double = 1.0,
originOffset: Point = Point.ORIGIN
): TileCoordinate
ParameterTypeDefaultDescription
tileSizeDouble1.0World units per tile side. Must be positive and finite.
originOffsetPointPoint.ORIGINWorld position of the grid’s (0, 0) corner.

The z-coordinate of the receiver Point is ignored — only x and y determine the tile.

Point(3.7, 5.2, 0.0).toTileCoordinate() // TileCoordinate(3, 5)
Point(-0.3, 0.0, 0.0).toTileCoordinate() // TileCoordinate(-1, 0) — floor, not truncation

A Long counter that increments whenever angle or scale changes. Caches that depend on projected output can check this value to know when their data is stale.

val engine = IsometricEngine()
val v1 = engine.projectionVersion // 0
engine.scale = 100.0
val v2 = engine.projectionVersion // 1

SceneProjector is the interface that IsometricEngine implements. Its purpose is dependency inversion: the Compose renderer depends on SceneProjector, not on IsometricEngine directly.

interface SceneProjector {
val projectionVersion: Long
fun add(shape: Shape, color: IsoColor)
fun add(path: Path, color: IsoColor, originalShape: Shape?, id: String?, ownerNodeId: String?)
fun clear()
fun projectScene(width: Int, height: Int, renderOptions: RenderOptions, lightDirection: Vector): PreparedScene
fun findItemAt(preparedScene: PreparedScene, x: Double, y: Double, order: HitOrder, touchRadius: Double): RenderCommand?
}

This abstraction enables:

  • Testing — supply a fake projector that returns canned PreparedScene data without running real 3D math.
  • Custom projections — implement an alternative projection (e.g., oblique, dimetric) while reusing the Compose runtime.

IsometricEngine can be used as a standalone projection library without the Compose runtime, for server-side rendering, screenshot generation, or non-Compose UI frameworks. See Advanced Patterns — Direct Engine Usage for a full worked example.