Sketch: associative, editable sketch patterns (#1103)
Rectangular and circular sketch patterns stay linked to their source: editing the pattern's count, spacing or the source geometry updates the copies, and the copies keep their point and radius relations. Patterns are saved under format 20; older designs keep their independent copies.
This commit is contained in:
1 parent
4cfa9c5d34
commit
9a4e8fbd19
20 files changed
+888
-55
No files matched your search
@@ -0,0 +1,227 @@
|
||||
//! Stored parameters and stable entity ids for editing an existing sketch pattern.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::Vec2;
|
||||
|
||||
use crate::{CurveKind, Sketch, SketchError};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum SketchPatternKind {
|
||||
Circular { center: String, count: usize, angle: f64 },
|
||||
Rectangular { dir: Vec2, count: usize, spacing: f64, count2: usize, spacing2: f64 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PatternInstance {
|
||||
pub index: [usize; 2],
|
||||
pub curves: Vec<String>,
|
||||
/// Seed point id and instance point id, in the order used to create the copy.
|
||||
pub points: Vec<(String, String)>,
|
||||
pub constraints: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SketchPattern {
|
||||
pub id: String,
|
||||
pub entities: Vec<String>,
|
||||
pub kind: SketchPatternKind,
|
||||
pub instances: Vec<PatternInstance>,
|
||||
}
|
||||
|
||||
impl SketchPattern {
|
||||
pub fn contains(&self, entity: &str) -> bool {
|
||||
self.entities.iter().any(|id| id == entity)
|
||||
|| self
|
||||
.instances
|
||||
.iter()
|
||||
.any(|instance| instance.curves.iter().any(|id| id == entity) || instance.points.iter().any(|(_, id)| id == entity))
|
||||
|| matches!(&self.kind, SketchPatternKind::Circular { center, .. } if center == entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sketch {
|
||||
/// Start an explicit Move/Scale of seed geometry with its copies already transformed.
|
||||
/// Interactive drags instead leave the ordinary relations to the held-point solver,
|
||||
/// which also supports dragging an instance back through its seed.
|
||||
pub fn follow_pattern_seeds(&mut self) {
|
||||
let constraint_indices: std::collections::BTreeMap<_, _> = self.constraints.iter().enumerate().map(|(i, c)| (c.id.as_str(), i)).collect();
|
||||
for pattern in &self.patterns {
|
||||
for instance in &pattern.instances {
|
||||
for cid in &instance.constraints {
|
||||
let Some(kind) = constraint_indices.get(cid.as_str()).and_then(|i| self.constraints.get(*i)).map(|c| c.kind.clone()) else {
|
||||
continue;
|
||||
};
|
||||
match kind {
|
||||
crate::ConstraintKind::PatternPoint { p, q, center, angle, offset } => {
|
||||
let Some(seed) = self.point(p) else { continue };
|
||||
let c = center.and_then(|c| self.point(c)).unwrap_or(Vec2::ZERO);
|
||||
let d = seed - c;
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
if let Some(copy) = self.points.get_mut(q) {
|
||||
copy.pos = c + Vec2::new(d.x * cos - d.y * sin, d.x * sin + d.y * cos) + offset;
|
||||
}
|
||||
}
|
||||
crate::ConstraintKind::PatternRadius { a, b } => {
|
||||
let r = match self.curves.get(a).map(|c| &c.kind) {
|
||||
Some(CurveKind::Circle { r, .. } | CurveKind::Ellipse { r, .. }) => Some(*r),
|
||||
_ => None,
|
||||
};
|
||||
if let (Some(r), Some(CurveKind::Circle { r: copy, .. } | CurveKind::Ellipse { r: copy, .. })) =
|
||||
(r, self.curves.get_mut(b).map(|c| &mut c.kind))
|
||||
{
|
||||
*copy = r;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.refresh_pattern_shapes();
|
||||
}
|
||||
/// Deleting a seed, instance or relation breaks that pattern's association. Remaining
|
||||
/// geometry stays in the sketch, without stale metadata that could resurrect a deletion.
|
||||
pub fn prune_patterns(&mut self) {
|
||||
if self.patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
let curves: std::collections::BTreeSet<_> = self.curves.iter().map(|c| c.id.as_str()).collect();
|
||||
let points: std::collections::BTreeSet<_> = self.points.iter().map(|p| p.id.as_str()).collect();
|
||||
let constraint_ids: std::collections::BTreeSet<_> = self.constraints.iter().map(|c| c.id.as_str()).collect();
|
||||
let retired: Vec<_> = self
|
||||
.patterns
|
||||
.iter()
|
||||
.filter(|pattern| {
|
||||
pattern.entities.iter().any(|id| !curves.contains(id.as_str()) && !points.contains(id.as_str()))
|
||||
|| matches!(&pattern.kind, SketchPatternKind::Circular { center, .. } if !points.contains(center.as_str()))
|
||||
|| pattern.instances.iter().any(|instance| {
|
||||
instance.curves.iter().any(|id| !curves.contains(id.as_str()))
|
||||
|| instance.points.iter().any(|(p, q)| !points.contains(p.as_str()) || !points.contains(q.as_str()))
|
||||
|| instance.constraints.iter().any(|id| !constraint_ids.contains(id.as_str()))
|
||||
})
|
||||
})
|
||||
.map(|pattern| pattern.id.clone())
|
||||
.collect();
|
||||
let constraints: std::collections::BTreeSet<_> =
|
||||
self.patterns.iter().filter(|p| retired.contains(&p.id)).flat_map(|p| &p.instances).flat_map(|i| i.constraints.iter().cloned()).collect();
|
||||
self.patterns.retain(|p| !retired.contains(&p.id));
|
||||
self.constraints.retain(|c| !constraints.contains(&c.id));
|
||||
}
|
||||
|
||||
/// Conic rho is an authored shape parameter rather than a solver unknown. Like its
|
||||
/// defining points, the copies follow the seed's shape when that parameter is edited.
|
||||
pub(crate) fn refresh_pattern_shapes(&mut self) {
|
||||
if !self.curves.iter().any(|c| matches!(c.kind, CurveKind::Conic { .. } | CurveKind::Arc { .. })) {
|
||||
return;
|
||||
}
|
||||
for pattern in &self.patterns {
|
||||
let sources: Vec<_> = pattern.entities.iter().filter_map(|id| self.curve_index(id)).collect();
|
||||
for instance in &pattern.instances {
|
||||
for (seed, copy) in sources.iter().zip(&instance.curves) {
|
||||
if let Some(CurveKind::Conic { rho, .. }) = self.curves.get(*seed).map(|c| &c.kind)
|
||||
&& let Some(copy) = self.curve_index(copy)
|
||||
{
|
||||
let rho = *rho;
|
||||
if let Some(CurveKind::Conic { rho: slot, .. }) = self.curves.get_mut(copy).map(|c| &mut c.kind) {
|
||||
*slot = rho;
|
||||
}
|
||||
}
|
||||
let Some(source) = self.curves.get(*seed).cloned() else { continue };
|
||||
if let CurveKind::Arc { c, a, b } = source.kind {
|
||||
let mapped = |p: usize| {
|
||||
self.points
|
||||
.get(p)
|
||||
.and_then(|p| instance.points.iter().find(|(id, _)| *id == p.id))
|
||||
.and_then(|(_, id)| self.point_index(id))
|
||||
};
|
||||
if let (Some(c), Some(a), Some(b), Some(copy)) = (mapped(c), mapped(a), mapped(b), self.curve_index(copy))
|
||||
&& let Some(curve) = self.curves.get_mut(copy)
|
||||
{
|
||||
curve.kind = CurveKind::Arc { c, a, b };
|
||||
curve.reversed = source.reversed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_patterns(&self) -> Result<(), SketchError> {
|
||||
if self.patterns.len() > crate::MAX_CONSTRAINTS {
|
||||
return Err(SketchError::TooLarge);
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
for pattern in &self.patterns {
|
||||
if pattern.id.is_empty()
|
||||
|| pattern.id.len() > 512
|
||||
|| !ids.insert(&pattern.id)
|
||||
|| pattern.entities.is_empty()
|
||||
|| pattern.entities.len() > crate::MAX_CURVES + crate::MAX_POINTS
|
||||
|| pattern.instances.len() >= 500
|
||||
{
|
||||
return Err(SketchError::Invalid("invalid sketch pattern".into()));
|
||||
}
|
||||
let valid_count = |n| (1..=500).contains(&n);
|
||||
let finite = |v: f64| v.is_finite() && v.abs() <= crate::MAX_COORD;
|
||||
let valid = match &pattern.kind {
|
||||
SketchPatternKind::Circular { count, angle, center } => {
|
||||
valid_count(*count) && angle.is_finite() && angle.abs() > 1e-9 && angle.abs() <= 360.0 && self.resolve_point(center).is_some()
|
||||
}
|
||||
SketchPatternKind::Rectangular { count, count2, spacing, spacing2, dir } => {
|
||||
valid_count(*count)
|
||||
&& valid_count(*count2)
|
||||
&& count.checked_mul(*count2).is_some_and(|n| n <= 500)
|
||||
&& finite(*spacing)
|
||||
&& finite(*spacing2)
|
||||
&& finite(dir.x)
|
||||
&& finite(dir.y)
|
||||
&& (dir.len() - 1.0).abs() < 1e-6
|
||||
}
|
||||
};
|
||||
if !valid {
|
||||
return Err(SketchError::Invalid("invalid sketch pattern settings".into()));
|
||||
}
|
||||
let (n1, n2) = match pattern.kind {
|
||||
SketchPatternKind::Circular { count, .. } => (count, 1),
|
||||
SketchPatternKind::Rectangular { count, count2, .. } => (count, count2),
|
||||
};
|
||||
if pattern.instances.len() != n1 * n2 - 1 {
|
||||
return Err(SketchError::Invalid("incomplete sketch pattern".into()));
|
||||
}
|
||||
let mut slots = std::collections::BTreeSet::new();
|
||||
for instance in &pattern.instances {
|
||||
let [i, j] = instance.index;
|
||||
let valid_id = |id: &String| !id.is_empty() && id.len() <= 512;
|
||||
if i >= n1
|
||||
|| j >= n2
|
||||
|| (i == 0 && j == 0)
|
||||
|| !slots.insert(instance.index)
|
||||
|| instance.points.len() > crate::MAX_POINTS
|
||||
|| instance.curves.len() > crate::MAX_CURVES
|
||||
|| instance.constraints.len() > crate::MAX_CONSTRAINTS
|
||||
|| instance.curves.iter().any(|id| !valid_id(id) || self.curve_index(id).is_none())
|
||||
|| instance
|
||||
.points
|
||||
.iter()
|
||||
.any(|(p, q)| !valid_id(p) || !valid_id(q) || self.point_index(p).is_none() || self.point_index(q).is_none())
|
||||
|| instance.constraints.iter().any(|id| {
|
||||
!valid_id(id)
|
||||
|| !self.constraints.iter().any(|c| {
|
||||
c.id == *id
|
||||
&& matches!(c.kind, crate::ConstraintKind::PatternPoint { .. } | crate::ConstraintKind::PatternRadius { .. })
|
||||
})
|
||||
})
|
||||
{
|
||||
return Err(SketchError::Invalid("invalid sketch pattern instance".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for constraint in &self.constraints {
|
||||
if matches!(constraint.kind, crate::ConstraintKind::PatternPoint { .. } | crate::ConstraintKind::PatternRadius { .. }) {
|
||||
self.validate(&constraint.kind)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user