Instances and Occupancy
A cell holds one value, and one value draws one instance by default. An instance span stretches one model over several cells: one ground plate instead of 640 columns, a 4x4 deck, a 2x8 walkway.
The build derives an occupancy from the painted grid. A hover, a collision test, a spawn query, and a draw all read the cell through it.
/// How many cells one drawn instance covers.
[<Struct>]
type InstanceSpan =
| Span of across: int * deep: int // square grids: a rectangle of cells
| Radius of r: int // hex grids: a disc of hex steps
| One // the identity: this cell only
Span(1, 1) and Radius 0 are also the identity, so a computed span needs no special case.
The four numbers
Do not mix them.
Number |
Meaning |
Lives in |
|---|---|---|
|
How many cells one instance covers in the grid plane |
The cell value, and optionally the statement |
|
How tall the column stands, in cells |
The cell value, set by the word |
|
How many cells an element paints |
The element box |
|
The mesh as authored |
The model catalog |
There is no spanY. The grid is two-dimensional: no cell sits above another, so a taller column owns nothing. Height stretches the mesh.
A word states a span
The word carries the value, so a span on a word needs no document change:
let slab = {
Model = model "platform"
Height = 0.2f
Lift = 0f
Span = One // the document sizes it, or the word states one
Solid = false
}
The surface tells the resolver how to read a span, and how to write one a statement states:
let surface: Doc.Surface<BlockCell> = {
Words = words
Kernels = kernels
Elements = elements
Span = ValueSome(fun cell -> cell.Span)
WithSpan = ValueSome(fun cell span -> { cell with Span = span })
}
Span and WithSpan are required by the Surface record. ValueNone on both means every cell covers one cell.
A statement states a span
set sizes one instance with spanX and spanZ. They come as a pair. No other statement takes them.
|
|
A word states the default span; a placement overrides it. One slab can serve a 16x6 plaza, a 4x4 deck, and a 2x8 walkway. A stated box does not size a word whose default is a Radius: a hex span takes a radius, not a box.
The occupancy
Occupancy.scan expands every populated cell through the projection, checks the result, and answers who owns what:
type Occupancy = {
Width: int
Height: int
Cells: CellPoint[] // anchor index -> the anchor's cell
Rects: CellRect[] // anchor index -> the rectangle it covers
Owner: int[] // cell -> anchor index + 1, or 0
Claimed: int // populated cells a span hides
}
A span claims the cells it covers. A plain cell under a plate stops drawing on its own. Claimed counts it.
Four queries serve every consumer:
Occupancy.owner x y occupancy // the anchor that owns a cell, ValueNone when empty
Occupancy.rectOf at occupancy // the rectangle that anchor covers
Occupancy.iterInWindow l t r b f occ // every anchor whose rectangle meets a window
Occupancy.identity grid // an occupancy where each populated cell owns itself
Layers stack, so a plate in one layer and a decoration above it are legal. Two spans that meet in the same layer fail the build and name both cells.
Rules
Spanneeds both sides at least one;Radiusneedsr >= 0. Otherwise:the span at (x,y) spans nothing.SpanandRadiusmust match the grid geometry.Oneis legal on both. Otherwise:Span spans need a square grid; (x,y) is hex, and the twin.- Only
setplaces a spanning word. An area statement paints every cell of its box, so one instance has no single place to stand:the word 'slab' spans 6x4, so only set may place it. - A span stays inside the grid:
the span at (x,y) covers past the grid edge. - Two spans do not overlap:
the span at (x,y) overlaps the span at (ax,ay). - An element's extent follows the span it places. A style
w=/h=or a declaredelement ... w= h=that disagrees fails and names the element and both sizes.
Rectangles
The rectangle comes from the span, is checked against the grid, and is then expanded. The hex range walk clips to the grid, so the rectangle is never read back from the walk.
- A square anchor grows toward +X and +Z from its cell:
Span(across, deep)at(x, y)covers(x, y, across, deep). - A hex anchor is centred on its cell. Its rectangle is the offset-space bounding box
(x - r, y - r, 2r + 1, 2r + 1), the same convention hex landmarks use.
Draw
Rendering reads the occupancy and hands the transform the rectangle each instance covers. 3D from 2D builds the context and the transform. The draw calls are:
context.RenderInstanced(buffer, grid, occupancy)
context.RenderWindowInstanced(buffer, left, top, right, bottom, grid, occupancy)
The whole-map form draws one instance per anchor. The windowed form converts the world-space window to a cell range and visits the anchors whose rectangle it meets, so an instance stays drawn while any cell of its rectangle is in view.
3D from 2D walks a whole map from an empty project to a drawn, queryable stack.
type StructAttribute = inherit Attribute new: unit -> StructAttribute
--------------------
new: unit -> StructAttribute
How many cells one drawn instance covers.
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
Mibo