feat: preserve modeling intent across sketches, assemblies, and fabrication
Persist typed sketch dimensions and bounded ordered configuration rules in format 8. Use exact occurrence paths and source identity for editing, movement, appearance, and mass. Preserve sheet material regions through cutting, folding, and exact DXF export; retain nested cavities and material islands. Validation: cargo xtask ci passed, including 368 engine tests, 178 UI tests, 68 UI scenarios, 136 kernel tests, and the 10-crate wasm check. Actual preview workflows and legacy format-7 origin-corner reopening were inspected.
This commit is contained in:
1 parent
d71bdb78ec
commit
07932d0f42
65 files changed
+6145
-484
No files matched your search
@@ -34,6 +34,9 @@ pub enum Target {
|
||||
Component {
|
||||
id: u64,
|
||||
},
|
||||
Occurrence {
|
||||
path: Vec<u64>,
|
||||
},
|
||||
/// A browser group of items.
|
||||
Group {
|
||||
id: u64,
|
||||
@@ -227,6 +230,7 @@ pub fn target_bodies(app: &SolveApp, target: &Target) -> Vec<String> {
|
||||
Target::Viewport => touched_bodies(app),
|
||||
Target::Body { name } => vec![name.clone()],
|
||||
Target::Component { id } => component_bodies(app, *id),
|
||||
Target::Occurrence { path } => app.session.occurrence_bodies(path, true).unwrap_or_default(),
|
||||
Target::Group { id } => crate::browser::group_bodies(app, *id),
|
||||
Target::Sketch { .. }
|
||||
| Target::Folder { .. }
|
||||
@@ -292,6 +296,7 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec<Item> {
|
||||
Target::Body { name } => body_items(app, std::slice::from_ref(name), true),
|
||||
Target::Sketch { id } => sketch_items(app, *id),
|
||||
Target::Component { id } => component_items(app, *id),
|
||||
Target::Occurrence { path } => occurrence_items(app, path),
|
||||
Target::Group { id } => crate::browser::group_items(app, *id),
|
||||
Target::Folder { component, folder } => crate::browser::folder_items(app, *component, folder),
|
||||
Target::Canvas { id } => crate::browser::canvas_items(app, *id),
|
||||
@@ -504,6 +509,54 @@ fn joint_items(app: &SolveApp, id: u64) -> Vec<Item> {
|
||||
]
|
||||
}
|
||||
|
||||
fn occurrence_items(app: &SolveApp, path: &[u64]) -> Vec<Item> {
|
||||
let Ok(placement) = app.session.occurrence_placement(path) else { return Vec::new() };
|
||||
let mut entries = component_items(app, placement.component);
|
||||
if app.session.occurrence_scope_count(path).is_ok_and(|n| n > 1) {
|
||||
entries.insert(0, Item::heading("Shared child: edits affect all copies of its parent"));
|
||||
}
|
||||
let bodies = app.session.occurrence_bodies(path, true).unwrap_or_default();
|
||||
for item in &mut entries {
|
||||
match item.id.as_str() {
|
||||
"ui.activate" => {
|
||||
item.params = Some(json!({"component": placement.component, "occurrence": placement.occurrence, "path": path}));
|
||||
item.enabled = app.session.active_occurrence_path.as_deref() != Some(path);
|
||||
}
|
||||
"occurrence.ground" => {
|
||||
item.label = if placement.grounded { "Unground" } else { "Ground" }.into();
|
||||
item.params = Some(json!({"occurrence": placement.occurrence, "grounded": !placement.grounded}));
|
||||
}
|
||||
"ui.moveOccurrence" => {
|
||||
item.params = Some(json!({"occurrence": placement.occurrence, "path": path}));
|
||||
item.enabled = !placement.grounded;
|
||||
}
|
||||
"ui.properties" => item.params = Some(json!({"occurrence": placement.occurrence, "path": path})),
|
||||
"occurrence.copy" => item.params = Some(json!({"occurrence": placement.occurrence, "translate": [10,10,0]})),
|
||||
"component.create" => item.params = Some(json!({"parent": placement.component, "parent_path": path})),
|
||||
"component.paste_new" => {
|
||||
item.label = "Paste New (Own Features)".into();
|
||||
item.params = Some(json!({"component":placement.component,"occurrence":placement.occurrence,"path":path,"translate":[10,10,0]}));
|
||||
}
|
||||
"ui.rename" => item.label = "Rename Component".into(),
|
||||
"ui.appearance" => item.label = "Component Appearance".into(),
|
||||
"ui.material" => item.label = "Component Physical Material".into(),
|
||||
"ui.delete" => item.params = Some(json!({"occurrences": [placement.occurrence]})),
|
||||
"ui.hide" | "ui.show" => {
|
||||
item.id = "browser.visibility".into();
|
||||
item.params = Some(json!({"visible": app.session.occurrence_hidden(path), "paths": [path]}));
|
||||
item.enabled = true;
|
||||
}
|
||||
"ui.isolate" => {
|
||||
item.id = "ui.isolateOccurrence".into();
|
||||
item.params = Some(json!({"path": path}));
|
||||
}
|
||||
"ui.export" | "ui.saveMesh" => item.params = Some(json!({"bodies": bodies})),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
fn component_items(app: &SolveApp, id: u64) -> Vec<Item> {
|
||||
let doc = &app.session.doc;
|
||||
let occ = doc.occurrence_of(id);
|
||||
@@ -681,7 +734,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
|
||||
}
|
||||
"ui.show" => app.session.visibility.hidden_bodies.retain(|b| !bodies.contains(b)),
|
||||
"ui.showAll" => {
|
||||
app.session.visibility.hidden_bodies.clear();
|
||||
let _ = app.run("browser.visibility", json!({"folder":"bodies", "visible":true}));
|
||||
app.session.visibility.hidden_sketches.clear();
|
||||
}
|
||||
"ui.isolate" => {
|
||||
@@ -722,8 +775,14 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
|
||||
start_rename(app, what, at);
|
||||
}
|
||||
"ui.properties" => {
|
||||
let command = if p.get("component").is_some() { "component.properties" } else { "inspect.measure" };
|
||||
let params = if command == "component.properties" { p.clone() } else { json!({ "bodies": bodies }) };
|
||||
let command = if p.get("occurrence").is_some() {
|
||||
"occurrence.properties"
|
||||
} else if p.get("component").is_some() {
|
||||
"component.properties"
|
||||
} else {
|
||||
"inspect.measure"
|
||||
};
|
||||
let params = if command == "inspect.measure" { json!({ "bodies": bodies }) } else { p.clone() };
|
||||
match app.session.execute(command, ¶ms) {
|
||||
Ok(v) => {
|
||||
let title = v["name"]
|
||||
@@ -798,15 +857,27 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
|
||||
}
|
||||
}
|
||||
"ui.activate" => {
|
||||
if let Some(id) = id_of(&p, "component") {
|
||||
let _ = app.run("component.activate", json!({ "component": id }));
|
||||
if id_of(&p, "component").is_some() {
|
||||
let _ = app.run("component.activate", p);
|
||||
}
|
||||
}
|
||||
"ui.moveOccurrence" => {
|
||||
if let Some(o) = id_of(&p, "occurrence") {
|
||||
app.tree.occurrence_move = Some(crate::browser::OccMove::new(o));
|
||||
let mut movement = crate::browser::OccMove::new(o);
|
||||
movement.had_pending = app.session.pending_moves.contains_key(&o);
|
||||
movement.path = p.get("path").and_then(Value::as_array).map(|p| p.iter().filter_map(Value::as_u64).collect()).unwrap_or_default();
|
||||
movement.baseline = app
|
||||
.session
|
||||
.pending_moves
|
||||
.get(&o)
|
||||
.copied()
|
||||
.or_else(|| app.session.doc.occurrences.iter().find(|occ| occ.id == o).map(|occ| occ.transform));
|
||||
app.tree.occurrence_move = Some(movement);
|
||||
}
|
||||
}
|
||||
"ui.isolateOccurrence" => {
|
||||
let _ = app.run("browser.visibility", json!({"visible": true, "paths": [p["path"]], "isolate": true}));
|
||||
}
|
||||
"ui.canvasVisible" => drop(app.run("canvas.edit", p)),
|
||||
"ui.toolbarPin" | "ui.toolbarRemove" | "ui.toolbarReset" | "ui.shortcutPin" => crate::toolbar_custom::run(app, &item.id, &p),
|
||||
"ui.workspace" => {}
|
||||
@@ -948,6 +1019,12 @@ pub fn delete(app: &mut SolveApp) {
|
||||
/// axes, components' bodies, canvases). When some are shown and some hidden, all are hidden.
|
||||
pub fn toggle_visibility(app: &mut SolveApp) {
|
||||
use solvecraft_engine::doc::FeatureKind;
|
||||
let paths: Vec<Vec<u64>> =
|
||||
app.session.selection.iter().filter_map(|s| if let Sel::Occurrence { path } = s { Some(path.clone()) } else { None }).collect();
|
||||
if !paths.is_empty() {
|
||||
let visible = !paths.iter().any(|p| !app.session.occurrence_hidden(p));
|
||||
let _ = app.run("browser.visibility", json!({"visible": visible, "paths": paths}));
|
||||
}
|
||||
let mut bodies: Vec<String> = touched_bodies(app);
|
||||
for c in app.tree.picked_components.clone() {
|
||||
bodies.extend(component_bodies(app, c));
|
||||
@@ -994,6 +1071,10 @@ pub fn rename_selection(app: &mut SolveApp, at: Pos2) -> bool {
|
||||
let what = match (app.session.selection.as_slice(), app.tree.picked_components.as_slice(), app.tree.picked_canvases.as_slice()) {
|
||||
([Sel::Body { name }], [], []) => RenameWhat::Body(name.clone()),
|
||||
([Sel::Feature { id }], [], []) => RenameWhat::Feature(*id),
|
||||
([Sel::Occurrence { path }], [], []) => match app.session.occurrence_placement(path) {
|
||||
Ok(p) => RenameWhat::Component(p.component),
|
||||
Err(_) => return false,
|
||||
},
|
||||
([Sel::Plane { name }], [], []) => match app.session.doc.find_feature(name) {
|
||||
Some(f) => RenameWhat::Feature(f.id),
|
||||
None => return false,
|
||||
@@ -1424,14 +1505,9 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
|
||||
return None;
|
||||
}
|
||||
if item.is_heading() {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, 20.0), Sense::hover());
|
||||
ui.painter().text(
|
||||
pos2(r.left() + 8.0, r.center().y + 2.0),
|
||||
Align2::LEFT_CENTER,
|
||||
item.label.to_uppercase(),
|
||||
FontId::proportional(10.5),
|
||||
t.text_dim,
|
||||
);
|
||||
let text = ui.painter().layout(item.label.to_uppercase(), FontId::proportional(10.5), t.text_dim, w - 16.0);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, (text.size().y + 8.0).max(20.0)), Sense::hover());
|
||||
ui.painter().galley(pos2(r.left() + 8.0, r.top() + 4.0), text, t.text_dim);
|
||||
return None;
|
||||
}
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 24.0), if item.enabled { Sense::click() } else { Sense::hover() });
|
||||
|
||||
Reference in new issue
Block a user