Merge remote-tracking branch 'origin/main' into feat/profiles-by-curve-ids
# Conflicts: # crates/doc/src/lib.rs # crates/sketch/src/lib.rs
This commit is contained in:
commit
184e301c46
180 files changed
+29315
-1568
No files matched your search
+508
-90
@@ -16,6 +16,10 @@ use crate::{AxisRef, DocError, Document, Feature, FeatureKind, HoleKind, Operati
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModelBody {
|
||||
pub name: String,
|
||||
/// Definition identity for a computed placement; never parsed from its display name.
|
||||
pub source_name: Option<String>,
|
||||
/// Definition geometry coordinates to this computed body's frame.
|
||||
pub source_transform: crate::Mat,
|
||||
pub body: Body,
|
||||
/// Feature that created it.
|
||||
pub feature: u64,
|
||||
@@ -29,13 +33,30 @@ pub struct ModelBody {
|
||||
pub(crate) names: crate::naming::NameCell,
|
||||
/// The component the body belongs to (set after each feature).
|
||||
pub component: u64,
|
||||
/// A timeline transfer has converted this body into its evaluated owner's frame.
|
||||
pub transferred: bool,
|
||||
/// The look it was imported with, carried through later features.
|
||||
pub imported: Option<Arc<crate::appearance::ImportedLook>>,
|
||||
}
|
||||
|
||||
impl ModelBody {
|
||||
pub fn new(name: String, body: Body, feature: u64) -> Self {
|
||||
ModelBody { name, body, feature, mesh: Arc::new(OnceLock::new()), key: 0, names: Default::default(), component: 0, imported: None }
|
||||
ModelBody {
|
||||
name,
|
||||
source_name: None,
|
||||
source_transform: crate::IDENTITY,
|
||||
body,
|
||||
feature,
|
||||
mesh: Arc::new(OnceLock::new()),
|
||||
key: 0,
|
||||
names: Default::default(),
|
||||
component: 0,
|
||||
transferred: false,
|
||||
imported: None,
|
||||
}
|
||||
}
|
||||
pub fn definition_name(&self) -> &str {
|
||||
self.source_name.as_deref().unwrap_or(&self.name)
|
||||
}
|
||||
/// Display mesh (chord tolerance 1/1000 of the body size), computed once.
|
||||
pub fn mesh(&self) -> Arc<Mesh> {
|
||||
@@ -256,6 +277,8 @@ impl ModelState {
|
||||
b.name.hash(&mut h);
|
||||
b.key.hash(&mut h);
|
||||
b.feature.hash(&mut h);
|
||||
b.component.hash(&mut h);
|
||||
b.transferred.hash(&mut h);
|
||||
}
|
||||
for s in &self.sketches {
|
||||
s.feature.hash(&mut h);
|
||||
@@ -274,6 +297,13 @@ impl ModelState {
|
||||
let depth = before.bodies.iter().filter_map(|b| b.names.origin.as_ref().map(|o| o.depth)).max().unwrap_or(0) + 1;
|
||||
let origin = Arc::new(crate::naming::NamingOrigin { feature: f.clone(), before: before.clone(), depth, mats });
|
||||
for b in &mut self.bodies {
|
||||
if !matches!(f.kind, FeatureKind::BodyTransfer { .. })
|
||||
&& let Some(old) = before.bodies.iter().find(|old| old.name == b.name && old.transferred)
|
||||
{
|
||||
b.transferred = true;
|
||||
b.component = old.component;
|
||||
b.source_transform = old.source_transform;
|
||||
}
|
||||
let kept = before.bodies.iter().find(|x| x.name == b.name && x.body.same(&b.body));
|
||||
match kept {
|
||||
Some(x) => {
|
||||
@@ -284,7 +314,9 @@ impl ModelState {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(key, &b.name).hash(&mut h);
|
||||
b.key = h.finish();
|
||||
b.names = crate::naming::NameCell::new(Some(origin.clone()));
|
||||
if !matches!(f.kind, FeatureKind::BodyTransfer { .. }) {
|
||||
b.names = crate::naming::NameCell::new(Some(origin.clone()));
|
||||
}
|
||||
// Faces split on purpose stay split through later features (for export).
|
||||
if b.body.split_lines().is_none()
|
||||
&& let Some(lines) = before.bodies.iter().find(|x| x.name == b.name).and_then(|x| x.body.split_lines())
|
||||
@@ -476,8 +508,31 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
|
||||
/// Where components are placed, for a sketch that sees other components' geometry (on a face,
|
||||
/// or with projections): it is resolved in its own component's view of them.
|
||||
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
let path = match &f.kind {
|
||||
FeatureKind::Sweep { path_sketch, .. }
|
||||
| FeatureKind::Pipe { path_sketch, .. }
|
||||
| FeatureKind::Pattern { pattern: crate::PatternKind::Path { path_sketch, .. }, .. } => Some(*path_sketch),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(path) = path
|
||||
&& doc.sketch(path).is_ok_and(|sketch| !sketch.wires.is_empty())
|
||||
{
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
let owner = doc.feature(path).map(|feature| feature.component).unwrap_or(0);
|
||||
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
|
||||
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
if let FeatureKind::Sweep { sketch, .. } = &f.kind {
|
||||
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
let FeatureKind::Sketch { plane, sketch } = &f.kind else { return 0 };
|
||||
if doc.occurrences.is_empty() || (!matches!(plane, PlaneRef::Face { .. }) && sketch.links.is_empty()) {
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
if (doc.occurrences.is_empty() && (anchor.is_none() || doc.body_offsets.is_empty()))
|
||||
|| (!matches!(plane, PlaneRef::Face { .. }) && anchor.is_none() && sketch.links.is_empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
@@ -614,7 +669,10 @@ impl Model {
|
||||
let mut warning = None;
|
||||
// Face references by name: move each face point onto its named face.
|
||||
let renamed;
|
||||
let f = if f.face_names.iter().any(|n| !n.is_empty()) {
|
||||
// Cylindrical emboss resolves its named face in the selected body's frame
|
||||
// and certifies an analytic anchor; mesh projection here would undo that.
|
||||
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
|
||||
let f = if !explicit_emboss && f.face_names.iter().any(|n| !n.is_empty()) {
|
||||
let mut g = f.clone();
|
||||
let mut notes = Vec::new();
|
||||
for (p, n) in g.kind.face_points_mut().into_iter().zip(&f.face_names) {
|
||||
@@ -652,7 +710,7 @@ impl Model {
|
||||
Ok(()) => {
|
||||
// Copies are named after their sources through the instance transforms.
|
||||
let mats = match &f.kind {
|
||||
FeatureKind::Pattern { pattern, .. } => pattern_transforms(&vals, &state, pattern).unwrap_or_default(),
|
||||
FeatureKind::Pattern { pattern, .. } => pattern_transforms(doc, &vals, &state, f.component, pattern).unwrap_or_default(),
|
||||
FeatureKind::Mirror { plane, .. } => {
|
||||
doc.resolve_plane_in(&vals, plane, 0, Some(&state)).map(|pl| vec![mirror_matrix(&pl)]).unwrap_or_default()
|
||||
}
|
||||
@@ -660,7 +718,9 @@ impl Model {
|
||||
};
|
||||
next.stamp(&state, f, key, mats);
|
||||
for b in &mut next.bodies {
|
||||
b.component = doc.body_component(&b.name, b.feature);
|
||||
if !b.transferred {
|
||||
b.component = doc.legacy_body_component(&b.name, b.feature);
|
||||
}
|
||||
}
|
||||
state = Arc::new(next);
|
||||
None
|
||||
@@ -702,7 +762,7 @@ fn val(vals: &BTreeMap<String, Value>, e: &str, k: Kind) -> Result<f64> {
|
||||
Document::eval_in(vals, e, k)
|
||||
}
|
||||
|
||||
fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region2>> {
|
||||
pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region2>> {
|
||||
let ps = &ss.profiles;
|
||||
if ps.is_empty() {
|
||||
return Err(DocError::Invalid(format!("{} has no closed profiles", ss.name)));
|
||||
@@ -773,8 +833,61 @@ fn new_name(state: &mut ModelState, f: &Feature, k: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_body_name(state: &ModelState, f: &Feature, k: usize, source: &str) -> String {
|
||||
let base = f.body_names.get(k).filter(|name| !name.trim().is_empty()).map(String::as_str).unwrap_or(source);
|
||||
unique_body_name(state, base)
|
||||
}
|
||||
|
||||
/// Add tool bodies to the model according to the operation.
|
||||
fn sheet_boolean_unchanged(body: &Body, tools: &[Body], op: Operation) -> Result<()> {
|
||||
if sheet_boolean_changes(body, tools, op)? {
|
||||
return Err(DocError::Invalid("this sheet machining cannot be preserved in its flat material: use a parallel, untapered extrude pocket/cut, or unfold before intersecting; unsupported 3D sheet machining is refused to prevent material restoration".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sheet_boolean_changes(body: &Body, tools: &[Body], op: Operation) -> Result<bool> {
|
||||
let united;
|
||||
let tools: &[Body] = if op == Operation::Intersect {
|
||||
united = intersection_tool_union(tools)?;
|
||||
&united
|
||||
} else {
|
||||
tools
|
||||
};
|
||||
for tool in tools {
|
||||
let test = if op == Operation::Cut { BoolOp::Intersect } else { BoolOp::Cut };
|
||||
if kernel::boolean(body, tool, test)?.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Selected profiles form one machining tool, including disconnected tool material.
|
||||
/// Balanced unions avoid repeatedly rebuilding a growing aggregate boundary.
|
||||
fn intersection_tool_union(tools: &[Body]) -> Result<Vec<Body>> {
|
||||
if tools.len() > 1000 || tools.iter().fold(0usize, |n, b| n.saturating_add(b.face_count())) > 16_384 {
|
||||
return Err(DocError::Invalid("intersection tools exceed the bounded geometry budget".into()));
|
||||
}
|
||||
let mut groups = tools.to_vec();
|
||||
while groups.len() > 1 {
|
||||
let mut next = Vec::new();
|
||||
let mut iter = groups.into_iter();
|
||||
while let Some(a) = iter.next() {
|
||||
match iter.next() {
|
||||
Some(b) => {
|
||||
next.push(kernel::boolean(&a, &b, BoolOp::Union)?.ok_or_else(|| DocError::Invalid("intersection tool union is empty".into()))?)
|
||||
}
|
||||
None => next.push(a),
|
||||
}
|
||||
}
|
||||
groups = next;
|
||||
}
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation, targets: &[String]) -> Result<()> {
|
||||
let tools = if op == Operation::Intersect { intersection_tool_union(&tools)? } else { tools };
|
||||
// Participants: the feature's own targets, else its explicit participants, else the bodies
|
||||
// of its component (and what it made itself): other components are left alone.
|
||||
let targets: &[String] = if targets.is_empty() { &f.participants } else { targets };
|
||||
@@ -800,6 +913,13 @@ fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if matches!(op, Operation::Cut | Operation::Intersect) {
|
||||
for &i in &target_idx {
|
||||
if let Some(mb) = state.bodies.get(i).filter(|b| state.sheets.iter().any(|s| s.body == b.name)) {
|
||||
sheet_boolean_unchanged(&mb.body, &tools, op)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
match op {
|
||||
Operation::Join => {
|
||||
let Some(&first) = target_idx.first() else { return Ok(()) };
|
||||
@@ -813,6 +933,8 @@ fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation
|
||||
}
|
||||
Operation::Cut | Operation::Intersect => {
|
||||
let bop = if op == Operation::Cut { BoolOp::Cut } else { BoolOp::Intersect };
|
||||
let affected: std::collections::BTreeSet<String> =
|
||||
target_idx.iter().filter_map(|i| state.bodies.get(*i).map(|b| b.name.clone())).collect();
|
||||
let mut remove = Vec::new();
|
||||
for &ti in &target_idx {
|
||||
let Some(mut acc) = state.bodies.get(ti).map(|b| Some(b.body.clone())) else { continue };
|
||||
@@ -837,9 +959,15 @@ fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation
|
||||
i += 1;
|
||||
keep
|
||||
});
|
||||
let live: std::collections::BTreeSet<&str> = state.bodies.iter().map(|b| b.name.as_str()).collect();
|
||||
state.threads.retain(|t| live.contains(t.body.as_str()));
|
||||
// A cut can leave a body in pieces: each piece becomes its own body ("Bar", "Bar (1)").
|
||||
let mut split: Vec<ModelBody> = Vec::new();
|
||||
for mb in std::mem::take(&mut state.bodies) {
|
||||
if !affected.contains(&mb.name) || state.sheets.iter().any(|s| s.body == mb.name) {
|
||||
split.push(mb);
|
||||
continue;
|
||||
}
|
||||
let pieces = mb.body.lumps().unwrap_or_else(|_| vec![mb.body.clone()]);
|
||||
if pieces.len() < 2 {
|
||||
split.push(mb);
|
||||
@@ -858,7 +986,7 @@ fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation
|
||||
}
|
||||
|
||||
/// Distance that reaches through every target body from the sketch plane (either side).
|
||||
fn through_all_distance(st: &ModelState, plane: &Plane, targets: &[String]) -> Result<f64> {
|
||||
pub fn through_all_distance(st: &ModelState, plane: &Plane, targets: &[String]) -> Result<f64> {
|
||||
let mut far: f64 = 0.0;
|
||||
let mut any = false;
|
||||
for b in st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)) {
|
||||
@@ -1356,8 +1484,107 @@ fn hole_tools(
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
/// The angular interval used by both the solid and its interactive extent handles.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RevolveExtent {
|
||||
pub start: f64,
|
||||
pub sweep: f64,
|
||||
pub rotation_axis: Vec3,
|
||||
pub display_start: f64,
|
||||
/// A curved target trims the swept solid and has no single angular boundary.
|
||||
pub curved_to: bool,
|
||||
}
|
||||
|
||||
pub fn revolve_extent(
|
||||
st: &ModelState,
|
||||
plane: &Plane,
|
||||
regions: &[Region2],
|
||||
(origin, axis): (Vec3, Vec3),
|
||||
(angle, angle2): (f64, Option<f64>),
|
||||
symmetric: bool,
|
||||
to: Option<Vec3>,
|
||||
) -> Result<RevolveExtent> {
|
||||
revolve_extent_with_convention(st, plane, regions, (origin, axis), (angle, angle2), symmetric, to, crate::RevolveConvention::LegacyAxisStart)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn revolve_extent_with_convention(
|
||||
st: &ModelState,
|
||||
plane: &Plane,
|
||||
regions: &[Region2],
|
||||
(origin, axis): (Vec3, Vec3),
|
||||
(angle, angle2): (f64, Option<f64>),
|
||||
symmetric: bool,
|
||||
to: Option<Vec3>,
|
||||
convention: crate::RevolveConvention,
|
||||
) -> Result<RevolveExtent> {
|
||||
let (start, sweep) = match (to, angle2, symmetric) {
|
||||
(Some(point), _, _) => {
|
||||
let points: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
|
||||
let centre = if convention == crate::RevolveConvention::SketchNormal {
|
||||
regions.first().map(|r| plane.to_world(r.centroid())).ok_or_else(|| DocError::Invalid("no profile selected".into()))?
|
||||
} else {
|
||||
plane.to_world(points.iter().fold(Vec2::default(), |sum, point| sum + *point) * (1.0 / points.len().max(1) as f64))
|
||||
};
|
||||
let projection_axis = if convention == crate::RevolveConvention::SketchNormal {
|
||||
axis.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let flat = |point: Vec3| {
|
||||
let v = point - origin;
|
||||
v - projection_axis * v.dot(projection_axis)
|
||||
};
|
||||
let (u, v) = (flat(centre), flat(point));
|
||||
let angle_axis = if convention == crate::RevolveConvention::SketchNormal {
|
||||
if v.len() < 1e-9 {
|
||||
return Err(DocError::Invalid("the revolve target lies on the axis; pick a point away from the axis".into()));
|
||||
}
|
||||
kernel::canonical_revolve_axis(
|
||||
plane,
|
||||
regions.first().ok_or_else(|| DocError::Invalid("no profile selected".into()))?,
|
||||
plane.to_local(origin),
|
||||
Vec2::new(projection_axis.dot(plane.x), projection_axis.dot(plane.y)),
|
||||
)?
|
||||
} else {
|
||||
-axis
|
||||
};
|
||||
let sweep = angle_axis.dot(u.cross(v)).atan2(u.dot(v));
|
||||
(0.0, if sweep <= 1e-9 { sweep + std::f64::consts::TAU } else { sweep })
|
||||
}
|
||||
(None, Some(other), _) => (-other, angle + other),
|
||||
(None, None, true) => (-angle / 2.0, angle),
|
||||
(None, None, false) => (0.0, angle),
|
||||
};
|
||||
if !(sweep.is_finite() && start.is_finite() && sweep.abs() > 1e-9 && sweep.abs() <= std::f64::consts::TAU + 1e-9) {
|
||||
return Err(DocError::Invalid("the revolve angle must be more than 0 and at most 360°".into()));
|
||||
}
|
||||
let points: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
|
||||
let centre = plane.to_world(points.iter().fold(Vec2::default(), |sum, point| sum + *point) * (1.0 / points.len().max(1) as f64));
|
||||
let unit_axis = axis.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
|
||||
let radial_centre = regions.first().map(|region| plane.to_world(region.centroid())).unwrap_or(centre);
|
||||
let radial = radial_centre - origin - unit_axis * (radial_centre - origin).dot(unit_axis);
|
||||
let rotation_axis = match (convention, regions.first()) {
|
||||
(crate::RevolveConvention::SketchNormal, Some(region)) => {
|
||||
kernel::canonical_revolve_axis(plane, region, plane.to_local(origin), Vec2::new(unit_axis.dot(plane.x), unit_axis.dot(plane.y)))?
|
||||
}
|
||||
_ => {
|
||||
if unit_axis.cross(radial).dot(plane.normal()) < 0.0 {
|
||||
-unit_axis
|
||||
} else {
|
||||
unit_axis
|
||||
}
|
||||
}
|
||||
};
|
||||
let display_start = match convention {
|
||||
crate::RevolveConvention::LegacyAxisStart => start * -unit_axis.dot(rotation_axis),
|
||||
crate::RevolveConvention::SketchNormal => start,
|
||||
};
|
||||
Ok(RevolveExtent { start, sweep, rotation_axis, display_start, curved_to: to.is_some_and(|point| curved_target_any(st, point).is_some()) })
|
||||
}
|
||||
|
||||
/// The tool bodies a feature adds or removes (extrude, revolve, primitives), in the current state.
|
||||
fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -> Result<Vec<Body>> {
|
||||
fn feature_tools(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -> Result<Vec<Body>> {
|
||||
match &f.kind {
|
||||
FeatureKind::Extrude { sketch, profiles, extent, operation, targets } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
|
||||
@@ -1397,11 +1624,12 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
if extent.distance2.is_some() || extent.start_offset.is_some() || extent.direction == crate::Direction::Symmetric {
|
||||
return Err(DocError::Invalid("not supported yet: taper with two-sided, symmetric or offset extents".into()));
|
||||
}
|
||||
let sign = if extent.direction == crate::Direction::Negative { -1.0 } else { 1.0 };
|
||||
let sign = if extent.to.is_none() && extent.direction == crate::Direction::Negative { -1.0 } else { 1.0 };
|
||||
let (len, sign) = if d < 0.0 { (-d, -sign) } else { (d, sign) };
|
||||
let start_plane = ss.plane.offset(from);
|
||||
return Ok(regions
|
||||
.iter()
|
||||
.map(|r| kernel::extrude_tapered(&ss.plane, r, len, sign, taper))
|
||||
.map(|r| kernel::extrude_tapered(&start_plane, r, len, sign, taper))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?);
|
||||
}
|
||||
let off = from
|
||||
@@ -1459,59 +1687,102 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
let start = if hi > 0.0 { lo + off } else { hi + off };
|
||||
tools = trim_to_body(tools, &tb.body, |q: Vec3| ((q - ss.plane.origin).dot(n) - start).abs())?;
|
||||
}
|
||||
if *operation == Operation::NewBody && extent.taper.is_none() {
|
||||
let cap_bindings = crate::config::extrude_cap_bindings(&ss, &tools, 10_000_000);
|
||||
tools = tools
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(body_index, body)| {
|
||||
let curves: Vec<_> = body
|
||||
.face_sources()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|source| match source {
|
||||
Some(kernel::FaceSource::ExtrudeSide { segment, .. }) if cap_bindings.is_some() => {
|
||||
crate::config::exact_sketch_curve(&ss, segment)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let caps =
|
||||
cap_bindings.as_ref().and_then(|bindings| bindings.get(body_index)).cloned().unwrap_or_else(|| vec![None; curves.len()]);
|
||||
body.with_extrude_certificates(f.id, *sketch, &curves)?
|
||||
.with_extrude_cap_certificates(f.id, *sketch, &caps)
|
||||
.map_err(DocError::from)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
}
|
||||
Ok(tools)
|
||||
}
|
||||
FeatureKind::Revolve { sketch, profiles, axis, angle, angle2, symmetric, to, .. } => {
|
||||
FeatureKind::Revolve { sketch, profiles, axis, angle, angle2, symmetric, convention, to, .. } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
|
||||
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
// Where the turn starts (from the profile) and how far it goes.
|
||||
let (start, ang) = match (to, angle2, symmetric) {
|
||||
(Some(p), _, _) => {
|
||||
let (ow, aw) = (ss.plane.to_world(o), ss.plane.dir_to_world(d));
|
||||
let pts: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
|
||||
let c = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, q| a + *q) * (1.0 / pts.len().max(1) as f64));
|
||||
let flat = |q: Vec3| {
|
||||
let v = q - ow;
|
||||
v - aw * v.dot(aw)
|
||||
};
|
||||
let (u, v) = (flat(c), flat(*p));
|
||||
// The revolve turns clockwise about its axis direction (the kernel's sense).
|
||||
let a = (aw * -1.0).dot(u.cross(v)).atan2(u.dot(v));
|
||||
let a = if a <= 1e-9 { a + std::f64::consts::TAU } else { a };
|
||||
(0.0, a)
|
||||
}
|
||||
(None, Some(a2), _) => {
|
||||
let (a, b) = (val(vals, angle, Kind::Angle)?, val(vals, a2, Kind::Angle)?);
|
||||
(-b, a + b)
|
||||
}
|
||||
(None, None, true) => {
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
(-a / 2.0, a)
|
||||
}
|
||||
(None, None, false) => (0.0, val(vals, angle, Kind::Angle)?),
|
||||
let angles = if to.is_some() {
|
||||
(0.0, None)
|
||||
} else {
|
||||
(val(vals, angle, Kind::Angle)?, angle2.as_ref().map(|a| val(vals, a, Kind::Angle)).transpose()?)
|
||||
};
|
||||
if !(ang.abs() > 1e-9 && ang.abs() <= std::f64::consts::TAU + 1e-9) {
|
||||
return Err(DocError::Invalid("the revolve angle must be more than 0 and at most 360°".into()));
|
||||
}
|
||||
let extent = revolve_extent_with_convention(
|
||||
st,
|
||||
&ss.plane,
|
||||
®ions,
|
||||
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
|
||||
angles,
|
||||
*symmetric,
|
||||
*to,
|
||||
*convention,
|
||||
)?;
|
||||
let (start, ang) = (extent.start, extent.sweep);
|
||||
let region_extents = regions
|
||||
.iter()
|
||||
.map(|r| {
|
||||
if *convention == crate::RevolveConvention::SketchNormal && to.is_some() {
|
||||
revolve_extent_with_convention(
|
||||
st,
|
||||
&ss.plane,
|
||||
std::slice::from_ref(r),
|
||||
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
|
||||
angles,
|
||||
*symmetric,
|
||||
*to,
|
||||
*convention,
|
||||
)
|
||||
} else {
|
||||
Ok(extent)
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let (operation, targets) = feature_op(f);
|
||||
let axis_w = ss.plane.dir_to_world(d);
|
||||
let origin_w = ss.plane.to_world(o);
|
||||
let probe = |p: Vec2| {
|
||||
let q = ss.plane.to_world(p);
|
||||
(1..4)
|
||||
.map(|k| {
|
||||
let a = ang * k as f64 / 4.0;
|
||||
let v = q - origin_w;
|
||||
origin_w + v * a.cos() + axis_w.cross(v) * a.sin() + axis_w * (axis_w.dot(v) * (1.0 - a.cos()))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let regions: Vec<Region2> = regions.iter().map(|r| grow_profile_for_coplanar(st, r, operation, &targets, &probe)).collect();
|
||||
let regions: Vec<Region2> = regions
|
||||
.iter()
|
||||
.zip(®ion_extents)
|
||||
.map(|(r, region_extent)| {
|
||||
let rotation_axis = match convention {
|
||||
crate::RevolveConvention::LegacyAxisStart => axis_w,
|
||||
crate::RevolveConvention::SketchNormal => kernel::canonical_revolve_axis(&ss.plane, r, o, d)?,
|
||||
};
|
||||
let probe = |p: Vec2| {
|
||||
let q = ss.plane.to_world(p);
|
||||
(1..4)
|
||||
.map(|k| {
|
||||
let a = region_extent.sweep * k as f64 / 4.0
|
||||
+ if *convention == crate::RevolveConvention::SketchNormal { region_extent.start } else { 0.0 };
|
||||
let v = q - origin_w;
|
||||
origin_w + v * a.cos() + rotation_axis.cross(v) * a.sin() + rotation_axis * (rotation_axis.dot(v) * (1.0 - a.cos()))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
Ok(grow_profile_for_coplanar(st, r, operation, &targets, &probe))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// To a curved face: turn past the whole target body, then cut back to its surface
|
||||
// (the piece that starts at the profile).
|
||||
let curved = to.and_then(|p| curved_target_any(st, p));
|
||||
if let Some(tb) = curved.and_then(|bi| st.bodies.get(bi)) {
|
||||
let curved_body = curved.and_then(|bi| st.bodies.get(bi));
|
||||
let build_curved = |regions: &[Region2], angle_axis: Vec3, tb: &ModelBody| -> Result<Vec<Body>> {
|
||||
let pts: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
|
||||
let c = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, q| a + *q) * (1.0 / pts.len().max(1) as f64));
|
||||
let flat = |q: Vec3| {
|
||||
@@ -1521,15 +1792,36 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
let u0 = flat(c);
|
||||
let angle_of = |q: Vec3| {
|
||||
let v = flat(q);
|
||||
let a = (axis_w * -1.0).dot(u0.cross(v)).atan2(u0.dot(v));
|
||||
let a = angle_axis.dot(u0.cross(v)).atan2(u0.dot(v));
|
||||
if a < 0.0 { a + std::f64::consts::TAU } else { a }
|
||||
};
|
||||
let far = tb.mesh().positions.iter().map(|q| angle_of(*q)).fold(0.0, f64::max);
|
||||
let sweep = (far + 0.05).min(std::f64::consts::TAU - 1e-3);
|
||||
let bodies = kernel::revolve(&ss.plane, ®ions, o, d, sweep)?;
|
||||
let bodies = kernel::revolve(&ss.plane, regions, o, d, sweep)?;
|
||||
let n = ss.plane.normal();
|
||||
let tol_side = |q: Vec3| if flat(q).dot(u0) > 0.0 { ((q - ss.plane.origin).dot(n)).abs() } else { f64::MAX };
|
||||
return trim_to_body(bodies, &tb.body, tol_side);
|
||||
trim_to_body(bodies, &tb.body, tol_side)
|
||||
};
|
||||
if *convention == crate::RevolveConvention::LegacyAxisStart
|
||||
&& let Some(tb) = curved_body
|
||||
{
|
||||
return build_curved(®ions, -axis_w, tb);
|
||||
}
|
||||
if *convention == crate::RevolveConvention::SketchNormal {
|
||||
let mut out = Vec::new();
|
||||
for (region, region_extent) in regions.iter().zip(®ion_extents) {
|
||||
let rotation_axis = kernel::canonical_revolve_axis(&ss.plane, region, o, d)?;
|
||||
if let Some(tb) = curved_body {
|
||||
out.extend(build_curved(std::slice::from_ref(region), rotation_axis, tb)?);
|
||||
continue;
|
||||
}
|
||||
let bodies = kernel::revolve(&ss.plane, std::slice::from_ref(region), o, d, region_extent.sweep)?;
|
||||
let m = crate::rigid(Vec3::ZERO, origin_w, rotation_axis, region_extent.start);
|
||||
for body in bodies {
|
||||
out.push(if region_extent.start.abs() < 1e-12 { body } else { kernel::transform_matrix(&body, m)? });
|
||||
}
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
let bodies = kernel::revolve(&ss.plane, ®ions, o, d, ang)?;
|
||||
if start.abs() < 1e-12 {
|
||||
@@ -1606,6 +1898,15 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
|
||||
let ps =
|
||||
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
|
||||
let profile;
|
||||
let ss = if !ps.sketch.wires.is_empty() {
|
||||
profile = profile_in_frame(doc, ss, f.component)?;
|
||||
&profile
|
||||
} else {
|
||||
ss
|
||||
};
|
||||
let mapped = path_in_frame(doc, ps, f.component)?;
|
||||
let ps = mapped.as_ref();
|
||||
let regions = solvecraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
|
||||
let start = regions.first().map(|r| ss.plane.to_world(r.centroid())).unwrap_or_default();
|
||||
// A path through 3D sketch curves follows them as points.
|
||||
@@ -1618,6 +1919,8 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
FeatureKind::Pipe { path_sketch, path, diameter, wall, .. } => {
|
||||
let ps =
|
||||
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
|
||||
let mapped = path_in_frame(doc, ps, f.component)?;
|
||||
let ps = mapped.as_ref();
|
||||
let r = val(vals, diameter, Kind::Length)? / 2.0;
|
||||
if !(r > 1e-6) {
|
||||
return Err(DocError::Invalid("pipe diameter must be positive".into()));
|
||||
@@ -1682,6 +1985,51 @@ fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<String>) ->
|
||||
Ok(Region2 { outer: solvecraft_geom::Loop2::circle(Vec2::ZERO, r), holes })
|
||||
}
|
||||
|
||||
fn map_wire(wire: &mut solvecraft_sketch::Wire, matrix: &Mat) {
|
||||
for point in wire.pts.iter_mut().chain(&mut wire.fit) {
|
||||
*point = crate::apply_point(matrix, *point);
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_in_frame(doc: &Document, sketch: &SolvedSketch, consumer: u64) -> Result<SolvedSketch> {
|
||||
let owner = doc.feature(sketch.feature).map(|feature| feature.component).unwrap_or(0);
|
||||
let inverse =
|
||||
crate::mat_inverse(&doc.component_transform(consumer)).ok_or_else(|| DocError::Invalid("profile consumer frame is invalid".into()))?;
|
||||
let relative = crate::mat_mul(&inverse, &doc.component_transform(owner));
|
||||
let mut mapped = sketch.clone();
|
||||
mapped.plane = Plane::new(
|
||||
crate::apply_point(&relative, sketch.plane.origin),
|
||||
crate::apply_vector(&relative, sketch.plane.x),
|
||||
crate::apply_vector(&relative, sketch.plane.y),
|
||||
)
|
||||
.ok_or_else(|| DocError::Invalid("profile sketch frame is invalid".into()))?;
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
/// Stored wire coordinates are converted once into the feature consumer frame.
|
||||
fn path_in_frame<'a>(doc: &Document, sketch: &'a SolvedSketch, consumer: u64) -> Result<std::borrow::Cow<'a, SolvedSketch>> {
|
||||
use std::borrow::Cow;
|
||||
if sketch.sketch.wires.is_empty() {
|
||||
return Ok(Cow::Borrowed(sketch));
|
||||
}
|
||||
let owner = doc.feature(sketch.feature).map(|f| f.component).unwrap_or(0);
|
||||
let inverse =
|
||||
crate::mat_inverse(&doc.component_transform(consumer)).ok_or_else(|| DocError::Invalid("path consumer frame is not invertible".into()))?;
|
||||
let relative = crate::mat_mul(&inverse, &doc.component_transform(owner));
|
||||
let mut mapped = sketch.clone();
|
||||
mapped.plane = Plane::new(
|
||||
crate::apply_point(&relative, sketch.plane.origin),
|
||||
crate::apply_vector(&relative, sketch.plane.x),
|
||||
crate::apply_vector(&relative, sketch.plane.y),
|
||||
)
|
||||
.ok_or_else(|| DocError::Invalid("path sketch frame is invalid".into()))?;
|
||||
for wire in &mut mapped.sketch.wires {
|
||||
let matrix = if wire.space == solvecraft_sketch::WireSpace::Component { &relative } else { &inverse };
|
||||
map_wire(wire, matrix);
|
||||
}
|
||||
Ok(Cow::Owned(mapped))
|
||||
}
|
||||
|
||||
/// A path that uses 3D sketch curves (wires), as one chain of world points from the end nearest
|
||||
/// `start` (no start: the first curve runs toward the second). `None` when the path is all
|
||||
/// planar curves.
|
||||
@@ -1863,10 +2211,10 @@ const MAX_INSTANCES: usize = 1000;
|
||||
/// The placements (column-major matrices) of a pattern's copies, the original excluded.
|
||||
pub fn pattern_matrices(doc: &Document, st: &ModelState, p: &crate::PatternKind) -> Result<Vec<[[f64; 4]; 4]>> {
|
||||
let (vals, _) = doc.param_values();
|
||||
pattern_transforms(&vals, st, p)
|
||||
pattern_transforms(doc, &vals, st, 0, p)
|
||||
}
|
||||
|
||||
fn pattern_transforms(vals: &BTreeMap<String, Value>, st: &ModelState, p: &crate::PatternKind) -> Result<Vec<Mat>> {
|
||||
fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &ModelState, component: u64, p: &crate::PatternKind) -> Result<Vec<Mat>> {
|
||||
let count = |e: &str| -> Result<usize> {
|
||||
let n = val(vals, e, Kind::Unitless)?.round();
|
||||
if !(1.0..=MAX_INSTANCES as f64).contains(&n) {
|
||||
@@ -1901,6 +2249,8 @@ fn pattern_transforms(vals: &BTreeMap<String, Value>, st: &ModelState, p: &crate
|
||||
let n = count(c)?;
|
||||
let d = val(vals, spacing, Kind::Length)?;
|
||||
let ps = st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("path sketch {path_sketch}")))?;
|
||||
let mapped = path_in_frame(doc, ps, component)?;
|
||||
let ps = mapped.as_ref();
|
||||
let mut segs = path_segments(ps, path, Vec3::new(f64::NAN, f64::NAN, f64::NAN))?;
|
||||
if *flip {
|
||||
segs = segs.iter().rev().map(reverse_seg).collect();
|
||||
@@ -2066,7 +2416,7 @@ fn replay(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tools = feature_tools(vals, src, st)?;
|
||||
let tools = feature_tools(doc, vals, src, st)?;
|
||||
for m in mats {
|
||||
for t in &tools {
|
||||
copies.push(kernel::transform_matrix(t, *m)?);
|
||||
@@ -2074,7 +2424,14 @@ fn replay(
|
||||
}
|
||||
}
|
||||
// New bodies from a pattern join their source when they touch it (as one feature would).
|
||||
apply_op(st, f, copies, op, &targets)?;
|
||||
if f.body_names.is_empty() {
|
||||
apply_op(st, f, copies, op, &targets)?;
|
||||
} else {
|
||||
let start = st.bodies.iter().filter(|b| b.feature == f.id).count();
|
||||
let mut named = f.clone();
|
||||
named.body_names = f.body_names.iter().skip(start).cloned().collect();
|
||||
apply_op(st, &named, copies, op, &targets)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2087,7 +2444,7 @@ mod surface;
|
||||
/// Height above `plane` (along its normal) of the object at `p` as seen from `centre`: the face
|
||||
/// there if it is planar and parallel (its plane), else where a ray from `centre` along the
|
||||
/// normal meets the body there, else the point itself.
|
||||
fn object_height(st: &ModelState, plane: &Plane, centre: Vec3, p: Vec3) -> f64 {
|
||||
pub fn object_height(st: &ModelState, plane: &Plane, centre: Vec3, p: Vec3) -> f64 {
|
||||
let n = plane.normal();
|
||||
let h = |q: Vec3| (q - plane.origin).dot(n);
|
||||
for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) {
|
||||
@@ -2314,6 +2671,7 @@ fn chamfer_with(
|
||||
|
||||
fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::BodyTransfer { bodies, from, to, transform } => crate::body_transfer::evaluate(doc, st, bodies, *from, *to, *transform),
|
||||
FeatureKind::SheetBase { .. }
|
||||
| FeatureKind::SheetContour { .. }
|
||||
| FeatureKind::SheetFlange { .. }
|
||||
@@ -2335,13 +2693,21 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::Align { .. }
|
||||
| FeatureKind::Remove { .. } => more::eval(doc, vals, f, st),
|
||||
FeatureKind::Sketch { plane, sketch } => {
|
||||
let fst = state_in_frame(doc, st, f.component);
|
||||
let (plane, face_warning) = match plane {
|
||||
PlaneRef::Face { plane: picked, at, name } => face_plane(&fst, picked, *at, name.as_deref()),
|
||||
other => (doc.resolve_plane_in(vals, other, 0, Some(&fst))?, None),
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
let fst = match anchor {
|
||||
Some(anchor) => crate::state_in_face_anchor_frame(doc, st, anchor, f.component)?,
|
||||
None => state_in_frame(doc, st, f.component),
|
||||
};
|
||||
let (plane, face_warning) = match (anchor, plane) {
|
||||
(Some(_), plane) => (doc.resolve_attached_plane(vals, plane, f.component, st, &fst)?, None),
|
||||
(None, PlaneRef::Face { plane: picked, at, name, anchor: None }) => face_plane(&fst, picked, *at, name.as_deref()),
|
||||
(None, other) => (doc.resolve_plane_in(vals, other, 0, Some(&fst))?, None),
|
||||
};
|
||||
let mut sk = sketch.clone();
|
||||
let mut warns = crate::project::refresh_links(doc, vals, &fst, &plane, &mut sk);
|
||||
let mut warns = match anchor {
|
||||
Some(anchor) => crate::project::refresh_face_anchor_links(doc, vals, &fst, &plane, &mut sk, anchor),
|
||||
None => crate::project::refresh_links(doc, vals, &fst, &plane, &mut sk),
|
||||
};
|
||||
drop(fst);
|
||||
sk.refresh_offsets()?;
|
||||
if let Some(w) = face_warning {
|
||||
@@ -2349,7 +2715,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
doc.apply_dimension_values(vals, &mut sk)?;
|
||||
let mut report = solve(&mut sk);
|
||||
if !sk.offsets.is_empty() {
|
||||
if !sk.offsets.is_empty() || !sk.freeform_offsets.is_empty() {
|
||||
// Source dimensions may have moved curves during the solve.
|
||||
sk.refresh_offsets()?;
|
||||
report = solve(&mut sk);
|
||||
@@ -2360,6 +2726,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
if !warns.is_empty() {
|
||||
*warning = Some(warns.join("; "));
|
||||
}
|
||||
sk.validate_periodic_splines()?;
|
||||
let profiles = find_profiles(&sk);
|
||||
st.sketches.retain(|s| s.feature != f.id);
|
||||
st.sketches.push(SolvedSketch { feature: f.id, name: f.name.clone(), plane, sketch: sk, report, profiles });
|
||||
@@ -2371,16 +2738,26 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::Loft { operation, targets, .. }
|
||||
| FeatureKind::Coil { operation, targets, .. }
|
||||
| FeatureKind::Sweep { operation, targets, .. } => {
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
let tools = feature_tools(doc, vals, f, st)?;
|
||||
// Cuts through sheet metal become cut-outs of its flat pattern.
|
||||
if *operation == Operation::Cut && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
|
||||
if matches!(operation, Operation::Cut | Operation::Intersect) && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
|
||||
let handled = more::sheet_cut(f, st, &tools, targets)?;
|
||||
if !handled.is_empty() {
|
||||
let effective = if targets.is_empty() { &f.participants } else { targets };
|
||||
let all = effective.len() == 1 && effective.first().is_some_and(|t| t == "*");
|
||||
let rest: Vec<String> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
!handled.contains(&b.name)
|
||||
&& (all
|
||||
|| if effective.is_empty() {
|
||||
b.component == f.component || b.feature == f.id
|
||||
} else {
|
||||
effective.contains(&b.name)
|
||||
})
|
||||
})
|
||||
.map(|b| b.name.clone())
|
||||
.filter(|n| !handled.contains(n) && (targets.is_empty() || targets.contains(n)))
|
||||
.collect();
|
||||
if rest.is_empty() {
|
||||
return Ok(());
|
||||
@@ -2392,7 +2769,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
FeatureKind::Hole { thread, diameter, .. } => {
|
||||
let (spots, dir) = hole_spots(st, f)?;
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
let tools = feature_tools(doc, vals, f, st)?;
|
||||
apply_op(st, f, tools, Operation::Cut, &[])?;
|
||||
if let Some(d) = thread {
|
||||
// A point on each hole's wall, a little below the entry.
|
||||
@@ -2593,17 +2970,19 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Pattern { features, pattern, bodies } => {
|
||||
let mats = pattern_transforms(vals, st, pattern)?;
|
||||
let mats = pattern_transforms(doc, vals, st, f.component, pattern)?;
|
||||
if bodies.is_empty() {
|
||||
return replay(doc, vals, f, st, features, &mats, warning);
|
||||
}
|
||||
// Bodies: each copy is a new body named after its source.
|
||||
let mut output = 0;
|
||||
for n in bodies {
|
||||
let mb = st.body(n).cloned().ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
for m in &mats {
|
||||
let copy = kernel::transform_matrix(&mb.body, *m)?;
|
||||
let name = unique_body_name(st, &mb.name);
|
||||
let name = copy_body_name(st, f, output, &mb.name);
|
||||
st.bodies.push(ModelBody::new(name, copy, f.id));
|
||||
output += 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -2614,6 +2993,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
return replay(doc, vals, f, st, features, &[mirror_matrix(&pl)], warning);
|
||||
}
|
||||
let m = mirror_matrix(&pl);
|
||||
let mut output = 0;
|
||||
for n in bodies {
|
||||
let i = st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { continue };
|
||||
@@ -2627,13 +3007,15 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
*slot = ModelBody::new(mb.name.clone(), p, mb.feature);
|
||||
}
|
||||
} else {
|
||||
let name = unique_body_name(st, &mb.name);
|
||||
let name = copy_body_name(st, f, output, &mb.name);
|
||||
st.bodies.push(ModelBody::new(name, p, f.id));
|
||||
output += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let name = unique_body_name(st, &mb.name);
|
||||
let name = copy_body_name(st, f, output, &mb.name);
|
||||
st.bodies.push(ModelBody::new(name, mirrored, f.id));
|
||||
output += 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -2708,6 +3090,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let s = Vec3::new(val(vals, length, Kind::Length)?, val(vals, width, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
let corner = point(vals, corner)?;
|
||||
let b = kernel::box_solid(corner, corner + s)?;
|
||||
let b = if *operation == Operation::NewBody { b.with_box_certificate(f.id)? } else { b };
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
|
||||
@@ -2829,18 +3212,19 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::MeshImport { meshes, .. } => {
|
||||
for (name, b) in mesh_bodies(meshes)? {
|
||||
let name = unique_body_name(st, &name);
|
||||
for (k, (name, b)) in mesh_bodies(meshes)?.into_iter().enumerate() {
|
||||
let name = copy_body_name(st, f, k, &name);
|
||||
st.bodies.push(ModelBody::new(name, b, f.id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Import { step, .. } => {
|
||||
let imp = kernel::step_import_shared(step)?;
|
||||
let digest = crate::config::payload_digest(step);
|
||||
for (k, b) in imp.bodies.iter().enumerate() {
|
||||
let base = f.body_names.get(k).filter(|n| !n.trim().is_empty()).unwrap_or(&b.name);
|
||||
let name = unique_body_name(st, base);
|
||||
st.bodies.push(ModelBody::new(name, b.body.clone(), f.id));
|
||||
st.bodies.push(ModelBody::new(name, b.body.clone().with_import_certificate(&digest, f.id)?, f.id));
|
||||
}
|
||||
if !imp.warnings.is_empty() {
|
||||
let n = imp.warnings.len();
|
||||
@@ -2879,22 +3263,36 @@ fn unique_body_name(st: &ModelState, base: &str) -> String {
|
||||
/// face finds the face where it is shown and projects its edges there. The model itself when
|
||||
/// every placement agrees (the usual case).
|
||||
pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std::borrow::Cow<'a, ModelState> {
|
||||
state_in_frame_at(doc, st, comp, doc.component_transform(comp))
|
||||
}
|
||||
|
||||
/// As `state_in_frame`, using the exact active placement for its shared source geometry.
|
||||
pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, frame: Mat) -> std::borrow::Cow<'a, ModelState> {
|
||||
use std::borrow::Cow;
|
||||
if doc.occurrences.is_empty() && doc.body_offsets.is_empty() {
|
||||
return Cow::Borrowed(st);
|
||||
}
|
||||
let Some(inv) = crate::mat_inverse(&doc.component_transform(comp)) else { return Cow::Borrowed(st) };
|
||||
let Some(inv) = crate::mat_inverse(&frame) else { return Cow::Borrowed(st) };
|
||||
let comp_of_feature = |id: u64| doc.feature(id).map(|f| f.component).unwrap_or(0);
|
||||
let rel = |c: u64, body: Option<&str>| {
|
||||
let m = crate::mat_mul(&inv, &doc.component_transform(c));
|
||||
let m = crate::mat_mul(&inv, &if c == comp { frame } else { doc.component_transform(c) });
|
||||
match body.and_then(|b| doc.body_offsets.get(b)) {
|
||||
Some(o) => crate::mat_mul(&m, o),
|
||||
None => m,
|
||||
}
|
||||
};
|
||||
let body_m: Vec<Mat> = st.bodies.iter().map(|b| rel(doc.body_component(&b.name, b.feature), Some(&b.name))).collect();
|
||||
let body_m: Vec<Mat> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let m = rel(doc.model_body_component(b), None);
|
||||
doc.model_body_offset(b).map(|offset| crate::mat_mul(&m, offset)).unwrap_or(m)
|
||||
})
|
||||
.collect();
|
||||
let sketch_m: Vec<Mat> = st.sketches.iter().map(|s| rel(comp_of_feature(s.feature), None)).collect();
|
||||
if body_m.iter().chain(&sketch_m).all(crate::is_identity) {
|
||||
let world_wires_need_mapping = !crate::is_identity(&inv)
|
||||
&& st.sketches.iter().any(|sketch| sketch.sketch.wires.iter().any(|wire| wire.space == solvecraft_sketch::WireSpace::World));
|
||||
if body_m.iter().chain(&sketch_m).all(crate::is_identity) && !world_wires_need_mapping {
|
||||
return Cow::Borrowed(st);
|
||||
}
|
||||
let mut out = st.clone();
|
||||
@@ -2906,6 +3304,10 @@ pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std:
|
||||
let names = crate::naming::face_names(b);
|
||||
let mut nb = ModelBody::new(b.name.clone(), t, b.feature);
|
||||
nb.component = b.component;
|
||||
nb.transferred = b.transferred;
|
||||
nb.source_name = b.source_name.clone();
|
||||
nb.source_transform = crate::mat_mul(m, &b.source_transform);
|
||||
nb.imported = b.imported.clone();
|
||||
nb.names = crate::naming::NameCell::known(names);
|
||||
*b = nb;
|
||||
}
|
||||
@@ -2916,6 +3318,10 @@ pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std:
|
||||
{
|
||||
s.plane = np;
|
||||
}
|
||||
for wire in &mut s.sketch.wires {
|
||||
let matrix = if wire.space == solvecraft_sketch::WireSpace::Component { m } else { &inv };
|
||||
map_wire(wire, matrix);
|
||||
}
|
||||
}
|
||||
Cow::Owned(out)
|
||||
}
|
||||
@@ -2944,17 +3350,19 @@ pub fn world_state(doc: &Document, st: &ModelState) -> ModelState {
|
||||
let mut out = ModelState { body_counter: st.body_counter, ..Default::default() };
|
||||
for (comp, path, m) in doc.placements() {
|
||||
let primary = path == first_path(comp);
|
||||
let label = || -> String {
|
||||
path.iter().filter_map(|id| doc.occurrences.iter().find(|o| o.id == *id)).map(|o| o.name.as_str()).collect::<Vec<_>>().join("/")
|
||||
};
|
||||
let ident = crate::is_identity(&m);
|
||||
for b in st.bodies.iter().filter(|b| doc.body_component(&b.name, b.feature) == comp) {
|
||||
let name = if primary { b.name.clone() } else { format!("{} ({})", b.name, label()) };
|
||||
for b in st.bodies.iter().filter(|b| doc.model_body_component(b) == comp) {
|
||||
let name = doc.world_body_name(st, &b.name, comp, &path);
|
||||
// A body moved in from another component keeps its place (its offset).
|
||||
let bm = doc.body_offsets.get(&b.name).map(|o| crate::mat_mul(&m, o)).unwrap_or(m);
|
||||
let bm = doc.model_body_offset(b).map(|o| crate::mat_mul(&m, o)).unwrap_or(m);
|
||||
let body =
|
||||
if crate::is_identity(&bm) { b.body.clone() } else { kernel::transform_matrix(&b.body, bm).unwrap_or_else(|_| b.body.clone()) };
|
||||
let mut placed = ModelBody::new(name, body, b.feature);
|
||||
placed.source_name = Some(b.definition_name().to_string());
|
||||
placed.source_transform = crate::mat_mul(&bm, &b.source_transform);
|
||||
placed.component = comp;
|
||||
placed.transferred = b.transferred;
|
||||
placed.imported = b.imported.clone();
|
||||
// Placement preserves topology: face ownership remains that of the
|
||||
// source model, including modifiers on component instances.
|
||||
placed.names = crate::naming::NameCell::known(crate::naming::face_names(b));
|
||||
@@ -2969,12 +3377,22 @@ pub fn world_state(doc: &Document, st: &ModelState) -> ModelState {
|
||||
s2.plane = np;
|
||||
}
|
||||
}
|
||||
for wire in &mut s2.sketch.wires {
|
||||
if wire.space == solvecraft_sketch::WireSpace::Component {
|
||||
map_wire(wire, &m);
|
||||
}
|
||||
}
|
||||
out.sketches.push(s2);
|
||||
}
|
||||
for t in st.threads.iter().filter(|t| comp_of_feature(t.feature) == comp) {
|
||||
for t in st
|
||||
.threads
|
||||
.iter()
|
||||
.filter(|t| st.body(&t.body).map(|body| doc.model_body_component(body)).unwrap_or_else(|| comp_of_feature(t.feature)) == comp)
|
||||
{
|
||||
let mut t2 = t.clone();
|
||||
t2.axis_point = crate::apply_point(&m, t.axis_point);
|
||||
t2.axis = crate::apply_vector(&m, t.axis);
|
||||
let frame = st.body(&t.body).and_then(|body| doc.model_body_offset(body)).map(|offset| crate::mat_mul(&m, offset)).unwrap_or(m);
|
||||
t2.axis_point = crate::apply_point(&frame, t.axis_point);
|
||||
t2.axis = crate::apply_vector(&frame, t.axis);
|
||||
out.threads.push(t2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user