*Rename criteria:* all slices done OR consciously dropped, consumers
running, FO4 exam green.
-*Current verified state (2026-10-21):* 132/132 tests green under
+*Current verified state (2026-10-24):* 142/142 tests green under
=xvfb-run -a=; flat temple oracle 688/307200 px (0.224%, meanAbs 0.19);
-GI oracle 37541 px (12.220%, meanAbs 11.62), both engines converged,
+GI oracle 36193 px (11.782%, meanAbs 11.62), both engines converged,
drift in named classes only. Numbers quoted inside the progress log
below are the state at the time each entry was written — this line is
the current one.
| =kernel/= | the fixed pipeline (=Kernel=, =Pass=, =Parallel=) and every substrate store: MeshStore, ScreenStore, ImageStore, CameraStore, LightStore, TextureStore, BvhStore, ChunkStore, InputStore, GuiStore, SdfFontStore, GiStore, LightmapStore, LineStore |
| =systems/= | one pass per file: Transform, Cull, Raster, Fog, Line, ChunkFrustumCull, WorldStreamer, Camera, SpaceMouse, StereoMerge |
| =systems/gi/= | GiPass (lifecycle + radiometry), GiScheduler, Raycast, EntryWork |
-| =systems/gui/= | GuiPass, GuiRenderPass, SdfFont, DiagnosticsHudPass |
+| =systems/gui/= | GuiPass, GuiRenderPass, LabelPass, SdfFont, SdfTextMetrics, DiagnosticsHudPass |
| =shapes/= | the geometry gallery: cube, sphere, cylinder, cone, arrow, pyramid, grid |
| =app/= | AwtShell, DeveloperToolsWindow, BugReport, PersistentLog, SpaceNavigatorHid, SpaceMouseManager |
| =oracle/= | compare.sh, gi-compare.sh, OracleTemple, GiOracle |
:CUSTOM_ID: pass-order
:END:
-Measured from code (2026-10-21):
+Measured from the current code:
| order | pass | job |
|-------+--------------------+------------------------------------------------------------|
| 40 | LinePass | world-space lines |
| 40 | StereoMergePass | anaglyph / side-by-side merge |
| 90 | FogPass | exponential distance fog, post-process |
+| 249 | LabelPass | viewer-facing SDF labels anchored to 3D points |
| 249 | DiagnosticsHudPass | fps / tris / chunks / camera readout |
| 250 | GuiRenderPass | panels, labels, SDF text |
double-blend into blobs), and advance the pen even for out-of-range
characters.
+=LabelPass= (order 249) maintains viewer-facing labels anchored to 3D
+world points. Applications explicitly register an instance and use
+=add(x, y, z, text, argb)= to supply their labels. Each frame it projects
+the anchors with the camera transform, centres the text, scales it with
+distance without a clamp, and hides anchors closer than camera-space
+=z=5. The pass owns only its configured =GuiStore.Label= records; it is
+not auto-discovered. =SdfTextMetrics= shares string-width measurement
+between the GUI systems without one pass referencing another.
+
* Platform
:PROPERTIES:
:CUSTOM_ID: platform
temple oracle unchanged 688 px / 0.224% / meanAbs 0.19; GI oracle meanAbs
11.62 (pixel share 11.78-11.82%, run-to-run noise). The HouseDemo now
becomes visible without any input.
+
+** 3D-anchored labels join the engine (2026-10-24)
+:PROPERTIES:
+:CUSTOM_ID: engine-label-pass
+:END:
+
+=LabelPass= moved out of demos-2 into =systems.gui=: world-anchored
+labels are an engine capability, while demos retain only their concrete
+label lists. Moving it exposed a contract violation hidden by the old
+location: the pass called =GuiRenderPass.textWidth=. Width measurement
+now lives in the non-pass =SdfTextMetrics= helper, shared by both GUI
+systems, so the no-pass-references-pass rule holds again.
+
+The projection, scaling, hiding and text-width arithmetic is unchanged.
+New =LabelPassTest= coverage pins projection and label reuse, near-plane
+hiding/restoration, absent font/image, and unclamped distance scaling;
+=SdfGuiTest= also pins width measurement. Verified 142/142 tests under
+=xvfb-run -a=, clean engine install and clean demos-2 package, and jar
+contents with the old demos class absent. Flat temple oracle: 688 px
+(0.224%, meanAbs 0.19). GI oracle: both engines converged, 36193 px
+(11.782%, meanAbs 11.62).
b.x, b.y, b.w, b.h, b.hover ? b.hoverArgb : b.argb);
if (data.guiFont != null) {
final double s = (b.h - 10.0) / SdfFontStore.CELL;
- final int textW = textWidth(data.guiFont, b.text, s);
+ final int textW = SdfTextMetrics.textWidth(
+ data.guiFont, b.text, s);
drawText(data, b.x + (b.w - textW) / 2,
b.y + b.h / 2 + (int) (data.guiFont.baseline * s / 2),
s, b.textArgb, b.text);
}
}
- /** Pixel width of a string at scale s (atlas advances are in cell px). */
- public static int textWidth(final SdfFontStore font, final String text,
- final double s) {
- int w = 0;
- for (int i = 0; i < text.length(); i++) {
- final char c = text.charAt(i);
- if (c < SdfFontStore.FIRST || c > SdfFontStore.LAST) {
- continue;
- }
- w += (int) (font.advance[c - SdfFontStore.FIRST] * s);
- }
- return w;
- }
-
private static int clamp(final int v, final int lo, final int hi) {
return v < lo ? lo : Math.min(v, hi);
}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2.systems.gui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.svjatoslav.aukio.e3d2.kernel.GuiStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Pass;
+import eu.svjatoslav.aukio.e3d2.kernel.SdfFontStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+
+/**
+ * Forward-oriented text labels anchored to 3D points — the e3d2 answer
+ * to legacy ForwardOrientedTextBlock: instead of text GEOMETRY (a
+ * plaque quad facing the start pose), each label is screen-space SDF
+ * text anchored to a projected 3D point. Always faces the viewer,
+ * never mirrored, scales with distance like world geometry (UNCLAMPED
+ * — user decision 2026-10-13), hides when the anchor is behind the
+ * camera.
+ *
+ * Applications configure instances with world anchors, text and colours;
+ * each instance maintains only its own GuiStore.Label records and is
+ * registered explicitly by the application.
+ */
+public final class LabelPass implements Pass {
+
+ /** World size the label emulates at the anchor's depth. */
+ private static final double WORLD_SIZE = 30;
+ /** Hide closer than this camera-space z (near-plane margin). */
+ private static final double HIDE_Z = 5;
+
+ private static final class Entry {
+ double x, y, z;
+ String text;
+ int argb;
+ GuiStore.Label label;
+ }
+
+ private final List<Entry> entries = new ArrayList<>();
+
+ /** Adds a label anchored at world (x,y,z). Returns this (fluent). */
+ public LabelPass add(final double x, final double y, final double z,
+ final String text, final int argb) {
+ final Entry e = new Entry();
+ e.x = x;
+ e.y = y;
+ e.z = z;
+ e.text = text;
+ e.argb = argb;
+ entries.add(e);
+ return this;
+ }
+
+ @Override
+ public int order() {
+ return 249; // with diag-hud, just before gui-render (250)
+ }
+
+ @Override
+ public String name() {
+ return "labels";
+ }
+
+ @Override
+ public void apply(final Substrate data) {
+ if (data.guiFont == null || data.image == null) {
+ return;
+ }
+ for (final Entry e : entries) {
+ if (e.label == null) {
+ e.label = new GuiStore.Label(0, 0, 0.2, e.argb, e.text);
+ data.gui.labels.add(e.label);
+ }
+ place(data, e);
+ }
+ }
+
+ /** Projects the anchor with exactly TransformPass's math, then
+ * moves the label's screen position/scale. */
+ private void place(final Substrate data, final Entry e) {
+ final double cosY = Math.cos(data.camera.yaw);
+ final double sinY = Math.sin(data.camera.yaw);
+ final double cosP = Math.cos(data.camera.pitch);
+ final double sinP = Math.sin(data.camera.pitch);
+ final double dx = e.x - data.camera.x;
+ final double dy = e.y - data.camera.y;
+ final double dz = e.z - data.camera.z;
+ // inverse yaw about +Y, then pitch about +X (TransformPass)
+ final double x1 = dx * cosY - dz * sinY;
+ final double z1 = dx * sinY + dz * cosY;
+ final double cy1 = dy * cosP - z1 * sinP;
+ final double cz1 = dy * sinP + z1 * cosP;
+ if (cz1 < HIDE_Z) {
+ e.label.text = ""; // behind the near plane: hide
+ return;
+ }
+ e.label.text = e.text;
+ final double sx = data.image.width / 2.0 + (x1 / cz1) * data.camera.scale;
+ final double sy = data.image.height / 2.0 - (cy1 / cz1) * data.camera.scale;
+ // perspective scale, UNCLAMPED (user decision 2026-10-13):
+ // labels shrink to specks far away and blow up point-blank,
+ // exactly like world geometry
+ final double s = WORLD_SIZE * data.camera.scale
+ / (cz1 * SdfFontStore.CELL);
+ e.label.scale = s;
+ e.label.x = (int) (sx - SdfTextMetrics.textWidth(
+ data.guiFont, e.text, s) / 2.0);
+ e.label.y = (int) (sy + SdfFontStore.CELL * s * 0.25); // baseline ~centered
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2.systems.gui;
+
+import eu.svjatoslav.aukio.e3d2.kernel.SdfFontStore;
+
+/** Text measurements shared by GUI systems without coupling passes. */
+final class SdfTextMetrics {
+
+ private SdfTextMetrics() {
+ }
+
+ /** Pixel width of a string at scale s (atlas advances are in cell px). */
+ static int textWidth(final SdfFontStore font, final String text,
+ final double s) {
+ int w = 0;
+ for (int i = 0; i < text.length(); i++) {
+ final char c = text.charAt(i);
+ if (c < SdfFontStore.FIRST || c > SdfFontStore.LAST) {
+ continue;
+ }
+ w += (int) (font.advance[c - SdfFontStore.FIRST] * s);
+ }
+ return w;
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2.systems.gui;
+
+import eu.svjatoslav.aukio.e3d2.kernel.GuiStore;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.SdfFontStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** World-anchored SDF labels. */
+class LabelPassTest {
+
+ private static Substrate data() {
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(200, 100);
+ data.guiFont = SdfFont.build(48);
+ data.camera.set(0, 0, 0, 0, 0, 100);
+ return data;
+ }
+
+ @Test
+ void projectsWorldAnchorAndReusesGuiLabel() {
+ final Substrate data = data();
+ final LabelPass pass = new LabelPass()
+ .add(0, 0, 100, "X", 0xFF112233);
+
+ pass.apply(data);
+
+ assertEquals(1, data.gui.labels.size());
+ final GuiStore.Label label = data.gui.labels.get(0);
+ final double scale = 30.0 * 100 / (100 * SdfFontStore.CELL);
+ final int width = (int) (data.guiFont.advance['X' - SdfFontStore.FIRST]
+ * scale);
+ assertEquals("X", label.text);
+ assertEquals(0xFF112233, label.argb);
+ assertEquals(scale, label.scale, 1e-9);
+ assertEquals((int) (100 - width / 2.0), label.x);
+ assertEquals((int) (50 + SdfFontStore.CELL * scale * 0.25), label.y);
+
+ pass.apply(data);
+ assertEquals(1, data.gui.labels.size(), "second frame adds no duplicate");
+ assertSame(label, data.gui.labels.get(0), "existing label is updated");
+ }
+
+ @Test
+ void hidesBehindNearPlaneAndRestoresWhenVisible() {
+ final Substrate data = data();
+ final LabelPass pass = new LabelPass().add(0, 0, 4, "X", 0xFFFFFFFF);
+
+ pass.apply(data);
+ assertEquals(1, data.gui.labels.size());
+ assertEquals("", data.gui.labels.get(0).text);
+
+ data.camera.set(0, 0, -96, 0, 0, 100);
+ pass.apply(data);
+ assertEquals("X", data.gui.labels.get(0).text);
+ }
+
+ @Test
+ void missingFontOrImageLeavesGuiUntouched() {
+ final Substrate data = new Substrate();
+ final LabelPass pass = new LabelPass().add(0, 0, 100, "X", 0xFFFFFFFF);
+
+ pass.apply(data);
+ assertTrue(data.gui.labels.isEmpty(), "no font or image");
+
+ data.guiFont = SdfFont.build(48);
+ pass.apply(data);
+ assertTrue(data.gui.labels.isEmpty(), "no image");
+
+ data.guiFont = null;
+ data.image = new ImageStore(200, 100);
+ pass.apply(data);
+ assertTrue(data.gui.labels.isEmpty(), "no font");
+ }
+
+ @Test
+ void distantLabelsShrinkWithoutClamp() {
+ final Substrate data = data();
+ final LabelPass pass = new LabelPass()
+ .add(0, 0, 100, "near", 0xFFFFFFFF)
+ .add(0, 0, 1000, "far", 0xFFFFFFFF);
+
+ pass.apply(data);
+
+ final double nearScale = data.gui.labels.get(0).scale;
+ final double farScale = data.gui.labels.get(1).scale;
+ assertEquals(10.0, nearScale / farScale, 1e-9);
+ assertTrue(farScale < 0.1, "far label is not clamped to a minimum size");
+ }
+}
assertEquals(1, data.gui.clicks.size(), "no click on release outside");
}
+ @Test
+ void textWidthUsesPerGlyphAtlasAdvances() {
+ final SdfFontStore font = new SdfFontStore();
+ font.advance['A' - SdfFontStore.FIRST] = 20;
+ font.advance['B' - SdfFontStore.FIRST] = 10;
+
+ assertEquals(25, SdfTextMetrics.textWidth(font, "ABA", 0.5));
+ }
+
@Test
void sdfTextRendersWithAntialiasedEdges() {
final Substrate data = new Substrate();