From 9d152feb2d452fa5b01a6cacf749139158ade816 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sat, 17 Oct 2026 22:08:24 +0700 Subject: [PATCH] spec(t27f): reel host entry points for wasm hosts (Closes #9980) A wasm export carries scalars only and gen-js emits no bodies, so a host could not run reel.t27's camera and screen mapping without re-implementing them. Add scalar entry points that name the demo reel and compose camera with screen mapping: corner_dx/dy, demo_frames, demo_scene_at, demo_still, demo_sx/sy, demo_r, demo_corner_sx/sy. gHashTag/t27-mcp t27f_view draws stills from a wasm build of the Zig output. Tests: hexagon_corners_close_the_cell, demo_entry_points_match_the_reel, overview_still_fits_every_frame (every corner of every cell on every preset). Refs #9965 Co-Authored-By: Claude Opus 5.5 --- specs/t27f/reel.t27 | 125 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/specs/t27f/reel.t27 b/specs/t27f/reel.t27 index 945445d19b..55b86c7752 100644 --- a/specs/t27f/reel.t27 +++ b/specs/t27f/reel.t27 @@ -22,7 +22,9 @@ ; and the MCP Apps quickstart): the terminal CLI does not render MCP Apps, and an image block in a tool ; result reaches the model but is not documented to reach the user. A file on disk with its path in the ; answer reaches the user everywhere; the desktop Code tab opens it in its Browser pane. CHANNEL_* below. -; Not claimed: no host imports this file yet, no renderer exists, no reel has been rendered. +; Host: gHashTag/t27-mcp t27f_view draws stills of the demo reel from a wasm build of this file's +; Zig output (bodies) and its gen-js output (constants); see "host entry points" below. +; Not claimed: no video has been rendered, and no host but t27f_view imports this file. ; phi^2 + 1/phi^2 = 3 | TRINITY module T27fReel; @@ -450,6 +452,77 @@ pub fn channel_for(host: u8) -> u8 { return CHANNEL_FILE_PATH; } +/// --- host entry points --- + +/// A host runs the bodies above as a wasm build of the Zig backend (gHashTag/t27-mcp t27f_view). +/// A wasm export carries scalars only, so these name the demo reel instead of taking its arrays, +/// and they compose the camera with the screen mapping here, so a host only draws what they return. + +/// Corner i (0..5) of a pointy-top hexagon in world units from its centre, clockwise from the top. +pub fn corner_dx(i: i32) -> i32 { + if (i == 1 or i == 2) { + return HALF_W; + } + if (i == 4 or i == 5) { + return 0 - HALF_W; + } + return 0; +} + +pub fn corner_dy(i: i32) -> i32 { + if (i == 0) { + return 0 - HEX_R; + } + if (i == 3) { + return HEX_R; + } + if (i == 1 or i == 5) { + return 0 - @divTrunc(HEX_R, 2); + } + if (i == 2 or i == 4) { + return @divTrunc(HEX_R, 2); + } + return 0; +} + +pub fn demo_frames() -> i32 { + return total_frames(DEMO_COUNT, DEMO_FRAMES); +} + +pub fn demo_scene_at(f: i32) -> i32 { + return scene_at(DEMO_COUNT, DEMO_FRAMES, f); +} + +/// The still of a demo scene, or END_OF_REEL for a scene the demo does not have. +pub fn demo_still(scene: i32) -> i32 { + if (scene < 0 or scene >= DEMO_COUNT) { + return END_OF_REEL; + } + return still_frame(DEMO_FRAMES, scene); +} + +/// A world point of the demo reel on screen at base frame f. +pub fn demo_sx(f: i32, preset: u8, x: i32) -> i32 { + return screen_x(camera_cx(DEMO_COUNT, DEMO_KIND, DEMO_CELL, DEMO_FRAMES, f), camera_hw(DEMO_COUNT, DEMO_KIND, DEMO_CELL, DEMO_FRAMES, f), preset, x); +} + +pub fn demo_sy(f: i32, preset: u8, y: i32) -> i32 { + return screen_y(camera_cy(DEMO_COUNT, DEMO_KIND, DEMO_CELL, DEMO_LENS, DEMO_FRAMES, f), camera_hw(DEMO_COUNT, DEMO_KIND, DEMO_CELL, DEMO_FRAMES, f), preset, y); +} + +pub fn demo_r(f: i32, preset: u8) -> i32 { + return screen_r(camera_hw(DEMO_COUNT, DEMO_KIND, DEMO_CELL, DEMO_FRAMES, f), preset); +} + +/// Corner i of a cell on screen: what a host draws as the cell's polygon. +pub fn demo_corner_sx(f: i32, preset: u8, cell: i32, i: i32) -> i32 { + return demo_sx(f, preset, cell_x(cell) + corner_dx(i)); +} + +pub fn demo_corner_sy(f: i32, preset: u8, cell: i32, i: i32) -> i32 { + return demo_sy(f, preset, cell_y(cell) + corner_dy(i)); +} + /// --- tests --- test map_geometry { @@ -646,6 +719,56 @@ test stills_reach_the_user { assert channel_for(9) == CHANNEL_FILE_PATH; } +test hexagon_corners_close_the_cell { + // six corners, opposite corners mirror each other, and the side corners sit on the half width + // the map steps by, so neighbours share an edge + var i : i32 = 0; + while (i < 3) { + assert corner_dx(i) + corner_dx(i + 3) == 0; + assert corner_dy(i) + corner_dy(i + 3) == 0; + i = i + 1; + } + assert corner_dy(0) == 0 - HEX_R; + assert corner_dx(1) == HALF_W; + assert corner_dx(4) == 0 - HALF_W; + assert corner_dx(6) == 0 and corner_dy(6) == 0; +} + +test demo_entry_points_match_the_reel { + assert demo_frames() == 360; + assert demo_scene_at(224) == 2; + assert demo_still(2) == 224; + assert demo_still(5) == END_OF_REEL; + assert demo_still(-1) == END_OF_REEL; + // the focus still is centred on step 10 at focus scale + assert demo_sx(224, PRESET_WIDE, cell_x(10)) == 640; + assert demo_sy(224, PRESET_WIDE, cell_y(10)) == 360; + assert demo_r(224, PRESET_WIDE) == screen_r(FOCUS_HW, PRESET_WIDE); + assert demo_corner_sy(224, PRESET_WIDE, 10, 0) == 360 - demo_r(224, PRESET_WIDE); + // step 10 sits in the middle row, at the overview's height; the lens still on row 0 is what + // shows the camera moving vertically + assert demo_sy(demo_still(1), PRESET_WIDE, lens_cy(0)) == 360; +} + +test overview_still_fits_every_frame { + // every corner of every cell of the first still lands inside the picture on every preset + var p : u8 = 0; + while (p < PRESET_COUNT) { + var k : i32 = 0; + while (k < CELL_COUNT) { + var i : i32 = 0; + while (i < 6) { + const x : i32 = demo_corner_sx(demo_still(0), p, k, i); + const y : i32 = demo_corner_sy(demo_still(0), p, k, i); + assert x >= 0 and x <= PRESET_W[p] and y >= 0 and y <= PRESET_H[p]; + i = i + 1; + } + k = k + 1; + } + p = p + 1; + } +} + invariant catalog_is_closed { assert SCENE_KIND_COUNT == 6 and valid_kind(SCENE_CALL) and valid_kind(SCENE_KIND_COUNT) == false; }