// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// A single 2D vertex holding spatial coordinates, UV texture coordinates,
/// and normalized RGBA color channels for hardware-accelerated GPU rasterization.
pub struct Vertex {
x : Double
y : Double
u : Double
v : Double
r : Double
g : Double
b : Double
a : Double
} derive(Eq, Debug, Default)
///|
pub fn Vertex::new(
x : Double,
y : Double,
u? : Double = 0.0,
v? : Double = 0.0,
color? : @color.Color = @color.Color::white(),
) -> Vertex {
{
x,
y,
u,
v,
r: color.r.to_double() / 255.0,
g: color.g.to_double() / 255.0,
b: color.b.to_double() / 255.0,
a: color.a.to_double() / 255.0,
}
}
///|
/// Contiguous 2D triangle mesh holding vertex and index buffers for batched WebGL/GPU rendering.
pub struct Mesh {
vertices : Array[Vertex]
indices : Array[Int]
} derive(Eq, Debug)
///|
pub fn Mesh::new() -> Mesh {
{ vertices: [], indices: [], }
}
///|
pub fn Mesh::clear(self : Mesh) -> Unit {
self.vertices.clear()
self.indices.clear()
}
///|
pub fn Mesh::vertex_count(self : Mesh) -> Int {
self.vertices.length()
}
///|
pub fn Mesh::index_count(self : Mesh) -> Int {
self.indices.length()
}
///|
pub fn Mesh::add_vertex(self : Mesh, v : Vertex) -> Int {
let idx = self.vertices.length()
self.vertices.push(v)
idx
}
///|
pub fn Mesh::add_triangle(self : Mesh, i0 : Int, i1 : Int, i2 : Int) -> Unit {
self.indices.push(i0)
self.indices.push(i1)
self.indices.push(i2)
}
///|
/// Appends a quad defined by 4 vertex indices (i0: top-left, i1: top-right, i2: bottom-right, i3: bottom-left).
pub fn Mesh::add_quad(
self : Mesh,
i0 : Int,
i1 : Int,
i2 : Int,
i3 : Int,
) -> Unit {
self.add_triangle(i0, i1, i2)
self.add_triangle(i0, i2, i3)
}
///|
/// Serializes interleaved vertex attributes (x, y, u, v, r, g, b, a) into a flat array
/// for direct WebGL Float32Buffer upload.
pub fn Mesh::write_vertex_floats(self : Mesh, out : Array[Double]) -> Unit {
for v in self.vertices {
out.push(v.x)
out.push(v.y)
out.push(v.u)
out.push(v.v)
out.push(v.r)
out.push(v.g)
out.push(v.b)
out.push(v.a)
}
}
///|
/// Tessellates a filled solid rectangle into the mesh.
pub fn Mesh::tessellate_rect(
self : Mesh,
rect : @math.Rect,
color : @color.Color,
corner_radius : Double,
) -> Unit {
if rect.w <= 0.0 || rect.h <= 0.0 {
return
}
let r_clamped = if corner_radius <= 0.0 {
0.0
} else {
let max_r = if rect.w < rect.h { rect.w * 0.5 } else { rect.h * 0.5 }
if corner_radius > max_r {
max_r
} else {
corner_radius
}
}
if r_clamped <= 0.0 {
let i0 = self.add_vertex(Vertex::new(rect.x, rect.y, color~))
let i1 = self.add_vertex(Vertex::new(rect.x + rect.w, rect.y, color~))
let i2 = self.add_vertex(
Vertex::new(rect.x + rect.w, rect.y + rect.h, color~),
)
let i3 = self.add_vertex(Vertex::new(rect.x, rect.y + rect.h, color~))
self.add_quad(i0, i1, i2, i3)
} else {
// Nine-patch rounded rect tessellation with 4 corner fans
let segments = 4
let center_idx = self.add_vertex(
Vertex::new(rect.x + rect.w * 0.5, rect.y + rect.h * 0.5, color~),
)
let corner_centers = [
(rect.x + rect.w - r_clamped, rect.y + r_clamped, 0.0), // Top-Right (0 to pi/2)
(rect.x + rect.w - r_clamped, rect.y + rect.h - r_clamped, @math.pi * 0.5), // Bottom-Right (pi/2 to pi)
(rect.x + r_clamped, rect.y + rect.h - r_clamped, @math.pi), // Bottom-Left (pi to 3pi/2)
(rect.x + r_clamped, rect.y + r_clamped, @math.pi * 1.5), // Top-Left (3pi/2 to 2pi)
]
let perimeter_indices : Array[Int] = []
for c in corner_centers {
let (cx, cy, base_angle) = c
for s = 0; s <= segments; s = s + 1 {
// Skip duplicate starting vertex for subsequent corners
if s == 0 && !perimeter_indices.is_empty() {
continue
}
let angle = base_angle +
@math.pi * 0.5 * (s.to_double() / segments.to_double())
let px = cx + r_clamped * @math.cos(angle)
let py = cy + r_clamped * @math.sin(angle)
let idx = self.add_vertex(Vertex::new(px, py, color~))
perimeter_indices.push(idx)
}
}
let p_len = perimeter_indices.length()
for i = 0; i < p_len; i = i + 1 {
let next_i = (i + 1) % p_len
self.add_triangle(
center_idx,
perimeter_indices[i],
perimeter_indices[next_i],
)
}
}
}
///|
/// Tessellates a stroked rectangle border into the mesh.
pub fn Mesh::tessellate_rect_stroke(
self : Mesh,
rect : @math.Rect,
color : @color.Color,
stroke_width : Double,
corner_radius? : Double = 0.0,
) -> Unit {
let _ = corner_radius
if stroke_width <= 0.0 || rect.w <= 0.0 || rect.h <= 0.0 {
return
}
let half_sw = stroke_width * 0.5
let o_x = rect.x - half_sw
let o_y = rect.y - half_sw
let o_w = rect.w + stroke_width
let o_h = rect.h + stroke_width
let i_x = rect.x + half_sw
let i_y = rect.y + half_sw
let i_w = if rect.w > stroke_width { rect.w - stroke_width } else { 0.0 }
let i_h = if rect.h > stroke_width { rect.h - stroke_width } else { 0.0 }
// 4 outer vertices
let o0 = self.add_vertex(Vertex::new(o_x, o_y, color~))
let o1 = self.add_vertex(Vertex::new(o_x + o_w, o_y, color~))
let o2 = self.add_vertex(Vertex::new(o_x + o_w, o_y + o_h, color~))
let o3 = self.add_vertex(Vertex::new(o_x, o_y + o_h, color~))
// 4 inner vertices
let i0 = self.add_vertex(Vertex::new(i_x, i_y, color~))
let i1 = self.add_vertex(Vertex::new(i_x + i_w, i_y, color~))
let i2 = self.add_vertex(Vertex::new(i_x + i_w, i_y + i_h, color~))
let i3 = self.add_vertex(Vertex::new(i_x, i_y + i_h, color~))
// Top edge
self.add_quad(o0, o1, i1, i0)
// Right edge
self.add_quad(o1, o2, i2, i1)
// Bottom edge
self.add_quad(i3, i2, o2, o3)
// Left edge
self.add_quad(o0, i0, i3, o3)
}
///|
/// Tessellates a filled solid circle into a triangle fan.
pub fn Mesh::tessellate_circle(
self : Mesh,
center : @math.Vec2,
radius : Double,
color : @color.Color,
segments? : Int = 32,
) -> Unit {
if radius <= 0.0 || segments < 3 {
return
}
let center_idx = self.add_vertex(Vertex::new(center.x, center.y, color~))
let step = @math.pi * 2.0 / segments.to_double()
let first_perim = self.add_vertex(
Vertex::new(center.x + radius, center.y, color~),
)
let mut prev_idx = first_perim
for i = 1; i < segments; i = i + 1 {
let angle = step * i.to_double()
let px = center.x + radius * @math.cos(angle)
let py = center.y + radius * @math.sin(angle)
let cur_idx = self.add_vertex(Vertex::new(px, py, color~))
self.add_triangle(center_idx, prev_idx, cur_idx)
prev_idx = cur_idx
}
self.add_triangle(center_idx, prev_idx, first_perim)
}
///|
/// Tessellates a line segment into an oriented rectangle quad.
pub fn Mesh::tessellate_line(
self : Mesh,
start : @math.Vec2,
end : @math.Vec2,
color : @color.Color,
stroke_width : Double,
) -> Unit {
if stroke_width <= 0.0 {
return
}
let dx = end.x - start.x
let dy = end.y - start.y
let len_sq = dx * dx + dy * dy
if len_sq <= 0.0001 {
return
}
// Fast approximation or exact normal
let half_sw = stroke_width * 0.5
// Length using standard distance
let len = len_sq / (len_sq / 2.0 + 0.5) // positive fallback
// Normalize normal vector (-dy, dx)
let nx = -dy / len * half_sw
let ny = dx / len * half_sw
let i0 = self.add_vertex(Vertex::new(start.x + nx, start.y + ny, color~))
let i1 = self.add_vertex(Vertex::new(end.x + nx, end.y + ny, color~))
let i2 = self.add_vertex(Vertex::new(end.x - nx, end.y - ny, color~))
let i3 = self.add_vertex(Vertex::new(start.x - nx, start.y - ny, color~))
self.add_quad(i0, i1, i2, i3)
}
///|
/// Batch converts an entire DrawList into a single contiguous Mesh.
///
/// NOTE on primitive support and lossy conversions:
/// - Supports geometric vector primitives: Rect, RectStroke, Circle, and Line.
/// - LinearGradient is approximated as a solid rectangle filled with `start_col`,
/// as smooth vertex gradient interpolation requires multi-color vertex meshes or shader support.
/// - Text commands are skipped here; text geometry must be generated via a font atlas texture.
/// - Clip commands are not tessellated into vertex geometry; viewport clipping must be
/// managed via hardware scissor test or geometric polygon clipping.
pub fn Mesh::tessellate_draw_list(self : Mesh, draw_list : DrawList) -> Unit {
for cmd in draw_list.commands {
match cmd {
DrawCmd::Rect(rect, color, radius) =>
self.tessellate_rect(rect, color, radius)
DrawCmd::RectStroke(rect, color, stroke_width, radius) =>
self.tessellate_rect_stroke(
rect,
color,
stroke_width,
corner_radius=radius,
)
DrawCmd::Circle(center, radius, color) =>
self.tessellate_circle(center, radius, color)
DrawCmd::Line(start, end, color, stroke_width) =>
self.tessellate_line(start, end, color, stroke_width)
DrawCmd::LinearGradient(rect, _p0, _p1, start_col, _end_col, radius) =>
self.tessellate_rect(rect, start_col, radius)
_ => () // Text and Clip are managed via font atlas or Scissor tests
}
}
}
///|
/// Extension method to directly compile a DrawList into a Mesh.
pub fn DrawList::to_mesh(self : DrawList) -> Mesh {
let mesh = Mesh::new()
mesh.tessellate_draw_list(self)
mesh
}