Table of Contents

Class GridPositioner3DSpike

Namespace
GridPlacement.GDScript
Assembly
MoonBark.GridPlacement.GDScript.dll

6.0.0-3D-Surface (Wave A.2 spike): GridPositioner3D camera ray-cast helper.\n\nThis is a spike, not a production positioner. It validates the highest-risk piece of the 3D surface (Camera3D ray → GridMap cell) end-to-end before committing to the full Wave B/C/D plan.\n\nWhat it does: given a Camera3D, a GridMap, and a screen-space cursor position, returns the GridMap cell the camera is looking at (or null if the ray misses the grid).\n\nWhat it does NOT do: full 2D-style positioner lifecycle, mode gating, multi-camera, recenter, keyboard nav, rotation, etc. Those are Wave B/C/D work; the spike is the math, not the surface.\n\nMath: Camera3D.project_ray_origin(screen_pos) + project_ray_normal(screen_pos) gives the world-space ray. We intersect it with the y=0 (or the configured ground plane) and use GridMap.local_to_map(world_hit) to resolve the cell. GridMap is a static body in Godot 4.6; we can also use PhysicsServer3D.space_get_direct_state().intersect_ray() for a more accurate hit (catches diagonal/blocked grids).\n\n6.0.0 host + session pattern: this spike does NOT extend PlacementNode2D / PlacementNode3D. It is a stand-alone Node3D helper; the eventual GridPositioner3D (Wave B) will wire it into the host's _input dispatch via the same pattern as GridPositioner2D (which is also a stand-alone Node2D, not a PlacementNode2D).

class GridPositioner3DSpike extends Node3D
GridPositioner3DSpike

Fields

SPIKE_MISS

Sentinel returned when the ray misses the grid. The test asserts on == SPIKE_MISS rather than == null to make the contract explicit and to avoid Godot's null-Variant ambiguity.

const SPIKE_MISS: Vector3i = Vector3i(2147483647, 2147483647, 2147483647)

Properties

camera

The camera that the spike ray-casts through. Set this at _ready() in your test/scene; the spike does not auto-discover.

@export var camera: Camera3D

grid

The grid the spike resolves cells on. Same pattern as camera.

@export var grid: GridMap

ground_plane_y

The y-coordinate (in grid-local space) of the ground plane the ray intersects. Default 0.0 = top face of the cell layer at y=0.

@export var ground_plane_y: float = 0.0

ray_origin

var ray_origin: Vector3 = camera.project_ray_origin(p_screen_pos)

ray_normal

var ray_normal: Vector3 = camera.project_ray_normal(p_screen_pos)

world_to_grid

var world_to_grid: Transform3D = grid.global_transform.affine_inverse()

grid_origin

var grid_origin: Vector3 = world_to_grid * ray_origin

grid_normal

var grid_normal: Vector3 = world_to_grid.basis * ray_normal

t

var t: float = (ground_plane_y - grid_origin.y) / grid_normal.y

hit_point_local

var hit_point_local: Vector3 = grid_origin + grid_normal * t

cell

var cell: Vector3i = ray_from_screen_to_cell(p_screen_pos)

cell

var cell: Vector3i = ray_from_screen_to_cell(p_screen_pos)

Methods

ray_from_screen_to_cell

func ray_from_screen_to_cell(p_screen_pos: Vector2) -> Vector3i:

Parameters

p_screen_pos

Returns

ray_from_screen_to_cell_or_null

func ray_from_screen_to_cell_or_null(p_screen_pos: Vector2) -> Variant:

Parameters

p_screen_pos

Returns