Remove deprecated crates

Those have been moved to the fornjot-extra repository:
https://github.com/hannobraun/fornjot-extra
This commit is contained in:
Hanno Braun 2023-05-15 10:44:16 +02:00
parent def53da7bf
commit 17efc2b2b2
66 changed files with 12 additions and 5851 deletions

891
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,12 @@
[workspace]
resolver = "2"
members = [
"crates/fj",
"crates/fj-app",
"crates/fj-export",
"crates/fj-host",
"crates/fj-interop",
"crates/fj-kernel",
"crates/fj-math",
"crates/fj-operations",
"crates/fj-proc",
"crates/fj-viewer",
"crates/fj-window",
"models/cuboid",
"models/spacer",
"models/star",
"models/test",
# "crates/fj-window",
"tools/autolib",
"tools/automator",
@ -25,17 +15,12 @@ members = [
"tools/release-operator",
]
default-members = [
"crates/fj",
"crates/fj-app",
"crates/fj-export",
"crates/fj-host",
"crates/fj-interop",
"crates/fj-kernel",
"crates/fj-math",
"crates/fj-operations",
"crates/fj-proc",
"crates/fj-viewer",
"crates/fj-window",
# "crates/fj-window",
]

View File

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

View File

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

View File

@ -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")
}
}

View File

@ -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."
))
}
}
}

View File

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

View File

@ -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"

View File

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

View File

@ -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),
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
})
}
}

View File

@ -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"

View File

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

View File

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

View File

@ -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(),
}
}
}

View File

@ -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),
}

View File

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

View File

@ -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()),
))
}
}

View File

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

View File

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

View File

@ -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());
}
});
}
}

View File

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

View File

@ -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(
&param_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);
}
}

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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());
}
}
}

View File

@ -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(),
}
}
}

View File

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

View File

@ -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 {}

View File

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

View File

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

View File

@ -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>),
}

View File

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

View File

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

View File

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

View File

@ -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>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {}

View File

@ -9,7 +9,7 @@ export RUSTDOCFLAGS := "-D warnings"
# For a full build that mirrors the CI build, see `just ci`.
test:
cargo test --all-features
cargo run --package export-validator
# cargo run --package export-validator
# Run a full build that mirrors the CI build
#

View File

@ -1,7 +0,0 @@
[package]
name = "cuboid"
version = "0.1.0"
edition = "2021"
[dependencies.fj]
path = "../../crates/fj"

View File

@ -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

View File

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

View File

@ -1,7 +0,0 @@
[package]
name = "spacer"
version = "0.1.0"
edition = "2021"
[dependencies.fj]
path = "../../crates/fj"

View File

@ -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

View File

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

View File

@ -1,7 +0,0 @@
[package]
name = "star"
version = "0.1.0"
edition = "2021"
[dependencies.fj]
path = "../../crates/fj"

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -13,38 +13,19 @@ fn main() -> anyhow::Result<()> {
let targets = [
Target {
triple: "aarch64-apple-ios",
crates: &[
"fj",
"fj-export",
"fj-interop",
"fj-kernel",
"fj-math",
"fj-operations",
"fj-proc",
],
crates: &["fj-export", "fj-interop", "fj-kernel", "fj-math"],
},
Target {
triple: "aarch64-linux-android",
crates: &[
"fj",
"fj-export",
"fj-interop",
"fj-kernel",
"fj-math",
"fj-operations",
"fj-proc",
],
crates: &["fj-export", "fj-interop", "fj-kernel", "fj-math"],
},
Target {
triple: "wasm32-unknown-unknown",
crates: &[
"fj",
"fj-export",
"fj-interop",
"fj-kernel",
"fj-math",
"fj-operations",
"fj-proc",
"fj-viewer",
],
},