From 8c23fa25b197915f0fd7dfa6b401e9da3618ec4a Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:12:31 -0600 Subject: [PATCH] Add exponentials --- src/bitmap.rs | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/consts.rs | 9 ++ src/main.rs | 45 ++++++++-- src/parser.rs | 104 +++++++++++++++-------- 4 files changed, 346 insertions(+), 43 deletions(-) create mode 100644 src/consts.rs diff --git a/src/bitmap.rs b/src/bitmap.rs index b7f50bb..0d81c7f 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -62,4 +62,235 @@ impl Bitmap { println!("{}", sixel_data); } +} + + +impl Bitmap { + /// Sets a pixel value with bounds checking + fn set_pixel(&mut self, x: usize, y: usize, value: u8) { + if x < self.width && y < self.height { + self.data[y * self.width + x] = value; + } + } + + /// Gets a pixel value with bounds checking + fn get_pixel(&self, x: usize, y: usize) -> u8 { + if x < self.width && y < self.height { + self.data[y * self.width + x] + } else { + 0 + } + } + + + /// Draws an antialiased line with arbitrary thickness + pub fn draw_line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, thickness: f32, color: u8) { + let x0 = x0 as f32; + let y0 = y0 as f32; + let x1 = x1 as f32; + let y1 = y1 as f32; + + let dx = x1 - x0; + let dy = y1 - y0; + let length = (dx * dx + dy * dy).sqrt(); + + if length < 0.001 { + // Handle degenerate case of zero-length line + // self.draw_thick_point(x0 as usize, y0 as usize, thickness, color); + return; + } + + // Unit vector perpendicular to the line + let perp_x = -dy / length; + let perp_y = dx / length; + + // Half thickness for calculations + let half_thickness = thickness * 0.5; + + // Calculate bounding box with some padding for antialiasing + let padding = (thickness * 0.5 + 1.0).ceil() as i32; + let min_x = ((x0.min(x1) - padding as f32).floor() as i32).max(0) as usize; + let max_x = ((x0.max(x1) + padding as f32).ceil() as i32).min(self.width as i32 - 1) as usize; + let min_y = ((y0.min(y1) - padding as f32).floor() as i32).max(0) as usize; + let max_y = ((y0.max(y1) + padding as f32).ceil() as i32).min(self.height as i32 - 1) as usize; + + // For each pixel in the bounding box, calculate distance to line + for y in min_y..=max_y { + for x in min_x..=max_x { + let px = x as f32; + let py = y as f32; + + // Calculate distance from point to line segment + let distance = self.point_to_line_segment_distance(px, py, x0, y0, x1, y1); + + // Calculate alpha based on distance and thickness + let alpha = self.calculate_alpha(distance, half_thickness); + + if alpha > 0.0 { + self.blend_pixel(x, y, color, alpha); + } + } + } + } + + /// Blends a pixel with the existing value using alpha blending + fn blend_pixel(&mut self, x: usize, y: usize, color: u8, alpha: f32) { + if x < self.width && y < self.height { + let existing = self.get_pixel(x, y) as f32; + let new_value = (existing * (1.0 - alpha) + color as f32 * alpha).round() as u8; + self.set_pixel(x, y, new_value); + } + } + + + /// Calculates the shortest distance from a point to a line segment + fn point_to_line_segment_distance(&self, px: f32, py: f32, x0: f32, y0: f32, x1: f32, y1: f32) -> f32 { + let dx = x1 - x0; + let dy = y1 - y0; + let length_sq = dx * dx + dy * dy; + + if length_sq < 0.001 { + // Line segment is actually a point + let dpx = px - x0; + let dpy = py - y0; + return (dpx * dpx + dpy * dpy).sqrt(); + } + + // Calculate parameter t for the closest point on the line segment + let t = ((px - x0) * dx + (py - y0) * dy) / length_sq; + let t = t.max(0.0).min(1.0); // Clamp to [0, 1] to stay on segment + + // Find the closest point on the line segment + let closest_x = x0 + t * dx; + let closest_y = y0 + t * dy; + + // Return distance to closest point + let dpx = px - closest_x; + let dpy = py - closest_y; + (dpx * dpx + dpy * dpy).sqrt() + } + + /// Calculates alpha value based on distance from line edge + fn calculate_alpha(&self, distance: f32, half_thickness: f32) -> f32 { + if distance <= half_thickness - 0.5 { + // Inside the line core - full opacity + 1.0 + } else if distance <= half_thickness + 0.5 { + // In the antialiasing zone - linear falloff + half_thickness + 0.5 - distance + } else { + // Outside the line - transparent + 0.0 + } + } + + + // // Draw a thick line by creating a capsule shape + // fn draw_line(&mut self, x1: usize, y1: usize, x2: usize, y2: usize, width: usize, color: u8) { + // let half_width = width as f32 / 2.0; + + // // Calculate line vector and length + // let dx = (x2 - x1) as f32; + // let dy = (y2 - y1) as f32; + // let length = (dx * dx + dy * dy).sqrt(); + + // // if length < 0.001 { + // // // Degenerate case: just draw a circle + // // self.draw_circle(x1, y1, half_width as i32, color); + // // return; + // // } + + // // Normalize the line direction + // let nx = dx / length; + // let ny = dy / length; + + // // Calculate perpendicular vector for line thickness + // let px = -ny * half_width; + // let py = nx * half_width; + + // // Get the four corners of the rectangle + // let corner1_x = (x1 as f32 + px) as usize; + // let corner1_y = (y1 as f32 + py) as usize; + // let corner2_x = (x1 as f32 - px) as usize; + // let corner2_y = (y1 as f32 - py) as usize; + // let corner3_x = (x2 as f32 - px) as usize; + // let corner3_y = (y2 as f32 - py) as usize; + // let corner4_x = (x2 as f32 + px) as usize; + // let corner4_y = (y2 as f32 + py) as usize; + + // // Draw the main rectangle body + // self.fill_quadrilateral( + // corner1_x, corner1_y, corner2_x, corner2_y, corner3_x, corner3_y, corner4_x, corner4_y, + // color, + // ); + + // // // Draw rounded ends + // // let radius = (half_width) as i32; + // // self.draw_circle(x1, y1, radius, color); + // // self.draw_circle(x2, y2, radius, color); + // } + + // fn fill_quadrilateral( + // &mut self, + // x1: usize, + // y1: usize, + // x2: usize, + // y2: usize, + // x3: usize, + // y3: usize, + // x4: usize, + // y4: usize, + // color: u8, + // ) { + // // Find bounding box + // let binding_x = [x1, x2, x3, x4]; + // let binding_y = [y1, y2, y3, y4]; + // let min_x = binding_x.iter().min().unwrap(); + // let max_x = binding_x.iter().max().unwrap(); + // let min_y = binding_y.iter().min().unwrap(); + // let max_y = binding_y.iter().max().unwrap(); + + // // For each point in bounding box, test if it's inside the quadrilateral + // for y in *min_y..=*max_y { + // for x in *min_x..=*max_x { + // if self.point_in_quad(x, y, x1, y1, x2, y2, x3, y3, x4, y4) { + // self.set_pixel(x, y, color); + // } + // } + // } + // } + + // // Test if a point is inside a quadrilateral using cross products + // fn point_in_quad( + // &self, + // px: usize, + // py: usize, + // x1: usize, + // y1: usize, + // x2: usize, + // y2: usize, + // x3: usize, + // y3: usize, + // x4: usize, + // y4: usize, + // ) -> bool { + // // Test against each edge of the quadrilateral + // let sign1 = self.cross_product(px - x1, py - y1, x2 - x1, y2 - y1); + // let sign2 = self.cross_product(px - x2, py - y2, x3 - x2, y3 - y2); + // let sign3 = self.cross_product(px - x3, py - y3, x4 - x3, y4 - y3); + // let sign4 = self.cross_product(px - x4, py - y4, x1 - x4, y1 - y4); + + // // Point is inside if all cross products have the same sign + // (sign1 >= 0 && sign2 >= 0 && sign3 >= 0 && sign4 >= 0) + // || (sign1 <= 0 && sign2 <= 0 && sign3 <= 0 && sign4 <= 0) + // } + + // // Calculate 2D cross product + // fn cross_product(&self, ax: usize, ay: usize, bx: usize, by: usize) -> usize { + // ax * by - ay * bx + // } + + // fn set_pixel(&mut self, x: usize, y: usize, color: u8) { + // self.data[y*self.width + x] = color; + // } } \ No newline at end of file diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..4180000 --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,9 @@ + +pub static LINE_WIDTH: f32 = 0.02; // Global scale +pub static MIN_LOCAL_SCALE: f32 = 0.1; // Global scale + +pub static FRACTION_SCALE: f32 = 0.95; // Local scale +pub static FRACTION_PADDING: f32 = 0.2; // global scale + +pub static SUPERSCRIPT_SCALE: f32 = 0.6; // local scale +pub static SUPERSCRIPT_Y_OFFSET: f32 = 0.4; // local scale \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 97a9c33..a4e212d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod fonts; mod parser; mod bitmap; +mod consts; use std::{rc::Rc, time::Instant}; @@ -17,15 +18,46 @@ fn main() -> Result<(), std::fmt::Error> { let element = KElement::LinearGroup(vec![ KElement::Fraction( - Rc::new(KElement::Integer(123)), + Rc::new(KElement::LinearGroup(vec![ + KElement::Integer(123), + KElement::Text("*".to_string()), + KElement::Superscript( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Integer(2)) + ) + ])), Rc::new(KElement::Integer(12)) ), // KElement::Integer(12), // KElement::Integer(12), KElement::Decimal(12.34), KElement::Fraction( - Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Integer(12)))), - Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Fraction(Rc::new(KElement::Integer(123)),Rc::new(KElement::Decimal(1234.5678)))))))))))))))), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Integer(12))) + ), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Fraction( + Rc::new(KElement::Integer(123)), + Rc::new(KElement::Decimal(1234.5678)) + )) + )) + )) + )) + )) + )) + )), ), ]); @@ -42,8 +74,8 @@ fn main() -> Result<(), std::fmt::Error> { } struct RusTeX { - settings: TeXSettings, - layout: Layout + pub settings: TeXSettings, + pub layout: Layout } struct TeXSettings { @@ -69,6 +101,7 @@ impl RusTeX { // } pub fn rasterize(&mut self, root_element: KElement) -> Bitmap { - root_element.rasterize(&mut self.layout, self.settings.scale) + let scale = self.settings.scale; + root_element.rasterize(self, scale) } } \ No newline at end of file diff --git a/src/parser.rs b/src/parser.rs index 19c4cbc..c6b4b08 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,36 +2,26 @@ use std::rc::Rc; use fontdue::layout::{Layout, TextStyle}; -use crate::{bitmap::Bitmap, fonts::FONTS}; +use crate::{bitmap::Bitmap, fonts::FONTS, RusTeX, consts::*}; + pub enum KElement { LinearGroup(Vec), Integer(i64), Decimal(f64), + Text(String), Fraction(Rc, Rc), + Superscript(Rc, Rc), } -pub static FRACTION_SCALE: f32 = 1.; -// pub static FRACTION_PADDING: usize = 10; - -// pub enum KSymbol { -// Text { -// data: String, -// x: f32, -// y: f32, -// scale: f32 -// }, -// None, -// } - impl KElement { - pub fn rasterize(&self, layout: &mut Layout, scale: f32) -> Bitmap { + pub fn rasterize(&self, globals: &mut RusTeX, current_scale: f32) -> Bitmap { match self { KElement::LinearGroup(elems) => { let (mut totalx, mut maxy) = (0,0); let mut positions = Vec::new(); for elem in elems { - let (x,y) = elem.get_bounds(layout, scale); + let (x,y) = elem.get_bounds(globals, current_scale); positions.push((totalx,y)); totalx += x; maxy = maxy.max(y); @@ -42,7 +32,7 @@ impl KElement { for i in 0..elems.len() { let elem = &elems[i]; let pos = positions[i]; - let new_bitmap = elem.rasterize(layout, scale); + let new_bitmap = elem.rasterize(globals, current_scale); // println!("{:?} {:?} {:?}", (bitmap.width, bitmap.height), pos, (new_bitmap.width, new_bitmap.height)); bitmap.overlay(&new_bitmap, pos.0, (maxy-pos.1)/2); } @@ -50,29 +40,39 @@ impl KElement { bitmap } KElement::Integer(i) => { - render_text_block(layout, &i.to_string(), scale) + render_text_block(&mut globals.layout, &i.to_string(), current_scale) }, KElement::Decimal(i) => { - render_text_block(layout, &i.to_string(), scale) + render_text_block(&mut globals.layout, &i.to_string(), current_scale) + }, + KElement::Text(str) => { + render_text_block(&mut globals.layout, &str, current_scale) }, KElement::Fraction(a,b) => { - let (ax,ay) = a.get_bounds(layout, scale * FRACTION_SCALE); - let (bx,by) = b.get_bounds(layout, scale * FRACTION_SCALE); - let (width, height) = (ax.max(bx), ay+by); + let padding = (FRACTION_PADDING * globals.settings.scale) as usize; + let (ax,ay) = a.get_bounds(globals, current_scale * FRACTION_SCALE); + let (bx,by) = b.get_bounds(globals, current_scale * FRACTION_SCALE); + + let (width, height) = ( + ax.max(bx) + padding*2, + ay+by + padding + ); let mut bitmap = Bitmap::new(width, height); - let bitmap_a = &mut a.rasterize(layout, scale * FRACTION_SCALE); - let bitmap_b = &mut b.rasterize(layout, scale * FRACTION_SCALE); + let bitmap_a = &mut a.rasterize(globals, current_scale * FRACTION_SCALE); + let bitmap_b = &mut b.rasterize(globals, current_scale * FRACTION_SCALE); if bitmap_a.width > bitmap_b.width { - bitmap.overlay(&bitmap_a, 0, 0); - bitmap.overlay(&bitmap_b, (bitmap_a.width-bitmap_b.width)/2, ay); + bitmap.overlay(&bitmap_a, padding, 0); + bitmap.overlay(&bitmap_b, padding+(bitmap_a.width-bitmap_b.width)/2, ay + padding); } else { - bitmap.overlay(&bitmap_a, (bitmap_b.width-bitmap_a.width)/2, 0); - bitmap.overlay(&bitmap_b, 0, ay); + bitmap.overlay(&bitmap_a, padding+(bitmap_b.width-bitmap_a.width)/2, 0); + bitmap.overlay(&bitmap_b, padding, ay + padding); } + bitmap.draw_line(0, ay+padding, bitmap.width, ay+padding, globals.settings.scale*LINE_WIDTH, 255); + bitmap // symbols.append(); @@ -80,30 +80,60 @@ impl KElement { // symbols } + KElement::Superscript(a, b) => { + let (ax, ay) = a.get_bounds(globals, current_scale); + let (bx, by) = b.get_bounds(globals, current_scale * SUPERSCRIPT_SCALE); + let yoffset = (by as f32*SUPERSCRIPT_Y_OFFSET) as usize; + + let (width, height) = ( + ax+bx, + ay + yoffset + ); + let mut bitmap = Bitmap::new(width, height); + + bitmap.overlay(&a.rasterize(globals, current_scale), 0, yoffset); + bitmap.overlay(&b.rasterize(globals, current_scale * SUPERSCRIPT_SCALE), ax, 0); + + bitmap + } } } - pub fn get_bounds(&self, layout: &mut Layout, scale: f32) -> (usize, usize) { + pub fn get_bounds(&self, globals: &mut RusTeX, current_scale: f32) -> (usize, usize) { match self { KElement::LinearGroup(elems) => { let (mut totalx, mut maxy) = (0,0); for elem in elems { - let (x,y) = elem.get_bounds(layout, scale); + let (x,y) = elem.get_bounds(globals, current_scale); totalx += x; maxy = maxy.max(y); } (totalx, maxy) } KElement::Integer(i) => { - measure_text_bounds(layout, &i.to_string(), scale) + measure_text_bounds(&mut globals.layout, &i.to_string(), current_scale) }, KElement::Decimal(i) => { - measure_text_bounds(layout, &i.to_string(), scale) + measure_text_bounds(&mut globals.layout, &i.to_string(), current_scale) + }, + KElement::Text(str) => { + measure_text_bounds(&mut globals.layout, &str, current_scale) }, KElement::Fraction(a,b) => { - let (ax,ay) = a.get_bounds(layout, scale * FRACTION_SCALE); - let (bx,by) = b.get_bounds(layout, scale * FRACTION_SCALE); - (ax.max(bx), ay+by) + let (ax,ay) = a.get_bounds(globals, current_scale * FRACTION_SCALE); + let (bx,by) = b.get_bounds(globals, current_scale * FRACTION_SCALE); + ( + (ax.max(bx)) + 2*(FRACTION_PADDING * globals.settings.scale) as usize, + ay+by + (FRACTION_PADDING * globals.settings.scale) as usize + ) }, + KElement::Superscript(a, b) => { + let (ax, ay) = a.get_bounds(globals, current_scale); + let (bx, by) = b.get_bounds(globals, current_scale * SUPERSCRIPT_SCALE); + ( + ax+bx, + ay + (by as f32*SUPERSCRIPT_Y_OFFSET) as usize + ) + } } } @@ -116,10 +146,10 @@ impl KElement { // // _ => (0.,0.) // // } // // } -// pub fn rasterize(&self, layout: &mut Layout, bitmap: &mut Bitmap) { +// pub fn rasterize(&self, globals.layout: &mut Layout, bitmap: &mut Bitmap) { // match self { // KSymbol::Text { data, x, y, scale } => { -// let new_bitmap = render_text_block(layout, data, *scale); +// let new_bitmap = render_text_block(globals.layout, data, *scale); // bitmap.overlay(&new_bitmap, *x as usize, *y as usize); // }, // KSymbol::None => {},