Class-A surfacing 1/3: the continuity core, a G3 free-form blend and patch crown (#1249)
The continuity core (kernel bridge: Hermite control rows reproducing a support surface's position, tangent, curvature and change of curvature, G0 to G3), surface.blend between two edge chains (#764), and a crown for fitted patches. Existing patches and fillets are unchanged by default (crown 0 is the plain patch bit for bit). SurfaceBlend and Patch.crown are noted under format 22.
This commit is contained in:
1 parent
4e10c31fdd
commit
e8e2ccc8a9
17 files changed
+1687
-56
No files matched your search
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::{Plane, Vec2, Vec3};
|
||||
use solidcraft_kernel::Continuity;
|
||||
use solidcraft_sketch::Sketch;
|
||||
|
||||
use crate::construct::{AxisDef, GeoRef, PointDef};
|
||||
@@ -1254,6 +1255,10 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[walk(pos)]
|
||||
edges: Vec<Vec3>,
|
||||
/// How full a fitted patch's middle is (`crown`, -0.9…3, default 0; a planar loop is an
|
||||
/// exact plane whatever this says).
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
crown: f64,
|
||||
},
|
||||
/// Surface bodies sewn along shared edges into the first one (a solid when they close).
|
||||
Stitch {
|
||||
@@ -1437,6 +1442,25 @@ pub enum FeatureKind {
|
||||
#[serde(default = "yes", skip_serializing_if = "is_yes")]
|
||||
neighbours: bool,
|
||||
},
|
||||
/// A blend surface bridging two picked edge chains of two bodies (`surface.blend`): each end
|
||||
/// repeats the surface it leaves up to its requested continuity, with the fullness `bulge`.
|
||||
SurfaceBlend {
|
||||
from: BlendSide,
|
||||
to: BlendSide,
|
||||
/// How much of each support surface the blend repeats: [from, to].
|
||||
#[serde(default = "blend_orders")]
|
||||
continuity: [Continuity; 2],
|
||||
/// How full each end leaves: [from, to]; 0 is the plain bridge.
|
||||
#[serde(default)]
|
||||
bulge: [f64; 2],
|
||||
/// Points the blend leaves towards, when the faces at the picks do not say: [from, to].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(pos)]
|
||||
align: Option<[Vec3; 2]>,
|
||||
/// Sew the blend onto its two bodies where the boundaries meet.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
stitch: bool,
|
||||
},
|
||||
/// A surface body trimmed by a plane, or by a face of a body (`tool`), keeping the side of
|
||||
/// `keep`.
|
||||
SurfaceTrim {
|
||||
@@ -1614,6 +1638,22 @@ pub struct FaceAt {
|
||||
pub point: Vec3,
|
||||
}
|
||||
|
||||
/// One end of a blend: a body and the points that pick the edges of its boundary chain. With two
|
||||
/// picks on the same edge the chain is that one edge; more pick a chain that runs on from pick to
|
||||
/// pick (the order of the picks does not matter).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)]
|
||||
pub struct BlendSide {
|
||||
#[walk(body)]
|
||||
pub body: String,
|
||||
#[walk(pos)]
|
||||
pub edges: Vec<Vec3>,
|
||||
}
|
||||
|
||||
/// The continuity a blend repeats by default: the tangent plane on both sides.
|
||||
fn blend_orders() -> [Continuity; 2] {
|
||||
[Continuity::G1, Continuity::G1]
|
||||
}
|
||||
|
||||
/// Triangles of one imported mesh body (millimetres).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MeshData {
|
||||
@@ -1686,6 +1726,11 @@ fn is_zero_i8(v: &i8) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
/// A parameter that is left out when it is 0 (an optional shape knob).
|
||||
fn is_zero(v: &f64) -> bool {
|
||||
*v == 0.0
|
||||
}
|
||||
|
||||
/// Hole shapes.
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, solidcraft_derive::Walk)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
@@ -1892,6 +1937,7 @@ impl FeatureKind {
|
||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoftFeature",
|
||||
FeatureKind::SurfaceBlend { .. } => "BlendFeature",
|
||||
FeatureKind::Stitch { .. } => "StitchFeature",
|
||||
FeatureKind::Thicken { .. } => "ThickenFeature",
|
||||
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
|
||||
@@ -1968,6 +2014,7 @@ impl FeatureKind {
|
||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoft",
|
||||
FeatureKind::SurfaceBlend { .. } => "Blend",
|
||||
FeatureKind::Stitch { .. } => "Stitch",
|
||||
FeatureKind::Thicken { .. } => "Thicken",
|
||||
FeatureKind::SurfaceTrim { .. } => "Trim",
|
||||
|
||||
@@ -4057,9 +4057,10 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::SurfaceRevolve { .. }
|
||||
| FeatureKind::SurfaceSweep { .. }
|
||||
| FeatureKind::SurfaceLoft { .. }
|
||||
| FeatureKind::SurfaceBlend { .. }
|
||||
| FeatureKind::SurfaceOffset { .. }
|
||||
| FeatureKind::SurfaceRuled { .. }
|
||||
| FeatureKind::SurfaceUntrim { .. } => surface::eval(doc, vals, f, st),
|
||||
| FeatureKind::SurfaceUntrim { .. } => surface::eval(doc, vals, f, st, warning),
|
||||
FeatureKind::Emboss { .. }
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::ReplaceFace { .. }
|
||||
|
||||
@@ -6,9 +6,9 @@ use super::*;
|
||||
use crate::{RuledCorners, RuledMode};
|
||||
use solidcraft_geom::Seg2;
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::Patch { sketch, profiles, body, edges } => {
|
||||
FeatureKind::Patch { sketch, profiles, body, edges, crown } => {
|
||||
let mut made = Vec::new();
|
||||
if let Some(sk) = sketch {
|
||||
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
|
||||
@@ -20,7 +20,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
if !edges.is_empty() {
|
||||
let i = body_at(st, body, edges)?;
|
||||
let b = st.bodies.get(i).ok_or_else(|| DocError::Invalid("body".into()))?;
|
||||
made.push(kernel::patch_edges(&b.body, edges)?);
|
||||
made.push(kernel::patch_edges_crowned(&b.body, edges, *crown)?);
|
||||
}
|
||||
if made.is_empty() {
|
||||
return Err(DocError::Invalid("a patch needs a sketch region or a loop of edges".into()));
|
||||
@@ -107,6 +107,26 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceBlend { from, to, continuity, bulge, align, stitch } => {
|
||||
let a = st.body(&from.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", from.body)))?.body.clone();
|
||||
let b = st.body(&to.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", to.body)))?.body.clone();
|
||||
if from.edges.is_empty() || to.edges.is_empty() {
|
||||
return Err(DocError::Invalid("a blend needs a chain of edges at each end".into()));
|
||||
}
|
||||
let (made, gap) = kernel::blend_between(&a, &from.edges, &b, &to.edges, *continuity, *bulge, *align, BLEND_STATIONS, *stitch)?;
|
||||
if gap > 1e-9 {
|
||||
*warning = Some(format!("the blend's boundary lies up to {gap:.4} mm from the edges it was built on"));
|
||||
}
|
||||
let name = new_name(st, f, 0);
|
||||
// A stitched blend takes the place of both bodies it joins; otherwise it is a new
|
||||
// surface body of its own.
|
||||
if *stitch {
|
||||
let gone = [from.body.clone(), to.body.clone()];
|
||||
st.bodies.retain(|x| !gone.contains(&x.name));
|
||||
}
|
||||
st.bodies.push(ModelBody::new(name, made, f.id));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
@@ -329,6 +349,9 @@ fn sheet_chains(ss: &SolvedSketch, profiles: Option<&ProfileSel>, curves: &[Stri
|
||||
|
||||
/// Most sketch curves one feature chains.
|
||||
const MAX_CHAIN_CURVES: usize = 1000;
|
||||
/// Stations a blend samples each of its two boundaries at: enough that the boundary it fits is
|
||||
/// within a small part of a thousandth of the chain's length of the edges it was built on.
|
||||
const BLEND_STATIONS: usize = 41;
|
||||
|
||||
/// The sketch curves `ids` (free-form ones exactly where they can be) grouped into chains of segments that meet end to end, each oriented
|
||||
/// to run one way. Curves are joined greedily at their free ends: where three meet, one of them
|
||||
|
||||
@@ -158,6 +158,7 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
|
||||
"orientation": "parallel", "twist": "surface_sweep.twist", "scale": "surface_sweep.scale"}),
|
||||
),
|
||||
("surface_loft", json!({"type": "surface_loft", "sections": []})),
|
||||
("surface_blend", json!({"type": "surface_blend", "from": {"body": "B", "edges": [o]}, "to": {"body": "C", "edges": [o]}, "align": [o, o]})),
|
||||
("surface_offset", json!({"type": "surface_offset", "faces": [o], "distance": "soff.distance", "tolerance": "soff.tolerance"})),
|
||||
("unstitch", json!({"type": "unstitch", "body": "B"})),
|
||||
("reverse_normal", json!({"type": "reverse_normal", "body": "B"})),
|
||||
|
||||
@@ -328,6 +328,7 @@ surface_sweep_options
|
||||
Scale = surface_sweep.scale (Unitless)
|
||||
expressions: surface_sweep.twist, surface_sweep.scale
|
||||
surface_loft
|
||||
surface_blend
|
||||
surface_offset
|
||||
inputs:
|
||||
Distance = soff.distance (Length)
|
||||
|
||||
@@ -147,6 +147,8 @@ surface_revolve
|
||||
surface_sweep
|
||||
surface_sweep_options
|
||||
surface_loft
|
||||
surface_blend
|
||||
moves: align[0][0], align[0][1], align[0][2], align[1][0], align[1][1], align[1][2], from.edges[0][0], from.edges[0][1], from.edges[0][2], to.edges[0][0], to.edges[0][1], to.edges[0][2]
|
||||
surface_offset
|
||||
moves: faces[0][0], faces[0][1], faces[0][2]
|
||||
face points: [0, 0, 0]
|
||||
|
||||
@@ -169,6 +169,8 @@ surface_sweep_options
|
||||
surface_loft
|
||||
sketches: [4, 5]
|
||||
remap_refs: sections[0].sketch, sections[1].sketch
|
||||
surface_blend
|
||||
remap_bodies: from.body
|
||||
surface_offset
|
||||
remap_bodies: body
|
||||
unstitch
|
||||
|
||||
@@ -212,6 +212,11 @@
|
||||
//! parentheses (`2,5 mm` is `2.5 mm`, #1347; inside a function's parentheses the comma still
|
||||
//! separates its arguments). Nothing new is saved, but an expression typed that way is saved as
|
||||
//! typed; 0.12.0 asks for an update instead of failing to evaluate it.
|
||||
//! Also under 22, a design can hold a `SurfaceBlend` (Blend Surface: `from`/`to` edge chains of
|
||||
//! two bodies, optional `continuity`, `bulge`, `align`, `stitch`), and a `Patch` can carry a
|
||||
//! `crown` (0 by default, and then not saved: a bump that vanishes on the loop, so the face
|
||||
//! still meets the picked edges). Version 21 designs need no transformation; 0.12.0 asks for an
|
||||
//! update instead of meeting a feature it can't read or dropping a patch's crown.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ impl Walk for ProfileSel {}
|
||||
impl Walk for solidcraft_sketch::Sketch {}
|
||||
impl Walk for solidcraft_kernel::StepNode {}
|
||||
impl Walk for crate::document::Operation {}
|
||||
impl Walk for solidcraft_kernel::Continuity {}
|
||||
impl Walk for crate::document::Direction {}
|
||||
impl Walk for crate::document::FilletFit {}
|
||||
impl Walk for crate::document::MeshData {}
|
||||
@@ -136,6 +137,36 @@ impl<T> Each<T> for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Each<T> for [T; N] {
|
||||
fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) {
|
||||
for t in self {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)) {
|
||||
for t in self {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Each<T> for Option<[T; N]> {
|
||||
fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) {
|
||||
if let Some(a) = self {
|
||||
for t in a {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)) {
|
||||
if let Some(a) = self {
|
||||
for t in a {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Each<T> for Option<Vec<T>> {
|
||||
fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) {
|
||||
for t in self.iter().flatten() {
|
||||
|
||||
Reference in new issue
Block a user