Document the code of the experiment

This commit is contained in:
Hanno Braun 2024-11-27 21:57:36 +01:00
parent b9a8dabe29
commit 059d5e616f
10 changed files with 228 additions and 0 deletions

View File

@ -1,3 +1,8 @@
//! # [winit] event loop
//!
//! Nothing interesting to see here! This is all pretty standard stuff, as far
//! as winit apps are concerned.
use std::{collections::BTreeSet, sync::Arc};
use winit::{

View File

@ -1,3 +1,8 @@
//! # Exporting geometry to 3MF
//!
//! Nothing interesting to see here! This is just a thin layer on top of
//! [3mf-rs](threemf).
use std::{collections::BTreeMap, fs::File};
use crate::geometry::{Operation, OpsLog, Vertex};

View File

@ -1,3 +1,22 @@
//! Core geometry representation
//!
//! This is the core of what this experiment is about! The goal was to build an
//! interactive core around a very simple geometry representation (a triangle
//! mesh).
//!
//! While the geometry representation is very basic, I actually expect that
//! follow-up experiments will add more layers on top to structure it (thereby
//! enriching it with topographical information), rather than replace it with
//! something inherently more advanced (like NURBS or whatever).
//!
//! The idea here is that the triangle mesh works as a uniform intermediate
//! representation for geometry (as I've already been working towards with the
//! mainline Fornjot code), that any necessary algorithms can be built around
//! of. But working that out is not the subject of this experiment.
//!
//! If you're interested in the details, I suggest you start with [`OpsLog`],
//! which is the entry point to this API, and work your way down from there.
mod operation;
mod ops_log;
mod primitives;

View File

@ -1,8 +1,34 @@
//! # The core trait that ties everything together
//!
//! See [`Operation`].
use std::fmt;
use super::{Triangle, Vertex};
/// # An operation
///
/// Provides access to the uniform intermediate representation of operations,
/// which is a triangle mesh.
///
/// This trait is implemented by all operations. Chiefly by the primitive
/// operations, [`Vertex`] and [`Triangle`], but also by
/// [`OpsLog`](crate::geometry::OpsLog) and various types of its supporting
/// infrastructure.
///
/// Even though the geometry representation in this experiment is much more
/// basic than what follow-up experiments are expected to explore, this
/// multitude of implementors is a good sign for the flexibility of this
/// concept.
pub trait Operation: fmt::Display {
/// # The vertices that are part of the operation's uniform representation
///
/// Many callers won't have to bother with this method, as the vertices are
/// also available indirectly through [`Operation::triangles`]. But this is
/// used by the viewer, for example, to render the shape as it is
/// constructed, vertex by vertex.
fn vertices(&self, vertices: &mut Vec<Vertex>);
/// # The triangles that are part of the operation's uniform representation
fn triangles(&self, triangles: &mut Vec<Triangle>);
}

View File

@ -1,16 +1,81 @@
//! # The operations log and supporting infrastructure
//!
//! See [`OpsLog`].
use std::fmt;
use tuples::CombinRight;
use super::{Operation, Triangle, Vertex};
/// # The operations that make up geometry, ordered in a specific sequence
///
/// This is the entry point to the API that is used to create geometry. You
/// create an instance of [`OpsLog`], call its methods to create operations, and
/// can later look at the final result, or any intermediate step.
///
/// A core idea here is that operations are the primitive that make up any
/// geometry. This is different from mainline Fornjot code, in which geometry
/// (and topology) is made up of *objects* instead.
///
/// In this new model, objects and operations are the same thing. The most basic
/// operations just create a single object. More complex operations could
/// combine multiple operations (or objects) into higher-level operations (or
/// objects). But they all present [through the same interface](Operation).
///
/// Right now, the only operations available are [`Vertex`] and [`Triangle`].
/// [`Vertex`] is the lower-level of the two, while [`Triangle`] builds on top
/// of it to create a higher-level operation/object.
///
/// It might be worth noting that [`OpsLog`] is itself an operation (i.e. it
/// implements [`Operation`]). It is simply the operation that unifies all the
/// operations that were added to it, and creates the uniform intermediate
/// representation of the complete shape.
///
/// ## Adding operations
///
/// Operations are added using the [`OpsLog::vertex`] and [`OpsLog::triangle`]
/// methods. Later prototypes might move these to extension traits or something,
/// to support arbitrary operations, but here those are hardcoded and the only
/// ones supported.
///
/// Those methods employ some trickery in the form of the [`OperationResult`]
/// return value, which allows the caller both to chain calls to add multiple
/// objects in a row, but then also access every single operation created in a
/// convenient way.
///
/// Check out the code of [`crate::model::model`] to see how that looks. I'm
/// pretty happy with how it turned out, but we'll have to see how well it
/// scales, once arbitrary (user-defined) operations have to be supported.
#[derive(Default)]
pub struct OpsLog {
/// # The operations
///
/// This doesn't store the operations directly. The goal here is to provide
/// access to every intermediate state that the geometry construction went
/// through, and storing operations directly won't achieve that.
///
/// For example, if you were to look at the second-to-last operation in
/// isolation, you wouldn't see the whole shape before the last operation,
/// but only the single triangle or something that made up this
/// second-to-last operation.
///
/// So what this does instead, is wrap operations into
/// [`OperationInSequence`], which knows about all previous operations and
/// can provide the intermediate state.
pub operations: Vec<OperationInSequence>,
/// # Which operation is currently selected
///
/// This is a UI concept and this field probably shouldn't live here. For
/// now, it still does though.
pub selected: usize,
}
impl OpsLog {
/// # Add a vertex
///
/// See documentation of [`OpsLog`] for context.
pub fn vertex(
&mut self,
vertex: impl Into<Vertex>,
@ -31,6 +96,9 @@ impl OpsLog {
}
}
/// # Add a triangle
///
/// See documentation of [`OpsLog`] for context.
pub fn triangle(
&mut self,
triangle: impl Into<Triangle>,
@ -51,20 +119,24 @@ impl OpsLog {
}
}
/// # Used by the UI; not interesting
pub fn select_last(&mut self) {
self.selected = self.operations.len().saturating_sub(1);
}
/// # Used by the UI; not interesting
pub fn select_next(&mut self) {
if self.selected < self.operations.len() {
self.selected += 1;
}
}
/// # Used by the UI; not interesting
pub fn select_previous(&mut self) {
self.selected = self.selected.saturating_sub(1);
}
/// # Used by the UI; not interesting
pub fn selected(&self) -> Option<&dyn Operation> {
self.operations.get(self.selected).map(|op| op as &_)
}
@ -94,6 +166,27 @@ impl Operation for OpsLog {
}
}
/// # Representation of an intermediate state in constructing a shape
///
/// This is a wrapper around an operation, but it also knows about the previous
/// operation in the sequence (which itself is expected to know about its
/// previous operation).
///
/// [`OperationInSequence`] is used by [`OpsLog`] to provide the ability to look
/// at every intermediate state.
///
/// ## Efficiency
///
/// This is implemented in a rather inefficient way, by cloning the uniform
/// representation of the operations it references, via [`ClonedOperation`].
///
/// This is fine within the context of this experiment, and I'm not too worried
/// about it long-term either. Operations can live in centralized stores, which
/// can return handles to refer to them. Similar to what current mainline
/// Fornjot does with its topological objects.
///
/// I'm not sure if that would be the best way to do it, but at least it
/// wouldn't create multiple clones of every operation.
pub struct OperationInSequence {
pub operation: ClonedOperation,
pub previous: Option<ClonedOperation>,
@ -121,6 +214,10 @@ impl fmt::Display for OperationInSequence {
}
}
/// # Allow chaining calls to create operations, but also access all results
///
/// This is an implementation details of [`OpsLog`]. See documentation on adding
/// operations there.
pub struct OperationResult<'r, T> {
operations: &'r mut OpsLog,
results: T,
@ -167,6 +264,7 @@ impl<'r, T> OperationResult<'r, T> {
}
}
/// # Implementation details of [`OperationInSequence`]
pub struct ClonedOperation {
pub description: String,
pub vertices: Vec<Vertex>,

View File

@ -1,11 +1,21 @@
//! # The primitive operations
//!
//! Future experiments may support user-defined operations on top of those, but
//! for now this is all there is.
use std::fmt;
use crate::math::Point;
use super::Operation;
/// # A vertex
///
/// This is the most basic operation, which creates a single vertex, represented
/// as a point in 3D space.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Vertex {
/// # The point that represents the vertex
pub point: Point,
}
@ -35,8 +45,27 @@ impl Operation for Vertex {
fn triangles(&self, _: &mut Vec<Triangle>) {}
}
/// # A triangle
///
/// Combines three [`Vertex`] instances into a triangle. This is still very
/// simple, but still kind of a prototype for how other non-trivial operations
/// that combine more primitives ones might later look like.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Triangle {
/// # The vertices of the triangle
///
/// This embeds a copy of the vertex, which isn't great for two reasons:
///
/// - It is rather space-inefficient, as multiple copies of every vertex are
/// bound to exist within the shape.
/// - It loses the identity of the vertex, making it harder to highlight it
/// in the viewer, for example, or to run validation code that could
/// possibly exist later.
///
/// For now, that's fine. Follow-up experiments will likely use some kind
/// of centralized storage of operations, and handles that refer to the
/// operations in those stores, similar to what mainline Fornjot does with
/// its topographic objects.
pub vertices: [Vertex; 3],
}

View File

@ -1,3 +1,12 @@
//! # Fornjot - Experiment 2024-10-30
//!
//! Please check out the accompanying `README.md` file for high-level
//! documentation.
//!
//! As for the details, you are in the right place! I recommend you get started
//! with the [`geometry`] module, as that is the core of what this experiment is
//! about.
#![allow(clippy::module_inception)]
mod app;

View File

@ -1,3 +1,9 @@
//! # Core math primitives
//!
//! This is just a little math library to support the code in
//! [`geometry`](crate::geometry). Nothing special.
use std::{cmp::Ordering, ops};
use iter_fixed::IntoIteratorFixed;

View File

@ -1,3 +1,12 @@
//! # The hardcoded geometry (a cube)
//!
//! This is just the hardcoded geometry that I'm using as a test case for this
//! experiment. I kept it simple, as there wasn't much point in trying to
//! exercise the code in [`geometry`](crate::geometry) with anything complex.
//! It's pretty clear that it's limited.
//!
//! I expect follow-up experiments to have more interesting geometry.
use crate::geometry::OpsLog;
pub fn model(ops: &mut OpsLog) {

View File

@ -1,3 +1,25 @@
//! # Rendering infrastructure
//!
//! Even though most of the work for this prototype went into the renderer, it
//! is not the most interesting aspect, and I'm not going to document it in
//! detail. It's a pretty basic architecture, optimized for the speed of having
//! written it, not speed of rendering.
//!
//! The most interesting aspect in terms of what this experiment could mean for
//! Fornjot, is that this renderer has a direct dependency on
//! [`geometry`](crate::geometry). Versus the current Fornjot renderer, which
//! only communicates with the CAD core through another interop crate.
//!
//! I'm pretty sure that whatever happens with these experiments, I'll go with
//! the simpler approach going forward. I'm not even sure anymore what the
//! thinking behind the original design was (it's been years).
//!
//! I probably overestimated the importance of making things pluggable, and
//! making parts of Fornjot usable in isolation. Going forward, I'm viewing the
//! renderer as something that is very purpose-built for the needs of developing
//! Fornjot. Not something I'd expect anybody building on top of Fornjot would
//! want to use, except maybe to get started.
mod geometry;
mod pipelines;
mod renderer;