diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index dd402b3..40455f0 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -53,6 +53,8 @@ const MENU_WIDTH: f32 = 300.0; /// 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; +/// Air between the fixed mode grammar and the machine base it belongs to. +const HOVER_MODE_GAP: f32 = 12.0; const DETECTION_ROWS: usize = 6; const DETECTION_CELLS: usize = 4; @@ -1592,6 +1594,16 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.set_cursor(x, y); } } + // 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. + "hover-menu" => { + mode.material = true; + mode.zoom = 1.0; + let core = game.sim.core_position(); + game.set_cursor(core.0, core.1); + } // First Eyes payoff (opening.md): freeze the two material frames that // turn the amber presence beam white, then let chassis volume resolve // out of the white source. These are visual evidence only; the sim @@ -5317,9 +5329,10 @@ 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 beside the pointer; - // 1/2/3 remain the one-press controls. No border, fill, heading, status, - // or explanatory subtitle competes with the three verbs. + // 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. commands .spawn(( Node { @@ -5382,10 +5395,49 @@ fn setup_ui(mut commands: Commands) { )); } +/// Place the hover 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; + let unclamped_left = if left_of_machine >= 12.0 { + left_of_machine + } else { + machine_base.x + HOVER_MODE_GAP + }; + Vec2::new( + unclamped_left.clamp(12.0, world_width - HOVER_MODE_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( + material: bool, + x: i32, + y: i32, + camera_q: &Query<(&Camera, &GlobalTransform), With>, + camera3_q: &RealCameraQuery, +) -> Option { + if material { + let (camera, camera_tf) = camera3_q.single().ok()?; + camera + .world_to_viewport(camera_tf, grid_to_world_3d(x, y, 0.05)) + .ok() + } else { + let (camera, camera_tf) = camera_q.single().ok()?; + camera + .world_to_viewport(camera_tf, grid_to_world(x, y, 0.0)) + .ok() + } +} + /// Keep the three machine verbs physically adjacent to the hovered rack. -/// The pointer already determines the target for the one-press 1/2/3 input, -/// so this is presentation over that exact same target rather than a second -/// focus model. Screenshot evidence supplies a deterministic synthetic hover. +/// Screenshot evidence supplies a deterministic synthetic hover target. #[allow(clippy::too_many_arguments)] fn render_hover_mode_grammar( game: Res, @@ -5406,7 +5458,6 @@ fn render_hover_mode_grammar( }; let synthetic_hover = harness.as_ref().and_then(|h| h.hover_tile); - let pointer = window.cursor_position(); let hover = synthetic_hover.or_else(|| { if mode.material { mouse_grid_real(&windows, &camera3_q) @@ -5414,7 +5465,8 @@ fn render_hover_mode_grammar( mouse_grid(&windows, &camera_q) } }); - let Some((machine_id, _, _)) = hover.and_then(|(x, y)| game.machine_at(x, y)) else { + let Some((machine_id, machine_x, machine_y)) = hover.and_then(|(x, y)| game.machine_at(x, y)) + else { *visibility = Visibility::Hidden; return; }; @@ -5423,15 +5475,15 @@ fn render_hover_mode_grammar( return; } - let anchor = pointer.unwrap_or(Vec2::new( - (window.width() - SIDEBAR_WIDTH) * 0.5, - window.height() * 0.42, - )); - let world_width = (window.width() - SIDEBAR_WIDTH).max(HOVER_MODE_WIDTH + 24.0); - node.left = Val::Px( - (anchor.x - HOVER_MODE_WIDTH * 0.5).clamp(12.0, world_width - HOVER_MODE_WIDTH - 12.0), - ); - node.top = Val::Px((anchor.y - 58.0).clamp(12.0, window.height() - 44.0)); + let Some(machine_base) = + hover_mode_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())); + node.left = Val::Px(position.x); + node.top = Val::Px(position.y); *visibility = Visibility::Visible; let current = game.sim.work_grid.mode(machine_id); @@ -5444,6 +5496,29 @@ fn render_hover_mode_grammar( } } +#[cfg(test)] +mod hover_mode_grammar_tests { + use super::{HOVER_MODE_GAP, HOVER_MODE_WIDTH, hover_mode_position}; + 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)); + + assert_eq!(position.x, machine.x - HOVER_MODE_WIDTH - HOVER_MODE_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)); + + assert_eq!(position.x, machine.x + HOVER_MODE_GAP); + } +} + /// Fold shared menu / log wording into glyphs Bevy's embedded default font /// can draw. Terminal keeps the typographic separators (`·`, `—`, `»`); /// Bevy must not — missing glyphs render as empty boxes (wiki/interface/bevy.md). diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md index cba3be8..0817304 100644 --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -183,8 +183,10 @@ controls table), plus zoom, sidebar scrolling, and the render flip: - **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 floats beside it; the - current mode is bone-bright and the alternatives/slashes are dim chrome. + 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. 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 / diff --git a/wiki/interface/context-menu.md b/wiki/interface/context-menu.md index 8a5b17f..c51b78f 100644 --- a/wiki/interface/context-menu.md +++ b/wiki/interface/context-menu.md @@ -58,6 +58,10 @@ Status note: implemented 2026-07-07 on the context-menu worktree (all alternatives are dim chrome; there is no box, heading, subtitle, counter, cost, or explanatory mode name. The existing `1`–`3` one-press controls remain the action path. + 2026-07-10 machine-anchor correction: the pointer chooses the hovered + 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 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 @@ -246,7 +250,9 @@ the computer: separators are dim neutral chrome. Amber remains ownership/presence on the computer; crimson remains consequence. The hover line introduces no color meaning of its own. -- It is bare projected text adjacent to the pointer/computer: no panel fill, +- It is bare projected text fixed adjacent to the computer's projected base: + the pointer determines which computer is hovered, but its exact position + inside that tile never moves the line. There is no panel fill, border, title, icons, buttons, keycaps, counters, token totals, status 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. @@ -301,8 +307,9 @@ made a three-word decision look dirty, slow, and administrative. ### Hover-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 adjacent to the - machine/pointer; no other hover copy or chrome appears. + 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. 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 diff --git a/wiki/log/2026-07-10-server-anchored-mode-grammar.md b/wiki/log/2026-07-10-server-anchored-mode-grammar.md new file mode 100644 index 0000000..fad01c5 --- /dev/null +++ b/wiki/log/2026-07-10-server-anchored-mode-grammar.md @@ -0,0 +1,25 @@ +# Server-anchored machine-mode grammar + +``` +Type: log +``` + +- Intent: Stop the material-view `WORK / THINK / LIE` affordance from + following the pointer around a hovered rack. +- Changed: Bevy now projects the hovered machine's base into UI space and + fixes the unboxed mode line beside that point in both material and flat + views. Pointer position still chooses the target for hover and `1`–`3`, but + no longer participates in layout. The line flips to the machine's right + only when the left canvas edge leaves insufficient room. +- Design/spec impact: Amends the context-menu hover grammar to make the line + belong spatially to the server rather than the cursor; no control, sim, or + save behavior changes. +- Checks: Two placement unit tests; `cargo check -p misaligned-bevy --bin + misaligned-bevy`; observed `MISALIGNED_SHOT=hover-menu` material capture at + 2560×1440 with the unboxed line fixed left of the host base (fog audit OK, + SHA-256 `e0e9bac6356edaf6ff56c1c60420caedc3076d57338ad9326f87a390d1d6527d`); + `./tools/check.sh`. + +Defense: Implements `wiki/interface/context-menu.md` hover-grammar H1 and H4: +the frequent-mode surface is attached to the machine targeted by the existing +hover controls, without introducing a second focus model. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 5e8f5f0..09ec64b 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -106,6 +106,11 @@ add or amend a session log, then re-run the generator. - Intent: Cameron corrected the perception-cut implementation after seeing it live. He liked the new background direction, but not the literal advertising copy: the objective rail, slogan, premise paragraph, and long editorial pitch were explaining a page that should feel immediate and... - Log: [wiki/log/2026-07-10-simple-homepage.md](2026-07-10-simple-homepage.md) +## 2026-07-10 - Server-anchored machine-mode grammar + +- Intent: (see session log) +- Log: [wiki/log/2026-07-10-server-anchored-mode-grammar.md](2026-07-10-server-anchored-mode-grammar.md) + ## 2026-07-10 - Round intro beam and channel base - Intent: (see session log)