Feat/Sketch: associative pattern tools with independent directions and Flip/Center controls (#1601)
Thanks @Karl-VonRichter.
This commit is contained in:
1 parent
44968dbfe6
commit
371578a06f
36 files changed
+1394
-87
No files matched your search
@@ -52,7 +52,7 @@ pub use model::{
|
||||
};
|
||||
pub use offset::OffsetChain;
|
||||
pub use offset_topology::{OffsetCreated, OffsetRole, OffsetRoleFlags, OffsetRoleKind, OffsetTopology};
|
||||
pub use pattern::{PatternInstance, SketchPattern, SketchPatternKind};
|
||||
pub use pattern::{PatternAlignment, PatternInstance, SketchPattern, SketchPatternKind};
|
||||
pub use periodic::periodic_beziers;
|
||||
pub use profiles::{
|
||||
CurvePick, Profile, distinct_curves, find_drawn_profiles, find_profiles, merge_regions, profile_at_point, profile_by_curves,
|
||||
|
||||
@@ -189,6 +189,9 @@ pub enum ConstraintKind {
|
||||
center: Option<usize>,
|
||||
angle: f64,
|
||||
offset: Vec2,
|
||||
/// Displacements along and perpendicular to live sketch lines (at most two).
|
||||
#[serde(default)]
|
||||
directions: [Option<(usize, Vec2)>; 2],
|
||||
},
|
||||
/// The independent radius of a patterned circle or ellipse follows its seed.
|
||||
PatternRadius {
|
||||
@@ -423,7 +426,9 @@ impl ConstraintKind {
|
||||
pub fn indices(&self) -> (Vec<usize>, Vec<usize>) {
|
||||
use ConstraintKind::*;
|
||||
match *self {
|
||||
PatternPoint { p, q, center, .. } => (vec![p, q].into_iter().chain(center).collect(), vec![]),
|
||||
PatternPoint { p, q, center, directions, .. } => {
|
||||
(vec![p, q].into_iter().chain(center).collect(), directions.into_iter().flatten().map(|(l, _)| l).collect())
|
||||
}
|
||||
PatternRadius { a, b } => (vec![], vec![a, b]),
|
||||
Coincident { p, q } | HorizontalPoints { p, q } | VerticalPoints { p, q } => (vec![p, q], vec![]),
|
||||
Distance { p, q, .. } | DistanceX { p, q, .. } | DistanceY { p, q, .. } => (vec![p, q], vec![]),
|
||||
@@ -458,12 +463,15 @@ impl ConstraintKind {
|
||||
None => ok2 = false,
|
||||
};
|
||||
match self {
|
||||
PatternPoint { p, q, center, .. } => {
|
||||
PatternPoint { p, q, center, directions, .. } => {
|
||||
fp(p);
|
||||
fp(q);
|
||||
if let Some(c) = center {
|
||||
fp(c);
|
||||
}
|
||||
for (l, _) in directions.iter_mut().flatten() {
|
||||
fc(l);
|
||||
}
|
||||
}
|
||||
PatternRadius { a, b } => {
|
||||
fc(a);
|
||||
@@ -848,7 +856,13 @@ impl Sketch {
|
||||
let need_line = |i: usize| if is_line(i) { Ok(()) } else { Err(SketchError::WrongKind(name(i), "needs a line".into())) };
|
||||
let need_round = |i: usize| if is_round(i) { Ok(()) } else { Err(SketchError::WrongKind(name(i), "needs a circle or arc".into())) };
|
||||
match *k {
|
||||
PatternPoint { p, q, angle, offset, .. } => {
|
||||
PatternPoint { p, q, angle, offset, directions, .. } => {
|
||||
for (l, distance) in directions.into_iter().flatten() {
|
||||
need_line(l)?;
|
||||
if !distance.x.is_finite() || !distance.y.is_finite() || distance.x.abs() > MAX_COORD || distance.y.abs() > MAX_COORD {
|
||||
return Err(SketchError::Invalid("invalid pattern direction displacement".into()));
|
||||
}
|
||||
}
|
||||
if p == q
|
||||
|| !angle.is_finite()
|
||||
|| angle.abs() > std::f64::consts::TAU
|
||||
|
||||
@@ -5,11 +5,62 @@ use solidcraft_geom::Vec2;
|
||||
|
||||
use crate::{CurveKind, Sketch, SketchError};
|
||||
|
||||
/// Placement relative to the original, along a pattern direction's oriented line.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PatternAlignment {
|
||||
Left,
|
||||
Center,
|
||||
#[default]
|
||||
Right,
|
||||
}
|
||||
|
||||
impl PatternAlignment {
|
||||
/// Slot zero is always the original. Center alternates sides to retain stable copy ids.
|
||||
pub fn multiplier(self, index: usize, reversed: bool) -> f64 {
|
||||
let value = match self {
|
||||
Self::Left => -(index as f64),
|
||||
Self::Right => index as f64,
|
||||
Self::Center if index.is_multiple_of(2) => -((index / 2) as f64),
|
||||
Self::Center => index.div_ceil(2) as f64,
|
||||
};
|
||||
if reversed { -value } else { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[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 },
|
||||
Circular {
|
||||
center: String,
|
||||
count: usize,
|
||||
angle: f64,
|
||||
#[serde(default)]
|
||||
alignment: PatternAlignment,
|
||||
#[serde(default)]
|
||||
reversed: bool,
|
||||
},
|
||||
Rectangular {
|
||||
dir: Vec2,
|
||||
#[serde(default)]
|
||||
dir2: Option<Vec2>,
|
||||
#[serde(default)]
|
||||
direction: Option<String>,
|
||||
#[serde(default)]
|
||||
direction2: Option<String>,
|
||||
count: usize,
|
||||
spacing: f64,
|
||||
count2: usize,
|
||||
spacing2: f64,
|
||||
#[serde(default)]
|
||||
alignment: PatternAlignment,
|
||||
#[serde(default)]
|
||||
alignment2: PatternAlignment,
|
||||
#[serde(default)]
|
||||
reversed: bool,
|
||||
#[serde(default)]
|
||||
reversed2: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -37,6 +88,8 @@ impl SketchPattern {
|
||||
.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)
|
||||
|| matches!(&self.kind, SketchPatternKind::Rectangular { direction, direction2, .. }
|
||||
if direction.as_deref() == Some(entity) || direction2.as_deref() == Some(entity))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +106,14 @@ impl Sketch {
|
||||
continue;
|
||||
};
|
||||
match kind {
|
||||
crate::ConstraintKind::PatternPoint { p, q, center, angle, offset } => {
|
||||
crate::ConstraintKind::PatternPoint { p, q, center, angle, mut offset, directions } => {
|
||||
for (l, distance) in directions.into_iter().flatten() {
|
||||
if let Some(crate::Shape::Line { a, b }) = self.shape(l)
|
||||
&& let Some(u) = (b - a).normalized()
|
||||
{
|
||||
offset += u * distance.x + u.perp() * distance.y;
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -95,6 +155,8 @@ impl Sketch {
|
||||
.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()))
|
||||
|| matches!(&pattern.kind, SketchPatternKind::Rectangular { direction, direction2, .. }
|
||||
if direction.iter().chain(direction2).any(|id| !curves.contains(id.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()))
|
||||
@@ -165,10 +227,10 @@ impl Sketch {
|
||||
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 } => {
|
||||
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 } => {
|
||||
SketchPatternKind::Rectangular { count, count2, spacing, spacing2, dir, dir2, direction, direction2, .. } => {
|
||||
valid_count(*count)
|
||||
&& valid_count(*count2)
|
||||
&& count.checked_mul(*count2).is_some_and(|n| n <= 500)
|
||||
@@ -177,6 +239,10 @@ impl Sketch {
|
||||
&& finite(dir.x)
|
||||
&& finite(dir.y)
|
||||
&& (dir.len() - 1.0).abs() < 1e-6
|
||||
&& dir2.is_none_or(|d| finite(d.x) && finite(d.y) && (d.len() - 1.0).abs() < 1e-6)
|
||||
&& direction.iter().chain(direction2).all(|id| {
|
||||
self.curve_index(id).is_some_and(|l| matches!(self.curves.get(l).map(|c| &c.kind), Some(CurveKind::Line { .. })))
|
||||
})
|
||||
}
|
||||
};
|
||||
if !valid {
|
||||
|
||||
@@ -400,7 +400,12 @@ fn line_dist(p: Vec2, a: Vec2, b: Vec2) -> f64 {
|
||||
fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||||
use ConstraintKind::*;
|
||||
match *k {
|
||||
PatternPoint { p, q, center, angle, offset } => {
|
||||
PatternPoint { p, q, center, angle, mut offset, directions } => {
|
||||
for (l, distance) in directions.into_iter().flatten() {
|
||||
let (a, b) = s.line(l);
|
||||
let u = (b - a).normalized().unwrap_or(Vec2::ZERO);
|
||||
offset += u * distance.x + u.perp() * distance.y;
|
||||
}
|
||||
let c = center.map(|c| s.p(c)).unwrap_or(Vec2::ZERO);
|
||||
let d = s.p(p) - c;
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
@@ -775,7 +780,9 @@ fn block_refs(sk: &Sketch, b: &Block) -> Vec<Ref> {
|
||||
Block::Closure { a, b } => vec![P(*a), P(*b)],
|
||||
Block::Arc { c, a, b } => vec![P(*c), P(*a), P(*b)],
|
||||
Block::User(_, k) => match **k {
|
||||
PatternPoint { p, q, center, .. } => vec![P(p), P(q)].into_iter().chain(center.map(P)).collect(),
|
||||
PatternPoint { p, q, center, directions, .. } => {
|
||||
vec![P(p), P(q)].into_iter().chain(center.map(P)).chain(directions.into_iter().flatten().map(|(l, _)| L(l))).collect()
|
||||
}
|
||||
PatternRadius { a, b } => vec![C(a), C(b)],
|
||||
Coincident { p, q }
|
||||
| HorizontalPoints { p, q }
|
||||
|
||||
Reference in new issue
Block a user