From 99aa65aa7d83e5ded7b3544988ab444959c194c9 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 09:20:49 +0700 Subject: [PATCH 1/3] feat(t27f): T27 OS home -- the 27-petal triangle menu as a spec specs/t27f/petal_menu.t27 (module T27fPetalMenu) is the single home of the 999 triangle menu that trios draws through QueenUILib (TriangleLogo.swift, ported from photon_trinity_canvas.zig). Layers L1-L7: geometry (27 quads, milli-units), meaning (3 realms x 9), look (glass-menu fills), fit (0.63 of the 596x526 box), input (division-free ray-cast hit test), state (home / petal open / back) and the menu (an empty 3x3 card grid in the MVP). Integer-only so it can lower to the FPGA later. Documents the 28-vs-27 defect in Screen.screenForBlock (settings unreachable). frontend.t27 registers SF_PETAL_MENU and the I_OS_HOME intent; nothing draws it yet. test-report on the lab: petal_menu 10/10, frontend 8/8, 0 vacuous. Co-Authored-By: Claude Opus 5.5 --- specs/t27f/frontend.t27 | 27 ++- specs/t27f/petal_menu.t27 | 376 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 397 insertions(+), 6 deletions(-) create mode 100644 specs/t27f/petal_menu.t27 diff --git a/specs/t27f/frontend.t27 b/specs/t27f/frontend.t27 index ed4a180e49..40fb2aa79c 100644 --- a/specs/t27f/frontend.t27 +++ b/specs/t27f/frontend.t27 @@ -9,7 +9,10 @@ ; What this spec does not restate: ; - The rules of each surface. They stay in the spec that decides it (spec_graph*.t27, reel.t27, ; deck.t27, token_floor*.t27, lesson_reel.t27, hot.t27, claw/t27f_gate.t27, claw/preview.t27, -; particle_field.t27, ui_plan.t27, t27mcp/dashboard.t27). This spec names the surface only. +; particle_field.t27, ui_plan.t27, t27mcp/dashboard.t27, petal_menu.t27). This spec names the +; surface only. +; - The T27 OS home (SF_PETAL_MENU, 2026-10-19): the 27-petal triangle, its hit test and its +; open/close rule live in petal_menu.t27; its hosts are the trios app today and an FPGA later. ; - Tool names. A surface drawn by an MCP tool points at its row of specs/t27mcp/tool_schema.t27. ; - Which tool belongs to TRI CLAW. specs/t27mcp/service.t27 decides that by prefix (t27_ and t27f_ ; are the t27 module's, mounted into TRI CLAW's /mcp). This spec checks that every T27F tool is @@ -51,10 +54,11 @@ pub const SF_MAP_VIDEO : u32 = 9; // honeycomb reel as video and live playe pub const SF_PARTICLE_LIVE : u32 = 10; // T27F Live, particle_field.t27 + particle_engine.t27 pub const SF_UI_PLAN : u32 = 11; // trusted-widget UI plan, ui_plan.t27 pub const SF_DASHBOARD : u32 = 12; // corpus and session health, t27mcp/dashboard.t27, tool t27_dashboard -pub const SF_COUNT : u32 = 13; +pub const SF_PETAL_MENU : u32 = 13; // T27 OS home: the 27-petal triangle menu, petal_menu.t27 +pub const SF_COUNT : u32 = 14; pub const SF_NONE : u32 = 255; -pub const SURFACE_NAMES : str = "graph,outline,map_still,graph_page,hot,check,preview,particle_reel,still,map_video,particle_live,ui_plan,dashboard"; +pub const SURFACE_NAMES : str = "graph,outline,map_still,graph_page,hot,check,preview,particle_reel,still,map_video,particle_live,ui_plan,dashboard,petal_menu"; /// What draws a surface today. pub const ST_LIVE : u8 = 0; @@ -63,7 +67,7 @@ pub const ST_PARTIAL : u8 = 2; pub const ST_BLOCKED : u8 = 3; pub const ST_SPEC : u8 = 4; -pub const SURFACE_STATUS : [13]u8 = [0, 0, 0, 0, 2, 3, 4, 1, 1, 4, 4, 4, 0]; +pub const SURFACE_STATUS : [14]u8 = [0, 0, 0, 0, 2, 3, 4, 1, 1, 4, 4, 4, 0, 4]; /// Where a surface is made: an MCP tool, a t27.ai page, the Railway lab, the TRI CLAW App, nowhere. pub const MK_MCP : u8 = 0; @@ -72,7 +76,7 @@ pub const MK_LAB : u8 = 2; pub const MK_APP : u8 = 3; pub const MK_NONE : u8 = 4; -pub const SURFACE_MAKER : [13]u8 = [0, 0, 0, 1, 0, 3, 3, 2, 2, 4, 4, 4, 0]; +pub const SURFACE_MAKER : [14]u8 = [0, 0, 0, 1, 0, 3, 3, 2, 2, 4, 4, 4, 0, 4]; /// What a caller wants shown. pub const I_SPEC_SHAPE : u32 = 0; // the structure of one spec @@ -85,7 +89,8 @@ pub const I_EDITING : u32 = 6; // a spec redrawn while it is edited pub const I_HEALTH : u32 = 7; // the state of the corpus and the session pub const I_LIVE_AGENT : u32 = 8; // an agent's work as a live particle field pub const I_APP : u32 = 9; // an application screen built from trusted widgets -pub const I_COUNT : u32 = 10; +pub const I_OS_HOME : u32 = 10; // the T27 OS home screen: 27 petals, a petal opens its menu +pub const I_COUNT : u32 = 11; /// The drawing tools TRI CLAW's chat agent carries. service.t27 gives its coder persona CODER_TOOLS /// for writing a spec; these are for showing one. @@ -171,6 +176,9 @@ pub fn route(intent: u32, host: u8) -> u32 { if (intent == I_APP) { return SF_UI_PLAN; } + if (intent == I_OS_HOME) { + return SF_PETAL_MENU; + } return SF_NONE; } @@ -244,6 +252,13 @@ test a_report_is_a_particle_still_and_a_lesson_a_particle_reel { assert SURFACE_MAKER[SF_PARTICLE_REEL] == MK_LAB; } +test the_os_home_is_the_petal_menu_and_nothing_draws_it_yet { + assert route(I_OS_HOME, HOST_CLAUDE_CODE_DESKTOP) == SF_PETAL_MENU; + assert route(I_OS_HOME, HOST_SITE) == SF_PETAL_MENU; + assert pick(I_OS_HOME, HOST_CLAUDE_CODE_DESKTOP) == SF_NONE; + assert claim_ok(SF_PETAL_MENU, true) == false; +} + test what_cannot_be_made_has_a_stand_in_or_none { assert pick(I_TOPIC_MAP, HOST_CLAUDE_CODE_DESKTOP) == SF_MAP_STILL; assert pick(I_PR, HOST_CLAUDE_CODE_DESKTOP) == SF_GRAPH; diff --git a/specs/t27f/petal_menu.t27 b/specs/t27f/petal_menu.t27 new file mode 100644 index 0000000000..875440a017 --- /dev/null +++ b/specs/t27f/petal_menu.t27 @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/petal_menu.t27 -- the 27-petal triangle menu as one spec: the home screen of T27 OS. +; Owner, 2026-10-19: the 27 petals are the visual core of the menu; pressing a petal opens that +; petal's menu; the menu is, for now, an empty frame of cards drawn from the existing widgets; the +; MVP is called T27 OS and its goal is to run on an FPGA. This spec is the T27F surface +; SF_PETAL_MENU of specs/t27f/frontend.t27. +; -- +; Where it comes from. The triangle is drawn today in Swift, in gHashTag/trinity +; apps/queen/QueenUI/Widgets/TriangleLogo.swift, which copied its coordinates from +; photon_trinity_canvas.zig. Three tables there each carry a piece of the same rule: +; TriangleLogo.rawBlocks the 27 quads +; TriangleLogo.worlds the 27 world names shown in the tooltip +; Screen.screenForBlock the screen a petal opens +; and they disagree: screenForBlock lists 28 screens for 27 petals (its DUKH row has 10), so its +; last entry (settings) is never reached, and its screens are not the worlds the tooltip names. +; This spec is the one home of the geometry, the names, the hit test, the fills and the open/close +; rule. Hosts (Swift today, the FPGA framebuffer later) draw from it; none keeps its own copy. +; -- +; Layers, outermost first: +; L1 geometry 27 quads, 4 vertices each, in a 596 x 526 box, as integer milli-units. +; L2 meaning realm = petal / 9: RAZUM (outer ring, 0-8), MATERIYA (middle, 9-17), +; DUKH (inner, 18-26); each ring runs clockwise from the bottom tip. +; L3 look idle petals share one translucent black fill; hover is white; the open petal +; takes its realm colour; every outline is white, 1 px. No realm labels. +; L4 fit the triangle is fitted to the viewport at 63 percent of the limiting side. +; L5 input a ray-cast point-in-quad test turns a tap into a petal, in integers only. +; L6 state HOME, or one petal's menu open. A tap on a petal opens it; back returns home. +; L7 menu a frame of card slots; a card names a widget of the WIDGETS gallery +; (gHashTag/trinity apps/website/specs/widgets/gallery.t27). MVP: every menu empty. +; -- +; Integer-only on purpose: no float, no division in the hit test, so the same spec can lower to +; Verilog for the board (gen-verilog) as well as to the desktop hosts. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27fPetalMenu; + +pub const PETAL_COUNT : u32 = 27; +pub const PETALS_PER_REALM : u32 = 9; +pub const REALM_COUNT : u32 = 3; +pub const PETAL_NONE : u32 = 255; + +pub const REALM_RAZUM : u32 = 0; +pub const REALM_MATERIYA : u32 = 1; +pub const REALM_DUKH : u32 = 2; + +/// L1. The box the quads live in, in milli-units (1 unit = 1000). +pub const BOX_W : i64 = 596000; +pub const BOX_H : i64 = 526000; +pub const BOX_CX : i64 = 298000; +pub const BOX_CY : i64 = 263000; + +/// L1. Vertex v of petal p is (QX[p * 4 + v], QY[p * 4 + v]). Copied once from +/// TriangleLogo.rawBlocks (photon_trinity_canvas.zig:2387-2442), scaled by 1000. +pub const QX : [108]i64 = [ + 296767, 236563, 211501, 296767, + 235710, 177201, 126893, 210755, + 116304, 175824, 126022, 42177, + 43019, 117106, 235544, 211460, + 213100, 237875, 356580, 381646, + 477724, 358701, 383404, 550969, + 477056, 418023, 468886, 553143, + 358646, 384221, 468192, 416976, + 298138, 357793, 383376, 298138, + 297148, 260326, 237943, 297148, + 259613, 224371, 179600, 237048, + 223536, 187285, 120085, 178781, + 121863, 187937, 260042, 237348, + 261857, 333559, 356010, 239269, + 335294, 407736, 474496, 357761, + 408358, 372034, 416476, 475012, + 336052, 358165, 415649, 371244, + 298893, 335156, 357382, 298893, + 296258, 282337, 260496, 296258, + 259547, 281633, 269336, 225274, + 254956, 268406, 224465, 189037, + 255476, 282068, 260192, 189751, + 261646, 283582, 309702, 331733, + 338542, 311435, 333423, 404244, + 338850, 325556, 369518, 404907, + 334380, 312392, 324681, 368779, + 298025, 311561, 333297, 298025, +]; + +pub const QY : [108]i64 = [ + 435228, 329491, 373560, 523496, + 328065, 224570, 224570, 372182, + 118557, 223238, 223260, 74909, + 73555, 116680, 116680, 73525, + 73520, 116409, 116741, 73509, + 116854, 116802, 73803, 73877, + 118915, 223109, 223131, 74338, + 327197, 372152, 224521, 224579, + 434656, 328533, 373808, 523876, + 352965, 288171, 327796, 432004, + 286780, 224818, 224818, 326301, + 223354, 159675, 120508, 223779, + 119193, 158358, 158355, 118746, + 158313, 158290, 118829, 118829, + 158300, 158226, 118923, 118923, + 159547, 223421, 223315, 120916, + 286778, 325872, 224808, 224759, + 352826, 288190, 327328, 430179, + 272716, 248309, 286972, 349653, + 285675, 246705, 225016, 224996, + 199798, 223578, 223598, 161206, + 198549, 198538, 160039, 160070, + 160062, 198505, 198505, 160062, + 198607, 198595, 160068, 160099, + 199978, 223591, 223610, 161243, + 285625, 246733, 224989, 224969, + 272637, 248279, 287010, 349402, +]; + +/// L2. The 27 worlds, in petal order (TriangleLogo.worlds, from sacred_worlds.zig:99-141). +pub const WORLD_NAMES : str = "CHAT,CODE,LOGS,DEBUG,REVIEW,TRANSLATE,VIBEE,VOICE,COMPOSE,FILES,EDITOR,BUILD,TEST,TERMINAL,GIT,DEPLOY,DePIN NODE,SETTINGS,DOCS,REELS,FEED,ROADMAP,BENCHMARKS,RESEARCH,FORMULAS,COMMUNITY,ABOUT"; +pub const REALM_NAMES : str = "RAZUM,MATERIYA,DUKH"; + +/// L3. Colours as 0xAARRGGBB. Realm colours from sacred_worlds.zig: gold, cyan, purple. +pub const REALM_RGB : [3]u32 = [16766720, 5307130, 12424185]; +pub const RGB_WHITE : u32 = 16777215; +pub const ALPHA_OPAQUE : u32 = 4278190080; +/// Idle fill: black at 22 percent (TriangleMenuPresentation.normalPetalOpacity = 0.22 -> 56/255). +pub const IDLE_ALPHA : u32 = 56; +pub const OUTLINE_PX : u32 = 1; +pub const SHOWS_REALM_LABELS : bool = false; + +/// L4. Fit factor 0.63 as permille. +pub const FIT_PERMILLE : i64 = 630; + +/// L6. The state of T27 OS's home: HOME, or the index of the petal whose menu is open. +pub const STATE_HOME : u32 = 255; + +/// L7. Card slots of a petal menu: a 3 x 3 grid. +pub const CARD_COLS : u32 = 3; +pub const CARD_ROWS : u32 = 3; +pub const CARD_SLOTS : u32 = 9; +pub const CARD_GAP_PX : i64 = 12; + +pub fn petal_ok(p: u32) -> bool { + return p < PETAL_COUNT; +} + +pub fn realm(p: u32) -> u32 { + if (petal_ok(p) == false) { + return REALM_COUNT; + } + return p / PETALS_PER_REALM; +} + +pub fn vx(p: u32, v: u32) -> i64 { + return QX[p * 4 + v]; +} + +pub fn vy(p: u32, v: u32) -> i64 { + return QY[p * 4 + v]; +} + +/// L5. Does the edge (xi, yi) -> (xj, yj) cross the ray from (px, py) towards +x? +/// The float form px < (xj - xi) * (py - yi) / (yj - yi) + xi, multiplied out by (yj - yi), +/// whose sign flips the inequality. yj != yi whenever the first test passes. +pub fn crosses(xi: i64, yi: i64, xj: i64, yj: i64, px: i64, py: i64) -> bool { + if ((yi > py) == (yj > py)) { + return false; + } + const lhs : i64 = (px - xi) * (yj - yi); + const rhs : i64 = (xj - xi) * (py - yi); + if (yj > yi) { + return lhs < rhs; + } + return lhs > rhs; +} + +/// L5. Ray-cast point-in-quad, the algorithm of photon_trinity_canvas.zig:2552. +pub fn inside(p: u32, px: i64, py: i64) -> bool { + var odd : bool = false; + var v : u32 = 0; + while (v < 4) { + const j : u32 = (v + 3) % 4; + if (crosses(vx(p, v), vy(p, v), vx(p, j), vy(p, j), px, py)) { + odd = odd == false; + } + v = v + 1; + } + return odd; +} + +/// L5. The petal under a point given in box milli-units, or PETAL_NONE. +pub fn hit(px: i64, py: i64) -> u32 { + var p : u32 = 0; + while (p < PETAL_COUNT) { + if (inside(p, px, py)) { + return p; + } + p = p + 1; + } + return PETAL_NONE; +} + +/// L4. Viewport pixels per box unit, in permille: min(w / 596, h / 526) * 0.63. +pub fn fit_permille(w_px: i64, h_px: i64) -> i64 { + const sw : i64 = @divTrunc(w_px * 1000000, BOX_W); + const sh : i64 = @divTrunc(h_px * 1000000, BOX_H); + var s : i64 = sw; + if (sh < sw) { + s = sh; + } + return @divTrunc(s * FIT_PERMILLE, 1000); +} + +/// L4. A viewport pixel coordinate to box milli-units. origin is the viewport centre on that axis, +/// centre the box centre on the same axis, scale from fit_permille. +pub fn to_box(px: i64, origin: i64, centre: i64, scale: i64) -> i64 { + return centre + @divTrunc((px - origin) * 1000000, scale); +} + +/// L4 + L5. The petal under a viewport pixel of a w x h viewport. +pub fn hit_px(x: i64, y: i64, w: i64, h: i64) -> u32 { + const s : i64 = fit_permille(w, h); + if (s <= 0) { + return PETAL_NONE; + } + return hit(to_box(x, @divTrunc(w, 2), BOX_CX, s), to_box(y, @divTrunc(h, 2), BOX_CY, s)); +} + +/// L3. The fill of petal p as 0xAARRGGBB, given the open petal (or STATE_HOME) and the hovered +/// petal (or PETAL_NONE). Open beats hover beats idle. +pub fn fill(p: u32, open: u32, hovered: u32) -> u32 { + if (p == open) { + return ALPHA_OPAQUE + REALM_RGB[realm(p)]; + } + if (p == hovered) { + return ALPHA_OPAQUE + RGB_WHITE; + } + return IDLE_ALPHA * 16777216; +} + +pub fn outline() -> u32 { + return ALPHA_OPAQUE + RGB_WHITE; +} + +/// L6. A tap on HOME opens the petal under it; a tap on empty space stays HOME. While a menu is +/// open the tap belongs to the menu: with no cards it changes nothing. +pub fn on_tap(state: u32, px: i64, py: i64) -> u32 { + if (state != STATE_HOME) { + return state; + } + const p : u32 = hit(px, py); + if (p == PETAL_NONE) { + return STATE_HOME; + } + return p; +} + +/// L6. Back (the Queen back control, the brand, or Escape) always returns HOME. +pub fn on_back(state: u32) -> u32 { + return STATE_HOME; +} + +pub fn menu_open(state: u32) -> bool { + return petal_ok(state); +} + +/// L7. How many cards petal p's menu holds. MVP: none -- every menu is the empty frame. +/// A card, when one is added, names a widget id of the WIDGETS gallery; this spec does not copy +/// that list. +pub fn card_count(p: u32) -> u32 { + return 0; +} + +/// L7. Left edge of card slot k in a menu panel w pixels wide. +pub fn card_x(k: u32, w: i64) -> i64 { + const col : i64 = k % CARD_COLS; + const cw : i64 = @divTrunc(w - CARD_GAP_PX * 4, 3); + return CARD_GAP_PX + col * (cw + CARD_GAP_PX); +} + +/// L7. Top edge of card slot k in a menu panel h pixels high. +pub fn card_y(k: u32, h: i64) -> i64 { + const row : i64 = k / CARD_COLS; + const ch : i64 = @divTrunc(h - CARD_GAP_PX * 4, 3); + return CARD_GAP_PX + row * (ch + CARD_GAP_PX); +} + +test there_are_27_petals_in_3_realms_of_9 { + assert PETAL_COUNT == REALM_COUNT * PETALS_PER_REALM; + assert realm(0) == REALM_RAZUM; + assert realm(8) == REALM_RAZUM; + assert realm(9) == REALM_MATERIYA; + assert realm(17) == REALM_MATERIYA; + assert realm(18) == REALM_DUKH; + assert realm(26) == REALM_DUKH; + assert realm(27) == REALM_COUNT; + assert realm(PETAL_NONE) == REALM_COUNT; +} + +test every_vertex_is_inside_the_box { + var i : u32 = 0; + while (i < PETAL_COUNT * 4) { + assert QX[i] > 0; + assert QX[i] < BOX_W; + assert QY[i] > 0; + assert QY[i] < BOX_H; + i = i + 1; + } +} + +test the_centre_of_every_petal_hits_that_petal { + var p : u32 = 0; + while (p < PETAL_COUNT) { + const cx : i64 = @divTrunc(vx(p, 0) + vx(p, 1) + vx(p, 2) + vx(p, 3), 4); + const cy : i64 = @divTrunc(vy(p, 0) + vy(p, 1) + vy(p, 2) + vy(p, 3), 4); + assert hit(cx, cy) == p; + p = p + 1; + } +} + +test the_bottom_tip_is_razum_0_and_8_either_side_of_the_seam { + assert hit(290000, 500000) == 0; + assert hit(305000, 500000) == 8; + assert hit(297500, 500000) == PETAL_NONE; +} + +test the_hollow_centre_and_the_corners_hit_nothing { + assert hit(298000, 240000) == PETAL_NONE; + assert hit(0, 0) == PETAL_NONE; + assert hit(BOX_W, BOX_H) == PETAL_NONE; + assert hit(BOX_CX, 525000) == PETAL_NONE; +} + +test a_viewport_tap_maps_through_the_fit { + assert fit_permille(596, 526) == 630; + assert fit_permille(1192, 526) == 630; + assert to_box(298, 298, BOX_CX, 630) == BOX_CX; + assert hit_px(298, 263, 596, 526) == hit(BOX_CX, BOX_CY); + assert hit_px(0, 0, 0, 0) == PETAL_NONE; +} + +test fills_follow_the_glass_menu_rule { + assert fill(5, STATE_HOME, PETAL_NONE) == 939524096; + assert fill(5, STATE_HOME, 5) == 4294967295; + assert fill(0, 0, PETAL_NONE) == 4294956800; + assert fill(9, 9, 9) == 4283497210; + assert fill(26, 26, PETAL_NONE) == 4290614265; + assert outline() == 4294967295; + assert SHOWS_REALM_LABELS == false; +} + +test a_tap_on_a_petal_opens_its_menu_and_back_returns_home { + const s1 : u32 = on_tap(STATE_HOME, 290000, 500000); + assert s1 == 0; + assert menu_open(s1); + assert on_tap(s1, 305000, 500000) == 0; + assert on_back(s1) == STATE_HOME; + assert menu_open(STATE_HOME) == false; + assert on_tap(STATE_HOME, 298000, 240000) == STATE_HOME; +} + +test every_petal_opens_and_every_menu_is_empty_in_the_mvp { + var p : u32 = 0; + while (p < PETAL_COUNT) { + const cx : i64 = @divTrunc(vx(p, 0) + vx(p, 1) + vx(p, 2) + vx(p, 3), 4); + const cy : i64 = @divTrunc(vy(p, 0) + vy(p, 1) + vy(p, 2) + vy(p, 3), 4); + assert on_tap(STATE_HOME, cx, cy) == p; + assert card_count(p) == 0; + p = p + 1; + } +} + +test card_slots_tile_a_panel_without_overlap { + assert card_x(0, 372) == 12; + assert card_x(1, 372) == 132; + assert card_x(2, 372) == 252; + assert card_y(3, 372) == 132; + assert card_y(8, 372) == 252; + assert card_x(2, 372) + 108 + CARD_GAP_PX == 372; +} From 21b9b625cedc4308040a9ba68f1573cfa30007b0 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 09:28:16 +0700 Subject: [PATCH 2/3] feat(t27f): petal_menu.t27 passes t27c icarus-lowerable (M4 of #10782) The structural Icarus classifier rejected the spec because of the @divTrunc builtin calls in fit_permille, to_box, hit_px, card_x and card_y (a call to a name that is neither a module function nor an Icarus builtin). The str constants and u32 colour arrays were never the blocker: const declarations are always accepted. Those functions now divide with `/`. gen-zig still lowers `/` on i64 to @divTrunc inside function bodies, so the semantics are unchanged; the two test-block centroids keep @divTrunc because tests are outside the classifier and gen-zig does not rewrite `/` there. Refs #10782 Co-Authored-By: Claude Opus 5.5 --- specs/t27f/petal_menu.t27 | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/specs/t27f/petal_menu.t27 b/specs/t27f/petal_menu.t27 index 875440a017..6e4fdc62db 100644 --- a/specs/t27f/petal_menu.t27 +++ b/specs/t27f/petal_menu.t27 @@ -29,7 +29,9 @@ ; (gHashTag/trinity apps/website/specs/widgets/gallery.t27). MVP: every menu empty. ; -- ; Integer-only on purpose: no float, no division in the hit test, so the same spec can lower to -; Verilog for the board (gen-verilog) as well as to the desktop hosts. +; Verilog for the board (gen-verilog) as well as to the desktop hosts. The fit and the card grid +; divide with `/`, never `@divTrunc`: `t27c icarus-lowerable` rejects the builtin call, and `/` +; on i64 truncates towards zero the same way (#10782, M4). ; phi^2 + 1/phi^2 = 3 | TRINITY module T27fPetalMenu; @@ -198,19 +200,19 @@ pub fn hit(px: i64, py: i64) -> u32 { /// L4. Viewport pixels per box unit, in permille: min(w / 596, h / 526) * 0.63. pub fn fit_permille(w_px: i64, h_px: i64) -> i64 { - const sw : i64 = @divTrunc(w_px * 1000000, BOX_W); - const sh : i64 = @divTrunc(h_px * 1000000, BOX_H); + const sw : i64 = w_px * 1000000 / BOX_W; + const sh : i64 = h_px * 1000000 / BOX_H; var s : i64 = sw; if (sh < sw) { s = sh; } - return @divTrunc(s * FIT_PERMILLE, 1000); + return s * FIT_PERMILLE / 1000; } /// L4. A viewport pixel coordinate to box milli-units. origin is the viewport centre on that axis, /// centre the box centre on the same axis, scale from fit_permille. pub fn to_box(px: i64, origin: i64, centre: i64, scale: i64) -> i64 { - return centre + @divTrunc((px - origin) * 1000000, scale); + return centre + (px - origin) * 1000000 / scale; } /// L4 + L5. The petal under a viewport pixel of a w x h viewport. @@ -219,7 +221,7 @@ pub fn hit_px(x: i64, y: i64, w: i64, h: i64) -> u32 { if (s <= 0) { return PETAL_NONE; } - return hit(to_box(x, @divTrunc(w, 2), BOX_CX, s), to_box(y, @divTrunc(h, 2), BOX_CY, s)); + return hit(to_box(x, w / 2, BOX_CX, s), to_box(y, h / 2, BOX_CY, s)); } /// L3. The fill of petal p as 0xAARRGGBB, given the open petal (or STATE_HOME) and the hovered @@ -270,14 +272,14 @@ pub fn card_count(p: u32) -> u32 { /// L7. Left edge of card slot k in a menu panel w pixels wide. pub fn card_x(k: u32, w: i64) -> i64 { const col : i64 = k % CARD_COLS; - const cw : i64 = @divTrunc(w - CARD_GAP_PX * 4, 3); + const cw : i64 = (w - CARD_GAP_PX * 4) / 3; return CARD_GAP_PX + col * (cw + CARD_GAP_PX); } /// L7. Top edge of card slot k in a menu panel h pixels high. pub fn card_y(k: u32, h: i64) -> i64 { const row : i64 = k / CARD_COLS; - const ch : i64 = @divTrunc(h - CARD_GAP_PX * 4, 3); + const ch : i64 = (h - CARD_GAP_PX * 4) / 3; return CARD_GAP_PX + row * (ch + CARD_GAP_PX); } From 11562da285f1b02576c6843c539b93f329818520 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 09:30:10 +0700 Subject: [PATCH 3/3] feat(t27f): petal_menu.t27 passes t27c icarus-simulate, 10/10 on Icarus The petal centroid moves from the two test blocks into module functions centre_x/centre_y. In the tests it hit two backend limits: gen-zig does not rewrite `/` on an untyped call sum, and the Verilog testbench emitted @divTrunc verbatim and declared a reg after a statement inside the loop. A typed local before `/` and inline calls in the asserts avoid both. Refs #10782 Co-Authored-By: Claude Opus 5.5 --- specs/t27f/petal_menu.t27 | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/specs/t27f/petal_menu.t27 b/specs/t27f/petal_menu.t27 index 6e4fdc62db..88c9d0db09 100644 --- a/specs/t27f/petal_menu.t27 +++ b/specs/t27f/petal_menu.t27 @@ -30,7 +30,7 @@ ; -- ; Integer-only on purpose: no float, no division in the hit test, so the same spec can lower to ; Verilog for the board (gen-verilog) as well as to the desktop hosts. The fit and the card grid -; divide with `/`, never `@divTrunc`: `t27c icarus-lowerable` rejects the builtin call, and `/` +; and the petal centres divide with `/`, never `@divTrunc`: `t27c icarus-lowerable` rejects the builtin call, and `/` ; on i64 truncates towards zero the same way (#10782, M4). ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -157,6 +157,17 @@ pub fn vy(p: u32, v: u32) -> i64 { return QY[p * 4 + v]; } +/// L1. The vertex mean of petal p: a point inside every petal, where a host can anchor a label. +pub fn centre_x(p: u32) -> i64 { + const sum : i64 = vx(p, 0) + vx(p, 1) + vx(p, 2) + vx(p, 3); + return sum / 4; +} + +pub fn centre_y(p: u32) -> i64 { + const sum : i64 = vy(p, 0) + vy(p, 1) + vy(p, 2) + vy(p, 3); + return sum / 4; +} + /// L5. Does the edge (xi, yi) -> (xj, yj) cross the ray from (px, py) towards +x? /// The float form px < (xj - xi) * (py - yi) / (yj - yi) + xi, multiplied out by (yj - yi), /// whose sign flips the inequality. yj != yi whenever the first test passes. @@ -309,9 +320,7 @@ test every_vertex_is_inside_the_box { test the_centre_of_every_petal_hits_that_petal { var p : u32 = 0; while (p < PETAL_COUNT) { - const cx : i64 = @divTrunc(vx(p, 0) + vx(p, 1) + vx(p, 2) + vx(p, 3), 4); - const cy : i64 = @divTrunc(vy(p, 0) + vy(p, 1) + vy(p, 2) + vy(p, 3), 4); - assert hit(cx, cy) == p; + assert hit(centre_x(p), centre_y(p)) == p; p = p + 1; } } @@ -360,9 +369,7 @@ test a_tap_on_a_petal_opens_its_menu_and_back_returns_home { test every_petal_opens_and_every_menu_is_empty_in_the_mvp { var p : u32 = 0; while (p < PETAL_COUNT) { - const cx : i64 = @divTrunc(vx(p, 0) + vx(p, 1) + vx(p, 2) + vx(p, 3), 4); - const cy : i64 = @divTrunc(vy(p, 0) + vy(p, 1) + vy(p, 2) + vy(p, 3), 4); - assert on_tap(STATE_HOME, cx, cy) == p; + assert on_tap(STATE_HOME, centre_x(p), centre_y(p)) == p; assert card_count(p) == 0; p = p + 1; }