Continuity: one table of orders G0-G4, and one check of what each construction builds

The kernel's Continuity moves out of bridge.rs into continuity_order.rs and gains G4. One
table holds each order's command word (g0-g4), its code (G0-G4, for tips and messages) and
the message ids of its label and hover text. Construction says which orders the patch,
bridge, network, match and sketch blend curve build today; Continuity::built_by is the one
refusal every command and kernel entry point uses. A design may now save g4 (format note).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-27 19:51:28 -04:00
committed by Irakli Kourtis
parent f042d20736
commit cb8712aa09
13 changed files with 309 additions and 105 deletions
+4
View File
@@ -20,6 +20,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if !edges.is_empty() {
let i = body_at(st, body, &crate::plain(edges))?;
let b = st.bodies.get(i).ok_or_else(|| DocError::Invalid("body".into()))?;
// A design may carry an order the fill does not build yet: refused, not built lower.
for g in std::iter::once(continuity.all).chain(continuity.edges.iter().map(|e| e.g)) {
g.built_by(kernel::Construction::Patch)?;
}
let spec = kernel::PatchSpec {
continuity: continuity.all,
edges: continuity.edges.iter().map(|e| (e.edge.0, e.g)).collect(),
@@ -0,0 +1,4 @@
//! A continuity order may be saved as `g4` (the curvature's change also changes smoothly across
//! the seam) wherever a design saves one (`g0` … `g3` before): a patch's, blend's, boundary's,
//! loft's or match surface's continuity. An older design has none and means what it did. The last
//! release refuses a design with this change (the newer format number) instead of misreading it.
+2 -8
View File
@@ -440,14 +440,8 @@ fn end_of(sk: &Sketch, pi: usize) -> Option<(usize, Vec2)> {
fn blend_order(p: &Value, cmd: &str) -> Result<usize> {
match p.get("continuity") {
None | Some(Value::Null) => Ok(1),
Some(Value::String(v)) => match v.trim().to_ascii_lowercase().as_str() {
"" | "g1" => Ok(1),
"g2" => Ok(2),
"g3" => Ok(3),
"g4" => Ok(4),
_ => Err(bad(cmd, "`continuity` must be g1, g2, g3 or g4")),
},
Some(_) => Err(bad(cmd, "`continuity` must be g1, g2, g3 or g4")),
Some(Value::String(v)) if v.trim().is_empty() => Ok(1),
given => Ok(crate::params::continuity(cmd, given, "`continuity`", solidcraft_kernel::Construction::BlendCurve)?.order()),
}
}
+9 -13
View File
@@ -8,7 +8,7 @@ use solidcraft_doc::{
};
use solidcraft_doc::{BodyName, SketchId};
use solidcraft_doc::{Dir, FacePick, Position};
use solidcraft_kernel::Continuity;
use solidcraft_kernel::{Construction, Continuity};
use super::CommandSpec;
use super::features::{add_feature, axis_param, check_expr, extent_param, feature_sketch, operation, plane_param, profiles, sketch_id};
@@ -134,12 +134,9 @@ fn patch(s: &mut Session, p: &Value) -> Result<Value> {
Ok(v)
}
/// A continuity word for a patch: g0, g1 or g2 (a patch does not hold g3).
/// A continuity word for a patch, within what a patch builds ([`Construction::Patch`]).
fn patch_order(v: Option<&Value>, cmd: &str) -> Result<Continuity> {
match v.and_then(Value::as_str).and_then(Continuity::parse) {
Some(g) if g != Continuity::G3 => Ok(g),
_ => Err(bad(cmd, "a patch's continuity is g0, g1 or g2")),
}
crate::params::continuity(cmd, v, "a patch's continuity", Construction::Patch)
}
/// The patch's `continuity`: one word for every edge, a list of `{edge, g}` for some edges (the
@@ -172,7 +169,10 @@ fn patch_continuity(p: &Value, cmd: &str) -> Result<PatchContinuity> {
};
Ok(PatchContinuity { all, edges })
}
Some(_) => Err(bad(cmd, "`continuity` must be g0, g1 or g2, or a list of {edge, g}")),
Some(_) => {
let (lowest, highest) = Construction::Patch.orders();
Err(bad(cmd, format!("`continuity` must be {}, or a list of {{edge, g}}", Continuity::words(lowest, highest))))
}
}
}
@@ -219,9 +219,7 @@ fn blend(s: &mut Session, p: &Value) -> Result<Value> {
None => [Continuity::G1, Continuity::G1],
Some(v) => {
let list = v.as_array().filter(|a| a.len() == 2).ok_or_else(|| bad(cmd, "`continuity` must be two words, one per end"))?;
let word = |x: Option<&Value>| -> Result<Continuity> {
x.and_then(Value::as_str).and_then(Continuity::parse).ok_or_else(|| bad(cmd, "`continuity` entries must be g0, g1, g2 or g3"))
};
let word = |x: Option<&Value>| crate::params::continuity(cmd, x, "a `continuity` entry", Construction::Bridge);
[word(list.first())?, word(list.get(1))?]
}
};
@@ -413,9 +411,7 @@ fn surface_loft(s: &mut Session, p: &Value) -> Result<Value> {
None => [Continuity::G0, Continuity::G0],
Some(v) => {
let list = v.as_array().filter(|a| a.len() == 2).ok_or_else(|| bad(cmd, "`continuity` must be two words, one per end"))?;
let word = |x: Option<&Value>| -> Result<Continuity> {
x.and_then(Value::as_str).and_then(Continuity::parse).ok_or_else(|| bad(cmd, "`continuity` entries must be g0, g1, g2 or g3"))
};
let word = |x: Option<&Value>| crate::params::continuity(cmd, x, "a `continuity` entry", Construction::Bridge);
[word(list.first())?, word(list.get(1))?]
}
};
+4 -10
View File
@@ -6,7 +6,7 @@
use serde_json::Value;
use solidcraft_doc::{BodyName, FeatureKind, NetworkCurve, Operation, Position, SketchId};
use solidcraft_kernel::Continuity;
use solidcraft_kernel::{Construction, Continuity};
use super::CommandSpec;
use super::features::{add_feature, feature_sketch, sketch_id};
@@ -88,15 +88,12 @@ fn loop_patch(s: &mut Session, p: &Value) -> Result<Value> {
drop(state);
let orders = match p.get("continuity") {
None | Some(Value::Null) => vec![Continuity::G1; curves.len()],
Some(Value::String(w)) => {
let one = Continuity::parse(w).ok_or_else(|| bad(cmd, "`continuity` must be g0, g1, g2 or g3"))?;
vec![one; curves.len()]
}
Some(one @ Value::String(_)) => vec![crate::params::continuity(cmd, Some(one), "`continuity`", Construction::Bridge)?; curves.len()],
Some(v) => {
let list = v.as_array().filter(|a| a.len() == curves.len()).ok_or_else(|| bad(cmd, "`continuity` must be one word, or one per curve"))?;
let mut out = Vec::with_capacity(list.len());
for x in list {
out.push(x.as_str().and_then(Continuity::parse).ok_or_else(|| bad(cmd, "`continuity` entries must be g0, g1, g2 or g3"))?);
out.push(crate::params::continuity(cmd, Some(x), "a `continuity` entry", Construction::Bridge)?);
}
out
}
@@ -205,10 +202,7 @@ fn network_curves(s: &Session, p: &Value, key: &str, default_sketch: &mut Option
}
}
if let Some(g) = o.get("continuity").filter(|v| !v.is_null()) {
let g = g.as_str().and_then(Continuity::parse).ok_or_else(|| bad(cmd, format!("{name}.continuity must be g0, g1 or g2")))?;
if g == Continuity::G3 {
return Err(bad(cmd, format!("not supported yet: G3 on {name} (a curve network holds up to g2)")));
}
let g = crate::params::continuity(cmd, Some(g), &format!("{name}.continuity"), Construction::Network)?;
if g != Continuity::G0 && curve.body.is_none() {
return Err(bad(cmd, format!("{name}: g1 and g2 apply to a curve on a body's edges (a surface's open edges)")));
}
+2 -2
View File
@@ -5,7 +5,7 @@
use serde_json::Value;
use solidcraft_doc::{BodyName, FacePick, FeatureKind, Position};
use solidcraft_kernel::Continuity;
use solidcraft_kernel::{Construction, Continuity};
use super::CommandSpec;
use super::features::add_feature;
@@ -43,7 +43,7 @@ fn match_surface(s: &mut Session, p: &Value) -> Result<Value> {
}
let order = match p.get("order") {
None => Continuity::G2,
Some(v) => v.as_str().and_then(Continuity::parse).filter(|o| *o != Continuity::G0).ok_or_else(|| bad(cmd, "`order` must be g1, g2 or g3"))?,
Some(v) => crate::params::continuity(cmd, Some(v), "`order`", Construction::Match)?,
};
add_feature(
s,
+16 -1
View File
@@ -4,7 +4,7 @@ use serde_json::Value;
use solidcraft_doc::Expr;
use solidcraft_geom::{Vec2, Vec3};
use crate::{EngineError, Result};
use crate::{EngineError, Result, kernel};
pub fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
EngineError::BadParams { cmd: cmd.to_string(), msg: msg.into() }
@@ -94,3 +94,18 @@ pub fn string_list(p: &Value, k: &str) -> Vec<String> {
_ => Vec::new(),
}
}
/// A continuity word (`g0` … `g4`, [`kernel::Continuity::parse`]) for `what` (the key or entry
/// it was given as), refused when it is not a word and when `by` does not build that order: the
/// kernel's [`kernel::Construction`] table says which orders it does.
pub fn continuity(cmd: &str, v: Option<&Value>, what: &str, by: kernel::Construction) -> Result<kernel::Continuity> {
let (lowest, highest) = by.orders();
let g = v
.and_then(Value::as_str)
.and_then(kernel::Continuity::parse)
.ok_or_else(|| bad(cmd, format!("{what} must be {}", kernel::Continuity::words(lowest, highest))))?;
g.built_by(by).map_err(|e| match e {
kernel::KernelError::Invalid(why) => bad(cmd, format!("{what}: {why}")),
other => other.into(),
})
}
+16 -12
View File
@@ -18,7 +18,7 @@ use solidcraft_geom::network::{self, CrossFace, FaceFrame, NetCurve, NetworkCurv
use solidcraft_geom::{Plane, Seg2, Vec3};
use crate::body::Body;
use crate::bridge::Continuity;
use crate::continuity_order::{Construction, Continuity};
use crate::surfaces::{Chain, Parts, spline_curve, spline_surface, surface_body, uv_line};
use crate::{KernelError, Result};
@@ -33,13 +33,18 @@ pub const MAX_EDGE_PICKS: usize = 10_000;
/// Most sketch curves one curve of a boundary surface chains.
pub const MAX_CHAIN_CURVES: usize = 1000;
/// The network's continuity for a curve's: G3 is refused (a network holds up to G2).
/// The network's continuity for a curve's, refused past what a network builds
/// ([`Construction::Network`]).
fn net_order(c: &BoundaryCurve) -> Result<network::Continuity> {
match c.continuity {
let named = |e: KernelError| match e {
KernelError::Invalid(why) => KernelError::Invalid(format!("{}: {why}", c.label)),
other => other,
};
match c.continuity.built_by(Construction::Network).map_err(named)? {
Continuity::G0 => Ok(network::Continuity::G0),
Continuity::G1 => Ok(network::Continuity::G1),
Continuity::G2 => Ok(network::Continuity::G2),
Continuity::G3 => Err(KernelError::Invalid(format!("not supported yet: G3 continuity on {} (a curve network holds up to G2)", c.label))),
higher => Err(KernelError::Failed(format!("a curve network has no {} condition yet", higher.code()))),
}
}
@@ -732,17 +737,16 @@ mod tests {
assert!((k1 + 1.0 / r).abs() > 0.1 / r, "G1 alone does not carry the curvature: {k1}");
}
/// G3 is more than a network holds: refused, never a panic.
/// G3 and G4 are more than a network holds: refused, naming the curve, never a panic.
#[test]
fn g3_is_not_supported() {
let base = sheet();
let edge = BoundaryCurve {
label: "edge".into(),
source: CurveSource::Edges { body: &base, at: vec![Vec3::new(5.0, 0.0, 0.0)] },
continuity: Continuity::G3,
};
let e = boundary_surface(&[edge, line3("far", Vec3::new(0.0, 10.0, 5.0), Vec3::new(10.0, 10.0, 5.0))], &[]).unwrap_err();
assert!(e.to_string().contains("not supported yet"), "{e}");
for g in [Continuity::G3, Continuity::G4] {
let edge =
BoundaryCurve { label: "edge".into(), source: CurveSource::Edges { body: &base, at: vec![Vec3::new(5.0, 0.0, 0.0)] }, continuity: g };
let e = boundary_surface(&[edge, line3("far", Vec3::new(0.0, 10.0, 5.0), Vec3::new(10.0, 10.0, 5.0))], &[]).unwrap_err();
assert!(e.to_string().contains("not supported yet") && e.to_string().contains("edge") && e.to_string().contains(g.as_str()), "{e}");
}
}
/// Refusals: G1 on a sketch curve, on an edge between two faces, curves that miss.
+6 -49
View File
@@ -1,5 +1,5 @@
//! Continuity-driven bridging surfaces: one B-spline face that leaves one surface and arrives at
//! another with a requested continuity (G0 up to G3). This is the core the Blend, Boundary and
//! another with a requested continuity (G0 up to G3, [`Construction::Bridge`]). This is the core the Blend, Boundary and
//! Match surfaces are built on.
//!
//! A bridge is a tensor product. Across it (v) every station's cross-section is a Bézier whose
@@ -22,57 +22,11 @@ use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval};
use solidcraft_geom::bspline::{CurveN, HPoint, NurbsSurface};
use crate::body::Body;
use crate::continuity_order::Construction;
pub use crate::continuity_order::Continuity;
use crate::surfaces::Chain;
use crate::{KernelError, Result};
/// How much of a support surface a bridge repeats along its boundary.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Continuity {
/// The boundary curve alone: the surfaces meet, nothing else.
G0,
/// Position and tangent plane.
#[default]
G1,
/// Position, tangent plane and curvature vector.
G2,
/// Position, tangent plane, curvature vector and its change along the leave direction.
G3,
}
impl Continuity {
/// Conditions a side carries: 1 (G0) up to 4 (G3).
pub fn conditions(self) -> usize {
match self {
Continuity::G0 => 1,
Continuity::G1 => 2,
Continuity::G2 => 3,
Continuity::G3 => 4,
}
}
/// The word a command parameter uses.
pub fn as_str(self) -> &'static str {
match self {
Continuity::G0 => "g0",
Continuity::G1 => "g1",
Continuity::G2 => "g2",
Continuity::G3 => "g3",
}
}
/// Parse `g0`…`g3` (upper case accepted), `None` for anything else.
pub fn parse(s: &str) -> Option<Continuity> {
match s.to_ascii_lowercase().as_str() {
"g0" => Some(Continuity::G0),
"g1" => Some(Continuity::G1),
"g2" => Some(Continuity::G2),
"g3" => Some(Continuity::G3),
_ => None,
}
}
}
/// One station of a boundary: where the bridge starts, the direction it leaves in, and what the
/// support surface does there along that direction.
#[derive(Clone, Copy, Debug)]
@@ -684,6 +638,9 @@ pub fn bridge(a: &[Frame], b: &[Frame], orders: [Continuity; 2], crown: [f64; 2]
return Err(KernelError::Invalid("a bridge's crown must be between -0.9 and 3".into()));
}
}
for g in orders {
g.built_by(Construction::Bridge)?;
}
let stations = a.len();
let n = orders[0].conditions() + orders[1].conditions() - 1;
if n == 0 || n > solidcraft_geom::bspline::MAX_SPLINE_DEGREE {
+3 -9
View File
@@ -209,6 +209,9 @@ fn the_bridge_refuses_what_it_cannot_hold() {
assert!(bridge(&bad, &b, [Continuity::G1, Continuity::G1], [0.0, 0.0]).is_err());
// Fewer than two stations.
assert!(bridge(&a[..1], &b[..1], [Continuity::G1, Continuity::G1], [0.0, 0.0]).is_err());
// G4 is past the bridge's Hermite rows: refused by the one check, not built as G3.
let e = bridge(&a, &b, [Continuity::G4, Continuity::G1], [0.0, 0.0]).unwrap_err().to_string();
assert!(e.contains("not supported yet") && e.contains("g4"), "{e}");
// Every order from G0 to G3 builds.
for (x, y) in [(Continuity::G0, Continuity::G0), (Continuity::G1, Continuity::G0), (Continuity::G3, Continuity::G1)] {
let body = bridge(&a, &b, [x, y], [0.0, 0.0]).unwrap();
@@ -219,15 +222,6 @@ fn the_bridge_refuses_what_it_cannot_hold() {
}
}
#[test]
fn continuity_parses_its_words() {
assert_eq!(Continuity::parse("g0"), Some(Continuity::G0));
assert_eq!(Continuity::parse("G3"), Some(Continuity::G3));
assert_eq!(Continuity::parse("g9"), None);
assert_eq!(Continuity::parse(""), None);
assert_eq!([Continuity::G0, Continuity::G1, Continuity::G2, Continuity::G3].map(Continuity::conditions), [1, 2, 3, 4]);
}
/// A radius-10 tube along z from `z0` to `z0 + 30`, turned `turn` radians about its axis (which
/// moves where its rim's seam sits), with the end face at `open_at` taken off: a surface body
/// whose rim there is a closed chain.
+237
View File
@@ -0,0 +1,237 @@
//! The orders of geometric continuity, G0 to G4, in one table: the word a command parameter and
//! a design file use (`g0` … `g4`), the code shown in hover texts, help and messages (`G0` …
//! `G4`), and the message ids of the plain word a control shows (Position, Tangent, Curvature,
//! Curvature change, Smooth curvature change) and of its hover text. Labels never show the code.
//!
//! Each order adds one condition across a seam: G0 the position, G1 the tangent plane, G2 the
//! curvature, G3 how the curvature changes across the seam (its first derivative by arc length),
//! G4 how that change itself changes (the second derivative).
//!
//! [`Construction`] says which orders each construction builds today, and
//! [`Continuity::built_by`] is the one check that refuses the rest.
use crate::{KernelError, Result};
/// How smoothly a surface or curve continues across a seam: the number of derivatives it shares.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Continuity {
/// The position alone: the surfaces meet, with a crease where they do.
G0,
/// Position and tangent plane.
#[default]
G1,
/// Position, tangent plane and curvature.
G2,
/// Position, tangent plane, curvature and the change of curvature across the seam.
G3,
/// All of G3, and how that change of curvature itself changes.
G4,
}
/// One row of the table: what an order is called where.
struct Words {
/// The command parameter and design-file word.
word: &'static str,
/// The code, for hover texts, help and messages only.
code: &'static str,
/// The message id of the plain word a control shows.
label: &'static str,
/// The message id of the hover text, which names the code.
tip: &'static str,
}
/// The table, indexed by [`Continuity::order`].
const WORDS: [Words; 5] = [
Words { word: "g0", code: "G0", label: "ui.continuity_position", tip: "ui.continuity_position_tip" },
Words { word: "g1", code: "G1", label: "ui.continuity_tangent", tip: "ui.continuity_tangent_tip" },
Words { word: "g2", code: "G2", label: "ui.continuity_curvature", tip: "ui.continuity_curvature_tip" },
Words { word: "g3", code: "G3", label: "ui.continuity_curvature_change", tip: "ui.continuity_curvature_change_tip" },
Words { word: "g4", code: "G4", label: "ui.continuity_smooth_curvature_change", tip: "ui.continuity_smooth_curvature_change_tip" },
];
impl Continuity {
/// Every order, from G0 up.
pub const ALL: [Continuity; 5] = [Continuity::G0, Continuity::G1, Continuity::G2, Continuity::G3, Continuity::G4];
/// The number of derivatives across the seam it fixes: 0 (G0) up to 4 (G4).
pub fn order(self) -> usize {
self as usize
}
/// The order with `order` derivatives, `None` past G4.
pub fn from_order(order: usize) -> Option<Continuity> {
Continuity::ALL.get(order).copied()
}
/// Conditions a side carries: the position and one per derivative, 1 (G0) up to 5 (G4).
pub fn conditions(self) -> usize {
self.order() + 1
}
/// The word a command parameter and a design use: `g0` … `g4`.
pub fn as_str(self) -> &'static str {
self.row().word
}
/// The code `G0` … `G4`: for hover texts, help and messages, never a label.
pub fn code(self) -> &'static str {
self.row().code
}
/// The message id of the plain word a control shows for this order.
pub fn label_id(self) -> &'static str {
self.row().label
}
/// The message id of this order's hover text (which names its code).
pub fn tip_id(self) -> &'static str {
self.row().tip
}
/// Parse a word (`g0` … `g4`, either case, surrounding blanks ignored); `None` for anything else.
pub fn parse(s: &str) -> Option<Continuity> {
let s = s.trim();
Continuity::ALL.into_iter().find(|g| g.as_str().eq_ignore_ascii_case(s))
}
/// The words of `from` up to `to` for a message: "g0, g1 or g2".
pub fn words(from: Continuity, to: Continuity) -> String {
let words: Vec<&str> = Continuity::ALL.into_iter().filter(|g| (from..=to).contains(g)).map(Continuity::as_str).collect();
match words.split_last() {
Some((last, [])) => (*last).to_string(),
Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
None => String::new(),
}
}
/// This order if `by` builds it, else a refusal that names the orders it does build. The one
/// check every command and kernel entry point makes ([`Construction`]).
pub fn built_by(self, by: Construction) -> Result<Continuity> {
let (lowest, highest) = by.orders();
if self < lowest {
return Err(KernelError::Invalid(format!("{} takes {}", by.what(), Continuity::words(lowest, highest))));
}
if self > highest {
return Err(KernelError::Invalid(format!(
"not supported yet: {} is built with {}, not {}",
by.what(),
Continuity::words(lowest, highest),
self.as_str()
)));
}
Ok(self)
}
fn row(self) -> &'static Words {
// `WORDS` has a row for every variant (`order` is at most 4).
&WORDS[self.order()]
}
}
/// What builds geometry to a continuity order, and the orders each one builds today.
///
/// HAND-OFF POINT: this table is the only place that says how far a construction goes. When a
/// construction learns a higher order, raise its `highest` in [`Construction::orders`] (for the
/// patch, from the fill's own `supported_orders` once it has one); every command, dialog and
/// kernel entry point reads it from here, so nothing else changes.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Construction {
/// A patch filling a loop of edges (`surface.patch`).
Patch,
/// A bridge of Hermite rows: a blend surface, a boundary patch on a loop of sketch curves, a
/// loft's ends and a match surface's seam.
Bridge,
/// A surface through a curve network (`surface.boundary` with `u` and `v` curves).
Network,
/// A match surface: a bridge that must at least share the tangent plane.
Match,
/// A sketch blend curve (`sketch.blend_curve`): one Bézier through G1 … G4.
BlendCurve,
}
impl Construction {
/// The lowest and the highest order this construction builds.
pub fn orders(self) -> (Continuity, Continuity) {
match self {
Construction::Patch => (Continuity::G0, Continuity::G2),
Construction::Bridge => (Continuity::G0, Continuity::G3),
Construction::Network => (Continuity::G0, Continuity::G2),
Construction::Match => (Continuity::G1, Construction::Bridge.orders().1),
Construction::BlendCurve => (Continuity::G1, Continuity::G4),
}
}
/// Whether this construction builds `g`.
pub fn builds(self, g: Continuity) -> bool {
let (lowest, highest) = self.orders();
(lowest..=highest).contains(&g)
}
/// The orders it builds, from its lowest up: what a dialog offers.
pub fn offered(self) -> Vec<Continuity> {
Continuity::ALL.into_iter().filter(|g| self.builds(*g)).collect()
}
/// What it is called in a message.
fn what(self) -> &'static str {
match self {
Construction::Patch => "a patch",
Construction::Bridge => "a blend, boundary, loft or match surface",
Construction::Network => "a curve network",
Construction::Match => "a match surface",
Construction::BlendCurve => "a blend curve",
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn every_order_round_trips_through_its_word_and_serde() {
for (k, g) in Continuity::ALL.into_iter().enumerate() {
assert_eq!(g.order(), k);
assert_eq!(Continuity::from_order(k), Some(g));
assert_eq!(g.conditions(), k + 1);
assert_eq!(g.as_str(), format!("g{k}"));
assert_eq!(g.code(), format!("G{k}"));
assert_eq!(Continuity::parse(g.as_str()), Some(g));
assert_eq!(Continuity::parse(&format!(" {} ", g.code())), Some(g));
// The design file's word is the command word.
assert_eq!(serde_json::to_value(g).unwrap(), serde_json::json!(g.as_str()));
assert_eq!(serde_json::from_value::<Continuity>(serde_json::json!(g.as_str())).unwrap(), g);
}
assert_eq!(Continuity::from_order(5), None);
for word in ["", "g5", "G-1", "curvature", "g 1"] {
assert_eq!(Continuity::parse(word), None, "{word:?}");
}
}
#[test]
fn labels_are_words_and_tips_are_their_own() {
let ids: Vec<&str> = Continuity::ALL.iter().flat_map(|g| [g.label_id(), g.tip_id()]).collect();
for (i, id) in ids.iter().enumerate() {
assert!(id.starts_with("ui.continuity_"), "{id}");
assert!(!ids[..i].contains(id), "{id} twice");
assert!(!id.contains("_g"), "a message id names its word, not its code: {id}");
}
}
#[test]
fn a_construction_refuses_what_it_does_not_build_and_names_what_it_does() {
assert_eq!(Continuity::words(Continuity::G0, Continuity::G2), "g0, g1 or g2");
assert_eq!(Continuity::words(Continuity::G4, Continuity::G4), "g4");
assert_eq!(Continuity::G2.built_by(Construction::Patch).unwrap(), Continuity::G2);
let e = Continuity::G3.built_by(Construction::Patch).unwrap_err().to_string();
assert!(e.contains("not supported yet") && e.contains("g0, g1 or g2") && e.contains("g3"), "{e}");
let e = Continuity::G0.built_by(Construction::Match).unwrap_err().to_string();
assert!(e.contains("g1, g2 or g3"), "{e}");
assert!(Continuity::G4.built_by(Construction::Bridge).is_err());
assert!(Continuity::G4.built_by(Construction::BlendCurve).is_ok());
assert_eq!(Construction::Patch.offered(), vec![Continuity::G0, Continuity::G1, Continuity::G2]);
assert_eq!(Construction::BlendCurve.offered().first(), Some(&Continuity::G1));
}
}
+3 -1
View File
@@ -28,6 +28,7 @@ mod build;
#[cfg(test)]
mod chamfer_sides_curved_tests;
mod continuity;
mod continuity_order;
#[cfg(test)]
mod coplanar_tests;
mod corner_g2;
@@ -161,7 +162,7 @@ pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fill
pub use body::{Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint};
pub use boundary::{Boundary, boundary_patch};
pub use boundary_network::{BoundaryCurve, CurveSource, MAX_CHAIN_CURVES, MAX_EDGE_PICKS, boundary_join, boundary_surface};
pub use bridge::{Continuity, Frame, blend_between, bridge, chain, chain_closes, frames, frames_across, sample_grid};
pub use bridge::{Frame, blend_between, bridge, chain, chain_closes, frames, frames_across, sample_grid};
pub use build::{
PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, extrude_tapered_between, extrude_whole_sides, loft, loft_regions,
loft_with_rails, path_polyline, planar_face_record, revolve, revolve_whole, sphere, sweep, sweep_with, torus,
@@ -170,6 +171,7 @@ pub use continuity::{
ContinuityReport, ContinuityTolerances, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamStation, SeamVerdict, SeamWorst, pick_edges,
seam_diagnostics,
};
pub use continuity_order::{Construction, Continuity};
pub use corner_g2::{CURVATURE_SHARE, DEFAULT_SETBACK_FACTOR};
pub use delete_face::delete_faces;
pub use distance::{BodyDistance, DistanceShape, body_distance};
+3
View File
@@ -77,6 +77,9 @@ const ROWS_PER_SOLVE: usize = 16;
/// [`MAX_BLEND_STATIONS`], a frame that is not finite or a `leave` with no direction, a crown
/// outside its range, and two neighbouring sections that meet at a station (no room for a blend).
pub fn loft_blend(sections: &[Vec<Frame>], orders: [Continuity; 2], crown: [f64; 2], samples: usize) -> Result<Body> {
for g in orders {
g.built_by(crate::continuity_order::Construction::Bridge)?;
}
let blend = Blend::new(sections, orders, crown, samples)?;
let net = blend.net()?;
crate::surfaces::tensor_sheet(&net)