HammeritePortal

Core: the map. Inherits Resource

A derived portal joining two rooms in the subtractive world model.

A portal exists between two air brushes when they share a face OR intersect; the portal is the boundary of the shared region. There is no separate stable portal id: a portal is identified by the unordered pair of air brush ids {A, B}, plus opening_index when the two touch in more than one place. Everything that references a portal (audio edge, per-portal override, lightmap adjacency) keys on that (key()), which survives a nudge - only the portal polygon changes shape. Side A is always the lower air brush id, and everything that has a side follows it: plane faces out of side A, and host_face is side A's face. Derived and rebuilt on every edit, never saved with a map. A Resource rather than a RefCounted like HammeriteBoundaryWall only because HammeriteCell is one too; the compile cache keeps neither as objects.

Properties

TypeNameDefault
intair_brush_a0
intair_brush_b0
intregion_a0
intregion_b0
intopening_index0
PackedVector3ArraypolygonPackedVector3Array()
PlaneplanePlane(0, 0, 0, 0)
HammeriteFacehost_facenull
HammeriteFacehost_face_bnull

Methods

ReturnsMethod
Vector3imake_key(a: int, b: int, discriminator: int = 0) static
Vector3ikey()
Vector2iregion_key_a()
Vector2iregion_key_b()
Vector2iother_region(region_key: Vector2i)
Vector3normal_from(region_key: Vector2i)
boolis_aperture()
StringNameproperty_key(field: StringName)
Stringget_portal_name()
boolset_portal_name(portal_name: String)
voidclear_face_property(prop_name: StringName)
Variantget_face_property(prop_name: StringName, default = null)
HammeriteFaceproperty_host(prop_name: StringName)

Constants

PROPERTY_NAMESPACE

const PROPERTY_NAMESPACE = &"portal"

Namespace for authored values stored on a portal's host face, including its name.

NAME_FIELD

const NAME_FIELD = &"name"

Field under that namespace holding the portal's name.

Property descriptions

air_brush_a

var air_brush_a: int = 0

The lower of the two air brush ids (HammeriteBrush.id) this portal joins, so the pair is canonical regardless of author order.

air_brush_b

var air_brush_b: int = 0

The higher of the two air brush ids this portal joins; see air_brush_a.

region_a

var region_a: int = 0

Which room of air_brush_a's brush this portal opens into, as HammeriteCell.region. A brush split by a structural wall has several rooms in it, and an opening leads into one of them - so a consumer that wants "the room on this side" wants region_key_a().

region_b

var region_b: int = 0

Which room of air_brush_b's brush this portal opens into; see region_a.

opening_index

var opening_index: int = 0

Which of the openings joining this pair of brushes this is. Two rooms sharing a wall with two windows in it get two portals, 0 and 1, separately addressable. Assigned by the compiler in a canonical geometric order, so it stays with the same opening across an edit.

polygon

var polygon: PackedVector3Array = PackedVector3Array()

Convex polygon of the opening (world space, ordered loop). Empty on an overlap portal - see is_aperture().

plane

var plane: Plane = Plane(0, 0, 0, 0)

Plane the portal polygon lies on, its normal pointing out of side A's room and into side B's: a point in front of it is on side B. See normal_from(). A zero Plane() on an overlap portal.

host_face

var host_face: HammeriteFace = null

The authored HammeriteFace of side A's brush this aperture was punched from. Null on an overlap portal, which has no aperture and therefore no face (is_aperture()), and null when side A's wall came from a clip rather than an authored face.

host_face_b

var host_face_b: HammeriteFace = null

The authored face of side B's brush this aperture was punched from, as host_face. An aperture is punched from a PAIR of faces, one per room, and either may be the one that holds an authored value, so a read checks both (get_face_property()).

Method descriptions

make_key()

static func make_key(a: int, b: int, discriminator: int = 0) -> Vector3i

Canonical dictionary key for a portal joining air brushes a and b as opening discriminator of the pair: Vector3i(min(a, b), max(a, b), discriminator). Keep portal-key construction in this single place so every consumer (audio edges, per-portal overrides, lightmap adjacency, HammeriteMap's own portal index) agrees on the key shape.

key()

func key() -> Vector3i

This portal's canonical key, as make_key(). Stable across a recompile as long as the two brushes and the order of their openings are.

region_key_a()

func region_key_a() -> Vector2i

The region key (HammeriteCell.key()) of the room on side A.

region_key_b()

func region_key_b() -> Vector2i

The region key (HammeriteCell.key()) of the room on side B.

other_region()

func other_region(region_key: Vector2i) -> Vector2i

The room on the far side of this portal from region_key, or Vector2i.ZERO - which names no room, as no brush has id 0 - when the given room is not one of its two sides.

normal_from()

func normal_from(region_key: Vector2i) -> Vector3

The way through this opening from the room region_key: plane's normal from side A, reversed from side B. Zero when that room is not one of its sides, or on an overlap portal.

is_aperture()

func is_aperture() -> bool

True when this portal is a real APERTURE - a hole in a shared wall, with a polygon and a plane. The other kind is connectivity-only: two air brushes that interpenetrate have no aperture between them at all, because there is no wall there to make a hole in. They are one continuous open space, which is why the boundary-wall holing skips them and why they cannot be named.

property_key()

func property_key(field: StringName) -> StringName

Key for one of this portal's authored values on its host face: portal/<air brush a>/<air brush b>/<opening index>/<field> - the portal's own key(), then the field. The whole key, because one authored wall can host several openings. The opening index only counts the openings joining one PAIR of rooms: a hall and a corridor joined by two doorways resolve both portals to the same face, and so does a room wall with a doorway into one neighbour and a window into another, which are both opening 0 of their pair. Keyed by anything less, naming one would name the other. Consumers put their own values under their own field names; the key shape lives here so they agree on it. The cost is that the key names the two rooms by brush id, so an opening of a room that becomes a new brush - copied, or clipped in two - starts out unnamed. And the opening index falls out of where the openings are (see HammeriteSubtractiveCompiler.compile_portals()), so cutting a third opening between the same pair that sorts ahead of the others shifts the indices and the authored values with them. Moving geometry - by far the commoner edit - does neither.

get_portal_name()

func get_portal_name() -> String

The author's name for this opening, or "" if it was never named. A portal's name is the one identity every consumer shares. A window is a route for navigation and a hole for sound, and it is the same window - naming it twice, once per addon, would let the two disagree about which opening a door entity means. So the name lives HERE, on the portal (which is to say on the authored face beneath it), and the addons read it.

set_portal_name()

func set_portal_name(portal_name: String) -> bool

Name this opening, writing through to the face the aperture is punched from. Returns false when there is no face to write to - an overlap portal is not an aperture and cannot be named (is_aperture()). An empty name clears the property rather than storing a blank, keeping the face's storage sparse.

clear_face_property()

func clear_face_property(prop_name: StringName) -> void

Erase prop_name from BOTH host faces. Both, because an aperture is cut from a pair of them and either may be carrying the value: a name written to one side earlier must not survive as a ghost when the author clears it from the other.

get_face_property()

func get_face_property(prop_name: StringName, default = null) -> Variant

The authored value of prop_name taken from whichever host face carries one, or default. host_face wins a disagreement, but only because something has to.

property_host()

func property_host(prop_name: StringName) -> HammeriteFace

The face an authored value for prop_name should be WRITTEN to: whichever host face already carries one, else host_face, else host_face_b - a side whose wall was cut by structure rather than authored has none. Null when neither side has one, as on an overlap portal.

Hammerite 1.0 · built from a6dd634, 2026-10-10