mirror of
https://github.com/hannobraun/Fornjot
synced 2025-06-27 13:46:07 +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