Added improved non-clay text handling along with consistent origin and rotation API
This commit is contained in:
284
draw/draw.odin
284
draw/draw.odin
@@ -1,14 +1,15 @@
|
||||
package draw
|
||||
|
||||
import clay "../vendor/clay"
|
||||
import "base:runtime"
|
||||
import "core:c"
|
||||
import "core:log"
|
||||
|
||||
import "core:math"
|
||||
import "core:strings"
|
||||
import sdl "vendor:sdl3"
|
||||
import sdl_ttf "vendor:sdl3/ttf"
|
||||
|
||||
import clay "../vendor/clay"
|
||||
|
||||
when ODIN_OS == .Darwin {
|
||||
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL}
|
||||
ENTRY_POINT :: "main0"
|
||||
@@ -89,34 +90,35 @@ Scissor :: struct {
|
||||
GLOB: Global
|
||||
|
||||
Global :: struct {
|
||||
odin_context: runtime.Context,
|
||||
pipeline_2d_base: Pipeline_2D_Base,
|
||||
text_cache: Text_Cache,
|
||||
layers: [dynamic]Layer,
|
||||
scissors: [dynamic]Scissor,
|
||||
tmp_shape_verts: [dynamic]Vertex,
|
||||
tmp_text_verts: [dynamic]Vertex,
|
||||
tmp_text_indices: [dynamic]c.int,
|
||||
tmp_text_batches: [dynamic]TextBatch,
|
||||
tmp_primitives: [dynamic]Primitive,
|
||||
tmp_sub_batches: [dynamic]Sub_Batch,
|
||||
clay_mem: [^]u8,
|
||||
msaa_texture: ^sdl.GPUTexture,
|
||||
curr_layer_index: uint,
|
||||
max_layers: int,
|
||||
max_scissors: int,
|
||||
max_shape_verts: int,
|
||||
max_text_verts: int,
|
||||
max_text_indices: int,
|
||||
max_text_batches: int,
|
||||
max_primitives: int,
|
||||
max_sub_batches: int,
|
||||
dpi_scaling: f32,
|
||||
msaa_w: u32,
|
||||
msaa_h: u32,
|
||||
sample_count: sdl.GPUSampleCount,
|
||||
clay_z_index: i16,
|
||||
cleared: bool,
|
||||
odin_context: runtime.Context,
|
||||
pipeline_2d_base: Pipeline_2D_Base,
|
||||
text_cache: Text_Cache,
|
||||
layers: [dynamic]Layer,
|
||||
scissors: [dynamic]Scissor,
|
||||
tmp_shape_verts: [dynamic]Vertex,
|
||||
tmp_text_verts: [dynamic]Vertex,
|
||||
tmp_text_indices: [dynamic]c.int,
|
||||
tmp_text_batches: [dynamic]TextBatch,
|
||||
tmp_primitives: [dynamic]Primitive,
|
||||
tmp_sub_batches: [dynamic]Sub_Batch,
|
||||
tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects to destroy after end()
|
||||
clay_mem: [^]u8,
|
||||
msaa_texture: ^sdl.GPUTexture,
|
||||
curr_layer_index: uint,
|
||||
max_layers: int,
|
||||
max_scissors: int,
|
||||
max_shape_verts: int,
|
||||
max_text_verts: int,
|
||||
max_text_indices: int,
|
||||
max_text_batches: int,
|
||||
max_primitives: int,
|
||||
max_sub_batches: int,
|
||||
dpi_scaling: f32,
|
||||
msaa_w: u32,
|
||||
msaa_h: u32,
|
||||
sample_count: sdl.GPUSampleCount,
|
||||
clay_z_index: i16,
|
||||
cleared: bool,
|
||||
}
|
||||
|
||||
Init_Options :: struct {
|
||||
@@ -161,20 +163,21 @@ init :: proc(
|
||||
}
|
||||
|
||||
GLOB = Global {
|
||||
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
|
||||
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
|
||||
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
odin_context = odin_context,
|
||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||
clay_mem = make([^]u8, min_memory_size, allocator = allocator),
|
||||
sample_count = resolved_sample_count,
|
||||
pipeline_2d_base = pipeline,
|
||||
text_cache = text_cache,
|
||||
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
|
||||
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
|
||||
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||
odin_context = odin_context,
|
||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||
clay_mem = make([^]u8, min_memory_size, allocator = allocator),
|
||||
sample_count = resolved_sample_count,
|
||||
pipeline_2d_base = pipeline,
|
||||
text_cache = text_cache,
|
||||
}
|
||||
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
||||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_mem)
|
||||
@@ -182,7 +185,7 @@ init :: proc(
|
||||
sdl.GetWindowSize(window, &window_width, &window_height)
|
||||
|
||||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
||||
clay.SetMeasureTextFunction(measure_text, nil)
|
||||
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -216,6 +219,8 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
||||
delete(GLOB.tmp_text_batches)
|
||||
delete(GLOB.tmp_primitives)
|
||||
delete(GLOB.tmp_sub_batches)
|
||||
for t in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(t)
|
||||
delete(GLOB.tmp_uncached_text)
|
||||
free(GLOB.clay_mem, allocator)
|
||||
if GLOB.msaa_texture != nil {
|
||||
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
||||
@@ -229,6 +234,9 @@ clear_global :: proc() {
|
||||
GLOB.curr_layer_index = 0
|
||||
GLOB.clay_z_index = 0
|
||||
GLOB.cleared = false
|
||||
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
|
||||
for t in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(t)
|
||||
clear(&GLOB.tmp_uncached_text)
|
||||
clear(&GLOB.layers)
|
||||
clear(&GLOB.scissors)
|
||||
clear(&GLOB.tmp_shape_verts)
|
||||
@@ -244,7 +252,7 @@ clear_global :: proc() {
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@(private = "file")
|
||||
measure_text :: proc "c" (
|
||||
measure_text_clay :: proc "c" (
|
||||
text: clay.StringSlice,
|
||||
config: ^clay.TextElementConfig,
|
||||
user_data: rawptr,
|
||||
@@ -384,6 +392,54 @@ prepare_text :: proc(layer: ^Layer, txt: Text) {
|
||||
}
|
||||
}
|
||||
|
||||
// Submit a text element with a 2D affine transform applied to vertices.
|
||||
// Used by the high-level `text` proc when rotation or a non-zero origin is specified.
|
||||
// NOTE: xform must be in physical (DPI-scaled) pixel space — the caller pre-scales
|
||||
// pos and origin by GLOB.dpi_scaling before building the transform.
|
||||
prepare_text_transformed :: proc(layer: ^Layer, txt: Text, xform: Transform_2D) {
|
||||
data := sdl_ttf.GetGPUTextDrawData(txt.ref)
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||
|
||||
for data != nil {
|
||||
vertex_start := u32(len(GLOB.tmp_text_verts))
|
||||
index_start := u32(len(GLOB.tmp_text_indices))
|
||||
|
||||
for i in 0 ..< data.num_vertices {
|
||||
pos := data.xy[i]
|
||||
uv := data.uv[i]
|
||||
// SDL_ttf gives glyph positions in physical pixels relative to text origin.
|
||||
// The transform is already in physical-pixel space (caller pre-scaled),
|
||||
// so we apply directly — no per-vertex DPI divide/multiply.
|
||||
append(
|
||||
&GLOB.tmp_text_verts,
|
||||
Vertex{position = apply_transform(xform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = txt.color},
|
||||
)
|
||||
}
|
||||
|
||||
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
|
||||
|
||||
batch_idx := u32(len(GLOB.tmp_text_batches))
|
||||
append(
|
||||
&GLOB.tmp_text_batches,
|
||||
TextBatch {
|
||||
atlas_texture = data.atlas_texture,
|
||||
vertex_start = vertex_start,
|
||||
vertex_count = u32(data.num_vertices),
|
||||
index_start = index_start,
|
||||
index_count = u32(data.num_indices),
|
||||
},
|
||||
)
|
||||
|
||||
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1)
|
||||
|
||||
data = data.next
|
||||
}
|
||||
}
|
||||
|
||||
// Append a new sub-batch or extend the last one if same kind and contiguous.
|
||||
@(private)
|
||||
append_or_extend_sub_batch :: proc(
|
||||
@@ -466,26 +522,13 @@ prepare_clay_batch :: proc(
|
||||
render_data := render_command.renderData.text
|
||||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
||||
c_text := strings.clone_to_cstring(txt, context.temp_allocator)
|
||||
sdl_text := GLOB.text_cache.cache[render_command.id]
|
||||
|
||||
if sdl_text == nil {
|
||||
// Cache a SDL text object
|
||||
sdl_text = sdl_ttf.CreateText(
|
||||
GLOB.text_cache.engine,
|
||||
get_font(render_data.fontId, render_data.fontSize),
|
||||
c_text,
|
||||
0,
|
||||
)
|
||||
if sdl_text == nil {
|
||||
log.panicf("Failed to create SDL text for clay render command: %s", sdl.GetError())
|
||||
}
|
||||
GLOB.text_cache.cache[render_command.id] = sdl_text
|
||||
} else {
|
||||
if !sdl_ttf.SetTextString(sdl_text, c_text, 0) {
|
||||
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
|
||||
}
|
||||
}
|
||||
|
||||
// Clay's render_command.id is already hashed with the same Jenkins algorithm
|
||||
// as text_cache_hash, so it shares the same keyspace.
|
||||
sdl_text := cache_get_or_update(
|
||||
render_command.id,
|
||||
c_text,
|
||||
get_font(render_data.fontId, render_data.fontSize),
|
||||
)
|
||||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
|
||||
case clay.RenderCommandType.Image:
|
||||
case clay.RenderCommandType.ScissorStart:
|
||||
@@ -749,3 +792,112 @@ destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
|
||||
sdl.ReleaseGPUBuffer(device, buffer.gpu)
|
||||
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Transform ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// 2x3 affine transform for 2D pivot-rotation.
|
||||
// Used internally by rotation-aware drawing procs.
|
||||
Transform_2D :: struct {
|
||||
m00, m01: f32, // row 0: rotation/scale
|
||||
m10, m11: f32, // row 1: rotation/scale
|
||||
tx, ty: f32, // translation
|
||||
}
|
||||
|
||||
// Build a pivot-rotation transform.
|
||||
//
|
||||
// Semantics (raylib-style):
|
||||
// The point whose local coordinates equal `origin` lands at `pos` in world space.
|
||||
// The rest of the shape rotates around that pivot.
|
||||
//
|
||||
// Formula: p_world = pos + R(θ) · (p_local - origin)
|
||||
//
|
||||
// Parameters:
|
||||
// pos – world-space position where the pivot lands.
|
||||
// origin – pivot point in local space (measured from the shape's natural reference point).
|
||||
// rotation_deg – rotation in degrees, counter-clockwise.
|
||||
//
|
||||
build_pivot_rot :: proc(pos: [2]f32, origin: [2]f32, rotation_deg: f32) -> Transform_2D {
|
||||
rad := math.to_radians(rotation_deg)
|
||||
c := math.cos(rad)
|
||||
s := math.sin(rad)
|
||||
return Transform_2D {
|
||||
m00 = c,
|
||||
m01 = -s,
|
||||
m10 = s,
|
||||
m11 = c,
|
||||
tx = pos.x - (c * origin.x - s * origin.y),
|
||||
ty = pos.y - (s * origin.x + c * origin.y),
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the transform to a local-space point, producing a world-space point.
|
||||
apply_transform :: #force_inline proc(t: Transform_2D, p: [2]f32) -> [2]f32 {
|
||||
return {t.m00 * p.x + t.m01 * p.y + t.tx, t.m10 * p.x + t.m11 * p.y + t.ty}
|
||||
}
|
||||
|
||||
// Fast-path check callers use BEFORE building a transform.
|
||||
// Returns true if either the origin is non-zero or rotation is non-zero,
|
||||
// meaning a transform actually needs to be computed.
|
||||
needs_transform :: #force_inline proc(origin: [2]f32, rotation: f32) -> bool {
|
||||
return origin != {0, 0} || rotation != 0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Procedure Groups ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
center_of :: proc {
|
||||
center_of_rect,
|
||||
center_of_triangle,
|
||||
center_of_text,
|
||||
}
|
||||
|
||||
top_left_of :: proc {
|
||||
top_left_of_rect,
|
||||
top_left_of_triangle,
|
||||
top_left_of_text,
|
||||
}
|
||||
|
||||
top_of :: proc {
|
||||
top_of_rect,
|
||||
top_of_triangle,
|
||||
top_of_text,
|
||||
}
|
||||
|
||||
top_right_of :: proc {
|
||||
top_right_of_rect,
|
||||
top_right_of_triangle,
|
||||
top_right_of_text,
|
||||
}
|
||||
|
||||
left_of :: proc {
|
||||
left_of_rect,
|
||||
left_of_triangle,
|
||||
left_of_text,
|
||||
}
|
||||
|
||||
right_of :: proc {
|
||||
right_of_rect,
|
||||
right_of_triangle,
|
||||
right_of_text,
|
||||
}
|
||||
|
||||
bottom_left_of :: proc {
|
||||
bottom_left_of_rect,
|
||||
bottom_left_of_triangle,
|
||||
bottom_left_of_text,
|
||||
}
|
||||
|
||||
bottom_of :: proc {
|
||||
bottom_of_rect,
|
||||
bottom_of_triangle,
|
||||
bottom_of_text,
|
||||
}
|
||||
|
||||
bottom_right_of :: proc {
|
||||
bottom_right_of_rect,
|
||||
bottom_right_of_triangle,
|
||||
bottom_right_of_text,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user