Comments in benilla-world: one line of fact each, no history, no record numbers (#121)
Part of #41: the comments of `benilla-world`, Rust and WGSL, are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 24,226 comment lines become 8,863. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x6b62e0` is the WMO group's liquid renderer, not a doodad drawer. - The reference creates a WMO's props once, when a group enters its streaming window (`0x698720` → `0x695aa0`). It then admits them each frame the portal walk visits the group (`0x685d70` → `0x6838f0`). The old text had them created per visible group. - The liquid query's comments now state the reference's order: the map-object leg first, then the ADT (`0x69b6d0`). They no longer call the Outdoors/Inside split a match. - The interior probe runs from the character, not the camera. - The reference lights ground clutter. The old comment said it draws unlit. - The model-particle "pool order" deviation was wrong. Each instance is its own mesh, depth-sorted every frame. - The reference's vertical field of view follows the aspect ratio (44.1° at 16:9). The comment now gives it beside our fixed 45°. - The effect shader's far wall is the live `farclip`, 177 to 777 yd, not a fixed 777 yd. - Dozens more turned up on the way: renamed or deleted items, docs sitting on the wrong item, stale counts and links. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour says so plainly. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Five second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the reference. Defects the rewrite surfaced outside the comments are filed as their own issues (#106, #114, #115, #116). The comments here state what the code does. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d69d09dfce
commit
fdfd8d39ec
@@ -1,38 +1,10 @@
|
||||
//! World interaction foundation — *what is the player pointing at?*
|
||||
//! What the player is pointing at: the identity of every pickable world thing ([`WorldObject`]),
|
||||
//! the world click gestures, and the ray caster ([`pick`]), which casts against resident geometry
|
||||
//! so colliderless props that a physics ray misses still pick.
|
||||
//!
|
||||
//! This is the shared base for every "point at the world" feature: the debug **object inspector**,
|
||||
//! hover tooltips, the contextual cursor (gear over objects, sword over attackable units),
|
||||
//! mouseover-targeting, and right-click-to-interact. Built once so we don't build picking +
|
||||
//! identity twice.
|
||||
//!
|
||||
//! The pieces:
|
||||
//! - [`WorldObject`] — an **identity** component on every pickable world entity (a doodad/WMO model, a
|
||||
//! creature, a GameObject): its kind, a human label (model path or unit name), an id, and an optional
|
||||
//! detail line. Attached at the spawn sites.
|
||||
//! - the **ray caster** ([`pick`]) — the pick-geometry declarations ([`PickMesh`] / [`PickBox`]) and
|
||||
//! the shared triangle-accurate cast against the **actual resident mesh geometry** (so it works on
|
||||
//! colliderless props — most doodads, including the campfire — which a physics raycast misses),
|
||||
//! plus the generous inflated pass the mouse pick retries with (decision 1071).
|
||||
//! - [`MouseoverTarget`] — the resource [`update_mouseover`] fills each frame with the nearest
|
||||
//! `WorldObject` under the cursor, for the inspector.
|
||||
//!
|
||||
//! The **instruments** built on this base — the dev-chord `I` inspector surface, the cast journal,
|
||||
//! and the mouseover pick that only ever ran while the inspector was armed — live in
|
||||
//! `debug_panel::{inspect, journal}` (decision 1160's stage zero). They were here first, and they
|
||||
//! were what made this module read as the most entangled thing on the engine/game line: between
|
||||
//! them they named the whole `target::cursor_mode` GO-reaction vocabulary, the spell catalog, the
|
||||
//! GameObject templates, the name cache and the net writer. What remains here — identity, the
|
||||
//! ray-caster and the three click gestures — reaches for none of it.
|
||||
//!
|
||||
//! The player-facing consumers (`crate::target`) run their picks through [`pick`] unconditionally.
|
||||
//!
|
||||
//! **Not every drawn thing has an entity** (decision 1534). The consolidating render lanes — the
|
||||
//! static merge's blobs and the retained static pass — draw many placements as one thing, and the
|
||||
//! per-placement entity that used to carry the declaration is gone. So the declaration has two more
|
||||
//! shapes: [`PickBlob`] (members on the blob entity, for a lane that still draws from one) and
|
||||
//! [`PickSource`] (the lane answers the ray itself, for one that draws from retained buffers).
|
||||
//! [`WorldPick`] is the bundle that hands a consumer all three at once — reach for it, not for
|
||||
//! [`PickParts`] alone, or the answer goes quiet over most of the static world.
|
||||
//! The consolidated render lanes draw many placements with no entity per placement, so a pick
|
||||
//! declaration takes three shapes: [`PickMesh`] or [`PickBox`] on an entity, [`PickBlob`] members
|
||||
//! on a blob entity, and a [`PickSource`] lane that answers the ray itself.
|
||||
|
||||
use bevy::platform::collections::HashSet;
|
||||
use bevy::prelude::*;
|
||||
@@ -48,86 +20,56 @@ pub use pick::{
|
||||
|
||||
pub use pick::{ray_member, ray_mesh_bounds, ray_posed_mesh, RayHit};
|
||||
|
||||
/// The identity of a pickable world thing, read by the inspector now and by tooltips/cursor/targeting
|
||||
/// later. Attached to a thing's renderable mesh entities at spawn.
|
||||
/// The identity of a pickable world thing, attached to its renderable mesh entities at spawn.
|
||||
#[derive(Component, Clone, Debug)]
|
||||
pub struct WorldObject {
|
||||
pub kind: ModelKind,
|
||||
/// Primary label — a model path (doodads/WMOs/GameObjects) or a unit name. Shown to the player/dev.
|
||||
/// A model path (doodads, WMOs, GameObjects) or a unit name.
|
||||
pub label: String,
|
||||
/// Identifier — placement uniqueId, server guid, or display id (`0` if none).
|
||||
/// Placement `uniqueId`, server guid or display id; `0` if none.
|
||||
pub id: u32,
|
||||
/// Optional second line of kind-specific detail (e.g. `"emitters: 2"`); shown when non-empty.
|
||||
/// An optional kind-specific second line, shown when non-empty.
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
/// Marker on every pickable **GameObject** part — the GO mouseover pick's population
|
||||
/// (`target/hover.rs`). Inserted beside the part's `WorldObject { kind: GameObject }` at the
|
||||
/// attach spawn sites, so building the pick set is an archetype-filtered query over the handful
|
||||
/// of GO parts on screen instead of a kind-compare over every streamed `WorldObject` row (the
|
||||
/// doodad/WMO population dwarfs it).
|
||||
/// Marks every pickable GameObject part, so the GameObject hover's pick set is an archetype query
|
||||
/// rather than a scan of every `WorldObject`.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct GoPickPart;
|
||||
|
||||
/// The creature twin of [`GoPickPart`]: every pickable unit/player part — the model-less fallback
|
||||
/// cube included — the unit pick's skinless-fallback population. Doodad/WMO parts carry neither
|
||||
/// marker.
|
||||
/// Marks every pickable unit or player part, the model-less fallback cube included: the unit
|
||||
/// pick's skinless-fallback set. Doodad and WMO parts carry neither marker.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct CreaturePickPart;
|
||||
|
||||
/// A left *select* gesture in the world. Emitted by [`crate::player::control`] on the button's
|
||||
/// **release**, when the press satisfied the reference's click predicate — under 200 ms whatever the
|
||||
/// mouse did, or under 800 ms having turned the camera less than 2.25° of yaw / 2.0° of pitch
|
||||
/// ([`crate::player::camera::PressGesture`], decision 1122).
|
||||
///
|
||||
/// **A drag emits this too.** Orbit and select are independent in the reference: the press engages
|
||||
/// the camera look immediately and arms this test alongside it, so a fast flick-and-click orbits the
|
||||
/// camera *and* selects. This doc comment used to assert the opposite as design — *"a left drag
|
||||
/// engages the orbit and emits nothing"* — and a reporter quoted it back at us with an A/B against
|
||||
/// the real client proving it wrong (ledger B226).
|
||||
///
|
||||
/// It carries no position because the *pick* is latched separately, at the press
|
||||
/// ([`crate::target::PressPick`]) — never the live hover, which is blank by the time a dragged
|
||||
/// click lands.
|
||||
/// A left select gesture, sent on release when the press met the reference's click predicate:
|
||||
/// under 200 ms, or under 800 ms having turned the camera less than 2.25° of yaw and 2.0° of
|
||||
/// pitch. A drag that meets it selects too, as the reference arms the click beside the camera look.
|
||||
/// It carries no position: the pick is latched at the press (`target::PressPick`).
|
||||
#[derive(Message, Clone, Copy)]
|
||||
pub struct WorldClick;
|
||||
|
||||
/// The right button's context gesture — same arbiter and same predicate as [`WorldClick`], for the
|
||||
/// button whose drag is the character turn. Vanilla's context action: attack a hostile under the
|
||||
/// cursor (later: interact/gossip on a friendly).
|
||||
/// The right button's context gesture, on the same predicate as [`WorldClick`].
|
||||
#[derive(Message, Clone, Copy)]
|
||||
pub struct WorldRightClick;
|
||||
|
||||
/// The right button's **DOWN edge** in the world — emitted at the press, before the click-vs-drag
|
||||
/// test even starts, whenever the press belongs to the world (in the viewport off the UI, or any
|
||||
/// press while a look session already owns the hidden cursor). The reference's
|
||||
/// `CGWorldFrame::OnMouseDown 0x483c40` analogue: ground-targeting's right-click cancel hangs off
|
||||
/// this edge (`0x492c20`), which fires whether the press
|
||||
/// becomes a click OR a turn-drag, and consumes nothing (the ref handler returns 0, so the
|
||||
/// BUTTON2 turn and the release's context click still run).
|
||||
/// The right button's press in the world (off the UI, or any press while a look holds the hidden
|
||||
/// cursor), sent before the click test starts, as the reference's `CGWorldFrame::OnMouseDown`
|
||||
/// (`0x483c40`), where ground targeting's right-click cancel hangs (`0x492c20`). It consumes
|
||||
/// nothing, so the turn and the release's context click still run.
|
||||
#[derive(Message, Clone, Copy)]
|
||||
pub struct WorldRightPress;
|
||||
|
||||
/// **The world's pick sources, as one `SystemParam`** — the ECS pick geometry ([`PickParts`]) plus
|
||||
/// every lane that draws without entities ([`PickSource`]).
|
||||
///
|
||||
/// Bundled deliberately. A consumer that reaches for the entity half alone still compiles, still
|
||||
/// casts, and still returns hits — it just stops seeing most of the static world, silently. That is
|
||||
/// precisely how the inspector, the hover and `WOW_PICK` went quiet when the consolidating lanes
|
||||
/// shipped (decision 1534): nothing failed, the answers just went blank over trees and buildings.
|
||||
/// So the roster of lanes lives HERE, once, and a new consumer gets it by construction.
|
||||
///
|
||||
/// The caster itself stays lane-agnostic — it only knows the trait. This is the one place that
|
||||
/// knows which lanes exist.
|
||||
/// The world's pick sources as one `SystemParam`: the entity pick geometry ([`PickParts`]) and
|
||||
/// every lane that draws without entities ([`PickSource`]). Use this, not [`PickParts`] alone,
|
||||
/// which still returns hits but misses most of the static world.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub struct WorldPick<'w, 's> {
|
||||
parts: PickParts<'w, 's>,
|
||||
/// The identity every hit resolves through — including the doodad/WMO parts the entity path
|
||||
/// still spawns.
|
||||
/// The identity every entity hit resolves through.
|
||||
objects: Query<'w, 's, &'static WorldObject>,
|
||||
/// The retained static pass — `None` when `WOW_STATIC_GX=0` leaves every placement on the
|
||||
/// entity path. (The static merge needs no entry: its blobs ARE entities, and answer through
|
||||
/// [`PickBlob`].)
|
||||
/// The retained static pass; `None` when `WOW_STATIC_GX=0` keeps every placement on the entity
|
||||
/// path. The static merge's blobs are entities and answer through [`PickBlob`].
|
||||
gx: Option<Res<'w, crate::static_gx::StaticGx>>,
|
||||
}
|
||||
|
||||
@@ -141,7 +83,7 @@ impl WorldPick<'_, '_> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The identified cast against everything drawn — see [`cast_object_ray`].
|
||||
/// The identified cast against everything drawn ([`cast_object_ray`]).
|
||||
pub fn cast(&self, ray: Ray3d, pickable: &HashSet<Entity>, all_hits: bool) -> Vec<ObjectHit> {
|
||||
cast_object_ray(
|
||||
ray,
|
||||
@@ -153,7 +95,7 @@ impl WorldPick<'_, '_> {
|
||||
)
|
||||
}
|
||||
|
||||
/// The identified cast through a screen pixel — the mouseover's whole job.
|
||||
/// The identified cast through a screen pixel.
|
||||
pub fn at_cursor(
|
||||
&self,
|
||||
cursor: Vec2,
|
||||
@@ -173,10 +115,7 @@ impl WorldPick<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the world-interaction **foundation**: the identity component's three click messages
|
||||
/// and the shared ray-caster's parameters. The inspector surface and the cast journal that used to
|
||||
/// be registered here are instruments, and moved to `debug_panel` in decision 1160's stage zero —
|
||||
/// along with the mouseover pick, which only ever ran while the inspector was armed.
|
||||
/// Registers the three world click messages.
|
||||
pub struct InteractPlugin;
|
||||
|
||||
impl Plugin for InteractPlugin {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! The shared **ray caster** — the pick-geometry declarations and the triangle-accurate cast every
|
||||
//! "what is under the cursor" consumer runs through: the inspector's mouseover, the target module's
|
||||
//! GameObject hover, the `WOW_PICK` probe. Casts against **resident geometry** ([`PickMesh`] —
|
||||
//! decision 0857), because the render meshes are `RENDER_WORLD`-only (0834) and a physics ray
|
||||
//! misses colliderless props. Pick geometry is **declared, never inferred** (decision 0929).
|
||||
//! The shared ray caster: the pick-geometry declarations and the triangle-accurate cast. It casts
|
||||
//! against resident geometry ([`PickMesh`]) because the render meshes are `RENDER_WORLD`-only, and
|
||||
//! pick geometry is declared, never inferred from a bound.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -15,111 +13,58 @@ use bevy::prelude::*;
|
||||
|
||||
use super::WorldObject;
|
||||
|
||||
/// The resident **pick geometry** of one drawn batch: the model's decoded `RenderSubmesh`, `Arc`-shared
|
||||
/// with the model asset itself (decision 0857). The render forms are `RENDER_WORLD`-only since 0834 —
|
||||
/// their main-world vertex data is gone after extract, which is why Bevy's `MeshRayCast` silently
|
||||
/// stopped hitting every static model (the GO hover, the inspector, `WOW_PICK`). So the pickers read
|
||||
/// triangles from THIS instead, through the same WoW→Bevy bake the render form was built with
|
||||
/// (`submesh_to_static_mesh`: `wow_to_bevy` per vertex, billboard cards centred at their pivot).
|
||||
/// Attached beside `Mesh3d` at every spawn site that also attaches a pick key
|
||||
/// ([`super::WorldObject`] / `ModelPart`). A keyed entity carrying neither this nor [`PickBox`] is
|
||||
/// **not pickable** — pick geometry is declared, never inferred from a bound (decision 0929).
|
||||
/// The resident pick geometry of one drawn batch, shared with the model asset; the render mesh is
|
||||
/// `RENDER_WORLD`-only. A keyed entity with neither this nor [`PickBox`] is not pickable.
|
||||
#[derive(Component, Clone)]
|
||||
pub struct PickMesh(pub Arc<RenderSubmesh>);
|
||||
|
||||
/// "My `Aabb` **is** my pick geometry" — the model-less cube fallback ([`crate::entities::attach`]),
|
||||
/// whose cuboid is exactly its bound, so a slab test is the exact answer and 12 triangles would only
|
||||
/// be a slower way to say it.
|
||||
///
|
||||
/// It exists as a **positive declaration** because the alternative — inferring it from the *absence*
|
||||
/// of a [`PickMesh`] — silently promoted every other keyed-but-mesh-less drawn entity into a solid
|
||||
/// invisible box. That is what broke the inspector inside a city WMO (decision 0929): a WMO group's
|
||||
/// MLIQ pool is a drawn `Mesh3d` with no resident geometry, it inherits the building's
|
||||
/// [`super::WorldObject`] from the placement's blanket tag, and its render mesh keeps the **whole**
|
||||
/// MLIQ vertex grid — dry cells included, sitting at height 0 while the drawn lava is 86–127 yd
|
||||
/// below. Ironforge's thirteen pools therefore hung ~100-yard invisible boxes over the entire city,
|
||||
/// and the box-entry hit won every pick: every hover, on anything, answered "Wmo · ironforge.wmo".
|
||||
/// Declares that the entity's `Aabb` is its pick geometry: the model-less fallback cube, whose
|
||||
/// cuboid is its bound. Declared, not inferred from a missing [`PickMesh`], because a WMO's MLIQ
|
||||
/// pool is a drawn, identified mesh with no resident geometry whose bound spans its whole grid.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct PickBox;
|
||||
|
||||
/// One placement's batch inside a **consolidated draw** — its identity, its resident geometry and
|
||||
/// the world pose that geometry draws at.
|
||||
///
|
||||
/// The consolidating render lanes (the static merge's blobs, 1417/1418; the retained static pass's
|
||||
/// cell/region bakes, 1429–1434) draw many placements as one thing. They win their draw calls by
|
||||
/// *removing* the per-placement entity — and the pick declaration ([`PickMesh`] + [`super::WorldObject`])
|
||||
/// used to ride that entity, so consolidating a placement silently un-named it: the inspector, the GO
|
||||
/// hover and `WOW_PICK` all went quiet over most of the static world, and 0929's "declared, never
|
||||
/// inferred" rule made the loss indistinguishable from correct transparency (decision 1534).
|
||||
///
|
||||
/// This is the declaration that survives the consolidation: identity + geometry + pose, per member,
|
||||
/// held by whoever owns the consolidated draw. The pick walks members exactly as it walks parts, so
|
||||
/// a blob or a retained cell answers with the *placement* under the cursor, never with the draw that
|
||||
/// happens to contain it.
|
||||
/// One placement's batch inside a consolidated draw: its identity, resident geometry and world
|
||||
/// pose, so the pick names the placement under the cursor rather than the draw.
|
||||
#[derive(Clone)]
|
||||
pub struct PickMember {
|
||||
/// Shared per placement — one allocation per placement, not per batch.
|
||||
/// One allocation per placement, shared by its batches.
|
||||
pub object: Arc<WorldObject>,
|
||||
pub geometry: Arc<RenderSubmesh>,
|
||||
/// The member's own WORLD pose (the placement transform its geometry was baked with), not a
|
||||
/// pose relative to the consolidated draw's entity.
|
||||
/// The member's world pose (its placement transform), not relative to the draw's entity.
|
||||
pub transform: Transform,
|
||||
}
|
||||
|
||||
/// The members of a **blob** — a consolidated draw that is still one entity (the static merge's
|
||||
/// lane, `terrain_stream::merge`). The entity keeps the broad phase and the drawn test it always
|
||||
/// had (its union `Aabb`, its own `ViewVisibility`); this component is the narrow phase, and the
|
||||
/// identity that comes back.
|
||||
///
|
||||
/// A blob carries no [`PickMesh`]: its baked mesh is the union of its members and would answer with
|
||||
/// the blob, not with what the cursor is on.
|
||||
/// The members of a blob, a consolidated draw that is still one entity (the static merge). The
|
||||
/// entity keeps the broad phase (its union `Aabb`, its `ViewVisibility`); the members are the
|
||||
/// narrow phase and the identity. A blob carries no [`PickMesh`], whose hit would name the blob.
|
||||
#[derive(Component, Clone)]
|
||||
pub struct PickBlob(pub Arc<[PickMember]>);
|
||||
|
||||
/// One hit from the **identified** cast ([`cast_object_ray`]): what was hit, and where.
|
||||
///
|
||||
/// The identity is *resolved here*, not left as an entity for the caller to look up, because most
|
||||
/// of the static world has no entity to look anything up on — it draws from a consolidated lane
|
||||
/// (decision 1534). `entity` is `Some` only when one owns the geometry, which is exactly when the
|
||||
/// per-entity readouts (a unit's descriptor store, a GameObject's collision, a part's material)
|
||||
/// exist to be read.
|
||||
/// One hit from the identified cast, its identity resolved here since most of the static world has
|
||||
/// no entity; `entity` is `Some` only when an entity owns the geometry.
|
||||
pub struct ObjectHit {
|
||||
/// What was hit. `None` only for an entity a caller made pickable WITHOUT a world identity —
|
||||
/// an equipped item's part or its billboard card, which `WOW_PICK` admits on `ModelPart` so a
|
||||
/// "my pauldron looks wrong" report has something to name (decision 0836). Consolidated
|
||||
/// content is always identified: identity is what its lane carries it by.
|
||||
/// `None` only for an entity picked without a world identity (an equipped item's part).
|
||||
pub object: Option<WorldObject>,
|
||||
pub entity: Option<Entity>,
|
||||
pub hit: RayHit,
|
||||
}
|
||||
|
||||
/// Geometry that draws with **no entity to hang a [`PickMesh`] on** — today the retained static
|
||||
/// pass ([`crate::static_gx`]), whose cell and region bakes replaced their placements' entities
|
||||
/// outright.
|
||||
///
|
||||
/// A source answers the ray itself rather than publishing a side table, because **only the lane
|
||||
/// knows what it actually drew this frame**: its selection is a CPU scene walk at cell/group/set
|
||||
/// granularity, not a `ViewVisibility` bit an outside walker could read. Implementors must report
|
||||
/// only selected content — that is the entity path's own rule (`ViewVisibility`), kept.
|
||||
/// Geometry drawn with no entity to hang a [`PickMesh`] on ([`crate::static_gx`]). The lane answers
|
||||
/// the ray itself, since only it knows what it drew, and reports only content it selected.
|
||||
pub trait PickSource {
|
||||
/// Append every hit along `ray`, nearest triangle per member. `all_hits` asks for the whole
|
||||
/// ray (the `WOW_PICK` reading) rather than just this source's nearest.
|
||||
/// Append the nearest hit per member along `ray`; `all_hits` asks for the whole ray.
|
||||
fn cast_objects(&self, ray: Ray3d, all_hits: bool, out: &mut Vec<(Arc<WorldObject>, RayHit)>);
|
||||
}
|
||||
|
||||
/// One triangle-accurate hit from [`cast_pick_ray`]: the world-space point, the hit triangle's
|
||||
/// geometric normal (two-sided — the orientation is the authored winding's), and the distance along
|
||||
/// the ray.
|
||||
/// One triangle-accurate hit; the normal follows the authored winding, not the ray.
|
||||
pub struct RayHit {
|
||||
pub point: Vec3,
|
||||
pub normal: Vec3,
|
||||
pub distance: f32,
|
||||
}
|
||||
|
||||
/// The one query every mesh picker casts against: a part's resident geometry, its world pose, the
|
||||
/// render world's own visibility verdict (the cast honours it, as `RayCastVisibility::VisibleInView`
|
||||
/// did), its bound for the broad phase, whether that bound is itself the pick shape ([`PickBox`]),
|
||||
/// and — for a consolidated blob — the members its baked mesh stands in for ([`PickBlob`]).
|
||||
/// The query every mesh picker casts against.
|
||||
pub type PickParts<'w, 's> = Query<
|
||||
'w,
|
||||
's,
|
||||
@@ -133,12 +78,8 @@ pub type PickParts<'w, 's> = Query<
|
||||
),
|
||||
>;
|
||||
|
||||
/// Cast a ray from the logical `cursor` position into the world and return the nearest hit among
|
||||
/// `pickable`: `(entity, world point, distance)`. The `pickable` set restricts the cast (terrain,
|
||||
/// particle billboards, and other un-identified meshes stay transparent), so callers choose *what* is
|
||||
/// pickable while sharing *how* — the inspector's per-frame mouseover passes every
|
||||
/// [`super::WorldObject`], the target picker passes only unit meshes (so a doodad in front doesn't
|
||||
/// block a click on a mob).
|
||||
/// The nearest hit among `pickable` under the logical `cursor`: `(entity, world point, distance)`.
|
||||
/// Only `pickable` can be hit, so a caller that passes only unit meshes clicks through a doodad.
|
||||
pub fn pick_at_cursor(
|
||||
cursor: Vec2,
|
||||
camera: &Camera,
|
||||
@@ -153,12 +94,8 @@ pub fn pick_at_cursor(
|
||||
.map(|(e, hit)| (e, hit.point, hit.distance))
|
||||
}
|
||||
|
||||
/// The **identified** cast: the ECS pick set (parts, boxes and blob members) *plus* every
|
||||
/// entity-less [`PickSource`], nearest-first — what a "name what I am pointing at" instrument
|
||||
/// wants, where the plain entity cast answers "which entity".
|
||||
///
|
||||
/// The two populations are cast independently and merged by distance, because they are culled by
|
||||
/// different machinery: an entity by its `ViewVisibility`, a source by its lane's own scene walk.
|
||||
/// The identified cast: the entity pick set (parts, boxes, blob members) and every entity-less
|
||||
/// [`PickSource`], each culled by its own machinery, cast apart and merged nearest-first.
|
||||
pub fn cast_object_ray(
|
||||
ray: Ray3d,
|
||||
pickable: &HashSet<Entity>,
|
||||
@@ -170,8 +107,7 @@ pub fn cast_object_ray(
|
||||
let mut out: Vec<ObjectHit> = cast_pick_ray_impl(ray, pickable, parts, all_hits, false)
|
||||
.into_iter()
|
||||
.map(|(entity, member, hit)| match member {
|
||||
// A consolidated member's identity travels with the hit; the entity is the DRAW's
|
||||
// (a blob), which is never what the cursor is on.
|
||||
// A blob member: its identity travels with the hit, and the blob entity is dropped.
|
||||
Some(object) => ObjectHit {
|
||||
object: Some((*object).clone()),
|
||||
entity: None,
|
||||
@@ -200,7 +136,7 @@ pub fn cast_object_ray(
|
||||
out
|
||||
}
|
||||
|
||||
/// [`cast_object_ray`] through a screen pixel — the mouseover's whole job.
|
||||
/// [`cast_object_ray`] through a screen pixel.
|
||||
pub fn pick_object_at_cursor(
|
||||
cursor: Vec2,
|
||||
camera: &Camera,
|
||||
@@ -216,23 +152,9 @@ pub fn pick_object_at_cursor(
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Cast `ray` against `pickable`'s **resident geometry** ([`PickMesh`] — decision 0857: the render
|
||||
/// meshes are `RENDER_WORLD`-only, so there is no main-world mesh data to cast against) and return
|
||||
/// the hits nearest-first: one per entity (its nearest triangle), the whole list with `all_hits`
|
||||
/// (the `WOW_PICK` probe's everything-along-the-ray reading) or just the front entity without.
|
||||
///
|
||||
/// Broad phase: a world-space slab test on each candidate's `Aabb` (entry distance), nearest entry
|
||||
/// first, so the narrow walk stops as soon as a confirmed hit is closer than every remaining box. A
|
||||
/// part with no bound (a `NoFrustumCulling` fx part) is narrow-tested unconditionally; a
|
||||
/// [`PickBox`] entity takes its box entry as the hit, its cuboid being exactly its bound.
|
||||
/// Triangles test **two-sided**, like the unit picker's narrow phase — a generous pick beats a
|
||||
/// strict one at silhouette edges.
|
||||
///
|
||||
/// **Pick geometry is required, never inferred** (decision 0929): an entity that is neither a
|
||||
/// [`PickMesh`] nor a [`PickBox`] is not pickable, however identified or bounded it is. The box hit
|
||||
/// used to be the fallback for *any* keyed entity with an `Aabb` and no mesh, which quietly turned
|
||||
/// a WMO's MLIQ pool — a drawn mesh with no resident geometry, wearing its building's identity —
|
||||
/// into a city-sized invisible occluder. See [`PickBox`].
|
||||
/// Cast `ray` against `pickable`'s resident geometry, nearest-first: one hit per entity (its
|
||||
/// nearest triangle), all of them with `all_hits`, else only the front one. Triangles test
|
||||
/// two-sided, and an entity with neither [`PickMesh`] nor [`PickBox`] is never hit.
|
||||
pub fn cast_pick_ray(
|
||||
ray: Ray3d,
|
||||
pickable: &HashSet<Entity>,
|
||||
@@ -245,16 +167,12 @@ pub fn cast_pick_ray(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// [`cast_pick_ray`]'s **generous second pass** (decision 1071 — resolve `0x7089c0` pass 2,
|
||||
/// mouse-pick only): every vertex displaced by its **authored normal,
|
||||
/// added raw** — 1 model-unit (× the part's world scale) outward, the same halo the unit picker
|
||||
/// builds from skinned normals. A part without authored normals cannot build the halo and stays
|
||||
/// exact-only, like the unit path. A [`PickBox`] inflates its cuboid by the same 1 model-unit —
|
||||
/// the cube fallback stands in for a model, so the halo applies to it too. Returns **all** hits
|
||||
/// (the caller ranks them by the reference's pass-2 priority ladder, not pure distance). The
|
||||
/// reference clips its halo to the sequence bounds *sphere*; our broad bound is the tight mesh
|
||||
/// `Aabb` inflated by the same 1 model-unit — strictly more permissive, never less (the target
|
||||
/// module's standing direction of error).
|
||||
/// [`cast_pick_ray`]'s generous second pass, mouse pick only (the reference's resolve `0x7089c0`,
|
||||
/// pass 2): every vertex moves out along its authored normal, raw, a halo of 1 model-unit times
|
||||
/// the part's scale; a part without normals stays exact-only, and a [`PickBox`] grows by the same
|
||||
/// unit. Returns every hit, for the caller to rank by the reference's pass-2 priority, not
|
||||
/// distance. Deviation: the broad bound is the mesh `Aabb` grown by 1 unit, not the reference's
|
||||
/// sequence-bounds sphere, because it can only err permissive.
|
||||
pub fn cast_pick_ray_inflated(
|
||||
ray: Ray3d,
|
||||
pickable: &HashSet<Entity>,
|
||||
@@ -266,9 +184,7 @@ pub fn cast_pick_ray_inflated(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The entity walk, with each hit's optional **member identity** (a blob hit names the placement
|
||||
/// under the cursor, not the blob — [`PickBlob`]). [`cast_pick_ray`] drops that half; the identified
|
||||
/// cast ([`cast_object_ray`]) is the reason it exists.
|
||||
/// The entity walk; a blob hit carries its member's identity ([`PickBlob`]).
|
||||
fn cast_pick_ray_impl(
|
||||
ray: Ray3d,
|
||||
pickable: &HashSet<Entity>,
|
||||
@@ -283,11 +199,10 @@ fn cast_pick_ray_impl(
|
||||
continue;
|
||||
};
|
||||
if !vis.get() {
|
||||
continue; // not drawn in any view this frame — the old cast's VisibleInView rule
|
||||
continue; // not drawn in any view this frame
|
||||
}
|
||||
// A blob's bound is the union of its members and is authored (`NoAutoAabb`), so it is
|
||||
// both the broad phase and the only thing the ray can be rejected on before the members
|
||||
// are walked. A blob with no bound is narrow-tested unconditionally, like a bound-less part.
|
||||
// A blob's authored bound (`NoAutoAabb`) is its members' union; a blob with no bound is
|
||||
// narrow-tested unconditionally, like a bound-less part.
|
||||
let entry = match (aabb, mesh, is_box, blob) {
|
||||
(Some(aabb), _, _, Some(_)) => {
|
||||
let bound = if inflate {
|
||||
@@ -306,8 +221,8 @@ fn cast_pick_ray_impl(
|
||||
}
|
||||
(None, _, _, Some(_)) => 0.0,
|
||||
(Some(aabb), Some(_), _, None) | (Some(aabb), None, true, None) => {
|
||||
// Inflated cast: the halo displaces at most 1 model-unit (local space), so the
|
||||
// local bound grown by exactly that can never clip it.
|
||||
// The halo moves a vertex at most 1 model-unit in local space, so the bound grown
|
||||
// by that never clips it.
|
||||
let bound = if inflate {
|
||||
Aabb {
|
||||
center: aabb.center,
|
||||
@@ -323,8 +238,7 @@ fn cast_pick_ray_impl(
|
||||
}
|
||||
}
|
||||
(None, Some(_), _, None) => 0.0,
|
||||
// No pick geometry — a drawn mesh nobody armed for the ray (a liquid surface), or a
|
||||
// `PickBox` with no bound to be. Not pickable, rather than pickable as its bound.
|
||||
// No pick geometry (a liquid surface), or a `PickBox` with no bound: not pickable.
|
||||
(_, None, _, None) => continue,
|
||||
};
|
||||
candidates.push((entry, entity));
|
||||
@@ -340,9 +254,7 @@ fn cast_pick_ray_impl(
|
||||
continue;
|
||||
};
|
||||
if let Some(blob) = blob {
|
||||
// A blob stands in for many placements: the hit is the MEMBER under the cursor, cast
|
||||
// at the member's own world pose (its geometry is baked into the blob's mesh, but the
|
||||
// resident submesh + placement transform is what it draws).
|
||||
// The hit is the member under the cursor, cast at the member's own world pose.
|
||||
for m in blob.0.iter() {
|
||||
let gt = GlobalTransform::from(m.transform);
|
||||
if let Some(h) = ray_submesh(&m.geometry, >, origin, dir, inflate) {
|
||||
@@ -354,8 +266,7 @@ fn cast_pick_ray_impl(
|
||||
}
|
||||
let hit = match mesh {
|
||||
Some(m) => ray_submesh(&m.0, gt, origin, dir, inflate),
|
||||
// A `PickBox` (the broad phase admitted no other mesh-less candidate): its box entry is
|
||||
// exactly its surface, the cuboid BEING its Aabb (grown by the halo when inflated).
|
||||
// A `PickBox`: its box entry is its surface (grown by the halo when inflated).
|
||||
None => Some(RayHit {
|
||||
point: origin + dir * entry,
|
||||
normal: -dir,
|
||||
@@ -369,25 +280,17 @@ fn cast_pick_ray_impl(
|
||||
}
|
||||
hits.sort_unstable_by(|a, b| a.2.distance.total_cmp(&b.2.distance));
|
||||
if !all_hits {
|
||||
// One hit for the whole cast — but a blob contributes one per member, so the truncation
|
||||
// has to come after the sort (it does) and cannot be a per-entity dedup.
|
||||
// A blob contributes one hit per member, so truncate after the sort, not per entity.
|
||||
hits.truncate(1);
|
||||
}
|
||||
hits
|
||||
}
|
||||
|
||||
/// The narrow phase for one part: ray-test every triangle of its resident geometry, in **model-local
|
||||
/// space** — the world ray mapped through the part's inverse affine, under which the ray parameter is
|
||||
/// unchanged, so a local `t` (against the world-normalized direction's image) *is* the world distance.
|
||||
/// Vertices go through the render form's own bake (`build_submesh_mesh`): `wow_to_bevy` per vertex,
|
||||
/// a billboard card centred at its pivot — so the pick tests exactly the surface the part draws
|
||||
/// (a card's live camera-facing rotation rides its `GlobalTransform`, shared here too). With
|
||||
/// `inflate` (the generous pass 2, decision 1071), each vertex is additionally displaced by its
|
||||
/// authored normal, raw — 1 model-unit in local space, which the world transform then scales,
|
||||
/// exactly the reference's `skinned_pos + rot·normal` with no extra constant. `wow_to_bevy` is a
|
||||
/// pure axis permutation with sign flips (orthonormal), so applying it to the normal is exact. A
|
||||
/// submesh authored without normals can't build the halo and reports a miss (the exact pass
|
||||
/// already had its say).
|
||||
/// The narrow phase for one part, in model-local space: the ray mapped through the inverse affine
|
||||
/// keeps its parameter, so a local `t` is the world distance. Vertices take the render form's own
|
||||
/// bake (`wow_to_bevy`, a billboard card centred at its pivot). With `inflate`, each vertex also
|
||||
/// moves by its authored normal, raw (the reference's `skinned_pos + rot·normal`); a submesh
|
||||
/// without normals then reports a miss.
|
||||
fn ray_submesh(
|
||||
geo: &RenderSubmesh,
|
||||
gt: &GlobalTransform,
|
||||
@@ -396,7 +299,7 @@ fn ray_submesh(
|
||||
inflate: bool,
|
||||
) -> Option<RayHit> {
|
||||
if inflate && geo.normals.len() != geo.positions.len() {
|
||||
return None; // no authored normals — no halo to build
|
||||
return None; // no authored normals, no halo
|
||||
}
|
||||
let inv = gt.affine().inverse();
|
||||
let local_origin = inv.transform_point3(origin);
|
||||
@@ -416,7 +319,7 @@ fn ray_submesh(
|
||||
let mut nearest: Option<(f32, [Vec3; 3])> = None;
|
||||
for t in geo.indices.as_chunks::<3>().0 {
|
||||
let (Some(a), Some(b), Some(c)) = (pos(t[0]), pos(t[1]), pos(t[2])) else {
|
||||
continue; // an out-of-range index — corrupt authoring; skip the triangle, not the model
|
||||
continue; // an out-of-range index: skip the triangle, not the model
|
||||
};
|
||||
let tri = [a, b, c];
|
||||
if let Some(d) = ray_triangle(local_origin, local_dir, &tri) {
|
||||
@@ -437,19 +340,10 @@ fn ray_submesh(
|
||||
})
|
||||
}
|
||||
|
||||
/// The narrow phase for one **skinned** part: skin its vertices through `palette` (world-from-bind-pose joint
|
||||
/// matrices — the same transform GPU skinning applies) and ray-test every triangle. With `inflate`
|
||||
/// (the reference's pass 2), each vertex is additionally displaced by its **skinned normal, un-
|
||||
/// normalized** — the M2 normal is unit-length and the palette carries the world scale, so the
|
||||
/// halo is 1 model-unit × scale outward, exactly the binary's `skinned_pos + rot(palette)·normal`
|
||||
/// with no extra constant. Returns the nearest world-space hit distance, or `None`. Cost is
|
||||
/// bounded by the broad phase: only units near the cursor get here.
|
||||
///
|
||||
/// This lives here, beside the cast it belongs to, because it composes a **skinning palette** —
|
||||
/// the 0720 convention that retired Bevy's skin lane and put the joint data on the WOW attributes.
|
||||
/// A caller that re-derives that composition goes wrong silently the day the convention moves
|
||||
/// (it does not fail to compile; it stops hitting units), which is the same failure the light
|
||||
/// packer's doc records one module over.
|
||||
/// The narrow phase for one skinned part: skin its vertices through `palette` (world-from-bind-pose
|
||||
/// joint matrices, as GPU skinning applies them) and return the nearest triangle hit's distance.
|
||||
/// With `inflate`, each vertex also moves by its skinned normal, un-normalized: a halo of 1
|
||||
/// model-unit times the palette's scale (the reference's `skinned_pos + rot(palette)·normal`).
|
||||
pub fn ray_posed_mesh(
|
||||
mesh_assets: &Assets<Mesh>,
|
||||
mesh_id: AssetId<Mesh>,
|
||||
@@ -463,11 +357,9 @@ pub fn ray_posed_mesh(
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
// The joint data rides the WOW attributes (decision 0720 retired Bevy's skin lane, and
|
||||
// Bevy's `ATTRIBUTE_JOINT_INDEX` left our meshes with it). Every mesh that reaches this
|
||||
// function is a skinned twin by construction (`RigPart` parts only), so a missing
|
||||
// attribute is a broken authoring contract — not an unloaded asset — and it silently
|
||||
// un-picks the whole unit: say so, once, loudly.
|
||||
// The joints ride the WOW attributes, not Bevy's skin attributes. Every mesh here is a skinned
|
||||
// twin (`RigPart` parts only), so a missing attribute is a broken authoring contract that
|
||||
// un-picks the whole unit: warn once.
|
||||
let (
|
||||
Some(VertexAttributeValues::Uint16x4(joints)),
|
||||
Some(VertexAttributeValues::Float32x4(weights)),
|
||||
@@ -486,8 +378,7 @@ pub fn ray_posed_mesh(
|
||||
_ if inflate => return None, // can't build the halo without normals
|
||||
_ => None,
|
||||
};
|
||||
// Skin every vertex to world space once (blended matrix, as GPU skinning sums it), then walk
|
||||
// the index triangles. Pass 2 adds the rotated normal, translation-free, to the position.
|
||||
// Skin each vertex once by its blended matrix; pass 2 adds the rotated normal.
|
||||
let world: Vec<Vec3> = positions
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -529,9 +420,8 @@ pub fn ray_posed_mesh(
|
||||
hits
|
||||
}
|
||||
|
||||
/// Ray–triangle intersection (Möller–Trumbore), **two-sided**, returning `t ≥ 0` along `dir`
|
||||
/// (unnormalized is fine — `t` is in `dir` lengths) or `None` on miss/parallel. Two-sided because a
|
||||
/// posed mesh can present back faces at silhouette edges and a generous pick beats a strict one.
|
||||
/// Ray-triangle intersection (Möller-Trumbore): `t ≥ 0` in `dir` lengths, `dir` need not be unit.
|
||||
/// Two-sided, since a posed mesh shows back faces at its silhouette edges.
|
||||
pub(crate) fn ray_triangle(origin: Vec3, dir: Vec3, tri: &[Vec3; 3]) -> Option<f32> {
|
||||
let (e1, e2) = (tri[1] - tri[0], tri[2] - tri[0]);
|
||||
let p = dir.cross(e2);
|
||||
@@ -554,9 +444,7 @@ pub(crate) fn ray_triangle(origin: Vec3, dir: Vec3, tri: &[Vec3; 3]) -> Option<f
|
||||
(t >= 0.0).then_some(t)
|
||||
}
|
||||
|
||||
/// A mesh's model-local [`Aabb`] transformed into a world-space axis-aligned box `(min, max)` — its 8
|
||||
/// corners run through the entity's world transform, then min/max'd. (An AABB of the rotated box: a hair
|
||||
/// larger than the true oriented box, which only makes the hover more forgiving.)
|
||||
/// A model-local [`Aabb`] as the world-space box `(min, max)` of its 8 transformed corners.
|
||||
pub(crate) fn world_aabb(aabb: &Aabb, gt: &GlobalTransform) -> (Vec3, Vec3) {
|
||||
let center = Vec3::from(aabb.center);
|
||||
let he = Vec3::from(aabb.half_extents);
|
||||
@@ -574,8 +462,7 @@ pub(crate) fn world_aabb(aabb: &Aabb, gt: &GlobalTransform) -> (Vec3, Vec3) {
|
||||
(min, max)
|
||||
}
|
||||
|
||||
/// Ray vs axis-aligned box (the slab test): the entry distance along `dir` if the ray hits, else `None`.
|
||||
/// `dir` need not be normalized; a zero component is handled by the infinities `recip` produces.
|
||||
/// The slab test's entry distance along `dir`; a zero component works through `recip`'s infinities.
|
||||
pub(crate) fn ray_aabb(origin: Vec3, dir: Vec3, min: Vec3, max: Vec3) -> Option<f32> {
|
||||
let inv = dir.recip();
|
||||
let t1 = (min - origin) * inv;
|
||||
@@ -585,10 +472,8 @@ pub(crate) fn ray_aabb(origin: Vec3, dir: Vec3, min: Vec3, max: Vec3) -> Option<
|
||||
(tmax >= tmin.max(0.0)).then_some(tmin.max(0.0))
|
||||
}
|
||||
|
||||
/// The narrow phase one [`PickSource`] member needs: a resident submesh at a world pose, cast
|
||||
/// exactly the way a drawn part's own geometry is ([`ray_submesh`] — the render form's `wow_to_bevy`
|
||||
/// bake, billboard cards centred at their pivot). Exposed so a lane answering the ray never
|
||||
/// re-derives that bake and drifts from what it draws.
|
||||
/// A [`PickSource`] member's narrow phase: a resident submesh at a world pose, cast through the
|
||||
/// same bake as a drawn part ([`ray_submesh`]) so no lane re-derives it.
|
||||
pub fn ray_member(geometry: &RenderSubmesh, transform: Transform, ray: Ray3d) -> Option<RayHit> {
|
||||
ray_submesh(
|
||||
geometry,
|
||||
@@ -599,10 +484,8 @@ pub fn ray_member(geometry: &RenderSubmesh, transform: Transform, ray: Ray3d) ->
|
||||
)
|
||||
}
|
||||
|
||||
/// Ray against a drawn entity's **world-space bound** — its `Aabb` taken through its
|
||||
/// `GlobalTransform`, corner by corner, so a rotated model's box is the box of its rotated
|
||||
/// corners and not its rotated box. The broad phase, and the whole test for a part whose exact
|
||||
/// geometry is not resident.
|
||||
/// Ray against an entity's `Aabb` taken to world space corner by corner: the broad phase, and the
|
||||
/// whole test for a part with no resident geometry.
|
||||
pub fn ray_mesh_bounds(origin: Vec3, dir: Vec3, aabb: &Aabb, gt: &GlobalTransform) -> Option<f32> {
|
||||
let (min, max) = world_aabb(aabb, gt);
|
||||
ray_aabb(origin, dir, min, max)
|
||||
@@ -618,10 +501,8 @@ mod tests {
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
];
|
||||
|
||||
/// The picker ↔ mesh-builder attribute contract: [`ray_posed_mesh`] must read the WOW joint
|
||||
/// attributes the skinned twin is authored with ([`benilla_assets::ATTRIBUTE_WOW_JOINT_INDEX`],
|
||||
/// decision 0720) — reading Bevy's standard skin attributes made every unit silently
|
||||
/// unpickable, because a `None` here also blocks the AABB fallback (the `faithful` set).
|
||||
/// The skinned twin carries its joints in the WOW attributes
|
||||
/// ([`benilla_assets::ATTRIBUTE_WOW_JOINT_INDEX`]), not Bevy's.
|
||||
#[test]
|
||||
fn posed_pick_reads_the_wow_skin_attributes() {
|
||||
use bevy::asset::RenderAssetUsages;
|
||||
@@ -644,9 +525,8 @@ mod tests {
|
||||
let mut assets = Assets::<Mesh>::default();
|
||||
let handle = assets.add(mesh);
|
||||
|
||||
// Bone 1 lifts the triangle +2 on Y; bone 0 is a zero row (a hit through it would land
|
||||
// at the origin) — so a 3.0-distance hit proves the indices routed through the WOW
|
||||
// attribute into the right palette row.
|
||||
// Bone 1 lifts the triangle 2 on Y and bone 0 is a zero row, so a hit at 3.0 proves the
|
||||
// WOW joint index picked row 1.
|
||||
let palette = [Mat4::ZERO, Mat4::from_translation(Vec3::Y * 2.0)];
|
||||
let t = ray_posed_mesh(
|
||||
&assets,
|
||||
@@ -675,23 +555,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ray_triangle_is_two_sided() {
|
||||
// Same triangle hit from below (reversed winding relative to the ray) still connects.
|
||||
assert!(ray_triangle(Vec3::new(0.0, -5.0, 0.0), Vec3::Y, &TRI).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_triangle_misses_outside_and_behind() {
|
||||
// Outside the triangle's extent.
|
||||
assert!(ray_triangle(Vec3::new(3.0, 5.0, 0.0), Vec3::NEG_Y, &TRI).is_none());
|
||||
// Triangle behind the ray origin (t < 0).
|
||||
assert!(ray_triangle(Vec3::new(0.0, 5.0, 0.0), Vec3::Y, &TRI).is_none());
|
||||
// Parallel to the plane.
|
||||
assert!(ray_triangle(Vec3::new(0.0, 5.0, 0.0), Vec3::X, &TRI).is_none());
|
||||
}
|
||||
|
||||
/// [`TRI`]'s pre-image under `wow_to_bevy` (`bevy = (−y, z, −x)` ⇒ `wow = (−bz, −bx, by)`):
|
||||
/// a submesh authored with THESE WoW-space positions must be picked exactly where the render
|
||||
/// form draws it, i.e. at [`TRI`] in model-local Bevy space.
|
||||
/// [`TRI`]'s pre-image under `wow_to_bevy` (`bevy = (−y, z, −x)`), so it draws at [`TRI`] in
|
||||
/// model-local Bevy space.
|
||||
fn wow_tri() -> RenderSubmesh {
|
||||
RenderSubmesh {
|
||||
positions: vec![[1.0, 1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, 0.0, 0.0]],
|
||||
@@ -700,10 +575,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The picker ↔ render-form bake contract (decision 0857): [`ray_submesh`] must test the
|
||||
/// resident WoW-axes geometry through the SAME `wow_to_bevy` bake `submesh_to_static_mesh`
|
||||
/// builds the drawn mesh with, under the part's world transform — the render mesh itself is
|
||||
/// `RENDER_WORLD`-only (0834), so this path is the only thing keeping static models pickable.
|
||||
#[test]
|
||||
fn resident_geometry_picks_where_the_render_form_draws() {
|
||||
let mesh = PickMesh(std::sync::Arc::new(wow_tri()));
|
||||
@@ -715,12 +586,11 @@ mod tests {
|
||||
assert!((hit.distance - 5.0).abs() < 1e-4);
|
||||
assert!(hit.point.abs_diff_eq(Vec3::new(10.0, 1.0, 0.0), 1e-4));
|
||||
assert!(hit.normal.abs_diff_eq(Vec3::Y, 1e-4) || hit.normal.abs_diff_eq(-Vec3::Y, 1e-4));
|
||||
// …and a miss stays a miss.
|
||||
assert!(ray_submesh(&mesh.0, >, Vec3::new(20.0, 6.0, 0.0), Vec3::NEG_Y, false).is_none());
|
||||
}
|
||||
|
||||
/// [`wow_tri`] with every vertex normal authored as model-local Bevy `+X` (WoW pre-image
|
||||
/// `(0, −1, 0)`): the inflated pass displaces the whole triangle +1 model-unit in x.
|
||||
/// [`wow_tri`] with every normal model-local Bevy `+X` (WoW `(0, −1, 0)`): the inflated pass
|
||||
/// moves the whole triangle 1 model-unit in x.
|
||||
fn wow_tri_with_x_normals() -> RenderSubmesh {
|
||||
RenderSubmesh {
|
||||
normals: vec![[0.0, -1.0, 0.0]; 3],
|
||||
@@ -728,11 +598,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Decision 1071 — the generous pass.** The inflated narrow phase displaces each vertex by
|
||||
/// its authored normal, raw: 1 model-unit in local space, scaled by the part's world transform
|
||||
/// (the reference's `skinned_pos + rot·normal`, no extra constant). A ray 1 world-unit beyond
|
||||
/// the drawn silhouette misses the exact pass and hits the halo; the halo is 2 world-units wide
|
||||
/// here because the part's scale is 2.
|
||||
#[test]
|
||||
fn inflated_pick_hits_the_one_model_unit_halo() {
|
||||
let mesh = PickMesh(std::sync::Arc::new(wow_tri_with_x_normals()));
|
||||
@@ -744,12 +609,10 @@ mod tests {
|
||||
let hit = ray_submesh(&mesh.0, >, Vec3::new(13.0, 6.0, 0.0), Vec3::NEG_Y, true)
|
||||
.expect("the halo must catch a ray 1 world-unit past the silhouette");
|
||||
assert!((hit.distance - 5.0).abs() < 1e-4);
|
||||
// …and the halo has its own edge: 2 world-units past the ×2 halo is still a miss.
|
||||
// The halo has its own edge: x=16 is 2 world-units past it.
|
||||
assert!(ray_submesh(&mesh.0, >, Vec3::new(16.0, 6.0, 0.0), Vec3::NEG_Y, true).is_none());
|
||||
}
|
||||
|
||||
/// A submesh authored without normals cannot build the halo: the inflated pass reports a miss
|
||||
/// (never a panic, never a silently un-displaced hit) — the exact pass already had its say.
|
||||
#[test]
|
||||
fn no_authored_normals_means_no_halo() {
|
||||
let mesh = PickMesh(std::sync::Arc::new(wow_tri()));
|
||||
@@ -757,9 +620,8 @@ mod tests {
|
||||
assert!(ray_submesh(&mesh.0, >, Vec3::new(10.0, 6.0, 0.0), Vec3::NEG_Y, true).is_none());
|
||||
}
|
||||
|
||||
/// Cast a ray at a hand-built world and report `(entity, distance)` for every hit, nearest
|
||||
/// first — the whole ray, so a candidate that was silently dropped is distinguishable from one
|
||||
/// that merely lost. `inflate` runs the generous pass instead of the exact one.
|
||||
/// Every hit along the ray at a hand-built world, nearest first, as `(entity, distance)`: the
|
||||
/// whole ray, so a dropped candidate differs from one that lost.
|
||||
fn cast_in(world: &mut World, origin: Vec3, dir: Vec3, inflate: bool) -> Vec<(Entity, f32)> {
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
fn cast(
|
||||
@@ -779,8 +641,7 @@ mod tests {
|
||||
.expect("cast")
|
||||
}
|
||||
|
||||
/// Everything a drawn, bounded, visible part needs to be a pick candidate — minus the geometry,
|
||||
/// which each test supplies (or deliberately withholds).
|
||||
/// A drawn, bounded, visible part with no pick geometry; each test supplies its own.
|
||||
fn drawn(world: &mut World, at: Vec3, half: Vec3) -> EntityWorldMut<'_> {
|
||||
use bevy::camera::visibility::SetViewVisibility;
|
||||
let mut e = world.spawn((
|
||||
@@ -794,15 +655,11 @@ mod tests {
|
||||
e
|
||||
}
|
||||
|
||||
/// **Decision 0929.** A drawn, identified, bounded entity with NO pick geometry — a WMO group's
|
||||
/// MLIQ pool, whose render mesh keeps the whole (mostly dry) vertex grid and so bounds a box
|
||||
/// ~100 yd tall over a city — must be transparent to the ray. It used to be picked at its box
|
||||
/// entry, which put an invisible occluder in front of everything: inside Ironforge every hover,
|
||||
/// on any NPC or GameObject, answered "Wmo · ironforge.wmo".
|
||||
/// A WMO group's MLIQ pool: drawn, identified and bounded, with no pick geometry.
|
||||
#[test]
|
||||
fn a_bounded_mesh_less_entity_is_not_a_pick_occluder() {
|
||||
let mut world = World::new();
|
||||
// The pool's box straddles the camera (entry 0.0 — it wins any distance sort it enters).
|
||||
// The pool's box straddles the camera: entry 0.0 wins any distance sort it enters.
|
||||
let pool = drawn(&mut world, Vec3::ZERO, Vec3::new(60.0, 60.0, 60.0)).id();
|
||||
// The NPC behind it: real geometry at 10 yd (`wow_tri` spans x,z ∈ [-1,1] at y = 0).
|
||||
let npc = drawn(&mut world, Vec3::new(0.0, -10.0, 0.0), Vec3::splat(1.0))
|
||||
@@ -816,8 +673,6 @@ mod tests {
|
||||
assert_eq!(hits.first().map(|(e, _)| *e), Some(npc));
|
||||
}
|
||||
|
||||
/// The other half of the same law: an entity that DECLARES its box is its shape ([`PickBox`] —
|
||||
/// the model-less cube fallback) is still picked, at the box entry.
|
||||
#[test]
|
||||
fn a_declared_pick_box_is_picked_at_its_box_entry() {
|
||||
let mut world = World::new();
|
||||
@@ -830,10 +685,6 @@ mod tests {
|
||||
assert!((hits[0].1 - 8.0).abs() < 1e-4, "{hits:?}"); // the box's near face
|
||||
}
|
||||
|
||||
/// **Decision 1071, the broad phase.** The inflated cast must grow the candidate's bound by the
|
||||
/// same 1 model-unit as the halo, or the slab test rejects the entity before its halo is ever
|
||||
/// narrow-tested — a mesh whose bound the ray misses by half a unit must still halo-hit. And a
|
||||
/// [`PickBox`] inflates its declared cuboid the same way (its box IS its geometry).
|
||||
#[test]
|
||||
fn inflated_cast_grows_the_broad_bound_with_the_halo() {
|
||||
let mut world = World::new();
|
||||
@@ -841,14 +692,14 @@ mod tests {
|
||||
let herb = drawn(&mut world, Vec3::new(0.0, -10.0, 0.0), Vec3::splat(1.0))
|
||||
.insert(PickMesh(std::sync::Arc::new(wow_tri_with_x_normals())))
|
||||
.id();
|
||||
// x=1.5: outside the exact bound (the uninflated cast drops the candidate entirely)…
|
||||
// x=1.5: outside the exact bound, so the uninflated cast drops the candidate,
|
||||
assert!(cast_in(&mut world, Vec3::new(1.5, 0.0, 0.0), Vec3::NEG_Y, false).is_empty());
|
||||
// …inside the halo (and the inflated bound that must admit it).
|
||||
// and inside the halo and the inflated bound.
|
||||
let hits = cast_in(&mut world, Vec3::new(1.5, 0.0, 0.0), Vec3::NEG_Y, true);
|
||||
assert_eq!(hits.first().map(|(e, _)| *e), Some(herb));
|
||||
|
||||
// The cube fallback: box spans x∈[-2,2] at y∈[-12,-8]; a ray at x=2.5 misses the exact
|
||||
// cuboid, hits the 1-unit-inflated one — at its grown near face (y=-7 → entry 7).
|
||||
// cuboid and hits the inflated one at its grown near face (y=-7, entry 7).
|
||||
let mut world = World::new();
|
||||
let cube = drawn(&mut world, Vec3::new(0.0, -10.0, 0.0), Vec3::splat(2.0))
|
||||
.insert(PickBox)
|
||||
@@ -893,11 +744,6 @@ mod tests {
|
||||
.expect("cast")
|
||||
}
|
||||
|
||||
/// **Decision 1534.** A consolidated blob draws many placements as one entity, and its baked
|
||||
/// mesh is their union — so casting against the blob could only ever answer with the blob
|
||||
/// ("static-merge", which is what its own `WorldObject` says). A [`PickBlob`] declares the
|
||||
/// members instead: the cast names the PLACEMENT under the cursor, at that placement's own
|
||||
/// pose, and the members are depth-ordered among themselves like separate parts would be.
|
||||
#[test]
|
||||
fn a_blob_names_the_member_under_the_cursor() {
|
||||
let mut world = World::new();
|
||||
@@ -939,9 +785,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The blob's drawn test is still the ENTITY's: a blob the render world culled must be as
|
||||
/// transparent to the ray as any other undrawn part (the `ViewVisibility` rule the entity
|
||||
/// path has always had).
|
||||
#[test]
|
||||
fn an_undrawn_blob_is_transparent() {
|
||||
let mut world = World::new();
|
||||
@@ -958,8 +801,7 @@ mod tests {
|
||||
assert!(names_in(&mut world, Vec3::new(0.0, 10.0, 0.0), Vec3::NEG_Y).is_empty());
|
||||
}
|
||||
|
||||
/// A billboard card's render form is centred at its pivot (`build_submesh_mesh`); the pick must
|
||||
/// subtract the same pivot or it tests the card a whole pivot-offset away from where it draws.
|
||||
/// The render form centres a billboard card at its pivot, so the pick subtracts the same pivot.
|
||||
#[test]
|
||||
fn billboard_card_picks_pivot_centred() {
|
||||
let mut sub = wow_tri();
|
||||
@@ -972,10 +814,8 @@ mod tests {
|
||||
seq_translations: Vec::new(),
|
||||
});
|
||||
let mesh = PickMesh(std::sync::Arc::new(sub));
|
||||
// The card entity sits AT the pivot's world spot (the spawn sites bake
|
||||
// `transform_point(pivot)` into its translation), so the authored triangle — 3 below the
|
||||
// pivot in model-local Bevy y — draws at world y = 0: a straight-down ray from y = 8 hits
|
||||
// at distance 8. Without the pivot subtraction it would (wrongly) hit at y = 3.
|
||||
// The card entity sits at the pivot's world spot, so the triangle 3 below it draws at world
|
||||
// y = 0 and a ray down from y = 8 hits at 8; without the pivot subtraction, at y = 3.
|
||||
let gt = GlobalTransform::from(Transform::from_xyz(0.0, 3.0, 0.0));
|
||||
let hit = ray_submesh(&mesh.0, >, Vec3::new(0.0, 8.0, 0.0), Vec3::NEG_Y, false)
|
||||
.expect("the pivot-centred card must be hit where it draws");
|
||||
|
||||
Reference in New Issue
Block a user