mirror of
https://github.com/hannobraun/Fornjot
synced 2025-06-28 06:06:06 +00:00
Start new experiment as copy of previous one
This commit is contained in:
parent
22a9c5506f
commit
67c2327b9a
5
experiments/2025-03-18/.gitignore
vendored
Normal file
5
experiments/2025-03-18/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Cargo
|
||||||
|
/target
|
||||||
|
|
||||||
|
# Fornjot
|
||||||
|
/*.3mf
|
5
experiments/2025-03-18/.vscode/settings.json
vendored
Normal file
5
experiments/2025-03-18/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"rust-analyzer.check.command": "clippy",
|
||||||
|
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
|
||||||
|
}
|
2932
experiments/2025-03-18/Cargo.lock
generated
Normal file
2932
experiments/2025-03-18/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
experiments/2025-03-18/Cargo.toml
Normal file
28
experiments/2025-03-18/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[workspace]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "fj"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "*"
|
||||||
|
glyphon = "*"
|
||||||
|
iter_fixed = "*"
|
||||||
|
itertools = "*"
|
||||||
|
pollster = "*"
|
||||||
|
spade = "*"
|
||||||
|
threemf = "*"
|
||||||
|
wgpu = "*"
|
||||||
|
winit = "*"
|
||||||
|
|
||||||
|
[dependencies.bytemuck]
|
||||||
|
version = "*"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
|
[dependencies.geo]
|
||||||
|
version = "*"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.glam]
|
||||||
|
version = "*"
|
||||||
|
features = ["bytemuck"]
|
44
experiments/2025-03-18/README.md
Normal file
44
experiments/2025-03-18/README.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Fornjot - Experiment 2025-03-18
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This experiment is packaged as a single application. Run it with `cargo run`.
|
||||||
|
This should open a window and also create a 3MF file in this directory.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
It has become clear, that Fornjot's current architecture is at a local maximum.
|
||||||
|
I also think it is too complicated for what it does, and suspect that a simpler
|
||||||
|
architecture would serve us much better going forward.
|
||||||
|
|
||||||
|
While it's certainly not impossible to address this piecemeal, through
|
||||||
|
incremental improvements (which is the approach that I usually prefer), I don't
|
||||||
|
think this is the best course of action.
|
||||||
|
|
||||||
|
Because while I don't consider the architecture to be very good, it is still
|
||||||
|
consistent and self-reinforcing. Whenever I try to simplify one aspect, I run
|
||||||
|
into the problem that it's there for a reason; that other aspects of the
|
||||||
|
architecture depend on it being the way it is.
|
||||||
|
|
||||||
|
And while I haven't figured out yet, how to break out of this situation, I do
|
||||||
|
have quite a few unproven ideas on how an improved architecture would look like,
|
||||||
|
redesigned from the ground up using the experience I've gained over the last few
|
||||||
|
years.
|
||||||
|
|
||||||
|
This experiment is the third in a series meant to prove out those ideas. The
|
||||||
|
results should provide a clearer picture of what is possible, and how the
|
||||||
|
current architecture can be evolved.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
This experiment builds on [the second one](../2024-12-09/). There are two
|
||||||
|
objectives:
|
||||||
|
|
||||||
|
- Simplify the `Object` trait by removing as much functionality as possible.
|
||||||
|
Compensate for the loss of insight into an object's structure by experimenting
|
||||||
|
with other means of providing required debug information.
|
||||||
|
- Expand the existing b-rep primitives, adding support for curved surfaces.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The experiment is still ongoing.
|
12
experiments/2025-03-18/shell.nix
Normal file
12
experiments/2025-03-18/shell.nix
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
|
let
|
||||||
|
libPath = with pkgs; lib.makeLibraryPath [
|
||||||
|
libxkbcommon
|
||||||
|
vulkan-loader
|
||||||
|
wayland
|
||||||
|
];
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
LD_LIBRARY_PATH = "${libPath}";
|
||||||
|
}
|
134
experiments/2025-03-18/src/app.rs
Normal file
134
experiments/2025-03-18/src/app.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
use std::{collections::BTreeSet, sync::Arc};
|
||||||
|
|
||||||
|
use winit::{
|
||||||
|
application::ApplicationHandler,
|
||||||
|
event::{ElementState, KeyEvent, WindowEvent},
|
||||||
|
event_loop::{ActiveEventLoop, EventLoop},
|
||||||
|
keyboard::{Key, NamedKey},
|
||||||
|
window::{Window, WindowAttributes, WindowId},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{object::HandleAny, render::Renderer, view::OperationView};
|
||||||
|
|
||||||
|
pub fn run(shape: HandleAny) -> anyhow::Result<()> {
|
||||||
|
let view = OperationView::new(shape);
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
|
||||||
|
let mut app = App {
|
||||||
|
view,
|
||||||
|
window: None,
|
||||||
|
renderer: None,
|
||||||
|
pressed_keys: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
view: OperationView,
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
renderer: Option<Renderer>,
|
||||||
|
pressed_keys: BTreeSet<Key>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let (window, renderer) = match init(event_loop) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Initialization error: `{err:?}`");
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.window = Some(window);
|
||||||
|
self.renderer = Some(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
_: WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
let (Some(window), Some(renderer)) =
|
||||||
|
(self.window.as_ref(), self.renderer.as_mut())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput {
|
||||||
|
event:
|
||||||
|
KeyEvent {
|
||||||
|
logical_key: Key::Named(NamedKey::Escape),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput {
|
||||||
|
event:
|
||||||
|
KeyEvent {
|
||||||
|
logical_key, state, ..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
match state {
|
||||||
|
ElementState::Pressed => {
|
||||||
|
if self.pressed_keys.contains(&logical_key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElementState::Released => {
|
||||||
|
self.pressed_keys.remove(&logical_key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match logical_key {
|
||||||
|
Key::Named(NamedKey::ArrowRight) => {
|
||||||
|
self.view.selected_mut().select_last();
|
||||||
|
}
|
||||||
|
Key::Named(NamedKey::ArrowLeft) => {
|
||||||
|
self.view.parent_of_selected_mut().select_none();
|
||||||
|
}
|
||||||
|
Key::Named(NamedKey::ArrowDown) => {
|
||||||
|
self.view.parent_of_selected_mut().select_next();
|
||||||
|
}
|
||||||
|
Key::Named(NamedKey::ArrowUp) => {
|
||||||
|
self.view.parent_of_selected_mut().select_previous();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
if let Err(err) = renderer.render(&self.view) {
|
||||||
|
eprintln!("Render error: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
) -> anyhow::Result<(Arc<Window>, Renderer)> {
|
||||||
|
let window = {
|
||||||
|
let window = event_loop.create_window(WindowAttributes::default())?;
|
||||||
|
Arc::new(window)
|
||||||
|
};
|
||||||
|
let renderer = pollster::block_on(Renderer::new(window.clone()))?;
|
||||||
|
|
||||||
|
Ok((window, renderer))
|
||||||
|
}
|
45
experiments/2025-03-18/src/export.rs
Normal file
45
experiments/2025-03-18/src/export.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use std::{collections::BTreeMap, fs::File};
|
||||||
|
|
||||||
|
use crate::object::Object;
|
||||||
|
|
||||||
|
pub fn export(op: &dyn Object) -> anyhow::Result<()> {
|
||||||
|
let tri_mesh = op.tri_mesh();
|
||||||
|
|
||||||
|
let mut indices_by_vertex = BTreeMap::new();
|
||||||
|
|
||||||
|
let mut points = Vec::new();
|
||||||
|
let mut triangles = Vec::new();
|
||||||
|
|
||||||
|
for triangle in tri_mesh.external_triangles() {
|
||||||
|
let triangle = triangle.points.map(|point| {
|
||||||
|
*indices_by_vertex.entry(point).or_insert_with(|| {
|
||||||
|
let index = points.len();
|
||||||
|
points.push(point);
|
||||||
|
index
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
triangles.push(triangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mesh = threemf::Mesh {
|
||||||
|
vertices: threemf::model::Vertices {
|
||||||
|
vertex: points
|
||||||
|
.into_iter()
|
||||||
|
.map(|point| point.coords.components.map(|coord| coord.value()))
|
||||||
|
.map(|[x, y, z]| threemf::model::Vertex { x, y, z })
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
triangles: threemf::model::Triangles {
|
||||||
|
triangle: triangles
|
||||||
|
.into_iter()
|
||||||
|
.map(|[v1, v2, v3]| threemf::model::Triangle { v1, v2, v3 })
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = File::create("output.3mf")?;
|
||||||
|
threemf::write(output, mesh)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
1
experiments/2025-03-18/src/extra/mod.rs
Normal file
1
experiments/2025-03-18/src/extra/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod triangulate;
|
146
experiments/2025-03-18/src/extra/triangulate.rs
Normal file
146
experiments/2025-03-18/src/extra/triangulate.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{BTreeSet, VecDeque},
|
||||||
|
mem,
|
||||||
|
};
|
||||||
|
|
||||||
|
use geo::{Contains, Coord, LineString, Polygon};
|
||||||
|
use spade::Triangulation;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::{MeshTriangle, TriMesh, Triangle},
|
||||||
|
math::Point,
|
||||||
|
topology::face::Face,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn triangulate(face: &Face) -> TriMesh {
|
||||||
|
let points = points(face);
|
||||||
|
let triangles = triangles(&points);
|
||||||
|
|
||||||
|
let polygon = polygon(&points);
|
||||||
|
let triangles_in_face = triangles
|
||||||
|
.into_iter()
|
||||||
|
.filter(|triangle| {
|
||||||
|
let points = triangle.map(|point| point.point_surface);
|
||||||
|
let triangle = Triangle { points };
|
||||||
|
|
||||||
|
let [x, y] = triangle.center().coords.components.map(|s| s.value());
|
||||||
|
polygon.contains(&Coord { x, y })
|
||||||
|
})
|
||||||
|
.map(|triangle| {
|
||||||
|
let points = triangle.map(|point| point.point_vertex);
|
||||||
|
MeshTriangle {
|
||||||
|
inner: Triangle { points },
|
||||||
|
is_internal: face.is_internal,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut mesh = TriMesh::new();
|
||||||
|
mesh.triangles.extend(triangles_in_face);
|
||||||
|
|
||||||
|
mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
fn points(face: &Face) -> Vec<TriangulationPoint> {
|
||||||
|
face.half_edges
|
||||||
|
.iter()
|
||||||
|
.map(|half_edge| {
|
||||||
|
// Here, we project a 3D point (from the vertex) into the face's
|
||||||
|
// surface, creating a 2D point. Through the surface, this 2D
|
||||||
|
// point has a position in 3D space.
|
||||||
|
//
|
||||||
|
// But this position isn't necessarily going to be the same as
|
||||||
|
// the position of the original 3D point, due to numerical
|
||||||
|
// inaccuracy.
|
||||||
|
//
|
||||||
|
// This doesn't matter. Neither does the fact, that other faces
|
||||||
|
// might share the same vertices and project them into their own
|
||||||
|
// surfaces, creating more redundancy.
|
||||||
|
//
|
||||||
|
// The reason that it doesn't, is that we're using the projected
|
||||||
|
// 2D points _only_ for this local triangulation. Once that
|
||||||
|
// tells us how the different 3D points must connect, we use the
|
||||||
|
// original 3D points to build those triangles. We never convert
|
||||||
|
// the 2D points back into 3D.
|
||||||
|
let point_surface =
|
||||||
|
face.surface.geometry.project_point(half_edge.start.point);
|
||||||
|
|
||||||
|
TriangulationPoint {
|
||||||
|
point_surface,
|
||||||
|
point_vertex: half_edge.start.point,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn triangles(points: &[TriangulationPoint]) -> Vec<[TriangulationPoint; 3]> {
|
||||||
|
let mut triangulation = spade::ConstrainedDelaunayTriangulation::<_>::new();
|
||||||
|
|
||||||
|
// We're passing duplicate points to the triangulation here. It doesn't seem
|
||||||
|
// to mind though.
|
||||||
|
triangulation
|
||||||
|
.add_constraint_edges(points.iter().copied(), true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
triangulation
|
||||||
|
.inner_faces()
|
||||||
|
.map(|triangle| triangle.vertices().map(|vertex| *vertex.data()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polygon(points: &[TriangulationPoint]) -> Polygon {
|
||||||
|
// This is a placeholder implementation that is probably not well-tested and
|
||||||
|
// probably doesn't support polygons with multiple holes.
|
||||||
|
|
||||||
|
let mut line_strings = VecDeque::new();
|
||||||
|
let mut current_line_string = Vec::new();
|
||||||
|
let mut visited_points = BTreeSet::new();
|
||||||
|
|
||||||
|
for point in points {
|
||||||
|
if visited_points.contains(point) {
|
||||||
|
line_strings.push_back(mem::take(&mut current_line_string));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [x, y] = point.point_surface.coords.components.map(|s| s.value());
|
||||||
|
current_line_string.push(Coord { x, y });
|
||||||
|
visited_points.insert(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (exterior, interiors) = if let Some(exterior) = line_strings.pop_front()
|
||||||
|
{
|
||||||
|
line_strings.push_back(mem::take(&mut current_line_string));
|
||||||
|
|
||||||
|
let exterior = LineString::new(exterior);
|
||||||
|
let interiors = line_strings
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|line_string| {
|
||||||
|
(!line_string.is_empty())
|
||||||
|
.then_some(LineString::new(line_string))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(exterior, interiors)
|
||||||
|
} else {
|
||||||
|
let exterior = LineString::new(current_line_string);
|
||||||
|
let interiors = Vec::new();
|
||||||
|
|
||||||
|
(exterior, interiors)
|
||||||
|
};
|
||||||
|
|
||||||
|
Polygon::new(exterior, interiors)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
struct TriangulationPoint {
|
||||||
|
point_surface: Point<2>,
|
||||||
|
point_vertex: Point<3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl spade::HasPosition for TriangulationPoint {
|
||||||
|
type Scalar = f64;
|
||||||
|
|
||||||
|
fn position(&self) -> spade::Point2<Self::Scalar> {
|
||||||
|
let [x, y] = self.point_surface.coords.components.map(|s| s.value());
|
||||||
|
spade::Point2 { x, y }
|
||||||
|
}
|
||||||
|
}
|
11
experiments/2025-03-18/src/geometry/mod.rs
Normal file
11
experiments/2025-03-18/src/geometry/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod sketch;
|
||||||
|
mod surface;
|
||||||
|
mod tri_mesh;
|
||||||
|
mod triangle;
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
sketch::Sketch,
|
||||||
|
surface::SurfaceGeometry,
|
||||||
|
tri_mesh::{MeshTriangle, TriMesh},
|
||||||
|
triangle::Triangle,
|
||||||
|
};
|
66
experiments/2025-03-18/src/geometry/sketch.rs
Normal file
66
experiments/2025-03-18/src/geometry/sketch.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
math::Point,
|
||||||
|
object::Handle,
|
||||||
|
topology::{
|
||||||
|
face::Face, half_edge::HalfEdge, surface::Surface, vertex::Vertex,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Sketch {
|
||||||
|
pub points: Vec<Point<2>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sketch {
|
||||||
|
pub fn to_face(&self, surface: Handle<Surface>) -> Face {
|
||||||
|
let mut vertices_by_local_point: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||||
|
let vertices = self
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|point| {
|
||||||
|
let point = surface.geometry.point_from_local(point);
|
||||||
|
let vertex = Handle::new(Vertex::new(point));
|
||||||
|
|
||||||
|
vertices_by_local_point
|
||||||
|
.entry(point)
|
||||||
|
.or_default()
|
||||||
|
.push(vertex.clone());
|
||||||
|
|
||||||
|
vertex
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut coincident_vertices = BTreeSet::new();
|
||||||
|
for vertices in vertices_by_local_point.into_values() {
|
||||||
|
if vertices.len() > 1 {
|
||||||
|
coincident_vertices.extend(vertices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_edges = vertices.into_iter().circular_tuple_windows().map(
|
||||||
|
|(start, end)| {
|
||||||
|
let is_internal = coincident_vertices.contains(&start)
|
||||||
|
&& coincident_vertices.contains(&end);
|
||||||
|
|
||||||
|
Handle::new(HalfEdge { start, is_internal })
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Face::new(surface, half_edges, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, P> From<I> for Sketch
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = P>,
|
||||||
|
P: Into<Point<2>>,
|
||||||
|
{
|
||||||
|
fn from(points: I) -> Self {
|
||||||
|
let points = points.into_iter().map(Into::into).collect();
|
||||||
|
Self { points }
|
||||||
|
}
|
||||||
|
}
|
26
experiments/2025-03-18/src/geometry/surface.rs
Normal file
26
experiments/2025-03-18/src/geometry/surface.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use crate::math::{Plane, Point, Vector};
|
||||||
|
|
||||||
|
pub trait SurfaceGeometry {
|
||||||
|
fn point_from_local(&self, point: Point<2>) -> Point<3>;
|
||||||
|
fn project_point(&self, point: Point<3>) -> Point<2>;
|
||||||
|
fn flip(&self) -> Box<dyn SurfaceGeometry>;
|
||||||
|
fn translate(&self, offset: Vector<3>) -> Box<dyn SurfaceGeometry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SurfaceGeometry for Plane {
|
||||||
|
fn point_from_local(&self, point: Point<2>) -> Point<3> {
|
||||||
|
self.point_from_local(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_point(&self, point: Point<3>) -> Point<2> {
|
||||||
|
self.project_point(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flip(&self) -> Box<dyn SurfaceGeometry> {
|
||||||
|
Box::new((*self).flip())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translate(&self, offset: Vector<3>) -> Box<dyn SurfaceGeometry> {
|
||||||
|
Box::new((*self).translate(offset))
|
||||||
|
}
|
||||||
|
}
|
35
experiments/2025-03-18/src/geometry/tri_mesh.rs
Normal file
35
experiments/2025-03-18/src/geometry/tri_mesh.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use super::Triangle;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TriMesh {
|
||||||
|
pub triangles: Vec<MeshTriangle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TriMesh {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
triangles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(mut self, other: Self) -> Self {
|
||||||
|
self.triangles.extend(other.triangles);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_triangles(&self) -> impl Iterator<Item = Triangle<3>> {
|
||||||
|
self.triangles.iter().map(|triangle| triangle.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn external_triangles(&self) -> impl Iterator<Item = Triangle<3>> {
|
||||||
|
self.triangles.iter().filter_map(|triangle| {
|
||||||
|
(!triangle.is_internal).then_some(triangle.inner)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MeshTriangle {
|
||||||
|
pub inner: Triangle<3>,
|
||||||
|
pub is_internal: bool,
|
||||||
|
}
|
51
experiments/2025-03-18/src/geometry/triangle.rs
Normal file
51
experiments/2025-03-18/src/geometry/triangle.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
math::Point,
|
||||||
|
object::{HandleAny, Object},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{MeshTriangle, TriMesh};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct Triangle<const D: usize> {
|
||||||
|
pub points: [Point<D>; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize> Triangle<D> {
|
||||||
|
pub fn center(&self) -> Point<D> {
|
||||||
|
let [a, b, c] = self.points;
|
||||||
|
let coords = (a.coords + b.coords + c.coords) / 3.;
|
||||||
|
Point { coords }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, const D: usize> From<[P; 3]> for Triangle<D>
|
||||||
|
where
|
||||||
|
P: Into<Point<D>>,
|
||||||
|
{
|
||||||
|
fn from(points: [P; 3]) -> Self {
|
||||||
|
Self {
|
||||||
|
points: points.map(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for Triangle<3> {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Triangle")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
TriMesh {
|
||||||
|
triangles: vec![MeshTriangle {
|
||||||
|
inner: *self,
|
||||||
|
is_internal: false,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
22
experiments/2025-03-18/src/main.rs
Normal file
22
experiments/2025-03-18/src/main.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#![allow(clippy::module_inception)]
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod export;
|
||||||
|
mod extra;
|
||||||
|
mod geometry;
|
||||||
|
mod math;
|
||||||
|
mod model;
|
||||||
|
mod object;
|
||||||
|
mod operations;
|
||||||
|
mod render;
|
||||||
|
mod topology;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let model = model::model();
|
||||||
|
|
||||||
|
export::export(&model)?;
|
||||||
|
app::run(model)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
7
experiments/2025-03-18/src/math/bivector.rs
Normal file
7
experiments/2025-03-18/src/math/bivector.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use super::Vector;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Bivector<const D: usize> {
|
||||||
|
pub a: Vector<D>,
|
||||||
|
pub b: Vector<D>,
|
||||||
|
}
|
10
experiments/2025-03-18/src/math/mod.rs
Normal file
10
experiments/2025-03-18/src/math/mod.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
mod bivector;
|
||||||
|
mod plane;
|
||||||
|
mod point;
|
||||||
|
mod scalar;
|
||||||
|
mod vector;
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
bivector::Bivector, plane::Plane, point::Point, scalar::Scalar,
|
||||||
|
vector::Vector,
|
||||||
|
};
|
80
experiments/2025-03-18/src/math/plane.rs
Normal file
80
experiments/2025-03-18/src/math/plane.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use super::{Bivector, Point, Vector};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Plane {
|
||||||
|
pub origin: Point<3>,
|
||||||
|
pub coords: Bivector<3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plane {
|
||||||
|
pub fn from_points([a, b, c]: [Point<3>; 3]) -> Self {
|
||||||
|
Self {
|
||||||
|
origin: a,
|
||||||
|
coords: Bivector { a: b - a, b: c - a },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn u(&self) -> Vector<3> {
|
||||||
|
self.coords.a
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn v(&self) -> Vector<3> {
|
||||||
|
self.coords.b
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normal(&self) -> Vector<3> {
|
||||||
|
self.u().cross(self.v()).normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn point_from_local(&self, point: impl Into<Point<2>>) -> Point<3> {
|
||||||
|
let [u, v] = point.into().coords.components;
|
||||||
|
self.origin + self.coords.a * u + self.coords.b * v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn project_point(&self, point: impl Into<Point<3>>) -> Point<2> {
|
||||||
|
let point = point.into();
|
||||||
|
let origin_to_point = point - self.origin;
|
||||||
|
|
||||||
|
let min_distance_plane_to_point = origin_to_point.dot(&self.normal());
|
||||||
|
let point_in_plane =
|
||||||
|
point - self.normal() * min_distance_plane_to_point;
|
||||||
|
let origin_to_point_in_plane = point_in_plane - self.origin;
|
||||||
|
|
||||||
|
let u = origin_to_point_in_plane.dot(&self.u());
|
||||||
|
let v = origin_to_point_in_plane.dot(&self.v());
|
||||||
|
|
||||||
|
Point::from([u, v])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flip(mut self) -> Self {
|
||||||
|
self.coords.b = -self.coords.b;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate(self, offset: impl Into<Vector<3>>) -> Self {
|
||||||
|
Self {
|
||||||
|
origin: self.origin + offset,
|
||||||
|
coords: self.coords,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::math::{Bivector, Point, Vector};
|
||||||
|
|
||||||
|
use super::Plane;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_point() {
|
||||||
|
let plane = Plane {
|
||||||
|
origin: Point::from([1., 1., 1.]),
|
||||||
|
coords: Bivector {
|
||||||
|
a: Vector::from([1., 0., 0.]),
|
||||||
|
b: Vector::from([0., 1., 0.]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(plane.project_point([2., 2., 2.]), Point::from([1., 1.]));
|
||||||
|
}
|
||||||
|
}
|
49
experiments/2025-03-18/src/math/point.rs
Normal file
49
experiments/2025-03-18/src/math/point.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use std::ops;
|
||||||
|
|
||||||
|
use super::Vector;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct Point<const D: usize> {
|
||||||
|
pub coords: Vector<D>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, const D: usize> From<V> for Point<D>
|
||||||
|
where
|
||||||
|
V: Into<Vector<D>>,
|
||||||
|
{
|
||||||
|
fn from(coords: V) -> Self {
|
||||||
|
Self {
|
||||||
|
coords: coords.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, const D: usize> ops::Add<V> for Point<D>
|
||||||
|
where
|
||||||
|
V: Into<Vector<D>>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: V) -> Self::Output {
|
||||||
|
let other = other.into();
|
||||||
|
let coords = self.coords + other;
|
||||||
|
Self { coords }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize> ops::Sub<Point<D>> for Point<D> {
|
||||||
|
type Output = Vector<D>;
|
||||||
|
|
||||||
|
fn sub(self, other: Point<D>) -> Self::Output {
|
||||||
|
self.coords - other.coords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize> ops::Sub<Vector<D>> for Point<D> {
|
||||||
|
type Output = Point<D>;
|
||||||
|
|
||||||
|
fn sub(self, other: Vector<D>) -> Self::Output {
|
||||||
|
let coords = self.coords - other;
|
||||||
|
Self { coords }
|
||||||
|
}
|
||||||
|
}
|
116
experiments/2025-03-18/src/math/scalar.rs
Normal file
116
experiments/2025-03-18/src/math/scalar.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use std::{cmp::Ordering, ops};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Scalar {
|
||||||
|
value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scalar {
|
||||||
|
pub fn new(value: f64) -> Self {
|
||||||
|
if value.is_nan() {
|
||||||
|
panic!("`Scalar` value must not be NaN");
|
||||||
|
}
|
||||||
|
if value.is_infinite() {
|
||||||
|
panic!("`Scalar` value must not be infinite. Value: `{value}`");
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self::new(0.)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> f64 {
|
||||||
|
self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sqrt(self) -> Self {
|
||||||
|
let value = self.value().sqrt();
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Scalar {}
|
||||||
|
|
||||||
|
impl Ord for Scalar {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
let Some(ordering) = self.value.partial_cmp(&other.value) else {
|
||||||
|
unreachable!(
|
||||||
|
"Failed to compare `Scalar` values `{}` and `{}`",
|
||||||
|
self.value, other.value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ordering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Scalar {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for Scalar {
|
||||||
|
fn from(value: f64) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ops::Add<S> for Scalar
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: S) -> Self::Output {
|
||||||
|
let value = self.value() + other.into().value();
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ops::Div<S> for Scalar
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn div(self, other: S) -> Self::Output {
|
||||||
|
let value = self.value() / other.into().value();
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ops::Mul<S> for Scalar
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, other: S) -> Self::Output {
|
||||||
|
let value = self.value() * other.into().value();
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Neg for Scalar {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self::Output {
|
||||||
|
let value = -self.value();
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ops::Sub<S> for Scalar
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, other: S) -> Self::Output {
|
||||||
|
let value = self.value() - other.into().value();
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
135
experiments/2025-03-18/src/math/vector.rs
Normal file
135
experiments/2025-03-18/src/math/vector.rs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
use std::ops;
|
||||||
|
|
||||||
|
use iter_fixed::IntoIteratorFixed;
|
||||||
|
|
||||||
|
use super::Scalar;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct Vector<const D: usize> {
|
||||||
|
pub components: [Scalar; D],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize> Vector<D> {
|
||||||
|
pub fn magnitude(&self) -> Scalar {
|
||||||
|
self.dot(self).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize(self) -> Self {
|
||||||
|
self / self.magnitude()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dot(&self, other: &Self) -> Scalar {
|
||||||
|
self.components
|
||||||
|
.into_iter()
|
||||||
|
.zip(other.components)
|
||||||
|
.map(|(a, b)| a * b)
|
||||||
|
.reduce(|a, b| a + b)
|
||||||
|
.unwrap_or(Scalar::zero())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vector<3> {
|
||||||
|
pub fn cross(self, other: Self) -> Self {
|
||||||
|
let [ax, ay, az] = self.components;
|
||||||
|
let [bx, by, bz] = other.components;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
components: [
|
||||||
|
ay * bz - az * by,
|
||||||
|
az * bx - ax * bz,
|
||||||
|
ax * by - ay * bx,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, const D: usize> From<[S; D]> for Vector<D>
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
fn from(components: [S; D]) -> Self {
|
||||||
|
Self {
|
||||||
|
components: components.map(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, const D: usize> ops::Add<V> for Vector<D>
|
||||||
|
where
|
||||||
|
V: Into<Vector<D>>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: V) -> Self::Output {
|
||||||
|
let other = other.into();
|
||||||
|
|
||||||
|
let components = self
|
||||||
|
.components
|
||||||
|
.into_iter_fixed()
|
||||||
|
.zip(other.components)
|
||||||
|
.map(|(a, b)| a + b)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self { components }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, const D: usize> ops::Div<S> for Vector<D>
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn div(self, scalar: S) -> Self::Output {
|
||||||
|
let scalar = scalar.into();
|
||||||
|
let components = self.components.map(|component| component / scalar);
|
||||||
|
Self { components }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, const D: usize> ops::Mul<S> for Vector<D>
|
||||||
|
where
|
||||||
|
S: Into<Scalar>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, scalar: S) -> Self::Output {
|
||||||
|
let scalar = scalar.into();
|
||||||
|
|
||||||
|
let components = self
|
||||||
|
.components
|
||||||
|
.into_iter_fixed()
|
||||||
|
.map(|v| v * scalar)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self { components }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize> ops::Neg for Vector<D> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self::Output {
|
||||||
|
self * -1.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, const D: usize> ops::Sub<V> for Vector<D>
|
||||||
|
where
|
||||||
|
V: Into<Vector<D>>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, other: V) -> Self::Output {
|
||||||
|
let other = other.into();
|
||||||
|
|
||||||
|
let components = self
|
||||||
|
.components
|
||||||
|
.into_iter_fixed()
|
||||||
|
.zip(other.components)
|
||||||
|
.map(|(a, b)| a - b)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self { components }
|
||||||
|
}
|
||||||
|
}
|
46
experiments/2025-03-18/src/model.rs
Normal file
46
experiments/2025-03-18/src/model.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use crate::{
|
||||||
|
geometry::Sketch,
|
||||||
|
math::{Bivector, Plane, Point, Vector},
|
||||||
|
object::{Handle, HandleAny},
|
||||||
|
operations::sweep::SweepExt,
|
||||||
|
topology::surface::Surface,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn model() -> HandleAny {
|
||||||
|
let top = {
|
||||||
|
let sketch = Sketch::from([
|
||||||
|
// outer boundary
|
||||||
|
[-1., -1.],
|
||||||
|
[1., -1.],
|
||||||
|
[1., 1.],
|
||||||
|
[-1., 1.],
|
||||||
|
// connection to inner boundary
|
||||||
|
[-1., -1.],
|
||||||
|
// inner boundary
|
||||||
|
[-0.5, -0.5],
|
||||||
|
[-0.5, 0.5],
|
||||||
|
[0.5, 0.5],
|
||||||
|
[0.5, -0.5],
|
||||||
|
// connection to outer boundary
|
||||||
|
[-0.5, -0.5],
|
||||||
|
// half-edge between last and first vertex is implicit, so we're done here
|
||||||
|
]);
|
||||||
|
|
||||||
|
let surface = Handle::new(Surface {
|
||||||
|
geometry: Box::new(Plane {
|
||||||
|
origin: Point::from([0., 0., 1.]),
|
||||||
|
coords: Bivector {
|
||||||
|
a: Vector::from([1., 0., 0.]),
|
||||||
|
b: Vector::from([0., 1., 0.]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let face = sketch.to_face(surface);
|
||||||
|
Handle::new(face)
|
||||||
|
};
|
||||||
|
|
||||||
|
let solid = top.sweep([0., 0., -2.]);
|
||||||
|
|
||||||
|
HandleAny::new(solid)
|
||||||
|
}
|
74
experiments/2025-03-18/src/object/handle.rs
Normal file
74
experiments/2025-03-18/src/object/handle.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use std::{cmp::Ordering, fmt, ops::Deref, rc::Rc};
|
||||||
|
|
||||||
|
use super::{HandleAny, Object};
|
||||||
|
|
||||||
|
pub struct Handle<T> {
|
||||||
|
inner: Rc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Handle<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Rc::new(inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Handle<T>
|
||||||
|
where
|
||||||
|
T: Object + 'static,
|
||||||
|
{
|
||||||
|
pub fn to_any(&self) -> HandleAny {
|
||||||
|
self.clone().into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_any(self) -> HandleAny {
|
||||||
|
HandleAny { inner: self.inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for Handle<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for Handle<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for Handle<T> {}
|
||||||
|
|
||||||
|
impl<T> Ord for Handle<T> {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
Rc::as_ptr(&self.inner)
|
||||||
|
.cast::<()>()
|
||||||
|
.cmp(&Rc::as_ptr(&other.inner).cast::<()>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for Handle<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.inner, &other.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialOrd for Handle<T> {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for Handle<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("Handle")
|
||||||
|
.field("inner", &Rc::as_ptr(&self.inner))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
33
experiments/2025-03-18/src/object/handle_any.rs
Normal file
33
experiments/2025-03-18/src/object/handle_any.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use std::{fmt, rc::Rc};
|
||||||
|
|
||||||
|
use crate::geometry::TriMesh;
|
||||||
|
|
||||||
|
use super::Object;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HandleAny {
|
||||||
|
pub(super) inner: Rc<dyn Object>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HandleAny {
|
||||||
|
pub fn new(op: impl Object + 'static) -> Self {
|
||||||
|
Self { inner: Rc::new(op) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for HandleAny {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.inner.display(f)?;
|
||||||
|
write!(f, " ({:?})", Rc::as_ptr(&self.inner))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
self.inner.tri_mesh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
self.inner.children()
|
||||||
|
}
|
||||||
|
}
|
5
experiments/2025-03-18/src/object/mod.rs
Normal file
5
experiments/2025-03-18/src/object/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod handle;
|
||||||
|
mod handle_any;
|
||||||
|
mod traits;
|
||||||
|
|
||||||
|
pub use self::{handle::Handle, handle_any::HandleAny, traits::Object};
|
28
experiments/2025-03-18/src/object/traits.rs
Normal file
28
experiments/2025-03-18/src/object/traits.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::geometry::TriMesh;
|
||||||
|
|
||||||
|
use super::HandleAny;
|
||||||
|
|
||||||
|
pub trait Object {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result;
|
||||||
|
fn tri_mesh(&self) -> TriMesh;
|
||||||
|
fn children(&self) -> Vec<HandleAny>;
|
||||||
|
|
||||||
|
fn label(&self) -> OperationDisplay
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
OperationDisplay { op: self as &_ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OperationDisplay<'r> {
|
||||||
|
pub op: &'r dyn Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for OperationDisplay<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.op.display(f)
|
||||||
|
}
|
||||||
|
}
|
76
experiments/2025-03-18/src/operations/connect.rs
Normal file
76
experiments/2025-03-18/src/operations/connect.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use crate::{
|
||||||
|
math::Plane,
|
||||||
|
object::Handle,
|
||||||
|
topology::{
|
||||||
|
face::Face, half_edge::HalfEdge, solid::Solid, surface::Surface,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait ConnectExt {
|
||||||
|
/// # Connect two faces by creating a side wall of faces from their vertices
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics, if the two faces provided do not have the same number of
|
||||||
|
/// half-edges.
|
||||||
|
///
|
||||||
|
/// Panics, if an internal half-edge of one face would connect to an
|
||||||
|
/// external half-edge of the other.
|
||||||
|
///
|
||||||
|
/// ## Implementation Note
|
||||||
|
///
|
||||||
|
/// This method has very particular (and undocumented) requirements about
|
||||||
|
/// the orientation of the two faces relative to each other, and will
|
||||||
|
/// happily generate invalid geometry, if those undocumented requirements
|
||||||
|
/// aren't met.
|
||||||
|
///
|
||||||
|
/// It should be seen as more of a placeholder for a real implementation of
|
||||||
|
/// this operation.
|
||||||
|
fn connect(self, other: Self) -> Solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectExt for Handle<Face> {
|
||||||
|
fn connect(self, other: Self) -> Solid {
|
||||||
|
assert_eq!(
|
||||||
|
self.half_edges.len(),
|
||||||
|
other.half_edges.len(),
|
||||||
|
"Can only connect faces that have the same number of vertices.",
|
||||||
|
);
|
||||||
|
|
||||||
|
let side_faces = self
|
||||||
|
.half_edges_with_end_vertex()
|
||||||
|
.zip(other.half_edges_with_end_vertex())
|
||||||
|
.map(|((q, r), (t, s))| {
|
||||||
|
let is_internal = match [q.is_internal, t.is_internal] {
|
||||||
|
[true, true] => true,
|
||||||
|
[false, false] => false,
|
||||||
|
_ => {
|
||||||
|
panic!(
|
||||||
|
"Trying to connect an internal half-edge of one \
|
||||||
|
face to an external half-edge of another"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let surface = Handle::new(Surface {
|
||||||
|
geometry: Box::new(Plane::from_points(
|
||||||
|
[&q.start, r, s].map(|vertex| vertex.point),
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
let face = Face::new(
|
||||||
|
surface,
|
||||||
|
[&q.start, r, s, &t.start].map(|vertex| {
|
||||||
|
Handle::new(HalfEdge {
|
||||||
|
start: vertex.clone(),
|
||||||
|
is_internal: false,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
is_internal,
|
||||||
|
);
|
||||||
|
Handle::new(face)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Solid::new([self, other].into_iter().chain(side_faces))
|
||||||
|
}
|
||||||
|
}
|
25
experiments/2025-03-18/src/operations/flip.rs
Normal file
25
experiments/2025-03-18/src/operations/flip.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use crate::{
|
||||||
|
object::Handle,
|
||||||
|
topology::{face::Face, surface::Surface},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait FlipExt {
|
||||||
|
fn flip(&self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlipExt for Face {
|
||||||
|
fn flip(&self) -> Self {
|
||||||
|
Face::new(
|
||||||
|
Handle::new(self.surface.flip()),
|
||||||
|
self.half_edges.clone(),
|
||||||
|
self.is_internal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlipExt for Surface {
|
||||||
|
fn flip(&self) -> Self {
|
||||||
|
let geometry = self.geometry.flip();
|
||||||
|
Self { geometry }
|
||||||
|
}
|
||||||
|
}
|
4
experiments/2025-03-18/src/operations/mod.rs
Normal file
4
experiments/2025-03-18/src/operations/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod connect;
|
||||||
|
pub mod flip;
|
||||||
|
pub mod sweep;
|
||||||
|
pub mod translate;
|
31
experiments/2025-03-18/src/operations/sweep.rs
Normal file
31
experiments/2025-03-18/src/operations/sweep.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use crate::{
|
||||||
|
math::Vector,
|
||||||
|
object::Handle,
|
||||||
|
topology::{face::Face, solid::Solid},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{connect::ConnectExt, flip::FlipExt, translate::TranslateExt};
|
||||||
|
|
||||||
|
pub trait SweepExt {
|
||||||
|
/// # Sweep a face along a path, creating a solid
|
||||||
|
///
|
||||||
|
/// ## Implementation Note
|
||||||
|
///
|
||||||
|
/// This method has very particular (and undocumented) requirements about
|
||||||
|
/// the orientation of the two faces relative to each other, and will
|
||||||
|
/// happily generate invalid geometry, if those undocumented requirements
|
||||||
|
/// aren't met.
|
||||||
|
///
|
||||||
|
/// It should be seen as more of a placeholder for a real implementation of
|
||||||
|
/// this operation.
|
||||||
|
fn sweep(self, path: impl Into<Vector<3>>) -> Solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SweepExt for Handle<Face> {
|
||||||
|
fn sweep(self, path: impl Into<Vector<3>>) -> Solid {
|
||||||
|
let bottom = self;
|
||||||
|
let top = Handle::new(bottom.flip().translate(path));
|
||||||
|
|
||||||
|
top.connect(bottom)
|
||||||
|
}
|
||||||
|
}
|
51
experiments/2025-03-18/src/operations/translate.rs
Normal file
51
experiments/2025-03-18/src/operations/translate.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use crate::{
|
||||||
|
math::Vector,
|
||||||
|
object::Handle,
|
||||||
|
topology::{
|
||||||
|
face::Face, half_edge::HalfEdge, surface::Surface, vertex::Vertex,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait TranslateExt {
|
||||||
|
fn translate(&self, offset: impl Into<Vector<3>>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranslateExt for Face {
|
||||||
|
fn translate(&self, offset: impl Into<Vector<3>>) -> Self {
|
||||||
|
let offset = offset.into();
|
||||||
|
|
||||||
|
Face::new(
|
||||||
|
Handle::new(self.surface.translate(offset)),
|
||||||
|
self.half_edges
|
||||||
|
.iter()
|
||||||
|
.map(|half_edge| Handle::new(half_edge.translate(offset))),
|
||||||
|
self.is_internal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranslateExt for HalfEdge {
|
||||||
|
fn translate(&self, offset: impl Into<Vector<3>>) -> Self {
|
||||||
|
let start = self.start.translate(offset);
|
||||||
|
|
||||||
|
HalfEdge {
|
||||||
|
start: Handle::new(start),
|
||||||
|
is_internal: self.is_internal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranslateExt for Surface {
|
||||||
|
fn translate(&self, offset: impl Into<Vector<3>>) -> Self {
|
||||||
|
let offset = offset.into();
|
||||||
|
let geometry = self.geometry.translate(offset);
|
||||||
|
Self { geometry }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranslateExt for Vertex {
|
||||||
|
fn translate(&self, offset: impl Into<Vector<3>>) -> Self {
|
||||||
|
let offset = offset.into();
|
||||||
|
Vertex::new(self.point + offset)
|
||||||
|
}
|
||||||
|
}
|
71
experiments/2025-03-18/src/render/geometry.rs
Normal file
71
experiments/2025-03-18/src/render/geometry.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use glam::Vec3;
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
|
use crate::object::Object;
|
||||||
|
|
||||||
|
use super::vertex::Vertex;
|
||||||
|
|
||||||
|
pub struct Geometry {
|
||||||
|
pub vertices: wgpu::Buffer,
|
||||||
|
pub indices: wgpu::Buffer,
|
||||||
|
pub num_indices: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Geometry {
|
||||||
|
pub fn new(device: &wgpu::Device, operation: &dyn Object) -> Self {
|
||||||
|
let tri_mesh = operation.tri_mesh();
|
||||||
|
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
let mut vertices = Vec::new();
|
||||||
|
|
||||||
|
for triangle in tri_mesh.all_triangles() {
|
||||||
|
let triangle = triangle.points.each_ref().map(|point| {
|
||||||
|
Vec3::from(
|
||||||
|
point.coords.components.map(|coord| coord.value() as f32),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let normal = {
|
||||||
|
let [a, b, c] = triangle;
|
||||||
|
|
||||||
|
let ab = b - a;
|
||||||
|
let ac = c - a;
|
||||||
|
|
||||||
|
ab.cross(ac)
|
||||||
|
};
|
||||||
|
|
||||||
|
for point in triangle {
|
||||||
|
let index = vertices.len() as u32;
|
||||||
|
let vertex = Vertex {
|
||||||
|
position: point.into(),
|
||||||
|
normal: normal.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
indices.push(index);
|
||||||
|
vertices.push(vertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(num_indices) = indices.len().try_into() else {
|
||||||
|
panic!("Unsupported number of indices: `{}`", indices.len());
|
||||||
|
};
|
||||||
|
|
||||||
|
let vertices =
|
||||||
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: None,
|
||||||
|
contents: bytemuck::cast_slice(&vertices),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
});
|
||||||
|
let indices =
|
||||||
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: None,
|
||||||
|
contents: bytemuck::cast_slice(&indices),
|
||||||
|
usage: wgpu::BufferUsages::INDEX,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
vertices,
|
||||||
|
indices,
|
||||||
|
num_indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
experiments/2025-03-18/src/render/mod.rs
Normal file
8
experiments/2025-03-18/src/render/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mod geometry;
|
||||||
|
mod pipeline;
|
||||||
|
mod renderer;
|
||||||
|
mod text;
|
||||||
|
mod uniforms;
|
||||||
|
mod vertex;
|
||||||
|
|
||||||
|
pub use self::renderer::Renderer;
|
145
experiments/2025-03-18/src/render/pipeline.rs
Normal file
145
experiments/2025-03-18/src/render/pipeline.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
|
use super::{geometry::Geometry, uniforms::Uniforms, vertex::Vertex};
|
||||||
|
|
||||||
|
pub struct Pipeline {
|
||||||
|
render_pipeline: wgpu::RenderPipeline,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pipeline {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
surface_configuration: &wgpu::SurfaceConfiguration,
|
||||||
|
) -> Self {
|
||||||
|
let aspect_ratio = surface_configuration.width as f32
|
||||||
|
/ surface_configuration.height as f32;
|
||||||
|
let uniforms =
|
||||||
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: None,
|
||||||
|
contents: bytemuck::cast_slice(&[Uniforms::from_transform(
|
||||||
|
default_transform(aspect_ratio),
|
||||||
|
)]),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group_layout =
|
||||||
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: None,
|
||||||
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::VERTEX,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout =
|
||||||
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: None,
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let shader_module = device.create_shader_module(wgpu::include_wgsl!(
|
||||||
|
"shaders/triangles.wgsl"
|
||||||
|
));
|
||||||
|
|
||||||
|
let render_pipeline =
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: None,
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader_module,
|
||||||
|
entry_point: Some("vertex"),
|
||||||
|
compilation_options:
|
||||||
|
wgpu::PipelineCompilationOptions::default(),
|
||||||
|
buffers: &[wgpu::VertexBufferLayout {
|
||||||
|
array_stride: size_of::<Vertex>()
|
||||||
|
as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: Vertex::ATTRIBUTES,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader_module,
|
||||||
|
entry_point: Some("fragment"),
|
||||||
|
compilation_options:
|
||||||
|
wgpu::PipelineCompilationOptions::default(),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: surface_configuration.format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::all(),
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
unclipped_depth: false,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: true,
|
||||||
|
depth_compare: wgpu::CompareFunction::Less,
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: None,
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniforms.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
Pipeline {
|
||||||
|
render_pipeline,
|
||||||
|
bind_group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(
|
||||||
|
&self,
|
||||||
|
render_pass: &mut wgpu::RenderPass,
|
||||||
|
geometry: &Geometry,
|
||||||
|
) {
|
||||||
|
if geometry.num_indices > 0 {
|
||||||
|
render_pass.set_index_buffer(
|
||||||
|
geometry.indices.slice(..),
|
||||||
|
wgpu::IndexFormat::Uint32,
|
||||||
|
);
|
||||||
|
render_pass.set_vertex_buffer(0, geometry.vertices.slice(..));
|
||||||
|
render_pass.set_pipeline(&self.render_pipeline);
|
||||||
|
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
render_pass.draw_indexed(0..geometry.num_indices, 0, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_transform(aspect_ratio: f32) -> Mat4 {
|
||||||
|
let fov_y_radians = std::f32::consts::PI / 2.;
|
||||||
|
let z_near = 0.1;
|
||||||
|
let z_far = 10.;
|
||||||
|
|
||||||
|
Mat4::perspective_rh(fov_y_radians, aspect_ratio, z_near, z_far)
|
||||||
|
* Mat4::from_translation(Vec3::new(0., 0., -4.))
|
||||||
|
* Mat4::from_rotation_x(-PI / 4.)
|
||||||
|
* Mat4::from_rotation_z(PI / 4.)
|
||||||
|
}
|
146
experiments/2025-03-18/src/render/renderer.rs
Normal file
146
experiments/2025-03-18/src/render/renderer.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
use crate::view::OperationView;
|
||||||
|
|
||||||
|
use super::{geometry::Geometry, pipeline::Pipeline, text::TextRenderer};
|
||||||
|
|
||||||
|
pub struct Renderer {
|
||||||
|
pub surface: wgpu::Surface<'static>,
|
||||||
|
pub device: wgpu::Device,
|
||||||
|
pub queue: wgpu::Queue,
|
||||||
|
pub surface_config: wgpu::SurfaceConfiguration,
|
||||||
|
pub pipeline: Pipeline,
|
||||||
|
pub depth_view: wgpu::TextureView,
|
||||||
|
pub text_renderer: TextRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
pub async fn new(window: Arc<Window>) -> anyhow::Result<Self> {
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
let surface = instance.create_surface(window.clone())?;
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| anyhow!("Failed to request adapter"))?;
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
label: None,
|
||||||
|
required_features: wgpu::Features::default(),
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
memory_hints: wgpu::MemoryHints::default(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
let surface_config = surface
|
||||||
|
.get_default_config(&adapter, size.width, size.height)
|
||||||
|
.ok_or_else(|| anyhow!("Failed to get default surface config"))?;
|
||||||
|
surface.configure(&device, &surface_config);
|
||||||
|
|
||||||
|
let pipeline = Pipeline::new(&device, &surface_config);
|
||||||
|
|
||||||
|
let depth_view = {
|
||||||
|
let depth_texture =
|
||||||
|
device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: surface_config.width,
|
||||||
|
height: surface_config.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||||
|
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
depth_texture.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_renderer = TextRenderer::new(
|
||||||
|
&device,
|
||||||
|
&queue,
|
||||||
|
&surface_config,
|
||||||
|
window.scale_factor() as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
surface_config,
|
||||||
|
pipeline,
|
||||||
|
depth_view,
|
||||||
|
text_renderer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, operations: &OperationView) -> anyhow::Result<()> {
|
||||||
|
let selected_operation = operations.selected();
|
||||||
|
|
||||||
|
let geometry = Geometry::new(&self.device, selected_operation);
|
||||||
|
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
|
||||||
|
let frame = self.surface.get_current_texture().unwrap();
|
||||||
|
let color_view = frame
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut render_pass =
|
||||||
|
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(
|
||||||
|
wgpu::RenderPassColorAttachment {
|
||||||
|
view: &color_view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)],
|
||||||
|
depth_stencil_attachment: Some(
|
||||||
|
wgpu::RenderPassDepthStencilAttachment {
|
||||||
|
view: &self.depth_view,
|
||||||
|
depth_ops: Some(wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(1.0),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
}),
|
||||||
|
stencil_ops: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.pipeline.draw(&mut render_pass, &geometry);
|
||||||
|
self.text_renderer.render(
|
||||||
|
operations,
|
||||||
|
&self.device,
|
||||||
|
&self.queue,
|
||||||
|
&self.surface_config,
|
||||||
|
&mut render_pass,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit(Some(encoder.finish()));
|
||||||
|
frame.present();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
32
experiments/2025-03-18/src/render/shaders/triangles.wgsl
Normal file
32
experiments/2025-03-18/src/render/shaders/triangles.wgsl
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
struct Uniforms {
|
||||||
|
transform: mat4x4<f32>,
|
||||||
|
transform_for_normals: mat4x4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> uniforms: Uniforms;
|
||||||
|
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec3<f32>,
|
||||||
|
@location(1) normal: vec3<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) normal: vec3<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertex(in: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.position = uniforms.transform * vec4(in.position, 1.0);
|
||||||
|
out.normal = (uniforms.transform_for_normals * vec4(in.normal, 0.0)).xyz;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
var color = vec4(in.normal, 1.0);
|
||||||
|
return color;
|
||||||
|
}
|
141
experiments/2025-03-18/src/render/text.rs
Normal file
141
experiments/2025-03-18/src/render/text.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use crate::{object::Object, view::OperationView};
|
||||||
|
|
||||||
|
pub struct TextRenderer {
|
||||||
|
text_atlas: glyphon::TextAtlas,
|
||||||
|
viewport: glyphon::Viewport,
|
||||||
|
text_renderer: glyphon::TextRenderer,
|
||||||
|
font_system: glyphon::FontSystem,
|
||||||
|
swash_cache: glyphon::SwashCache,
|
||||||
|
scale_factor: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextRenderer {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
surface_config: &wgpu::SurfaceConfiguration,
|
||||||
|
scale_factor: f32,
|
||||||
|
) -> Self {
|
||||||
|
let cache = glyphon::Cache::new(device);
|
||||||
|
let swash_cache = glyphon::SwashCache::new();
|
||||||
|
|
||||||
|
let mut text_atlas = glyphon::TextAtlas::new(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&cache,
|
||||||
|
surface_config.format,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut viewport = glyphon::Viewport::new(device, &cache);
|
||||||
|
viewport.update(
|
||||||
|
queue,
|
||||||
|
glyphon::Resolution {
|
||||||
|
width: surface_config.width,
|
||||||
|
height: surface_config.height,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let text_renderer = glyphon::TextRenderer::new(
|
||||||
|
&mut text_atlas,
|
||||||
|
device,
|
||||||
|
wgpu::MultisampleState::default(),
|
||||||
|
Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: true,
|
||||||
|
depth_compare: wgpu::CompareFunction::Less,
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let font_system = glyphon::FontSystem::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
text_atlas,
|
||||||
|
viewport,
|
||||||
|
text_renderer,
|
||||||
|
font_system,
|
||||||
|
swash_cache,
|
||||||
|
scale_factor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
operations: &OperationView,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
surface_config: &wgpu::SurfaceConfiguration,
|
||||||
|
render_pass: &mut wgpu::RenderPass,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut buffer = glyphon::Buffer::new(
|
||||||
|
&mut self.font_system,
|
||||||
|
glyphon::Metrics {
|
||||||
|
font_size: 16.,
|
||||||
|
line_height: 16.,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (op, selected, indent_level) in operations.operations() {
|
||||||
|
let mut attrs = glyphon::Attrs::new();
|
||||||
|
|
||||||
|
if selected {
|
||||||
|
attrs = attrs.color(glyphon::Color::rgb(0, 127, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
|
||||||
|
for _ in 0..indent_level {
|
||||||
|
write!(line, "\t")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(line, "{}", op.label())?;
|
||||||
|
|
||||||
|
buffer.lines.push(glyphon::BufferLine::new(
|
||||||
|
line,
|
||||||
|
glyphon::cosmic_text::LineEnding::Lf,
|
||||||
|
glyphon::AttrsList::new(attrs),
|
||||||
|
glyphon::Shaping::Advanced,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.shape_until_scroll(&mut self.font_system, false);
|
||||||
|
|
||||||
|
let text_area = glyphon::TextArea {
|
||||||
|
buffer: &buffer,
|
||||||
|
left: 0.,
|
||||||
|
top: 0.,
|
||||||
|
scale: self.scale_factor,
|
||||||
|
bounds: glyphon::TextBounds {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: surface_config.width as i32,
|
||||||
|
bottom: surface_config.height as i32,
|
||||||
|
},
|
||||||
|
default_color: glyphon::Color::rgb(0, 0, 0),
|
||||||
|
custom_glyphs: &[],
|
||||||
|
};
|
||||||
|
|
||||||
|
self.text_renderer
|
||||||
|
.prepare(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&mut self.font_system,
|
||||||
|
&mut self.text_atlas,
|
||||||
|
&self.viewport,
|
||||||
|
[text_area],
|
||||||
|
&mut self.swash_cache,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.text_renderer.render(
|
||||||
|
&self.text_atlas,
|
||||||
|
&self.viewport,
|
||||||
|
render_pass,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
19
experiments/2025-03-18/src/render/uniforms.rs
Normal file
19
experiments/2025-03-18/src/render/uniforms.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use glam::Mat4;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Uniforms {
|
||||||
|
pub transform: Mat4,
|
||||||
|
pub transform_for_normals: Mat4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Uniforms {
|
||||||
|
pub fn from_transform(transform: Mat4) -> Self {
|
||||||
|
let transform_for_normals = transform.inverse().transpose();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
transform,
|
||||||
|
transform_for_normals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
experiments/2025-03-18/src/render/vertex.rs
Normal file
13
experiments/2025-03-18/src/render/vertex.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Vertex {
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub normal: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vertex {
|
||||||
|
pub const ATTRIBUTES: &[wgpu::VertexAttribute] = &wgpu::vertex_attr_array![
|
||||||
|
0 => Float32x3,
|
||||||
|
1 => Float32x3,
|
||||||
|
];
|
||||||
|
}
|
58
experiments/2025-03-18/src/topology/face.rs
Normal file
58
experiments/2025-03-18/src/topology/face.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
extra::triangulate::triangulate,
|
||||||
|
geometry::TriMesh,
|
||||||
|
object::{Handle, HandleAny, Object},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{half_edge::HalfEdge, surface::Surface, vertex::Vertex};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Face {
|
||||||
|
pub surface: Handle<Surface>,
|
||||||
|
pub half_edges: Vec<Handle<HalfEdge>>,
|
||||||
|
pub is_internal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Face {
|
||||||
|
pub fn new(
|
||||||
|
surface: Handle<Surface>,
|
||||||
|
half_edges: impl IntoIterator<Item = Handle<HalfEdge>>,
|
||||||
|
is_internal: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
surface,
|
||||||
|
half_edges: half_edges.into_iter().collect(),
|
||||||
|
is_internal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn half_edges_with_end_vertex(
|
||||||
|
&self,
|
||||||
|
) -> impl Iterator<Item = (&Handle<HalfEdge>, &Handle<Vertex>)> {
|
||||||
|
self.half_edges
|
||||||
|
.iter()
|
||||||
|
.circular_tuple_windows()
|
||||||
|
.map(|(a, b)| (a, &b.start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for Face {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Face")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
triangulate(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
self.half_edges
|
||||||
|
.iter()
|
||||||
|
.map(|vertex| vertex.to_any())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
27
experiments/2025-03-18/src/topology/half_edge.rs
Normal file
27
experiments/2025-03-18/src/topology/half_edge.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::TriMesh,
|
||||||
|
object::{Handle, HandleAny, Object},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::vertex::Vertex;
|
||||||
|
|
||||||
|
pub struct HalfEdge {
|
||||||
|
pub start: Handle<Vertex>,
|
||||||
|
pub is_internal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for HalfEdge {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "HalfEdge")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
TriMesh::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
vec![self.start.to_any()]
|
||||||
|
}
|
||||||
|
}
|
5
experiments/2025-03-18/src/topology/mod.rs
Normal file
5
experiments/2025-03-18/src/topology/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod face;
|
||||||
|
pub mod half_edge;
|
||||||
|
pub mod solid;
|
||||||
|
pub mod surface;
|
||||||
|
pub mod vertex;
|
41
experiments/2025-03-18/src/topology/solid.rs
Normal file
41
experiments/2025-03-18/src/topology/solid.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::TriMesh,
|
||||||
|
object::{Handle, HandleAny, Object},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::face::Face;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Solid {
|
||||||
|
faces: Vec<Handle<Face>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Solid {
|
||||||
|
pub fn new(faces: impl IntoIterator<Item = Handle<Face>>) -> Self {
|
||||||
|
Self {
|
||||||
|
faces: faces.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for Solid {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Solid")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
let mut tri_mesh = TriMesh::new();
|
||||||
|
|
||||||
|
for face in &self.faces {
|
||||||
|
tri_mesh = tri_mesh.merge(face.tri_mesh());
|
||||||
|
}
|
||||||
|
|
||||||
|
tri_mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
self.faces.iter().map(|face| face.to_any()).collect()
|
||||||
|
}
|
||||||
|
}
|
15
experiments/2025-03-18/src/topology/surface.rs
Normal file
15
experiments/2025-03-18/src/topology/surface.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::geometry::SurfaceGeometry;
|
||||||
|
|
||||||
|
pub struct Surface {
|
||||||
|
pub geometry: Box<dyn SurfaceGeometry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Surface {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Surface")
|
||||||
|
.field("geometry", &"Box<dyn SurfaceGeometry>")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
33
experiments/2025-03-18/src/topology/vertex.rs
Normal file
33
experiments/2025-03-18/src/topology/vertex.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::TriMesh,
|
||||||
|
math::Point,
|
||||||
|
object::{HandleAny, Object},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct Vertex {
|
||||||
|
pub point: Point<3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vertex {
|
||||||
|
pub fn new(point: impl Into<Point<3>>) -> Self {
|
||||||
|
let point = point.into();
|
||||||
|
Self { point }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for Vertex {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Vertex")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
TriMesh::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
136
experiments/2025-03-18/src/view.rs
Normal file
136
experiments/2025-03-18/src/view.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
use std::{fmt, iter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::TriMesh,
|
||||||
|
object::{HandleAny, Object},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OperationView {
|
||||||
|
operation: HandleAny,
|
||||||
|
children: Vec<Self>,
|
||||||
|
selected: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationView {
|
||||||
|
pub fn new(operation: HandleAny) -> Self {
|
||||||
|
let children =
|
||||||
|
operation.children().into_iter().map(Self::new).collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
operation,
|
||||||
|
children,
|
||||||
|
selected: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn operations(&self) -> impl Iterator<Item = (&Self, bool, usize)> {
|
||||||
|
self.operations_inner(true, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operations_inner(
|
||||||
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
indent_level: usize,
|
||||||
|
) -> Box<dyn Iterator<Item = (&Self, bool, usize)> + '_> {
|
||||||
|
let self_ = iter::once((self, selected, indent_level));
|
||||||
|
|
||||||
|
if self.selected.is_some() {
|
||||||
|
Box::new(self_.chain(self.children.iter().enumerate().flat_map(
|
||||||
|
move |(i, view)| {
|
||||||
|
let selected = Some(i) == self.selected;
|
||||||
|
view.operations_inner(selected, indent_level + 1)
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Box::new(self_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_last(&mut self) {
|
||||||
|
self.selected = Some(self.last_index());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next(&mut self) {
|
||||||
|
if let Some(selected) = self.selected {
|
||||||
|
self.selected = Some(usize::min(selected + 1, self.last_index()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous(&mut self) {
|
||||||
|
if let Some(selected) = self.selected {
|
||||||
|
self.selected = Some(selected.saturating_sub(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_none(&mut self) {
|
||||||
|
self.selected = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected(&self) -> &Self {
|
||||||
|
self.selected
|
||||||
|
.and_then(|selected| self.children.get(selected))
|
||||||
|
.map(|child| child.selected())
|
||||||
|
.unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_mut(&mut self) -> &mut Self {
|
||||||
|
let Some(selected) = self.selected else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The way this is done, first checking for `is_none` and then
|
||||||
|
// `unwrap`ing below, is really ugly. But the borrow checker is forcing
|
||||||
|
// my hand.
|
||||||
|
//
|
||||||
|
// I've tried several variations of matching, and it can't see that in
|
||||||
|
// the `None` case, `self` no longer needs to be borrowed, preventing me
|
||||||
|
// from returning it.
|
||||||
|
|
||||||
|
if self.children.get_mut(selected).is_none() {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.children.get_mut(selected).unwrap().selected_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent_of_selected_mut(&mut self) -> &mut Self {
|
||||||
|
let Some(selected) = self.selected else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The same comment in `selected_mut` applies here too. Plus, some ugly
|
||||||
|
// duplication.
|
||||||
|
|
||||||
|
if self.children.get_mut(selected).is_none() {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.children.get_mut(selected).unwrap().selected.is_none() {
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
self.children
|
||||||
|
.get_mut(selected)
|
||||||
|
.unwrap()
|
||||||
|
.parent_of_selected_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_index(&self) -> usize {
|
||||||
|
self.children.len().saturating_sub(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object for OperationView {
|
||||||
|
fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.operation.display(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tri_mesh(&self) -> TriMesh {
|
||||||
|
self.operation.tri_mesh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<HandleAny> {
|
||||||
|
self.operation.children()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user