Start new experiment as copy of previous one

This commit is contained in:
Hanno Braun 2025-03-18 20:26:49 +01:00
parent 22a9c5506f
commit 67c2327b9a
47 changed files with 5223 additions and 0 deletions

5
experiments/2025-03-18/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Cargo
/target
# Fornjot
/*.3mf

View 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

File diff suppressed because it is too large Load Diff

View 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"]

View 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.

View 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}";
}

View 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))
}

View 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(())
}

View File

@ -0,0 +1 @@
pub mod triangulate;

View 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 }
}
}

View 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,
};

View 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 }
}
}

View 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))
}
}

View 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,
}

View 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()
}
}

View 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(())
}

View 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>,
}

View 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,
};

View 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.]));
}
}

View 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 }
}
}

View 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)
}
}

View 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 }
}
}

View 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)
}

View 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()
}
}

View 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()
}
}

View File

@ -0,0 +1,5 @@
mod handle;
mod handle_any;
mod traits;
pub use self::{handle::Handle, handle_any::HandleAny, traits::Object};

View 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)
}
}

View 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))
}
}

View 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 }
}
}

View File

@ -0,0 +1,4 @@
pub mod connect;
pub mod flip;
pub mod sweep;
pub mod translate;

View 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)
}
}

View 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)
}
}

View 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,
}
}
}

View File

@ -0,0 +1,8 @@
mod geometry;
mod pipeline;
mod renderer;
mod text;
mod uniforms;
mod vertex;
pub use self::renderer::Renderer;

View 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.)
}

View 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(())
}
}

View 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;
}

View 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(())
}
}

View 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,
}
}
}

View 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,
];
}

View 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()
}
}

View 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()]
}
}

View File

@ -0,0 +1,5 @@
pub mod face;
pub mod half_edge;
pub mod solid;
pub mod surface;
pub mod vertex;

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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()
}
}