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

+3 -3
View File
@@ -90,7 +90,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
let Some(id) = s("command").or(s("id")) else { return err("missing `command`") };
let params = p.get("params").cloned().filter(|v| !v.is_null()).unwrap_or(json!({}));
let r = app.run(id, params);
if r.is_ok() && app.session.active_sketch.is_none() && app.session.model.state().bodies.len() == 1 && app.session.undo.len() <= 2 {
if r.is_ok() && app.session.active_sketch.is_none() && app.session.world_state().bodies.len() == 1 && app.session.undo.len() <= 2 {
app.fit_view();
}
wrap(r)
@@ -269,7 +269,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
let world = if let Some(w) = p.get("world") {
Some(solvecraft_engine::geom::Vec3::new(num(w, 0), num(w, 1), num(w, 2)))
} else if let Some(s) = p.get("sketch") {
let st = app.session.model.state();
let st = app.session.world_state();
app.session
.active_sketch
.and_then(|id| st.sketch(id))
@@ -311,7 +311,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
}
"ui.sketchToScreen" => {
// Screen points of active-sketch coordinates (for driving sketch interaction).
let st = app.session.model.state();
let st = app.session.world_state();
let (Some(rect), Some(ss)) = (app.viewport.rect, app.session.active_sketch.and_then(|s| st.sketch(s))) else {
return err("no active sketch in a drawn viewport");
};