Fix modelling regressions and reconcile WIP UI work
This commit is contained in:
1 parent
49bd89de13
commit
d6cdec40da
67 files changed
+1793
-260
No files matched your search
@@ -13,6 +13,7 @@ mod dims;
|
||||
mod linalg;
|
||||
mod link;
|
||||
mod model;
|
||||
mod offset;
|
||||
mod profiles;
|
||||
mod shape;
|
||||
mod solver;
|
||||
@@ -25,6 +26,7 @@ pub use curves::{
|
||||
pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_layout, encode_text};
|
||||
pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES};
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError, SketchView};
|
||||
pub use offset::OffsetChain;
|
||||
pub use profiles::{Profile, find_drawn_profiles, find_profiles, merge_regions};
|
||||
pub use shape::{Shape, intersections};
|
||||
pub use solver::{Hold, SolveReport, SolveStatus, solve, solve_holding};
|
||||
|
||||
@@ -104,6 +104,9 @@ pub struct Link {
|
||||
#[serde(default)]
|
||||
pub kind: LinkKind,
|
||||
pub source: LinkSource,
|
||||
/// Persistent model face identity, resolved by the document before geometry refresh.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub face_name: Option<String>,
|
||||
/// The reference could not be found on the last recompute; the geometry is the last good one.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub lost: bool,
|
||||
@@ -169,7 +172,7 @@ struct Layout {
|
||||
curves: Vec<LCurve>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum LCurve {
|
||||
Line(usize, usize),
|
||||
Circle(usize, f64),
|
||||
@@ -368,7 +371,7 @@ impl Sketch {
|
||||
return Err(SketchError::TooLarge);
|
||||
}
|
||||
let id = self.fresh("j");
|
||||
self.links.push(Link { id: id.clone(), kind, source, lost: false });
|
||||
self.links.push(Link { id: id.clone(), kind, source, face_name: None, lost: false });
|
||||
self.build_link(&id, &lay)?;
|
||||
Ok(id)
|
||||
}
|
||||
@@ -579,6 +582,77 @@ impl Sketch {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
// A resized face can enumerate the same boundary in another order. Match the
|
||||
// shared-point graph in normalized sketch space before discarding its identity.
|
||||
if let Some(cur) = &cur
|
||||
&& points.len() == lay.pts.len()
|
||||
&& cur.len() == lay.curves.len()
|
||||
&& points.len() <= 512
|
||||
{
|
||||
let old: Vec<Vec2> = points.iter().filter_map(|i| self.point(*i)).collect();
|
||||
let normalized = |ps: &[Vec2]| -> Vec<Vec2> {
|
||||
let lo = ps.iter().fold(Vec2::new(f64::INFINITY, f64::INFINITY), |a, p| Vec2::new(a.x.min(p.x), a.y.min(p.y)));
|
||||
let hi = ps.iter().fold(Vec2::new(f64::NEG_INFINITY, f64::NEG_INFINITY), |a, p| Vec2::new(a.x.max(p.x), a.y.max(p.y)));
|
||||
ps.iter().map(|p| Vec2::new((p.x - lo.x) / (hi.x - lo.x).max(1e-9), (p.y - lo.y) / (hi.y - lo.y).max(1e-9))).collect()
|
||||
};
|
||||
let (a, b) = (normalized(&old), normalized(&lay.pts));
|
||||
let mut mapping = Vec::new();
|
||||
for p in &a {
|
||||
let mut candidates: Vec<(usize, f64)> = b.iter().enumerate().map(|(i, q)| (i, p.dist(*q))).collect();
|
||||
candidates.sort_by(|x, y| x.1.total_cmp(&y.1));
|
||||
if let Some((i, d)) = candidates.first()
|
||||
&& *d < 0.1
|
||||
&& candidates.get(1).is_none_or(|(_, second)| *d < *second * 0.5)
|
||||
&& !mapping.contains(i)
|
||||
{
|
||||
mapping.push(*i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut used = Vec::new();
|
||||
let mut curve_map = Vec::new();
|
||||
if mapping.len() == points.len() {
|
||||
for c in cur {
|
||||
let mapped: Option<Vec<usize>> = curve_points(c).iter().map(|i| mapping.get(*i).copied()).collect();
|
||||
let found = lay
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(i, fresh)| {
|
||||
if used.contains(i) || !same_shape(c, fresh) {
|
||||
return false;
|
||||
}
|
||||
let fresh_pts = curve_points(fresh);
|
||||
mapped.as_ref().is_some_and(|p| {
|
||||
*p == fresh_pts || matches!(c, LCurve::Line(..)) && p.iter().rev().copied().collect::<Vec<_>>() == fresh_pts
|
||||
})
|
||||
})
|
||||
.map(|(i, _)| i);
|
||||
if let Some(i) = found {
|
||||
used.push(i);
|
||||
curve_map.push(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if curve_map.len() == cur.len() && mapping.len() == points.len() {
|
||||
for (pi, li) in points.iter().zip(mapping) {
|
||||
if let (Some(p), Some(q)) = (self.points.get_mut(*pi), lay.pts.get(li)) {
|
||||
p.pos = *q;
|
||||
}
|
||||
}
|
||||
for (ci, li) in curves.iter().zip(curve_map) {
|
||||
if let (Some(Curve { kind: CurveKind::Circle { r, .. }, .. }), Some(LCurve::Circle(_, value))) =
|
||||
(self.curves.get_mut(*ci), lay.curves.get(li))
|
||||
{
|
||||
*r = *value;
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
// Rebuild: drop the old entities (constraints on them go too) and make new ones.
|
||||
self.drop_link_entities(id);
|
||||
self.build_link(id, &lay)?;
|
||||
|
||||
@@ -467,6 +467,8 @@ pub struct Sketch {
|
||||
/// Links to geometry outside the sketch (projections, intersections, includes).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub links: Vec<crate::link::Link>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub offsets: Vec<crate::offset::OffsetChain>,
|
||||
/// 3D curves (world coordinates) carried by the sketch.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub wires: Vec<crate::wire::Wire>,
|
||||
@@ -495,6 +497,7 @@ impl Sketch {
|
||||
curves: Vec::new(),
|
||||
constraints: Vec::new(),
|
||||
links: Vec::new(),
|
||||
offsets: Vec::new(),
|
||||
wires: Vec::new(),
|
||||
counters: Default::default(),
|
||||
view: SketchView::default(),
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
//! Associative offsets keep their output entity ids while their source curves move.
|
||||
use crate::{CurveKind, Shape, Sketch, SketchError, intersections};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solvecraft_geom::Vec2;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OffsetChain {
|
||||
pub sources: Vec<(String, bool)>,
|
||||
pub outputs: Vec<String>,
|
||||
pub distance: f64,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl Sketch {
|
||||
pub fn refresh_offsets(&mut self) -> Result<(), SketchError> {
|
||||
for offset in self.offsets.clone() {
|
||||
if offset.sources.len() != offset.outputs.len() || offset.sources.len() > 10_000 || !offset.distance.is_finite() {
|
||||
return Err(SketchError::Invalid("damaged offset chain".into()));
|
||||
}
|
||||
let bad = || SketchError::Invalid("an offset source vanished or changed shape".into());
|
||||
let mut pieces = Vec::new();
|
||||
for (id, forward) in &offset.sources {
|
||||
let i = self.curve_index(id).ok_or_else(bad)?;
|
||||
let kind = &self.curves.get(i).ok_or_else(bad)?.kind;
|
||||
let piece = match *kind {
|
||||
CurveKind::Line { a, b } => {
|
||||
let (a, b) = (self.point(a).ok_or_else(bad)?, self.point(b).ok_or_else(bad)?);
|
||||
let (a, b) = if *forward { (a, b) } else { (b, a) };
|
||||
let n = (b - a).normalized().ok_or_else(bad)?.perp() * offset.distance;
|
||||
(Shape::Line { a: a + n, b: b + n }, a + n, b + n)
|
||||
}
|
||||
CurveKind::Arc { c, a, b } => {
|
||||
let (c, a, b) = (self.point(c).ok_or_else(bad)?, self.point(a).ok_or_else(bad)?, self.point(b).ok_or_else(bad)?);
|
||||
let r = c.dist(a) - if *forward { offset.distance } else { -offset.distance };
|
||||
if r <= 1e-9 {
|
||||
return Err(SketchError::Invalid("the offset collapses an arc".into()));
|
||||
}
|
||||
let a = c + (a - c).normalized().ok_or_else(bad)? * r;
|
||||
let b = c + (b - c).normalized().ok_or_else(bad)? * r;
|
||||
let (a, b) = if *forward { (a, b) } else { (b, a) };
|
||||
(Shape::Round { c, r, start: 0.0, sweep: std::f64::consts::TAU }, a, b)
|
||||
}
|
||||
CurveKind::Circle { c, r } => {
|
||||
let c = self.point(c).ok_or_else(bad)?;
|
||||
let r = r - offset.distance;
|
||||
if r <= 1e-9 {
|
||||
return Err(SketchError::Invalid("the offset collapses a circle".into()));
|
||||
}
|
||||
let out = offset.outputs.first().and_then(|id| self.curve_index(id)).ok_or_else(bad)?;
|
||||
if let Some(crate::Curve { kind: CurveKind::Circle { r: target, .. }, .. }) = self.curves.get_mut(out) {
|
||||
*target = r;
|
||||
} else {
|
||||
return Err(bad());
|
||||
}
|
||||
let _ = c;
|
||||
continue;
|
||||
}
|
||||
_ => return Err(bad()),
|
||||
};
|
||||
pieces.push(piece);
|
||||
}
|
||||
if pieces.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let joint = |i: usize, j: usize| -> Result<Vec2, SketchError> {
|
||||
let (x, y) = (pieces.get(i).ok_or_else(bad)?, pieces.get(j).ok_or_else(bad)?);
|
||||
if x.2.dist(y.1) < 1e-6 {
|
||||
return Ok(x.2);
|
||||
}
|
||||
let guess = (x.2 + y.1) * 0.5;
|
||||
intersections(&x.0, &y.0)
|
||||
.into_iter()
|
||||
.min_by(|a, b| a.dist(guess).total_cmp(&b.dist(guess)))
|
||||
.ok_or_else(|| SketchError::Invalid("the offset curves no longer meet".into()))
|
||||
};
|
||||
let n = pieces.len();
|
||||
let mut joints = vec![if offset.closed { joint(n - 1, 0)? } else { pieces.first().ok_or_else(bad)?.1 }];
|
||||
for i in 0..n.saturating_sub(1) {
|
||||
joints.push(joint(i, i + 1)?);
|
||||
}
|
||||
if !offset.closed {
|
||||
joints.push(pieces.last().ok_or_else(bad)?.2);
|
||||
}
|
||||
for (i, id) in offset.outputs.iter().enumerate() {
|
||||
let a = *joints.get(i).ok_or_else(bad)?;
|
||||
let b = *joints.get(if offset.closed { (i + 1) % n } else { i + 1 }).ok_or_else(bad)?;
|
||||
let shape = &pieces.get(i).ok_or_else(bad)?.0;
|
||||
if let Shape::Line { a: p, b: q } = shape
|
||||
&& (b - a).dot(*q - *p) <= 1e-9
|
||||
{
|
||||
return Err(SketchError::Invalid("the offset curves would cross".into()));
|
||||
}
|
||||
let ci = self.curve_index(id).ok_or_else(bad)?;
|
||||
let kind = self.curves.get(ci).ok_or_else(bad)?.kind.clone();
|
||||
let (pa, pb) = match kind {
|
||||
CurveKind::Line { a, b } => (a, b),
|
||||
CurveKind::Arc { a, b, .. } => {
|
||||
if offset.sources.get(i).is_some_and(|x| x.1) {
|
||||
(a, b)
|
||||
} else {
|
||||
(b, a)
|
||||
}
|
||||
}
|
||||
_ => return Err(bad()),
|
||||
};
|
||||
for (pi, pos) in [(pa, a), (pb, b)] {
|
||||
if let Some(p) = self.points.get_mut(pi) {
|
||||
p.pos = pos;
|
||||
p.fixed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl Sketch {
|
||||
}
|
||||
let id = if geom.is_empty() {
|
||||
let id = self.fresh("j");
|
||||
self.links.push(Link { id: id.clone(), kind, source, lost: false });
|
||||
self.links.push(Link { id: id.clone(), kind, source, face_name: None, lost: false });
|
||||
id
|
||||
} else {
|
||||
self.add_link(kind, source, geom)?
|
||||
|
||||
Reference in new issue
Block a user