Components: occurrences with transforms (instances and Paste New copies), world placement of the model (Session::world_state for display, measure, export), active component authoring frame, move/ground/capture/revert positions, move bodies and sketches between components, rename/delete, migration of older designs
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
54ee20bef4
commit
97d03e3824
12 files changed
+767
-25
No files matched your search
@@ -0,0 +1,285 @@
|
||||
//! Components and occurrences: the design as a tree.
|
||||
//!
|
||||
//! The root component (id 0) is the design. Every other component lives in a parent component
|
||||
//! through one or more occurrences, each with a rigid transform (instances share the component's
|
||||
//! contents; a "Paste New" copy is a separate component). A component's sketches, construction
|
||||
//! geometry and features are authored in its own frame (its origin); the model is evaluated in
|
||||
//! those frames and placed in the world through the occurrence transforms when bodies are
|
||||
//! gathered for display, measuring and export (`world_bodies`). Commands take world coordinates
|
||||
//! and the engine maps a new feature's geometry into the active component's frame.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solvecraft_geom::{Plane, Vec3};
|
||||
|
||||
use crate::{AxisRef, DocError, Document, FeatureKind, PatternKind, PlaneRef, Result};
|
||||
|
||||
/// Column-major 4×4 affine transform (`m[col][row]`).
|
||||
pub type Mat = [[f64; 4]; 4];
|
||||
|
||||
pub const IDENTITY: Mat = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
|
||||
|
||||
pub fn mat_mul(a: &Mat, b: &Mat) -> Mat {
|
||||
let mut r = [[0.0; 4]; 4];
|
||||
for (c, col) in r.iter_mut().enumerate() {
|
||||
for (row, cell) in col.iter_mut().enumerate() {
|
||||
*cell = (0..4).map(|k| a[k][row] * b[c][k]).sum();
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
pub fn apply_point(m: &Mat, p: Vec3) -> Vec3 {
|
||||
Vec3::new(
|
||||
m[0][0] * p.x + m[1][0] * p.y + m[2][0] * p.z + m[3][0],
|
||||
m[0][1] * p.x + m[1][1] * p.y + m[2][1] * p.z + m[3][1],
|
||||
m[0][2] * p.x + m[1][2] * p.y + m[2][2] * p.z + m[3][2],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_vector(m: &Mat, v: Vec3) -> Vec3 {
|
||||
Vec3::new(
|
||||
m[0][0] * v.x + m[1][0] * v.y + m[2][0] * v.z,
|
||||
m[0][1] * v.x + m[1][1] * v.y + m[2][1] * v.z,
|
||||
m[0][2] * v.x + m[1][2] * v.y + m[2][2] * v.z,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_identity(m: &Mat) -> bool {
|
||||
m.iter().flatten().zip(IDENTITY.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-12)
|
||||
}
|
||||
|
||||
/// Inverse of an affine transform.
|
||||
pub fn mat_inverse(m: &Mat) -> Option<Mat> {
|
||||
let a = |r: usize, c: usize| m[c][r];
|
||||
let det = a(0, 0) * (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1)) - a(0, 1) * (a(1, 0) * a(2, 2) - a(1, 2) * a(2, 0))
|
||||
+ a(0, 2) * (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0));
|
||||
if det.abs() < 1e-14 || !det.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let inv = |r: usize, c: usize| -> f64 {
|
||||
// Cofactor of (c, r) / det.
|
||||
let (r0, r1) = match c {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
let (c0, c1) = match r {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
let minor = a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0);
|
||||
let sign = if (r + c) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
sign * minor / det
|
||||
};
|
||||
let mut out = IDENTITY;
|
||||
for r in 0..3 {
|
||||
for c in 0..3 {
|
||||
out[c][r] = inv(r, c);
|
||||
}
|
||||
}
|
||||
let t = Vec3::new(m[3][0], m[3][1], m[3][2]);
|
||||
let it = apply_vector(&out, t);
|
||||
out[3] = [-it.x, -it.y, -it.z, 1.0];
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A rigid transform: rotation by `angle` about `axis` through `origin`, then a translation.
|
||||
pub fn rigid(translate: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Mat {
|
||||
let k = axis.normalized().unwrap_or(Vec3::Z);
|
||||
let (s, c) = angle.sin_cos();
|
||||
let t = 1.0 - c;
|
||||
let r = [
|
||||
[t * k.x * k.x + c, t * k.x * k.y + s * k.z, t * k.x * k.z - s * k.y],
|
||||
[t * k.x * k.y - s * k.z, t * k.y * k.y + c, t * k.y * k.z + s * k.x],
|
||||
[t * k.x * k.z + s * k.y, t * k.y * k.z - s * k.x, t * k.z * k.z + c],
|
||||
];
|
||||
let mut m = IDENTITY;
|
||||
for col in 0..3 {
|
||||
for row in 0..3 {
|
||||
m[col][row] = r[col][row];
|
||||
}
|
||||
}
|
||||
let ro = apply_vector(&m, origin);
|
||||
let tr = origin - ro + translate;
|
||||
m[3] = [tr.x, tr.y, tr.z, 1.0];
|
||||
m
|
||||
}
|
||||
|
||||
/// A placement of a component inside a parent component.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Occurrence {
|
||||
pub id: u64,
|
||||
pub component: u64,
|
||||
/// The component it sits in (0 = the root).
|
||||
#[serde(default)]
|
||||
pub parent: u64,
|
||||
pub name: String,
|
||||
#[serde(default = "identity")]
|
||||
pub transform: Mat,
|
||||
/// Grounded occurrences don't move.
|
||||
#[serde(default)]
|
||||
pub grounded: bool,
|
||||
}
|
||||
|
||||
fn identity() -> Mat {
|
||||
IDENTITY
|
||||
}
|
||||
|
||||
fn map_plane(m: &Mat, p: &Plane) -> Plane {
|
||||
let (o, x, y) = (apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y));
|
||||
Plane::new(o, x, y).unwrap_or(*p)
|
||||
}
|
||||
|
||||
fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
|
||||
match p {
|
||||
PlaneRef::Custom { plane } => *plane = map_plane(m, plane),
|
||||
PlaneRef::Offset { base, .. } => map_plane_ref(m, base),
|
||||
PlaneRef::AtAngle { base, axis_origin, axis_dir, .. } => {
|
||||
map_plane_ref(m, base);
|
||||
*axis_origin = apply_point(m, *axis_origin);
|
||||
*axis_dir = apply_vector(m, *axis_dir);
|
||||
}
|
||||
// Named origin planes and construction planes are the component's own.
|
||||
PlaneRef::Origin { .. } | PlaneRef::Construction { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// Map the feature's world geometry (points, directions, explicit planes) by `m`.
|
||||
pub fn transform_geometry(&mut self, m: &Mat) {
|
||||
let pt = |p: &mut Vec3| *p = apply_point(m, *p);
|
||||
let dir = |v: &mut Vec3| *v = apply_vector(m, *v);
|
||||
match self {
|
||||
FeatureKind::Sketch { plane, .. }
|
||||
| FeatureKind::ConstructionPlane { plane }
|
||||
| FeatureKind::Split { plane, .. }
|
||||
| FeatureKind::Mirror { plane, .. } => map_plane_ref(m, plane),
|
||||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||||
pt(origin);
|
||||
dir(d);
|
||||
}
|
||||
FeatureKind::Fillet { edges, .. } | FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::Shell { faces, .. } | FeatureKind::OffsetFace { faces, .. } => faces.iter_mut().for_each(pt),
|
||||
FeatureKind::Draft { faces, neutral, pull, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, neutral);
|
||||
dir(pull);
|
||||
}
|
||||
FeatureKind::Box { corner, .. } => pt(corner),
|
||||
FeatureKind::Cylinder { base, axis, .. } => {
|
||||
pt(base);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::Sphere { center, .. } | FeatureKind::Torus { center, .. } => pt(center),
|
||||
FeatureKind::Pattern { pattern: PatternKind::Rectangular { dir1, dir2, .. }, .. } => {
|
||||
dir(dir1);
|
||||
if let Some(d) = dir2 {
|
||||
dir(d);
|
||||
}
|
||||
}
|
||||
FeatureKind::Pattern { pattern: PatternKind::Circular { origin, axis, .. }, .. } => {
|
||||
pt(origin);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::Hole { position, direction, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
}
|
||||
FeatureKind::Thread { face, .. } => pt(face),
|
||||
FeatureKind::Scale { origin, .. } => pt(origin),
|
||||
FeatureKind::Move { rotate_axis, .. } => {
|
||||
if let Some(a) = rotate_axis {
|
||||
dir(a);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { .. }
|
||||
| FeatureKind::Extrude { .. }
|
||||
| FeatureKind::Combine { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
| FeatureKind::BoundingSolid { .. }
|
||||
| FeatureKind::Pipe { .. }
|
||||
| FeatureKind::Import { .. }
|
||||
| FeatureKind::MeshImport { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// The first occurrence of a component (its placement for authoring).
|
||||
pub fn occurrence_of(&self, component: u64) -> Option<&crate::Occurrence> {
|
||||
self.occurrences.iter().find(|o| o.component == component)
|
||||
}
|
||||
|
||||
/// World transform of a component's frame (through its first occurrence and its parents').
|
||||
pub fn component_transform(&self, component: u64) -> Mat {
|
||||
let mut m = IDENTITY;
|
||||
let mut c = component;
|
||||
for _ in 0..1000 {
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
let Some(o) = self.occurrence_of(c) else { break };
|
||||
m = mat_mul(&o.transform, &m);
|
||||
c = o.parent;
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// Every placement of each component: (component, occurrence path ids, world transform).
|
||||
pub fn placements(&self) -> Vec<(u64, Vec<u64>, Mat)> {
|
||||
let mut out = vec![(0, Vec::new(), IDENTITY)];
|
||||
let mut stack: Vec<(u64, Vec<u64>, Mat)> = vec![(0, Vec::new(), IDENTITY)];
|
||||
while let Some((comp, path, m)) = stack.pop() {
|
||||
if path.len() > 64 || out.len() > 100_000 {
|
||||
break;
|
||||
}
|
||||
for o in self.occurrences.iter().filter(|o| o.parent == comp) {
|
||||
let mut p = path.clone();
|
||||
p.push(o.id);
|
||||
let w = mat_mul(&m, &o.transform);
|
||||
out.push((o.component, p.clone(), w));
|
||||
stack.push((o.component, p, w));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Is `a` inside `b` (or `b` itself)?
|
||||
pub fn component_within(&self, a: u64, b: u64) -> bool {
|
||||
let mut c = a;
|
||||
for _ in 0..1000 {
|
||||
if c == b {
|
||||
return true;
|
||||
}
|
||||
if c == 0 {
|
||||
return false;
|
||||
}
|
||||
match self.components.iter().find(|x| x.id == c) {
|
||||
Some(x) => c = x.parent,
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Add an occurrence of an existing component into `parent`.
|
||||
pub fn add_occurrence(&mut self, component: u64, parent: u64, transform: Mat) -> Result<u64> {
|
||||
if component == 0 || !self.components.iter().any(|c| c.id == component) {
|
||||
return Err(DocError::Unknown(format!("component {component}")));
|
||||
}
|
||||
if self.component_within(parent, component) {
|
||||
return Err(DocError::Invalid("a component can't contain itself".into()));
|
||||
}
|
||||
if self.occurrences.len() >= 100_000 {
|
||||
return Err(DocError::Invalid("too many occurrences".into()));
|
||||
}
|
||||
let id = self.next_id.max(1);
|
||||
self.next_id = id + 1;
|
||||
let cname = self.components.iter().find(|c| c.id == component).map(|c| c.name.clone()).unwrap_or_default();
|
||||
let k = self.occurrences.iter().filter(|o| o.component == component).count() + 1;
|
||||
self.occurrences.push(crate::Occurrence { id, component, parent, name: format!("{cname}:{k}"), transform, grounded: false });
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
@@ -638,6 +638,9 @@ pub struct Document {
|
||||
/// Components below the root, as a tree (by parent).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub components: Vec<Component>,
|
||||
/// Placements of components in their parents (each non-root component has at least one).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub occurrences: Vec<crate::Occurrence>,
|
||||
/// Physical material per body (by body name); others use the default.
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub materials: std::collections::BTreeMap<String, String>,
|
||||
@@ -670,6 +673,7 @@ impl Document {
|
||||
marker: None,
|
||||
next_id: 1,
|
||||
components: Vec::new(),
|
||||
occurrences: Vec::new(),
|
||||
body_components: Default::default(),
|
||||
materials: Default::default(),
|
||||
}
|
||||
@@ -706,6 +710,7 @@ impl Document {
|
||||
}
|
||||
};
|
||||
self.components.push(Component { id, name, parent });
|
||||
self.add_occurrence(id, parent, crate::IDENTITY)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
@@ -719,6 +724,13 @@ impl Document {
|
||||
|
||||
pub fn from_json(s: &str) -> Result<Document> {
|
||||
let d: Document = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("document: {e}")))?;
|
||||
// Designs from before occurrences: every component gets one, in place.
|
||||
let mut d = d;
|
||||
let missing: Vec<(u64, u64)> =
|
||||
d.components.iter().filter(|c| !d.occurrences.iter().any(|o| o.component == c.id)).map(|c| (c.id, c.parent)).collect();
|
||||
for (c, p) in missing {
|
||||
d.add_occurrence(c, p, crate::IDENTITY)?;
|
||||
}
|
||||
if d.features.len() > MAX_FEATURES || d.params.len() > MAX_PARAMS {
|
||||
return Err(DocError::Invalid("document too large".into()));
|
||||
}
|
||||
|
||||
@@ -1443,3 +1443,58 @@ fn unique_body_name(st: &ModelState, base: &str) -> String {
|
||||
}
|
||||
(2..).map(|i| format!("{base} ({i})")).find(|n| st.body(n).is_none()).unwrap_or_else(|| base.to_string())
|
||||
}
|
||||
|
||||
/// The model placed in the world: each component's bodies, sketches and threads through every
|
||||
/// occurrence of it (bodies of the first placement keep their names; further instances get the
|
||||
/// occurrence path in brackets). Without components this is the model itself.
|
||||
pub fn world_state(doc: &Document, st: &ModelState) -> ModelState {
|
||||
if doc.occurrences.is_empty() {
|
||||
return st.clone();
|
||||
}
|
||||
let comp_of_feature = |id: u64| doc.feature(id).map(|f| f.component).unwrap_or(0);
|
||||
let first_path = |comp: u64| -> Vec<u64> {
|
||||
let mut path = Vec::new();
|
||||
let mut c = comp;
|
||||
for _ in 0..1000 {
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
let Some(o) = doc.occurrence_of(c) else { break };
|
||||
path.insert(0, o.id);
|
||||
c = o.parent;
|
||||
}
|
||||
path
|
||||
};
|
||||
let mut out = ModelState { body_counter: st.body_counter, ..Default::default() };
|
||||
for (comp, path, m) in doc.placements() {
|
||||
let primary = path == first_path(comp);
|
||||
let label = || -> String {
|
||||
path.iter().filter_map(|id| doc.occurrences.iter().find(|o| o.id == *id)).map(|o| o.name.as_str()).collect::<Vec<_>>().join("/")
|
||||
};
|
||||
let ident = crate::is_identity(&m);
|
||||
for b in st.bodies.iter().filter(|b| doc.body_component(&b.name, b.feature) == comp) {
|
||||
let name = if primary { b.name.clone() } else { format!("{} ({})", b.name, label()) };
|
||||
let body = if ident { b.body.clone() } else { kernel::transform_matrix(&b.body, m).unwrap_or_else(|_| b.body.clone()) };
|
||||
out.bodies.push(ModelBody::new(name, body, b.feature));
|
||||
}
|
||||
if primary {
|
||||
for s in st.sketches.iter().filter(|s| comp_of_feature(s.feature) == comp) {
|
||||
let mut s2 = s.clone();
|
||||
if !ident {
|
||||
let p = &s.plane;
|
||||
if let Some(np) = Plane::new(crate::apply_point(&m, p.origin), crate::apply_vector(&m, p.x), crate::apply_vector(&m, p.y)) {
|
||||
s2.plane = np;
|
||||
}
|
||||
}
|
||||
out.sketches.push(s2);
|
||||
}
|
||||
for t in st.threads.iter().filter(|t| comp_of_feature(t.feature) == comp) {
|
||||
let mut t2 = t.clone();
|
||||
t2.axis_point = crate::apply_point(&m, t.axis_point);
|
||||
t2.axis = crate::apply_vector(&m, t.axis);
|
||||
out.threads.push(t2);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -5,13 +5,15 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod assembly;
|
||||
mod document;
|
||||
mod eval;
|
||||
pub mod expr;
|
||||
pub mod project;
|
||||
|
||||
pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, mat_inverse, mat_mul, rigid};
|
||||
pub use document::*;
|
||||
pub use eval::{FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, parse_metric_thread};
|
||||
pub use eval::{FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, parse_metric_thread, world_state};
|
||||
pub use solvecraft_kernel as kernel;
|
||||
pub use solvecraft_sketch as sketch;
|
||||
|
||||
|
||||
Reference in new issue
Block a user