hex_draw API

← hex_draw · version 0.1.4

24 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 WallSurface { ws_dk: integer, ws_dm: integer, ws_num_k: integer, ws_num_m: integer, ws_den: integer, ws_n: integer }

The averaged surface of one wall run. Direction is exact integer; position is an exact rational held as (num_k, num_m) / den.

pub fn surface_of(p: Plan, cells: HexSet, side: integer) -> WallSurface

THE DERIVATION. Sum the edge vectors; sum the doubled midpoints. Both are integer sums, so the surface is exact before any float is involved.

pub fn surface_heading(w: WallSurface) -> integer

Which of the 12 HEADINGS is the mean direction an exact positive multiple of? -1 if none.

This is the exactness claim: the averaged direction equals the nominal side direction EXACTLY, as an integer multiple of a heading step. Any other value would mean the average had drifted off the lattice, which is what a fit would do and an average must not.

PARALLEL, not an integer multiple, and not "one component is zero". Both of those tests reject geometry that is exact:

"one component is zero" only 4 of the 12 headings have one — a side running NE is (1,3), both components non-zero and perfectly exact. Failed 16 of 24. "an integer multiple" the summed vector is (0,14) where the heading step is (0,6); the SUM over a run is not a whole number of steps. Failed 12 of 24.

What §6.1 actually claims is that the DIRECTION is exact — no residual perpendicular component — which is a zero CROSS PRODUCT against the heading step, in exact integers. The magnitude is the run's length and carries no claim.

pub fn surface_is_exact(w: WallSurface) -> boolean

Does this surface run along one of the exact headings? An inexact one is still drawable but cannot be reasoned about on the lattice, so ask before treating it as integer.

pub fn surface_mean_x(w: WallSurface) -> float

World position of the mean, and the unit perpendicular to the mean direction.

pub fn surface_mean_y(w: WallSurface) -> float

The surface's mean point in world y — the companion of surface_mean_x, and where a label or a normal is anchored.

pub fn surface_band_corners(p: Plan, cells: HexSet, side: integer) -> float

The perpendicular extent of the run's CORNERS about the mean line — the band of §6.2.

pub fn surface_span(p: Plan, cells: HexSet, side: integer) -> (float, float, float, float)

── G2: THE WALL AS *ONE* QUAD ─────────────────────────────────────────────

G2 asks that every wall render as **one flat quad — its analytic surface — not the strip**. Today the shot draws the wall *as it is stored*: one strip per boundary edge, so a side that is nine edges long is nine quads that zigzag by the band X47 measured. The surface is already exact (direction exactly a heading, position an exact rational); what was missing is its EXTENT, so a renderer can emit the segment instead of the strip.

The span is the run's corners **projected onto the mean line**, taking the extremes. The two points returned lie ON the line by construction, so the quad the renderer builds from them has perpendicular spread exactly **0** — which is G2's check — while the strip it replaces spreads by surface_band_corners.

pub fn surface_fitted_spread(p: Plan, cells: HexSet, side: integer) -> float

The perpendicular spread of the FITTED quad's own corners about its line — G2's check. Both endpoints come off the line, so this is the number eave_spread(fitted) = 0 names. Measured rather than asserted, so that if the span ever stops lying on the line the gate says so.

⚠ *EXACTLY* 0 IS A CLAIM ABOUT FLOATS, AND IT IS ORIENTATION-DEPENDENT. The derivation above is exact — integer sums and rationals — but the span is projected through world coordinates, and on the orientations where that arithmetic does not cancel the round trip costs an ulp. Measured over 12 orientations x 4 sides: 32 read exactly 0 and 16 read 2.22e-16. So == 0.0 passes only on the orientations it was tried on; what holds everywhere is the comparison that means something — flat against the STRIP's band, which is fifteen orders of magnitude larger.

pub fn surface_miter(p: Plan, cells: HexSet, sa: integer, sb: integer) -> (float, float, boolean)

THE MITER POINT (I-CORNER part 4): where two adjacent fitted surfaces intersect. This is the corner the RENDER should use — each quad running miter-to-miter, so consecutive sides share an endpoint exactly and the outline closes with no gap. That closure is the invariant, and it is exact.

⚠ IT IS NOT THE Plan'S CORNER, and it never can be. An earlier note here claimed the intersection *"must land on the exact model corner, or the surfaces have drifted off the shape they were fitted to"* — a check nobody ran. Measured, it misses by 0.47 world units on the 5x4 cottage, and swept over plan sizes the miss is neither constant (0.465 .. 0.827) nor even consistently outward. The reason is structural rather than numerical: what these surfaces recover is the face of the wall AS DRAWN, and the drawn footprint is the plan quantised to cells, so the recovered corner is an exact rational of the lattice while the plan's half-depth is an irrational multiple of sqrt(3)/2. Two number systems that meet only at the origin.

So a caller must never diff a recovered outline against plan geometry, and must not try to correct for the offset either. Use the recovered corner for anything that has to line up with what was drawn.

ok is false when the two lines are parallel, which for adjacent sides of a closed form cannot happen — so a false here is a real defect, not a degenerate input to be shrugged at.

pub fn surface_quad(p: Plan, cells: HexSet, side: integer, nsides: integer) -> (float, float, float, float)

THE MITERED QUAD (G2 + I-CORNER part 4): the side's segment running MITER TO MITER, so consecutive sides share an endpoint exactly and the outline closes with no corner gap. This is what a renderer should draw; surface_span (the projected extent) leaves the gaps the fitted render made visible.

pub fn surface_edge_span(p: Plan, cells: HexSet, side: integer, i: integer) -> (float, float, float, float)

One stored edge's INTERVAL along the fitted line — where edge i of the run projects onto the surface. This is what makes a feature renderable as FEATURES describes it: *"a door is [s0,s1] along an exact straight or arc"*, an interval on the analytic surface rather than a stored edge. The renderer draws the wall as one quad (surface_span) and each feature as a sub-interval of it, so the zigzag never reaches the picture.

pub fn surface_edge_mat(p: Plan, cells: HexSet, edges: EdgeSet, side: integer, i: integer) -> integer

The material on stored edge i of a side — so the renderer can find the features on a run without re-deriving the run's order.

pub fn surface_band_mids(p: Plan, cells: HexSet, side: integer) -> float

The perpendicular extent of the MIDPOINTS about the mean line. Not in §6.2, and it is not the same as the corner band: on the east family the midpoints are exactly COLINEAR (0), while on the north family they span the full band — the row stagger, which the corners hide.

pub fn surface_lsq_residual(p: Plan, cells: HexSet, side: integer) -> float

The scatter a LEAST-SQUARES formulation would have to threshold: the sum of squared perpendicular deviations of the midpoints from the derived mean line. Averaging names the same geometry as an exact band constant and has nothing to tune; this is the control that the distinction is real rather than rhetorical.

pub fn feature_t_num(sr: SideRun, i: integer, n: integer) -> integer

The exact t of stored edge i, as the numerator over 2n: an odd integer in 1 .. 2n-1. sr_t is a float only because SideRun holds it that way; the value is an exact rational and this recovers the numerator without a tolerance being involved in the answer.

pub fn feature_t_admissible(num: integer, n: integer) -> boolean

Is num/2n a legal feature position — an edge centre, i.e. an ODD numerator in range?

pub fn feature_find(p: Plan, cells: HexSet, edges: EdgeSet, side: integer, kind: integer) -> vector<integer>

Recover a feature from the field: the t numerators of every edge on side carrying kind, in ascending t order (C2's ordering, not the storage order).

pub fn wall_edge_count(cells: HexSet, edges: EdgeSet) -> integer

How many boundary edges carry a wall material at all — the run's length, which a re-materialled feature must leave UNCHANGED. Deleting an edge instead is what fragments a run.

pub fn draw_floor(p: Plan, cells: HexSet, types: Labels, floor_type: integer) -> integer

── 1. THE FLOOR ────────────────────────────────────────────────────────────

The massing is filled FIRST and walled second. Comparisons are inclusive (<= bound + eps) so the fill never loses a column of cells to the structural tie: along u every cell-to-side distance is an exact multiple of sqrt(3)/2, which is exactly the default halfwidth, so a strict comparison sits on the last bit of a float for a whole column.

pub fn draw_walls(cells: HexSet, edges: EdgeSet, wall_mat: integer) -> integer

── 2. THE WALLS ────────────────────────────────────────────────────────────

Every edge between a cell of the region and one outside it. Closed by construction, and it costs no floor at all: a 7.5 x 6.0 m cottage keeps all 27 of its cells, where a wall drawn as a band of cells would eat 16 of them — 59% of the house.

pub fn place_opening(p: Plan, cells: HexSet, edges: EdgeSet, side: integer, t: float, nedges: integer, kind: integer) -> integer

── 3. OPENINGS ─────────────────────────────────────────────────────────────

A door is N EDGES centred at t, not a t-interval erased with a segment: erasing over-erases by ~halfwidth at EACH end (a 3.00 m interval opens 4.50 m of wall) and shrinking the segment over-corrects. One hex edge is 1.0 world unit = 0.87 m of clear width — a real domestic door, which a cell-thick wall could not express at all.

The opening material REPLACES the wall material on those edges (the slot holds one). With any kind but OPEN_NONE nothing is deleted, so the wall still fits as one run. Returns how many edges it wrote: fewer than nedges when the side is shorter.

pub fn grow_ring(src: HexSet, dst: HexSet)

── 4. THE ROOF ─────────────────────────────────────────────────────────────

One ring of halo around the massing, because that ring is what makes the EAVE interpolable: without it 38 of the 76 wall-edge samples face a cell with no stored height, and the wall top has nothing to meet.

pub fn draw_roof(p: Plan, cells: HexSet, hz: Heights, eave: float, pitch: float, hip_steps: integer)

A gable: the ridge runs along local u THROUGH the anchor and PAST both gable ends. Stopping it at the wall makes the distance go radial past the tip, so the gable end rolls over like a cone quadrant — 0.43 units, 0.37 m of bulge. A hip is the same call with the ridge shortened; then the conical ends are the feature, not a defect.