← hex_edge · version 0.2.6
44 public items, in source order. Generated from the registry index, which re-derives them from the package source at publish time — so a signature here is the one that version actually ships.
pub struct Surfaces { sf_kind: vector<integer>, sf_a: vector<float>, sf_b: vector<float>, sf_c: vector<float>, sf_n: integer }
The surfaces a collision can reference. Small and flat on purpose: a normal must be cheap to evaluate at contact time.
pub fn surfaces_new() -> Surfaces
pub fn surf_straight(s: Surfaces, nx: float, ny: float, c: float) -> integer
A straight wall with unit normal (nx,ny) at offset c. Normal is constant -> exact for every one of the 24 headings, and for anything between them.
pub fn surf_arc(s: Surfaces, cx: float, cy: float, r: float) -> integer
An arc / round tower / tree trunk: the normal is radial, so it is exact everywhere along the curve — no faceting, no mesh.
pub fn surf_distance(s: Surfaces, id: integer, px: float, py: float) -> float
Distance from a world point to surface id — the metric junction arbitration uses.
pub fn surf_normal(s: Surfaces, id: integer, px: float, py: float) -> (float, float)
The outward normal of surface id at world point (px,py).
pub struct Materials { mt_solid: vector<boolean>, mt_height: vector<float>, mt_opacity: vector<float>, mt_sound: vector<float>, mt_perm: vector<float>, mt_bounce: vector<float>, mt_n: integer }
MATERIALS — the PHYSICS half of an edge. Kept separate from Surfaces (the geometry half) because they vary independently: a stone wall and a hedge on the SAME arc share a surface and differ entirely in material; a wall and a tower of the same stone share a material and differ entirely in surface.
The consumers do not all want "blocked". A low wall blocks movement but not sight; a hedge muffles sound without stopping it; water runs along a wall it cannot cross. So a material carries a small vector of transmission terms rather than one flag.
pub fn materials_new() -> Materials
An empty palette. Id 0 is reserved for "nothing there", so the first material_add returns 1 and an unmarked edge never looks like a real material.
pub fn material_add(m: Materials, solid: boolean, height: float, opacity: float, sound_db: float, perm: float, bounce: float) -> integer
Add a material and get its id. One call fixes everything the simulation asks later — whether it blocks, how tall, how opaque, how loud, how permeable, how bouncy.
pub fn material_set_solid(m: Materials, id: integer, solid: boolean)
Change whether a material counts as solid. Flipping the table entry retargets every edge already carrying this id, with no edge write and no surface change — that is why material is a separate axis from surface.
⚠ It changes what mat_solid answers, and NOTHING in this package's movement: passable and sweep_path read the edge's surface mark and no material. To open a gate, clear the mark (edge_set_surf(e, …, 0)); the material says what kind of gate it was, for the consumer that reads it.
pub fn mat_solid(m: Materials, id: integer) -> boolean
The per-consumer accessors. Each reads ONE property of the same edge.
pub fn mat_height(m: Materials, id: integer) -> float
How tall this material stands. 0.0 for id 0 or an unknown id, so an unmarked edge answers "nothing there" rather than faulting.
pub fn mat_opacity(m: Materials, id: integer) -> float
How much it blocks sight, 0.0 when there is nothing there.
pub fn mat_sound(m: Materials, id: integer) -> float
How much it deadens sound, in dB, 0.0 when there is nothing there.
pub fn mat_perm(m: Materials, id: integer) -> float
How freely air or fluid passes. Note the neutral value is 1.0, not 0.0: nothing there means fully permeable, where nothing there means zero height.
pub fn mat_bounce(m: Materials, id: integer) -> float
How much it rebounds, 0.0 when there is nothing there.
pub fn edge_k(qa: integer, ra: integer, qb: integer, rb: integer) -> integer
The canonical edge key: doubled midpoint of the two cell centres. Kept as two integer accessors rather than the library's tuple-shaped edge_key, because edge_point wants the halves separately and a tuple destructure per edge is a hot-path cost for no gain.
pub fn edge_m(qa: integer, ra: integer, qb: integer, rb: integer) -> integer
The edge's m key — the companion of edge_k. The pair names the edge between two cells the same way from either side, which is what stops one wall becoming two.
pub fn edge_block_surf(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer, surf: integer)
Block an edge, recording WHICH surface blocks it (surf 0 = a bare blocker with no recoverable normal — allowed, but it cannot be bounced off correctly).
FIRST-WRITER-WINS, and that is load-bearing rather than incidental: plain construction stamps parts in an arbitrary order, and without the guard the LAST part to touch a shared edge would own it. edge_block_arb is the version that resolves the contest on geometry instead of order; this is the cheap one for edges only one part can claim.
pub fn edge_point(qa: integer, ra: integer, qb: integer, rb: integer) -> (float, float)
The world point where the two cells' shared edge sits (its midpoint) — the contact point a normal is evaluated at.
pub fn edge_block_arb(e: EdgeSet, sf: Surfaces, qa: integer, ra: integer, qb: integer, rb: integer, surf: integer, mat: integer)
── JUNCTION ARBITRATION ─────────────────────────────────────────────────────
When two parts meet, one edge is claimed by two surfaces. First-writer-wins is order-dependent there — stamp the wall then the tower and you get a different world than the other way round, which is the class of bug that never shows up in a unit test.
THE RULE: the surface whose geometry is NEAREST the edge's contact point wins; ties break to the LOWER surface id. Both halves matter — nearest is what a ball should actually bounce off, and the tie-break is what makes it deterministic.
pub struct Junctions { jn_a: vector<integer>, jn_b: vector<integer>, jn_kind: vector<integer>, jn_x: vector<float>, jn_y: vector<float>, jn_n: integer }
Where surfaces meet: for each junction, the two surfaces, the continuity intended there (JOIN_SMOOTH, JOIN_CORNER) and its position. junctions_new makes an empty set.
pub fn junctions_new() -> Junctions
An empty set of junctions: where walls meet, for consumers that draw a corner post or need to know a wall's ends rather than just its middle.
pub fn junction_add(j: Junctions, sa: integer, sb: integer, kind: integer, x: float, y: float)
Record that two surfaces meet here, of this kind, at this world point.
pub fn junction_ok(j: Junctions, i: integer, sf: Surfaces) -> boolean
Does the junction honour what was intended? G0 is exact by construction (both surfaces pass through the seam point); this checks the TANGENT.
pub fn junction_g0(j: Junctions, i: integer, sf: Surfaces, tol: float) -> boolean
G0: both surfaces must actually pass through the seam point.
pub struct Features { fe_surf: vector<integer>, fe_s0: vector<float>, fe_s1: vector<float>, fe_z0: vector<float>, fe_z1: vector<float>, fe_kind: vector<integer>, fe_mat: vector<integer>, fe_n: integer }
The openings cut into surfaces — doors, windows, loopholes (FEAT_*): for each, its surface, its interval along it, its vertical extent (sill to head), its kind and the material inside it. features_new makes an empty set.
pub fn features_new() -> Features
An empty set of features: doors, windows and openings placed ALONG a surface rather than replacing it, so the wall stays one wall with holes in it.
pub fn feature_add(f: Features, surf: integer, s0: float, s1: float, z0: float, z1: float, kind: integer, mat: integer) -> integer
Place a feature on a surface, between two positions along it and two heights up it, so a door or a window is a hole IN a wall rather than a break in it.
pub fn surf_param(s: Surfaces, id: integer, px: float, py: float) -> float
Where along surface id does this point sit? Straight: distance along its direction. Arc: arc length from angle 0. This is the parameter a feature interval is stated in.
pub fn apply_features(e: EdgeSet, sf: Surfaces, f: Features, q0: integer, r0: integer, w: integer, h: integer) -> integer
Apply every feature to the edge layer: an edge whose contact point falls inside a feature's interval takes that feature's material. The SURFACE is untouched — which is why the body never fragments and the matcher still sees one wall.
pub fn edge_block_full(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer, surf: integer, mat: integer)
Record BOTH halves: which surface (geometry) and which material (physics). The surface keeps first-writer-wins; the material does NOT, because a feature (a door, a window) legitimately retargets the material of an edge whose surface is already settled.
pub fn edge_blocked(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer) -> boolean
Can a mover cross this edge? THE collision question, and the reason the edge layer exists — asked from either side with the same answer.
pub fn collide(e: EdgeSet, s: Surfaces, qa: integer, ra: integer, qb: integer, rb: integer) -> (boolean, float, float)
THE COLLISION RESPONSE QUERY: blocked?, and the EXACT outward normal to bounce off.
pub fn passable(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer) -> boolean
THE collision query. Everything else is construction.
pub fn sweep_path(e: EdgeSet, x0: float, y0: float, x1: float, y1: float) -> (float, integer, integer, integer)
── SWEPT MOVEMENT ───────────────────────────────────────────────────────────
passable answers about one edge; this answers about a PATH. Walk the segment (x0,y0) -> (x1,y1) across the lattice one edge at a time and stop at the first edge the field blocks.
There is no step size anywhere, which is the whole point. A point's cell is its NEAREST CENTRE, so a crossing is exactly where the segment meets the bisector between the current centre and a neighbour's — one linear solve per neighbour:
|P(t) - C|² = |P(t) - N|² ⟺ P(t)·(N-C) = midpoint·(N-C) ⟹ t = ((M - P0)·D) / (V·D) with D = N-C, and only when V·D > 0
So the answer is a function of the SEGMENT, not of how many frames it was cut into: dropping frames or falling fast cannot step over a wall. Centres come from the exact integer lattice (lattice_k/lattice_m), so the geometry carries no accumulated error.
Returns the fraction of the segment that may be travelled (1.0 = all of it), the cell it stopped in, and the DIRECTION of the blocking edge — dir < 0 meaning nothing blocked. The signal is the direction and never a coordinate: hex coordinates are signed, so a -1 sentinel in q or r would collide with real cells in any chunk that spans the origin. Feed (q, r, dir) to nb_q/nb_r and collide for the exact bounce normal.
pub fn sweep_path_from(e: EdgeSet, sq: integer, sr: integer, x0: float, y0: float, x1: float, y1: float) -> (float, integer, integer, integer)
THE SAME SWEEP, TOLD WHICH CELL IT STARTS IN — and the one to call in a movement loop.
sweep_path derives the starting cell with hex_at, and that is exactly what a caller cannot afford on the step AFTER a stop. Stopping at a wall leaves the position exactly ON the bisector between two cells (the comment in the loop below says so: it is the normal state, not an edge case), and hex_at on a bisector does not toss a coin — measured over all six directions, it answers the FAR cell every time. So the next call starts past the wall with the wall behind it, sees nothing, and reports the whole segment clear. The symptom is "collision does not work"; the truth is that it worked for exactly one step.
The cure is not a tolerance. The previous call already RETURNED the cell you are resting in, so hand it back: (t, cq, cr, d) = sweep_path_from(e, cq, cr, …), threading cq, cr from one step to the next. Pressing on into the wall then answers t = 0 in the cell you are in, which is the honest answer, and sliding along the wall works from the same call.
WHY NOT A SKIN. The consumer-side workaround is to come to rest a small distance short, and it does work — but the smallest distance that works is not a constant. Measured: 1e-15 at the origin, 1e-11 about 1.7e3 world units out, 1e-9 about 1.7e6 out. It is a float-resolution floor, not a geometric clearance, so a constant chosen at one extent is silently wrong at another and the library has no better idea of the right value than the caller does. Threading the cell needs no such number.
THE FAR-FIELD LIMIT, which no cure removes: past roughly 5e6 cells from the origin a double position can no longer hold "on the bisector" to better than this code's 1e-9 tolerance (measured: the recoverable offset from an absolute position loses 2.9e-10 at 3e6 cells and 6.4e-10 at 1e7). At that point the position itself is the limit, not the sweep, and the fix is to recentre the world rather than the arithmetic.
pub fn edges_cut(s: HexSet, e: EdgeSet)
A THIN wall: the cut between the cells of s and everything else. One edge thick, and it separates by construction — in every one of the 24 headings.
pub fn edges_solid(s: HexSet, e: EdgeSet)
A SOLID body: every edge incident to an occupied cell, so the cells cannot be entered at all. This is the cell model expressed in the edge primitive — which is the point: one collision layer serves both.
pub fn edges_halfplane_surf(e: EdgeSet, q0: integer, r0: integer, w: integer, h: integer, nx: float, ny: float, c: float, surf: integer)
Block every edge a straight line crosses: the wall runs along n·p = c in world units, with n the unit normal. A thin wall in ANY heading, one edge thick.
pub fn edges_halfplane(e: EdgeSet, q0: integer, r0: integer, w: integer, h: integer, nx: float, ny: float, c: float)
Back-compat: a halfplane wall with no surface recorded.
pub fn sight_clear(e: EdgeSet, m: Materials, x0: float, y0: float, z0: float, x1: float, y1: float, z1: float, opaque_at: float) -> boolean
── SIGHT ───────────────────────────────────────────────────────────────────
Is there a clear line of sight from an eye to a target?
Sight is blocked by OPACITY, not by solidity. A chain-link fence is solid and transparent; a hedge is permeable and opaque. This is the first consumer to read one material axis while deliberately ignoring another, and it is the whole reason a material carries a vector of transmission terms rather than a single flag.
Height matters as much as opacity: the sight line rises from eye to target, so an obstruction blocks only while the line is still below its top. A near wall hides what the identical wall further along does not — which is exactly the railway sighting problem, and it falls out of the geometry rather than being special-cased.
The walk steps at 0.2 world units. A hex inradius is sqrt(3)/2 = 0.866, so consecutive samples cannot skip a cell: every cell change is a step to a NEIGHBOUR, and the edge between them is exactly the one to test.
pub fn surf_point(s: Surfaces, id: integer, t: float) -> (float, float)
The inverse of surf_param: where along a surface does this parameter land?
surf_param answers "where is this point on that surface"; derived placement needs the other direction — a Feature states an INTERVAL in surface parameters, and putting a door there means turning that interval back into a position. Without this the only way to place a door on an existing opening is to search for it.