Files
levlib/draw/shapes.odin
Zachary Levy bca19277b3 draw-improvements (#17)
Major rework to draw rendering system. We are making a SDF first rendering system with tesselated stuff only as a fallback strategy for specific situations where SDF is particularly poorly suited

Co-authored-by: Zachary Levy <zachary@sunforge.is>
Reviewed-on: #17
2026-04-24 07:57:44 +00:00

777 lines
26 KiB
Odin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package draw
import "core:math"
// ----- Internal helpers ----
// Internal
extrude_line :: proc(
start, end_pos: Vec2,
thickness: f32,
color: Color,
vertices: []Vertex,
offset: int,
) -> int {
direction := end_pos - start
delta_x := direction[0]
delta_y := direction[1]
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if length < 0.0001 do return 0
scale := thickness / (2 * length)
perpendicular := Vec2{-delta_y * scale, delta_x * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = solid_vertex(p0, color)
vertices[offset + 1] = solid_vertex(p1, color)
vertices[offset + 2] = solid_vertex(p2, color)
vertices[offset + 3] = solid_vertex(p0, color)
vertices[offset + 4] = solid_vertex(p2, color)
vertices[offset + 5] = solid_vertex(p3, color)
return 6
}
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
// Color is premultiplied: the tessellated fragment shader passes it through directly
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
solid_vertex :: proc(position: Vec2, color: Color) -> Vertex {
return Vertex{position = position, color = premultiply_color(color)}
}
emit_rectangle :: proc(x, y, width, height: f32, color: Color, vertices: []Vertex, offset: int) {
vertices[offset + 0] = solid_vertex({x, y}, color)
vertices[offset + 1] = solid_vertex({x + width, y}, color)
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
vertices[offset + 3] = solid_vertex({x, y}, color)
vertices[offset + 4] = solid_vertex({x + width, y + height}, color)
vertices[offset + 5] = solid_vertex({x, y + height}, color)
}
// Internal
prepare_sdf_primitive_textured :: proc(
layer: ^Layer,
prim: Primitive,
texture_id: Texture_Id,
sampler: Sampler_Preset,
) {
offset := u32(len(GLOB.tmp_primitives))
append(&GLOB.tmp_primitives, prim)
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1, texture_id, sampler)
}
//Internal
//
// Compute the visual center of a center-parametrized shape after applying
// Convention B origin semantics: `center` is where the origin-point lands in
// world space; the visual center is offset by -origin and then rotated around
// the landing point.
// visual_center = center + R(θ) · (-origin)
// When θ=0: visual_center = center - origin (pure positioning shift).
// When origin={0,0}: visual_center = center (no change).
compute_pivot_center :: proc(center: Vec2, origin: Vec2, sin_angle, cos_angle: f32) -> Vec2 {
if origin == {0, 0} do return center
return(
center +
{cos_angle * (-origin.x) - sin_angle * (-origin.y), sin_angle * (-origin.x) + cos_angle * (-origin.y)} \
)
}
// Compute the AABB half-extents of a rectangle with half-size (half_width, half_height) rotated by the given cos/sin.
rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, sin_angle: f32) -> [2]f32 {
cos_abs := abs(cos_angle)
sin_abs := abs(sin_angle)
return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs}
}
// Pack sin/cos into the Primitive.rotation_sc field as two f16 values.
pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
return pack_f16_pair(f16(sin_angle), f16(cos_angle))
}
// Internal
//
// Build an RRect Primitive with bounds, params, and rotation computed from rectangle geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_rrect_primitive :: proc(
rect: Rectangle,
radii: Rectangle_Radii,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Primitive {
max_radius := min(rect.width, rect.height) * 0.5
clamped_top_left := clamp(radii.top_left, 0, max_radius)
clamped_top_right := clamp(radii.top_right, 0, max_radius)
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5
half_height := rect.height * 0.5
center_x := rect.x + half_width - origin.x
center_y := rect.y + half_height - origin.y
sin_angle: f32 = 0
cos_angle: f32 = 1
has_rotation := false
if needs_transform(origin, rotation) {
rotation_radians := math.to_radians(rotation)
sin_angle, cos_angle = math.sincos(rotation_radians)
has_rotation = rotation != 0
transform := build_pivot_rotation_sc({rect.x + origin.x, rect.y + origin.y}, origin, cos_angle, sin_angle)
new_center := apply_transform(transform, {half_width, half_height})
center_x = new_center.x
center_y = new_center.y
}
bounds_half_width, bounds_half_height := half_width, half_height
if has_rotation {
expanded := rotated_aabb_half_extents(half_width, half_height, cos_angle, sin_angle)
bounds_half_width = expanded.x
bounds_half_height = expanded.y
}
prim := Primitive {
bounds = {
center_x - bounds_half_width - padding,
center_y - bounds_half_height - padding,
center_x + bounds_half_width + padding,
center_y + bounds_half_height + padding,
},
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
}
prim.params.rrect = RRect_Params {
half_size = {half_width * dpi_scale, half_height * dpi_scale},
radii = {
clamped_bottom_right * dpi_scale,
clamped_top_right * dpi_scale,
clamped_bottom_left * dpi_scale,
clamped_top_left * dpi_scale,
},
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build an RRect Primitive for a circle (fully-rounded square RRect).
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_circle_primitive :: proc(
center: Vec2,
radius: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} {
sin_a, cos_a := math.sincos(math.to_radians(rotation))
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
}
prim := Primitive {
bounds = {
actual_center.x - radius - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
}
scaled_radius := radius * dpi_scale
prim.params.rrect = RRect_Params {
half_size = {scaled_radius, scaled_radius},
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build an Ellipse Primitive with bounds, params, and rotation computed from ellipse geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_ellipse_primitive :: proc(
center: Vec2,
radius_horizontal, radius_vertical: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
sin_angle: f32 = 0
cos_angle: f32 = 1
has_rotation := false
if needs_transform(origin, rotation) {
rotation_radians := math.to_radians(rotation)
sin_angle, cos_angle = math.sincos(rotation_radians)
actual_center = compute_pivot_center(center, origin, sin_angle, cos_angle)
has_rotation = rotation != 0
}
bound_horizontal, bound_vertical := radius_horizontal, radius_vertical
if has_rotation {
expanded := rotated_aabb_half_extents(radius_horizontal, radius_vertical, cos_angle, sin_angle)
bound_horizontal = expanded.x
bound_vertical = expanded.y
}
prim := Primitive {
bounds = {
actual_center.x - bound_horizontal - padding,
actual_center.y - bound_vertical - padding,
actual_center.x + bound_horizontal + padding,
actual_center.y + bound_vertical + padding,
},
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build an NGon Primitive with bounds, params, and rotation computed from polygon geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_polygon_primitive :: proc(
center: Vec2,
sides: int,
radius: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} && rotation != 0 {
sin_a, cos_a := math.sincos(math.to_radians(rotation))
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
}
rotation_radians := math.to_radians(rotation)
sin_rot, cos_rot := math.sincos(rotation_radians)
prim := Primitive {
bounds = {
actual_center.x - radius - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
sides = f32(sides),
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build a Ring_Arc Primitive with bounds and params computed from ring/arc geometry.
// Pre-computes the angular boundary normals on the CPU so the fragment shader needs
// no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly
// handles pie slices (inner_radius = 0) and full rings.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_ring_arc_primitive :: proc(
center: Vec2,
inner_radius, outer_radius: f32,
start_angle: f32,
end_angle: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> (
Primitive,
Shape_Flags,
) {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
rotation_offset: f32 = 0
if needs_transform(origin, rotation) {
sin_a, cos_a := math.sincos(math.to_radians(rotation))
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
rotation_offset = math.to_radians(rotation)
}
start_rad := math.to_radians(start_angle) + rotation_offset
end_rad := math.to_radians(end_angle) + rotation_offset
// Normalize arc span to [0, 2π]
arc_span := end_rad - start_rad
if arc_span < 0 {
arc_span += 2 * math.PI
}
// Pre-compute edge normals and arc flags on CPU — no per-pixel trig needed.
// arc_flags: {} = full ring, {.Arc_Narrow} = span ≤ π (intersect), {.Arc_Wide} = span > π (union)
arc_flags: Shape_Flags = {}
normal_start: [2]f32 = {}
normal_end: [2]f32 = {}
if arc_span < 2 * math.PI - 0.001 {
sin_start, cos_start := math.sincos(start_rad)
sin_end, cos_end := math.sincos(end_rad)
normal_start = {sin_start, -cos_start}
normal_end = {-sin_end, cos_end}
arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide}
}
prim := Primitive {
bounds = {
actual_center.x - outer_radius - padding,
actual_center.y - outer_radius - padding,
actual_center.x + outer_radius + padding,
actual_center.y + outer_radius + padding,
},
}
prim.params.ring_arc = Ring_Arc_Params {
inner_radius = inner_radius * dpi_scale,
outer_radius = outer_radius * dpi_scale,
normal_start = normal_start,
normal_end = normal_end,
half_feather = half_feather,
}
return prim, arc_flags
}
// Apply gradient and outline effects to a primitive. Sets flags, uv.effects, and expands bounds.
// All parameters (outline_width) are in logical pixels, matching the rest of the public API.
// The helper converts to physical pixels for GPU packing internally.
@(private)
apply_shape_effects :: proc(
prim: ^Primitive,
kind: Shape_Kind,
gradient: Gradient,
outline_color: Color,
outline_width: f32,
extra_flags: Shape_Flags = {},
) {
flags: Shape_Flags = extra_flags
gradient_dir_sc: u32 = 0
switch g in gradient {
case Linear_Gradient:
flags += {.Gradient}
prim.uv.effects.gradient_color = g.end_color
rad := math.to_radians(g.angle)
sin_a, cos_a := math.sincos(rad)
gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a))
case Radial_Gradient:
flags += {.Gradient, .Gradient_Radial}
prim.uv.effects.gradient_color = g.outer_color
case:
}
outline_packed: u32 = 0
if outline_width > 0 {
flags += {.Outline}
prim.uv.effects.outline_color = outline_color
outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0)
// Expand bounds to contain the outline (bounds are in logical pixels)
prim.bounds[0] -= outline_width
prim.bounds[1] -= outline_width
prim.bounds[2] += outline_width
prim.bounds[3] += outline_width
}
// Set .Rotated flag if rotation_sc was populated by the build proc
if prim.rotation_sc != 0 {
flags += {.Rotated}
}
prim.uv.effects.gradient_dir_sc = gradient_dir_sc
prim.uv.effects.outline_packed = outline_packed
prim.flags = pack_kind_flags(kind, flags)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Rectangle procs -----------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled rectangle via SDF with optional per-corner rounding radii.
// Use `uniform_radii(rect, roundness)` to compute uniform radii from a 01 fraction.
//
// Origin semantics:
// `origin` is a local offset from the rect's top-left corner that selects both the positioning
// anchor and the rotation pivot. `rect.x, rect.y` specifies where that anchor point lands in
// world space. When `origin = {0, 0}` (default), `rect.x, rect.y` is the top-left corner.
// Rotation always occurs around the anchor point.
rectangle :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
gradient: Gradient = nil,
outline_color: Color = {},
outline_width: f32 = 0,
radii: Rectangle_Radii = {},
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
prim.color = color
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
}
// Draw a rectangle with a texture fill via SDF with optional per-corner rounding radii.
// Texture and gradient/outline are mutually exclusive (they share the same storage in the
// primitive). To outline a textured rect, draw the texture first, then a stroke-only rect on top.
// Origin semantics: see `rectangle`.
rectangle_texture :: proc(
layer: ^Layer,
rect: Rectangle,
id: Texture_Id,
tint: Color = DFT_TINT,
uv_rect: Rectangle = DFT_UV_RECT,
sampler: Sampler_Preset = DFT_SAMPLER,
radii: Rectangle_Radii = {},
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
prim.color = tint
tex_flags: Shape_Flags = {.Textured}
if prim.rotation_sc != 0 {
tex_flags += {.Rotated}
}
prim.flags = pack_kind_flags(.RRect, tex_flags)
prim.uv.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
prepare_sdf_primitive_textured(layer, prim, id, sampler)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Circle procs (emit RRect primitives) ------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled circle via SDF (emitted as a fully-rounded RRect).
//
// Origin semantics (Convention B):
// `origin` is a local offset from the shape's center that selects both the positioning anchor
// and the rotation pivot. The `center` parameter specifies where that anchor point lands in
// world space. When `origin = {0, 0}` (default), `center` is the visual center.
// When `origin = {r, 0}`, the point `r` pixels to the right of the shape center lands at
// `center`, shifting the shape left by `r`.
circle :: proc(
layer: ^Layer,
center: Vec2,
radius: f32,
color: Color,
gradient: Gradient = nil,
outline_color: Color = {},
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
prim.color = color
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Ellipse procs (emit Ellipse primitives) ---
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled ellipse via SDF.
// Origin semantics: see `circle`.
ellipse :: proc(
layer: ^Layer,
center: Vec2,
radius_horizontal, radius_vertical: f32,
color: Color,
gradient: Gradient = nil,
outline_color: Color = {},
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
prim.color = color
apply_shape_effects(&prim, .Ellipse, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Polygon procs (emit NGon primitives) ------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled regular polygon via SDF.
// `sides` must be >= 3. The polygon is inscribed in a circle of the given `radius`.
// Origin semantics: see `circle`.
polygon :: proc(
layer: ^Layer,
center: Vec2,
sides: int,
radius: f32,
color: Color,
gradient: Gradient = nil,
outline_color: Color = {},
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
if sides < 3 do return
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
prim.color = color
apply_shape_effects(&prim, .NGon, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Ring / Arc procs (emit Ring_Arc primitives) ----
// ---------------------------------------------------------------------------------------------------------------------
// Draw a ring, arc, or pie slice via SDF.
// Full ring by default. Pass start_angle/end_angle (degrees) for partial arcs.
// Use inner_radius = 0 for pie slices (sectors).
// Origin semantics: see `circle`.
ring :: proc(
layer: ^Layer,
center: Vec2,
inner_radius, outer_radius: f32,
color: Color,
gradient: Gradient = nil,
outline_color: Color = {},
outline_width: f32 = 0,
start_angle: f32 = 0,
end_angle: f32 = DFT_CIRC_END_ANGLE,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim, arc_flags := build_ring_arc_primitive(
center,
inner_radius,
outer_radius,
start_angle,
end_angle,
origin,
rotation,
feather_px,
)
prim.color = color
apply_shape_effects(&prim, .Ring_Arc, gradient, outline_color, outline_width, arc_flags)
prepare_sdf_primitive(layer, prim)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Line procs (emit rotated RRect primitives) ----
// ---------------------------------------------------------------------------------------------------------------------
// Draw a line segment via SDF (emitted as a rotated capsule-shaped RRect).
// Round caps are produced by setting corner radii equal to half the thickness.
line :: proc(
layer: ^Layer,
start_position, end_position: Vec2,
color: Color,
thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {},
outline_width: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
delta_x := end_position.x - start_position.x
delta_y := end_position.y - start_position.y
seg_length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if seg_length < 0.0001 do return
rotation_radians := math.atan2(delta_y, delta_x)
sin_angle, cos_angle := math.sincos(rotation_radians)
center_x := (start_position.x + end_position.x) * 0.5
center_y := (start_position.y + end_position.y) * 0.5
half_length := seg_length * 0.5
half_thickness := thickness * 0.5
cap_radius := half_thickness
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
// Expand bounds for rotation
bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle)
prim := Primitive {
bounds = {
center_x - bounds_half.x - padding,
center_y - bounds_half.y - padding,
center_x + bounds_half.x + padding,
center_y + bounds_half.y + padding,
},
color = color,
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
}
prim.params.rrect = RRect_Params {
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
radii = {
cap_radius * dpi_scale,
cap_radius * dpi_scale,
cap_radius * dpi_scale,
cap_radius * dpi_scale,
},
half_feather = half_feather,
}
apply_shape_effects(&prim, .RRect, nil, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
}
// Draw a line strip via decomposed SDF line segments.
line_strip :: proc(
layer: ^Layer,
points: []Vec2,
color: Color,
thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {},
outline_width: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
if len(points) < 2 do return
for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], color, thickness, outline_color, outline_width, feather_px)
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Helpers ----------------
// ---------------------------------------------------------------------------------------------------------------------
// Returns uniform radii (all corners the same) as a fraction of the shorter side.
// `roundness` is clamped to [0, 1]; 0 = sharp corners, 1 = fully rounded (stadium or circle).
uniform_radii :: #force_inline proc(rect: Rectangle, roundness: f32) -> Rectangle_Radii {
cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
return {cr, cr, cr, cr}
}
// Return Vec2 pixel offsets for use as the `origin` parameter of draw calls.
// Composable with normal vector +/- arithmetic.
//
// Text anchor helpers are in text.odin (they depend on measure_text / SDL_ttf).
// ----- Rectangle anchors (origin measured from rectangle's top-left) ---------------------------------------------
center_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, rectangle.height * 0.5}
}
top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, 0}
}
top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, 0}
}
top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, 0}
}
left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, rectangle.height * 0.5}
}
right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, rectangle.height * 0.5}
}
bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, rectangle.height}
}
bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, rectangle.height}
}
bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, rectangle.height}
}
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
center_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
return (v1 + v2 + v3) / 3 - bounds_min
}
top_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
return {0, 0}
}
top_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {(max_x - min_x) * 0.5, 0}
}
top_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {max_x - min_x, 0}
}
left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, (max_y - min_y) * 0.5}
}
right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {bounds_max.x - bounds_min.x, (bounds_max.y - bounds_min.y) * 0.5}
}
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, max_y - min_y}
}
bottom_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {(bounds_max.x - bounds_min.x) * 0.5, bounds_max.y - bounds_min.y}
}
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return bounds_max - bounds_min
}