Exploded views are shown only: the design's placements and Capture Position leave them out

explode.show put the view's placements in the uncaptured moves, so occurrence.properties reported
the exploded placement and Capture Position (and reparenting, moving bodies between components,
copying or pasting in place, URDF export) wrote the view's offsets into the design. The session
now records which uncaptured moves are the view's (Session::exploded); what reports the design's
placements or works a new placement out from them uses the design's own moves
(uncaptured_moves, design_document), while the viewport and picks still show the view.

New case cov-explode-show-keeps-the-placements: the arm's properties are the same before, while
and after the view is shown, and Capture Position keeps nothing. The two explode cases that read
occurrence.properties while exploded now expect the design's placement, centre (20, 0, 5), as the
command says ("shown only; the design keeps its placements").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 20:41:34 -04:00
committed by Irakli Kourtis
parent ffb1e650df
commit 177eb207fe
18 changed files with 322 additions and 39 deletions
+8 -6
View File
@@ -430,8 +430,8 @@ pub(super) fn copy_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
let m = if bool_(p, "in_place") == Some(true) {
placing::in_place(s, &occurrence_path(s, p, src.id, cmd)?, parent, placing::parent_path(p, cmd)?.as_deref(), t, cmd)?
} else {
let base = s.pending_moves.get(&src.id).unwrap_or(&src.transform);
solidcraft_doc::mat_mul(&translation(t), base)
let base = s.uncaptured_moves().get(&src.id).copied().unwrap_or(src.transform);
solidcraft_doc::mat_mul(&translation(t), &base)
};
if !valid_position(&m) {
return Err(bad(cmd, "copy transform exceeds supported numeric range"));
@@ -469,7 +469,7 @@ fn paste_new(s: &mut Session, p: &Value) -> Result<Value> {
let transform = match occ.as_ref().filter(|_| bool_(p, "in_place") == Some(true)) {
Some(o) => placing::in_place(s, &occurrence_path(s, p, o.id, cmd)?, parent, placing::parent_path(p, cmd)?.as_deref(), t, cmd)?,
None => {
let base = occ.as_ref().map(|o| s.pending_moves.get(&o.id).copied().unwrap_or(o.transform)).unwrap_or(solidcraft_doc::IDENTITY);
let base = occ.as_ref().map(|o| s.uncaptured_moves().get(&o.id).copied().unwrap_or(o.transform)).unwrap_or(solidcraft_doc::IDENTITY);
solidcraft_doc::mat_mul(&translation(t), &base)
}
};
@@ -560,7 +560,7 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
}
let path = if p.get("path").is_some() || space == "world" { Some(occurrence_path(s, p, id, cmd)?) } else { None };
let delta = if space == "world" {
let placement = s.occurrence_placement(path.as_deref().unwrap_or(&[]))?;
let placement = s.occurrence_placement_with(path.as_deref().unwrap_or(&[]), &s.uncaptured_moves())?;
let inverse = solidcraft_doc::mat_inverse(&placement.parent_world).ok_or_else(|| bad(cmd, "parent frame is singular"))?;
solidcraft_doc::mat_mul(&solidcraft_doc::mat_mul(&inverse, &delta), &placement.parent_world)
} else {
@@ -579,7 +579,7 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
}
base
} else {
s.pending_moves.get(&id).copied().unwrap_or(o.transform)
s.uncaptured_moves().get(&id).copied().unwrap_or(o.transform)
};
let next = solidcraft_doc::mat_mul(&delta, &cur);
if !valid_position(&next) {
@@ -611,7 +611,9 @@ fn ground(s: &mut Session, p: &Value) -> Result<Value> {
}
fn capture(s: &mut Session, _p: &Value) -> Result<Value> {
let moves = std::mem::take(&mut s.pending_moves);
// The exploded view is shown, never captured: it stays shown, the design keeps its placements.
let moves = s.uncaptured_moves().into_owned();
s.pending_moves.retain(|id, _| !moves.contains_key(id));
let n = moves.len();
for (id, m) in moves {
if let Some(o) = s.doc_mut().occurrences.iter_mut().find(|o| o.id == id) {
+1 -1
View File
@@ -277,7 +277,7 @@ pub(super) fn paste(
for old in source.occurrences.iter().filter(|o| selected.contains(&o.parent)) {
let child = *components.get(&old.component).ok_or_else(|| bad(CMD, "missing child definition"))?;
let owner = *components.get(&old.parent).ok_or_else(|| bad(CMD, "missing parent definition"))?;
let pose = s.pending_moves.get(&old.id).copied().unwrap_or(old.transform);
let pose = s.uncaptured_moves().get(&old.id).copied().unwrap_or(old.transform);
if !super::component::valid_position(&pose) {
return Err(bad(CMD, "invalid internal pending occurrence transform"));
}
+1 -1
View File
@@ -228,7 +228,7 @@ fn scope(s: &Session, comp: u64, cmd: &str) -> Result<Scope> {
if source.marker.is_some_and(|m| m < source.features.len()) {
return Err(bad(cmd, "move the timeline marker to the end first"));
}
if !s.pending_moves.is_empty() {
if !s.uncaptured_moves().is_empty() {
return Err(bad(cmd, "capture or revert the moved components first"));
}
let subtree: BTreeSet<u64> = source.components.iter().filter(|c| source.component_within(c.id, comp)).map(|c| c.id).collect();
+1 -1
View File
@@ -21,7 +21,7 @@ pub(super) fn parent_path(p: &Value, cmd: &str) -> Result<Option<Vec<u64>>> {
/// is in the world, then moved by `translate` (in the world). The parent is placed by
/// [`placement_of`]: in its other instances the copy sits in the same place within them.
pub(super) fn in_place(s: &Session, path: &[u64], parent: u64, parent_path: Option<&[u64]>, translate: Vec3, cmd: &str) -> Result<Mat> {
let doc = s.placed_document();
let doc = s.design_document();
let component =
path.last().and_then(|id| doc.occurrences.iter().find(|o| o.id == *id)).map(|o| o.component).ok_or_else(|| bad(cmd, "no placement"))?;
let world = doc.path_transform(component, path).ok_or_else(|| bad(cmd, "that placement is gone"))?;
@@ -15,13 +15,7 @@ fn descendants(p: &Value, cmd: &str) -> Result<bool> {
}
fn placed_doc(s: &Session) -> Document {
let mut doc = (*s.doc).clone();
for occurrence in &mut doc.occurrences {
if let Some(transform) = s.pending_moves.get(&occurrence.id) {
occurrence.transform = *transform;
}
}
doc
s.design_document().into_owned()
}
/// A property total must cover the entire assembly. The display traversal intentionally
@@ -97,7 +91,7 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
let selected_placements = if selected.is_empty() {
if descendants { placements(doc, cmd)? } else { vec![(0, Vec::new(), solidcraft_doc::IDENTITY)] }
} else {
let placement = s.occurrence_placement(selected).map_err(|e| bad(cmd, e.to_string()))?;
let placement = s.occurrence_placement_with(selected, &s.uncaptured_moves()).map_err(|e| bad(cmd, e.to_string()))?;
let mut out = vec![(placement.component, selected.to_vec(), placement.world)];
let mut stack = if descendants { out.clone() } else { Vec::new() };
while let Some((component, path, transform)) = stack.pop() {
+6 -4
View File
@@ -35,7 +35,7 @@ pub(super) fn reparent(s: &mut Session, p: &Value) -> Result<Value> {
// From the placement named (`path`), into the parent's placement named (`parent_path`).
let moved = occurrence_path(s, p, id, cmd)?;
let to_given = parent_path(p, cmd)?;
let doc = s.placed_document();
let doc = s.design_document();
let (from_path, from_frame) = placement_of(s, &doc, o.parent, moved.split_last().map(|(_, up)| up), "path", cmd)?;
let (to_path, to_frame) = placement_of(s, &doc, to, to_given.as_deref(), "parent_path", cmd)?;
let local = doc.occurrences.iter().find(|x| x.id == id).map(|x| x.transform).unwrap_or(o.transform);
@@ -52,10 +52,12 @@ pub(super) fn reparent(s: &mut Session, p: &Value) -> Result<Value> {
let mut scratch = s.scratch();
// Pending moves along both parents and of the placement itself are kept: its new transform
// was worked out from them.
// The exploded view is shown only: an ancestor it moves keeps its placement, and stays shown.
let uncaptured = s.uncaptured_moves().into_owned();
for ancestor in from_path.iter().chain(&to_path) {
if let Some(pending) = scratch.pending_moves.remove(ancestor)
&& let Some(x) = scratch.doc_mut().occurrences.iter_mut().find(|x| x.id == *ancestor)
{
let Some(pending) = uncaptured.get(ancestor).copied() else { continue };
scratch.pending_moves.remove(ancestor);
if let Some(x) = scratch.doc_mut().occurrences.iter_mut().find(|x| x.id == *ancestor) {
x.transform = pending;
}
}
+7 -5
View File
@@ -6,7 +6,7 @@ use std::collections::{BTreeMap, BTreeSet};
type UniqueFrames = BTreeMap<u64, Option<(Vec<u64>, Mat)>>;
fn unique_frames(s: &Session, cmd: &str) -> Result<UniqueFrames> {
let doc = s.placed_document();
let doc = s.design_document();
let mut children = BTreeMap::<u64, Vec<&Occurrence>>::new();
for occurrence in &doc.occurrences {
children.entry(occurrence.parent).or_default().push(occurrence);
@@ -54,7 +54,7 @@ fn transfer(s: &mut Session, name: &str, to: u64, frames: &UniqueFrames, cmd: &s
let state = s.model.state();
let body = state.body(name).ok_or_else(|| bad(cmd, "body is missing"))?;
let from = s.doc.model_body_component(body);
let doc = s.placed_document();
let doc = s.design_document();
let unique = |component| {
frames.get(&component).and_then(Option::as_ref).ok_or_else(|| {
bad(cmd, "body transfer requires one source and destination placement; shared instances cannot preserve a unique world pose")
@@ -78,7 +78,9 @@ fn transfer(s: &mut Session, name: &str, to: u64, frames: &UniqueFrames, cmd: &s
}
let inherited = (!s.doc.appearances.bodies.contains_key(name)).then(|| s.doc.appearances.components.get(&from).cloned()).flatten();
let paths: BTreeSet<u64> = source.0.iter().chain(&destination.0).copied().collect();
let captures: Vec<_> = paths.into_iter().filter_map(|id| s.pending_moves.get(&id).map(|transform| (id, *transform))).collect();
let uncaptured = s.uncaptured_moves();
let captures: Vec<_> = paths.into_iter().filter_map(|id| uncaptured.get(&id).map(|transform| (id, *transform))).collect();
drop(uncaptured);
if captures.iter().any(|(_, transform)| !solidcraft_doc::valid_transfer_matrix(transform)) {
return Err(bad(cmd, "pending transfer position is not a supported rigid transform"));
}
@@ -164,13 +166,13 @@ fn move_at_boundary(s: &mut Session, p: &Value, allow_boundary: bool) -> Result<
// component placed more than once gets the bodies in every instance, placed by that one.
let given = path_param(p, "component_path", cmd)?;
if given.is_some() || frames.get(&to).is_some_and(Option::is_none) {
let placed = super::placing::placement_of(&scratch, &scratch.placed_document(), to, given.as_deref(), "component_path", cmd)?;
let placed = super::placing::placement_of(&scratch, &scratch.design_document(), to, given.as_deref(), "component_path", cmd)?;
frames.insert(to, Some(placed));
}
// The bodies move from the placement named (`from_path`: one of several where their component
// is placed), as that placement shows them.
if let Some(path) = path_param(p, "from_path", cmd)? {
let placed = scratch.placed_document();
let placed = scratch.design_document();
let component = path
.last()
.and_then(|id| placed.occurrences.iter().find(|o| o.id == *id))
+5 -1
View File
@@ -2091,9 +2091,13 @@ fn placed_bodies(s: &Session, names: Vec<String>, cmd: &str) -> Result<(Vec<Stri
if !crate::frames::placed(s) && picked.iter().all(|(_, component, _)| *component == s.active_component) {
return Ok((names, Vec::new()));
}
if let Some((name, ..)) = picked.iter().find(|(_, _, path)| path.iter().any(|id| s.pending_moves.contains_key(id))) {
let uncaptured = s.uncaptured_moves();
if let Some((name, ..)) = picked.iter().find(|(_, _, path)| path.iter().any(|id| uncaptured.contains_key(id))) {
return Err(bad(cmd, format!("`{name}` has a position that is not captured yet: Capture Position (component.capture_position) first")));
}
if let Some((name, ..)) = picked.iter().find(|(_, _, path)| path.iter().any(|id| s.pending_moves.contains_key(id))) {
return Err(bad(cmd, format!("`{name}` is shown in an exploded view: show it assembled (explode.show with no view) first")));
}
Ok(picked.into_iter().map(|(own, _, path)| (own, path)).unzip())
}
+4 -2
View File
@@ -660,13 +660,15 @@ fn export_urdf(s: &Session, path: &str, p: &Value) -> Result<Value> {
// their links.
let st = s.model.state();
let mut held = Vec::new();
let doc = if s.pending_moves.is_empty() {
// The exploded view is shown only: the links are where the design places them.
let moves = s.uncaptured_moves();
let doc = if moves.is_empty() {
std::borrow::Cow::Borrowed(&*s.doc)
} else {
let placed = solidcraft_doc::joints::solve(&s.doc, &st).world;
let mut doc = (*s.doc).clone();
for o in &mut doc.occurrences {
match s.pending_moves.get(&o.id) {
match moves.get(&o.id) {
Some(_) if placed.contains_key(&o.id) => held.push(o.name.clone()),
Some(t) => o.transform = *t,
None => {}
+3
View File
@@ -1061,6 +1061,7 @@ fn explode_show(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "explode.show";
if p.get("view").is_none_or(|v| v.is_null() || v.as_str().is_some_and(|x| x.eq_ignore_ascii_case("none"))) {
s.pending_moves.clear();
s.exploded.clear();
s.revision += 1;
return Ok(json!({"view": null}));
}
@@ -1070,6 +1071,8 @@ fn explode_show(s: &mut Session, p: &Value) -> Result<Value> {
if !moves.values().all(solidcraft_doc::is_placement) {
return Err(bad(cmd, format!("exploded view `{}` moves an occurrence out of range", v.name)));
}
// Shown only: `exploded` marks these as the view's, so the design's placements leave them out.
s.exploded.clone_from(&moves);
s.pending_moves = moves;
s.revision += 1;
Ok(json!({"view": v.id, "name": v.name}))
+12
View File
@@ -652,6 +652,18 @@ fn exploded_views_show_without_changing_the_design() {
assert!((bb(&mut s, &names[0]) + 10.0).abs() < 1e-6, "{names:?}");
assert!((bb(&mut s, &names[1]) - 30.0).abs() < 1e-6);
assert_eq!(s.doc.occurrences, before, "shown only");
// The design's placements are not the view's: the properties report where the design
// places B, and Capture Position has nothing of the view's to keep (it stays shown).
let b_id = occ(&s, "B");
let centre =
|s: &mut Session| run(s, "occurrence.properties", json!({"occurrence": b_id}))["total"]["center_of_mass"][0].as_f64().unwrap_or(f64::NAN);
run(&mut s, "explode.show", json!({"view": null}));
let placed = centre(&mut s);
run(&mut s, "explode.show", json!({"view": "Apart"}));
assert!((centre(&mut s) - placed).abs() < 1e-9, "{} shown, {placed} placed", centre(&mut s));
assert_eq!(run(&mut s, "component.capture_position", json!({}))["captured"], 0);
assert_eq!(s.doc.occurrences, before, "the exploded view is never captured");
assert!((bb(&mut s, &names[1]) - 30.0).abs() < 1e-6, "still shown");
run(&mut s, "explode.show", json!({"view": null}));
assert!(bb(&mut s, &names[0]).abs() < 1e-6);
run(&mut s, "explode.create", json!({"name": "Hand", "moves": [{"occurrence": "B", "translate": [0, 0, 50]}]}));
+8
View File
@@ -400,6 +400,11 @@ pub struct Session {
pub active_occurrence_path: Option<Vec<u64>>,
/// Occurrence moves not captured yet (Capture Position keeps them, a recompute drops them).
pub pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
/// The placements the exploded view (`explode.show`) put in `pending_moves` to be shown:
/// display only. An entry of `pending_moves` still equal to its one here is the exploded view's,
/// not a move of the design's, so the placements the design reports and Capture Position
/// keeps leave it out ([`Session::uncaptured_moves`]).
pub exploded: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
world_cache: std::sync::Mutex<Option<WorldCache>>,
/// The design the joints were last solved for, as the solve left it, and the model it read
/// (see [`Session::solve_joints`]).
@@ -611,6 +616,7 @@ impl Session {
active_component: 0,
active_occurrence_path: None,
pending_moves: Default::default(),
exploded: Default::default(),
world_cache: Default::default(),
joints_settled: None,
occurrence_visibility_cache: Default::default(),
@@ -1420,6 +1426,7 @@ impl Session {
active_component: self.active_component,
active_occurrence_path: self.active_occurrence_path.clone(),
pending_moves: self.pending_moves.clone(),
exploded: self.exploded.clone(),
world_cache: std::sync::Mutex::new(self.world_cache.lock().ok().and_then(|cache| cache.clone())),
joints_settled: self.joints_settled.clone(),
occurrence_visibility_cache: Default::default(),
@@ -1609,6 +1616,7 @@ impl Session {
self.active_component = computed.active_component;
self.active_occurrence_path = computed.active_occurrence_path;
self.pending_moves = computed.pending_moves;
self.exploded = computed.exploded;
// Joints the computation hid (an import's rigid group) stay hidden.
for j in computed.visibility.hidden_joints {
if !self.visibility.hidden_joints.contains(&j) {
+40 -3
View File
@@ -139,8 +139,44 @@ impl Session {
}
std::borrow::Cow::Owned(doc)
}
/// Resolve only the supplied path, never an exponentially expanded assembly.
/// The occurrence moves of the design not captured yet: `pending_moves` without the
/// placements the exploded view shows ([`Session::exploded`]), which are display only. An
/// entry the exploded view put there and nothing has moved since is left out; one moved since
/// (dragged, played) is a move like any other.
pub fn uncaptured_moves(&self) -> std::borrow::Cow<'_, std::collections::BTreeMap<u64, Mat>> {
if !self.pending_moves.iter().any(|(id, m)| self.exploded.get(id) == Some(m)) {
return std::borrow::Cow::Borrowed(&self.pending_moves);
}
std::borrow::Cow::Owned(self.pending_moves.iter().filter(|(id, m)| self.exploded.get(id) != Some(*m)).map(|(id, m)| (*id, *m)).collect())
}
/// The document with the design's own uncaptured moves applied ([`Session::uncaptured_moves`]):
/// [`Session::placed_document`] without the exploded view, for what reports the design's
/// placements or works a new placement out from them.
pub fn design_document(&self) -> std::borrow::Cow<'_, solidcraft_doc::Document> {
let moves = self.uncaptured_moves();
if moves.is_empty() {
return std::borrow::Cow::Borrowed(&self.doc);
}
let mut doc = (*self.doc).clone();
for o in &mut doc.occurrences {
if let Some(transform) = moves.get(&o.id) {
o.transform = *transform;
}
}
std::borrow::Cow::Owned(doc)
}
/// Resolve only the supplied path, never an exponentially expanded assembly: placed where
/// it is shown (uncaptured moves and the exploded view included).
pub fn occurrence_placement(&self, path: &[u64]) -> Result<OccurrencePlacement> {
self.occurrence_placement_with(path, &self.pending_moves)
}
/// [`Session::occurrence_placement`] with the occurrences moved by `moves` instead of
/// `pending_moves` (the design's own, [`Session::uncaptured_moves`], for what reports the
/// design's placements).
pub fn occurrence_placement_with(&self, path: &[u64], moves: &std::collections::BTreeMap<u64, Mat>) -> Result<OccurrencePlacement> {
if path.is_empty() || path.len() > 64 {
return Err(invalid("occurrence path must contain 1..64 ids"));
}
@@ -162,7 +198,7 @@ impl Session {
return Err(invalid("occurrence path has a missing component"));
}
parent_world = world;
world = mat_mul(&world, self.pending_moves.get(id).unwrap_or(&o.transform));
world = mat_mul(&world, moves.get(id).unwrap_or(&o.transform));
if world.iter().flatten().any(|x| !x.is_finite()) {
return Err(invalid("occurrence path has a non-finite transform"));
}
@@ -183,7 +219,7 @@ impl Session {
path: path.to_vec(),
name: leaf.name.clone(),
world,
local: self.pending_moves.get(&leaf.id).copied().unwrap_or(leaf.transform),
local: moves.get(&leaf.id).copied().unwrap_or(leaf.transform),
parent_world,
grounded: leaf.grounded,
bodies,
@@ -497,6 +533,7 @@ impl Session {
pub(crate) fn prune_occurrence_state(&mut self) {
self.pending_moves.retain(|id, _| self.doc.occurrences.iter().any(|o| o.id == *id));
self.exploded.retain(|id, _| self.doc.occurrences.iter().any(|o| o.id == *id));
if self.active_component != 0 && !self.doc.components.iter().any(|c| c.id == self.active_component) {
self.active_component = 0;
self.active_occurrence_path = None;
+3
View File
@@ -18,9 +18,12 @@ crates/engine/src/cmd/component_clone_assembly.rs
crates/engine/src/cmd/component_drag.rs
crates/engine/src/cmd/component_extract.rs
crates/engine/src/cmd/component_link.rs
crates/engine/src/cmd/component_placing.rs
crates/engine/src/cmd/component_properties.rs
crates/engine/src/cmd/component_reparent.rs
crates/engine/src/cmd/component_transfer.rs
# Where occurrences are placed, shown and reported (the exploded view is shown only).
crates/engine/src/occurrences.rs
# Interference and clearance between placed bodies.
crates/engine/src/cmd/clearance.rs
crates/engine/src/cmd/interference.rs
@@ -2,7 +2,7 @@
"sources": [],
"what": "an automatic exploded view of scale 2",
"source": "analytic",
"method": "Base: box 20x20x10 from (-10,-10,-10), grounded; Arm: box 40x10x10 from (0,-5,0), centre (20,0,5); each occurrence moves scale \u00d7 (its centre - the mean of the centres): centres (0,0,-5) and (20,0,5), mean (10,0,0): Base by (-20,0,-10), Arm by (20,0,10); shown, Arm at (40,0,15)",
"method": "Base: box 20x20x10 from (-10,-10,-10), grounded; Arm: box 40x10x10 from (0,-5,0), centre (20,0,5); each occurrence moves scale \u00d7 (its centre - the mean of the centres): centres (0,0,-5) and (20,0,5), mean (10,0,0): Base by (-20,0,-10), Arm by (20,0,10); shown only (the design keeps its placements), so occurrence.properties reports Arm where the design places it: centre (20,0,5)",
"body_count": 2,
"volume_mm3": 8000.0,
"results": [
@@ -32,12 +32,12 @@
"json": {
"total": {
"center_of_mass": [
40.0,
20.0,
0.0,
15.0
5.0
]
}
}
}
]
}
}
@@ -2,7 +2,7 @@
"sources": [],
"what": "an exploded view moving the arm 30 mm up, shown",
"source": "analytic",
"method": "Base: box 20x20x10 from (-10,-10,-10), grounded; Arm: box 40x10x10 from (0,-5,0), centre (20,0,5); explode.show places Arm by (0,0,30): centre (20,0,35)",
"method": "Base: box 20x20x10 from (-10,-10,-10), grounded; Arm: box 40x10x10 from (0,-5,0), centre (20,0,5); the view moves Arm by (0,0,30). explode.show is shown only (the design keeps its placements), so occurrence.properties reports Arm where the design places it: centre (20,0,5)",
"body_count": 2,
"volume_mm3": 8000.0,
"results": [
@@ -27,10 +27,10 @@
"center_of_mass": [
20.0,
0.0,
35.0
5.0
]
}
}
}
]
}
}
@@ -0,0 +1,91 @@
{
"sources": [],
"what": "an exploded view shown, then the arm's properties and Capture Position: the design keeps its placements",
"source": "analytic",
"method": "Arm: box 40x10x10 from (0,-5,0), centre (20,0,5), volume 4000. explode.show is shown only (the design keeps its placements), so occurrence.properties reports the arm where the design places it before the view is shown (step 8), while it is shown (step 11) and after Capture Position (step 12, which has no move of the design's to keep: 0 captured) and the assembled view (step 14): centre (20,0,5), box (0,-5,0)..(40,5,10)",
"body_count": 2,
"volume_mm3": 8000.0,
"results": [
{
"step": 8,
"json": {
"total": {
"center_of_mass": [
20.0,
0.0,
5.0
],
"bbox": {
"min": [
0.0,
-5.0,
0.0
],
"max": [
40.0,
5.0,
10.0
]
},
"volume_mm3": 4000.0
}
}
},
{
"step": 11,
"json": {
"total": {
"center_of_mass": [
20.0,
0.0,
5.0
],
"bbox": {
"min": [
0.0,
-5.0,
0.0
],
"max": [
40.0,
5.0,
10.0
]
},
"volume_mm3": 4000.0
}
}
},
{
"step": 12,
"json": {
"captured": 0
}
},
{
"step": 14,
"json": {
"total": {
"center_of_mass": [
20.0,
0.0,
5.0
],
"bbox": {
"min": [
0.0,
-5.0,
0.0
],
"max": [
40.0,
5.0,
10.0
]
},
"volume_mm3": 4000.0
}
}
}
]
}
@@ -0,0 +1,123 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 20,
"height": 10,
"corner": [
-10,
-10,
-10
],
"operation": "new",
"body_name": "BaseBox"
}
},
{
"command": "solid.box",
"params": {
"length": 40,
"width": 10,
"height": 10,
"corner": [
0,
-5,
0
],
"operation": "new",
"body_name": "ArmBox"
}
},
{
"command": "component.create",
"params": {
"name": "Base",
"parent": "root",
"activate": false
}
},
{
"command": "component.move_bodies",
"params": {
"bodies": [
"BaseBox"
],
"component": "Base"
}
},
{
"command": "component.create",
"params": {
"name": "Arm",
"parent": "root",
"activate": false
}
},
{
"command": "component.move_bodies",
"params": {
"bodies": [
"ArmBox"
],
"component": "Arm"
}
},
{
"command": "occurrence.ground",
"params": {
"occurrence": "Base:1",
"grounded": true
}
},
{
"command": "occurrence.properties",
"params": {
"occurrence": "Arm:1"
}
},
{
"command": "explode.create",
"params": {
"name": "Up",
"moves": [
{
"occurrence": "Arm:1",
"translate": [
0,
0,
30
]
}
]
}
},
{
"command": "explode.show",
"params": {
"view": "Up"
}
},
{
"command": "occurrence.properties",
"params": {
"occurrence": "Arm:1"
}
},
{
"command": "component.capture_position",
"params": {}
},
{
"command": "explode.show",
"params": {}
},
{
"command": "occurrence.properties",
"params": {
"occurrence": "Arm:1"
}
}
]
}