One bit in the rounding gap
layoutc round-trips paintball field layouts between formats. Fields are symmetric, so internally a coordinate folds into a quadrant plus absolute values. The JSON format, though, stores plain signed positions—the sign is the quadrant. A bunker sitting exactly on the fold axis has x == 0, no sign, and its quadrant would vanish on the next load.
The fix, in data():
q, self = self.q, self.unfold(Unit.METER, Unit.DEGREE)
# Offset negative zero by 1/2**20 meters towards its negative dimension. The sign
# communicates the quadrant while Codec.update()'s 1.5 mm grid snap folds the resulting
# 0.000954 mm magnitude back to zero after the quadrant has been recovered.
if q.NWSW and not self.x:
self = self._replace(x=q.X(1/2**20))
if q.SWSE and not self.y:
self = self._replace(y=q.Y(1/2**20))1/2**20 is about 0.954 microns. Folding scales meters to millimeters before rounding to six decimal places, so the nudge survives as 0.000954 mm. The codec then snaps the coordinate to its much coarser grid: pitch/depth = 381/254 = 1.5 mm. That returns the position to exactly zero, with the quadrant already recovered from its sign.
The trick depends on the codec’s grid snap, not on ndigits=6. The offset already survives fold as a phantom micron at the default six decimal places: in millimeters, seven decimal places change 0.000954 to 0.0009537, but both remain nonzero. An entity that never passes through codec.update keeps the nudge: a second dump emits -1e-06 instead of clean zero.
The companion trick is how the quadrant gets computed from signs in the first place:
q = Quadrant(((self.x < 0) ^ (3 * (self.y < 0))) + 1)Booleans as integers: x < 0 contributes 0 or 1, 3 * (y < 0) contributes 0 or 3—both bits at once. XOR with 3 flips both bits, so the four sign pairs land on 0 through 3 in rotational order rather than zigzag: (+,+) is 1 NE, (-,+) is 2 NW, (-,-) is 3 SW, (+,-) is 4 SE. It reads like a bug until you tabulate it, which is also true of the rounding nudge—which is why both carry comments at the site where they happen.
The implementations are in src/layoutc/entity/json.py and src/layoutc/entity/__init__.py.