Guide sketch patterns with live previews and editable offsets (#966)
Sketch Mirror, rectangular and circular Pattern, and Offset now show a green live preview that follows the selection and settings; OK commits, Cancel leaves the sketch unchanged. The shared dialogs show the current selection step and source counts, sources can be toggled with Ctrl/Cmd-click, cleared and reselected. Offsets keep editable driving distance dimensions (One Side, Two Sides, Symmetric) that update the derived curves without moving the source, with undo/redo and save/reopen. Format 19: older files keep their fixed offsets. New dialog labels in en, zh, ja, de and ko. Fixes #696. Fixes #517.
This commit is contained in:
1 parent
9c51f9d433
commit
47a7f29485
29 files changed
+1107
-186
No files matched your search
@@ -50,6 +50,10 @@ pub fn dim_frame(sk: &Sketch, k: &ConstraintKind) -> Option<DimFrame> {
|
||||
use ConstraintKind::*;
|
||||
let unit = |v: Vec2, or: Vec2| v.normalized().unwrap_or(or);
|
||||
Some(match *k {
|
||||
OffsetDistance { ref output, .. } => {
|
||||
let (p0, p1) = sk.offset_dimension_points(output)?;
|
||||
DimFrame::Linear { p0, p1, dir: unit(p1 - p0, Vec2::X) }
|
||||
}
|
||||
Distance { p, q, .. } => {
|
||||
let (a, b) = (sk.point(p)?, sk.point(q)?);
|
||||
DimFrame::Linear { p0: a, p1: b, dir: unit(b - a, Vec2::X) }
|
||||
|
||||
@@ -45,7 +45,9 @@ pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_
|
||||
pub use fonts::{DEFAULT_FONT_FAMILY, FontData, families_in, find_style_in, font_data, font_data_styled, font_file_path, installed_families};
|
||||
pub use freeform_offset::{FreeformOffset, OFFSET_TOLERANCE};
|
||||
pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES, SKETCH_AXIS_HALF, sketch_axis_geom};
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, MAX_CONSTRAINTS, MAX_CURVES, MAX_POINTS, SPoint, Sketch, SketchError, SketchView};
|
||||
pub use model::{
|
||||
Constraint, ConstraintKind, Curve, CurveKind, MAX_CONSTRAINTS, MAX_COORD, MAX_CURVES, MAX_POINTS, SPoint, Sketch, SketchError, SketchView,
|
||||
};
|
||||
pub use offset::OffsetChain;
|
||||
pub use offset_topology::{OffsetCreated, OffsetRole, OffsetRoleFlags, OffsetRoleKind, OffsetTopology};
|
||||
pub use periodic::periodic_beziers;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub const MAX_CONSTRAINTS: usize = 40_000;
|
||||
pub const MAX_CURVES: usize = MAX_POINTS;
|
||||
|
||||
/// Largest coordinate or dimension value (mm) a sketch accepts.
|
||||
pub(crate) const MAX_COORD: f64 = 1e9;
|
||||
pub const MAX_COORD: f64 = 1e9;
|
||||
|
||||
/// A position a sketch accepts: finite and within [`MAX_COORD`].
|
||||
pub(crate) fn in_range(p: Vec2) -> bool {
|
||||
@@ -310,6 +310,15 @@ pub enum ConstraintKind {
|
||||
l: usize,
|
||||
value: f64,
|
||||
},
|
||||
/// Normal distance of an associative offset. Its output id remains stable
|
||||
/// when a curved boundary retires a corner; refresh_offsets drives the geometry.
|
||||
OffsetDistance {
|
||||
output: String,
|
||||
value: f64,
|
||||
/// Keep the chosen side when the positive driving length is edited.
|
||||
#[serde(default)]
|
||||
reverse: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ConstraintKind {
|
||||
@@ -331,6 +340,7 @@ impl ConstraintKind {
|
||||
| Diameter { value, .. }
|
||||
| ArcLength { value, .. }
|
||||
| LinearDiameter { value, .. }
|
||||
| OffsetDistance { value, .. }
|
||||
| Angle { value, .. } => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
@@ -350,6 +360,7 @@ impl ConstraintKind {
|
||||
| Diameter { value, .. }
|
||||
| ArcLength { value, .. }
|
||||
| LinearDiameter { value, .. }
|
||||
| OffsetDistance { value, .. }
|
||||
| Angle { value, .. } => *value = v,
|
||||
_ => {}
|
||||
}
|
||||
@@ -381,6 +392,7 @@ impl ConstraintKind {
|
||||
Smooth { .. } => "Smooth",
|
||||
ArcLength { .. } => "ArcLengthDimension",
|
||||
LinearDiameter { .. } => "LinearDiameterDimension",
|
||||
OffsetDistance { .. } => "OffsetDimension",
|
||||
}
|
||||
}
|
||||
/// Does the constraint refer to point `p` (by index)?
|
||||
@@ -406,6 +418,7 @@ impl ConstraintKind {
|
||||
Smooth { a, b } => (vec![], vec![a, b]),
|
||||
ArcLength { c, .. } => (vec![], vec![c]),
|
||||
LinearDiameter { p, l, .. } => (vec![p], vec![l]),
|
||||
OffsetDistance { .. } => (vec![], vec![]),
|
||||
}
|
||||
}
|
||||
pub(crate) fn remap(&mut self, pmap: &dyn Fn(usize) -> Option<usize>, cmap: &dyn Fn(usize) -> Option<usize>) -> bool {
|
||||
@@ -462,6 +475,7 @@ impl ConstraintKind {
|
||||
fp(p);
|
||||
fc(l);
|
||||
}
|
||||
OffsetDistance { .. } => {}
|
||||
}
|
||||
ok && ok2
|
||||
}
|
||||
@@ -868,6 +882,11 @@ impl Sketch {
|
||||
{
|
||||
return Err(SketchError::Invalid(format!("dimension value {v}")));
|
||||
}
|
||||
if let OffsetDistance { output, value, .. } = k
|
||||
&& (*value <= 1e-9 || output.len() > 512 || self.offset_distance(output).is_none())
|
||||
{
|
||||
return Err(SketchError::Invalid("offset dimension needs a nonzero distance and an existing association".into()));
|
||||
}
|
||||
if let ArcSweep { value, .. } = *k
|
||||
&& !(value.abs() > 1e-9 && value.abs() < std::f64::consts::TAU - 1e-9)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Associative offsets keep their output entity ids while their source curves move.
|
||||
use crate::{CurveKind, Shape, Sketch, SketchError, intersections};
|
||||
use crate::{ConstraintKind, CurveKind, Shape, Sketch, SketchError, intersections};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::Vec2;
|
||||
|
||||
@@ -13,7 +13,37 @@ pub struct OffsetChain {
|
||||
pub topology: Option<crate::offset_topology::OffsetTopology>,
|
||||
}
|
||||
|
||||
impl OffsetChain {
|
||||
/// Includes reserved corner roles that may temporarily disappear from the boundary.
|
||||
pub fn owns_output(&self, output: &str) -> bool {
|
||||
self.outputs.iter().any(|id| id == output) || self.topology.as_ref().is_some_and(|t| t.roles.iter().any(|r| r.output == output))
|
||||
}
|
||||
}
|
||||
|
||||
impl Sketch {
|
||||
/// The signed distance of the association that owns an output (including retired roles).
|
||||
pub fn offset_distance(&self, output: &str) -> Option<f64> {
|
||||
self.offsets
|
||||
.iter()
|
||||
.find(|o| o.owns_output(output))
|
||||
.map(|o| o.distance)
|
||||
.or_else(|| self.freeform_offsets.iter().find(|o| o.output == output).map(|o| o.distance))
|
||||
}
|
||||
|
||||
/// Endpoints of a normal-distance annotation, evaluated from its source curve.
|
||||
pub fn offset_dimension_points(&self, output: &str) -> Option<(Vec2, Vec2)> {
|
||||
let (source, forward, distance) = self
|
||||
.offsets
|
||||
.iter()
|
||||
.find(|o| o.owns_output(output))
|
||||
.and_then(|o| o.sources.first().map(|(id, forward)| (id.as_str(), *forward, o.distance)))
|
||||
.or_else(|| self.freeform_offsets.iter().find(|o| o.output == output).map(|o| (o.source.as_str(), o.forward, o.distance)))?;
|
||||
let index = self.curve_index(source)?;
|
||||
let segment = self.exact_segs(index).unwrap_or_else(|| self.segs(index)).into_iter().next()?;
|
||||
let p = segment.point_at(0.5);
|
||||
let tangent = (segment.point_at(0.5001) - segment.point_at(0.4999)).normalized()?;
|
||||
Some((p, p + tangent.perp() * if forward { distance } else { -distance }))
|
||||
}
|
||||
/// Removing an output removes its relationship. A partial analytic chain
|
||||
/// deletion releases the surviving outputs, while retained dependents keep
|
||||
/// their source references so the command's refresh refuses broken sources.
|
||||
@@ -26,11 +56,12 @@ impl Sketch {
|
||||
.offsets
|
||||
.iter()
|
||||
.filter(|offset| offset.outputs.iter().any(|id| removed.contains(id)))
|
||||
.flat_map(|offset| offset.outputs.iter().cloned())
|
||||
.flat_map(|offset| offset.outputs.iter().cloned().chain(offset.topology.iter().flat_map(|t| t.roles.iter().map(|r| r.output.clone()))))
|
||||
.chain(self.freeform_offsets.iter().filter(|offset| removed.contains(&offset.output)).map(|offset| offset.output.clone()))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
self.offsets.retain(|offset| !offset.outputs.iter().any(|id| removed.contains(id)));
|
||||
self.freeform_offsets.retain(|offset| !removed.contains(&offset.output));
|
||||
self.constraints.retain(|c| !matches!(&c.kind, ConstraintKind::OffsetDistance { output, .. } if released.contains(output)));
|
||||
let points = self
|
||||
.curves
|
||||
.iter()
|
||||
@@ -52,6 +83,27 @@ impl Sketch {
|
||||
return Ok(());
|
||||
}
|
||||
let mut candidate = self.clone();
|
||||
let mut driven = std::collections::BTreeSet::new();
|
||||
for constraint in &self.constraints {
|
||||
if let ConstraintKind::OffsetDistance { output, value, reverse } = &constraint.kind {
|
||||
self.validate(&constraint.kind)?;
|
||||
let analytic = candidate.offsets.iter().position(|o| o.owns_output(output));
|
||||
let freeform = candidate.freeform_offsets.iter().position(|o| o.output == *output);
|
||||
let identity = analytic
|
||||
.or_else(|| freeform.map(|i| candidate.offsets.len() + i))
|
||||
.ok_or_else(|| SketchError::Invalid("missing offset dimension association".into()))?;
|
||||
if constraint.driven || !driven.insert(identity) {
|
||||
return Err(SketchError::Invalid("an offset must have exactly one driving distance".into()));
|
||||
}
|
||||
let distance = if *reverse { -*value } else { *value };
|
||||
if let Some(offset) = analytic.and_then(|i| candidate.offsets.get_mut(i)) {
|
||||
offset.distance = distance;
|
||||
}
|
||||
if let Some(offset) = freeform.and_then(|i| candidate.freeform_offsets.get_mut(i)) {
|
||||
offset.distance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
candidate.refresh_analytic_offsets()?;
|
||||
candidate.refresh_freeform_offsets()?;
|
||||
*self = candidate;
|
||||
|
||||
@@ -829,6 +829,7 @@ impl Sketch {
|
||||
}
|
||||
}
|
||||
candidate.offsets.remove(index);
|
||||
candidate.constraints.retain(|c| !matches!(&c.kind, crate::ConstraintKind::OffsetDistance { output, .. } if record.owns_output(output)));
|
||||
for point in owned {
|
||||
if point != 0
|
||||
&& candidate.curves.iter().all(|c| !c.kind.uses(point) || outputs.contains(c.id.as_str()))
|
||||
|
||||
@@ -400,6 +400,8 @@ fn line_dist(p: Vec2, a: Vec2, b: Vec2) -> f64 {
|
||||
fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||||
use ConstraintKind::*;
|
||||
match *k {
|
||||
// Offset distances drive derived geometry in refresh_offsets, never source DOFs.
|
||||
OffsetDistance { .. } => {}
|
||||
Coincident { p, q } => {
|
||||
let d = s.p(p) - s.p(q);
|
||||
out.extend([d.x, d.y]);
|
||||
@@ -782,7 +784,7 @@ fn block_refs(sk: &Sketch, b: &Block) -> Vec<Ref> {
|
||||
Tangent { a, b } => tangent(a, b).to_vec(),
|
||||
Smooth { a, b } => vec![C(a), C(b)],
|
||||
Symmetric { p, q, l } => vec![P(p), P(q), L(l)],
|
||||
Fix { .. } => vec![],
|
||||
Fix { .. } | OffsetDistance { .. } => vec![],
|
||||
},
|
||||
Block::TangentAt(_, k, p) => match **k {
|
||||
ConstraintKind::Tangent { a, b } if freeform(a) || freeform(b) => vec![P(*p), C(a), C(b)],
|
||||
@@ -1343,7 +1345,7 @@ fn build_blocks<'a>(sk: &'a Sketch, lay: &Layout, on_curve: &[(usize, usize)]) -
|
||||
}
|
||||
}
|
||||
for (i, c) in sk.constraints.iter().enumerate() {
|
||||
if c.driven {
|
||||
if c.driven || matches!(c.kind, ConstraintKind::OffsetDistance { .. }) {
|
||||
continue;
|
||||
}
|
||||
raw.push(match touch_point(sk, &c.kind, on_curve) {
|
||||
|
||||
Reference in new issue
Block a user