Border the placement tile highlight with perimeter quads #47

Closed
opened 2026-08-30 08:22:24 +01:00 by liamjd · 0 comments
Owner

Draw a perimeter border around the tile highlight during building placement.

#37 delivered the fill quads and the validity tint, but left the border as a TODO
(entities/buildings/placement_controller.gd, _rebuild_highlight()). This issue
finishes that half of the highlight.

Current state

_rebuild_highlight() emits one inset quad per footprint cell into a single
ImmediateMesh PRIMITIVE_TRIANGLES surface, coloured per-vertex from the
placement Reason. The corners are inset by a hardcoded 0.3 on each axis, which
leaves a visible gap between neighbouring cells — this reads well and should stay,
but it means each cell is drawn as an island rather than as part of one shape.

What to add

Thin quads along the outer perimeter of the footprint, drawn into the same
surface at higher alpha (~0.9 against the fill's ~0.35, per #37).

The perimeter rule from #37 still applies and is what makes L-shapes and holes work:

A cell's edge in direction d is on the perimeter iff cell + d is not in the
footprint set.

Do not derive the outline from footprint.bounds() — the bounding box of an L-shape
is a rectangle and would draw a border through empty space. Build a set from
footprint.cells() and test membership.

Constraints carried over from #37

  • Quads, not PRIMITIVE_LINES. Line width is not adjustable under
    gl_compatibility — always one pixel — so a line border vanishes when zoomed out
    and never thickens when zoomed in. A quad ~0.06 m wide has real world-space
    thickness.
  • One material, colour on the vertices. surface_set_color() before each
    surface_add_vertex(); the border differs from the fill only in alpha, so it needs
    no second material.
  • Per-cell Y. Each border quad sits at its own cell's
    surface_y(level_at(cell)) + 0.02, like the fill. On a terraced site the outline
    steps with the ground.

Open question: the border and the inset interact

The inset means a cell's drawn edge is 0.3 m inside its true cell boundary, so
"border along the perimeter" now has two plausible readings:

  1. Border per cell — outline every drawn quad, so each cell reads as its own
    framed tile and the gaps stay visible. Simpler; ignores the perimeter rule
    entirely.
  2. Border around the footprint — one outline around the whole shape, at the true
    cell boundary or just inside it, with the inset fill floating inside it. Matches
    #37's intent and reads as "this is the building's plot", but the gaps between
    cells then sit inside an unbroken frame.

Decide by eye with a headed screenshot on the 3x3 and the L-shape footprints before
committing to one. Worth trying (1) first — it composes with the gap effect rather
than fighting it.

While in this code: the 0.3 inset is a magic number coupled to
TerrainGrid.tile_size (2.0 today), and inverts the quads if tile_size is ever set
below 0.6. Replace it with an exported ratio hoisted out of the loop:

@export var highlight_inset_ratio: float = 0.15
...
var inset := map.tile_size * highlight_inset_ratio

That scales with the grid, cannot invert for ratios below 0.5, and gives a live
inspector knob for tuning the border/gap balance this issue is about.

Acceptance criteria

  • The highlight has a visible border with world-space thickness that stays legible
    across the full zoom range.
  • The border takes the same valid/invalid tint as the fill, at higher alpha.
  • An L-shaped footprint is outlined along its actual shape — no border across the
    notch, and no border through cells the footprint does not occupy.
  • The border steps with the terrain on a multi-level site.
  • Still one ImmediateMesh, one material, one surface, rebuilt only when the anchor,
    type or validity changes.
  • Verified by eye from a headed screenshot; tools/screenshot_buildings.gd is the
    template.
Draw a perimeter border around the tile highlight during building placement. #37 delivered the fill quads and the validity tint, but left the border as a TODO (`entities/buildings/placement_controller.gd`, `_rebuild_highlight()`). This issue finishes that half of the highlight. ## Current state `_rebuild_highlight()` emits one inset quad per footprint cell into a single `ImmediateMesh` `PRIMITIVE_TRIANGLES` surface, coloured per-vertex from the placement `Reason`. The corners are inset by a hardcoded `0.3` on each axis, which leaves a visible gap between neighbouring cells — this reads well and should stay, but it means each cell is drawn as an island rather than as part of one shape. ## What to add Thin quads along the **outer perimeter** of the footprint, drawn into the same surface at higher alpha (~0.9 against the fill's ~0.35, per #37). The perimeter rule from #37 still applies and is what makes L-shapes and holes work: > A cell's edge in direction `d` is on the perimeter iff `cell + d` is **not** in the > footprint set. Do not derive the outline from `footprint.bounds()` — the bounding box of an L-shape is a rectangle and would draw a border through empty space. Build a set from `footprint.cells()` and test membership. ## Constraints carried over from #37 - **Quads, not `PRIMITIVE_LINES`.** Line width is not adjustable under `gl_compatibility` — always one pixel — so a line border vanishes when zoomed out and never thickens when zoomed in. A quad ~0.06 m wide has real world-space thickness. - **One material, colour on the vertices.** `surface_set_color()` before each `surface_add_vertex()`; the border differs from the fill only in alpha, so it needs no second material. - **Per-cell Y.** Each border quad sits at its own cell's `surface_y(level_at(cell)) + 0.02`, like the fill. On a terraced site the outline steps with the ground. ## Open question: the border and the inset interact The inset means a cell's drawn edge is 0.3 m inside its true cell boundary, so "border along the perimeter" now has two plausible readings: 1. **Border per cell** — outline every drawn quad, so each cell reads as its own framed tile and the gaps stay visible. Simpler; ignores the perimeter rule entirely. 2. **Border around the footprint** — one outline around the whole shape, at the true cell boundary or just inside it, with the inset fill floating inside it. Matches #37's intent and reads as "this is the building's plot", but the gaps between cells then sit *inside* an unbroken frame. Decide by eye with a headed screenshot on the 3x3 and the L-shape footprints before committing to one. Worth trying (1) first — it composes with the gap effect rather than fighting it. ## Related refactor While in this code: the `0.3` inset is a magic number coupled to `TerrainGrid.tile_size` (2.0 today), and inverts the quads if `tile_size` is ever set below 0.6. Replace it with an exported ratio hoisted out of the loop: ```gdscript @export var highlight_inset_ratio: float = 0.15 ... var inset := map.tile_size * highlight_inset_ratio ``` That scales with the grid, cannot invert for ratios below 0.5, and gives a live inspector knob for tuning the border/gap balance this issue is about. ## Acceptance criteria - The highlight has a visible border with world-space thickness that stays legible across the full zoom range. - The border takes the same valid/invalid tint as the fill, at higher alpha. - An L-shaped footprint is outlined along its actual shape — no border across the notch, and no border through cells the footprint does not occupy. - The border steps with the terrain on a multi-level site. - Still one `ImmediateMesh`, one material, one surface, rebuilt only when the anchor, type or validity changes. - Verified by eye from a headed screenshot; `tools/screenshot_buildings.gd` is the template.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder#47
No description provided.