Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Thanks for your interest in contributing!

- Rust stable toolchain.
- Run tests: `cargo test`.
- Run the original full-size randomized suites: `ITRIANGLE_FULL_RANDOM_TESTS=1 cargo test`.
- Run formatter: `cargo fmt`.
- Run lints (optional): `cargo clippy`.

Expand Down
1 change: 1 addition & 0 deletions DebugApp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
19 changes: 19 additions & 0 deletions DebugApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# iTriangle debug applications

`uniform_grid` is an interactive visual check for `UniformTriangulatable` and
`IntUniformGrid`. It shows the editable input boundary, the separately colored
boundary resampled by `SliceContour`, lattice candidates after point
containment, points remaining after the `edge_length / 3` edge-clearance
filter, and the resulting Delaunay mesh. Every resampled boundary edge is at
most `edge_length` long. Optional centroid-net relaxation can be enabled in the
sidebar; its iteration limit defaults to 40. Convex decomposition and centroid
net overlays can be toggled independently on top of the Delaunay mesh.

Run it from the iTriangle repository root:

```sh
cargo run --manifest-path DebugApp/uniform_grid/Cargo.toml
```

Drag a yellow vertex to edit the active contour. Use the mouse wheel to zoom,
and the middle or right mouse button to pan.
8 changes: 8 additions & 0 deletions DebugApp/debug_ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "debug_ui"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
eframe = "^0.34.0"
62 changes: 62 additions & 0 deletions DebugApp/debug_ui/src/camera.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use eframe::egui::{Pos2, Rect, Vec2};

#[derive(Clone, Copy, Debug)]
pub struct Camera {
pub center: Pos2,
pub zoom: f32,
pub min_zoom: f32,
pub max_zoom: f32,
}

impl Default for Camera {
fn default() -> Self {
Self {
center: Pos2::ZERO,
zoom: 1.0,
min_zoom: 0.02,
max_zoom: 256.0,
}
}
}

impl Camera {
pub fn screen_from_world(&self, rect: Rect, world: Pos2) -> Pos2 {
let origin = rect.center();

Pos2::new(
origin.x + (world.x - self.center.x) * self.zoom,
origin.y - (world.y - self.center.y) * self.zoom,
)
}

pub fn world_from_screen(&self, rect: Rect, screen: Pos2) -> Pos2 {
let origin = rect.center();

Pos2::new(
self.center.x + (screen.x - origin.x) / self.zoom,
self.center.y - (screen.y - origin.y) / self.zoom,
)
}

pub fn world_delta_from_screen_delta(&self, delta: Vec2) -> Vec2 {
Vec2::new(delta.x / self.zoom, -delta.y / self.zoom)
}

pub fn pan_by_screen_delta(&mut self, delta: Vec2) {
self.center -= self.world_delta_from_screen_delta(delta);
}

pub fn zoom_at_screen_pos(&mut self, rect: Rect, screen_pos: Pos2, factor: f32) {
let before = self.world_from_screen(rect, screen_pos);
self.zoom = (self.zoom * factor).clamp(self.min_zoom, self.max_zoom);
let after = self.world_from_screen(rect, screen_pos);
self.center += before - after;
}

pub fn visible_world_rect(&self, rect: Rect) -> Rect {
Rect::from_two_pos(
self.world_from_screen(rect, rect.left_bottom()),
self.world_from_screen(rect, rect.right_top()),
)
}
}
117 changes: 117 additions & 0 deletions DebugApp/debug_ui/src/grid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use crate::camera::Camera;
use eframe::egui::{
Align2, Color32, FontId, Painter, PointerButton, Rect, Response, Stroke, Ui, Vec2,
};

#[derive(Clone, Copy, Debug)]
pub struct Grid {
pub base_step: f32,
pub min_screen_step: f32,
pub max_screen_step: f32,
pub minor_stroke: Stroke,
pub major_stroke: Stroke,
pub axis_stroke: Stroke,
pub background: Color32,
}

impl Default for Grid {
fn default() -> Self {
Self {
base_step: 16.0,
min_screen_step: 12.0,
max_screen_step: 48.0,
minor_stroke: Stroke::new(1.0_f32, Color32::from_gray(36)),
major_stroke: Stroke::new(1.0_f32, Color32::from_gray(56)),
axis_stroke: Stroke::new(1.5_f32, Color32::from_rgb(88, 102, 124)),
background: Color32::from_rgb(18, 20, 24),
}
}
}

impl Grid {
pub fn handle_input(&self, ui: &Ui, response: &Response, rect: Rect, camera: &mut Camera) {
if response.hovered() {
let scroll_y = ui.input(|input| input.smooth_scroll_delta.y);

if scroll_y.abs() > f32::EPSILON {
let factor = (scroll_y * 0.0018).exp();

if let Some(pointer_pos) = ui.input(|input| input.pointer.hover_pos()) {
camera.zoom_at_screen_pos(rect, pointer_pos, factor);
}
}
}

if response.dragged_by(PointerButton::Middle)
|| response.dragged_by(PointerButton::Secondary)
{
camera.pan_by_screen_delta(response.drag_delta());
}
}

pub fn paint(&self, painter: &Painter, rect: Rect, camera: &Camera) {
painter.rect_filled(rect, 0.0, self.background);

let world = camera.visible_world_rect(rect);
let step = self.step_for_zoom(camera.zoom);
let min_x_index = (world.left() / step).floor() as i32 - 1;
let max_x_index = (world.right() / step).ceil() as i32 + 1;
let min_y_index = (world.top() / step).floor() as i32 - 1;
let max_y_index = (world.bottom() / step).ceil() as i32 + 1;

for index in min_x_index..=max_x_index {
let x = index as f32 * step;
let stroke = self.stroke_for_index(index);
let a = camera.screen_from_world(rect, eframe::egui::pos2(x, world.bottom()));
let b = camera.screen_from_world(rect, eframe::egui::pos2(x, world.top()));
painter.line_segment([a, b], stroke);
}

for index in min_y_index..=max_y_index {
let y = index as f32 * step;
let stroke = self.stroke_for_index(index);
let a = camera.screen_from_world(rect, eframe::egui::pos2(world.left(), y));
let b = camera.screen_from_world(rect, eframe::egui::pos2(world.right(), y));
painter.line_segment([a, b], stroke);
}
}

fn step_for_zoom(&self, zoom: f32) -> f32 {
let mut step = self.base_step;

while step * zoom < self.min_screen_step {
step *= 2.0;
}

while step * zoom > self.max_screen_step {
step *= 0.5;
}

step
}

fn stroke_for_index(&self, index: i32) -> Stroke {
if index == 0 {
self.axis_stroke
} else if index.rem_euclid(5) == 0 {
self.major_stroke
} else {
self.minor_stroke
}
}
}

pub fn paint_camera_readout(painter: &Painter, rect: Rect, camera: &Camera) {
let text = format!(
"center ({:.1}, {:.1}) zoom {:.2}x",
camera.center.x, camera.center.y, camera.zoom
);

painter.text(
rect.left_top() + Vec2::new(12.0, 10.0),
Align2::LEFT_TOP,
text,
FontId::monospace(12.0),
Color32::from_rgb(196, 202, 214),
);
}
4 changes: 4 additions & 0 deletions DebugApp/debug_ui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod camera;
pub mod grid;

pub use eframe::egui;
11 changes: 11 additions & 0 deletions DebugApp/uniform_grid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "uniform_grid"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
eframe = "^0.34.0"
debug_ui = { path = "../debug_ui" }
i_triangle = { path = "../../iTriangle" }
i_overlay = { path = "../../../iOverlay/iOverlay" }
89 changes: 89 additions & 0 deletions DebugApp/uniform_grid/src/examples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
pub type Point = [f32; 2];
pub type Shape = Vec<Vec<Point>>;

#[derive(Clone)]
pub struct GridExample {
pub name: &'static str,
pub shape: Shape,
pub edge_length: f32,
}

pub fn load_examples() -> Vec<GridExample> {
vec![
GridExample {
name: "concave polygon",
shape: vec![vec![
[-220.0, -135.0],
[35.0, -175.0],
[220.0, -70.0],
[80.0, 5.0],
[205.0, 155.0],
[-25.0, 105.0],
[-205.0, 175.0],
[-115.0, 10.0],
]],
edge_length: 42.0,
},
GridExample {
name: "shape with hole",
shape: vec![
vec![
[-225.0, -170.0],
[225.0, -170.0],
[225.0, 170.0],
[-225.0, 170.0],
],
// Clockwise winding makes this contour a hole for NonZero fill.
vec![[-95.0, -65.0], [-95.0, 75.0], [105.0, 75.0], [105.0, -65.0]],
],
edge_length: 38.0,
},
GridExample {
name: "narrow contour",
shape: vec![vec![
[-235.0, -28.0],
[235.0, -28.0],
[235.0, 28.0],
[-235.0, 28.0],
]],
edge_length: 72.0,
},
GridExample {
name: "narrow passage",
shape: vec![vec![
[-230.0, -175.0],
[-25.0, -175.0],
[-25.0, 45.0],
[25.0, 45.0],
[25.0, -175.0],
[230.0, -175.0],
[230.0, 175.0],
[25.0, 175.0],
[25.0, 95.0],
[-25.0, 95.0],
[-25.0, 175.0],
[-230.0, 175.0],
]],
edge_length: 50.0,
},
GridExample {
name: "two holes",
shape: vec![
vec![
[-240.0, -175.0],
[240.0, -175.0],
[215.0, 175.0],
[-215.0, 175.0],
],
vec![
[-150.0, -55.0],
[-150.0, 70.0],
[-45.0, 70.0],
[-45.0, -55.0],
],
vec![[45.0, -80.0], [45.0, 45.0], [160.0, 45.0], [160.0, -80.0]],
],
edge_length: 34.0,
},
]
}
Loading
Loading