app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
//! THE SIMULATOR'S SOLID, BY COLUMNS (0.11.0) — so an exam part's mass comes
//! out of the simulator to the key, partial cuts included.
//!
//! The arithmetic model (mock_model.rs) keeps a box and a volume per body:
//! exact for a boss, a cut wholly inside the material and a revolve, and only
//! approximate for a cut that is partly outside (the CSWA Tool Block's R41
//! slot, its under-notch). This model keeps every feature as a PRIMITIVE —
//! a prism (a sketch's closed profiles between two heights, optionally
//! drafted) or a full revolve — added or subtracted in order (CSG), and
//! integrates the part by COLUMNS: along one model axis every primitive's
//! extent is an exact set of intervals (the profile sliced by a line —
//! lines and arcs intersected in closed form), and the columns sample the
//! other two axes on a grid. The error is the grid's, at internal edges only
//! (the grid is aligned to the part's box): about 0.1 % on the Tool Block,
//! well inside the exam's 1 %.
//!
//! What it cannot represent (fillets, chamfers, shells, circular patterns,
//! partial revolves, a revolve whose axis is not the column axis) makes it
//! decline, saying why; the arithmetic model answers then.
use serde::{Deserialize, Serialize};
use crate::modeling::{self, PlaneFrame, V3};
use crate::script::Entity;
/// One feature's solid, added or removed.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Prim {
pub add: bool,
/// The feature that made it (mirror and patterns copy by feature).
pub feature: String,
pub kind: Kind,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Kind {
/// The closed profiles of `entities` (plane coordinates of `frame`; nested
/// profiles alternate solid / hole) between heights `lo` and `hi` along the
/// frame's normal. `draft`: (k, h0) — the profile is offset INWARD by
/// k·|h − h0| at height h (k < 0: outward); rectangles and circles only.
Prism { frame: PlaneFrame, lo: f64, hi: f64, entities: Vec<Entity>, draft: Option<(f64, f64)> },
/// A FULL revolve of `entities` (in `frame` at height `h`) about the line
/// entity `axis`.
Revolve { frame: PlaneFrame, h: f64, entities: Vec<Entity>, axis: usize },
}
/// The model axis (0 X, 1 Y, 2 Z) a unit vector of the default frames runs
/// along, and its sign.
fn axis_of(v: V3) -> (usize, f64) {
let i = (0..3).max_by(|a, b| v[*a].abs().partial_cmp(&v[*b].abs()).unwrap()).unwrap_or(0);
(i, v[i].signum())
}
/// Where the line `p + s·e` (plane coordinates) crosses the boundary of the
/// profiles: the sorted parameters `s`. Between consecutive pairs the line is
/// inside (even-odd: a hole inside a profile is outside).
pub fn crossings(entities: &[Entity], p: (f64, f64), e: (f64, f64), shrink: f64) -> Vec<f64> {
let mut out = Vec::new();
let seg = |a: (f64, f64), b: (f64, f64), out: &mut Vec<f64>| {
let d = (b.0 - a.0, b.1 - a.1);
let den = e.0 * d.1 - e.1 * d.0;
if den.abs() < 1e-18 {
return;
}
let w = (a.0 - p.0, a.1 - p.1);
let s = (w.0 * d.1 - w.1 * d.0) / den;
let t = (w.0 * e.1 - w.1 * e.0) / den;
if (0.0..1.0).contains(&t) {
out.push(s);
}
};
let circle = |c: (f64, f64), r: f64, sweep: Option<(f64, f64)>, out: &mut Vec<f64>| {
if r <= 0.0 {
return;
}
let w = (p.0 - c.0, p.1 - c.1);
let b = w.0 * e.0 + w.1 * e.1;
let cc = w.0 * w.0 + w.1 * w.1 - r * r;
let disc = b * b - cc;
if disc <= 0.0 {
return;
}
for s in [-b - disc.sqrt(), -b + disc.sqrt()] {
let q = (p.0 + s * e.0 - c.0, p.1 + s * e.1 - c.1);
match sweep {
None => out.push(s),
Some((t0, sw)) => {
let phi = q.1.atan2(q.0);
// Half-open at the END of the arc, as segments are.
let tau = std::f64::consts::TAU;
let rel = if sw >= 0.0 { (phi - t0).rem_euclid(tau) } else { (t0 - phi).rem_euclid(tau) };
if rel < sw.abs() {
out.push(s);
}
}
}
}
};
for en in entities {
match *en {
Entity::Rect { x0, y0, x1, y1 } => {
let (a0, b0, a1, b1) = (x0.min(x1) + shrink, y0.min(y1) + shrink, x0.max(x1) - shrink, y0.max(y1) - shrink);
if a0 >= a1 || b0 >= b1 {
continue;
}
for (a, b) in [((a0, b0), (a1, b0)), ((a1, b0), (a1, b1)), ((a1, b1), (a0, b1)), ((a0, b1), (a0, b0))] {
seg(a, b, &mut out);
}
}
Entity::Circle { cx, cy, r } => circle((cx, cy), r - shrink, None, &mut out),
Entity::Line { x0, y0, x1, y1, construction: false } => seg((x0, y0), (x1, y1), &mut out),
Entity::Arc { construction: false, .. } => {
if let Some((cx, cy, r, t0, sw)) = crate::script::arc_geom(en) {
circle((cx, cy), r, Some((t0, sw)), &mut out);
}
}
_ => {}
}
}
out.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
out
}
/// Is `p` inside the closed profile that `members` (indices into `entities`)
/// make? Even-odd along a ray in a direction no drawn vertex lines up with
/// (0.11.4, the simulator's sketch regions).
pub fn point_in(entities: &[Entity], members: &[usize], p: (f64, f64)) -> bool {
let sub: Vec<Entity> = members.iter().filter_map(|i| entities.get(*i).cloned()).collect();
let a: f64 = 0.618_033_988_75;
crossings(&sub, p, (a.cos(), a.sin()), 0.0).iter().filter(|s| **s > 0.0).count() % 2 == 1
}
/// Points along a profile's boundary, `n` per entity.
fn boundary_samples(entities: &[Entity], members: &[usize], n: usize) -> Vec<(f64, f64)> {
let mut out = Vec::new();
let seg = |a: (f64, f64), b: (f64, f64), out: &mut Vec<(f64, f64)>| {
for k in 0..n {
let t = (k as f64 + 0.5) / n as f64;
out.push((a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t));
}
};
for i in members {
match entities.get(*i).cloned() {
Some(Entity::Rect { x0, y0, x1, y1 }) => {
for (a, b) in [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x1, y1), (x0, y1)), ((x0, y1), (x0, y0))] {
seg(a, b, &mut out);
}
}
Some(Entity::Circle { cx, cy, r }) => {
for k in 0..4 * n {
let t = std::f64::consts::TAU * (k as f64 + 0.5) / (4 * n) as f64;
out.push((cx + r * t.cos(), cy + r * t.sin()));
}
}
Some(Entity::Line { x0, y0, x1, y1, .. }) => seg((x0, y0), (x1, y1), &mut out),
Some(e @ Entity::Arc { .. }) => {
if let Some((cx, cy, r, t0, sw)) = crate::script::arc_geom(&e) {
for k in 0..n {
let t = t0 + sw * (k as f64 + 0.5) / n as f64;
out.push((cx + r * t.cos(), cy + r * t.sin()));
}
}
}
None => {}
}
}
out
}
/// Do two closed profiles' boundaries CROSS — one partly inside the other
/// (sampled: 64 points per entity, either way round)?
pub fn profiles_cross(entities: &[Entity], a: &[usize], b: &[usize]) -> bool {
let mixed = |samples: &[(f64, f64)], other: &[usize]| {
let ins = samples.iter().filter(|p| point_in(entities, other, **p)).count();
ins > 0 && ins < samples.len()
};
mixed(&boundary_samples(entities, a, 64), b) || mixed(&boundary_samples(entities, b, 64), a)
}
/// Pairs of crossings as intervals (an odd count drops the last: a tangency).
fn pairs(c: &[f64]) -> Vec<(f64, f64)> {
c.chunks_exact(2).map(|w| (w[0], w[1])).filter(|(a, b)| b > a).collect()
}
fn union(a: &[(f64, f64)], b: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut all: Vec<(f64, f64)> = a.iter().chain(b.iter()).copied().collect();
all.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
let mut out: Vec<(f64, f64)> = Vec::new();
for (s, e) in all {
match out.last_mut() {
Some(l) if s <= l.1 => l.1 = l.1.max(e),
_ => out.push((s, e)),
}
}
out
}
fn minus(a: &[(f64, f64)], b: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut out = Vec::new();
for &(s, e) in a {
let mut cur = vec![(s, e)];
for &(bs, be) in b {
let mut next = Vec::new();
for (cs, ce) in cur {
if be <= cs || bs >= ce {
next.push((cs, ce));
} else {
if bs > cs {
next.push((cs, bs));
}
if be < ce {
next.push((be, ce));
}
}
}
cur = next;
}
out.extend(cur);
}
out
}
fn intersect(a: &[(f64, f64)], lo: f64, hi: f64) -> Vec<(f64, f64)> {
a.iter().filter_map(|&(s, e)| {
let (s, e) = (s.max(lo), e.min(hi));
(e > s).then_some((s, e))
}).collect()
}
/// A rectangle or circle alone (no holes): what a draft is modelled on.
fn single_shape(entities: &[Entity]) -> Option<&Entity> {
let solid: Vec<&Entity> = entities.iter().filter(|e| !e.is_construction()).collect();
match solid.as_slice() {
[e @ (Entity::Rect { .. } | Entity::Circle { .. })] => Some(e),
_ => None,
}
}
/// Can this primitive be represented? (Why not, if not.)
pub fn supported(kind: &Kind) -> Result<(), String> {
match kind {
Kind::Prism { draft: Some(_), entities, .. } if single_shape(entities).is_none() => {
Err("a draft on a profile other than one rectangle or one circle".into())
}
_ => Ok(()),
}
}
/// 8-point Gauss-Legendre nodes and weights on [-1, 1].
const GAUSS: [(f64, f64); 8] = [
(-0.960_289_856_497_536_2, 0.101_228_536_290_376_26),
(-0.796_666_477_413_626_7, 0.222_381_034_453_374_47),
(-0.525_532_409_916_329_0, 0.313_706_645_877_887_3),
(-0.183_434_642_495_649_8, 0.362_683_783_378_362_0),
(0.183_434_642_495_649_8, 0.362_683_783_378_362_0),
(0.525_532_409_916_329_0, 0.313_706_645_877_887_3),
(0.796_666_477_413_626_7, 0.222_381_034_453_374_47),
(0.960_289_856_497_536_2, 0.101_228_536_290_376_26),
];
/// Quadrature nodes on [a, b]: Gauss-Legendre after the substitution
/// x = mid + half·sin φ, which smooths the square-root behaviour a circle's
/// chord length has at its ends (a band ends exactly there) — and integrates
/// a straight edge's linear length to ~1e-11.
fn nodes(a: f64, b: f64) -> Vec<(f64, f64)> {
let (mid, half) = ((a + b) / 2.0, (b - a) / 2.0);
let h = std::f64::consts::FRAC_PI_2;
GAUSS.iter().map(|(x, w)| {
let phi = x * h;
(mid + half * phi.sin(), w * h * half * phi.cos())
}).collect()
}
/// The coordinates along one plane direction where a profile has a vertex or
/// an extreme: where the slices of it change shape.
fn profile_coords(entities: &[Entity], along_u: bool) -> Vec<f64> {
let pick = |x: f64, y: f64| if along_u { x } else { y };
let mut out = Vec::new();
for e in entities.iter().filter(|e| !e.is_construction()) {
match *e {
Entity::Rect { x0, y0, x1, y1 } => out.extend([pick(x0, y0), pick(x1, y1)]),
Entity::Circle { cx, cy, r } => out.extend([pick(cx, cy) - r, pick(cx, cy), pick(cx, cy) + r]),
Entity::Line { x0, y0, x1, y1, .. } => out.extend([pick(x0, y0), pick(x1, y1)]),
Entity::Arc { x0, y0, x1, y1, .. } => {
out.extend([pick(x0, y0), pick(x1, y1)]);
let (lo, hi) = if along_u { crate::script::extent_along(e, 1.0, 0.0) } else { crate::script::extent_along(e, 0.0, 1.0) };
out.extend([lo, hi]);
}
}
}
out
}
/// The column axis: a revolve's axis if there is one (every revolve must
/// share it), else the axis most prisms are extruded along.
fn column_axis(prims: &[Prim]) -> Result<usize, String> {
let mut col: Option<usize> = None;
for p in prims {
if let Kind::Revolve { frame, entities, axis, .. } = &p.kind {
let ((ax, ay), (bx, by)) = entities.get(*axis).and_then(|e| e.ends()).ok_or("a revolve without its axis line")?;
let d3 = modeling::sub(frame.point(bx, by, 0.0), frame.point(ax, ay, 0.0));
let (i, _) = axis_of(d3);
if d3[i].abs() / modeling::norm(d3) < 1.0 - 1e-9 {
return Err("a revolve axis that is not along a model axis".into());
}
if col.map(|c| c != i).unwrap_or(false) {
return Err("revolves about different axes".into());
}
col = Some(i);
}
}
Ok(col.unwrap_or_else(|| {
let mut count = [0usize; 3];
for p in prims {
if let Kind::Prism { frame, .. } = &p.kind {
count[axis_of(frame.n).0] += 1;
}
}
(0..3).max_by_key(|i| (count[*i], 3 - *i)).unwrap_or(2)
}))
}
/// The plane coordinate (and sign) of model axis `m` in `frame`, if `m` lies
/// in the plane: (true for u / false for v, sign).
fn in_plane(frame: &PlaneFrame, m: usize) -> Option<(bool, f64)> {
let (xa, xs) = axis_of(frame.x);
let (ya, ys) = axis_of(frame.y);
if xa == m {
Some((true, xs))
} else if ya == m {
Some((false, ys))
} else {
None
}
}
/// Breakpoints of a revolve along model axis `m` (≠ the column axis): where
/// the distance from its axis crosses a profile radius.
fn revolve_radii(frame: &PlaneFrame, h: f64, entities: &[Entity], axis: usize) -> Option<(V3, Vec<f64>)> {
let ((ax, ay), (bx, by)) = entities.get(axis)?.ends()?;
let l = (bx - ax).hypot(by - ay);
let nrm = (-(by - ay) / l, (bx - ax) / l);
let o = nrm.0 * ax + nrm.1 * ay;
let mut radii = Vec::new();
for (i, e) in entities.iter().enumerate() {
if i == axis || e.is_construction() {
continue;
}
let (lo, hi) = crate::script::extent_along(e, nrm.0, nrm.1);
radii.extend([(lo - o).abs(), (hi - o).abs()]);
if let Some(((x0, y0), (x1, y1))) = e.ends() {
radii.extend([(nrm.0 * x0 + nrm.1 * y0 - o).abs(), (nrm.0 * x1 + nrm.1 * y1 - o).abs()]);
}
}
Some((frame.point(ax, ay, h), radii))
}
/// THE INTEGRATION. Along the column axis every primitive is an exact set of
/// intervals. Across it, the part is cut into BANDS at every coordinate where
/// a profile has a vertex or an extreme (and at every primitive's own ends),
/// and each band is integrated by 8-point Gauss-Legendre — so a polygon's
/// straight edges carry no grid error at all, and arcs converge fast. Returns
/// the volume (m³) and the centre of mass.
pub fn integrate(prims: &[Prim], bounds: (V3, V3), _n: usize) -> Result<(f64, V3), String> {
if prims.is_empty() {
return Err("nothing to integrate".into());
}
let c = column_axis(prims)?;
let (r_ax, q_ax) = match c {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
let (lo, hi) = bounds;
// Breakpoints along q, for the whole part.
let mut qb: Vec<f64> = vec![lo[q_ax], hi[q_ax]];
for p in prims {
match &p.kind {
Kind::Prism { frame, lo: h0, hi: h1, entities, .. } => {
if axis_of(frame.n).0 == q_ax {
qb.extend([*h0, *h1]);
} else if let Some((u, sg)) = in_plane(frame, q_ax) {
qb.extend(profile_coords(entities, u).into_iter().map(|x| sg * x));
}
}
Kind::Revolve { frame, h, entities, axis } => {
if let Some((a3, radii)) = revolve_radii(frame, *h, entities, *axis) {
for r in radii {
qb.extend([a3[q_ax] - r, a3[q_ax] + r]);
}
qb.push(a3[q_ax]);
}
}
}
}
let clean = |mut v: Vec<f64>, a: f64, b: f64| -> Vec<f64> {
v.retain(|x| x.is_finite() && *x >= a && *x <= b);
v.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
v.dedup_by(|x, y| (*x - *y).abs() < 1e-12);
v
};
let qb = clean(qb, lo[q_ax], hi[q_ax]);
let continuous = prims.iter().any(|p| match &p.kind {
Kind::Revolve { .. } => true,
Kind::Prism { frame, draft, .. } => draft.is_some() || axis_of(frame.n).0 == q_ax,
});
let mut vol = 0.0;
let mut mom = [0.0; 3];
for w in qb.windows(2) {
let (q0, q1) = (w[0], w[1]);
if q1 - q0 < 1e-15 {
continue;
}
for (q, wq) in nodes(q0, q1) {
// Breakpoints along r, on this row.
let mut rb: Vec<f64> = vec![lo[r_ax], hi[r_ax]];
for p in prims {
match &p.kind {
Kind::Prism { frame, lo: h0, hi: h1, entities, .. } => {
let pa = axis_of(frame.n).0;
if pa == r_ax {
rb.extend([*h0, *h1]);
} else if pa == c {
// The row's line in the plane: q fixed, r varying.
if let (Some((uq, sq)), Some((_, sr))) = (in_plane(frame, q_ax), in_plane(frame, r_ax)) {
let (pt, dir) = if uq { ((sq * q, 0.0), (0.0, 1.0)) } else { ((0.0, sq * q), (1.0, 0.0)) };
rb.extend(crossings(entities, pt, dir, 0.0).into_iter().map(|s| sr * s));
}
} else if let Some((u, sg)) = in_plane(frame, r_ax) {
rb.extend(profile_coords(entities, u).into_iter().map(|x| sg * x));
}
}
Kind::Revolve { frame, h, entities, axis } => {
if let Some((a3, radii)) = revolve_radii(frame, *h, entities, *axis) {
let dq = q - a3[q_ax];
for r in radii {
if r * r > dq * dq {
let d = (r * r - dq * dq).sqrt();
rb.extend([a3[r_ax] - d, a3[r_ax] + d]);
}
}
}
}
}
}
let rb = clean(rb, lo[r_ax], hi[r_ax]);
for v in rb.windows(2) {
let (r0, r1) = (v[0], v[1]);
if r1 - r0 < 1e-15 {
continue;
}
let at: Vec<(f64, f64)> = if continuous { nodes(r0, r1) } else { vec![((r0 + r1) / 2.0, r1 - r0)] };
for (r, wr) in at {
let mut point = [0.0; 3];
point[r_ax] = r;
point[q_ax] = q;
let mut set: Vec<(f64, f64)> = Vec::new();
for p in prims {
let iv = column(&p.kind, c, point);
set = if p.add { union(&set, &iv) } else { minus(&set, &iv) };
}
let cell = wq * wr;
for (a, b) in set {
let len = b - a;
vol += len * cell;
mom[c] += (b * b - a * a) / 2.0 * cell;
mom[r_ax] += len * r * cell;
mom[q_ax] += len * q * cell;
}
}
}
}
}
if vol <= 0.0 {
return Ok((0.0, [0.0; 3]));
}
Ok((vol, modeling::scale(mom, 1.0 / vol)))
}
/// One primitive's extent along the column through `point` (axis `c`).
fn column(kind: &Kind, c: usize, point: V3) -> Vec<(f64, f64)> {
match kind {
Kind::Prism { frame, lo, hi, entities, draft } => {
let (pa, _) = axis_of(frame.n);
if pa == c {
// In-plane point; along the column, the prism's own heights.
let (u, v) = (modeling::dot(point, frame.x), modeling::dot(point, frame.y));
if let (Some((k, h0)), Some(shape)) = (draft, single_shape(entities)) {
// How far from the sketch plane this point stays inside.
let room = match *shape {
Entity::Circle { cx, cy, r } => r - (u - cx).hypot(v - cy),
Entity::Rect { x0, y0, x1, y1 } => {
((x1 - x0).abs() / 2.0 - (u - (x0 + x1) / 2.0).abs()).min((y1 - y0).abs() / 2.0 - (v - (y0 + y1) / 2.0).abs())
}
_ => return Vec::new(),
};
if *k > 0.0 {
if room <= 0.0 {
return Vec::new();
}
let m = room / k;
return intersect(&[(h0 - m, h0 + m)], *lo, *hi);
}
let m = (-room / -k).max(0.0);
return minus(&[(*lo, *hi)], &[(h0 - m, h0 + m)]);
}
let xs = crossings(entities, (u, v), (1.0, 0.0), 0.0);
let inside = xs.iter().filter(|s| **s > 0.0).count() % 2 == 1;
if inside { vec![(*lo, *hi)] } else { Vec::new() }
} else {
// The column runs IN the sketch plane: inside the prism's
// heights, the profile sliced by the column's line.
let h = point[pa];
if h < *lo || h > *hi {
return Vec::new();
}
let (xa, xs) = axis_of(frame.x);
let (ya, ys) = axis_of(frame.y);
let (p, e, sign) = if xa == c {
((0.0, ys * point[ya]), (1.0, 0.0), xs)
} else if ya == c {
((xs * point[xa], 0.0), (0.0, 1.0), ys)
} else {
return Vec::new();
};
let (shrink, ents): (f64, std::borrow::Cow<[Entity]>) = match (draft, single_shape(entities)) {
(Some((k, h0)), Some(_)) => (k * (h - h0).abs(), std::borrow::Cow::Borrowed(entities.as_slice())),
_ => (0.0, std::borrow::Cow::Borrowed(entities.as_slice())),
};
pairs(&crossings(&ents, p, e, shrink))
.into_iter()
.map(|(a, b)| if sign > 0.0 { (a, b) } else { (-b, -a) })
.collect()
}
}
Kind::Revolve { frame, h, entities, axis } => {
let Some(((ax, ay), (bx, by))) = entities.get(*axis).and_then(|e| e.ends()) else { return Vec::new() };
let l = (bx - ax).hypot(by - ay);
let dir = ((bx - ax) / l, (by - ay) / l);
let nrm = (-dir.1, dir.0);
let a3 = frame.point(ax, ay, *h);
let d3 = modeling::scale(modeling::sub(frame.point(bx, by, *h), a3), 1.0 / l);
// The column's distance from the axis.
let mut off = modeling::sub(point, a3);
off[c] = 0.0;
let rho = modeling::norm(off);
// Which side of the axis the profile lies on.
let side = entities.iter().enumerate().filter(|(i, e)| *i != *axis && !e.is_construction()).map(|(_, e)| {
let (lo, hi) = crate::script::extent_along(e, nrm.0, nrm.1);
let o = nrm.0 * ax + nrm.1 * ay;
(lo - o) + (hi - o)
}).sum::<f64>().signum();
let p = (ax + nrm.0 * side * rho, ay + nrm.1 * side * rho);
let profile: Vec<Entity> = entities.iter().enumerate().filter(|(i, _)| *i != *axis || !entities[*i].is_construction()).map(|(_, e)| e.clone()).collect();
let base = a3[c];
let sgn = d3[c].signum();
pairs(&crossings(&profile, p, dir, 0.0))
.into_iter()
.map(|(s0, s1)| {
let (u0, u1) = (base + sgn * s0, base + sgn * s1);
(u0.min(u1), u0.max(u1))
})
.collect()
}
}
}
/// A primitive reflected across the model plane `coord` along `axis`.
pub fn reflect(kind: &Kind, axis: usize, coord: f64) -> Kind {
let flip = |es: &[Entity], along_u: bool, about: f64| -> Vec<Entity> {
let m = |x: f64| 2.0 * about - x;
es.iter()
.map(|e| match *e {
Entity::Rect { x0, y0, x1, y1 } => {
if along_u { Entity::Rect { x0: m(x1), y0, x1: m(x0), y1 } } else { Entity::Rect { x0, y0: m(y1), x1, y1: m(y0) } }
}
Entity::Circle { cx, cy, r } => if along_u { Entity::Circle { cx: m(cx), cy, r } } else { Entity::Circle { cx, cy: m(cy), r } },
Entity::Line { x0, y0, x1, y1, construction } => {
if along_u { Entity::Line { x0: m(x0), y0, x1: m(x1), y1, construction } } else { Entity::Line { x0, y0: m(y0), x1, y1: m(y1), construction } }
}
Entity::Arc { cx, cy, x0, y0, x1, y1, ccw, construction } => {
if along_u {
Entity::Arc { cx: m(cx), cy, x0: m(x0), y0, x1: m(x1), y1, ccw: !ccw, construction }
} else {
Entity::Arc { cx, cy: m(cy), x0, y0: m(y0), x1, y1: m(y1), ccw: !ccw, construction }
}
}
})
.collect()
};
let in_plane = |frame: &PlaneFrame, es: &[Entity]| -> Option<Vec<Entity>> {
let (xa, xs) = axis_of(frame.x);
let (ya, ys) = axis_of(frame.y);
if xa == axis {
Some(flip(es, true, xs * coord))
} else if ya == axis {
Some(flip(es, false, ys * coord))
} else {
None
}
};
match kind {
Kind::Prism { frame, lo, hi, entities, draft } => {
if axis_of(frame.n).0 == axis {
Kind::Prism { frame: *frame, lo: 2.0 * coord - hi, hi: 2.0 * coord - lo, entities: entities.clone(), draft: draft.map(|(k, h0)| (k, 2.0 * coord - h0)) }
} else {
Kind::Prism { frame: *frame, lo: *lo, hi: *hi, entities: in_plane(frame, entities).unwrap_or_else(|| entities.clone()), draft: *draft }
}
}
Kind::Revolve { frame, h, entities, axis: a } => {
if axis_of(frame.n).0 == axis {
Kind::Revolve { frame: *frame, h: 2.0 * coord - h, entities: entities.clone(), axis: *a }
} else {
Kind::Revolve { frame: *frame, h: *h, entities: in_plane(frame, entities).unwrap_or_else(|| entities.clone()), axis: *a }
}
}
}
}
/// A primitive moved by `d` along model axis `axis`.
pub fn translate(kind: &Kind, axis: usize, d: f64) -> Kind {
// A translation is two reflections: about 0, then about d/2.
reflect(&reflect(kind, axis, 0.0), axis, d / 2.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::script::Entity;
fn front() -> PlaneFrame {
PlaneFrame::defaults()[0]
}
fn sq(x0: f64, y0: f64, x1: f64, y1: f64) -> Entity {
Entity::Rect { x0, y0, x1, y1 }
}
#[test]
fn a_partial_cut_is_integrated_where_the_box_model_guesses() {
// A 40 x 20 x 10 block; a Ø30 circle centred on its right edge cut through.
let block = Prim { add: true, feature: "B".into(), kind: Kind::Prism { frame: front(), lo: 0.0, hi: 0.010, entities: vec![sq(0.0, 0.0, 0.040, 0.020)], draft: None } };
let cut = Prim { add: false, feature: "C".into(), kind: Kind::Prism { frame: front(), lo: 0.0, hi: 0.010, entities: vec![Entity::Circle { cx: 0.040, cy: 0.010, r: 0.015 }], draft: None } };
let (v, _) = integrate(&[block.clone(), cut], ([0.0, 0.0, 0.0], [0.040, 0.020, 0.010]), 800).unwrap();
// The disc ∩ rectangle: the half disc clipped to |y| ≤ 10 of a r 15 circle.
let r: f64 = 15.0;
let seg = |h: f64| r * r * (h / r).asin() + h * (r * r - h * h).sqrt(); // ∫_{-h}^{h} sqrt(r²−y²) dy
let removed = seg(10.0) * 10.0; // (half of the band area: x from 25 to 40) × depth
let want = 40.0 * 20.0 * 10.0 - removed;
assert!((v * 1e9 - want).abs() / want < 1e-6, "{} vs {want}", v * 1e9);
// The block alone: exact (edges on the grid), centroid at its middle.
let (v, c) = integrate(&[block], ([0.0, 0.0, 0.0], [0.040, 0.020, 0.010]), 200).unwrap();
assert!((v * 1e9 - 8000.0).abs() < 1e-6 && (c[0] - 0.020).abs() < 1e-12 && (c[2] - 0.005).abs() < 1e-12);
}
#[test]
fn a_prism_across_the_columns_is_sliced_exactly() {
// A cylinder along Y (Top Plane) through a block extruded along Z: the
// columns run along Z, the cylinder is sliced by each one.
let top = PlaneFrame::defaults()[1];
let block = Prim { add: true, feature: "B".into(), kind: Kind::Prism { frame: front(), lo: 0.0, hi: 0.060, entities: vec![sq(0.0, 0.0, 0.100, 0.020)], draft: None } };
let hole = Prim { add: false, feature: "H".into(), kind: Kind::Prism { frame: top, lo: 0.0, hi: 0.020, entities: vec![Entity::Circle { cx: 0.025, cy: -0.030, r: 0.010 }], draft: None } };
let (v, c) = integrate(&[block, hole], ([0.0, 0.0, 0.0], [0.100, 0.020, 0.060]), 600).unwrap();
let want = 120000.0 - std::f64::consts::PI * 100.0 * 20.0;
assert!((v * 1e9 - want).abs() / want < 1e-7, "{} vs {want}", v * 1e9);
assert!((c[0] * 1e3 - 51.38).abs() < 0.005 && (c[1] * 1e3 - 10.0).abs() < 1e-9 && (c[2] * 1e3 - 30.0).abs() < 0.01, "{c:?}");
}
#[test]
fn interval_sets_union_and_subtract() {
assert_eq!(union(&[(0.0, 2.0)], &[(1.0, 3.0), (5.0, 6.0)]), vec![(0.0, 3.0), (5.0, 6.0)]);
assert_eq!(minus(&[(0.0, 10.0)], &[(2.0, 3.0), (5.0, 12.0)]), vec![(0.0, 2.0), (3.0, 5.0)]);
assert_eq!(intersect(&[(0.0, 10.0)], 4.0, 20.0), vec![(4.0, 10.0)]);
// A ring: crossings of a line through two concentric circles alternate.
let ring = [Entity::Circle { cx: 0.0, cy: 0.0, r: 2.0 }, Entity::Circle { cx: 0.0, cy: 0.0, r: 1.0 }];
assert_eq!(pairs(&crossings(&ring, (-5.0, 0.0), (1.0, 0.0), 0.0)), vec![(3.0, 4.0), (6.0, 7.0)]);
}
}