use crate::panels::toolbar_button;
use crate::panels::tree::{self, TreeRow};
use brep_render::engine_state::EngineState;
use brep_render::visibility::{EntityKind as VisKind, GroupState};
use eframe::egui;
use std::collections::{HashMap, HashSet};
#[derive(Clone)]
enum EntityKind {
Face(String),
Edge(String),
Vertex([f64; 3]),
}
#[derive(Clone)]
struct Entity {
label: String,
kind: EntityKind,
selected: bool,
visible: bool,
}
struct SolidRow {
name: String,
visible: bool,
selected: bool,
faces: Vec<Entity>,
edges: Vec<Entity>,
vertices: Vec<Entity>,
}
enum Action {
SetVisible(String, bool),
SetAllVisible(bool),
SetEntityVisible(String, VisKind, usize, bool),
SetGroupVisible(String, VisKind, bool),
SetAllGroupVisible(VisKind, bool),
SetAllSketchVisible(bool),
SetAllDatumVisible(bool),
Select(&'static str, String),
SelectVertex(String, [f64; 3]),
SetSketchVisible(String, bool),
SetDatumVisible(String, bool),
SelectDatum(String),
}
#[derive(Default)]
pub struct ScenePanel {
hits: HashMap<String, egui::Rect>,
root_collapsed: bool,
collapsed_solids: HashSet<String>,
expanded_groups: HashSet<String>,
datums_collapsed: bool,
}
impl ScenePanel {
pub fn new() -> Self {
Self::default()
}
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
self.hits.clear();
let sel_solids = state.emphasis.selected_solids.clone();
let sel_faces = state.emphasis.selected_faces.clone();
let sel_edges = state.emphasis.selected_edges.clone();
let sel_vertices = state.emphasis.selected_vertices.clone();
let solids = snapshot(state, &sel_solids, &sel_faces, &sel_edges, &sel_vertices);
let sketches = state.committed_sketches();
let sel_datums = state.emphasis.selected_datums.clone();
let datums = state.construction_datums();
ui.spacing_mut().item_spacing.y = 2.0;
let mut action: Option<Action> = None;
ui.horizontal(|ui| {
let has_solids = !solids.is_empty();
let groups: [(VisKind, &str, bool); 3] = [
(
VisKind::Face,
"Faces",
has_solids
&& solids
.iter()
.all(|s| matches!(group_state(&s.faces), GroupState::All)),
),
(
VisKind::Edge,
"Edges",
has_solids
&& solids
.iter()
.all(|s| matches!(group_state(&s.edges), GroupState::All)),
),
(
VisKind::Vertex,
"Vertices",
has_solids
&& solids
.iter()
.all(|s| matches!(group_state(&s.vertices), GroupState::All)),
),
];
for (kind, label, all_visible) in groups {
let tip = if all_visible {
format!("Hide all {}", label.to_lowercase())
} else {
format!("Show all {}", label.to_lowercase())
};
let resp = ui
.add_enabled_ui(has_solids, |ui| {
toolbar_button::toggle(ui, all_visible, label, &tip)
})
.inner;
self.hits.insert(format!("typevis:{label}"), resp.rect);
if resp.clicked() {
action = Some(Action::SetAllGroupVisible(kind, !all_visible));
}
}
if !sketches.is_empty() {
let all_visible = sketches.iter().all(|(_, v)| *v);
let tip = if all_visible {
"Hide all sketches"
} else {
"Show all sketches"
};
let resp = toolbar_button::toggle(ui, all_visible, "Sketches", tip);
self.hits.insert("typevis:Sketches".into(), resp.rect);
if resp.clicked() {
action = Some(Action::SetAllSketchVisible(!all_visible));
}
}
if !datums.is_empty() {
let all_visible = datums.iter().all(|(_, v)| *v);
let tip = if all_visible {
"Hide all planes & datums"
} else {
"Show all planes & datums"
};
let resp = toolbar_button::toggle(ui, all_visible, "Planes", tip);
self.hits.insert("typevis:Datums".into(), resp.rect);
if resp.clicked() {
action = Some(Action::SetAllDatumVisible(!all_visible));
}
}
});
let root_open = !self.root_collapsed;
let all_visible = !solids.is_empty() && solids.iter().all(|s| s.visible);
let mut root_vis = all_visible;
let mut root_vis_rect = egui::Rect::NOTHING;
let mut root_vis_clicked = false;
let root_resp = tree::node(
ui,
TreeRow {
guides: &[],
is_last: true,
expandable: true,
expanded: root_open,
root: true,
glyph: None,
label: "Scene",
selected: false,
draggable: false,
},
|ui| {
let cb = ui.add_enabled(!solids.is_empty(), egui::Checkbox::new(&mut root_vis, ""));
root_vis_rect = cb.rect;
root_vis_clicked = cb.clicked();
},
);
self.hits.insert("box:__scene".into(), root_resp.box_rect);
self.hits.insert("vis:__scene".into(), root_vis_rect);
if root_vis_clicked {
action = Some(Action::SetAllVisible(root_vis));
}
if root_resp.toggled || root_resp.label.clicked() {
self.root_collapsed = !self.root_collapsed;
}
if solids.is_empty() && sketches.is_empty() && datums.is_empty() {
let g = tree::child_guides(&[], true);
tree::node(ui, TreeRow::leaf(&g, true, "(scene is empty)"), |_| {});
}
if root_open {
let has_sketches = !sketches.is_empty();
let has_datums = !datums.is_empty();
let n = solids.len();
for (si, solid) in solids.iter().enumerate() {
let is_last = !has_sketches && !has_datums && si + 1 == n;
self.render_solid(ui, solid, is_last, &mut action);
}
let m = sketches.len();
for (i, (id, visible)) in sketches.iter().enumerate() {
let is_last = !has_datums && i + 1 == m;
let selected = sel_solids.contains(id);
self.render_sketch_row(ui, id, *visible, selected, is_last, &mut action);
}
if has_datums {
self.render_datums(ui, &datums, &sel_datums, &mut action);
}
}
match action {
Some(Action::SetVisible(name, v)) => {
state.set_visible(&name, v);
}
Some(Action::SetAllVisible(v)) => {
for s in &solids {
state.set_visible(&s.name, v);
}
}
Some(Action::SetEntityVisible(name, kind, index, v)) => {
state.set_entity_visible(&name, kind, index, v);
}
Some(Action::SetGroupVisible(name, kind, v)) => {
state.set_group_visible(&name, kind, v);
}
Some(Action::SetAllGroupVisible(kind, v)) => {
for s in &solids {
state.set_group_visible(&s.name, kind, v);
}
}
Some(Action::SetAllSketchVisible(v)) => {
for (id, _) in &sketches {
state.set_sketch_visible(id, v);
}
}
Some(Action::SetAllDatumVisible(v)) => {
for (name, _) in &datums {
state.set_datum_visible(name, v);
}
}
Some(Action::Select(kind, name)) => {
state.select_by_name(kind, &name);
}
Some(Action::SelectVertex(solid, pos)) => {
state.select_vertex_by_position(&solid, pos);
}
Some(Action::SetSketchVisible(id, v)) => {
state.set_sketch_visible(&id, v);
}
Some(Action::SetDatumVisible(name, v)) => {
state.set_datum_visible(&name, v);
}
Some(Action::SelectDatum(name)) => {
state.select_datum(&name);
}
None => {}
}
#[cfg(target_arch = "wasm32")]
{
publish("__brepScene", &state.scene_entities_json());
publish("__brepSketches", &state.sketch_entities_json());
publish("__brepDatums", &state.datum_entities_json());
publish("__brepSceneVis", &state.scene_visibility_json());
publish("__brepSceneHit", &self.hits_json());
}
}
fn render_sketch_row(
&mut self,
ui: &mut egui::Ui,
id: &str,
visible: bool,
selected: bool,
is_last: bool,
action: &mut Option<Action>,
) {
let mut vis = visible;
let mut vis_rect = egui::Rect::NOTHING;
let mut vis_clicked = false;
let resp = tree::node(
ui,
TreeRow::leaf(&[], is_last, id).selected(selected),
|ui| {
let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
vis_rect = cb.rect;
vis_clicked = cb.clicked();
},
);
self.hits.insert(format!("vis:sketch/{id}"), vis_rect);
self.hits.insert(format!("sel:sketch/{id}"), resp.label.rect);
if vis_clicked {
*action = Some(Action::SetSketchVisible(id.to_string(), vis));
} else if resp.label.clicked() {
*action = Some(Action::Select("solid", id.to_string()));
}
}
fn render_datums(
&mut self,
ui: &mut egui::Ui,
datums: &[(String, bool)],
sel_datums: &HashSet<String>,
action: &mut Option<Action>,
) {
let open = !self.datums_collapsed;
let resp = tree::node(
ui,
TreeRow::branch(&[], true, open, "Planes & Datums"),
|ui| {
ui.add_space(6.0);
ui.label(egui::RichText::new(format!("{}", datums.len())).weak());
},
);
self.hits.insert("box:__datums".into(), resp.box_rect);
if resp.toggled || resp.label.clicked() {
self.datums_collapsed = !self.datums_collapsed;
}
if !open {
return;
}
let base = tree::child_guides(&[], true);
let m = datums.len();
for (i, (name, visible)) in datums.iter().enumerate() {
let last = i + 1 == m;
let selected = sel_datums.contains(name);
let mut vis = *visible;
let mut vis_rect = egui::Rect::NOTHING;
let mut vis_clicked = false;
let resp = tree::node(
ui,
TreeRow::leaf(&base, last, name).selected(selected),
|ui| {
let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
vis_rect = cb.rect;
vis_clicked = cb.clicked();
},
);
self.hits.insert(format!("vis:datum/{name}"), vis_rect);
self.hits.insert(format!("sel:datum/{name}"), resp.label.rect);
if vis_clicked {
*action = Some(Action::SetDatumVisible(name.clone(), vis));
} else if resp.label.clicked() {
*action = Some(Action::SelectDatum(name.clone()));
}
}
}
fn render_solid(
&mut self,
ui: &mut egui::Ui,
solid: &SolidRow,
is_last: bool,
action: &mut Option<Action>,
) {
let name = &solid.name;
let open = !self.collapsed_solids.contains(name);
let mut vis = solid.visible;
let mut vis_rect = egui::Rect::NOTHING;
let mut vis_clicked = false;
let resp = tree::node(
ui,
TreeRow::branch(&[], is_last, open, name).selected(solid.selected),
|ui| {
let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
vis_rect = cb.rect;
vis_clicked = cb.clicked();
},
);
self.hits.insert(format!("box:{name}"), resp.box_rect);
self.hits.insert(format!("vis:{name}"), vis_rect);
self.hits.insert(format!("sel:{name}"), resp.label.rect);
if vis_clicked {
*action = Some(Action::SetVisible(name.clone(), vis));
}
if resp.toggled {
if open {
self.collapsed_solids.insert(name.clone());
} else {
self.collapsed_solids.remove(name);
}
}
if resp.label.clicked() {
*action = Some(Action::Select("solid", name.clone()));
}
if open {
let base = tree::child_guides(&[], is_last);
self.render_group(ui, name, &base, false, "Faces", VisKind::Face, &solid.faces, action);
self.render_group(ui, name, &base, false, "Edges", VisKind::Edge, &solid.edges, action);
self.render_group(ui, name, &base, true, "Vertices", VisKind::Vertex, &solid.vertices, action);
}
}
#[allow(clippy::too_many_arguments)]
fn render_group(
&mut self,
ui: &mut egui::Ui,
solid_name: &str,
base: &[bool],
is_last: bool,
group: &str,
kind: VisKind,
entities: &[Entity],
action: &mut Option<Action>,
) {
let key = format!("{solid_name}/{group}");
let open = self.expanded_groups.contains(&key);
let state = group_state(entities);
let mut checked = matches!(state, GroupState::All);
let indeterminate = matches!(state, GroupState::Partial);
let mut vis_rect = egui::Rect::NOTHING;
let mut vis_clicked = false;
let resp = tree::node(
ui,
TreeRow::branch(base, is_last, open, group),
|ui| {
let cb = ui.add(
egui::Checkbox::new(&mut checked, "").indeterminate(indeterminate),
);
vis_rect = cb.rect;
vis_clicked = cb.clicked();
ui.add_space(6.0);
ui.label(egui::RichText::new(format!("{}", entities.len())).weak());
},
);
self.hits.insert(format!("box:{key}"), resp.box_rect);
self.hits.insert(format!("vis:{key}"), vis_rect);
if vis_clicked {
let want_visible = !matches!(state, GroupState::All);
*action = Some(Action::SetGroupVisible(solid_name.to_string(), kind, want_visible));
}
if resp.toggled || resp.label.clicked() {
if open {
self.expanded_groups.remove(&key);
} else {
self.expanded_groups.insert(key.clone());
}
}
if !open {
return;
}
let gg = tree::child_guides(base, is_last);
if entities.is_empty() {
tree::node(ui, TreeRow::leaf(&gg, true, "(none)"), |_| {});
return;
}
let m = entities.len();
for (ei, e) in entities.iter().enumerate() {
let last = ei + 1 == m;
let mut vis = e.visible;
let mut ev_rect = egui::Rect::NOTHING;
let mut ev_clicked = false;
let resp = tree::node(
ui,
TreeRow::leaf(&gg, last, &e.label).selected(e.selected),
|ui| {
let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
ev_rect = cb.rect;
ev_clicked = cb.clicked();
},
);
self.hits.insert(format!("sel:{key}/{ei}"), resp.label.rect);
self.hits.insert(format!("vis:{key}/{ei}"), ev_rect);
if ev_clicked {
*action = Some(Action::SetEntityVisible(solid_name.to_string(), kind, ei, vis));
} else if resp.label.clicked() {
*action = Some(match &e.kind {
EntityKind::Face(n) => Action::Select("face", n.clone()),
EntityKind::Edge(n) => Action::Select("edge", n.clone()),
EntityKind::Vertex(p) => Action::SelectVertex(solid_name.to_string(), *p),
});
}
}
}
#[cfg(target_arch = "wasm32")]
pub fn hits_json(&self) -> String {
let map: serde_json::Map<String, serde_json::Value> = self
.hits
.iter()
.map(|(k, r)| {
(
k.clone(),
serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
)
})
.collect();
serde_json::Value::Object(map).to_string()
}
}
fn group_state(entities: &[Entity]) -> GroupState {
if entities.is_empty() {
return GroupState::All;
}
let visible = entities.iter().filter(|e| e.visible).count();
if visible == entities.len() {
GroupState::All
} else if visible == 0 {
GroupState::None
} else {
GroupState::Partial
}
}
fn snapshot(
state: &EngineState,
sel_solids: &HashSet<String>,
sel_faces: &HashSet<String>,
sel_edges: &HashSet<String>,
sel_vertices: &[brep_render::style::VertexRef],
) -> Vec<SolidRow> {
const TOL: f64 = 1e-6;
state
.scene
.solids()
.iter()
.filter(|s| !s.is_sketch)
.map(|s| {
let faces = s
.faces
.iter()
.enumerate()
.map(|(i, f)| Entity {
label: if f.name.is_empty() {
format!("Face {i}")
} else {
f.name.clone()
},
selected: !f.name.is_empty() && sel_faces.contains(&f.name),
visible: s.visibility.is_face_visible(i),
kind: EntityKind::Face(f.name.clone()),
})
.collect();
let edges = s
.edges
.iter()
.enumerate()
.map(|(i, e)| Entity {
label: if e.name.is_empty() {
format!("Edge {i}")
} else {
e.name.clone()
},
selected: !e.name.is_empty() && sel_edges.contains(&e.name),
visible: s.visibility.is_edge_visible(i),
kind: EntityKind::Edge(e.name.clone()),
})
.collect();
let vertices = s
.vertices
.iter()
.enumerate()
.map(|(i, v)| Entity {
label: format!("Vertex {i}"),
selected: sel_vertices.iter().any(|r| {
r.solid == s.name
&& (r.position[0] - v.position[0]).abs() <= TOL
&& (r.position[1] - v.position[1]).abs() <= TOL
&& (r.position[2] - v.position[2]).abs() <= TOL
}),
visible: s.visibility.is_vertex_visible(i),
kind: EntityKind::Vertex(v.position),
})
.collect();
SolidRow {
name: s.name.clone(),
visible: s.visible,
selected: sel_solids.contains(&s.name),
faces,
edges,
vertices,
}
})
.collect()
}
#[cfg(target_arch = "wasm32")]
fn publish(name: &str, json: &str) {
if let Some(win) = web_sys::window() {
let _ = js_sys::Reflect::set(
&win,
&wasm_bindgen::JsValue::from_str(name),
&wasm_bindgen::JsValue::from_str(json),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cube_history(name: &str) -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": name,
"sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
fn run_frame(
ctx: &egui::Context,
panel: &mut ScenePanel,
state: &mut EngineState,
events: Vec<egui::Event>,
scroll: bool,
) {
let raw = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::pos2(0.0, 0.0),
egui::vec2(800.0, 600.0),
)),
events,
..Default::default()
};
let _ = ctx.run_ui(raw, |ui| {
if scroll {
egui::ScrollArea::vertical().show(ui, |ui| panel.show(ui, state));
} else {
panel.show(ui, state);
}
});
}
fn click_at(
ctx: &egui::Context,
panel: &mut ScenePanel,
state: &mut EngineState,
pos: egui::Pos2,
scroll: bool,
) {
run_frame(
ctx,
panel,
state,
vec![
egui::Event::PointerMoved(pos),
egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::default(),
},
],
scroll,
);
run_frame(
ctx,
panel,
state,
vec![egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::default(),
}],
scroll,
);
}
fn drive_group_checkbox(kind: VisKind, hit_key: &str, scroll: bool) {
let ctx = egui::Context::default();
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box")).unwrap();
let mut panel = ScenePanel::new();
run_frame(&ctx, &mut panel, &mut state, vec![], scroll);
assert_eq!(
state.group_visibility("Box", kind),
Some(GroupState::All),
"precondition: every {kind:?} starts visible"
);
let rect = *panel.hits.get(hit_key).unwrap_or_else(|| {
panic!(
"missing hit rect {hit_key}; have {:?}",
panel.hits.keys().collect::<Vec<_>>()
)
});
click_at(&ctx, &mut panel, &mut state, rect.center(), scroll);
assert_eq!(
state.group_visibility("Box", kind),
Some(GroupState::None),
"clicking the {kind:?} group checkbox must hide the whole group"
);
run_frame(&ctx, &mut panel, &mut state, vec![], scroll);
let rect = *panel.hits.get(hit_key).expect("checkbox rect after hide");
click_at(&ctx, &mut panel, &mut state, rect.center(), scroll);
assert_eq!(
state.group_visibility("Box", kind),
Some(GroupState::All),
"re-clicking the {kind:?} group checkbox must re-show the whole group"
);
}
#[test]
fn scene_tree_faces_group_checkbox_hides_and_shows_through_egui() {
drive_group_checkbox(VisKind::Face, "vis:Box/Faces", false);
}
#[test]
fn scene_tree_edges_group_checkbox_hides_and_shows_through_egui() {
drive_group_checkbox(VisKind::Edge, "vis:Box/Edges", false);
}
#[test]
fn scene_tree_faces_group_checkbox_works_inside_scroll_area() {
drive_group_checkbox(VisKind::Face, "vis:Box/Faces", true);
}
#[test]
fn scene_tree_individual_face_checkbox_hides_one_face_through_egui() {
let ctx = egui::Context::default();
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box")).unwrap();
let mut panel = ScenePanel::new();
run_frame(&ctx, &mut panel, &mut state, vec![], false);
let box_rect = *panel.hits.get("box:Box/Faces").expect("faces group collapse box");
click_at(&ctx, &mut panel, &mut state, box_rect.center(), false);
run_frame(&ctx, &mut panel, &mut state, vec![], false);
assert_eq!(
state.entity_visible("Box", VisKind::Face, 0),
Some(true),
"face 0 starts visible"
);
let leaf = *panel.hits.get("vis:Box/Faces/0").unwrap_or_else(|| {
panic!(
"missing face-0 leaf checkbox; have {:?}",
panel.hits.keys().collect::<Vec<_>>()
)
});
click_at(&ctx, &mut panel, &mut state, leaf.center(), false);
assert_eq!(
state.entity_visible("Box", VisKind::Face, 0),
Some(false),
"clicking face 0's checkbox must hide exactly face 0"
);
assert_eq!(
state.entity_visible("Box", VisKind::Face, 1),
Some(true),
"sibling face 1 stays visible"
);
assert_eq!(
state.group_visibility("Box", VisKind::Face),
Some(GroupState::Partial),
"one hidden face → group reads Partial"
);
}
fn two_cube_history() -> String {
let cube = |name: &str, x: f64| {
serde_json::json!({
"type": "P.CU",
"inputParams": {
"id": name,
"sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
"transform": {
"position": [x, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
})
};
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [cube("Box1", 0.0), cube("Box2", 20.0)]
})
.to_string()
}
fn drive_type_button(kind: VisKind, hit_key: &str) {
let ctx = egui::Context::default();
let mut state = EngineState::new();
state.set_history_json(&two_cube_history()).unwrap();
let mut panel = ScenePanel::new();
run_frame(&ctx, &mut panel, &mut state, vec![], false);
for name in ["Box1", "Box2"] {
assert_eq!(
state.group_visibility(name, kind),
Some(GroupState::All),
"precondition: every {kind:?} on {name} starts visible"
);
}
let rect = *panel.hits.get(hit_key).unwrap_or_else(|| {
panic!(
"missing hit rect {hit_key}; have {:?}",
panel.hits.keys().collect::<Vec<_>>()
)
});
click_at(&ctx, &mut panel, &mut state, rect.center(), false);
for name in ["Box1", "Box2"] {
assert_eq!(
state.group_visibility(name, kind),
Some(GroupState::None),
"the scene-wide {kind:?} button must hide {name}'s whole group"
);
}
run_frame(&ctx, &mut panel, &mut state, vec![], false);
let rect = *panel.hits.get(hit_key).expect("button rect after hide");
click_at(&ctx, &mut panel, &mut state, rect.center(), false);
for name in ["Box1", "Box2"] {
assert_eq!(
state.group_visibility(name, kind),
Some(GroupState::All),
"re-clicking the scene-wide {kind:?} button must re-show {name}'s group"
);
}
}
#[test]
fn scene_type_faces_button_hides_and_shows_all_solids_through_egui() {
drive_type_button(VisKind::Face, "typevis:Faces");
}
#[test]
fn scene_type_edges_button_hides_and_shows_all_solids_through_egui() {
drive_type_button(VisKind::Edge, "typevis:Edges");
}
#[test]
fn scene_type_vertices_button_hides_and_shows_all_solids_through_egui() {
drive_type_button(VisKind::Vertex, "typevis:Vertices");
}
fn cube_and_sketch_history() -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [
{
"type": "P.CU",
"inputParams": {
"id": "Box",
"sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
},
{
"type": "S",
"inputParams": { "id": "Sk" },
"persistentData": {
"basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
"sketch": {
"points": [
{ "id": 0, "x": 0.0, "y": 0.0 },
{ "id": 1, "x": 10.0, "y": 0.0 },
{ "id": 2, "x": 10.0, "y": 6.0 },
{ "id": 3, "x": 0.0, "y": 6.0 }
],
"geometries": [
{ "id": 10, "type": "line", "points": [0, 1] },
{ "id": 11, "type": "line", "points": [1, 2] },
{ "id": 12, "type": "line", "points": [2, 3] },
{ "id": 13, "type": "line", "points": [3, 0] }
],
"constraints": []
}
}
}
]
})
.to_string()
}
#[test]
fn committed_sketch_is_a_top_level_row_not_a_group() {
let ctx = egui::Context::default();
let mut state = EngineState::new();
state.set_history_json(&cube_and_sketch_history()).unwrap();
let mut panel = ScenePanel::new();
run_frame(&ctx, &mut panel, &mut state, vec![], false);
let keys = || panel.hits.keys().cloned().collect::<Vec<_>>();
assert!(
panel.hits.contains_key("sel:sketch/Sk"),
"sketch select row present: {:?}",
keys()
);
assert!(
panel.hits.contains_key("vis:sketch/Sk"),
"sketch visibility checkbox present: {:?}",
keys()
);
assert!(
!panel.hits.contains_key("box:__sketches"),
"no Sketches group node: {:?}",
keys()
);
assert!(
!panel.hits.contains_key("box:Sk"),
"sketch is not a plain solid branch: {:?}",
keys()
);
assert!(
!panel.hits.contains_key("vis:Sk"),
"sketch is not a plain solid checkbox: {:?}",
keys()
);
assert!(
panel.hits.contains_key("box:Box"),
"cube solid branch present: {:?}",
keys()
);
}
}