mirror of https://github.com/hannobraun/Fornjot
Merge pull request #1822 from hannobraun/deprecate
Move deprecated components to new fornjot-extra repository
This commit is contained in:
commit
20cac5f675
|
@ -59,5 +59,5 @@ jobs:
|
|||
run: cargo build --all-features
|
||||
- name: Run `cargo test`
|
||||
run: cargo test --all-features
|
||||
- name: Run `export-validator`
|
||||
run: cargo run --package export-validator
|
||||
# - name: Run `export-validator`
|
||||
# run: cargo run --package export-validator
|
||||
|
|
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
|
@ -1,22 +1,12 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/fj",
|
||||
"crates/fj-app",
|
||||
"crates/fj-export",
|
||||
"crates/fj-host",
|
||||
"crates/fj-interop",
|
||||
"crates/fj-kernel",
|
||||
"crates/fj-math",
|
||||
"crates/fj-operations",
|
||||
"crates/fj-proc",
|
||||
"crates/fj-viewer",
|
||||
"crates/fj-window",
|
||||
|
||||
"models/cuboid",
|
||||
"models/spacer",
|
||||
"models/star",
|
||||
"models/test",
|
||||
# "crates/fj-window",
|
||||
|
||||
"tools/autolib",
|
||||
"tools/automator",
|
||||
|
@ -25,17 +15,12 @@ members = [
|
|||
"tools/release-operator",
|
||||
]
|
||||
default-members = [
|
||||
"crates/fj",
|
||||
"crates/fj-app",
|
||||
"crates/fj-export",
|
||||
"crates/fj-host",
|
||||
"crates/fj-interop",
|
||||
"crates/fj-kernel",
|
||||
"crates/fj-math",
|
||||
"crates/fj-operations",
|
||||
"crates/fj-proc",
|
||||
"crates/fj-viewer",
|
||||
"crates/fj-window",
|
||||
# "crates/fj-window",
|
||||
]
|
||||
|
||||
|
||||
|
|
126
README.md
126
README.md
|
@ -4,9 +4,7 @@
|
|||
|
||||
## About
|
||||
|
||||
Fornjot is an **early-stage project** to create a **next-generation, code-first CAD application**. Because [**the world needs another CAD program**](https://github.com/sponsors/hannobraun).
|
||||
|
||||
![Screenshot of Fornjot](models/star/star.png)
|
||||
Fornjot is an early-stage [b-rep](https://en.wikipedia.org/wiki/Boundary_representation) CAD kernel written in the Rust programming language.
|
||||
|
||||
For an introduction of what the project aims to achieve, [please check out the website](https://www.fornjot.app/).
|
||||
|
||||
|
@ -21,132 +19,20 @@ Fornjot is supported by [**@webtrax-oz**](https://github.com/webtrax-oz), [**@re
|
|||
## Table of Contents
|
||||
|
||||
- [**Status**](#status)
|
||||
- [**Features**](#features)
|
||||
- [**Usage**](#usage)
|
||||
- [**Community**](#community)
|
||||
- [**Get Involved**](#get-involved)
|
||||
- [**License**](#license)
|
||||
|
||||
|
||||
## Status
|
||||
|
||||
Fornjot is **under active development, but still experimental**. Efforts are currently focused on providing a [stable set of basic CAD features](https://github.com/hannobraun/Fornjot/milestone/1).
|
||||
|
||||
If you are interested in Fornjot and are considering to use it, you should fully expect to run into limitations pretty much immediately. Unless you are willing to contribute to its development, it would be better to wait for a year or ten, to let it mature. For more information on current limitations and improvements that could be implemented in the near future, [check out the open issues](https://github.com/hannobraun/Fornjot/issues).
|
||||
|
||||
To learn about the project's longer-term direction, please refer to the [roadmap](https://www.fornjot.app/roadmap/).
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
### Code-first CAD in Rust
|
||||
|
||||
Models are defined as Rust code. To ensure fast compile times, they are compiled separately, and loaded into the Fornjot application as a plug-in.
|
||||
|
||||
``` rust
|
||||
use fj::syntax::*;
|
||||
|
||||
#[fj::model]
|
||||
pub fn model(
|
||||
#[param(default = 1.0, min = inner * 1.01)] outer: f64,
|
||||
#[param(default = 0.5, max = outer * 0.99)] inner: f64,
|
||||
#[param(default = 1.0)] height: f64,
|
||||
) -> fj::Shape {
|
||||
let outer_edge = fj::Sketch::from_circle(fj::Circle::from_radius(outer));
|
||||
let inner_edge = fj::Sketch::from_circle(fj::Circle::from_radius(inner));
|
||||
|
||||
let footprint = outer_edge.difference(&inner_edge);
|
||||
let spacer = footprint.sweep([0., 0., height]);
|
||||
|
||||
spacer.into()
|
||||
}
|
||||
```
|
||||
|
||||
This is the code for the [spacer model](/models/spacer).
|
||||
|
||||
### Basic modeling features
|
||||
|
||||
At this point, Fornjot supports basic 2D shapes (sketches made from lines segments, circles, and limited combinations between them) and sweeping those 2D shapes along a straight path to create a 3D shape.
|
||||
|
||||
The short- to mid-term priority is to provide CSG support, more flexible sketches, and more flexible sweeps (along a circle or helix). Long-term, the plan is to keep adding more advanced CAD modeling features, to support even complex models and workflows.
|
||||
|
||||
### Supports the major desktop platforms
|
||||
|
||||
As of this writing, Fornjot runs on Linux, Windows, and macOS. The project is primarily developed on Linux, so the other platforms might be subject to bugs. If you want to help out, regularly testing on Windows and macOS, and reporting bugs, is a good way to do so.
|
||||
|
||||
Short- to mid-term, the plan is to add support for the web platform, so Fornjot can run in browsers. Long-term, the plan is to additionally support the major mobile platforms.
|
||||
|
||||
### Export to 3MF & STL
|
||||
|
||||
Exporting models to both the [3D Manufacturing Format](https://en.wikipedia.org/wiki/3D_Manufacturing_Format) (3MF), which is used in 3D printing, and STL is supported.
|
||||
Fornjot is usable for some toy examples, but currently lacks the features for something more advanced. Work to change that is underway.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Since Fornjot uses Rust as the language for defining models, a [Rust toolchain](https://www.rust-lang.org/tools/install) is required to use Fornjot.
|
||||
|
||||
To install Fornjot itself, you have the following options:
|
||||
|
||||
1. Download a binary from the [latest release](https://github.com/hannobraun/Fornjot/releases).
|
||||
2. Compile the latest release yourself: `cargo install fj-app`
|
||||
3. Compile a development version from this repository: `cd path/to/repo; cargo install --path crates/fj-app`
|
||||
|
||||
While the Fornjot application is a graphical application that opens a window and displays a 3D view of your model, it can currently only be started from the command-line. The instructions below assume that you have the Fornjot application installed somewhere on your path, under the name `fj-app`.
|
||||
|
||||
#### Mac OS
|
||||
If you download in [latest release](https://github.com/hannobraun/Fornjot/releases) binary, two problems need to be noticed when running on your local computer.
|
||||
1. The mac will prompt that "fj-app-x86_64-apple-arwin" cannot be opened because it is from an unknown developer.
|
||||
|
||||
The solution is Open the Mac system preferences. In the preferences interface, click to open **security and privacy**. In the security and privacy interface, click **General**. In the call settings panel, click the **Still want to open** button. In the pop-up window, click the **Open** button.
|
||||
|
||||
2. Failed to open the document "fj-app". Text encoding Unicode (UTF-8) is not applicable.
|
||||
|
||||
The solution is enter the following command in the terminal, (we recommend changing the file name to fj-app)
|
||||
```shell
|
||||
chmod a+x fj-app
|
||||
```
|
||||
|
||||
#### Via Nix
|
||||
|
||||
There's a Nix [flake](https://nixos.wiki/wiki/Flakes) in the subdirectory `./nix` which contains a devshell environment (via `nix develop` or `nix-shell`) and the package `fj-app`.
|
||||
It can be run/tested with a [flake enabled](https://nixos.wiki/wiki/Flakes#Enable_flakes) nix via `nix run github:hannobraun/Fornjot?dir=nix` or with legacy nix in the directory `./nix`, `nix-build`.
|
||||
|
||||
### Defining models
|
||||
|
||||
Models are Rust libraries that depend on the [`fj`](https://crates.io/crates/fj) library, which they use to define the geometry. Furthermore, they need to be built as a dynamic library. Just use the examples in the [`models/`](models) directory as a template to define your own.
|
||||
|
||||
### Viewing models
|
||||
|
||||
To view a model, run:
|
||||
|
||||
``` sh
|
||||
fj-app my-model
|
||||
```
|
||||
|
||||
This will usually compile and load the model in the `my-model/` directory. If there is a configuration file (`fj.toml`) available, it might define a default path to load models from that is different from the current working directory. This is the case [in the Fornjot repository](fj.toml).
|
||||
|
||||
Rotate the model by pressing the left mouse button while moving the mouse. Move the model by pressing the right mouse button while moving the mouse. Zoom with the mouse wheel.
|
||||
|
||||
Toggle model rendering by pressing `1`. Toggle mesh rendering by pressing `2`. Toggle rendering of debug data by pressing `3`.
|
||||
|
||||
### Exporting models
|
||||
|
||||
To export a model to a file, run:
|
||||
|
||||
``` sh
|
||||
fj-app my-model --export my-model.3mf
|
||||
```
|
||||
|
||||
The file type is chosen based on the file extension. Both 3MF and STL are supported.
|
||||
|
||||
### Model parameters
|
||||
|
||||
Models can define parameters that can be overridden. This can be done using the `--parameters` argument:
|
||||
|
||||
``` sh
|
||||
fj-app my-model --parameters "width=3.0,height=5.0"
|
||||
```
|
||||
This repository is currently [in flux](https://www.fornjot.app/blog/a-new-direction/) and the documentation previously available here no longer applies. This section will be updated in due time.
|
||||
|
||||
|
||||
## Community
|
||||
|
@ -163,9 +49,7 @@ If you are interested in helping out, just fork one of the GitHub repositories a
|
|||
- [Main Fornjot repository](https://github.com/hannobraun/Fornjot)
|
||||
- [Website repository](https://github.com/hannobraun/www.fornjot.app)
|
||||
|
||||
If you don't know what to work on, check out the [`good first issues`](https://github.com/hannobraun/Fornjot/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). To get an overview over current priorities, take a look at the [open milestones](https://github.com/hannobraun/Fornjot/milestones).
|
||||
|
||||
If you need some more guidance, check out the [contribution guide](CONTRIBUTING.md), [or just ask](https://www.fornjot.app/community/)!
|
||||
If you don't know what to work on, check out the [`good first issues`](https://github.com/hannobraun/Fornjot/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). If you need some more guidance, check out the [contribution guide](CONTRIBUTING.md), [or just ask](https://www.fornjot.app/community/)!
|
||||
|
||||
|
||||
## License
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
[package]
|
||||
name = "fj-app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
fj.workspace = true
|
||||
fj-export.workspace = true
|
||||
fj-host.workspace = true
|
||||
fj-interop.workspace = true
|
||||
fj-kernel.workspace = true
|
||||
fj-math.workspace = true
|
||||
fj-operations.workspace = true
|
||||
fj-viewer.workspace = true
|
||||
fj-window.workspace = true
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.2.7"
|
||||
features = ["derive", "string"]
|
||||
|
||||
[dependencies.figment]
|
||||
version = "0.10.8"
|
||||
features = ["env", "toml"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.162"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3.17"
|
||||
features = ["env-filter", "fmt"]
|
|
@ -1,67 +0,0 @@
|
|||
use std::{path::PathBuf, str::FromStr as _};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use fj_host::Parameters;
|
||||
use fj_kernel::algorithms::approx::Tolerance;
|
||||
use fj_math::Scalar;
|
||||
|
||||
/// Fornjot - Experimental CAD System
|
||||
#[derive(clap::Parser)]
|
||||
#[command(version = fj::version::VERSION_FULL.to_string())]
|
||||
pub struct Args {
|
||||
/// The model to open
|
||||
pub model: Option<PathBuf>,
|
||||
|
||||
/// Export model to this path
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
pub export: Option<PathBuf>,
|
||||
|
||||
/// Parameters for the model, each in the form `key=value`
|
||||
#[arg(short, long, value_parser = parse_parameters)]
|
||||
pub parameters: Option<Parameters>,
|
||||
|
||||
/// Model deviation tolerance
|
||||
#[arg(short, long, value_parser = parse_tolerance)]
|
||||
pub tolerance: Option<Tolerance>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Parse the command-line arguments
|
||||
///
|
||||
/// Convenience method that saves the caller from having to import the
|
||||
/// `clap::Parser` trait.
|
||||
pub fn parse() -> Self {
|
||||
<Self as clap::Parser>::parse()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_parameters(input: &str) -> anyhow::Result<Parameters> {
|
||||
let mut parameters = Parameters::empty();
|
||||
|
||||
for parameter in input.split(',') {
|
||||
let mut parameter = parameter.splitn(2, '=');
|
||||
|
||||
let key = parameter
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Expected model parameter key"))?
|
||||
.trim()
|
||||
.to_owned();
|
||||
let value = parameter
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Expected model parameter value"))?
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
parameters.0.insert(key, value);
|
||||
}
|
||||
|
||||
Ok(parameters)
|
||||
}
|
||||
|
||||
fn parse_tolerance(input: &str) -> anyhow::Result<Tolerance> {
|
||||
let tolerance = f64::from_str(input)?;
|
||||
let tolerance = Scalar::from_f64(tolerance);
|
||||
let tolerance = Tolerance::from_scalar(tolerance)?;
|
||||
|
||||
Ok(tolerance)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use figment::{
|
||||
providers::{Env, Format as _, Toml},
|
||||
Figment,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub default_path: Option<PathBuf>,
|
||||
pub default_model: Option<PathBuf>,
|
||||
pub invert_zoom: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self, anyhow::Error> {
|
||||
Figment::new()
|
||||
.merge(Toml::file("fj.toml"))
|
||||
.merge(Env::prefixed("FJ_"))
|
||||
.extract()
|
||||
.context("Error loading configuration")
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
//! # Fornjot Application
|
||||
//!
|
||||
//! This library is part of the [Fornjot] ecosystem. Fornjot is an open-source,
|
||||
//! code-first CAD application; and collection of libraries that make up the CAD
|
||||
//! application, but can be used independently.
|
||||
//!
|
||||
//! Together with the [`fj`] library, this application forms the part of Fornjot
|
||||
//! that is relevant to end users. Please refer to the [Fornjot repository] for
|
||||
//! usage examples.
|
||||
//!
|
||||
//! [Fornjot]: https://www.fornjot.app/
|
||||
//! [`fj`]: https://crates.io/crates/fj
|
||||
//! [Fornjot repository]: https://github.com/hannobraun/Fornjot
|
||||
|
||||
mod args;
|
||||
mod config;
|
||||
mod path;
|
||||
|
||||
use std::{env, error::Error};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use fj_export::export;
|
||||
use fj_host::Parameters;
|
||||
use fj_operations::shape_processor::ShapeProcessor;
|
||||
use fj_window::run::run;
|
||||
use path::ModelPath;
|
||||
use tracing_subscriber::fmt::format;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::{args::Args, config::Config};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
// Respect `RUST_LOG`. If that's not defined, log warnings and above. Fail if it's erroneous.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(try_default_env_filter()?)
|
||||
.event_format(format().pretty())
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
let config = Config::load()?;
|
||||
let model_path = ModelPath::from_args_and_config(&args, &config);
|
||||
let parameters = args.parameters.unwrap_or_else(Parameters::empty);
|
||||
let shape_processor = ShapeProcessor {
|
||||
tolerance: args.tolerance,
|
||||
};
|
||||
|
||||
let model = model_path.map(|m| m.load_model(parameters)).transpose()?;
|
||||
|
||||
if let Some(export_path) = args.export {
|
||||
// export only mode. just load model, process, export and exit
|
||||
|
||||
let evaluation = model.with_context(no_model_error)?.evaluate()?;
|
||||
let shape = shape_processor.process(&evaluation.shape)?;
|
||||
|
||||
export(&shape.mesh, &export_path)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let invert_zoom = config.invert_zoom.unwrap_or(false);
|
||||
run(model, shape_processor, invert_zoom)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn no_model_error() -> anyhow::Error {
|
||||
anyhow!(
|
||||
"You must specify a model to start Fornjot in export only mode.\n\
|
||||
- Pass a model as a command-line argument. See `fj-app --help`.\n\
|
||||
- Specify a default model in the configuration file."
|
||||
)
|
||||
}
|
||||
|
||||
fn try_default_env_filter() -> anyhow::Result<EnvFilter> {
|
||||
let env_filter = EnvFilter::try_from_default_env();
|
||||
|
||||
match env_filter {
|
||||
Ok(env_filter) => Ok(env_filter),
|
||||
|
||||
Err(err) => {
|
||||
if let Some(kind) = err.source() {
|
||||
if let Some(env::VarError::NotPresent) =
|
||||
kind.downcast_ref::<env::VarError>()
|
||||
{
|
||||
return Ok(EnvFilter::new("WARN"));
|
||||
}
|
||||
} else {
|
||||
// `tracing_subscriber::filter::FromEnvError` currently returns a source
|
||||
// in all cases.
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"There was an error parsing the RUST_LOG environment variable."
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
use std::{
|
||||
fmt::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use fj_host::{Model, Parameters};
|
||||
|
||||
use crate::{args::Args, config::Config};
|
||||
|
||||
pub struct ModelPath {
|
||||
default_path: Option<PathBuf>,
|
||||
model_path: ModelPathSource,
|
||||
}
|
||||
|
||||
impl ModelPath {
|
||||
pub fn from_args_and_config(args: &Args, config: &Config) -> Option<Self> {
|
||||
let default_path = config.default_path.clone();
|
||||
|
||||
let model_path_from_args = args
|
||||
.model
|
||||
.as_ref()
|
||||
.map(|model| ModelPathSource::Args(model.clone()));
|
||||
let model_path_from_config = config
|
||||
.default_model
|
||||
.as_ref()
|
||||
.map(|model| ModelPathSource::Config(model.clone()));
|
||||
let model_path = model_path_from_args.or(model_path_from_config)?;
|
||||
|
||||
Some(Self {
|
||||
default_path,
|
||||
model_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_model(&self, parameters: Parameters) -> anyhow::Result<Model> {
|
||||
let default_path = self
|
||||
.default_path
|
||||
.as_ref()
|
||||
.map(|path| -> anyhow::Result<_> {
|
||||
let rel = path;
|
||||
let abs = path.canonicalize().with_context(|| {
|
||||
format!(
|
||||
"Converting `default-path` from `fj.toml` (`{}`) into \
|
||||
absolute path",
|
||||
path.display(),
|
||||
)
|
||||
})?;
|
||||
Ok((rel, abs))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let path = default_path
|
||||
.clone()
|
||||
.map(|(_, abs)| abs)
|
||||
.unwrap_or_else(PathBuf::new)
|
||||
.join(self.model_path.path());
|
||||
|
||||
let model = Model::new(&path, parameters).with_context(|| {
|
||||
load_error_context(default_path, &self.model_path, path)
|
||||
})?;
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
|
||||
enum ModelPathSource {
|
||||
Args(PathBuf),
|
||||
Config(PathBuf),
|
||||
}
|
||||
|
||||
impl ModelPathSource {
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::Args(path) => path,
|
||||
Self::Config(path) => path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_error_context(
|
||||
default_path: Option<(&PathBuf, PathBuf)>,
|
||||
model_path: &ModelPathSource,
|
||||
path: PathBuf,
|
||||
) -> String {
|
||||
load_error_context_inner(default_path, model_path, path)
|
||||
.expect("Expected `write!` to `String` to never fail")
|
||||
}
|
||||
|
||||
fn load_error_context_inner(
|
||||
default_path: Option<(&PathBuf, PathBuf)>,
|
||||
model_path: &ModelPathSource,
|
||||
path: PathBuf,
|
||||
) -> Result<String, fmt::Error> {
|
||||
let mut error = String::new();
|
||||
write!(
|
||||
error,
|
||||
"Failed to load model: `{}`",
|
||||
model_path.path().display()
|
||||
)?;
|
||||
match model_path {
|
||||
ModelPathSource::Args(_) => {
|
||||
write!(error, "\n- Passed via command-line argument")?;
|
||||
}
|
||||
ModelPathSource::Config(_) => {
|
||||
write!(error, "\n- Specified as default model in configuration")?;
|
||||
}
|
||||
}
|
||||
write!(error, "\n- Path of model: {}", path.display())?;
|
||||
|
||||
let mut suggestions = String::new();
|
||||
write!(suggestions, "Suggestions:")?;
|
||||
write!(
|
||||
suggestions,
|
||||
"\n- Did you mis-type the model path `{}`?",
|
||||
model_path.path().display()
|
||||
)?;
|
||||
|
||||
if let Some((default_path_rel, default_path_abs)) = &default_path {
|
||||
write!(
|
||||
error,
|
||||
"\n- Searching inside default path from configuration: {}",
|
||||
default_path_abs.display(),
|
||||
)?;
|
||||
|
||||
write!(
|
||||
suggestions,
|
||||
"\n- Did you mis-type the default path `{}`?",
|
||||
default_path_rel.display()
|
||||
)?;
|
||||
write!(
|
||||
suggestions,
|
||||
"\n- Did you accidentally pick up a local configuration file?"
|
||||
)?;
|
||||
}
|
||||
|
||||
let context = format!("{error}\n\n{suggestions}");
|
||||
|
||||
Ok(context)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "fj-host"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
|
||||
[dependencies]
|
||||
cargo_metadata = "0.15.4"
|
||||
crossbeam-channel = "0.5.8"
|
||||
fj.workspace = true
|
||||
fj-interop.workspace = true
|
||||
fj-operations.workspace = true
|
||||
libloading = "0.8.0"
|
||||
notify = "5.1.0"
|
||||
thiserror = "1.0.40"
|
||||
tracing = "0.1.37"
|
|
@ -1,87 +0,0 @@
|
|||
use std::thread::JoinHandle;
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use fj_operations::shape_processor::ShapeProcessor;
|
||||
|
||||
use crate::{EventLoopClosed, HostThread, Model, ModelEvent};
|
||||
|
||||
/// A host for watching models and responding to model updates
|
||||
pub struct Host {
|
||||
command_tx: Sender<HostCommand>,
|
||||
host_thread: Option<JoinHandle<Result<(), EventLoopClosed>>>,
|
||||
model_loaded: bool,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// Create a host with a shape processor and a send channel to the event
|
||||
/// loop.
|
||||
pub fn new(
|
||||
shape_processor: ShapeProcessor,
|
||||
model_event_tx: Sender<ModelEvent>,
|
||||
) -> Self {
|
||||
let (command_tx, host_thread) =
|
||||
HostThread::spawn(shape_processor, model_event_tx);
|
||||
|
||||
Self {
|
||||
command_tx,
|
||||
host_thread: Some(host_thread),
|
||||
model_loaded: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a model to the host for evaluation and processing.
|
||||
pub fn load_model(&mut self, model: Model) {
|
||||
self.command_tx
|
||||
.try_send(HostCommand::LoadModel(model))
|
||||
.expect("Host channel disconnected unexpectedly");
|
||||
self.model_loaded = true;
|
||||
}
|
||||
|
||||
/// Whether a model has been sent to the host yet
|
||||
pub fn is_model_loaded(&self) -> bool {
|
||||
self.model_loaded
|
||||
}
|
||||
|
||||
/// Check if the host thread has exited with a panic. This method runs at
|
||||
/// each tick of the event loop. Without an explicit check, an operation
|
||||
/// will appear to hang forever (e.g. processing a model). An error
|
||||
/// will be printed to the terminal, but the gui will not notice until
|
||||
/// a new `HostCommand` is issued on the disconnected channel.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics on purpose so the main thread can exit on an
|
||||
/// unrecoverable error.
|
||||
pub fn propagate_panic(&mut self) {
|
||||
if self.host_thread.is_none() {
|
||||
unreachable!("Constructor requires host thread")
|
||||
}
|
||||
if let Some(host_thread) = &self.host_thread {
|
||||
// The host thread should not finish while this handle holds the
|
||||
// `command_tx` channel open, so an exit means the thread panicked.
|
||||
if host_thread.is_finished() {
|
||||
let host_thread = self.host_thread.take().unwrap();
|
||||
match host_thread.join() {
|
||||
Ok(_) => {
|
||||
unreachable!(
|
||||
"Host thread cannot exit until host handle disconnects"
|
||||
)
|
||||
}
|
||||
// The error value has already been reported by the panic
|
||||
// in the host thread, so just ignore it here.
|
||||
Err(_) => {
|
||||
panic!("Host thread panicked")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands that can be sent to a host
|
||||
pub enum HostCommand {
|
||||
/// Load a model to be evaluated and processed
|
||||
LoadModel(Model),
|
||||
/// Used by a `Watcher` to trigger evaluation when a model is edited
|
||||
TriggerEvaluation,
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use crossbeam_channel::{self, Receiver, Sender};
|
||||
use fj_interop::processed_shape::ProcessedShape;
|
||||
use fj_operations::shape_processor::ShapeProcessor;
|
||||
|
||||
use crate::{Error, HostCommand, Model, Watcher};
|
||||
|
||||
// Use a zero-sized error type to silence `#[warn(clippy::result_large_err)]`.
|
||||
// The only error from `EventLoopProxy::send_event` is `EventLoopClosed<T>`,
|
||||
// so we don't need the actual value. We just need to know there was an error.
|
||||
pub(crate) struct EventLoopClosed;
|
||||
|
||||
pub(crate) struct HostThread {
|
||||
shape_processor: ShapeProcessor,
|
||||
model_event_tx: Sender<ModelEvent>,
|
||||
command_tx: Sender<HostCommand>,
|
||||
command_rx: Receiver<HostCommand>,
|
||||
}
|
||||
|
||||
impl HostThread {
|
||||
// Spawn a background thread that will process models for an event loop.
|
||||
pub(crate) fn spawn(
|
||||
shape_processor: ShapeProcessor,
|
||||
event_loop_proxy: Sender<ModelEvent>,
|
||||
) -> (Sender<HostCommand>, JoinHandle<Result<(), EventLoopClosed>>) {
|
||||
let (command_tx, command_rx) = crossbeam_channel::unbounded();
|
||||
let command_tx_2 = command_tx.clone();
|
||||
|
||||
let host_thread = Self {
|
||||
shape_processor,
|
||||
model_event_tx: event_loop_proxy,
|
||||
command_tx,
|
||||
command_rx,
|
||||
};
|
||||
|
||||
let join_handle = host_thread.spawn_thread();
|
||||
|
||||
(command_tx_2, join_handle)
|
||||
}
|
||||
|
||||
fn spawn_thread(mut self) -> JoinHandle<Result<(), EventLoopClosed>> {
|
||||
thread::Builder::new()
|
||||
.name("host".to_string())
|
||||
.spawn(move || -> Result<(), EventLoopClosed> {
|
||||
let mut model: Option<Model> = None;
|
||||
let mut _watcher: Option<Watcher> = None;
|
||||
|
||||
while let Ok(command) = self.command_rx.recv() {
|
||||
match command {
|
||||
HostCommand::LoadModel(new_model) => {
|
||||
// Right now, `fj-app` will only load a new model
|
||||
// once. The gui does not have a feature to load a
|
||||
// new model after the initial load. If that were
|
||||
// to change, there would be a race condition here
|
||||
// if the prior watcher sent `TriggerEvaluation`
|
||||
// before it and the model were replaced.
|
||||
match Watcher::watch_model(
|
||||
new_model.watch_path(),
|
||||
self.command_tx.clone(),
|
||||
) {
|
||||
Ok(watcher) => {
|
||||
_watcher = Some(watcher);
|
||||
self.send_event(ModelEvent::StartWatching)?;
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
self.send_event(ModelEvent::Error(err))?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.process_model(&new_model)?;
|
||||
model = Some(new_model);
|
||||
}
|
||||
HostCommand::TriggerEvaluation => {
|
||||
self.send_event(ModelEvent::ChangeDetected)?;
|
||||
if let Some(model) = &model {
|
||||
self.process_model(model)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.expect("Cannot create OS thread for host")
|
||||
}
|
||||
|
||||
// Evaluate and process a model.
|
||||
fn process_model(&mut self, model: &Model) -> Result<(), EventLoopClosed> {
|
||||
let evaluation = match model.evaluate() {
|
||||
Ok(evaluation) => evaluation,
|
||||
|
||||
Err(err) => {
|
||||
self.send_event(ModelEvent::Error(err))?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
self.send_event(ModelEvent::Evaluated)?;
|
||||
|
||||
if let Some(warn) = evaluation.warning {
|
||||
self.send_event(ModelEvent::Warning(warn))?;
|
||||
}
|
||||
|
||||
match self.shape_processor.process(&evaluation.shape) {
|
||||
Ok(shape) => self.send_event(ModelEvent::ProcessedShape(shape))?,
|
||||
|
||||
Err(err) => {
|
||||
self.send_event(ModelEvent::Error(err.into()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Send a message to the event loop.
|
||||
fn send_event(&mut self, event: ModelEvent) -> Result<(), EventLoopClosed> {
|
||||
self.model_event_tx
|
||||
.send(event)
|
||||
.map_err(|_| EventLoopClosed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An event emitted by the host thread
|
||||
#[derive(Debug)]
|
||||
pub enum ModelEvent {
|
||||
/// A new model is being watched
|
||||
StartWatching,
|
||||
|
||||
/// A change in the model has been detected
|
||||
ChangeDetected,
|
||||
|
||||
/// The model has been evaluated
|
||||
Evaluated,
|
||||
|
||||
/// The model has been processed
|
||||
ProcessedShape(ProcessedShape),
|
||||
|
||||
/// A warning
|
||||
Warning(String),
|
||||
|
||||
/// An error
|
||||
Error(Error),
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
//! # Fornjot Model Host
|
||||
//!
|
||||
//! This library is part of the [Fornjot] ecosystem. Fornjot is an open-source,
|
||||
//! code-first CAD application; and collection of libraries that make up the CAD
|
||||
//! application, but can be used independently.
|
||||
//!
|
||||
//! This library is an internal component of Fornjot. It is not relevant to end
|
||||
//! users that just want to create CAD models.
|
||||
//!
|
||||
//! The purpose of this library is to load Fornjot models and watch them for
|
||||
//! changes. Fornjot models are basically plugins that can be loaded into a CAD
|
||||
//! application. This library is the host for these model plugins.
|
||||
//!
|
||||
//! [Fornjot]: https://www.fornjot.app/
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod host;
|
||||
mod host_thread;
|
||||
mod model;
|
||||
mod parameters;
|
||||
mod platform;
|
||||
mod watcher;
|
||||
|
||||
pub(crate) use self::host_thread::{EventLoopClosed, HostThread};
|
||||
|
||||
pub use self::{
|
||||
host::{Host, HostCommand},
|
||||
host_thread::ModelEvent,
|
||||
model::{Error, Evaluation, Model},
|
||||
parameters::Parameters,
|
||||
watcher::Watcher,
|
||||
};
|
|
@ -1,354 +0,0 @@
|
|||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str,
|
||||
};
|
||||
|
||||
use fj::{abi, version::Version};
|
||||
use fj_operations::shape_processor;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{platform::HostPlatform, Parameters};
|
||||
|
||||
/// Represents a Fornjot model
|
||||
pub struct Model {
|
||||
src_path: PathBuf,
|
||||
lib_path: PathBuf,
|
||||
manifest_path: PathBuf,
|
||||
parameters: Parameters,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Initialize the model using the path to its crate
|
||||
///
|
||||
/// The path expected here is the root directory of the model's Cargo
|
||||
/// package, that is the folder containing `Cargo.toml`.
|
||||
pub fn new(
|
||||
path: impl AsRef<Path>,
|
||||
parameters: Parameters,
|
||||
) -> Result<Self, Error> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let crate_dir = path.canonicalize()?;
|
||||
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.current_dir(&crate_dir)
|
||||
.exec()?;
|
||||
|
||||
let pkg = package_associated_with_directory(&metadata, &crate_dir)?;
|
||||
let src_path = crate_dir.join("src");
|
||||
|
||||
let lib_path = {
|
||||
let name = pkg.name.replace('-', "_");
|
||||
let file = HostPlatform::lib_file_name(&name);
|
||||
let target_dir =
|
||||
metadata.target_directory.clone().into_std_path_buf();
|
||||
target_dir.join("debug").join(file)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
src_path,
|
||||
lib_path,
|
||||
manifest_path: pkg.manifest_path.as_std_path().to_path_buf(),
|
||||
parameters,
|
||||
})
|
||||
}
|
||||
|
||||
/// Access the path that needs to be watched for changes
|
||||
pub fn watch_path(&self) -> PathBuf {
|
||||
self.src_path.clone()
|
||||
}
|
||||
|
||||
/// Evaluate the model
|
||||
pub fn evaluate(&self) -> Result<Evaluation, Error> {
|
||||
let manifest_path = self.manifest_path.display().to_string();
|
||||
|
||||
let cargo_output = Command::new("cargo")
|
||||
.arg("rustc")
|
||||
.args(["--manifest-path", &manifest_path])
|
||||
.args(["--crate-type", "cdylib"])
|
||||
.output()?;
|
||||
|
||||
if !cargo_output.status.success() {
|
||||
let output =
|
||||
String::from_utf8(cargo_output.stderr).unwrap_or_else(|_| {
|
||||
String::from("Failed to fetch command output")
|
||||
});
|
||||
|
||||
return Err(Error::Compile { output });
|
||||
}
|
||||
|
||||
let seconds_taken = str::from_utf8(&cargo_output.stderr)
|
||||
.unwrap()
|
||||
.rsplit_once(' ')
|
||||
.unwrap()
|
||||
.1
|
||||
.trim();
|
||||
|
||||
let mut warnings = None;
|
||||
|
||||
// So, strictly speaking this is all unsound:
|
||||
// - `Library::new` requires us to abide by the arbitrary requirements
|
||||
// of any library initialization or termination routines.
|
||||
// - `Library::get` requires us to specify the correct type for the
|
||||
// model function.
|
||||
// - The model function itself is `unsafe`, because it is a function
|
||||
// from across an FFI interface.
|
||||
//
|
||||
// Typical models won't have initialization or termination routines (I
|
||||
// think), should abide by the `ModelFn` signature, and might not do
|
||||
// anything unsafe. But we have no way to know that the library the user
|
||||
// told us to load actually does (I think).
|
||||
//
|
||||
// I don't know of a way to fix this. We should take this as motivation
|
||||
// to switch to a better technique:
|
||||
// https://github.com/hannobraun/Fornjot/issues/71
|
||||
let shape = unsafe {
|
||||
let lib = libloading::Library::new(&self.lib_path)
|
||||
.map_err(Error::LoadingLibrary)?;
|
||||
|
||||
let version_pkg_host = fj::version::VERSION_PKG.to_string();
|
||||
|
||||
let version_pkg_model: libloading::Symbol<*const Version> =
|
||||
lib.get(b"VERSION_PKG").map_err(Error::LoadingVersion)?;
|
||||
let version_pkg_model = (**version_pkg_model).to_string();
|
||||
|
||||
debug!(
|
||||
"Comparing package versions (host: {}, model: {})",
|
||||
version_pkg_host, version_pkg_model
|
||||
);
|
||||
if version_pkg_host != version_pkg_model {
|
||||
let host = String::from_utf8_lossy(version_pkg_host.as_bytes())
|
||||
.into_owned();
|
||||
let model = version_pkg_model;
|
||||
|
||||
return Err(Error::VersionMismatch { host, model });
|
||||
}
|
||||
|
||||
let version_full_host = fj::version::VERSION_FULL.to_string();
|
||||
|
||||
let version_full_model: libloading::Symbol<*const Version> =
|
||||
lib.get(b"VERSION_FULL").map_err(Error::LoadingVersion)?;
|
||||
let version_full_model = (**version_full_model).to_string();
|
||||
|
||||
debug!(
|
||||
"Comparing full versions (host: {}, model: {})",
|
||||
version_full_host, version_full_model
|
||||
);
|
||||
if version_full_host != version_full_model {
|
||||
let host =
|
||||
String::from_utf8_lossy(version_full_host.as_bytes())
|
||||
.into_owned();
|
||||
let model = version_full_model;
|
||||
|
||||
warnings =
|
||||
Some(format!("{}", Error::VersionMismatch { host, model }));
|
||||
}
|
||||
|
||||
let init: libloading::Symbol<abi::InitFunction> = lib
|
||||
.get(abi::INIT_FUNCTION_NAME.as_bytes())
|
||||
.map_err(Error::LoadingInit)?;
|
||||
|
||||
let mut host = Host::new(&self.parameters);
|
||||
|
||||
match init(&mut abi::Host::from(&mut host)) {
|
||||
abi::ffi_safe::Result::Ok(_metadata) => {}
|
||||
abi::ffi_safe::Result::Err(e) => {
|
||||
return Err(Error::InitializeModel(e.into()));
|
||||
}
|
||||
}
|
||||
|
||||
let model = host.take_model().ok_or(Error::NoModelRegistered)?;
|
||||
|
||||
model.shape(&host).map_err(Error::Shape)?
|
||||
};
|
||||
|
||||
Ok(Evaluation {
|
||||
shape,
|
||||
compile_time: seconds_taken.into(),
|
||||
warning: warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of evaluating a model
|
||||
///
|
||||
/// See [`Model::evaluate`].
|
||||
#[derive(Debug)]
|
||||
pub struct Evaluation {
|
||||
/// The shape
|
||||
pub shape: fj::Shape,
|
||||
|
||||
/// The time it took to compile the shape, from the Cargo output
|
||||
pub compile_time: String,
|
||||
|
||||
/// Warnings
|
||||
pub warning: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Host<'a> {
|
||||
args: &'a Parameters,
|
||||
model: Option<Box<dyn fj::models::Model>>,
|
||||
}
|
||||
|
||||
impl<'a> Host<'a> {
|
||||
pub fn new(parameters: &'a Parameters) -> Self {
|
||||
Self {
|
||||
args: parameters,
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_model(&mut self) -> Option<Box<dyn fj::models::Model>> {
|
||||
self.model.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fj::models::Host for Host<'a> {
|
||||
fn register_boxed_model(&mut self, model: Box<dyn fj::models::Model>) {
|
||||
self.model = Some(model);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fj::models::Context for Host<'a> {
|
||||
fn get_argument(&self, name: &str) -> Option<&str> {
|
||||
self.args.get(name).map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
fn package_associated_with_directory<'m>(
|
||||
metadata: &'m cargo_metadata::Metadata,
|
||||
dir: &Path,
|
||||
) -> Result<&'m cargo_metadata::Package, Error> {
|
||||
for pkg in metadata.workspace_packages() {
|
||||
let crate_dir = pkg
|
||||
.manifest_path
|
||||
.parent()
|
||||
.and_then(|p| p.canonicalize().ok());
|
||||
|
||||
if crate_dir.as_deref() == Some(dir) {
|
||||
return Ok(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ambiguous_path_error(metadata, dir))
|
||||
}
|
||||
|
||||
fn ambiguous_path_error(
|
||||
metadata: &cargo_metadata::Metadata,
|
||||
dir: &Path,
|
||||
) -> Error {
|
||||
let mut possible_paths = Vec::new();
|
||||
|
||||
for id in &metadata.workspace_members {
|
||||
let cargo_toml = &metadata[id].manifest_path;
|
||||
let crate_dir = cargo_toml
|
||||
.parent()
|
||||
.expect("A Cargo.toml always has a parent");
|
||||
// Try to make the path relative to the workspace root so error messages
|
||||
// aren't super long.
|
||||
let simplified_path = crate_dir
|
||||
.strip_prefix(&metadata.workspace_root)
|
||||
.unwrap_or(crate_dir);
|
||||
|
||||
possible_paths.push(simplified_path.into());
|
||||
}
|
||||
|
||||
Error::AmbiguousPath {
|
||||
dir: dir.to_path_buf(),
|
||||
possible_paths,
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that can occur when loading or reloading a model
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Error loading model library
|
||||
#[error(
|
||||
"Failed to load model library\n\
|
||||
This might be a bug in Fornjot, or, at the very least, this error \
|
||||
message should be improved. Please report this!"
|
||||
)]
|
||||
LoadingLibrary(#[source] libloading::Error),
|
||||
|
||||
/// Error loading Fornjot version that the model uses
|
||||
#[error(
|
||||
"Failed to load the Fornjot version that the model uses\n\
|
||||
- Is your model using the `fj` library? All models must!\n\
|
||||
- Was your model created with a really old version of Fornjot?"
|
||||
)]
|
||||
LoadingVersion(#[source] libloading::Error),
|
||||
|
||||
/// Error loading the model's `init` function
|
||||
#[error(
|
||||
"Failed to load the model's `init` function\n\
|
||||
- Did you define a model function using `#[fj::model]`?"
|
||||
)]
|
||||
LoadingInit(#[source] libloading::Error),
|
||||
|
||||
/// Host version and model version do not match
|
||||
#[error("Host version ({host}) and model version ({model}) do not match")]
|
||||
VersionMismatch {
|
||||
/// The host version
|
||||
host: String,
|
||||
|
||||
/// The model version
|
||||
model: String,
|
||||
},
|
||||
|
||||
/// Model failed to compile
|
||||
#[error("Error compiling model\n{output}")]
|
||||
Compile {
|
||||
/// The compiler output
|
||||
output: String,
|
||||
},
|
||||
|
||||
/// I/O error while loading the model
|
||||
#[error("I/O error while loading model")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
/// Initializing a model failed.
|
||||
#[error("Unable to initialize the model")]
|
||||
InitializeModel(#[source] fj::models::Error),
|
||||
|
||||
/// The user forgot to register a model when calling
|
||||
/// [`fj::register_model!()`].
|
||||
#[error("No model was registered")]
|
||||
NoModelRegistered,
|
||||
|
||||
/// An error was returned from [`fj::models::Model::shape()`].
|
||||
#[error("Unable to determine the model's geometry")]
|
||||
Shape(#[source] fj::models::Error),
|
||||
|
||||
/// An error was returned from
|
||||
/// [`fj_operations::shape_processor::ShapeProcessor::process()`].
|
||||
#[error("Shape processing error")]
|
||||
ShapeProcessor(#[from] shape_processor::Error),
|
||||
|
||||
/// Error while watching the model code for changes
|
||||
#[error("Error watching model for changes")]
|
||||
Notify(#[from] notify::Error),
|
||||
|
||||
/// An error occurred while trying to use evaluate
|
||||
/// [`cargo_metadata::MetadataCommand`].
|
||||
#[error("Unable to determine the crate's metadata")]
|
||||
CargoMetadata(#[from] cargo_metadata::Error),
|
||||
|
||||
/// The user pointed us to a directory, but it doesn't look like that was
|
||||
/// a crate root (i.e. the folder containing `Cargo.toml`).
|
||||
#[error(
|
||||
"It doesn't look like \"{}\" is a crate directory. Did you mean one of {}?",
|
||||
dir.display(),
|
||||
possible_paths.iter().map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)]
|
||||
AmbiguousPath {
|
||||
/// The model directory supplied by the user.
|
||||
dir: PathBuf,
|
||||
/// The directories for each crate in the workspace, relative to the
|
||||
/// workspace root.
|
||||
possible_paths: Vec<PathBuf>,
|
||||
},
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
/// Parameters that are passed to a model.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Parameters(pub HashMap<String, String>);
|
||||
|
||||
impl Parameters {
|
||||
/// Construct an empty instance of `Parameters`
|
||||
pub fn empty() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
/// Insert a value into the [`Parameters`] dictionary, implicitly converting
|
||||
/// the arguments to strings and returning `&mut self` to enable chaining.
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
key: impl Into<String>,
|
||||
value: impl ToString,
|
||||
) -> &mut Self {
|
||||
self.0.insert(key.into(), value.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Parameters {
|
||||
type Target = HashMap<String, String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Parameters {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
// Represents platform trait
|
||||
pub trait Platform {
|
||||
fn model_lib_file_name(&self, name: &str) -> String;
|
||||
}
|
||||
|
||||
// Represents all supported platforms
|
||||
|
||||
// Mac OS
|
||||
struct Macos;
|
||||
// Windows
|
||||
struct Windows;
|
||||
// Linux
|
||||
struct Unix;
|
||||
|
||||
impl Platform for Windows {
|
||||
fn model_lib_file_name(&self, name: &str) -> String {
|
||||
format!("{name}.dll")
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for Macos {
|
||||
fn model_lib_file_name(&self, name: &str) -> String {
|
||||
format!("lib{name}.dylib")
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for Unix {
|
||||
fn model_lib_file_name(&self, name: &str) -> String {
|
||||
format!("lib{name}.so")
|
||||
}
|
||||
}
|
||||
|
||||
// Abstracts over differences in host platforms
|
||||
pub struct HostPlatform;
|
||||
|
||||
impl HostPlatform {
|
||||
pub fn get_os() -> Box<dyn Platform> {
|
||||
if cfg!(windows) {
|
||||
Box::new(Windows)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Box::new(Macos)
|
||||
} else {
|
||||
Box::new(Unix)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lib_file_name(name: &str) -> String {
|
||||
Self::get_os().model_lib_file_name(name)
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
use std::{collections::HashSet, ffi::OsStr, path::Path};
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use notify::Watcher as _;
|
||||
|
||||
use crate::{Error, HostCommand};
|
||||
|
||||
/// Watches a model for changes, reloading it continually
|
||||
pub struct Watcher {
|
||||
_watcher: Box<dyn notify::Watcher>,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
/// Watch the provided model for changes
|
||||
pub fn watch_model(
|
||||
watch_path: impl AsRef<Path>,
|
||||
host_tx: Sender<HostCommand>,
|
||||
) -> Result<Self, Error> {
|
||||
let watch_path = watch_path.as_ref();
|
||||
|
||||
let mut watcher = notify::recommended_watcher(
|
||||
move |event: notify::Result<notify::Event>| {
|
||||
// Unfortunately the `notify` documentation doesn't say when
|
||||
// this might happen, so no idea if it needs to be handled.
|
||||
let event = event.expect("Error handling watch event");
|
||||
|
||||
// Various acceptable ModifyKind kinds. Varies across platforms
|
||||
// (e.g. MacOs vs. Windows10)
|
||||
if let notify::EventKind::Modify(
|
||||
notify::event::ModifyKind::Any
|
||||
| notify::event::ModifyKind::Data(
|
||||
notify::event::DataChange::Any
|
||||
| notify::event::DataChange::Content,
|
||||
),
|
||||
) = event.kind
|
||||
{
|
||||
let file_ext = event
|
||||
.paths
|
||||
.get(0)
|
||||
.expect("File path missing in watch event")
|
||||
.extension();
|
||||
|
||||
let black_list = HashSet::from([
|
||||
OsStr::new("swp"),
|
||||
OsStr::new("tmp"),
|
||||
OsStr::new("swx"),
|
||||
]);
|
||||
|
||||
if let Some(ext) = file_ext {
|
||||
if black_list.contains(ext) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This will panic, if the other end is disconnected, which
|
||||
// is probably the result of a panic on that thread, or the
|
||||
// application is being shut down.
|
||||
//
|
||||
// Either way, not much we can do about it here.
|
||||
host_tx
|
||||
.send(HostCommand::TriggerEvaluation)
|
||||
.expect("Channel is disconnected");
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
watcher.watch(watch_path, notify::RecursiveMode::Recursive)?;
|
||||
|
||||
Ok(Self {
|
||||
_watcher: Box::new(watcher),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "fj-operations"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fj.workspace = true
|
||||
fj-interop.workspace = true
|
||||
fj-kernel.workspace = true
|
||||
fj-math.workspace = true
|
||||
itertools = "0.10.5"
|
||||
thiserror = "1.0.40"
|
|
@ -1,99 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use fj_interop::{debug::DebugInfo, ext::ArrayExt, mesh::Color};
|
||||
use fj_kernel::{
|
||||
algorithms::reverse::Reverse,
|
||||
objects::{Face, Sketch},
|
||||
operations::Insert,
|
||||
services::Services,
|
||||
};
|
||||
use fj_math::Aabb;
|
||||
|
||||
use super::Shape;
|
||||
|
||||
impl Shape for fj::Difference2d {
|
||||
type Brep = Sketch;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
// This method assumes that `b` is fully contained within `a`:
|
||||
// https://github.com/hannobraun/Fornjot/issues/92
|
||||
|
||||
let mut faces = Vec::new();
|
||||
|
||||
let mut exteriors = Vec::new();
|
||||
let mut interiors = Vec::new();
|
||||
|
||||
let [a, b] = self
|
||||
.shapes()
|
||||
.each_ref_ext()
|
||||
.map(|shape| shape.compute_brep(services, debug_info));
|
||||
|
||||
if let Some(face) = a.faces().into_iter().next() {
|
||||
// If there's at least one face to subtract from, we can proceed.
|
||||
|
||||
let surface = face.surface();
|
||||
|
||||
for face in a.faces() {
|
||||
assert_eq!(
|
||||
surface,
|
||||
face.surface(),
|
||||
"Trying to subtract faces with different surfaces.",
|
||||
);
|
||||
|
||||
exteriors.push(face.exterior().clone());
|
||||
for cycle in face.interiors() {
|
||||
interiors.push(cycle.clone().reverse(services));
|
||||
}
|
||||
}
|
||||
|
||||
for face in b.faces() {
|
||||
assert_eq!(
|
||||
surface,
|
||||
face.surface(),
|
||||
"Trying to subtract faces with different surfaces.",
|
||||
);
|
||||
|
||||
interiors.push(face.exterior().clone().reverse(services));
|
||||
}
|
||||
|
||||
// Faces only support one exterior, while the code here comes from
|
||||
// the time when a face could have multiple exteriors. This was only
|
||||
// a special case, i.e. faces that connected to themselves, and I
|
||||
// have my doubts that this code was ever correct in the first
|
||||
// place.
|
||||
//
|
||||
// Anyway, the following should make sure that at least any problems
|
||||
// this code causes become obvious. I don't know if this can ever
|
||||
// trigger, but better safe than sorry.
|
||||
let exterior = exteriors
|
||||
.pop()
|
||||
.expect("Can't construct face without an exterior");
|
||||
assert!(
|
||||
exteriors.is_empty(),
|
||||
"Can't construct face with multiple exteriors"
|
||||
);
|
||||
|
||||
let face = Face::new(
|
||||
surface.clone(),
|
||||
exterior,
|
||||
interiors,
|
||||
Some(Color(self.color())),
|
||||
);
|
||||
faces.push(face.insert(services));
|
||||
}
|
||||
|
||||
let difference = Sketch::new(faces).insert(services);
|
||||
difference.deref().clone()
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
// This is a conservative estimate of the bounding box: It's never going
|
||||
// to be bigger than the bounding box of the original shape that another
|
||||
// is being subtracted from.
|
||||
self.shapes()[0].bounding_volume()
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use fj_interop::debug::DebugInfo;
|
||||
use fj_kernel::{objects::FaceSet, services::Services};
|
||||
use fj_math::Aabb;
|
||||
|
||||
use super::Shape;
|
||||
|
||||
impl Shape for fj::Group {
|
||||
type Brep = FaceSet;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
let mut faces = FaceSet::new();
|
||||
|
||||
let a = self.a.compute_brep(services, debug_info);
|
||||
let b = self.b.compute_brep(services, debug_info);
|
||||
|
||||
faces.extend(a);
|
||||
faces.extend(b);
|
||||
|
||||
faces
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
let a = self.a.bounding_volume();
|
||||
let b = self.b.bounding_volume();
|
||||
|
||||
a.merged(&b)
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
//! # Fornjot CAD Operations
|
||||
//!
|
||||
//! This library is part of the [Fornjot] ecosystem. Fornjot is an open-source,
|
||||
//! code-first CAD application; and collection of libraries that make up the CAD
|
||||
//! application, but can be used independently.
|
||||
//!
|
||||
//! This library is an internal component of Fornjot. It is not relevant to end
|
||||
//! users that just want to create CAD models.
|
||||
//!
|
||||
//! Fornjot models use the [`fj`] crate to define a shape. This crate provides
|
||||
//! the connection between [`fj`] and the Fornjot kernel. It translates those
|
||||
//! operations into terms the kernel can understand.
|
||||
//!
|
||||
//! [Fornjot]: https://www.fornjot.app/
|
||||
//! [`fj`]: https://crates.io/crates/fj
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod shape_processor;
|
||||
|
||||
mod difference_2d;
|
||||
mod group;
|
||||
mod sketch;
|
||||
mod sweep;
|
||||
mod transform;
|
||||
|
||||
use fj_interop::debug::DebugInfo;
|
||||
use fj_kernel::{
|
||||
objects::{FaceSet, Sketch},
|
||||
services::Services,
|
||||
};
|
||||
use fj_math::Aabb;
|
||||
|
||||
/// Implemented for all operations from the [`fj`] crate
|
||||
pub trait Shape {
|
||||
/// The type that is used for the shape's boundary representation
|
||||
type Brep;
|
||||
|
||||
/// Compute the boundary representation of the shape
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep;
|
||||
|
||||
/// Access the axis-aligned bounding box of a shape
|
||||
///
|
||||
/// If a shape is empty, its [`Aabb`]'s `min` and `max` points must be equal
|
||||
/// (but are otherwise not specified).
|
||||
fn bounding_volume(&self) -> Aabb<3>;
|
||||
}
|
||||
|
||||
impl Shape for fj::Shape {
|
||||
type Brep = FaceSet;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
match self {
|
||||
Self::Shape2d(shape) => {
|
||||
shape.compute_brep(services, debug_info).faces().clone()
|
||||
}
|
||||
Self::Group(shape) => shape.compute_brep(services, debug_info),
|
||||
Self::Sweep(shape) => shape
|
||||
.compute_brep(services, debug_info)
|
||||
.shells()
|
||||
.map(|shell| shell.faces().clone())
|
||||
.reduce(|mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
Self::Transform(shape) => shape.compute_brep(services, debug_info),
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
match self {
|
||||
Self::Shape2d(shape) => shape.bounding_volume(),
|
||||
Self::Group(shape) => shape.bounding_volume(),
|
||||
Self::Sweep(shape) => shape.bounding_volume(),
|
||||
Self::Transform(shape) => shape.bounding_volume(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for fj::Shape2d {
|
||||
type Brep = Sketch;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
match self {
|
||||
Self::Difference(shape) => shape.compute_brep(services, debug_info),
|
||||
Self::Sketch(shape) => shape.compute_brep(services, debug_info),
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
match self {
|
||||
Self::Difference(shape) => shape.bounding_volume(),
|
||||
Self::Sketch(shape) => shape.bounding_volume(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//! API for processing shapes
|
||||
|
||||
use fj_interop::{debug::DebugInfo, processed_shape::ProcessedShape};
|
||||
use fj_kernel::{
|
||||
algorithms::{
|
||||
approx::{InvalidTolerance, Tolerance},
|
||||
triangulate::Triangulate,
|
||||
},
|
||||
services::Services,
|
||||
validate::ValidationError,
|
||||
};
|
||||
use fj_math::Scalar;
|
||||
|
||||
use crate::Shape as _;
|
||||
|
||||
/// Processes an [`fj::Shape`] into a [`ProcessedShape`]
|
||||
pub struct ShapeProcessor {
|
||||
/// The tolerance value used for creating the triangle mesh
|
||||
pub tolerance: Option<Tolerance>,
|
||||
}
|
||||
|
||||
impl ShapeProcessor {
|
||||
/// Process an [`fj::Shape`] into [`ProcessedShape`]
|
||||
pub fn process(&self, shape: &fj::Shape) -> Result<ProcessedShape, Error> {
|
||||
let aabb = shape.bounding_volume();
|
||||
|
||||
let tolerance = match self.tolerance {
|
||||
None => {
|
||||
// Compute a reasonable default for the tolerance value. To do
|
||||
// this, we just look at the smallest non-zero extent of the
|
||||
// bounding box and divide that by some value.
|
||||
let mut min_extent = Scalar::MAX;
|
||||
for extent in aabb.size().components {
|
||||
if extent > Scalar::ZERO && extent < min_extent {
|
||||
min_extent = extent;
|
||||
}
|
||||
}
|
||||
|
||||
let tolerance = min_extent / Scalar::from_f64(1000.);
|
||||
Tolerance::from_scalar(tolerance)?
|
||||
}
|
||||
Some(user_defined_tolerance) => user_defined_tolerance,
|
||||
};
|
||||
|
||||
let mut services = Services::new();
|
||||
let mut debug_info = DebugInfo::new();
|
||||
let shape = shape.compute_brep(&mut services, &mut debug_info);
|
||||
let mesh = (&shape, tolerance).triangulate();
|
||||
|
||||
Ok(ProcessedShape {
|
||||
aabb,
|
||||
mesh,
|
||||
debug_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A shape processing error
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Error converting to shape
|
||||
#[error("Error converting to shape")]
|
||||
ToShape(#[from] Box<ValidationError>),
|
||||
|
||||
/// Model has zero size
|
||||
#[error("Model has zero size")]
|
||||
Extent(#[from] InvalidTolerance),
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use fj_interop::{debug::DebugInfo, mesh::Color};
|
||||
use fj_kernel::{
|
||||
objects::{Cycle, Face, HalfEdge, Sketch},
|
||||
operations::{BuildCycle, BuildHalfEdge, Insert, UpdateCycle},
|
||||
services::Services,
|
||||
};
|
||||
use fj_math::{Aabb, Point};
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::Shape;
|
||||
|
||||
impl Shape for fj::Sketch {
|
||||
type Brep = Sketch;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
_: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
let surface = services.objects.surfaces.xy_plane();
|
||||
|
||||
let face = match self.chain() {
|
||||
fj::Chain::Circle(circle) => {
|
||||
let half_edge = HalfEdge::circle(circle.radius(), services)
|
||||
.insert(services);
|
||||
let exterior = Cycle::new([half_edge]).insert(services);
|
||||
|
||||
Face::new(
|
||||
surface,
|
||||
exterior,
|
||||
Vec::new(),
|
||||
Some(Color(self.color())),
|
||||
)
|
||||
}
|
||||
fj::Chain::PolyChain(poly_chain) => {
|
||||
let segments = poly_chain.to_segments();
|
||||
assert!(
|
||||
!segments.is_empty(),
|
||||
"Attempted to compute a Brep from an empty sketch"
|
||||
);
|
||||
|
||||
let exterior = {
|
||||
let mut cycle = Cycle::empty();
|
||||
|
||||
let segments = poly_chain
|
||||
.to_segments()
|
||||
.into_iter()
|
||||
.map(|fj::SketchSegment { endpoint, route }| {
|
||||
let endpoint = Point::from(endpoint);
|
||||
(endpoint, route)
|
||||
})
|
||||
.circular_tuple_windows();
|
||||
|
||||
for ((start, route), (end, _)) in segments {
|
||||
let half_edge = match route {
|
||||
fj::SketchSegmentRoute::Direct => {
|
||||
HalfEdge::line_segment(
|
||||
[start, end],
|
||||
None,
|
||||
services,
|
||||
)
|
||||
}
|
||||
fj::SketchSegmentRoute::Arc { angle } => {
|
||||
HalfEdge::arc(start, end, angle.rad(), services)
|
||||
}
|
||||
};
|
||||
let half_edge = half_edge.insert(services);
|
||||
|
||||
cycle = cycle.add_half_edges([half_edge]);
|
||||
}
|
||||
|
||||
cycle.insert(services)
|
||||
};
|
||||
|
||||
Face::new(
|
||||
surface,
|
||||
exterior,
|
||||
Vec::new(),
|
||||
Some(Color(self.color())),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let sketch = Sketch::new(vec![face.insert(services)]).insert(services);
|
||||
sketch.deref().clone()
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
match self.chain() {
|
||||
fj::Chain::Circle(circle) => Aabb {
|
||||
min: Point::from([-circle.radius(), -circle.radius(), 0.0]),
|
||||
max: Point::from([circle.radius(), circle.radius(), 0.0]),
|
||||
},
|
||||
fj::Chain::PolyChain(poly_chain) => {
|
||||
let segments = poly_chain.to_segments();
|
||||
assert!(
|
||||
!segments.is_empty(),
|
||||
"Attempted to compute a bounding box from an empty sketch"
|
||||
);
|
||||
|
||||
let mut points = vec![];
|
||||
|
||||
let mut start_point = segments[segments.len() - 1].endpoint;
|
||||
segments.iter().for_each(|segment| {
|
||||
match segment.route {
|
||||
fj::SketchSegmentRoute::Direct => (),
|
||||
fj::SketchSegmentRoute::Arc { angle } => {
|
||||
use std::f64::consts::PI;
|
||||
let arc = fj_math::Arc::from_endpoints_and_angle(
|
||||
start_point,
|
||||
segment.endpoint,
|
||||
fj_math::Scalar::from_f64(angle.rad()),
|
||||
);
|
||||
for circle_min_max_angle in
|
||||
[0., PI / 2., PI, 3. * PI / 2.]
|
||||
{
|
||||
let mm_angle = fj_math::Scalar::from_f64(
|
||||
circle_min_max_angle,
|
||||
);
|
||||
if arc.start_angle < mm_angle
|
||||
&& mm_angle < arc.end_angle
|
||||
{
|
||||
points.push(
|
||||
arc.center
|
||||
+ [
|
||||
arc.radius
|
||||
* circle_min_max_angle
|
||||
.cos(),
|
||||
arc.radius
|
||||
* circle_min_max_angle
|
||||
.sin(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
points.push(Point::from(segment.endpoint));
|
||||
start_point = segment.endpoint;
|
||||
});
|
||||
|
||||
Aabb::<3>::from_points(points.into_iter().map(Point::to_xyz))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use fj_interop::debug::DebugInfo;
|
||||
use fj_kernel::{
|
||||
algorithms::sweep::Sweep, objects::Solid, operations::Insert,
|
||||
services::Services,
|
||||
};
|
||||
use fj_math::{Aabb, Vector};
|
||||
|
||||
use super::Shape;
|
||||
|
||||
impl Shape for fj::Sweep {
|
||||
type Brep = Solid;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
let sketch = self
|
||||
.shape()
|
||||
.compute_brep(services, debug_info)
|
||||
.insert(services);
|
||||
|
||||
let path = Vector::from(self.path());
|
||||
|
||||
let solid = sketch.sweep(path, services);
|
||||
solid.deref().clone()
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
self.shape()
|
||||
.bounding_volume()
|
||||
.merged(&Aabb::<3>::from_points(
|
||||
self.shape()
|
||||
.bounding_volume()
|
||||
.vertices()
|
||||
.map(|v| v + self.path()),
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use fj_interop::debug::DebugInfo;
|
||||
use fj_kernel::{
|
||||
algorithms::transform::TransformObject, objects::FaceSet,
|
||||
services::Services,
|
||||
};
|
||||
use fj_math::{Aabb, Transform, Vector};
|
||||
|
||||
use super::Shape;
|
||||
|
||||
impl Shape for fj::Transform {
|
||||
type Brep = FaceSet;
|
||||
|
||||
fn compute_brep(
|
||||
&self,
|
||||
services: &mut Services,
|
||||
debug_info: &mut DebugInfo,
|
||||
) -> Self::Brep {
|
||||
self.shape
|
||||
.compute_brep(services, debug_info)
|
||||
.transform(&make_transform(self), services)
|
||||
}
|
||||
|
||||
fn bounding_volume(&self) -> Aabb<3> {
|
||||
make_transform(self).transform_aabb(&self.shape.bounding_volume())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_transform(transform: &fj::Transform) -> Transform {
|
||||
let axis = Vector::from(transform.axis).normalize();
|
||||
Transform::translation(transform.offset)
|
||||
* Transform::rotation(axis * transform.angle.rad())
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
[package]
|
||||
name = "fj-proc"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.56"
|
||||
quote = "1.0.23"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.162"
|
||||
optional = true
|
||||
|
||||
[dependencies.syn]
|
||||
version = "2.0.15"
|
||||
features = ["full", "extra-traits"]
|
|
@ -1,185 +0,0 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
|
||||
use crate::parse::{
|
||||
ArgumentMetadata, Constraint, ConstraintKind, ExtractedArgument,
|
||||
GeometryFunction, Initializer, Metadata, Model,
|
||||
};
|
||||
|
||||
impl Initializer {
|
||||
fn register() -> TokenStream {
|
||||
quote! {
|
||||
const _: () = {
|
||||
fj::register_model!(|host| {
|
||||
fj::models::HostExt::register_model(host, Model);
|
||||
|
||||
Ok(
|
||||
fj::models::Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
||||
.with_short_description(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.with_homepage(env!("CARGO_PKG_HOMEPAGE"))
|
||||
.with_repository(env!("CARGO_PKG_REPOSITORY"))
|
||||
.with_license(env!("CARGO_PKG_LICENSE")),
|
||||
)
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Initializer {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self { model } = self;
|
||||
|
||||
tokens.extend(Self::register());
|
||||
model.to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn definition() -> TokenStream {
|
||||
quote! { struct Model; }
|
||||
}
|
||||
|
||||
fn trait_implementation(&self) -> TokenStream {
|
||||
let Self { metadata, geometry } = self;
|
||||
|
||||
quote! {
|
||||
impl fj::models::Model for Model {
|
||||
#metadata
|
||||
#geometry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Model {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.extend(Self::definition());
|
||||
tokens.extend(self.trait_implementation());
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Metadata {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self { name, arguments } = self;
|
||||
|
||||
tokens.extend(quote! {
|
||||
fn metadata(&self) -> std::result::Result<fj::models::ModelMetadata, Box<dyn std::error::Error + Send + Sync +'static>> {
|
||||
Ok(fj::models::ModelMetadata::new(#name)
|
||||
#( .with_argument(#arguments) )*)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ArgumentMetadata {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
name,
|
||||
default_value,
|
||||
} = self;
|
||||
|
||||
tokens.extend(quote! { fj::models::ArgumentMetadata::new(#name) });
|
||||
|
||||
if let Some(default_value) = default_value {
|
||||
tokens.extend(quote! {
|
||||
.with_default_value(stringify!(#default_value))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for GeometryFunction {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
geometry_function,
|
||||
arguments,
|
||||
constraints,
|
||||
fallible,
|
||||
} = self;
|
||||
|
||||
let argument_names = arguments.iter().map(|a| &a.ident);
|
||||
|
||||
let invocation = quote! {
|
||||
#geometry_function(#( #argument_names ),*)
|
||||
};
|
||||
let invocation = if *fallible {
|
||||
quote! { #invocation.map(fj::Shape::from).map_err(Into::into) }
|
||||
} else {
|
||||
quote! { Ok(#invocation.into()) }
|
||||
};
|
||||
|
||||
tokens.extend(quote! {
|
||||
fn shape(
|
||||
&self,
|
||||
ctx: &dyn fj::models::Context,
|
||||
) -> Result<fj::Shape, fj::models::Error> {
|
||||
#( #arguments )*
|
||||
#( #constraints )*
|
||||
#invocation
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ExtractedArgument {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
ident,
|
||||
ty,
|
||||
default_value,
|
||||
} = self;
|
||||
|
||||
let name = ident.to_string();
|
||||
let t = match default_value {
|
||||
Some(default) => quote! {
|
||||
let #ident: #ty = match ctx.get_argument(#name) {
|
||||
Some(value) => value.parse()?,
|
||||
None => #default
|
||||
};
|
||||
},
|
||||
None => {
|
||||
let error_message = format!("Expected {name}");
|
||||
quote! {
|
||||
let #ident: #ty = match ctx.get_argument(#name) {
|
||||
Some(value) => value.parse()?,
|
||||
None => return Err(#error_message.into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokens.extend(t);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Constraint {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self { target, expr, kind } = self;
|
||||
|
||||
let operator = match kind {
|
||||
ConstraintKind::Max => quote!(<=),
|
||||
ConstraintKind::Min => quote!(>=),
|
||||
};
|
||||
let predicate = quote! { #target #operator #expr };
|
||||
// Note: this will cause `expr` to be evaluated twice. Predicates should
|
||||
// be pure functions, so in theory this shouldn't be an issue.
|
||||
let error_message = quote! {
|
||||
format!(
|
||||
"Expected {} {} {} (i.e. {} {} {})",
|
||||
stringify!(#target),
|
||||
stringify!(#operator),
|
||||
stringify!(#expr),
|
||||
#target,
|
||||
stringify!(#operator),
|
||||
#expr,
|
||||
)
|
||||
};
|
||||
|
||||
tokens.extend(quote! {
|
||||
if !(#predicate) {
|
||||
return Err(#error_message.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
mod expand;
|
||||
mod parse;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{parse_macro_input, FnArg, ItemFn};
|
||||
|
||||
/// Define a function-based model.
|
||||
///
|
||||
/// The simplest model function takes no parameters and returns a hard-coded
|
||||
/// `fj::Shape`.
|
||||
///
|
||||
/// ``` rust ignore
|
||||
/// # use fj_proc::model;
|
||||
/// use fj::{Circle, Sketch, Shape};
|
||||
/// #[model]
|
||||
/// fn model() -> Shape {
|
||||
/// let circle = Circle::from_radius(10.0);
|
||||
/// Sketch::from_circle(circle).into()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For convenience, you can also return anything that could be converted into
|
||||
/// a `fj::Shape` (e.g. a `fj::Sketch`).
|
||||
///
|
||||
/// ``` rust ignore
|
||||
/// # use fj_proc::model;
|
||||
/// use fj::{Circle, Sketch};
|
||||
/// #[model]
|
||||
/// fn model() -> Sketch {
|
||||
/// let circle = Circle::from_radius(10.0);
|
||||
/// Sketch::from_circle(circle)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The return type is checked at compile time. That means something like this
|
||||
/// won't work because `()` can't be converted into a `fj::Shape`.
|
||||
///
|
||||
/// ``` rust ignore
|
||||
/// # use fj_proc::model;
|
||||
/// #[model]
|
||||
/// fn model() { todo!() }
|
||||
/// ```
|
||||
///
|
||||
/// The model function's arguments can be anything that implement
|
||||
/// [`std::str::FromStr`].
|
||||
///
|
||||
/// ``` rust ignore
|
||||
/// # use fj_proc::model;
|
||||
/// #[model]
|
||||
/// fn cylinder(height: f64, label: String, is_horizontal: bool) -> fj::Shape { todo!() }
|
||||
/// ```
|
||||
///
|
||||
/// Constraints and default values can be added to an argument using the
|
||||
/// `#[param]` attribute.
|
||||
///
|
||||
/// ``` rust ignore
|
||||
/// use fj::syntax::*;
|
||||
///
|
||||
/// #[fj::model]
|
||||
/// pub fn spacer(
|
||||
/// #[param(default = 1.0, min = inner * 1.01)] outer: f64,
|
||||
/// #[param(default = 0.5, max = outer * 0.99)] inner: f64,
|
||||
/// #[param(default = 1.0)] height: f64,
|
||||
/// ) -> fj::Shape {
|
||||
/// let outer_edge = fj::Sketch::from_circle(fj::Circle::from_radius(outer));
|
||||
/// let inner_edge = fj::Sketch::from_circle(fj::Circle::from_radius(inner));
|
||||
///
|
||||
/// let footprint = outer_edge.difference(&inner_edge);
|
||||
/// let spacer = footprint.sweep([0., 0., height]);
|
||||
///
|
||||
/// spacer.into()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For more complex situations, model functions are allowed to return any
|
||||
/// error type that converts into a model error.
|
||||
///
|
||||
/// ``` rust ignore
|
||||
/// #[fj::model]
|
||||
/// pub fn model() -> Result<fj::Shape, std::env::VarError> {
|
||||
/// let home_dir = std::env::var("HOME")?;
|
||||
///
|
||||
/// todo!("Do something with {home_dir}")
|
||||
/// }
|
||||
///
|
||||
/// fn assert_convertible(e: std::env::VarError) -> fj::models::Error { e.into() }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn model(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(input as syn::ItemFn);
|
||||
|
||||
match parse::parse(&item) {
|
||||
Ok(init) => {
|
||||
let item = without_param_attrs(item);
|
||||
|
||||
let attrs = item.attrs;
|
||||
let vis = item.vis;
|
||||
let sig = item.sig;
|
||||
let statements = item.block.stmts;
|
||||
|
||||
let item = quote::quote! {
|
||||
#(#attrs)* #vis #sig {
|
||||
fj::abi::initialize_panic_handling();
|
||||
#(#statements)*
|
||||
}
|
||||
};
|
||||
|
||||
let tokens = quote::quote! {
|
||||
#item
|
||||
#init
|
||||
|
||||
};
|
||||
|
||||
tokens.into()
|
||||
}
|
||||
Err(e) => e.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip out any of our `#[param(...)]` attributes so the item will compile.
|
||||
fn without_param_attrs(mut item: ItemFn) -> ItemFn {
|
||||
for input in &mut item.sig.inputs {
|
||||
let attrs = match input {
|
||||
FnArg::Receiver(r) => &mut r.attrs,
|
||||
FnArg::Typed(t) => &mut t.attrs,
|
||||
};
|
||||
attrs.retain(|attr| !attr.path().is_ident("param"));
|
||||
}
|
||||
|
||||
item
|
||||
}
|
|
@ -1,388 +0,0 @@
|
|||
use proc_macro2::Ident;
|
||||
use syn::{
|
||||
bracketed, parenthesized, parse::Parse, parse_quote, Expr, ItemFn,
|
||||
ReturnType, Type,
|
||||
};
|
||||
|
||||
/// The call to `fj::register_model!()`.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Initializer {
|
||||
pub(crate) model: Model,
|
||||
}
|
||||
|
||||
/// The generated `Model` struct and its `fj::Model` impl.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Model {
|
||||
pub(crate) metadata: Metadata,
|
||||
pub(crate) geometry: GeometryFunction,
|
||||
}
|
||||
|
||||
/// The model metadata we return in `<_ as fj::Model>::metadata()`.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Metadata {
|
||||
pub(crate) name: String,
|
||||
pub(crate) arguments: Vec<ArgumentMetadata>,
|
||||
}
|
||||
|
||||
/// Metadata for a specific argument.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ArgumentMetadata {
|
||||
pub(crate) name: String,
|
||||
pub(crate) default_value: Option<Expr>,
|
||||
}
|
||||
|
||||
/// The `<_ as fj::Model>::shape()` function.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GeometryFunction {
|
||||
pub(crate) geometry_function: Ident,
|
||||
pub(crate) arguments: Vec<ExtractedArgument>,
|
||||
pub(crate) constraints: Vec<Constraint>,
|
||||
pub(crate) fallible: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExtractedArgument {
|
||||
pub(crate) ident: Ident,
|
||||
pub(crate) ty: Type,
|
||||
pub(crate) default_value: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Constraint {
|
||||
pub(crate) target: Ident,
|
||||
pub(crate) expr: Expr,
|
||||
pub(crate) kind: ConstraintKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum ConstraintKind {
|
||||
Min,
|
||||
Max,
|
||||
}
|
||||
|
||||
pub(crate) fn parse(f: &ItemFn) -> syn::Result<Initializer> {
|
||||
let model = parse_model(f)?;
|
||||
|
||||
Ok(Initializer { model })
|
||||
}
|
||||
|
||||
fn parse_model(item: &ItemFn) -> syn::Result<Model> {
|
||||
let geometry_function = item.sig.ident.clone();
|
||||
|
||||
let args: Vec<Argument> = item
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|inp| parse_quote!(#inp))
|
||||
.collect();
|
||||
|
||||
let metadata = Metadata {
|
||||
name: geometry_function.to_string(),
|
||||
arguments: args
|
||||
.iter()
|
||||
.map(|a| ArgumentMetadata {
|
||||
name: a.ident.to_string(),
|
||||
default_value: a.default(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let geometry = GeometryFunction {
|
||||
geometry_function,
|
||||
arguments: args
|
||||
.iter()
|
||||
.map(|a| ExtractedArgument {
|
||||
ident: a.ident.clone(),
|
||||
default_value: a.default(),
|
||||
ty: a.ty.clone(),
|
||||
})
|
||||
.collect(),
|
||||
constraints: args.iter().flat_map(argument_constraints).collect(),
|
||||
fallible: match &item.sig.output {
|
||||
ReturnType::Default => false,
|
||||
ReturnType::Type(_, ty) => contains_result(ty),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Model { metadata, geometry })
|
||||
}
|
||||
|
||||
fn contains_result(ty: &Type) -> bool {
|
||||
match ty {
|
||||
Type::Path(p) => p.path.segments.last().unwrap().ident == "Result",
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn argument_constraints(arg: &Argument) -> Vec<Constraint> {
|
||||
let Some(attr) = arg.attr.as_ref() else {
|
||||
return Vec::new()
|
||||
};
|
||||
|
||||
let mut constraints = Vec::new();
|
||||
|
||||
if let Some(min) = attr.get_minimum() {
|
||||
constraints.push(Constraint {
|
||||
target: arg.ident.clone(),
|
||||
expr: min.val,
|
||||
kind: ConstraintKind::Min,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(max) = attr.get_maximum() {
|
||||
constraints.push(Constraint {
|
||||
target: arg.ident.clone(),
|
||||
expr: max.val,
|
||||
kind: ConstraintKind::Max,
|
||||
});
|
||||
}
|
||||
|
||||
constraints
|
||||
}
|
||||
|
||||
/// Represents one parameter given to the `model`.
|
||||
///
|
||||
/// ```text
|
||||
/// #[param(default=3, min=4)] num_points: u64
|
||||
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~ ^^^-- ty
|
||||
/// | |
|
||||
/// attr ident
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
struct Argument {
|
||||
attr: Option<HelperAttribute>,
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
fn default(&self) -> Option<Expr> {
|
||||
self.attr
|
||||
.as_ref()
|
||||
.and_then(|attr| attr.get_default())
|
||||
.map(|param| param.val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Argument {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let attr = if input.peek(syn::token::Pound) {
|
||||
Some(input.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ident: Ident = input.parse()?;
|
||||
|
||||
let _: syn::token::Colon = input.parse()?;
|
||||
|
||||
let ty: Type = input.parse()?;
|
||||
Ok(Self { attr, ident, ty })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all arguments given to the `#[param]` attribute eg:
|
||||
///
|
||||
/// ```text
|
||||
/// #[param(default=3, min=4)]
|
||||
/// ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
struct HelperAttribute {
|
||||
param: Option<syn::punctuated::Punctuated<DefaultParam, syn::Token![,]>>,
|
||||
}
|
||||
|
||||
impl Parse for HelperAttribute {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let attr_content;
|
||||
let param_content;
|
||||
let _: syn::token::Pound = input.parse()?;
|
||||
bracketed!(attr_content in input);
|
||||
let ident: Ident = attr_content.parse()?;
|
||||
if ident != *"param" {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ident.clone(),
|
||||
format!(
|
||||
"Unknown attribute \"{ident}\" found, expected \"param\""
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if attr_content.peek(syn::token::Paren) {
|
||||
parenthesized!(param_content in attr_content);
|
||||
if param_content.is_empty() {
|
||||
Ok(Self { param: None })
|
||||
} else {
|
||||
Ok(Self {
|
||||
param: Some(
|
||||
syn::punctuated::Punctuated::parse_separated_nonempty_with(
|
||||
¶m_content,
|
||||
DefaultParam::parse,
|
||||
)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(Self { param: None })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HelperAttribute {
|
||||
fn get_parameter(&self, parameter_name: &str) -> Option<DefaultParam> {
|
||||
if let Some(values) = self.param.clone() {
|
||||
values.into_iter().find(|val| val.ident == *parameter_name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default(&self) -> Option<DefaultParam> {
|
||||
self.get_parameter("default")
|
||||
}
|
||||
|
||||
fn get_minimum(&self) -> Option<DefaultParam> {
|
||||
self.get_parameter("min")
|
||||
}
|
||||
|
||||
fn get_maximum(&self) -> Option<DefaultParam> {
|
||||
self.get_parameter("max")
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents one argument given to the `#[param]` attribute eg:
|
||||
///
|
||||
/// ```text
|
||||
/// #[param(default=3)]
|
||||
/// ^^^^^^^^^----- is parsed as DefaultParam{ ident: Some(default), val: 3 }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
struct DefaultParam {
|
||||
ident: Ident,
|
||||
val: syn::Expr,
|
||||
}
|
||||
|
||||
impl Parse for DefaultParam {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
if input.peek(syn::Ident) {
|
||||
let ident: Ident = input.parse()?;
|
||||
let _: syn::token::Eq = input.parse()?;
|
||||
Ok(Self {
|
||||
ident,
|
||||
val: input.parse()?,
|
||||
})
|
||||
} else {
|
||||
Err(input.parse::<Ident>().expect_err("No identifier found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::{quote, ToTokens};
|
||||
|
||||
#[test]
|
||||
fn parse_a_typical_model_function() {
|
||||
let tokens = quote! {
|
||||
pub fn spacer(
|
||||
#[param(default = 1.0, min = inner * 1.01)] outer: f64,
|
||||
#[param(default = 0.5, max = outer * 0.99)] inner: f64,
|
||||
height: f64,
|
||||
) -> fj::Shape {
|
||||
let outer_edge = fj::Sketch::from_circle(fj::Circle::from_radius(outer));
|
||||
let inner_edge = fj::Sketch::from_circle(fj::Circle::from_radius(inner));
|
||||
|
||||
let footprint = outer_edge.difference(&inner_edge);
|
||||
let spacer = footprint.sweep([0., 0., height]);
|
||||
|
||||
spacer.into()
|
||||
}
|
||||
};
|
||||
let function: ItemFn = syn::parse2(tokens).unwrap();
|
||||
|
||||
let Initializer {
|
||||
model: Model { metadata, geometry },
|
||||
} = parse(&function).unwrap();
|
||||
|
||||
// Note: we can't #[derive(PartialEq)] on our parsed structs because
|
||||
// proc_macro2::Ident and friends don't implement PartialEq, so let's
|
||||
// manually check everything parsed correctly.
|
||||
let Metadata { name, arguments } = metadata;
|
||||
assert_eq!(name, "spacer");
|
||||
let expected_meta = &[
|
||||
("outer".to_string(), Some("1.0".to_string())),
|
||||
("inner".to_string(), Some("0.5".to_string())),
|
||||
("height".to_string(), None),
|
||||
];
|
||||
let meta: Vec<_> = arguments
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
(
|
||||
arg.name.clone(),
|
||||
arg.default_value
|
||||
.as_ref()
|
||||
.map(|v| v.to_token_stream().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(meta, expected_meta);
|
||||
|
||||
let GeometryFunction {
|
||||
geometry_function,
|
||||
arguments,
|
||||
constraints,
|
||||
fallible,
|
||||
} = geometry;
|
||||
assert_eq!(geometry_function.to_string(), "spacer");
|
||||
assert!(!fallible);
|
||||
let arguments: Vec<_> = arguments
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
(
|
||||
arg.ident.to_string(),
|
||||
arg.default_value
|
||||
.as_ref()
|
||||
.map(|v| v.to_token_stream().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(arguments, expected_meta);
|
||||
let expected_constraints = &[
|
||||
(
|
||||
"outer".to_string(),
|
||||
"inner * 1.01".to_string(),
|
||||
ConstraintKind::Min,
|
||||
),
|
||||
(
|
||||
"inner".to_string(),
|
||||
"outer * 0.99".to_string(),
|
||||
ConstraintKind::Max,
|
||||
),
|
||||
];
|
||||
let constraints: Vec<_> = constraints
|
||||
.iter()
|
||||
.map(|Constraint { kind, expr, target }| {
|
||||
(
|
||||
target.to_string(),
|
||||
expr.to_token_stream().to_string(),
|
||||
*kind,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(constraints, expected_constraints);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fallible_function() {
|
||||
let tokens = quote! {
|
||||
pub fn spacer() -> Result<fj::Shape, Whatever> {
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
let function: ItemFn = syn::parse2(tokens).unwrap();
|
||||
|
||||
let init = parse(&function).unwrap();
|
||||
|
||||
assert!(init.model.geometry.fallible);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
[package]
|
||||
name = "fj"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
|
||||
|
||||
[dependencies]
|
||||
fj-proc.workspace = true
|
||||
backtrace = "0.3.67"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.162"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.96"
|
|
@ -1,103 +0,0 @@
|
|||
use std::{
|
||||
fmt::Write,
|
||||
path::PathBuf,
|
||||
process::{Command, Output, Stdio},
|
||||
};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let version = Version::determine()?;
|
||||
|
||||
println!("cargo:rustc-env=FJ_VERSION_PKG={}", version.pkg);
|
||||
println!("cargo:rustc-env=FJ_VERSION_FULL={}", version.full);
|
||||
|
||||
// Make sure the build script doesn't run too often.
|
||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Version {
|
||||
pkg: String,
|
||||
full: String,
|
||||
}
|
||||
impl Version {
|
||||
fn determine() -> anyhow::Result<Self> {
|
||||
let pkg = std::env::var("CARGO_PKG_VERSION").unwrap();
|
||||
let commit = git_description();
|
||||
|
||||
let official_release =
|
||||
std::env::var("RELEASE_DETECTED").as_deref() == Ok("true");
|
||||
println!("cargo:rerun-if-env-changed=RELEASE_DETECTED");
|
||||
|
||||
let mut full = format!("{pkg} (");
|
||||
|
||||
if official_release {
|
||||
write!(full, "official release binary")?;
|
||||
} else {
|
||||
write!(full, "development build")?;
|
||||
}
|
||||
|
||||
if let Some(commit) = commit {
|
||||
write!(full, "; {commit}")?;
|
||||
}
|
||||
|
||||
writeln!(full, ")")?;
|
||||
|
||||
Ok(Self { pkg, full })
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the current git commit.
|
||||
///
|
||||
/// This may fail if `git` isn't installed (unlikely) or if the `.git/` folder
|
||||
/// isn't accessible (more likely than you think). This typically happens when
|
||||
/// we're building just the `fj-app` crate in a Docker container or when
|
||||
/// someone is installing from crates.io via `cargo install`.
|
||||
fn git_description() -> Option<String> {
|
||||
let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(["describe", "--always", "--tags"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(&crate_dir);
|
||||
|
||||
let Output {
|
||||
status,
|
||||
stdout,
|
||||
stderr,
|
||||
} = cmd.output().ok()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&stdout);
|
||||
|
||||
if !status.success() {
|
||||
// Not sure if anyone will ever see this, but it could be helpful for
|
||||
// troubleshooting.
|
||||
eprintln!("Command failed: {cmd:?}");
|
||||
let stderr = String::from_utf8_lossy(&stderr);
|
||||
eprintln!("---- Stdout ----");
|
||||
eprintln!("{stdout}");
|
||||
eprintln!("---- Stderr ----");
|
||||
eprintln!("{stderr}");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Make sure we re-run whenever the current commit changes
|
||||
let project_root = crate_dir.ancestors().nth(2).unwrap();
|
||||
let head_file = project_root.join(".git").join("HEAD");
|
||||
println!("cargo:rerun-if-changed={}", head_file.display());
|
||||
|
||||
if let Ok(contents) = std::fs::read_to_string(&head_file) {
|
||||
// Most of the time the HEAD file will be `ref: refs/heads/$branch`, but
|
||||
// when it's a detached head we'll only get the commit hash and can skip
|
||||
// the rerun-if-changed logic.
|
||||
|
||||
if let Some((_, branch)) = contents.split_once(' ') {
|
||||
let commit_hash_file =
|
||||
project_root.join(".git").join(branch.trim());
|
||||
println!("cargo:rerun-if-changed={}", commit_hash_file.display());
|
||||
}
|
||||
}
|
||||
|
||||
Some(stdout.trim().to_string())
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
use std::{marker::PhantomData, os::raw::c_void, panic::AssertUnwindSafe};
|
||||
|
||||
use crate::abi::ffi_safe::StringSlice;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Context<'a> {
|
||||
user_data: *const c_void,
|
||||
get_argument: unsafe extern "C" fn(
|
||||
*const c_void,
|
||||
StringSlice,
|
||||
) -> crate::abi::ffi_safe::Result<
|
||||
StringSlice,
|
||||
crate::abi::ffi_safe::String,
|
||||
>,
|
||||
_lifetime: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a &dyn crate::models::Context> for Context<'a> {
|
||||
fn from(ctx: &'a &dyn crate::models::Context) -> Self {
|
||||
unsafe extern "C" fn get_argument(
|
||||
user_data: *const c_void,
|
||||
name: StringSlice,
|
||||
) -> crate::abi::ffi_safe::Result<
|
||||
StringSlice,
|
||||
crate::abi::ffi_safe::String,
|
||||
> {
|
||||
let ctx = &*(user_data as *const &dyn crate::models::Context);
|
||||
|
||||
match std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
ctx.get_argument(&name)
|
||||
})) {
|
||||
Ok(Some(arg)) => {
|
||||
crate::abi::ffi_safe::Result::Ok(StringSlice::from_str(arg))
|
||||
}
|
||||
Ok(None) => {
|
||||
crate::abi::ffi_safe::Result::Ok(StringSlice::from_str(""))
|
||||
}
|
||||
Err(payload) => crate::abi::ffi_safe::Result::Err(
|
||||
crate::abi::on_panic(payload),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Context {
|
||||
user_data: ctx as *const &dyn crate::models::Context
|
||||
as *const c_void,
|
||||
get_argument,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::models::Context for Context<'_> {
|
||||
fn get_argument(&self, name: &str) -> Option<&str> {
|
||||
unsafe {
|
||||
let Context {
|
||||
user_data,
|
||||
get_argument,
|
||||
_lifetime,
|
||||
} = *self;
|
||||
|
||||
let name = StringSlice::from_str(name);
|
||||
|
||||
match name.trim().is_empty() {
|
||||
true => None,
|
||||
false => match get_argument(user_data, name) {
|
||||
super::ffi_safe::Result::Ok(other) => {
|
||||
match other.is_empty() {
|
||||
true => None,
|
||||
false => Some(other.into_str()),
|
||||
}
|
||||
}
|
||||
super::ffi_safe::Result::Err(_) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,400 +0,0 @@
|
|||
//! FFI-safe versions of common `std` types.
|
||||
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
fmt::{self, Debug, Display, Formatter},
|
||||
ops::Deref,
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
use crate::models::Error;
|
||||
|
||||
/// A FFI-safe version of `Vec<T>`.
|
||||
#[repr(C)]
|
||||
pub(crate) struct Vec<T> {
|
||||
ptr: NonNull<T>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for Vec<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", &**self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Vec<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
**self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<std::vec::Vec<T>> for Vec<T> {
|
||||
fn from(mut items: std::vec::Vec<T>) -> Self {
|
||||
// Safety: To avoid accidental double-frees and other memory issues, we
|
||||
// need to go through a specific dance.
|
||||
unsafe {
|
||||
// first, get a pointer to the first element and its length
|
||||
let first_item = items.as_mut_ptr();
|
||||
let len = items.len();
|
||||
|
||||
// next, tell Vec to forget about these items so it won't try to
|
||||
// run their destructors if we return early (e.g. via a panic).
|
||||
// We've now taken over ownership of the items, but *not* the Vec's
|
||||
// backing array.
|
||||
items.set_len(0);
|
||||
|
||||
// Use the system allocator to create some space for our
|
||||
// FfiSafeVec's buffer.
|
||||
let layout = Layout::array::<T>(len).unwrap();
|
||||
let ptr: *mut T = System::default().alloc(layout).cast();
|
||||
let ptr = NonNull::new(ptr).expect("Allocation failed");
|
||||
|
||||
// Now, we can copy the items across
|
||||
std::ptr::copy_nonoverlapping(first_item, ptr.as_ptr(), len);
|
||||
|
||||
// the items are gone, time to free the original vec
|
||||
drop(items);
|
||||
|
||||
Self { ptr, len }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> From<Vec<T>> for std::vec::Vec<T> {
|
||||
fn from(v: Vec<T>) -> Self {
|
||||
v.iter().map(Clone::clone).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for Vec<T> {
|
||||
fn clone(&self) -> Self {
|
||||
self.iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<Vec<T>> for Box<[T]> {
|
||||
fn from(v: Vec<T>) -> Self {
|
||||
Self::from(&*v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Vec<T> {
|
||||
fn default() -> Self {
|
||||
std::vec::Vec::default().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<T> for Vec<T> {
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
let vec: std::vec::Vec<T> = iter.into_iter().collect();
|
||||
vec.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Vec<T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
// Safety: We control "ptr" and "len", so we know they are always
|
||||
// initialized and within bounds.
|
||||
unsafe {
|
||||
let Self { ptr, len } = *self;
|
||||
std::slice::from_raw_parts(ptr.as_ptr(), len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Vec<T> {
|
||||
fn drop(&mut self) {
|
||||
let Self { ptr, len } = *self;
|
||||
let ptr = ptr.as_ptr();
|
||||
|
||||
for i in 0..self.len {
|
||||
// Safety: We control the "len" field, so the item we're accessing
|
||||
// is always within bounds. We also don't touch values after their
|
||||
// destructors are called.
|
||||
unsafe {
|
||||
let item = ptr.add(i);
|
||||
std::ptr::drop_in_place(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: This vec is immutable, so we're using the same layout as the
|
||||
// original allocation. It's also not possible to touch the allocation
|
||||
// after Drop completes.
|
||||
unsafe {
|
||||
let layout = Layout::array::<T>(len).unwrap();
|
||||
System::default().dealloc(ptr.cast(), layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: We're Send+Sync as long as the underlying type is.
|
||||
unsafe impl<T: Send> Send for Vec<T> {}
|
||||
unsafe impl<T: Sync> Sync for Vec<T> {}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T> serde::ser::Serialize for Vec<T>
|
||||
where
|
||||
T: serde::ser::Serialize,
|
||||
{
|
||||
fn serialize<S>(
|
||||
&self,
|
||||
serializer: S,
|
||||
) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
self.deref().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de, T> serde::de::Deserialize<'de> for Vec<T>
|
||||
where
|
||||
T: serde::de::Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
Ok(std::vec::Vec::deserialize(deserializer)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A FFI-safe version of `Box<str>`.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct String(Vec<u8>);
|
||||
|
||||
impl From<std::string::String> for String {
|
||||
fn from(s: std::string::String) -> Self {
|
||||
Self(s.into_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for std::string::String {
|
||||
fn from(s: String) -> Self {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Box<str> {
|
||||
fn from(s: String) -> Self {
|
||||
Self::from(&*s)
|
||||
}
|
||||
}
|
||||
impl PartialEq<str> for String {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
**self == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for String {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
*self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for String {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for String {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
// Safety: The only way to create a FfiSafeString is from a valid Rust
|
||||
// string, so we can skip the UTF-8 checks.
|
||||
unsafe { std::str::from_utf8_unchecked(&self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// A version of `Result` that is `#[repr(C)]`.
|
||||
#[must_use]
|
||||
#[repr(C)]
|
||||
pub enum Result<T, E> {
|
||||
Ok(T),
|
||||
Err(E),
|
||||
}
|
||||
|
||||
impl<T, E: Debug> Result<T, E> {
|
||||
pub fn unwrap(self) -> T {
|
||||
match self {
|
||||
Self::Ok(value) => value,
|
||||
Self::Err(e) => panic!("Unwrapped an Err({e:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> From<std::result::Result<T, E>> for Result<T, E> {
|
||||
fn from(result: std::result::Result<T, E>) -> Self {
|
||||
match result {
|
||||
Ok(ok) => Self::Ok(ok),
|
||||
Err(err) => Self::Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> From<Result<T, E>> for std::result::Result<T, E> {
|
||||
fn from(result: Result<T, E>) -> Self {
|
||||
match result {
|
||||
Result::Ok(ok) => Self::Ok(ok),
|
||||
Result::Err(err) => Self::Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub(crate) struct Slice<T> {
|
||||
ptr: NonNull<T>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<T> Slice<T> {
|
||||
/// Create a new [`Slice`] from a slice.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// It is the caller's responsibility to make sure this [`Slice`] doesn't
|
||||
/// outlive the slice that was passed in.
|
||||
pub unsafe fn from_slice(items: &[T]) -> Self {
|
||||
let ptr = items.as_ptr();
|
||||
let len = items.len();
|
||||
Self {
|
||||
// Safety: It's okay to cast away the const because you can't mutate
|
||||
// a slice.
|
||||
ptr: NonNull::new(ptr as *mut T).unwrap(),
|
||||
len,
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn into_slice<'a>(self) -> &'a [T] {
|
||||
let Self { ptr, len } = self;
|
||||
std::slice::from_raw_parts(ptr.as_ptr(), len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for Slice<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Debug::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Slice<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
**self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Slice<T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
// Safety: We control both "ptr" and "len", so the array is always
|
||||
// initialized and within bounds.
|
||||
//
|
||||
// The lifetime of the &[T] is also bound to the lifetime of &self, so
|
||||
// this should be safe as long as people can never get a Slice<T> that
|
||||
// outlives the data it points to.
|
||||
unsafe {
|
||||
let Self { ptr, len, .. } = *self;
|
||||
std::slice::from_raw_parts(ptr.as_ptr(), len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct StringSlice(Slice<u8>);
|
||||
|
||||
impl StringSlice {
|
||||
/// Create a new [`StringSlice`].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// It is the caller's responsibility to make sure this [`Slice`] doesn't
|
||||
/// outlive the slice that was passed in.
|
||||
pub unsafe fn from_str(s: &str) -> Self {
|
||||
Self(Slice::from_slice(s.as_bytes()))
|
||||
}
|
||||
|
||||
pub unsafe fn into_str<'a>(self) -> &'a str {
|
||||
let bytes = self.0.into_slice();
|
||||
std::str::from_utf8_unchecked(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for StringSlice {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
// Safety: the only way you can construct a StringSlice is via a string.
|
||||
unsafe { std::str::from_utf8_unchecked(&self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct BoxedError {
|
||||
pub(crate) msg: String,
|
||||
}
|
||||
|
||||
impl Display for BoxedError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.msg, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BoxedError {}
|
||||
|
||||
impl From<Error> for BoxedError {
|
||||
fn from(err: Error) -> Self {
|
||||
// Open question: is it worth capturing the message from each source
|
||||
// error, too? We could have some sort of `sources: Vec<Source>` field
|
||||
// where `Source` is a private wrapper around String that implements
|
||||
// std::error::Error, however then people will see what *looks* like a
|
||||
// particular error type, but they won't be able to downcast to it.
|
||||
Self {
|
||||
msg: err.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(C)]
|
||||
pub enum Option<T> {
|
||||
Some(T),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<T> Option<T> {
|
||||
pub fn map<T2>(self, func: impl FnOnce(T) -> T2) -> Option<T2> {
|
||||
match self {
|
||||
Self::Some(value) => Option::Some(func(value)),
|
||||
Self::None => Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1, T2> From<std::option::Option<T1>> for Option<T2>
|
||||
where
|
||||
T1: Into<T2>,
|
||||
{
|
||||
fn from(opt: std::option::Option<T1>) -> Self {
|
||||
match opt {
|
||||
Some(value) => Self::Some(value.into()),
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for std::option::Option<T> {
|
||||
fn from(opt: Option<T>) -> Self {
|
||||
match opt {
|
||||
Option::Some(value) => Some(value),
|
||||
Option::None => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use std::{marker::PhantomData, os::raw::c_void, panic::AssertUnwindSafe};
|
||||
|
||||
use crate::abi::Model;
|
||||
|
||||
/// A FFI-safe `&mut dyn Host`.
|
||||
#[repr(C)]
|
||||
pub struct Host<'a> {
|
||||
user_data: *mut c_void,
|
||||
register_boxed_model: unsafe extern "C" fn(*mut c_void, model: Model),
|
||||
_lifetime: PhantomData<&'a mut ()>,
|
||||
}
|
||||
|
||||
impl<'a, H: crate::models::Host + Sized> From<&'a mut H> for Host<'a> {
|
||||
fn from(host: &'a mut H) -> Self {
|
||||
extern "C" fn register_boxed_model<H: crate::models::Host + Sized>(
|
||||
user_data: *mut c_void,
|
||||
model: Model,
|
||||
) {
|
||||
let host = unsafe { &mut *(user_data as *mut H) };
|
||||
|
||||
if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
host.register_boxed_model(Box::new(model));
|
||||
})) {
|
||||
crate::abi::on_panic(e);
|
||||
}
|
||||
}
|
||||
|
||||
Host {
|
||||
user_data: host as *mut H as *mut c_void,
|
||||
register_boxed_model: register_boxed_model::<H>,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::models::Host for Host<'a> {
|
||||
fn register_boxed_model(&mut self, model: Box<dyn crate::models::Model>) {
|
||||
let Host {
|
||||
user_data,
|
||||
register_boxed_model,
|
||||
..
|
||||
} = *self;
|
||||
|
||||
unsafe {
|
||||
register_boxed_model(user_data, model.into());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
use crate::abi::ffi_safe;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct ModelMetadata {
|
||||
name: ffi_safe::String,
|
||||
description: ffi_safe::Option<ffi_safe::String>,
|
||||
arguments: ffi_safe::Vec<ArgumentMetadata>,
|
||||
}
|
||||
|
||||
impl From<ModelMetadata> for crate::models::ModelMetadata {
|
||||
fn from(m: ModelMetadata) -> Self {
|
||||
let ModelMetadata {
|
||||
name,
|
||||
description,
|
||||
arguments,
|
||||
} = m;
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.map(Into::into).into(),
|
||||
arguments: arguments.iter().cloned().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::models::ModelMetadata> for ModelMetadata {
|
||||
fn from(m: crate::models::ModelMetadata) -> Self {
|
||||
let crate::models::ModelMetadata {
|
||||
name,
|
||||
description,
|
||||
arguments,
|
||||
} = m;
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
arguments: arguments.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(C)]
|
||||
pub struct Metadata {
|
||||
name: ffi_safe::String,
|
||||
version: ffi_safe::String,
|
||||
short_description: ffi_safe::Option<ffi_safe::String>,
|
||||
description: ffi_safe::Option<ffi_safe::String>,
|
||||
homepage: ffi_safe::Option<ffi_safe::String>,
|
||||
repository: ffi_safe::Option<ffi_safe::String>,
|
||||
license: ffi_safe::Option<ffi_safe::String>,
|
||||
}
|
||||
|
||||
impl From<Metadata> for crate::models::Metadata {
|
||||
fn from(m: Metadata) -> Self {
|
||||
let Metadata {
|
||||
name,
|
||||
version,
|
||||
short_description,
|
||||
description,
|
||||
homepage,
|
||||
repository,
|
||||
license,
|
||||
} = m;
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
version: version.into(),
|
||||
short_description: short_description.map(Into::into).into(),
|
||||
description: description.map(Into::into).into(),
|
||||
homepage: homepage.map(Into::into).into(),
|
||||
repository: repository.map(Into::into).into(),
|
||||
license: license.map(Into::into).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::models::Metadata> for Metadata {
|
||||
fn from(m: crate::models::Metadata) -> Self {
|
||||
let crate::models::Metadata {
|
||||
name,
|
||||
version,
|
||||
short_description,
|
||||
description,
|
||||
homepage,
|
||||
repository,
|
||||
license,
|
||||
} = m;
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
version: version.into(),
|
||||
short_description: short_description.into(),
|
||||
description: description.into(),
|
||||
homepage: homepage.into(),
|
||||
repository: repository.into(),
|
||||
license: license.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(C)]
|
||||
pub struct ArgumentMetadata {
|
||||
name: ffi_safe::String,
|
||||
description: ffi_safe::Option<ffi_safe::String>,
|
||||
default_value: ffi_safe::Option<ffi_safe::String>,
|
||||
}
|
||||
|
||||
impl From<crate::models::ArgumentMetadata> for ArgumentMetadata {
|
||||
fn from(meta: crate::models::ArgumentMetadata) -> Self {
|
||||
let crate::models::ArgumentMetadata {
|
||||
name,
|
||||
description,
|
||||
default_value,
|
||||
} = meta;
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
default_value: default_value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArgumentMetadata> for crate::models::ArgumentMetadata {
|
||||
fn from(meta: ArgumentMetadata) -> Self {
|
||||
let ArgumentMetadata {
|
||||
name,
|
||||
description,
|
||||
default_value,
|
||||
} = meta;
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.map(Into::into).into(),
|
||||
default_value: default_value.map(Into::into).into(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//! Internal implementation details for the host-guest interface.
|
||||
//!
|
||||
//! Note that the vast majority of this module is just providing FFI-safe
|
||||
//! versions of common `std` types (e.g. `Vec`, `String`, and `Box<dyn Error>`),
|
||||
//! or FFI-safe trait objects.
|
||||
//!
|
||||
/// If the macro generated the wrong code, this doctest would fail.
|
||||
///
|
||||
/// ```rust
|
||||
/// use fj::{abi::INIT_FUNCTION_NAME, models::Metadata};
|
||||
///
|
||||
/// fj::register_model!(|_| {
|
||||
/// Ok(Metadata::new("My Model", "1.2.3"))
|
||||
/// });
|
||||
///
|
||||
/// mod x {
|
||||
/// extern "C" {
|
||||
/// pub fn fj_model_init(_: *mut fj::abi::Host<'_>) -> fj::abi::InitResult;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // make sure our function has the right signature
|
||||
/// let func: fj::abi::InitFunction = fj_model_init;
|
||||
///
|
||||
/// // We can also make sure the unmangled name is correct by calling it.
|
||||
///
|
||||
/// let metadata: fj::models::Metadata = unsafe {
|
||||
/// let mut host = Host;
|
||||
/// let mut host = fj::abi::Host::from(&mut host);
|
||||
/// x::fj_model_init(&mut host).unwrap().into()
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(metadata.name, "My Model");
|
||||
///
|
||||
/// struct Host;
|
||||
/// impl fj::models::Host for Host {
|
||||
/// fn register_boxed_model(&mut self, model: Box<dyn fj::models::Model>) { todo!() }
|
||||
/// }
|
||||
/// ```
|
||||
mod context;
|
||||
pub mod ffi_safe;
|
||||
mod host;
|
||||
mod metadata;
|
||||
mod model;
|
||||
|
||||
use backtrace::Backtrace;
|
||||
use std::{any::Any, fmt::Display, panic, sync::Mutex};
|
||||
|
||||
pub use self::{
|
||||
context::Context,
|
||||
host::Host,
|
||||
metadata::{Metadata, ModelMetadata},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
/// Define the initialization routine used when registering models.
|
||||
///
|
||||
/// See the [`crate::model`] macro if your model can be implemented as a pure
|
||||
/// function.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use fj::models::*;
|
||||
///
|
||||
/// fj::register_model!(|host: &mut dyn Host| {
|
||||
/// host.register_model(MyModel::default());
|
||||
///
|
||||
/// Ok(Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")))
|
||||
/// });
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyModel { }
|
||||
///
|
||||
/// impl Model for MyModel {
|
||||
/// fn metadata(&self) -> std::result::Result<fj::models::ModelMetadata, Box<(dyn std::error::Error + Send + Sync + 'static)>> { todo!() }
|
||||
///
|
||||
/// fn shape(&self, ctx: &dyn Context) -> Result<fj::Shape, Error> {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! register_model {
|
||||
($init:expr) => {
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn fj_model_init(
|
||||
mut host: *mut $crate::abi::Host<'_>,
|
||||
) -> $crate::abi::InitResult {
|
||||
let init: fn(
|
||||
&mut dyn $crate::models::Host,
|
||||
) -> Result<
|
||||
$crate::models::Metadata,
|
||||
$crate::models::Error,
|
||||
> = $init;
|
||||
|
||||
match init(&mut *host) {
|
||||
Ok(meta) => $crate::abi::InitResult::Ok(meta.into()),
|
||||
Err(e) => $crate::abi::InitResult::Err(e.into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The signature of the function generated by [`register_model`].
|
||||
///
|
||||
/// ```rust
|
||||
/// fj::register_model!(|_| { todo!() });
|
||||
///
|
||||
/// const _: fj::abi::InitFunction = fj_model_init;
|
||||
/// ```
|
||||
pub type InitFunction = unsafe extern "C" fn(*mut Host<'_>) -> InitResult;
|
||||
pub type InitResult = ffi_safe::Result<Metadata, ffi_safe::BoxedError>;
|
||||
pub type ShapeResult = ffi_safe::Result<crate::Shape, ffi_safe::BoxedError>;
|
||||
pub type ModelMetadataResult =
|
||||
ffi_safe::Result<ModelMetadata, ffi_safe::BoxedError>;
|
||||
|
||||
/// The name of the function generated by [`register_model`].
|
||||
///
|
||||
pub const INIT_FUNCTION_NAME: &str = "fj_model_init";
|
||||
|
||||
// Contains details about a panic that we need to pass back to the application from the panic hook.
|
||||
struct PanicInfo {
|
||||
message: Option<String>,
|
||||
location: Option<Location>,
|
||||
backtrace: Backtrace,
|
||||
}
|
||||
|
||||
impl Display for PanicInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = self
|
||||
.message
|
||||
.as_ref()
|
||||
.map_or("No error given", |message| message.as_str());
|
||||
|
||||
write!(f, "\"{}\", ", message)?;
|
||||
|
||||
if let Some(location) = self.location.as_ref() {
|
||||
write!(f, "{}", location)?;
|
||||
} else {
|
||||
write!(f, "no location given")?;
|
||||
}
|
||||
|
||||
writeln!(f, "\nBacktrace:\n{:?}", self.backtrace)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Location {
|
||||
file: String,
|
||||
line: u32,
|
||||
column: u32,
|
||||
}
|
||||
|
||||
impl Display for Location {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.file, self.line, self.column)
|
||||
}
|
||||
}
|
||||
|
||||
static LAST_PANIC: Mutex<Option<PanicInfo>> = Mutex::new(None);
|
||||
|
||||
/// Capturing panics is something Rust really doesn't want you to do, and as such, they make it convoluted.
|
||||
/// This sets up all the machinery in the background to pull it off.
|
||||
///
|
||||
/// It's okay to call this multiple times.
|
||||
pub fn initialize_panic_handling() {
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
let mut last_panic =
|
||||
LAST_PANIC.lock().expect("Panic queue was poisoned."); // FIXME that can probably overflow the stack.
|
||||
let message = if let Some(s) =
|
||||
panic_info.payload().downcast_ref::<std::string::String>()
|
||||
{
|
||||
Some(s.as_str())
|
||||
} else {
|
||||
panic_info.payload().downcast_ref::<&str>().copied()
|
||||
}
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let location = panic_info.location().map(|location| Location {
|
||||
file: location.file().to_string(),
|
||||
line: location.line(),
|
||||
column: location.column(),
|
||||
});
|
||||
|
||||
let backtrace = backtrace::Backtrace::new();
|
||||
*last_panic = Some(PanicInfo {
|
||||
message,
|
||||
location,
|
||||
backtrace,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
fn on_panic(_payload: Box<dyn Any + Send>) -> crate::abi::ffi_safe::String {
|
||||
// The payload is technically no longer needed, but I left it there just in case a change to `catch_unwind` made
|
||||
// it useful again.
|
||||
if let Ok(mut panic_info) = LAST_PANIC.lock() {
|
||||
if let Some(panic_info) = panic_info.take() {
|
||||
crate::abi::ffi_safe::String::from(format!(
|
||||
"Panic in model: {}",
|
||||
panic_info
|
||||
))
|
||||
} else {
|
||||
crate::abi::ffi_safe::String::from(
|
||||
"Panic in model: No details were given.".to_string(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
crate::abi::ffi_safe::String::from(
|
||||
"Panic in model, but due to a poisoned panic queue the information could not be collected."
|
||||
.to_string())
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
use std::{os::raw::c_void, panic::AssertUnwindSafe};
|
||||
|
||||
use crate::{
|
||||
abi::{Context, ModelMetadataResult, ShapeResult},
|
||||
models::Error,
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Model {
|
||||
ptr: *mut c_void,
|
||||
metadata: unsafe extern "C" fn(*mut c_void) -> ModelMetadataResult,
|
||||
shape: unsafe extern "C" fn(*mut c_void, Context<'_>) -> ShapeResult,
|
||||
free: unsafe extern "C" fn(*mut c_void),
|
||||
}
|
||||
|
||||
impl crate::models::Model for Model {
|
||||
fn shape(
|
||||
&self,
|
||||
ctx: &dyn crate::models::Context,
|
||||
) -> Result<crate::Shape, Error> {
|
||||
let ctx = Context::from(&ctx);
|
||||
|
||||
let Self { ptr, shape, .. } = *self;
|
||||
|
||||
let result = unsafe { shape(ptr, ctx) };
|
||||
|
||||
match result {
|
||||
super::ffi_safe::Result::Ok(shape) => Ok(shape),
|
||||
super::ffi_safe::Result::Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Result<crate::models::ModelMetadata, Error> {
|
||||
let Self { ptr, metadata, .. } = *self;
|
||||
|
||||
let result = unsafe { metadata(ptr) };
|
||||
|
||||
match result {
|
||||
super::ffi_safe::Result::Ok(meta) => Ok(meta.into()),
|
||||
super::ffi_safe::Result::Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn crate::models::Model>> for Model {
|
||||
fn from(m: Box<dyn crate::models::Model>) -> Self {
|
||||
unsafe extern "C" fn metadata(
|
||||
user_data: *mut c_void,
|
||||
) -> ModelMetadataResult {
|
||||
let model = &*(user_data as *mut Box<dyn crate::models::Model>);
|
||||
|
||||
match std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
model.metadata()
|
||||
})) {
|
||||
Ok(Ok(meta)) => ModelMetadataResult::Ok(meta.into()),
|
||||
Ok(Err(err)) => ModelMetadataResult::Err(err.into()),
|
||||
Err(payload) => {
|
||||
ModelMetadataResult::Err(crate::abi::ffi_safe::BoxedError {
|
||||
msg: crate::abi::on_panic(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn shape(
|
||||
user_data: *mut c_void,
|
||||
ctx: Context<'_>,
|
||||
) -> ShapeResult {
|
||||
let model = &*(user_data as *mut Box<dyn crate::models::Model>);
|
||||
|
||||
match std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
model.shape(&ctx)
|
||||
})) {
|
||||
Ok(Ok(shape)) => ShapeResult::Ok(shape),
|
||||
Ok(Err(err)) => ShapeResult::Err(err.into()),
|
||||
Err(payload) => {
|
||||
ShapeResult::Err(crate::abi::ffi_safe::BoxedError {
|
||||
msg: crate::abi::on_panic(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn free(user_data: *mut c_void) {
|
||||
let model = user_data as *mut Box<dyn crate::models::Model>;
|
||||
|
||||
if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let model = Box::from_raw(model);
|
||||
drop(model);
|
||||
})) {
|
||||
crate::abi::on_panic(e);
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
ptr: Box::into_raw(Box::new(m)).cast(),
|
||||
metadata,
|
||||
shape,
|
||||
free,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Model {
|
||||
fn drop(&mut self) {
|
||||
let Self { ptr, free, .. } = *self;
|
||||
|
||||
unsafe {
|
||||
free(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: Our Model type is a FFI-safe version of Box<dyn crate::Model>, and
|
||||
// Box<dyn crate::Model>: Send+Sync.
|
||||
unsafe impl Send for Model {}
|
||||
unsafe impl Sync for Model {}
|
|
@ -1,130 +0,0 @@
|
|||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
// One gon in radians
|
||||
const GON_RAD: f64 = PI / 200.;
|
||||
|
||||
/// An angle
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Angle {
|
||||
/// The value of the angle in radians
|
||||
rad: f64,
|
||||
}
|
||||
|
||||
impl Angle {
|
||||
/// Create a new angle specified in radians
|
||||
pub fn from_rad(rad: f64) -> Self {
|
||||
Self { rad }
|
||||
}
|
||||
/// Create a new angle specified in degrees
|
||||
pub fn from_deg(deg: f64) -> Self {
|
||||
Self::from_rad(deg.to_radians())
|
||||
}
|
||||
/// Create a new angle specified in [revolutions](https://en.wikipedia.org/wiki/Turn_(angle))
|
||||
pub fn from_rev(rev: f64) -> Self {
|
||||
Self::from_rad(rev * TAU)
|
||||
}
|
||||
/// Create a new angle specified in [gon](https://en.wikipedia.org/wiki/Gradian)
|
||||
pub fn from_gon(gon: f64) -> Self {
|
||||
Self::from_rad(gon * GON_RAD)
|
||||
}
|
||||
/// Retrieve value of angle as radians
|
||||
pub fn rad(&self) -> f64 {
|
||||
self.rad
|
||||
}
|
||||
/// Retrieve value of angle as degrees
|
||||
pub fn deg(&self) -> f64 {
|
||||
self.rad.to_degrees()
|
||||
}
|
||||
/// Retrieve value of angle as [revolutions](https://en.wikipedia.org/wiki/Turn_(angle))
|
||||
pub fn rev(&self) -> f64 {
|
||||
self.rad / TAU
|
||||
}
|
||||
/// Retrieve value of angle as [gon](https://en.wikipedia.org/wiki/Gradian)
|
||||
pub fn gon(&self) -> f64 {
|
||||
self.rad / GON_RAD
|
||||
}
|
||||
|
||||
/// Returns this angle normalized to the range [0, 2pi) radians
|
||||
pub fn normalized(&self) -> Self {
|
||||
Self {
|
||||
rad: Self::wrap(self.rad),
|
||||
}
|
||||
}
|
||||
|
||||
// ensures that the angle is always 0 <= a < 2*pi
|
||||
fn wrap(rad: f64) -> f64 {
|
||||
let modulo = rad % TAU;
|
||||
if modulo < 0. {
|
||||
TAU + modulo
|
||||
} else {
|
||||
modulo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Angle {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self::from_rad(self.rad + rhs.rad)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::AddAssign for Angle {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.rad += rhs.rad;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for Angle {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self::from_rad(self.rad - rhs.rad)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::SubAssign for Angle {
|
||||
fn sub_assign(&mut self, rhs: Self) {
|
||||
self.rad -= rhs.rad;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for Angle {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: f64) -> Self::Output {
|
||||
Self::from_rad(self.rad * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Angle> for f64 {
|
||||
type Output = Angle;
|
||||
fn mul(self, rhs: Angle) -> Self::Output {
|
||||
rhs * self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::MulAssign<f64> for Angle {
|
||||
fn mul_assign(&mut self, rhs: f64) {
|
||||
self.rad *= rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<f64> for Angle {
|
||||
type Output = Self;
|
||||
fn div(self, rhs: f64) -> Self::Output {
|
||||
Self::from_rad(self.rad / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DivAssign<f64> for Angle {
|
||||
fn div_assign(&mut self, rhs: f64) {
|
||||
self.rad /= rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div for Angle {
|
||||
type Output = f64;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
self.rad / rhs.rad
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
use crate::Shape;
|
||||
|
||||
/// A group of two 3-dimensional shapes
|
||||
///
|
||||
/// A group is a collection of disjoint shapes. It is not a union, in that the
|
||||
/// shapes in the group are not allowed to touch or overlap.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Convenient syntax for this operation is available through [`crate::syntax`].
|
||||
///
|
||||
/// ``` rust
|
||||
/// # let a = fj::Sketch::from_points(vec![[0., 0.], [1., 0.], [0., 1.]]).unwrap();
|
||||
/// # let b = fj::Sketch::from_points(vec![[2., 0.], [3., 0.], [2., 1.]]).unwrap();
|
||||
/// use fj::syntax::*;
|
||||
///
|
||||
/// // `a` and `b` can be anything that converts to `fj::Shape`
|
||||
/// let group = a.group(&b);
|
||||
/// ```
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// Whether the shapes in the group touch or overlap is not currently checked.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Group {
|
||||
/// The first of the shapes
|
||||
pub a: Shape,
|
||||
|
||||
/// The second of the shapes
|
||||
pub b: Shape,
|
||||
}
|
||||
|
||||
impl From<Group> for Shape {
|
||||
fn from(shape: Group) -> Self {
|
||||
Self::Group(Box::new(shape))
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
//! # Fornjot Modeling Library
|
||||
//!
|
||||
//! This library is part of the [Fornjot] ecosystem. Fornjot is an open-source,
|
||||
//! code-first CAD application; and collection of libraries that make up the CAD
|
||||
//! application, but can be used independently.
|
||||
//!
|
||||
//! The purpose of this library is to support Fornjot models, which are just
|
||||
//! Rust libraries. Models depend on this library and use the primitives defined
|
||||
//! here to define a CAD model. Together with the Fornjot application, this
|
||||
//! library forms the part of Fornjot that is relevant to end users.
|
||||
//!
|
||||
//! To display the created CAD model, or export it to another file format, you
|
||||
//! need the Fornjot application. Please refer to the [Fornjot repository] for
|
||||
//! usage examples.
|
||||
//!
|
||||
//! [Fornjot]: https://www.fornjot.app/
|
||||
//! [Fornjot repository]: https://github.com/hannobraun/Fornjot
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod syntax;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod abi;
|
||||
mod angle;
|
||||
mod group;
|
||||
pub mod models;
|
||||
mod shape_2d;
|
||||
mod sweep;
|
||||
mod transform;
|
||||
pub mod version;
|
||||
|
||||
pub use self::{
|
||||
angle::*, group::Group, shape_2d::*, sweep::Sweep, transform::Transform,
|
||||
};
|
||||
pub use fj_proc::*;
|
||||
|
||||
/// A shape
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub enum Shape {
|
||||
/// A group of two 3-dimensional shapes
|
||||
Group(Box<Group>),
|
||||
|
||||
/// A 2D shape
|
||||
Shape2d(Shape2d),
|
||||
|
||||
/// A sweep of 2-dimensional shape along a straight path
|
||||
Sweep(Sweep),
|
||||
|
||||
/// A transformed 3-dimensional shape
|
||||
Transform(Box<Transform>),
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/// Contextual information passed to a [`Model`][crate::models::Model] when it
|
||||
/// is being initialized.
|
||||
pub trait Context {
|
||||
/// Get an argument that was passed to this model.
|
||||
fn get_argument(&self, name: &str) -> Option<&str>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn context_is_object_safe() {
|
||||
let _: &dyn Context;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use crate::models::Model;
|
||||
|
||||
/// An abstract interface to the Fornjot host.
|
||||
pub trait Host {
|
||||
/// Register a model.
|
||||
///
|
||||
/// This is mainly for more advanced use cases (e.g. when you need to close
|
||||
/// over extra state to load the model). For simpler models, you probably
|
||||
/// want to use [`HostExt::register_model()`] instead.
|
||||
fn register_boxed_model(&mut self, model: Box<dyn Model>);
|
||||
}
|
||||
|
||||
/// Extension methods to augment the [`Host`] API.
|
||||
///
|
||||
/// The purpose of this trait is to keep [`Host`] object-safe.
|
||||
pub trait HostExt {
|
||||
/// Register a model with the Fornjot runtime.
|
||||
fn register_model<M>(&mut self, model: M)
|
||||
where
|
||||
M: Model + 'static;
|
||||
}
|
||||
|
||||
impl<H: Host + ?Sized> HostExt for H {
|
||||
fn register_model<M>(&mut self, model: M)
|
||||
where
|
||||
M: Model + 'static,
|
||||
{
|
||||
self.register_boxed_model(Box::new(model));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn host_is_object_safe() {
|
||||
let _: &dyn Host;
|
||||
}
|
||||
}
|
|
@ -1,232 +0,0 @@
|
|||
/// Information about a particular module that can be used by the host for
|
||||
/// things like introspection and search.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Metadata {
|
||||
/// A short, human-friendly name used to identify this module.
|
||||
pub name: String,
|
||||
|
||||
/// A semver-compliant version number.
|
||||
pub version: String,
|
||||
|
||||
/// A short, one-line description.
|
||||
pub short_description: Option<String>,
|
||||
|
||||
/// A more elaborate description.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// A link to the homepage.
|
||||
pub homepage: Option<String>,
|
||||
|
||||
/// A link to the source code.
|
||||
pub repository: Option<String>,
|
||||
|
||||
/// The name of the software license(s) this software is released under.
|
||||
///
|
||||
/// This is interpreted as a SPDX license expression (e.g. `MIT OR
|
||||
/// Apache-2.0`). See [the SPDX site][spdx] for more information.
|
||||
///
|
||||
/// [spdx]: https://spdx.dev/spdx-specification-21-web-version/#h.jxpfx0ykyb60
|
||||
pub license: Option<String>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Create a [`Metadata`] object with the bare minimum required fields.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The `name` and `version` fields must not be empty.
|
||||
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
|
||||
let name = name.into();
|
||||
assert!(!name.is_empty());
|
||||
let version = version.into();
|
||||
assert!(!version.is_empty());
|
||||
|
||||
Self {
|
||||
name,
|
||||
version,
|
||||
short_description: None,
|
||||
description: None,
|
||||
homepage: None,
|
||||
repository: None,
|
||||
license: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`Metadata::short_description`] field.
|
||||
pub fn with_short_description(
|
||||
self,
|
||||
short_description: impl Into<String>,
|
||||
) -> Self {
|
||||
let short_description = short_description.into();
|
||||
if short_description.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
Self {
|
||||
short_description: Some(short_description),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`Metadata::description`] field.
|
||||
pub fn with_description(self, description: impl Into<String>) -> Self {
|
||||
let description = description.into();
|
||||
if description.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
Self {
|
||||
description: Some(description),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`Metadata::homepage`] field.
|
||||
pub fn with_homepage(self, homepage: impl Into<String>) -> Self {
|
||||
let homepage = homepage.into();
|
||||
if homepage.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
Self {
|
||||
homepage: Some(homepage),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`Metadata::repository`] field.
|
||||
pub fn with_repository(self, repository: impl Into<String>) -> Self {
|
||||
let repository = repository.into();
|
||||
if repository.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
Self {
|
||||
repository: Some(repository),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`Metadata::license`] field.
|
||||
pub fn with_license(self, license: impl Into<String>) -> Self {
|
||||
let license = license.into();
|
||||
if license.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
Self {
|
||||
license: Some(license),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata about a [`crate::models::Model`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ModelMetadata {
|
||||
/// A short, human-friendly name used to identify this model.
|
||||
pub name: String,
|
||||
|
||||
/// A description of what this model does.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Arguments that the model uses when calculating its geometry.
|
||||
pub arguments: Vec<ArgumentMetadata>,
|
||||
}
|
||||
|
||||
impl ModelMetadata {
|
||||
/// Create metadata for a model.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The `name` must not be empty.
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
let name = name.into();
|
||||
assert!(!name.is_empty());
|
||||
|
||||
Self {
|
||||
name,
|
||||
description: None,
|
||||
arguments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`ModelMetadata::description`].
|
||||
pub fn with_description(self, description: impl Into<String>) -> Self {
|
||||
let description = description.into();
|
||||
if description.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
Self {
|
||||
description: Some(description),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an argument to the [`ModelMetadata::arguments`] list.
|
||||
///
|
||||
/// As a convenience, string literals can be automatically converted into
|
||||
/// [`ArgumentMetadata`] with no description or default value.
|
||||
pub fn with_argument(mut self, arg: impl Into<ArgumentMetadata>) -> Self {
|
||||
self.arguments.push(arg.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata describing a model's argument.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ArgumentMetadata {
|
||||
/// The name used to refer to this argument.
|
||||
pub name: String,
|
||||
|
||||
/// A short description of this argument that could be shown to the user
|
||||
/// in something like a tooltip.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Something that could be used as a default if no value was provided.
|
||||
pub default_value: Option<String>,
|
||||
}
|
||||
|
||||
impl ArgumentMetadata {
|
||||
/// Create a new [`ArgumentMetadata`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The `name` must not be empty.
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
let name = name.into();
|
||||
assert!(!name.is_empty());
|
||||
Self {
|
||||
name,
|
||||
description: None,
|
||||
default_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`ArgumentMetadata::description`].
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
let description = description.into();
|
||||
if description.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
self.description = Some(description);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`ArgumentMetadata::default_value`].
|
||||
pub fn with_default_value(
|
||||
mut self,
|
||||
default_value: impl Into<String>,
|
||||
) -> Self {
|
||||
self.default_value = Some(default_value.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ArgumentMetadata {
|
||||
fn from(name: &str) -> Self {
|
||||
Self::new(name)
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
//! Interfaces used when defining models.
|
||||
|
||||
mod context;
|
||||
mod host;
|
||||
mod metadata;
|
||||
mod model;
|
||||
|
||||
pub use self::{
|
||||
context::Context,
|
||||
host::{Host, HostExt},
|
||||
metadata::{ArgumentMetadata, Metadata, ModelMetadata},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
/// A generic error used when defining a model.
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
|
@ -1,23 +0,0 @@
|
|||
use crate::{
|
||||
models::{Context, Error, ModelMetadata},
|
||||
Shape,
|
||||
};
|
||||
|
||||
/// A model.
|
||||
pub trait Model: Send + Sync {
|
||||
/// Calculate this model's concrete geometry.
|
||||
fn shape(&self, ctx: &dyn Context) -> Result<Shape, Error>;
|
||||
|
||||
/// Get metadata for the model.
|
||||
fn metadata(&self) -> Result<ModelMetadata, Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_object_safe() {
|
||||
let _: &dyn Model;
|
||||
}
|
||||
}
|
|
@ -1,259 +0,0 @@
|
|||
use crate::{abi::ffi_safe, Angle, Shape};
|
||||
|
||||
/// A 2-dimensional shape
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub enum Shape2d {
|
||||
/// A difference between two shapes
|
||||
Difference(Box<Difference2d>),
|
||||
|
||||
/// A sketch
|
||||
Sketch(Sketch),
|
||||
}
|
||||
|
||||
impl Shape2d {
|
||||
/// Get the rendering color of the larger object in RGBA
|
||||
pub fn color(&self) -> [u8; 4] {
|
||||
match &self {
|
||||
Self::Sketch(s) => s.color(),
|
||||
Self::Difference(d) => d.color(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A difference between two shapes
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Convenient syntax for this operation is available through [`crate::syntax`].
|
||||
///
|
||||
/// ``` rust
|
||||
/// # let a = fj::Sketch::from_points(vec![[0., 0.], [1., 0.], [0., 1.]]).unwrap();
|
||||
/// # let b = fj::Sketch::from_points(vec![[2., 0.], [3., 0.], [2., 1.]]).unwrap();
|
||||
/// use fj::syntax::*;
|
||||
///
|
||||
/// // `a` and `b` can be anything that converts to `fj::Shape2d`
|
||||
/// let difference = a.difference(&b);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Difference2d {
|
||||
shapes: [Shape2d; 2],
|
||||
}
|
||||
|
||||
impl Difference2d {
|
||||
/// Create a `Difference2d` from two shapes
|
||||
pub fn from_shapes(shapes: [Shape2d; 2]) -> Self {
|
||||
Self { shapes }
|
||||
}
|
||||
|
||||
/// Get the rendering color of the larger object in RGBA
|
||||
pub fn color(&self) -> [u8; 4] {
|
||||
self.shapes[0].color()
|
||||
}
|
||||
|
||||
/// Access the shapes that make up the difference
|
||||
pub fn shapes(&self) -> &[Shape2d; 2] {
|
||||
&self.shapes
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Difference2d> for Shape {
|
||||
fn from(shape: Difference2d) -> Self {
|
||||
Self::Shape2d(shape.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Difference2d> for Shape2d {
|
||||
fn from(shape: Difference2d) -> Self {
|
||||
Self::Difference(Box::new(shape))
|
||||
}
|
||||
}
|
||||
|
||||
/// A sketch
|
||||
///
|
||||
/// Sketches are currently limited to a single cycle of straight lines,
|
||||
/// represented by a number of points. For example, if the points a, b, and c
|
||||
/// are provided, the edges ab, bc, and ca are assumed.
|
||||
///
|
||||
/// Nothing about these edges is checked right now, but algorithms might assume
|
||||
/// that the edges are non-overlapping. If you create a `Sketch` with
|
||||
/// overlapping edges, you're on your own.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Convenient syntax for this operation is available through [`crate::syntax`].
|
||||
///
|
||||
/// ``` rust
|
||||
/// use fj::syntax::*;
|
||||
///
|
||||
/// // `a` and `b` can be anything that converts to `fj::Shape`
|
||||
/// let sketch = [[0., 0.], [1., 0.], [0., 1.]].sketch();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Sketch {
|
||||
chain: Chain,
|
||||
color: [u8; 4],
|
||||
}
|
||||
|
||||
impl Sketch {
|
||||
/// Create a sketch made of sketch segments
|
||||
pub fn from_segments(segments: Vec<SketchSegment>) -> Option<Self> {
|
||||
// TODO Returning an option is just a temporary solution, see: https://github.com/hannobraun/Fornjot/issues/1507
|
||||
if segments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Self {
|
||||
chain: Chain::PolyChain(PolyChain::from_segments(segments)),
|
||||
color: [255, 0, 0, 255],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a sketch made of straight lines from a bunch of points
|
||||
pub fn from_points(points: Vec<[f64; 2]>) -> Option<Self> {
|
||||
if points.is_empty() {
|
||||
// TODO Returning an option is just a temporary solution, see: https://github.com/hannobraun/Fornjot/issues/1507
|
||||
None
|
||||
} else {
|
||||
Some(Self {
|
||||
chain: Chain::PolyChain(PolyChain::from_points(points)),
|
||||
color: [255, 0, 0, 255],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a sketch from a circle
|
||||
pub fn from_circle(circle: Circle) -> Self {
|
||||
Self {
|
||||
chain: Chain::Circle(circle),
|
||||
color: [255, 0, 0, 255],
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the rendering color of the sketch in RGBA
|
||||
pub fn with_color(mut self, color: [u8; 4]) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Access the chain of the sketch
|
||||
pub fn chain(&self) -> &Chain {
|
||||
&self.chain
|
||||
}
|
||||
|
||||
/// Get the rendering color of the sketch in RGBA
|
||||
pub fn color(&self) -> [u8; 4] {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sketch> for Shape {
|
||||
fn from(shape: Sketch) -> Self {
|
||||
Self::Shape2d(shape.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sketch> for Shape2d {
|
||||
fn from(shape: Sketch) -> Self {
|
||||
Self::Sketch(shape)
|
||||
}
|
||||
}
|
||||
|
||||
/// A chain of elements that is part of a [`Sketch`]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub enum Chain {
|
||||
/// The chain is a circle
|
||||
Circle(Circle),
|
||||
|
||||
/// The chain is a polygonal chain
|
||||
PolyChain(PolyChain),
|
||||
}
|
||||
|
||||
/// A circle that is part of a [`Sketch`]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Circle {
|
||||
/// The radius of the circle
|
||||
radius: f64,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
/// Construct a new circle with a specific radius
|
||||
pub fn from_radius(radius: f64) -> Self {
|
||||
Self { radius }
|
||||
}
|
||||
|
||||
/// Access the circle's radius
|
||||
pub fn radius(&self) -> f64 {
|
||||
self.radius
|
||||
}
|
||||
}
|
||||
|
||||
/// A polygonal chain that is part of a [`Sketch`]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct PolyChain {
|
||||
segments: ffi_safe::Vec<SketchSegment>,
|
||||
}
|
||||
|
||||
impl PolyChain {
|
||||
/// Construct an instance from a list of segments
|
||||
pub fn from_segments(segments: Vec<SketchSegment>) -> Self {
|
||||
Self {
|
||||
segments: segments.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an instance from a list of points
|
||||
pub fn from_points(points: Vec<[f64; 2]>) -> Self {
|
||||
let segments = points
|
||||
.into_iter()
|
||||
.map(|endpoint| SketchSegment {
|
||||
endpoint,
|
||||
route: SketchSegmentRoute::Direct,
|
||||
})
|
||||
.collect();
|
||||
Self::from_segments(segments)
|
||||
}
|
||||
|
||||
/// Return the points that define the polygonal chain
|
||||
pub fn to_segments(&self) -> Vec<SketchSegment> {
|
||||
self.segments.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A segment of a sketch
|
||||
///
|
||||
/// Each segment starts at the previous point of the sketch.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct SketchSegment {
|
||||
/// The destination point of the segment
|
||||
pub endpoint: [f64; 2],
|
||||
/// The path taken by the segment to get to the endpoint
|
||||
pub route: SketchSegmentRoute,
|
||||
}
|
||||
|
||||
/// Possible paths that a [`SketchSegment`] can take to the next endpoint
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub enum SketchSegmentRoute {
|
||||
/// A straight line to the endpoint
|
||||
Direct,
|
||||
/// An arc to the endpoint with a given angle
|
||||
Arc {
|
||||
/// The angle of the arc
|
||||
angle: Angle,
|
||||
},
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use crate::{Shape, Shape2d};
|
||||
|
||||
/// A sweep of a 2-dimensional shape along straight path
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Convenient syntax for this operation is available through [`crate::syntax`].
|
||||
///
|
||||
/// ``` rust
|
||||
/// # let shape = fj::Sketch::from_points(vec![[0., 0.], [1., 0.], [0., 1.]]).unwrap();
|
||||
/// use fj::syntax::*;
|
||||
///
|
||||
/// // `shape` can be anything that converts to `fj::Shape2d`
|
||||
/// let group = shape.sweep([0., 0., 1.]);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Sweep {
|
||||
/// The 2-dimensional shape being swept
|
||||
shape: Shape2d,
|
||||
|
||||
/// The length and direction of the sweep
|
||||
path: [f64; 3],
|
||||
}
|
||||
|
||||
impl Sweep {
|
||||
/// Create a `Sweep` along a straight path
|
||||
pub fn from_path(shape: Shape2d, path: [f64; 3]) -> Self {
|
||||
Self { shape, path }
|
||||
}
|
||||
|
||||
/// Access the shape being swept
|
||||
pub fn shape(&self) -> &Shape2d {
|
||||
&self.shape
|
||||
}
|
||||
|
||||
/// Access the path of the sweep
|
||||
pub fn path(&self) -> [f64; 3] {
|
||||
self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sweep> for Shape {
|
||||
fn from(shape: Sweep) -> Self {
|
||||
Self::Sweep(shape)
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
//! Convenient syntax for `fj` operations
|
||||
//!
|
||||
//! This model defines extension traits, which provide convenient syntax for
|
||||
//! the various operations defined in this trait.
|
||||
|
||||
/// Convenient syntax to create an [`fj::Difference2d`]
|
||||
///
|
||||
/// [`fj::Difference2d`]: crate::Difference2d
|
||||
pub trait Difference {
|
||||
/// Create a difference between `self` and `other`
|
||||
fn difference<Other>(&self, other: &Other) -> crate::Difference2d
|
||||
where
|
||||
Other: Clone + Into<crate::Shape2d>;
|
||||
}
|
||||
|
||||
impl<T> Difference for T
|
||||
where
|
||||
T: Clone + Into<crate::Shape2d>,
|
||||
{
|
||||
fn difference<Other>(&self, other: &Other) -> crate::Difference2d
|
||||
where
|
||||
Other: Clone + Into<crate::Shape2d>,
|
||||
{
|
||||
let a = self.clone().into();
|
||||
let b = other.clone().into();
|
||||
|
||||
crate::Difference2d::from_shapes([a, b])
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenient syntax to create an [`fj::Group`]
|
||||
///
|
||||
/// [`fj::Group`]: crate::Group
|
||||
pub trait Group {
|
||||
/// Create a group with `self` and `other`
|
||||
fn group<Other>(&self, other: &Other) -> crate::Group
|
||||
where
|
||||
Other: Clone + Into<crate::Shape>;
|
||||
}
|
||||
|
||||
impl<T> Group for T
|
||||
where
|
||||
T: Clone + Into<crate::Shape>,
|
||||
{
|
||||
fn group<Other>(&self, other: &Other) -> crate::Group
|
||||
where
|
||||
Other: Clone + Into<crate::Shape>,
|
||||
{
|
||||
let a = self.clone().into();
|
||||
let b = other.clone().into();
|
||||
|
||||
crate::Group { a, b }
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenient syntax to create an [`fj::Sketch`]
|
||||
///
|
||||
/// [`fj::Sketch`]: crate::Sketch
|
||||
pub trait Sketch {
|
||||
/// Create a sketch from `self`
|
||||
///
|
||||
/// Can be called on any type that implements `AsRef<[[f64; 2]]`, which is
|
||||
/// implemented for types like slices, arrays, or `Vec`.
|
||||
fn sketch(&self) -> crate::Sketch;
|
||||
}
|
||||
|
||||
impl<T> Sketch for T
|
||||
where
|
||||
T: AsRef<[[f64; 2]]>,
|
||||
{
|
||||
fn sketch(&self) -> crate::Sketch {
|
||||
crate::Sketch::from_points(self.as_ref().to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenient syntax to create an [`fj::Sweep`]
|
||||
///
|
||||
/// [`fj::Sweep`]: crate::Sweep
|
||||
pub trait Sweep {
|
||||
/// Sweep `self` along a straight path
|
||||
fn sweep(&self, path: [f64; 3]) -> crate::Sweep;
|
||||
}
|
||||
|
||||
impl<T> Sweep for T
|
||||
where
|
||||
T: Clone + Into<crate::Shape2d>,
|
||||
{
|
||||
fn sweep(&self, path: [f64; 3]) -> crate::Sweep {
|
||||
let shape = self.clone().into();
|
||||
crate::Sweep::from_path(shape, path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenient syntax to create an [`fj::Transform`]
|
||||
///
|
||||
/// [`fj::Transform`]: crate::Transform
|
||||
pub trait Transform {
|
||||
/// Create a rotation
|
||||
///
|
||||
/// Create a rotation that rotates `shape` by `angle` around an axis defined
|
||||
/// by `axis`.
|
||||
fn rotate(&self, axis: [f64; 3], angle: crate::Angle) -> crate::Transform;
|
||||
|
||||
/// Create a translation
|
||||
///
|
||||
/// Create a translation that translates `shape` by `offset`.
|
||||
fn translate(&self, offset: [f64; 3]) -> crate::Transform;
|
||||
}
|
||||
|
||||
impl<T> Transform for T
|
||||
where
|
||||
T: Clone + Into<crate::Shape>,
|
||||
{
|
||||
fn rotate(&self, axis: [f64; 3], angle: crate::Angle) -> crate::Transform {
|
||||
let shape = self.clone().into();
|
||||
crate::Transform {
|
||||
shape,
|
||||
axis,
|
||||
angle,
|
||||
offset: [0.; 3],
|
||||
}
|
||||
}
|
||||
|
||||
fn translate(&self, offset: [f64; 3]) -> crate::Transform {
|
||||
let shape = self.clone().into();
|
||||
crate::Transform {
|
||||
shape,
|
||||
axis: [1., 0., 0.],
|
||||
angle: crate::Angle::from_rad(0.),
|
||||
offset,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
use crate::{Angle, Shape};
|
||||
|
||||
/// A transformed 3-dimensional shape
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Convenient syntax for this operation is available through [`crate::syntax`].
|
||||
///
|
||||
/// ``` rust
|
||||
/// # let shape = fj::Sketch::from_points(vec![[0., 0.], [1., 0.], [0., 1.]]).unwrap();
|
||||
/// use fj::syntax::*;
|
||||
///
|
||||
/// // `shape` can be anything that converts to `fj::Shape`
|
||||
/// let rotated = shape.rotate([0., 0., 1.], fj::Angle::from_rev(0.5));
|
||||
/// let translated = shape.translate([1., 2., 3.]);
|
||||
/// ```
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// Transformations are currently limited to a rotation, followed by a
|
||||
/// translation.
|
||||
///
|
||||
/// See issue:
|
||||
/// <https://github.com/hannobraun/Fornjot/issues/101>
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Transform {
|
||||
/// The shape being transformed
|
||||
pub shape: Shape,
|
||||
|
||||
/// The axis of the rotation
|
||||
pub axis: [f64; 3],
|
||||
|
||||
/// The angle of the rotation
|
||||
pub angle: Angle,
|
||||
|
||||
/// The offset of the translation
|
||||
pub offset: [f64; 3],
|
||||
}
|
||||
|
||||
impl From<Transform> for Shape {
|
||||
fn from(shape: Transform) -> Self {
|
||||
Self::Transform(Box::new(shape))
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//! API for checking compatibility between the Fornjot app and a model
|
||||
|
||||
use std::{fmt, slice};
|
||||
|
||||
/// The Fornjot package version
|
||||
///
|
||||
/// Can be used to check for compatibility between a model and the Fornjot app
|
||||
/// that runs it.
|
||||
///
|
||||
/// This is just the version specified in the Cargo package, which will stay
|
||||
/// constant between releases, even though changes are made throughout. A match
|
||||
/// of this version does not conclusively determine that the app and a model are
|
||||
/// compatible.
|
||||
#[no_mangle]
|
||||
pub static VERSION_PKG: Version =
|
||||
Version::from_static_str(env!("FJ_VERSION_PKG"));
|
||||
|
||||
/// The full Fornjot version
|
||||
///
|
||||
/// Can be used to check for compatibility between a model and the Fornjot app
|
||||
/// that runs it.
|
||||
#[no_mangle]
|
||||
pub static VERSION_FULL: Version =
|
||||
Version::from_static_str(env!("FJ_VERSION_FULL"));
|
||||
|
||||
/// C-ABI-compatible representation of a version
|
||||
///
|
||||
/// Used by the Fornjot application to check for compatibility between a model
|
||||
/// and the app.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Version {
|
||||
ptr: *const u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
const fn from_static_str(s: &'static str) -> Self {
|
||||
Self {
|
||||
ptr: s.as_ptr(),
|
||||
len: s.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// This is sound. We only ever create `ptr` and `len` from static
|
||||
// strings.
|
||||
let slice = unsafe { slice::from_raw_parts(self.ptr, self.len) };
|
||||
|
||||
write!(f, "{}", String::from_utf8_lossy(slice).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
// The only reason this is not derived automatically, is that `Version` contains
|
||||
// a `*const u8`. `Version` can still safely be `Send`, for the following
|
||||
// reasons:
|
||||
// - The field is private, and no code in this module uses it for any write
|
||||
// access, un-synchronized or not.
|
||||
// - `Version` can only be constructed from strings with a static lifetime, so
|
||||
// it's guaranteed that the pointer is valid over the whole lifetime of the
|
||||
// program.
|
||||
unsafe impl Send for Version {}
|
||||
|
||||
// There is no reason why a `&Version` wouldn't be `Send`, so per definition,
|
||||
// `Version` can be `Sync`.
|
||||
unsafe impl Sync for Version {}
|
2
justfile
2
justfile
|
@ -9,7 +9,7 @@ export RUSTDOCFLAGS := "-D warnings"
|
|||
# For a full build that mirrors the CI build, see `just ci`.
|
||||
test:
|
||||
cargo test --all-features
|
||||
cargo run --package export-validator
|
||||
# cargo run --package export-validator
|
||||
|
||||
# Run a full build that mirrors the CI build
|
||||
#
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
[package]
|
||||
name = "cuboid"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.fj]
|
||||
path = "../../crates/fj"
|
|
@ -1,10 +0,0 @@
|
|||
# Fornjot - Cuboid
|
||||
|
||||
A model of a simple cuboid that demonstrates sweeping a 3D shape from a primitive with multiple connected edges.
|
||||
|
||||
To display this model, run the following from the repository root (model parameters are optional):
|
||||
``` sh
|
||||
cargo run -- cuboid --parameters x=3.0,y=2.0,z=1.0
|
||||
```
|
||||
|
||||
![Screenshot of the cuboid model](cuboid.png)
|
Binary file not shown.
Before Width: | Height: | Size: 62 KiB |
|
@ -1,18 +0,0 @@
|
|||
#[fj::model]
|
||||
pub fn model(
|
||||
#[param(default = 3.0)] x: f64,
|
||||
#[param(default = 2.0)] y: f64,
|
||||
#[param(default = 1.0)] z: f64,
|
||||
) -> fj::Shape {
|
||||
#[rustfmt::skip]
|
||||
let rectangle = fj::Sketch::from_points(vec![
|
||||
[-x / 2., -y / 2.],
|
||||
[ x / 2., -y / 2.],
|
||||
[ x / 2., y / 2.],
|
||||
[-x / 2., y / 2.],
|
||||
]).unwrap().with_color([100,255,0,200]);
|
||||
|
||||
let cuboid = fj::Sweep::from_path(rectangle.into(), [0., 0., z]);
|
||||
|
||||
cuboid.into()
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
[package]
|
||||
name = "spacer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.fj]
|
||||
path = "../../crates/fj"
|
|
@ -1,10 +0,0 @@
|
|||
# Fornjot - Spacer
|
||||
|
||||
A simple spacer model that demonstrates the circle primitive, the difference operation, and sweeping that into a 3-dimensional shape.
|
||||
|
||||
To display this model, run the following from the repository root (model parameters are optional):
|
||||
``` sh
|
||||
cargo run -- spacer --parameters outer=1.0,inner=0.5,height=1.0
|
||||
```
|
||||
|
||||
![Screenshot of the spacer model](spacer.png)
|
Binary file not shown.
Before Width: | Height: | Size: 73 KiB |
|
@ -1,16 +0,0 @@
|
|||
use fj::syntax::*;
|
||||
|
||||
#[fj::model]
|
||||
pub fn model(
|
||||
#[param(default = 1.0, min = inner * 1.01)] outer: f64,
|
||||
#[param(default = 0.5, max = outer * 0.99)] inner: f64,
|
||||
#[param(default = 1.0)] height: f64,
|
||||
) -> fj::Shape {
|
||||
let outer_edge = fj::Sketch::from_circle(fj::Circle::from_radius(outer));
|
||||
let inner_edge = fj::Sketch::from_circle(fj::Circle::from_radius(inner));
|
||||
|
||||
let footprint = outer_edge.difference(&inner_edge);
|
||||
let spacer = footprint.sweep([0., 0., height]);
|
||||
|
||||
spacer.into()
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
[package]
|
||||
name = "star"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.fj]
|
||||
path = "../../crates/fj"
|
|
@ -1,10 +0,0 @@
|
|||
# Fornjot - Star
|
||||
|
||||
A model of a star that demonstrates sweeping a sketch to create a 3D shape, concave sketches, and how to generate sketches from code.
|
||||
|
||||
To display this model, run the following from the repository root (model parameters are optional):
|
||||
``` sh
|
||||
cargo run -- star --parameters num_points=5,r1=1.0,r2=2.0,h=1.0
|
||||
```
|
||||
|
||||
![Screenshot of the star model](star.png)
|
|
@ -1,40 +0,0 @@
|
|||
use std::f64::consts::PI;
|
||||
|
||||
#[fj::model]
|
||||
pub fn model(
|
||||
#[param(default = 5, min = 3)] num_points: u64,
|
||||
#[param(default = 1.0, min = 1.0)] r1: f64,
|
||||
#[param(default = 2.0, min = 2.0)] r2: f64,
|
||||
#[param(default = 1.0)] h: f64,
|
||||
) -> fj::Shape {
|
||||
let num_vertices = num_points * 2;
|
||||
let vertex_iter = (0..num_vertices).map(|i| {
|
||||
let angle =
|
||||
fj::Angle::from_rad(2. * PI / num_vertices as f64 * i as f64);
|
||||
let radius = if i % 2 == 0 { r1 } else { r2 };
|
||||
(angle, radius)
|
||||
});
|
||||
|
||||
// Now that we got that iterator prepared, generating the vertices is just a
|
||||
// bit of trigonometry.
|
||||
let mut outer = Vec::new();
|
||||
let mut inner = Vec::new();
|
||||
for (angle, radius) in vertex_iter {
|
||||
let (sin, cos) = angle.rad().sin_cos();
|
||||
|
||||
let x = cos * radius;
|
||||
let y = sin * radius;
|
||||
|
||||
outer.push([x, y]);
|
||||
inner.push([x / 2., y / 2.]);
|
||||
}
|
||||
|
||||
let outer = fj::Sketch::from_points(outer).unwrap();
|
||||
let inner = fj::Sketch::from_points(inner).unwrap();
|
||||
|
||||
let footprint = fj::Difference2d::from_shapes([outer.into(), inner.into()]);
|
||||
|
||||
let star = fj::Sweep::from_path(footprint.into(), [0., 0., h]);
|
||||
|
||||
star.into()
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 102 KiB |
|
@ -1,11 +0,0 @@
|
|||
[package]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "An example model"
|
||||
homepage = "https://www.fornjot.app/"
|
||||
repository = "https://github.com/hannobraun/fornjot"
|
||||
license = "0BSD"
|
||||
|
||||
[dependencies.fj]
|
||||
path = "../../crates/fj"
|
|
@ -1,10 +0,0 @@
|
|||
# Fornjot - Test Model
|
||||
|
||||
A model that tries to use all of Fornjot's features, to serve as a testing ground.
|
||||
|
||||
To display this model, run the following from the repository root:
|
||||
``` sh
|
||||
cargo run -- test
|
||||
```
|
||||
|
||||
![Screenshot of the test model](test.png)
|
|
@ -1,95 +0,0 @@
|
|||
use std::f64::consts::PI;
|
||||
|
||||
use fj::{syntax::*, Angle};
|
||||
|
||||
#[fj::model]
|
||||
pub fn model() -> fj::Shape {
|
||||
let a = star(4, 1., [0, 255, 0, 200], Some(-30.));
|
||||
let b = star(5, -1., [255, 0, 0, 255], None)
|
||||
.rotate([1., 1., 1.], Angle::from_deg(45.))
|
||||
.translate([3., 3., 1.]);
|
||||
let c = spacer().translate([6., 6., 1.]);
|
||||
|
||||
let group = a.group(&b).group(&c);
|
||||
|
||||
group.into()
|
||||
}
|
||||
|
||||
fn star(
|
||||
num_points: u64,
|
||||
height: f64,
|
||||
color: [u8; 4],
|
||||
arm_angle: Option<f64>,
|
||||
) -> fj::Shape {
|
||||
let r1 = 1.;
|
||||
let r2 = 2.;
|
||||
|
||||
// We need to figure out where to generate vertices, depending on the number
|
||||
// of points the star is supposed to have. Let's generate an iterator that
|
||||
// gives us the angle and radius for each vertex.
|
||||
let num_vertices = num_points * 2;
|
||||
let vertex_iter = (0..num_vertices).map(|i| {
|
||||
let angle = Angle::from_rad(2. * PI / num_vertices as f64 * i as f64);
|
||||
let radius = if i % 2 == 0 { r1 } else { r2 };
|
||||
(angle, radius)
|
||||
});
|
||||
|
||||
// Now that we got that iterator prepared, generating the vertices is just a
|
||||
// bit of trigonometry.
|
||||
let mut outer = Vec::new();
|
||||
let mut inner = Vec::new();
|
||||
for (angle, radius) in vertex_iter {
|
||||
let (sin, cos) = angle.rad().sin_cos();
|
||||
|
||||
let x = cos * radius;
|
||||
let y = sin * radius;
|
||||
|
||||
if let Some(angle) = arm_angle {
|
||||
outer.push(fj::SketchSegment {
|
||||
endpoint: [x, y],
|
||||
route: fj::SketchSegmentRoute::Arc {
|
||||
angle: fj::Angle::from_deg(angle),
|
||||
},
|
||||
});
|
||||
inner.push(fj::SketchSegment {
|
||||
endpoint: [x / 2., y / 2.],
|
||||
route: fj::SketchSegmentRoute::Arc {
|
||||
angle: fj::Angle::from_deg(-angle),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
outer.push(fj::SketchSegment {
|
||||
endpoint: [x, y],
|
||||
route: fj::SketchSegmentRoute::Direct,
|
||||
});
|
||||
inner.push(fj::SketchSegment {
|
||||
endpoint: [x / 2., y / 2.],
|
||||
route: fj::SketchSegmentRoute::Direct,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outer = fj::Sketch::from_segments(outer).unwrap().with_color(color);
|
||||
let inner = fj::Sketch::from_segments(inner).unwrap();
|
||||
|
||||
let footprint = fj::Difference2d::from_shapes([outer.into(), inner.into()]);
|
||||
|
||||
let star = fj::Sweep::from_path(footprint.into(), [0., 0., height]);
|
||||
|
||||
star.into()
|
||||
}
|
||||
|
||||
fn spacer() -> fj::Shape {
|
||||
let outer = 2.;
|
||||
let inner = 1.;
|
||||
let height = 2.;
|
||||
|
||||
let outer_edge = fj::Sketch::from_circle(fj::Circle::from_radius(outer))
|
||||
.with_color([0, 0, 255, 255]);
|
||||
let inner_edge = fj::Sketch::from_circle(fj::Circle::from_radius(inner));
|
||||
|
||||
let footprint = outer_edge.difference(&inner_edge);
|
||||
let spacer = footprint.sweep([0., 0., height]);
|
||||
|
||||
spacer.into()
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 125 KiB |
|
@ -1,10 +0,0 @@
|
|||
(import
|
||||
(
|
||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).defaultNix
|
113
nix/flake.lock
113
nix/flake.lock
|
@ -1,113 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": [
|
||||
"rust-overlay"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1678152261,
|
||||
"narHash": "sha256-cPRDxwygVMleiSEGELrvAiq9vYAN4c3KK/K4UEO13vU=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "5291dd0aa7a52d607fc952763ef60714e4c881d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1678901627,
|
||||
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1678949004,
|
||||
"narHash": "sha256-8c0hnhTjmLRxtYp7wLr2nN4sRa61quL9PBSaGuYaBL0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "67712ddbdcfa2790dc0d58c56b83eb980ab43213",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1678933473,
|
||||
"narHash": "sha256-UY19R278O9bwneLWC7ady8VMoQ+UlAWy8SkUsfDZvQs=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "5c1af9b9d618e02a87cdd30a3022aec0b78cd9aa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
{
|
||||
description = "Fornjot is an early-stage project to create a next-generation, code-first CAD application";
|
||||
inputs = {
|
||||
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay = { url = "github:oxalica/rust-overlay"; inputs = { nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; }; };
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
flake-compat.follows = "flake-compat";
|
||||
flake-utils.follows = "flake-utils";
|
||||
rust-overlay.follows = "rust-overlay";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, crane, flake-utils, rust-overlay, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; overlays = [ (import rust-overlay) ]; };
|
||||
rustToolchain = pkgs.rust-bin.fromRustupToolchain (
|
||||
# extend toolchain with rust-analyzer for better IDE support
|
||||
let toolchainToml = (builtins.fromTOML (builtins.readFile ../rust-toolchain.toml)).toolchain; in
|
||||
{
|
||||
channel = toolchainToml.channel;
|
||||
components = toolchainToml.components ++ [ "rust-analyzer" "rust-src" ];
|
||||
}
|
||||
);
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
version = (builtins.fromTOML (builtins.readFile ../Cargo.toml)).workspace.package.version;
|
||||
# Only keeps assets in crates/ (currently shaders and fonts)
|
||||
assetsFilter = path: _type: (builtins.match ".*(:?wgsl|ttf|png|obj|fj.toml|mtl)$" path) != null;
|
||||
filter = path: type: (assetsFilter path type) || (craneLib.filterCargoSources path type);
|
||||
buildInputs = with pkgs; [
|
||||
pkg-config
|
||||
fontconfig
|
||||
cmake
|
||||
wayland
|
||||
libGL
|
||||
xorg.libX11
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
xorg.libXrandr
|
||||
];
|
||||
|
||||
fornjot = craneLib.buildPackage {
|
||||
pname = "fj-app";
|
||||
src = nixpkgs.lib.cleanSourceWith { src = ../.; inherit filter; };
|
||||
inherit buildInputs version;
|
||||
};
|
||||
|
||||
wrappedFornjot = pkgs.symlinkJoin {
|
||||
name = "fj-app";
|
||||
inherit version;
|
||||
paths = [ fornjot ];
|
||||
|
||||
buildInputs = [ pkgs.makeWrapper ];
|
||||
|
||||
postBuild = ''
|
||||
wrapProgram $out/bin/fj-app \
|
||||
--prefix LD_LIBRARY_PATH : ${nixpkgs.lib.makeLibraryPath [ pkgs.vulkan-loader ]}
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
checks = { inherit fornjot; };
|
||||
|
||||
packages.default = wrappedFornjot;
|
||||
|
||||
apps.default = flake-utils.lib.mkApp { drv = wrappedFornjot; };
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = builtins.attrValues self.checks;
|
||||
|
||||
inherit buildInputs;
|
||||
nativeBuildInputs = [ rustToolchain ];
|
||||
|
||||
LD_LIBRARY_PATH = "${nixpkgs.lib.makeLibraryPath [ pkgs.vulkan-loader pkgs.stdenv.cc.cc.lib pkgs.lib3mf]}";
|
||||
};
|
||||
});
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
(import
|
||||
(
|
||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).shellNix
|
|
@ -13,38 +13,19 @@ fn main() -> anyhow::Result<()> {
|
|||
let targets = [
|
||||
Target {
|
||||
triple: "aarch64-apple-ios",
|
||||
crates: &[
|
||||
"fj",
|
||||
"fj-export",
|
||||
"fj-interop",
|
||||
"fj-kernel",
|
||||
"fj-math",
|
||||
"fj-operations",
|
||||
"fj-proc",
|
||||
],
|
||||
crates: &["fj-export", "fj-interop", "fj-kernel", "fj-math"],
|
||||
},
|
||||
Target {
|
||||
triple: "aarch64-linux-android",
|
||||
crates: &[
|
||||
"fj",
|
||||
"fj-export",
|
||||
"fj-interop",
|
||||
"fj-kernel",
|
||||
"fj-math",
|
||||
"fj-operations",
|
||||
"fj-proc",
|
||||
],
|
||||
crates: &["fj-export", "fj-interop", "fj-kernel", "fj-math"],
|
||||
},
|
||||
Target {
|
||||
triple: "wasm32-unknown-unknown",
|
||||
crates: &[
|
||||
"fj",
|
||||
"fj-export",
|
||||
"fj-interop",
|
||||
"fj-kernel",
|
||||
"fj-math",
|
||||
"fj-operations",
|
||||
"fj-proc",
|
||||
"fj-viewer",
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue