mirror of https://github.com/hannobraun/Fornjot
Remove deprecated crates
Those have been moved to the fornjot-extra repository: https://github.com/hannobraun/fornjot-extra
This commit is contained in:
parent
def53da7bf
commit
17efc2b2b2
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
|
@ -1,22 +1,12 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/fj",
|
|
||||||
"crates/fj-app",
|
|
||||||
"crates/fj-export",
|
"crates/fj-export",
|
||||||
"crates/fj-host",
|
|
||||||
"crates/fj-interop",
|
"crates/fj-interop",
|
||||||
"crates/fj-kernel",
|
"crates/fj-kernel",
|
||||||
"crates/fj-math",
|
"crates/fj-math",
|
||||||
"crates/fj-operations",
|
|
||||||
"crates/fj-proc",
|
|
||||||
"crates/fj-viewer",
|
"crates/fj-viewer",
|
||||||
"crates/fj-window",
|
# "crates/fj-window",
|
||||||
|
|
||||||
"models/cuboid",
|
|
||||||
"models/spacer",
|
|
||||||
"models/star",
|
|
||||||
"models/test",
|
|
||||||
|
|
||||||
"tools/autolib",
|
"tools/autolib",
|
||||||
"tools/automator",
|
"tools/automator",
|
||||||
|
@ -25,17 +15,12 @@ members = [
|
||||||
"tools/release-operator",
|
"tools/release-operator",
|
||||||
]
|
]
|
||||||
default-members = [
|
default-members = [
|
||||||
"crates/fj",
|
|
||||||
"crates/fj-app",
|
|
||||||
"crates/fj-export",
|
"crates/fj-export",
|
||||||
"crates/fj-host",
|
|
||||||
"crates/fj-interop",
|
"crates/fj-interop",
|
||||||
"crates/fj-kernel",
|
"crates/fj-kernel",
|
||||||
"crates/fj-math",
|
"crates/fj-math",
|
||||||
"crates/fj-operations",
|
|
||||||
"crates/fj-proc",
|
|
||||||
"crates/fj-viewer",
|
"crates/fj-viewer",
|
||||||
"crates/fj-window",
|
# "crates/fj-window",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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`.
|
# For a full build that mirrors the CI build, see `just ci`.
|
||||||
test:
|
test:
|
||||||
cargo test --all-features
|
cargo test --all-features
|
||||||
cargo run --package export-validator
|
# cargo run --package export-validator
|
||||||
|
|
||||||
# Run a full build that mirrors the CI build
|
# 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 |
|
@ -13,38 +13,19 @@ fn main() -> anyhow::Result<()> {
|
||||||
let targets = [
|
let targets = [
|
||||||
Target {
|
Target {
|
||||||
triple: "aarch64-apple-ios",
|
triple: "aarch64-apple-ios",
|
||||||
crates: &[
|
crates: &["fj-export", "fj-interop", "fj-kernel", "fj-math"],
|
||||||
"fj",
|
|
||||||
"fj-export",
|
|
||||||
"fj-interop",
|
|
||||||
"fj-kernel",
|
|
||||||
"fj-math",
|
|
||||||
"fj-operations",
|
|
||||||
"fj-proc",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
Target {
|
Target {
|
||||||
triple: "aarch64-linux-android",
|
triple: "aarch64-linux-android",
|
||||||
crates: &[
|
crates: &["fj-export", "fj-interop", "fj-kernel", "fj-math"],
|
||||||
"fj",
|
|
||||||
"fj-export",
|
|
||||||
"fj-interop",
|
|
||||||
"fj-kernel",
|
|
||||||
"fj-math",
|
|
||||||
"fj-operations",
|
|
||||||
"fj-proc",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
Target {
|
Target {
|
||||||
triple: "wasm32-unknown-unknown",
|
triple: "wasm32-unknown-unknown",
|
||||||
crates: &[
|
crates: &[
|
||||||
"fj",
|
|
||||||
"fj-export",
|
"fj-export",
|
||||||
"fj-interop",
|
"fj-interop",
|
||||||
"fj-kernel",
|
"fj-kernel",
|
||||||
"fj-math",
|
"fj-math",
|
||||||
"fj-operations",
|
|
||||||
"fj-proc",
|
|
||||||
"fj-viewer",
|
"fj-viewer",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue