Components are frames (user bug #4)

What a component holds moves with its occurrence, and picks in the world land in the
component's frame:
- Sketches and construction planes show where their component is placed (free move, joint
  drive, Capture Position); sketch.inspect reports sketch points in the world.
- frames.rs maps a command's world picks into the frame of the body they lie on (through the
  inverse of its placement: a moved component or a pasted instance), points that make new
  geometry into the active component's frame, and directions with them; instance body names
  ("Body1 (Pin:2)") become the component's body. Fillet/chamfer edges, shell/offset/draft
  faces, face extrude/revolve, sketch on face, holes, sheet metal and plastic picks, primitives,
  appearance faces, analyses. Once per command (not again for commands run by commands); the
  active-frame fallback stays for commands without a table. Nothing changes while every
  placement is the identity.
- component.move_bodies keeps a body where it is in the world (Document.body_offsets: from the
  new component's frame to the one it was made in; renames and deleted components follow).
- The UI reads the placed model (Session::world_state) for display, picking, sketch editing and
  dialogs; live previews use it once anything is placed.
Tests (engine): sketches and construction planes move (and turn) with the component; world
picks fillet, sketch, hole and make boxes in a moved component; a pick on a pasted instance;
move_bodies keeps the world place there and back; a sketch on a face follows a Move feature.
QA's s34 now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 15:04:32 +00:00
1 parent c0725ebc86
commit 7a2eb059c4
30 files changed
+669 -72

No files matched your search

+37
View File
@@ -298,6 +298,43 @@ impl Document {
out
}
/// Every body as the world shows it: its world name ("Body1", or "Body1 (Occ:2)" for a
/// further instance), its component, the occurrence path, and the transform from the frame
/// its geometry is in to the world.
pub fn body_frames(&self, st: &crate::ModelState) -> Vec<(String, u64, Vec<u64>, Mat)> {
let mut out = Vec::new();
for (comp, path, m) in self.placements() {
let primary = path == self.first_path(comp);
let label = || -> String {
path.iter().filter_map(|id| self.occurrences.iter().find(|o| o.id == *id)).map(|o| o.name.as_str()).collect::<Vec<_>>().join("/")
};
for b in st.bodies.iter().filter(|b| self.body_component(&b.name, b.feature) == comp) {
let name = if primary { b.name.clone() } else { format!("{} ({})", b.name, label()) };
let w = match self.body_offsets.get(&b.name) {
Some(o) => mat_mul(&m, o),
None => m,
};
out.push((name, comp, path.clone(), w));
}
}
out
}
/// The occurrence path of a component's first placement.
pub fn first_path(&self, component: u64) -> Vec<u64> {
let mut path = Vec::new();
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = self.occurrence_of(c) else { break };
path.insert(0, o.id);
c = o.parent;
}
path
}
/// Is `a` inside `b` (or `b` itself)?
pub fn component_within(&self, a: u64, b: u64) -> bool {
let mut c = a;
+5
View File
@@ -1128,6 +1128,10 @@ pub struct Document {
/// Bodies moved into another component than their feature's (by body name).
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub body_components: std::collections::BTreeMap<String, u64>,
/// Bodies moved into another component keep their place in the world: the transform from
/// the new component's frame to the frame the body was made in (by body name).
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub body_offsets: std::collections::BTreeMap<String, crate::Mat>,
/// Browser groups (folders of bodies, sketches or construction planes).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub browser_groups: Vec<BrowserGroup>,
@@ -1196,6 +1200,7 @@ impl Document {
configs: Default::default(),
parts: Default::default(),
body_components: Default::default(),
body_offsets: Default::default(),
materials: Default::default(),
appearances: Default::default(),
favorites: Default::default(),
+4 -1
View File
@@ -2291,7 +2291,10 @@ pub fn world_state(doc: &Document, st: &ModelState) -> ModelState {
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()) };
// A body moved in from another component keeps its place (its offset).
let bm = doc.body_offsets.get(&b.name).map(|o| crate::mat_mul(&m, o)).unwrap_or(m);
let body =
if crate::is_identity(&bm) { b.body.clone() } else { kernel::transform_matrix(&b.body, bm).unwrap_or_else(|_| b.body.clone()) };
out.bodies.push(ModelBody::new(name, body, b.feature));
}
if primary {