mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 03:46:53 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae80124cb |
@@ -358,6 +358,56 @@ fn components<'a>(cons: &[&'a Constraint]) -> Vec<Vec<&'a Constraint>> {
|
||||
groups.into_values().collect()
|
||||
}
|
||||
|
||||
/// The solver weight of each unknown of `model`: kept handles weigh more, preferred-to-move ones
|
||||
/// less, angles and ratios by size², and contact parameters move freely.
|
||||
fn solver_weights(model: &Model, opts: &SolveOptions) -> Vec<f64> {
|
||||
let mut weights = vec![1.0; model.x0.len()];
|
||||
for s in &model.slots {
|
||||
let w = if opts.keep.contains(&s.handle) {
|
||||
1e4
|
||||
} else if opts.prefer_move.contains(&s.handle) {
|
||||
1e-2
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
for j in s.off..s.off + s.len {
|
||||
if let Some(slot) = weights.get_mut(j) {
|
||||
*slot = w;
|
||||
}
|
||||
}
|
||||
for (j, f) in slot_scales(model, s) {
|
||||
if let Some(slot) = weights.get_mut(j) {
|
||||
*slot *= f;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Contact parameters move freely.
|
||||
let geo = model.slots.iter().map(|s| s.off + s.len).max().unwrap_or(0);
|
||||
for w in weights.iter_mut().skip(geo) {
|
||||
*w = 1e-3;
|
||||
}
|
||||
weights
|
||||
}
|
||||
|
||||
/// Per-unknown factors for the angles and ratios of one slot: size², so that the movement of
|
||||
/// points is what is minimised.
|
||||
fn slot_scales(model: &Model, s: &model::Slot) -> Vec<(usize, f64)> {
|
||||
let sq = |j: usize| model.x0.get(j).map_or(1.0, |r| (r * r).max(1e-6));
|
||||
match s.shape {
|
||||
model::Shape::Arc => vec![(s.off + 3, sq(s.off + 2)), (s.off + 4, sq(s.off + 2))],
|
||||
model::Shape::Ellipse { arc } => {
|
||||
let a2 = (sq(s.off + 2) + sq(s.off + 3)).max(1e-6);
|
||||
let mut v = vec![(s.off + 4, a2)];
|
||||
if arc {
|
||||
v.extend([(s.off + 5, a2), (s.off + 6, a2)]);
|
||||
}
|
||||
v
|
||||
}
|
||||
model::Shape::Insert { .. } => vec![(s.off + 2, model.rigid_extent2(s.handle).max(1.0))],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the component systems. Constraints that cannot be interpreted are returned as skipped.
|
||||
pub(crate) fn build(d: &Drawing, cons: &[&Constraint], opts: &SolveOptions) -> Result<(Vec<Comp>, Vec<(u32, String)>), SolveError> {
|
||||
let mut members_left = model::MAX_RIGID_WORK;
|
||||
@@ -396,46 +446,7 @@ pub(crate) fn build_budgeted(
|
||||
if built.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut weights = vec![1.0; model.x0.len()];
|
||||
for s in &model.slots {
|
||||
let w = if opts.keep.contains(&s.handle) {
|
||||
1e4
|
||||
} else if opts.prefer_move.contains(&s.handle) {
|
||||
1e-2
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
for j in s.off..s.off + s.len {
|
||||
if let Some(slot) = weights.get_mut(j) {
|
||||
*slot = w;
|
||||
}
|
||||
}
|
||||
// Angles and ratios: weight by size² so the movement of points is what is minimised.
|
||||
let sq = |j: usize| model.x0.get(j).map_or(1.0, |r| (r * r).max(1e-6));
|
||||
let scaled: Vec<(usize, f64)> = match s.shape {
|
||||
model::Shape::Arc => vec![(s.off + 3, sq(s.off + 2)), (s.off + 4, sq(s.off + 2))],
|
||||
model::Shape::Ellipse { arc } => {
|
||||
let a2 = (sq(s.off + 2) + sq(s.off + 3)).max(1e-6);
|
||||
let mut v = vec![(s.off + 4, a2)];
|
||||
if arc {
|
||||
v.extend([(s.off + 5, a2), (s.off + 6, a2)]);
|
||||
}
|
||||
v
|
||||
}
|
||||
model::Shape::Insert { .. } => vec![(s.off + 2, model.rigid_extent2(s.handle).max(1.0))],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (j, f) in scaled {
|
||||
if let Some(slot) = weights.get_mut(j) {
|
||||
*slot *= f;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Contact parameters move freely.
|
||||
let geo = model.slots.iter().map(|s| s.off + s.len).max().unwrap_or(0);
|
||||
for w in weights.iter_mut().skip(geo) {
|
||||
*w = 1e-3;
|
||||
}
|
||||
let weights = solver_weights(&model, opts);
|
||||
let held = opts.hold.iter().filter_map(|r| model.lock_indices(r)).flatten().collect();
|
||||
let tol = scale_tol(&model.x0);
|
||||
comps.push(Comp { model, built, weights, held, tol });
|
||||
|
||||
@@ -963,57 +963,6 @@ impl AreaM {
|
||||
AreaMode::Subtract => self.total - v,
|
||||
}
|
||||
}
|
||||
/// Input while a height is pending: typed, or given by two points (their distance).
|
||||
fn height_input(&mut self, s: &mut Session, i: Input) -> Step {
|
||||
match (i, self.height_from) {
|
||||
(Input::Point(p), None) => self.height_from = Some(p),
|
||||
(Input::Point(p), Some(from)) => {
|
||||
self.height_from = None;
|
||||
return self.height(s, p.dist(from));
|
||||
}
|
||||
(Input::Text(t), None) => match crate::units::parse_distance(&t).filter(|v| v.is_finite()) {
|
||||
Some(h) => return self.height(s, h),
|
||||
None => s.echo("Requires a distance."),
|
||||
},
|
||||
(Input::Enter, None) => s.echo("Requires a height."),
|
||||
(Input::Enter, Some(_)) => s.echo("Requires a second point."),
|
||||
(Input::Text(_), Some(_)) => s.echo("Requires a point."),
|
||||
_ => {}
|
||||
}
|
||||
Step::Continue
|
||||
}
|
||||
|
||||
/// Input while the objects to measure are picked.
|
||||
fn object_input(&mut self, s: &mut Session, i: Input) -> Step {
|
||||
match i {
|
||||
Input::Pick(hs) => {
|
||||
for h in hs {
|
||||
match object_area(s, h) {
|
||||
Ok((a, per)) => {
|
||||
if self.measured(s, a, per) == Step::Done {
|
||||
return Step::Done;
|
||||
}
|
||||
// A single volume takes the first object with an area.
|
||||
if self.volume && self.mode == AreaMode::Single {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => s.echo("The selected object has no area."),
|
||||
}
|
||||
}
|
||||
}
|
||||
Input::Enter => {
|
||||
self.object = false;
|
||||
if self.mode == AreaMode::Single {
|
||||
return Step::Done;
|
||||
}
|
||||
}
|
||||
Input::Text(_) | Input::Keyword(_) => s.echo("*Invalid selection*"),
|
||||
_ => {}
|
||||
}
|
||||
Step::Continue
|
||||
}
|
||||
|
||||
/// Measure the oldest pending area's volume with height `h`.
|
||||
fn height(&mut self, s: &mut Session, h: f64) -> Step {
|
||||
match self.pending.first().map(|a| a * h.abs()) {
|
||||
@@ -1093,10 +1042,52 @@ impl Interactive for AreaM {
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if !self.pending.is_empty() {
|
||||
return Ok(self.height_input(s, i));
|
||||
// A height is typed, or given by two points (their distance).
|
||||
match (i, self.height_from) {
|
||||
(Input::Point(p), None) => self.height_from = Some(p),
|
||||
(Input::Point(p), Some(from)) => {
|
||||
self.height_from = None;
|
||||
return Ok(self.height(s, p.dist(from)));
|
||||
}
|
||||
(Input::Text(t), None) => match crate::units::parse_distance(&t).filter(|v| v.is_finite()) {
|
||||
Some(h) => return Ok(self.height(s, h)),
|
||||
None => s.echo("Requires a distance."),
|
||||
},
|
||||
(Input::Enter, None) => s.echo("Requires a height."),
|
||||
(Input::Enter, Some(_)) => s.echo("Requires a second point."),
|
||||
(Input::Text(_), Some(_)) => s.echo("Requires a point."),
|
||||
_ => {}
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.object {
|
||||
return Ok(self.object_input(s, i));
|
||||
match i {
|
||||
Input::Pick(hs) => {
|
||||
for h in hs {
|
||||
match object_area(s, h) {
|
||||
Ok((a, per)) => {
|
||||
if self.measured(s, a, per) == Step::Done {
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
// A single volume takes the first object with an area.
|
||||
if self.volume && self.mode == AreaMode::Single {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => s.echo("The selected object has no area."),
|
||||
}
|
||||
}
|
||||
}
|
||||
Input::Enter => {
|
||||
self.object = false;
|
||||
if self.mode == AreaMode::Single {
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
}
|
||||
Input::Text(_) | Input::Keyword(_) => s.echo("*Invalid selection*"),
|
||||
_ => {}
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.length {
|
||||
if let Some(p) = length_input(&self.pts, i)? {
|
||||
|
||||
@@ -117,21 +117,7 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
// Endpoints closer than the tolerance meet. Transitive components of three or more ends are
|
||||
// branch junctions; a component of exactly two is a simple joint.
|
||||
let mut parent: Vec<usize> = (0..n).collect();
|
||||
for i in 0..n {
|
||||
let Some(a) = ends.get(i) else { continue };
|
||||
for j in (i + 1)..n {
|
||||
let Some(b) = ends.get(j) else { continue };
|
||||
if a.at.dist(b.at) <= tol {
|
||||
let (ra, rb) = (find(&mut parent, i), find(&mut parent, j));
|
||||
if ra != rb
|
||||
&& let Some(slot) = parent.get_mut(ra)
|
||||
{
|
||||
*slot = rb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut parent = merge_ends(&ends, tol);
|
||||
let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
|
||||
for i in 0..n {
|
||||
let root = find(&mut parent, i);
|
||||
@@ -140,54 +126,8 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
// Junctions are reported and their pieces left out of the chains: not resolved.
|
||||
let mut excluded = vec![false; pieces.len()];
|
||||
let mut junctions: Vec<Value> = Vec::new();
|
||||
let mut joints: Vec<(usize, usize)> = Vec::new();
|
||||
for group in groups.values() {
|
||||
match group.as_slice() {
|
||||
&[a, b] => joints.push((a, b)),
|
||||
[_, _, ..] => {
|
||||
let scale = group.len() as f64;
|
||||
let mut at = Vec2::ZERO;
|
||||
let mut members: Vec<Handle> = Vec::new();
|
||||
for &e in group {
|
||||
let Some(end) = ends.get(e) else { continue };
|
||||
at += end.at / scale;
|
||||
let Some(piece) = pieces.get(end.piece) else { continue };
|
||||
if !members.contains(&piece.handle) {
|
||||
members.push(piece.handle);
|
||||
}
|
||||
if let Some(slot) = excluded.get_mut(end.piece) {
|
||||
*slot = true;
|
||||
}
|
||||
}
|
||||
junctions.push(json!({
|
||||
"at": [at.x, at.y],
|
||||
"ends": group.len(),
|
||||
"members": members.iter().map(|h| h.hex()).collect::<Vec<_>>(),
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// A joint links the two ends for the walk. A joint into an excluded piece is not a link, and
|
||||
// a line whose own ends meet within a huge tolerance is a spur, not a zero-area loop.
|
||||
let mut link: Vec<Option<usize>> = vec![None; n];
|
||||
for (a, b) in joints {
|
||||
let (Some(ea), Some(eb)) = (ends.get(a), ends.get(b)) else { continue };
|
||||
if ea.piece == eb.piece && pieces.get(ea.piece).is_some_and(|p| p.arc.is_none()) {
|
||||
continue;
|
||||
}
|
||||
if excluded.get(ea.piece).copied().unwrap_or(true) || excluded.get(eb.piece).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
if let Some(slot) = link.get_mut(a) {
|
||||
*slot = Some(b);
|
||||
}
|
||||
if let Some(slot) = link.get_mut(b) {
|
||||
*slot = Some(a);
|
||||
}
|
||||
}
|
||||
let (joints, junctions) = sort_groups(&groups, &ends, &pieces, &mut excluded);
|
||||
let link = joint_links(joints, &ends, &pieces, &excluded);
|
||||
|
||||
let mut visited = vec![false; pieces.len()];
|
||||
let mut loops: Vec<Value> = Vec::new();
|
||||
@@ -225,6 +165,84 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// A report with nothing analyzed: an empty drawing, or one past the pair cap.
|
||||
/// Union-find over the ends: ends closer than `tol` share a root. Returns each end's parent.
|
||||
fn merge_ends(ends: &[End], tol: f64) -> Vec<usize> {
|
||||
let n = ends.len();
|
||||
let mut parent: Vec<usize> = (0..n).collect();
|
||||
for i in 0..n {
|
||||
let Some(a) = ends.get(i) else { continue };
|
||||
for j in (i + 1)..n {
|
||||
let Some(b) = ends.get(j) else { continue };
|
||||
if a.at.dist(b.at) <= tol {
|
||||
let (ra, rb) = (find(&mut parent, i), find(&mut parent, j));
|
||||
if ra != rb
|
||||
&& let Some(slot) = parent.get_mut(ra)
|
||||
{
|
||||
*slot = rb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parent
|
||||
}
|
||||
|
||||
/// Each group of two ends is a joint (end indices); each of three or more is a junction, reported
|
||||
/// with its pieces marked `excluded`.
|
||||
fn sort_groups(groups: &BTreeMap<usize, Vec<usize>>, ends: &[End], pieces: &[Piece], excluded: &mut [bool]) -> (Vec<(usize, usize)>, Vec<Value>) {
|
||||
let mut joints: Vec<(usize, usize)> = Vec::new();
|
||||
let mut junctions: Vec<Value> = Vec::new();
|
||||
for group in groups.values() {
|
||||
match group.as_slice() {
|
||||
&[a, b] => joints.push((a, b)),
|
||||
[_, _, ..] => {
|
||||
let scale = group.len() as f64;
|
||||
let mut at = Vec2::ZERO;
|
||||
let mut members: Vec<Handle> = Vec::new();
|
||||
for &e in group {
|
||||
let Some(end) = ends.get(e) else { continue };
|
||||
at += end.at / scale;
|
||||
let Some(piece) = pieces.get(end.piece) else { continue };
|
||||
if !members.contains(&piece.handle) {
|
||||
members.push(piece.handle);
|
||||
}
|
||||
if let Some(slot) = excluded.get_mut(end.piece) {
|
||||
*slot = true;
|
||||
}
|
||||
}
|
||||
junctions.push(json!({
|
||||
"at": [at.x, at.y],
|
||||
"ends": group.len(),
|
||||
"members": members.iter().map(|h| h.hex()).collect::<Vec<_>>(),
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(joints, junctions)
|
||||
}
|
||||
|
||||
/// The link each end takes from the joints. A joint into an excluded piece is not a link, and
|
||||
/// a line whose own ends meet within a huge tolerance is a spur, not a zero-area loop.
|
||||
fn joint_links(joints: Vec<(usize, usize)>, ends: &[End], pieces: &[Piece], excluded: &[bool]) -> Vec<Option<usize>> {
|
||||
let mut link: Vec<Option<usize>> = vec![None; ends.len()];
|
||||
for (a, b) in joints {
|
||||
let (Some(ea), Some(eb)) = (ends.get(a), ends.get(b)) else { continue };
|
||||
if ea.piece == eb.piece && pieces.get(ea.piece).is_some_and(|p| p.arc.is_none()) {
|
||||
continue;
|
||||
}
|
||||
if excluded.get(ea.piece).copied().unwrap_or(true) || excluded.get(eb.piece).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
if let Some(slot) = link.get_mut(a) {
|
||||
*slot = Some(b);
|
||||
}
|
||||
if let Some(slot) = link.get_mut(b) {
|
||||
*slot = Some(a);
|
||||
}
|
||||
}
|
||||
link
|
||||
}
|
||||
|
||||
fn empty_report(offset: usize, tol: f64, pair_tests: usize, pair_tests_needed: usize, pair_limit: usize, capped: bool, skipped: Skipped) -> Value {
|
||||
json!({
|
||||
"schema": 1,
|
||||
|
||||
+130
-127
@@ -1296,128 +1296,6 @@ fn run_chamfer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
// ---------------- explode / array / order / break / join ----------------
|
||||
|
||||
fn explode_block_ref(d: &cadcraft_doc::Drawing, e: &Entity, ins: &cadcraft_doc::Insert) -> Option<Vec<Entity>> {
|
||||
if let Some(items) = super::array::explode_array(d, e) {
|
||||
return Some(items);
|
||||
}
|
||||
let blk = d.block(&ins.block)?;
|
||||
let m = ins.transform(blk.base.xy());
|
||||
Some(
|
||||
blk.entities
|
||||
.iter()
|
||||
.map(|be| {
|
||||
let mut ne = (**be).clone();
|
||||
ne.kind.transform(&m);
|
||||
if ne.common.layer == "0" {
|
||||
ne.common.layer = e.common.layer.clone();
|
||||
}
|
||||
if ne.common.color == cadcraft_color::Color::ByBlock {
|
||||
ne.common.color = e.common.color;
|
||||
}
|
||||
ne
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn explode_mtext(e: &Entity, m: &cadcraft_doc::MText) -> Vec<Entity> {
|
||||
cadcraft_fonts::plain_mtext(&m.contents)
|
||||
.split('\n')
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
let at = m.insert.xy() + Vec2::from_angle(m.rotation - std::f64::consts::FRAC_PI_2) * (m.height * (1.0 + 5.0 / 3.0 * i as f64));
|
||||
Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Text(cadcraft_doc::Text {
|
||||
insert: v3(at),
|
||||
align_pt: None,
|
||||
height: m.height,
|
||||
value: l.into(),
|
||||
rotation: m.rotation,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: m.style.clone(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The entities of a boundary's lines and arcs, on the object's layer and style.
|
||||
fn segment_entities(e: &Entity, segs: impl IntoIterator<Item = Segment>) -> Vec<Entity> {
|
||||
segs.into_iter()
|
||||
.map(|s| {
|
||||
let k = match s {
|
||||
Segment::Line(l) => line(l.a, l.b),
|
||||
Segment::Arc { arc: a, .. } => arc(&a),
|
||||
};
|
||||
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn explode_dimension(d: &cadcraft_doc::Drawing, e: &Entity, dm: &cadcraft_doc::Dimension) -> Vec<Entity> {
|
||||
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
|
||||
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
|
||||
let g = cadcraft_render::dimension_in(d, dm);
|
||||
let line_entity = |l: &Vec<Vec2>| {
|
||||
if l.len() == 2 {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: line(*l.first()?, *l.get(1)?) })
|
||||
} else {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: lwpoly(l.iter().map(|p| PolyVertex::new(*p)).collect(), false) })
|
||||
}
|
||||
};
|
||||
// The text's marks (arc length symbol, DIMINSPECT frame) go after the text background.
|
||||
let text_marks = &g.text_marks;
|
||||
let marks = |want: bool| g.lines.iter().enumerate().filter(move |(i, _)| text_marks.contains(i) == want).map(|(_, l)| l);
|
||||
let mut v: Vec<Entity> = marks(false).filter_map(&line_entity).collect();
|
||||
for t in &g.fills {
|
||||
if let [a, b, c] = t.as_slice() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
// DIMTFILL 2: the text background as a SOLID in DIMTFILLCLR, over the lines and
|
||||
// arrowheads, under the text and its marks (SOLID corners go 1, 2, 4, 3).
|
||||
if style.text_fill == 2
|
||||
&& let [p0, p1, p2, p3] = g.text_fill.as_slice()
|
||||
{
|
||||
let fill = style.text_fill_color;
|
||||
let color = if fill == cadcraft_doc::color::Color::ByBlock { e.common.color } else { fill };
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: cadcraft_doc::Common { color, ..e.common.clone() },
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*p0), v3(*p1), v3(*p3), v3(*p2)] }),
|
||||
});
|
||||
}
|
||||
v.extend(marks(true).filter_map(&line_entity));
|
||||
if !g.value.is_empty() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::MText(cadcraft_doc::MText {
|
||||
insert: v3(g.text_pos),
|
||||
height: g.text_height,
|
||||
width: 0.0,
|
||||
attach: 5,
|
||||
rotation: g.text_angle,
|
||||
style: style.text_style.clone(),
|
||||
contents: g.mtext_contents(),
|
||||
line_spacing: 1.0,
|
||||
line_spacing_exact: false,
|
||||
columns: None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<Entity>> {
|
||||
// An annotative object explodes into plain objects as it draws at the current annotation scale.
|
||||
if cadcraft_doc::is_annotative(e) {
|
||||
@@ -1431,9 +1309,42 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
|
||||
match &e.kind {
|
||||
EntityKind::LwPolyline(p) => {
|
||||
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
|
||||
Some(segment_entities(e, pl.segments()))
|
||||
Some(
|
||||
pl.segments()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let k = match s {
|
||||
Segment::Line(l) => line(l.a, l.b),
|
||||
Segment::Arc { arc: a, .. } => arc(&a),
|
||||
};
|
||||
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
EntityKind::Insert(ins) => {
|
||||
if let Some(items) = super::array::explode_array(d, e) {
|
||||
return Some(items);
|
||||
}
|
||||
let blk = d.block(&ins.block)?;
|
||||
let m = ins.transform(blk.base.xy());
|
||||
Some(
|
||||
blk.entities
|
||||
.iter()
|
||||
.map(|be| {
|
||||
let mut ne = (**be).clone();
|
||||
ne.kind.transform(&m);
|
||||
if ne.common.layer == "0" {
|
||||
ne.common.layer = e.common.layer.clone();
|
||||
}
|
||||
if ne.common.color == cadcraft_color::Color::ByBlock {
|
||||
ne.common.color = e.common.color;
|
||||
}
|
||||
ne
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
EntityKind::Insert(ins) => explode_block_ref(d, e, ins),
|
||||
// A helix explodes into its spline (seen from above).
|
||||
EntityKind::Helix(h) => Some(vec![Entity { handle: Handle(0), common: e.common.clone(), kind: EntityKind::Spline(h.spline()) }]),
|
||||
EntityKind::Polyline3d(p) => Some(
|
||||
@@ -1446,10 +1357,102 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
|
||||
.collect(),
|
||||
),
|
||||
EntityKind::MLine(m) => Some(super::draw2::mline::explode(e, m)),
|
||||
EntityKind::MText(m) => Some(explode_mtext(e, m)),
|
||||
EntityKind::MText(m) => Some(
|
||||
cadcraft_fonts::plain_mtext(&m.contents)
|
||||
.split('\n')
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
let at = m.insert.xy() + Vec2::from_angle(m.rotation - std::f64::consts::FRAC_PI_2) * (m.height * (1.0 + 5.0 / 3.0 * i as f64));
|
||||
Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Text(cadcraft_doc::Text {
|
||||
insert: v3(at),
|
||||
align_pt: None,
|
||||
height: m.height,
|
||||
value: l.into(),
|
||||
rotation: m.rotation,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: m.style.clone(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
// A region explodes into its boundary's lines and arcs.
|
||||
EntityKind::Region(r) => Some(segment_entities(e, r.segments())),
|
||||
EntityKind::Dimension(dm) => Some(explode_dimension(d, e, dm)),
|
||||
EntityKind::Region(r) => Some(
|
||||
r.segments()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let k = match s {
|
||||
Segment::Line(l) => line(l.a, l.b),
|
||||
Segment::Arc { arc: a, .. } => arc(&a),
|
||||
};
|
||||
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
EntityKind::Dimension(dm) => {
|
||||
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
|
||||
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
|
||||
let g = cadcraft_render::dimension_in(d, dm);
|
||||
let line_entity = |l: &Vec<Vec2>| {
|
||||
if l.len() == 2 {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: line(*l.first()?, *l.get(1)?) })
|
||||
} else {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: lwpoly(l.iter().map(|p| PolyVertex::new(*p)).collect(), false) })
|
||||
}
|
||||
};
|
||||
// The text's marks (arc length symbol, DIMINSPECT frame) go after the text background.
|
||||
let text_marks = &g.text_marks;
|
||||
let marks = |want: bool| g.lines.iter().enumerate().filter(move |(i, _)| text_marks.contains(i) == want).map(|(_, l)| l);
|
||||
let mut v: Vec<Entity> = marks(false).filter_map(&line_entity).collect();
|
||||
for t in &g.fills {
|
||||
if let [a, b, c] = t.as_slice() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
// DIMTFILL 2: the text background as a SOLID in DIMTFILLCLR, over the lines and
|
||||
// arrowheads, under the text and its marks (SOLID corners go 1, 2, 4, 3).
|
||||
if style.text_fill == 2
|
||||
&& let [p0, p1, p2, p3] = g.text_fill.as_slice()
|
||||
{
|
||||
let fill = style.text_fill_color;
|
||||
let color = if fill == cadcraft_doc::color::Color::ByBlock { e.common.color } else { fill };
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: cadcraft_doc::Common { color, ..e.common.clone() },
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*p0), v3(*p1), v3(*p3), v3(*p2)] }),
|
||||
});
|
||||
}
|
||||
v.extend(marks(true).filter_map(&line_entity));
|
||||
if !g.value.is_empty() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::MText(cadcraft_doc::MText {
|
||||
insert: v3(g.text_pos),
|
||||
height: g.text_height,
|
||||
width: 0.0,
|
||||
attach: 5,
|
||||
rotation: g.text_angle,
|
||||
style: style.text_style.clone(),
|
||||
contents: g.mtext_contents(),
|
||||
line_spacing: 1.0,
|
||||
line_spacing_exact: false,
|
||||
columns: None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
EntityKind::Tolerance(t) => Some(super::tolerance::explode(d, e, t)),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -785,6 +785,48 @@ fn glob_cached(
|
||||
Ok(matched)
|
||||
}
|
||||
|
||||
/// For the bracket class opening at `pat[p]`, whether `text[t]` is in it: the index of its closing
|
||||
/// `]` when it is, `None` when it is not or the class is never closed.
|
||||
fn class_end(text: &[char], pat: &[char], p: usize, t: usize, budget: &mut u64) -> Result<Option<usize>, ()> {
|
||||
let mut close = p;
|
||||
while let Some(c) = pat.get(close) {
|
||||
charge_match(budget)?;
|
||||
if *c == ']' {
|
||||
break;
|
||||
}
|
||||
close += 1;
|
||||
}
|
||||
if close == pat.len() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(&c) = text.get(t) else { return Ok(None) };
|
||||
let set = &pat[p + 1..close];
|
||||
let (neg, set) = match set.split_first() {
|
||||
Some(('~', r)) if !r.is_empty() => (true, r),
|
||||
_ => (false, set),
|
||||
};
|
||||
let mut hit = false;
|
||||
let mut i = 0;
|
||||
while i < set.len() {
|
||||
charge_match(budget)?;
|
||||
if set.get(i + 1) == Some(&'-') && i + 2 < set.len() {
|
||||
if set[i] <= c && c <= set[i + 2] {
|
||||
hit = true;
|
||||
}
|
||||
i += 3;
|
||||
} else {
|
||||
if set[i] == c {
|
||||
hit = true;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if hit == neg {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(close))
|
||||
}
|
||||
|
||||
/// Evaluate wildcard tokens and backtracking branches under shared work and recursion limits.
|
||||
fn glob_inner(
|
||||
text: &[char],
|
||||
@@ -822,42 +864,7 @@ fn glob_inner(
|
||||
p += 2;
|
||||
}
|
||||
'[' => {
|
||||
let mut close = p;
|
||||
while let Some(c) = pat.get(close) {
|
||||
charge_match(budget)?;
|
||||
if *c == ']' {
|
||||
break;
|
||||
}
|
||||
close += 1;
|
||||
}
|
||||
if close == pat.len() {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(&c) = text.get(t) else { return Ok(false) };
|
||||
let set = &pat[p + 1..close];
|
||||
let (neg, set) = match set.split_first() {
|
||||
Some(('~', r)) if !r.is_empty() => (true, r),
|
||||
_ => (false, set),
|
||||
};
|
||||
let mut hit = false;
|
||||
let mut i = 0;
|
||||
while i < set.len() {
|
||||
charge_match(budget)?;
|
||||
if set.get(i + 1) == Some(&'-') && i + 2 < set.len() {
|
||||
if set[i] <= c && c <= set[i + 2] {
|
||||
hit = true;
|
||||
}
|
||||
i += 3;
|
||||
} else {
|
||||
if set[i] == c {
|
||||
hit = true;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if hit == neg {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(close) = class_end(text, pat, p, t, budget)? else { return Ok(false) };
|
||||
t += 1;
|
||||
p = close + 1;
|
||||
}
|
||||
|
||||
+40
-32
@@ -447,6 +447,44 @@ fn islands(g: &Graph, outer: &[usize], p: Vec2) -> Vec<Island> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The point of the nearest other curve within `snap` of the end `e` of curve `i`, as (curve,
|
||||
/// segment, parameter, point). `None` when the end touches a curve, or none is near. `Err` when
|
||||
/// the budget of segment checks runs out.
|
||||
fn nearest_curve(polys: &[Vec<Vec2>], i: usize, e: Vec2, snap: f64, tol: f64, budget: &mut usize) -> Result<Option<(usize, usize, f64, Vec2)>, ()> {
|
||||
let mut best: Option<(f64, usize, usize, f64, Vec2)> = None;
|
||||
let mut touching = false;
|
||||
for (j, other) in polys.iter().enumerate() {
|
||||
if j == i {
|
||||
continue;
|
||||
}
|
||||
for (k, w) in other.windows(2).enumerate() {
|
||||
if *budget == 0 {
|
||||
return Err(());
|
||||
}
|
||||
*budget -= 1;
|
||||
let [a, b] = w else { continue };
|
||||
if !(a.is_finite() && b.is_finite())
|
||||
|| e.x < a.x.min(b.x) - snap
|
||||
|| e.x > a.x.max(b.x) + snap
|
||||
|| e.y < a.y.min(b.y) - snap
|
||||
|| e.y > a.y.max(b.y) + snap
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let line = Line::new(*a, *b);
|
||||
let t = line.param_of(e).clamp(0.0, 1.0);
|
||||
let q = line.at(t);
|
||||
let d = q.dist(e);
|
||||
if d <= tol {
|
||||
touching = true;
|
||||
} else if d <= snap && best.is_none_or(|(bd, ..)| d < bd) {
|
||||
best = Some((d, j, k, t, q));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(best.filter(|_| !touching).map(|(_, j, k, t, q)| (j, k, t, q)))
|
||||
}
|
||||
|
||||
/// The curves with each open end that stops within `snap` of another curve moved onto that
|
||||
/// curve (which gains a vertex there), so the two meet exactly. A curve that is flattened
|
||||
/// into chords (an ellipse, a spline) lies up to the flattening tolerance off the true curve,
|
||||
@@ -474,38 +512,8 @@ pub fn snap_ends(polys: &[Vec<Vec2>], snap: f64, tol: f64) -> Vec<Vec<Vec2>> {
|
||||
if !e.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let mut best: Option<(f64, usize, usize, f64, Vec2)> = None;
|
||||
let mut touching = false;
|
||||
for (j, other) in polys.iter().enumerate() {
|
||||
if j == i {
|
||||
continue;
|
||||
}
|
||||
for (k, w) in other.windows(2).enumerate() {
|
||||
if budget == 0 {
|
||||
break 'ends;
|
||||
}
|
||||
budget -= 1;
|
||||
let [a, b] = w else { continue };
|
||||
if !(a.is_finite() && b.is_finite())
|
||||
|| e.x < a.x.min(b.x) - snap
|
||||
|| e.x > a.x.max(b.x) + snap
|
||||
|| e.y < a.y.min(b.y) - snap
|
||||
|| e.y > a.y.max(b.y) + snap
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let line = Line::new(*a, *b);
|
||||
let t = line.param_of(e).clamp(0.0, 1.0);
|
||||
let q = line.at(t);
|
||||
let d = q.dist(e);
|
||||
if d <= tol {
|
||||
touching = true;
|
||||
} else if d <= snap && best.is_none_or(|(bd, ..)| d < bd) {
|
||||
best = Some((d, j, k, t, q));
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((_, j, k, t, q)) = best.filter(|_| !touching) else { continue };
|
||||
let Ok(near) = nearest_curve(polys, i, e, snap, tol, &mut budget) else { break 'ends };
|
||||
let Some((j, k, t, q)) = near else { continue };
|
||||
let Some(slot) = out.get_mut(i).and_then(|v| if at_end { v.last_mut() } else { v.first_mut() }) else { continue };
|
||||
*slot = q;
|
||||
let ends = polys.get(j).and_then(|o| o.get(k).zip(o.get(k + 1)));
|
||||
|
||||
@@ -235,6 +235,46 @@ pub struct Op {
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
/// The index just past the string literal opening at `b[i]`, honouring escapes and nesting.
|
||||
fn skip_string(b: &[char], mut i: usize) -> usize {
|
||||
let mut depth = 0;
|
||||
while i < b.len() {
|
||||
match b[i] {
|
||||
'\\' => i += 1,
|
||||
'(' => depth += 1,
|
||||
')' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i + 1
|
||||
}
|
||||
|
||||
/// The index just past the inline dictionary opening at `b[i]` (`<<`).
|
||||
fn skip_dict(b: &[char], mut i: usize) -> usize {
|
||||
let mut depth = 0;
|
||||
while i < b.len() {
|
||||
if b[i] == '<' && b.get(i + 1) == Some(&'<') {
|
||||
depth += 1;
|
||||
i += 1;
|
||||
} else if b[i] == '>' && b.get(i + 1) == Some(&'>') {
|
||||
depth -= 1;
|
||||
i += 1;
|
||||
if depth == 0 {
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// The operators of content text in order, each with its numeric operands (other operands
|
||||
/// are skipped).
|
||||
pub fn ops(content: &str) -> Vec<Op> {
|
||||
@@ -253,39 +293,10 @@ pub fn ops(content: &str) -> Vec<Op> {
|
||||
i += 1;
|
||||
}
|
||||
} else if c == '(' {
|
||||
let mut depth = 0;
|
||||
while i < b.len() {
|
||||
match b[i] {
|
||||
'\\' => i += 1,
|
||||
'(' => depth += 1,
|
||||
')' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i += 1;
|
||||
i = skip_string(&b, i);
|
||||
} else if c == '<' && b.get(i + 1) == Some(&'<') {
|
||||
// An inline dictionary (marked-content properties): skip to its end.
|
||||
let mut depth = 0;
|
||||
while i < b.len() {
|
||||
if b[i] == '<' && b.get(i + 1) == Some(&'<') {
|
||||
depth += 1;
|
||||
i += 1;
|
||||
} else if b[i] == '>' && b.get(i + 1) == Some(&'>') {
|
||||
depth -= 1;
|
||||
i += 1;
|
||||
if depth == 0 {
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i = skip_dict(&b, i);
|
||||
} else if c == '<' {
|
||||
while i < b.len() && b[i] != '>' {
|
||||
i += 1;
|
||||
|
||||
+26
-38
@@ -465,26 +465,6 @@ impl Family<'_> {
|
||||
self.origin + p.rotate(self.ang)
|
||||
}
|
||||
|
||||
/// The range of the lines swept, in this frame: the boundary's extent, cut to the view's
|
||||
/// when there is one, and the view's x extent that spans are cut to. Spans are cut to the
|
||||
/// view's extent only: they already lie within the boundary, and cutting them to its extent
|
||||
/// too would move an end that sits on a boundary corner by a rounding error, so a view
|
||||
/// covering the hatch would change its lines. `None` when the view misses the boundary.
|
||||
fn lines_range(&self, bb: &Bounds2, view: Option<&Bounds2>) -> Option<(Vec2, Vec2, Option<(f64, f64)>)> {
|
||||
let (mut lo, mut hi) = (bb.min, bb.max);
|
||||
let mut span_cut = None;
|
||||
if let Some(v) = view {
|
||||
let vb = Bounds2::from_points(v.corners().iter().map(|p| self.to_local(*p)));
|
||||
if vb.is_empty() || !vb.intersects(bb) {
|
||||
return None;
|
||||
}
|
||||
lo = Vec2::new(lo.x.max(vb.min.x), lo.y.max(vb.min.y));
|
||||
hi = Vec2::new(hi.x.min(vb.max.x), hi.y.min(vb.max.y));
|
||||
span_cut = Some((vb.min.x, vb.max.x));
|
||||
}
|
||||
Some((lo, hi, span_cut))
|
||||
}
|
||||
|
||||
/// Intersect a pattern family with boundary loops, respecting view, output, cancellation, and shared dash-work limits.
|
||||
fn sweep(&self, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>, cancel: Option<&AtomicBool>, out: &mut Vec<Vec<Vec2>>, work: &mut u64) {
|
||||
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect();
|
||||
@@ -492,7 +472,21 @@ impl Family<'_> {
|
||||
if bb.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some((lo, hi, span_cut)) = self.lines_range(&bb, view) else { return };
|
||||
// The lines swept: the boundary's extent, cut to the view's (its corners in this frame)
|
||||
// when there is one. Spans are cut to the view's extent only: they already lie within the
|
||||
// boundary, and cutting them to its extent too would move an end that sits on a boundary
|
||||
// corner by a rounding error, so a view covering the hatch would change its lines.
|
||||
let (mut lo, mut hi) = (bb.min, bb.max);
|
||||
let mut span_cut = None;
|
||||
if let Some(v) = view {
|
||||
let vb = Bounds2::from_points(v.corners().iter().map(|p| self.to_local(*p)));
|
||||
if vb.is_empty() || !vb.intersects(&bb) {
|
||||
return;
|
||||
}
|
||||
lo = Vec2::new(lo.x.max(vb.min.x), lo.y.max(vb.min.y));
|
||||
hi = Vec2::new(hi.x.min(vb.max.x), hi.y.min(vb.max.y));
|
||||
span_cut = Some((vb.min.x, vb.max.x));
|
||||
}
|
||||
let shift_per_line = self.delta.rotate(-self.ang).x;
|
||||
let k0 = (lo.y / self.spacing).floor() as i64;
|
||||
let k1 = (hi.y / self.spacing).ceil() as i64;
|
||||
@@ -510,7 +504,17 @@ impl Family<'_> {
|
||||
return;
|
||||
}
|
||||
let y = k as f64 * self.spacing;
|
||||
let xs = crossings(&local, y);
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in &local {
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
let offset = shift_per_line * k as f64;
|
||||
for span in xs.chunks(2) {
|
||||
let [x0, x1] = span else { continue };
|
||||
@@ -561,22 +565,6 @@ impl Family<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The x positions where the boundary loops cross the horizontal line at `y`, in order.
|
||||
fn crossings(local: &[Vec<Vec2>], y: f64) -> Vec<f64> {
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in local {
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
xs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_engine::cmd::file::compare::Comparison;
|
||||
use cadcraft_engine::doc::{Drawing, Handle};
|
||||
use cadcraft_engine::doc::Handle;
|
||||
use cadcraft_render::DisplayList;
|
||||
use egui::{Rect, RichText, Stroke, vec2};
|
||||
use serde_json::json;
|
||||
@@ -30,23 +29,6 @@ fn id() -> egui::Id {
|
||||
egui::Id::unique("cadcraft-compare-overlay")
|
||||
}
|
||||
|
||||
/// The display lists of the differences for this frame: the stored ones when they still match
|
||||
/// `key`, otherwise rebuilt at zoom `scale` and stored.
|
||||
fn cache_for(ctx: &egui::Context, key: (u64, u64, usize, usize, i64), doc: &Drawing, c: &Comparison, scale: f64) -> Cache {
|
||||
let mut cache: Cache = ctx.data_mut(|d| d.get_temp(id())).unwrap_or_default();
|
||||
if cache.key != key || cache.ours.is_none() {
|
||||
let opts = cadcraft_render::Options { tolerance: 0.5 / scale.max(1e-12), ..Default::default() };
|
||||
let ours: Vec<Handle> = c.added.iter().copied().chain(c.changed.iter().map(|(a, _)| *a)).take(MAX_DRAWN).collect();
|
||||
let theirs: Vec<Handle> = c.removed.iter().copied().chain(c.changed.iter().map(|(_, b)| *b)).take(MAX_DRAWN).collect();
|
||||
let list = |d: &Drawing, hs: &[Handle]| {
|
||||
Arc::new(cadcraft_render::build_entities(d, hs.iter().filter_map(|h| d.entity(*h)).map(|e| e.as_ref()), &opts))
|
||||
};
|
||||
cache = Cache { key, ours: Some(list(doc, &ours)), theirs: Some(list(&c.other, &theirs)) };
|
||||
ctx.data_mut(|d| d.insert_temp(id(), cache.clone()));
|
||||
}
|
||||
cache
|
||||
}
|
||||
|
||||
/// Draw the comparison over the canvas when it was made for the drawing and tab shown.
|
||||
pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf: &Xf, rect: Rect) {
|
||||
let t = Tokens::get();
|
||||
@@ -56,7 +38,17 @@ pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf
|
||||
p.rect_filled(rect, 0.0, t.canvas.gamma_multiply(0.75));
|
||||
let band = xf.scale.max(1e-300).log2().round() as i64;
|
||||
let key = (st.uid, st.revision, Arc::as_ptr(&c.other) as usize, c.differences(), band);
|
||||
let cache = cache_for(ctx, key, &st.doc, c, xf.scale);
|
||||
let mut cache: Cache = ctx.data_mut(|d| d.get_temp(id())).unwrap_or_default();
|
||||
if cache.key != key || cache.ours.is_none() {
|
||||
let opts = cadcraft_render::Options { tolerance: 0.5 / xf.scale.max(1e-12), ..Default::default() };
|
||||
let ours: Vec<Handle> = c.added.iter().copied().chain(c.changed.iter().map(|(a, _)| *a)).take(MAX_DRAWN).collect();
|
||||
let theirs: Vec<Handle> = c.removed.iter().copied().chain(c.changed.iter().map(|(_, b)| *b)).take(MAX_DRAWN).collect();
|
||||
let list = |d: &cadcraft_engine::doc::Drawing, hs: &[Handle]| {
|
||||
Arc::new(cadcraft_render::build_entities(d, hs.iter().filter_map(|h| d.entity(*h)).map(|e| e.as_ref()), &opts))
|
||||
};
|
||||
cache = Cache { key, ours: Some(list(&st.doc, &ours)), theirs: Some(list(&c.other, &theirs)) };
|
||||
ctx.data_mut(|d| d.insert_temp(id(), cache.clone()));
|
||||
}
|
||||
for (list, color) in [(&cache.theirs, t.compare_other), (&cache.ours, t.compare_this)] {
|
||||
if let Some(l) = list {
|
||||
let all: Vec<usize> = (0..l.prims.len()).collect();
|
||||
|
||||
@@ -217,15 +217,73 @@ fn display(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
/// Files tab: save format and default version, new-drawing units, autosave interval and autosave file location; edits accumulate in `changes`.
|
||||
fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
grid(ui, "options_files", |ui| {
|
||||
save_format_row(ui, app, changes);
|
||||
ui.label(crate::tl!("Save new drawings as"));
|
||||
ui.horizontal(|ui| {
|
||||
for f in crate::SaveFormat::ALL {
|
||||
let label = match f {
|
||||
crate::SaveFormat::Dxf => crate::tl!("DXF"),
|
||||
crate::SaveFormat::Dwg => crate::tl!("DWG"),
|
||||
};
|
||||
if ui.radio(app.ui.save_format == f, label).clicked() && app.ui.save_format != f {
|
||||
changes.insert("saveFormat".into(), json!(f.as_str()));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
save_version_row(ui, app, changes);
|
||||
|
||||
let version_caption = ui.label(crate::tl!("Default save version"));
|
||||
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
|
||||
let mut version = current;
|
||||
let version_combo = egui::ComboBox::from_id_salt("options_save_version").selected_text(version.year()).show_ui(ui, |ui| {
|
||||
for v in crate::SaveVersion::ALL {
|
||||
ui.selectable_value(&mut version, v, v.year());
|
||||
}
|
||||
});
|
||||
version_combo.response.labelled_by(version_caption.id);
|
||||
if version != current {
|
||||
changes.insert("saveVersion".into(), json!(version.year()));
|
||||
}
|
||||
ui.end_row();
|
||||
units_row(ui, app, changes);
|
||||
|
||||
ui.label(crate::tl!("Units of new drawings"));
|
||||
ui.horizontal(|ui| {
|
||||
let metric = app.session.settings.measureinit;
|
||||
if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(0));
|
||||
}
|
||||
if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(1));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
autosave_row(ui, changes);
|
||||
|
||||
let minutes_caption = ui.label(crate::tl!("Automatic save every"));
|
||||
let mut minutes = cadcraft_engine::autosave::savetime();
|
||||
let max = cadcraft_engine::autosave::MAX_SAVETIME;
|
||||
if ui
|
||||
.add(egui::DragValue::new(&mut minutes).range(0..=max).suffix(format!(" {}", crate::tl!("min"))))
|
||||
.labelled_by(minutes_caption.id)
|
||||
.changed()
|
||||
{
|
||||
changes.insert("SAVETIME".into(), json!(minutes));
|
||||
}
|
||||
ui.end_row();
|
||||
savefile_row(ui, changes);
|
||||
|
||||
let savefile_label = ui.label(crate::tl!("Automatic save file location"));
|
||||
// Edited as text; the folder changes when the field loses focus.
|
||||
let id = egui::Id::unique("cc_options_savefilepath");
|
||||
let current = cadcraft_engine::autosave::savefilepath();
|
||||
let mut path = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.clone());
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut path).desired_width(330.0)).labelled_by(savefile_label.id);
|
||||
if r.changed() {
|
||||
ui.data_mut(|d| d.insert_temp(id, path.clone()));
|
||||
}
|
||||
if r.lost_focus() {
|
||||
ui.data_mut(|d| d.remove::<String>(id));
|
||||
if path.trim() != current {
|
||||
changes.insert("SAVEFILEPATH".into(), json!(path.trim()));
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
hint(ui, crate::tl!("New and the first drawing at startup use these units unless a default template is set (QNEWTEMPLATE)."));
|
||||
@@ -233,76 +291,6 @@ fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
hint(ui, crate::tl!("Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off."));
|
||||
}
|
||||
|
||||
fn save_format_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
|
||||
ui.label(crate::tl!("Save new drawings as"));
|
||||
ui.horizontal(|ui| {
|
||||
for f in crate::SaveFormat::ALL {
|
||||
let label = match f {
|
||||
crate::SaveFormat::Dxf => crate::tl!("DXF"),
|
||||
crate::SaveFormat::Dwg => crate::tl!("DWG"),
|
||||
};
|
||||
if ui.radio(app.ui.save_format == f, label).clicked() && app.ui.save_format != f {
|
||||
changes.insert("saveFormat".into(), json!(f.as_str()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn save_version_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
|
||||
let version_caption = ui.label(crate::tl!("Default save version"));
|
||||
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
|
||||
let mut version = current;
|
||||
let version_combo = egui::ComboBox::from_id_salt("options_save_version").selected_text(version.year()).show_ui(ui, |ui| {
|
||||
for v in crate::SaveVersion::ALL {
|
||||
ui.selectable_value(&mut version, v, v.year());
|
||||
}
|
||||
});
|
||||
version_combo.response.labelled_by(version_caption.id);
|
||||
if version != current {
|
||||
changes.insert("saveVersion".into(), json!(version.year()));
|
||||
}
|
||||
}
|
||||
|
||||
fn units_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
|
||||
ui.label(crate::tl!("Units of new drawings"));
|
||||
ui.horizontal(|ui| {
|
||||
let metric = app.session.settings.measureinit;
|
||||
if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(0));
|
||||
}
|
||||
if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn autosave_row(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
let minutes_caption = ui.label(crate::tl!("Automatic save every"));
|
||||
let mut minutes = cadcraft_engine::autosave::savetime();
|
||||
let max = cadcraft_engine::autosave::MAX_SAVETIME;
|
||||
if ui.add(egui::DragValue::new(&mut minutes).range(0..=max).suffix(format!(" {}", crate::tl!("min")))).labelled_by(minutes_caption.id).changed() {
|
||||
changes.insert("SAVETIME".into(), json!(minutes));
|
||||
}
|
||||
}
|
||||
|
||||
fn savefile_row(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
let savefile_label = ui.label(crate::tl!("Automatic save file location"));
|
||||
// Edited as text; the folder changes when the field loses focus.
|
||||
let id = egui::Id::unique("cc_options_savefilepath");
|
||||
let current = cadcraft_engine::autosave::savefilepath();
|
||||
let mut path = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.clone());
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut path).desired_width(330.0)).labelled_by(savefile_label.id);
|
||||
if r.changed() {
|
||||
ui.data_mut(|d| d.insert_temp(id, path.clone()));
|
||||
}
|
||||
if r.lost_focus() {
|
||||
ui.data_mut(|d| d.remove::<String>(id));
|
||||
if path.trim() != current {
|
||||
changes.insert("SAVEFILEPATH".into(), json!(path.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drafting tab: object-snap marker size and aperture previews plus the Drafting Settings entry; edits accumulate in `changes` and the entry may request a command through `start`.
|
||||
fn drafting(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>, start: &mut Option<&'static str>) {
|
||||
let t = Tokens::get();
|
||||
|
||||
Reference in New Issue
Block a user