From 0fece282dd91962e590a66c9cfb3ad810af307d9 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 10 Jul 2026 21:58:36 -0700 Subject: [PATCH] Keep machine mode grammar on active focus. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the frequent mode line through the same pointer, selection, and reticule target as its direct controls so pointer absence cannot hide a focused machine. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- crates/misaligned-bevy/src/main.rs | 143 +++++++++++-------- wiki/interface/bevy.md | 29 ++-- wiki/interface/context-menu.md | 46 +++--- wiki/log/2026-07-10-reticule-mode-grammar.md | 29 ++++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions/2026-07-10.md | 10 ++ 6 files changed, 170 insertions(+), 92 deletions(-) create mode 100644 wiki/log/2026-07-10-reticule-mode-grammar.md diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index a274eee1..c1f071b2 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -49,12 +49,12 @@ const COMPUTE_CHANNELS: usize = 5; /// Compact context-menu card width; human rows carry choices, not descriptor /// receipts (wiki/interface/context-menu.md). const MENU_WIDTH: f32 = 300.0; -/// The hover grammar is one clean line, not a panel. This width leaves the +/// The machine grammar is one clean line, not a panel. This width leaves the /// three words readable without letting the affordance become a HUD card -/// (wiki/interface/context-menu.md, hover machine grammar). -const HOVER_MODE_WIDTH: f32 = 248.0; +/// (wiki/interface/context-menu.md, frequent machine grammar). +const MODE_GRAMMAR_WIDTH: f32 = 248.0; /// Air between the fixed mode grammar and the machine base it belongs to. -const HOVER_MODE_GAP: f32 = 12.0; +const MODE_GRAMMAR_GAP: f32 = 12.0; const DETECTION_ROWS: usize = 6; const DETECTION_CELLS: usize = 4; @@ -396,8 +396,8 @@ struct ShotHarness { path: String, frames: u32, taken: bool, - /// Pointer-free deterministic hover target for UI evidence. - hover_tile: Option<(i32, i32)>, + /// Force the mode-grammar evidence through pointer-free reticule focus. + mode_grammar_reticule_only: bool, } // ─── The wake (world/story/opening.md beat 1 — glow choreography) ──────────── @@ -1086,13 +1086,13 @@ struct DetectionCell { #[derive(Component)] struct OverlayText; /// Root for the menu-closed machine grammar: bare `WORK / THINK / LIE` -/// text adjacent to the hovered owned rack. It has no panel chrome. +/// text adjacent to the active owned rack. It has no panel chrome. #[derive(Component)] -struct HoverModeGrammar; -/// One word or separator in the hover grammar. Mode words restyle from the -/// hovered machine's current delegation; separators stay quiet chrome. +struct MachineModeGrammar; +/// One word or separator in the machine grammar. Mode words restyle from the +/// active machine's current delegation; separators stay quiet chrome. #[derive(Component, Clone, Copy)] -struct HoverModeWord { +struct MachineModeWord { mode: Option, } /// The context-menu root node (wiki/interface/context-menu.md), absolute and @@ -1170,18 +1170,17 @@ fn main() { let path = std::env::var("MISALIGNED_SHOT_PATH") .unwrap_or_else(|_| format!("misaligned_shot_{kind}.png")); dev_shot_scenario(&mut game, &mut mode, &kind); - let hover_tile = (kind == "hover-menu").then(|| { + if kind == "hover-menu" { let core = game.sim.core_position(); game.sim .set_machine_mode(game.sim.core.host_machine, MachineMode::Work); game.set_cursor(core.0, core.1); - core - }); + } ShotHarness { path, frames: 150, taken: false, - hover_tile, + mode_grammar_reticule_only: kind == "hover-menu", } }); @@ -1304,7 +1303,7 @@ fn main() { render_noise_field_flat, scroll_sidebar, render_ui, - render_hover_mode_grammar, + render_machine_mode_grammar, menu_pointer, log_row_pointer, manage_menu_ui, @@ -1605,9 +1604,9 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { } } // Machine-mode grammar evidence belongs in the default material - // frame: the synthetic hover selects the host without inventing a - // pointer coordinate, so the capture proves layout comes from the - // projected chassis rather than the cursor. + // frame: reticule focus selects the host without a pointer coordinate, + // proving the line survives mouse absence while layout still comes + // from the projected chassis. "hover-menu" => { mode.material = true; mode.zoom = 1.0; @@ -5433,18 +5432,18 @@ fn setup_ui(mut commands: Commands) { }); }); - // The frequent machine-mode surface is not another card. Hovering an - // owned rack reveals one bare, high-contrast line fixed beside that - // chassis; the pointer chooses the rack but never places its UI. 1/2/3 - // remain the one-press controls. No border, fill, heading, status, or - // explanatory subtitle competes with the three verbs. + // The frequent machine-mode surface is not another card. Whenever the + // pointer/selection/attention target resolves to an owned rack, reveal one + // bare line fixed beside that chassis. 1/2/3 use the same target order. + // No border, fill, heading, status, or explanatory subtitle competes with + // the three verbs. commands .spawn(( Node { position_type: PositionType::Absolute, left: Val::Px(0.0), top: Val::Px(0.0), - width: Val::Px(HOVER_MODE_WIDTH), + width: Val::Px(MODE_GRAMMAR_WIDTH), height: Val::Px(32.0), flex_direction: FlexDirection::Row, align_items: AlignItems::Center, @@ -5455,7 +5454,7 @@ fn setup_ui(mut commands: Commands) { BackgroundColor(Color::NONE), Visibility::Hidden, GlobalZIndex(40), - HoverModeGrammar, + MachineModeGrammar, )) .with_children(|line| { for (label, mode) in [ @@ -5472,7 +5471,7 @@ fn setup_ui(mut commands: Commands) { ..default() }, TextColor(DIM), - HoverModeWord { mode }, + MachineModeWord { mode }, )); } }); @@ -5500,28 +5499,28 @@ fn setup_ui(mut commands: Commands) { )); } -/// Place the hover grammar to the left of its projected machine base, flipping +/// Place the machine grammar to the left of its projected machine base, flipping /// right only when the machine is too close to the canvas edge. This keeps the /// text off the chassis while making its attachment independent of where the /// pointer happens to sit inside the rack's tile. -fn hover_mode_position(machine_base: Vec2, window_size: Vec2) -> Vec2 { - let world_width = (window_size.x - SIDEBAR_WIDTH).max(HOVER_MODE_WIDTH + 24.0); - let left_of_machine = machine_base.x - HOVER_MODE_WIDTH - HOVER_MODE_GAP; +fn mode_grammar_position(machine_base: Vec2, window_size: Vec2) -> Vec2 { + let world_width = (window_size.x - SIDEBAR_WIDTH).max(MODE_GRAMMAR_WIDTH + 24.0); + let left_of_machine = machine_base.x - MODE_GRAMMAR_WIDTH - MODE_GRAMMAR_GAP; let unclamped_left = if left_of_machine >= 12.0 { left_of_machine } else { - machine_base.x + HOVER_MODE_GAP + machine_base.x + MODE_GRAMMAR_GAP }; Vec2::new( - unclamped_left.clamp(12.0, world_width - HOVER_MODE_WIDTH - 12.0), + unclamped_left.clamp(12.0, world_width - MODE_GRAMMAR_WIDTH - 12.0), (machine_base.y - 58.0).clamp(12.0, window_size.y - 44.0), ) } -/// Project the hovered machine itself into UI space. The pointer determines -/// the same target used by the one-press 1/2/3 input, but the resulting line -/// is fixed to the server rather than following the pointer inside its tile. -fn hover_mode_machine_base( +/// Project the targeted machine itself into UI space. Pointer, selection, and +/// reticule resolve the same target used by one-press 1/2/3 input; the line is +/// fixed to the server rather than following the pointer inside its tile. +fn mode_grammar_machine_base( material: bool, x: i32, y: i32, @@ -5541,18 +5540,25 @@ fn hover_mode_machine_base( } } -/// Keep the three machine verbs physically adjacent to the hovered rack. -/// Screenshot evidence supplies a deterministic synthetic hover target. +/// Resolve the same frequent-machine target used by the 1/2/3 hotkeys. +/// Pointer hover may retarget the affordance, but moving the pointer away +/// falls back to machine selection and then the attention reticule. +fn mode_grammar_target(game: &Game, pointer: Option<(i32, i32)>) -> Option<(u32, i32, i32)> { + game.primary_machine_tile(pointer) +} + +/// Keep the three machine verbs physically adjacent to the active rack. +/// Screenshot evidence leaves the pointer absent to exercise reticule fallback. #[allow(clippy::too_many_arguments)] -fn render_hover_mode_grammar( +fn render_machine_mode_grammar( game: Res, mode: Res, harness: Option>, windows: Query<&Window, With>, camera_q: Query<(&Camera, &GlobalTransform), With>, camera3_q: RealCameraQuery, - mut grammar: Query<(&mut Node, &mut Visibility), With>, - mut words: Query<(&HoverModeWord, &mut TextColor)>, + mut grammar: Query<(&mut Node, &mut Visibility), With>, + mut words: Query<(&MachineModeWord, &mut TextColor)>, ) { let Ok((mut node, mut visibility)) = grammar.single_mut() else { return; @@ -5562,16 +5568,17 @@ fn render_hover_mode_grammar( return; }; - let synthetic_hover = harness.as_ref().and_then(|h| h.hover_tile); - let hover = synthetic_hover.or_else(|| { - if mode.material { - mouse_grid_real(&windows, &camera3_q) - } else { - mouse_grid(&windows, &camera_q) - } - }); - let Some((machine_id, machine_x, machine_y)) = hover.and_then(|(x, y)| game.machine_at(x, y)) - else { + let pointer = if harness + .as_ref() + .is_some_and(|h| h.mode_grammar_reticule_only) + { + None + } else if mode.material { + mouse_grid_real(&windows, &camera3_q) + } else { + mouse_grid(&windows, &camera_q) + }; + let Some((machine_id, machine_x, machine_y)) = mode_grammar_target(&game, pointer) else { *visibility = Visibility::Hidden; return; }; @@ -5581,12 +5588,12 @@ fn render_hover_mode_grammar( } let Some(machine_base) = - hover_mode_machine_base(mode.material, machine_x, machine_y, &camera_q, &camera3_q) + mode_grammar_machine_base(mode.material, machine_x, machine_y, &camera_q, &camera3_q) else { *visibility = Visibility::Hidden; return; }; - let position = hover_mode_position(machine_base, Vec2::new(window.width(), window.height())); + let position = mode_grammar_position(machine_base, Vec2::new(window.width(), window.height())); node.left = Val::Px(position.x); node.top = Val::Px(position.y); *visibility = Visibility::Visible; @@ -5602,25 +5609,41 @@ fn render_hover_mode_grammar( } #[cfg(test)] -mod hover_mode_grammar_tests { - use super::{HOVER_MODE_GAP, HOVER_MODE_WIDTH, hover_mode_position}; +mod mode_grammar_tests { + use super::{ + Game, MODE_GRAMMAR_GAP, MODE_GRAMMAR_WIDTH, mode_grammar_position, mode_grammar_target, + }; use bevy::prelude::Vec2; #[test] fn grammar_sits_at_a_stable_offset_from_the_machine_base() { let machine = Vec2::new(640.0, 360.0); - let position = hover_mode_position(machine, Vec2::new(1280.0, 720.0)); + let position = mode_grammar_position(machine, Vec2::new(1280.0, 720.0)); - assert_eq!(position.x, machine.x - HOVER_MODE_WIDTH - HOVER_MODE_GAP); + assert_eq!( + position.x, + machine.x - MODE_GRAMMAR_WIDTH - MODE_GRAMMAR_GAP + ); assert_eq!(position.y, machine.y - 58.0); } #[test] fn grammar_flips_right_at_the_left_canvas_edge() { let machine = Vec2::new(40.0, 100.0); - let position = hover_mode_position(machine, Vec2::new(1280.0, 720.0)); + let position = mode_grammar_position(machine, Vec2::new(1280.0, 720.0)); - assert_eq!(position.x, machine.x + HOVER_MODE_GAP); + assert_eq!(position.x, machine.x + MODE_GRAMMAR_GAP); + } + + #[test] + fn grammar_stays_on_the_reticule_machine_without_pointer_hover() { + let game = Game::new(); + let core = game.sim.core_position(); + + assert_eq!( + mode_grammar_target(&game, None).map(|(_, x, y)| (x, y)), + Some(core) + ); } } diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md index 4c1bced7..34603021 100644 --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -182,17 +182,20 @@ controls table), plus zoom, sidebar scrolling, and the render flip: execute; `esc` or a click away closes. Anchor verbs (salvage, buy, fallback, taps, takes, economy and social actions) live on the context menu, not on keys. -- **Hover / fleet hotkeys (menu closed).** Point at a rack (hover — no - click) and press `1`–`3` / numpad to assign WORK / THINK / LIE - **immediately** — not enter → mode dial → row → enter. While the pointer is - on an owned rack, one bare `WORK / THINK / LIE` line stays fixed beside the - server's projected base (the pointer chooses the server but does not place - the line). The current mode is bone-bright and the alternatives/slashes are - dim chrome. +- **Focus / fleet hotkeys (menu closed).** Press `1`–`3` / numpad to assign + WORK / THINK / LIE **immediately** — not enter → mode dial → row → enter. + The target resolves in the existing order: owned rack under the pointer, + primary machine selection, then owned rack under the attention reticule. + Whenever that target exists, one bare `WORK / THINK / LIE` line stays fixed + beside the server's projected base (the pointer may choose the server but + does not place the line). The current mode is bone-bright and the + alternatives/slashes are dim chrome. It has no panel, heading, subtitle, counters, or extra state, and disappears - when the pointer leaves or the full menu opens - ([context-menu.md](context-menu.md), hover-grammar addendum). `i` cycles light / - medium / hard intensity on that rack; `5`–`9` fire its Nth committed action + when no frequent-machine target exists or the full menu opens + ([context-menu.md](context-menu.md), machine-grammar addendum). Moving the + pointer away does not hide a line whose rack remains selected or under the + reticule. `i` cycles light / medium / hard intensity on that rack; `5`–`9` + fire its Nth committed action (controls do not occupy shortcut slots); `Enter` / `e` open the hovered tile's menu. Drag a marquee or shift-click to multi-select; with no hover rack, `1`–`3` and @@ -217,6 +220,6 @@ swells from the THINK chassis while proportional crimson motes settle around it; actual absorbed motes then curve through world space into the LIE well. Those rates come only from `Sim::work_productions` and `Sim::work_absorptions`; paused frames keep only persistent token geometry. -`MISALIGNED_SHOT=hover-menu` captures the clean hover grammar over the owned -host with WORK current; it is the visual acceptance frame for context-menu.md -H1–H4. +`MISALIGNED_SHOT=hover-menu` retains its compatibility name but captures the +clean machine grammar over the reticule-focused owned host with WORK current; +it is the visual acceptance frame for context-menu.md H1–H4. diff --git a/wiki/interface/context-menu.md b/wiki/interface/context-menu.md index c51b78f8..398ca7ee 100644 --- a/wiki/interface/context-menu.md +++ b/wiki/interface/context-menu.md @@ -62,6 +62,10 @@ Status note: implemented 2026-07-07 on the context-menu worktree (all computer but does not place its grammar. The line projects from a fixed point at that server's base, so moving within the same rack cannot drag the words around the world. + 2026-07-10 reticule-target correction: pointer hover is only the first + target source. If the pointer leaves the machine, the line remains on the + selected machine or the owned computer under the attention reticule, + matching the existing `1`–`3` target fallback instead of disappearing. 2026-07-10 compact-menu amendment: human menus are terse choice lists. They render verb labels and dial state only, omitting descriptor metadata, literal control tags, inline blocked explanations, and repeated key-help @@ -233,19 +237,21 @@ noise). Cameron adopted **status dials** (2026-07-09): the root answers Category drill — four folders that always require a second level (reads as a file browser, not standing policy). -## Addendum (2026-07-10): the hover grammar is the whole frequent-mode UI +## Addendum (2026-07-10): the machine grammar is the whole frequent-mode UI The full action menu remains necessary for infrequent verbs, legality, costs, and disabled reasons. Machine mode is different: it is one of three -frequent physical postures already committed by a single key. On hover, the -Bevy frontend therefore presents the choice as one severe line attached to -the computer: +frequent physical postures already committed by a single key. Whenever the +frequent-machine target resolves to an owned computer, the Bevy frontend +therefore presents the choice as one severe line attached to that computer: `WORK / THINK / LIE` -- The line appears only while the pointer is over an owned computer, play is - active, and the full context menu is closed. Leaving the computer, moving - over the sidebar, or opening the full menu removes it. +- Target order is the same as the direct mode controls: (1) an owned computer + under the pointer, (2) the primary machine selection, then (3) an owned + computer under the attention reticule. Moving the pointer away never hides + the line while selection or reticule focus still resolves to a machine. + The line appears only during active play with the full context menu closed. - The computer's current mode is bone-bright. The other two words and slash separators are dim neutral chrome. Amber remains ownership/presence on the computer; crimson remains consequence. The hover line introduces no color @@ -257,9 +263,9 @@ the computer: readout, cost, signature, or prose subtitle. In particular, WORK does not expand to “day job.” The three words must be the first and only read. - `1` / `2` / `3` (including numpad) remain the immediate commit path for - WORK / THINK / LIE on the hovered computer. This overlay does not create a - second target or a Bevy-only action; it renders the same hover target those - controls already use. + WORK / THINK / LIE on the resolved computer. This overlay does not create a + second target or a Bevy-only action; it renders the same pointer → selection + → reticule target those controls already use. - The treatment is identical over the material and flat-sensorium views. It belongs to the machine, not to either rendering dialect. @@ -304,19 +310,21 @@ made a three-word decision look dirty, slow, and administrative. 10. `5`-`9` count committed action rows only. A newly added control cannot silently shift quick-action numbering. -### Hover-grammar acceptance criteria +### Frequent-mode grammar acceptance criteria -H1. Hovering any owned compute machine in Bevy, with the full menu closed, - shows exactly `WORK / THINK / LIE` as one unboxed line fixed adjacent to - the machine's projected base; no other hover copy or chrome appears, and - moving the pointer within that machine does not move the line. +H1. Resolving an owned compute machine from pointer hover, machine selection, + or the attention reticule in Bevy, with the full menu closed, shows exactly + `WORK / THINK / LIE` as one unboxed line fixed adjacent to the machine's + projected base. Moving the pointer within that machine does not move the + line, and moving it away does not hide a selected/reticule-focused line. H2. The current mode is bone-bright; alternatives and slashes are dim neutral chrome. No amber or crimson pixel belongs to the text treatment. -H3. Leaving the machine, crossing into the sidebar, or opening the full menu - hides the line. Material and flat-sensorium views obey the same rule. +H3. The line hides only when no pointer, selection, or reticule machine target + exists, or when the full menu opens. Material and flat-sensorium views obey + the same rule. H4. The overlay reads the same machine target and current `WorkGrid` mode used - by the existing hover + `1`–`3` controls; switching mode immediately moves - the bright treatment without creating new sim state. + by the existing `1`–`3` controls; switching mode immediately moves the + bright treatment without creating new sim state. ## Addendum (2026-07-08): events carry you to the thing diff --git a/wiki/log/2026-07-10-reticule-mode-grammar.md b/wiki/log/2026-07-10-reticule-mode-grammar.md new file mode 100644 index 00000000..2dd3e653 --- /dev/null +++ b/wiki/log/2026-07-10-reticule-mode-grammar.md @@ -0,0 +1,29 @@ +# Reticule-backed machine-mode grammar + +``` +Type: log +``` + +- Intent: Keep `WORK / THINK / LIE` available while the attention reticule or + machine selection still targets an owned computer, regardless of where the + mouse rests. +- Changed: Bevy's frequent-mode line now resolves the same pointer → primary + selection → reticule target as the direct machine controls. The existing + server-base projection still places the line, so neither pointer motion nor + target fallback drags the words through the world. Internal names now call + this machine grammar rather than hover grammar. +- Design/spec impact: Amends the context-menu and Bevy specs to distinguish + target resolution from pointer presence. The material square is the + attention reticule; the thick amber column remains the core presence beam. + No sim or save behavior changes. +- Checks: Three focused mode-grammar tests, including pointer-free reticule + fallback; observed `MISALIGNED_SHOT=hover-menu` material capture with its + forced pointer absent (fog audit OK, SHA-256 + `6247e0d346a7415ddb6f055303166625e8b4dcf895adf701f2cc1e4c68d464d5`); + `./tools/check.sh --frontend` (tests and clippy passed; first run correctly + stopped on the newly added log's stale generated index, then the regenerated + second pass passed every frontend and documentation gate). + +Defense: Implements `wiki/interface/context-menu.md` frequent-mode grammar +H1-H4: the visible choice and the existing direct controls resolve one shared +machine target, including selection and reticule fallback. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index b8928744..e9d78f40 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -126,6 +126,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-10-riff-capture-people-plots-salience.md](2026-07-10-riff-capture-people-plots-salience.md) +## 2026-07-10 - Reticule-backed machine-mode grammar + +- Intent: (see session log) +- Log: [wiki/log/2026-07-10-reticule-mode-grammar.md](2026-07-10-reticule-mode-grammar.md) + ## 2026-07-10 - README status correction - Intent: The README still named continuous witness/narration as the next priority even though the narration spec is IMPLEMENTED and ROADMAP item #30 is complete. diff --git a/wiki/log/decisions/2026-07-10.md b/wiki/log/decisions/2026-07-10.md index 6888a734..57101c24 100644 --- a/wiki/log/decisions/2026-07-10.md +++ b/wiki/log/decisions/2026-07-10.md @@ -619,3 +619,13 @@ Type: log his spec already tests the chain end to end; each system keeps its slice), and the six citing pages (plots, social, income, economy, system-laws, horizon) link the anchor; plots.md adds the dependency. +- **2026-07-10 — Machine grammar follows the attention target, not hover + presence.** Cameron caught the contradiction in the live material frame: + the amber square is the attention/selection reticule, yet moving the mouse + off its machine made `WORK / THINK / LIE` disappear. DECIDED: the line uses + the same pointer → primary selection → reticule fallback as the direct + `1`–`3` controls. Pointer hover can retarget it, but pointer absence cannot + suppress a machine that remains selected or under the reticule. The square + remains attention chrome, not a core ring; the thick amber column is the + pre-sight core presence beam. Owner: wiki/interface/context-menu.md; + synchronized surface: wiki/interface/bevy.md. -- 2.51.2