feat(overlay,viewport): generation and rendering of topological navmesh

This commit is contained in:
Ellen Emilia Anna Zscheile 2025-01-08 18:46:09 +01:00
parent 6eb941a137
commit da3be763c6
7 changed files with 179 additions and 1 deletions

View File

@ -82,6 +82,7 @@ pub struct EditActions {
pub abort: Trigger, pub abort: Trigger,
pub select_all: Trigger, pub select_all: Trigger,
pub unselect_all: Trigger, pub unselect_all: Trigger,
pub recalculate_topo_navmesh: Trigger,
pub remove_bands: Trigger, pub remove_bands: Trigger,
} }
@ -118,6 +119,10 @@ impl EditActions {
egui::Key::A, egui::Key::A,
) )
.into_trigger(), .into_trigger(),
recalculate_topo_navmesh: Action::new_keyless(
tr.text("tr-menu-edit-recalculate-topo-navmesh"),
)
.into_trigger(),
remove_bands: Action::new( remove_bands: Action::new(
tr.text("tr-menu-edit-remove-bands"), tr.text("tr-menu-edit-remove-bands"),
egui::Modifiers::NONE, egui::Modifiers::NONE,
@ -146,6 +151,7 @@ impl EditActions {
self.select_all.button(ctx, ui); self.select_all.button(ctx, ui);
self.unselect_all.button(ctx, ui); self.unselect_all.button(ctx, ui);
self.recalculate_topo_navmesh.button(ctx, ui);
ui.separator(); ui.separator();
@ -205,6 +211,10 @@ impl ViewActions {
&mut menu_bar.show_navmesh, &mut menu_bar.show_navmesh,
tr.text("tr-menu-view-show-navmesh"), tr.text("tr-menu-view-show-navmesh"),
); );
ui.checkbox(
&mut menu_bar.show_topo_navmesh,
tr.text("tr-menu-view-show-topo-navmesh"),
);
ui.checkbox( ui.checkbox(
&mut menu_bar.show_bboxes, &mut menu_bar.show_bboxes,
tr.text("tr-menu-view-show-bboxes"), tr.text("tr-menu-view-show-bboxes"),

View File

@ -9,6 +9,8 @@ pub struct AppearancePanel {
// In1.Cu shall be #7fc87f (#d5ecd5 when selected). // In1.Cu shall be #7fc87f (#d5ecd5 when selected).
// In2.Cu shall be #ce7d2c (#e8c39e when selected). // In2.Cu shall be #ce7d2c (#e8c39e when selected).
pub visible: Box<[bool]>, pub visible: Box<[bool]>,
pub active_layer: usize,
} }
impl AppearancePanel { impl AppearancePanel {
@ -18,7 +20,10 @@ impl AppearancePanel {
.take(layer_count) .take(layer_count)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_boxed_slice(); .into_boxed_slice();
Self { visible } Self {
visible,
active_layer: 0,
}
} }
pub fn update(&mut self, ctx: &egui::Context, board: &Board<impl AccessMesadata>) { pub fn update(&mut self, ctx: &egui::Context, board: &Board<impl AccessMesadata>) {
@ -33,7 +38,11 @@ impl AppearancePanel {
.layer_layername(layer) .layer_layername(layer)
.unwrap_or("Unnamed layer"); .unwrap_or("Unnamed layer");
let old = *visible;
ui.checkbox(visible, layername); ui.checkbox(visible, layername);
if old != *visible {
self.active_layer = layer;
}
} }
}); });
} }

View File

@ -25,6 +25,7 @@ pub struct MenuBar {
pub is_placing_via: bool, pub is_placing_via: bool,
pub show_ratsnest: bool, pub show_ratsnest: bool,
pub show_navmesh: bool, pub show_navmesh: bool,
pub show_topo_navmesh: bool,
pub show_bboxes: bool, pub show_bboxes: bool,
pub show_origin_destination: bool, pub show_origin_destination: bool,
pub show_appearance_panel: bool, pub show_appearance_panel: bool,
@ -45,6 +46,7 @@ impl MenuBar {
is_placing_via: false, is_placing_via: false,
show_ratsnest: true, show_ratsnest: true,
show_navmesh: false, show_navmesh: false,
show_topo_navmesh: false,
show_bboxes: false, show_bboxes: false,
show_origin_destination: false, show_origin_destination: false,
show_appearance_panel: true, show_appearance_panel: true,
@ -249,6 +251,15 @@ impl MenuBar {
workspace workspace
.overlay .overlay
.select_all(board, &workspace.appearance_panel); .select_all(board, &workspace.appearance_panel);
} else if actions
.edit
.recalculate_topo_navmesh
.consume_key_triggered(ctx, ui)
{
let board = workspace.interactor.invoker().autorouter().board();
workspace
.overlay
.recalculate_topo_navmesh(board, &workspace.appearance_panel);
} else if actions.place.place_via.consume_key_enabled( } else if actions.place.place_via.consume_key_enabled(
ctx, ctx,
ui, ui,

View File

@ -13,10 +13,24 @@ use topola::{
}, },
board::{AccessMesadata, Board}, board::{AccessMesadata, Board},
geometry::shape::AccessShape, geometry::shape::AccessShape,
layout::NodeIndex,
router::planar_incr_embed,
}; };
use crate::appearance_panel::AppearancePanel; use crate::appearance_panel::AppearancePanel;
#[derive(Clone, Copy, Debug)]
pub struct PieNavmeshBase;
impl planar_incr_embed::NavmeshBase for PieNavmeshBase {
type PrimalNodeIndex = NodeIndex;
type EtchedPath = planar_incr_embed::navmesh::EdgeIndex<NodeIndex>;
type GapComment = ();
type Scalar = f64;
}
pub type PieNavmesh = planar_incr_embed::navmesh::Navmesh<PieNavmeshBase>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelectionMode { pub enum SelectionMode {
Addition, Addition,
@ -27,6 +41,7 @@ pub enum SelectionMode {
pub struct Overlay { pub struct Overlay {
ratsnest: Ratsnest, ratsnest: Ratsnest,
selection: Selection, selection: Selection,
planar_incr_navmesh: Option<PieNavmesh>,
reselect_bbox: Option<(SelectionMode, Point)>, reselect_bbox: Option<(SelectionMode, Point)>,
} }
@ -37,6 +52,7 @@ impl Overlay {
Ok(Self { Ok(Self {
ratsnest: Ratsnest::new(board.layout())?, ratsnest: Ratsnest::new(board.layout())?,
selection: Selection::new(), selection: Selection::new(),
planar_incr_navmesh: None,
reselect_bbox: None, reselect_bbox: None,
}) })
} }
@ -63,6 +79,58 @@ impl Overlay {
self.reselect_bbox = None; self.reselect_bbox = None;
} }
pub fn recalculate_topo_navmesh(
&mut self,
board: &Board<impl AccessMesadata>,
appearance_panel: &AppearancePanel,
) {
use spade::Triangulation;
use topola::router::planar_incr_embed::navmesh::TrianVertex;
if let Ok(triangulation) =
spade::DelaunayTriangulation::<TrianVertex<NodeIndex, f64>>::bulk_load(
board
.layout()
.drawing()
.rtree()
.locate_in_envelope_intersecting(&AABB::<[f64; 3]>::from_corners(
[
-f64::INFINITY,
-f64::INFINITY,
appearance_panel.active_layer as f64,
],
[
f64::INFINITY,
f64::INFINITY,
appearance_panel.active_layer as f64,
],
))
.map(|&geom| geom.data)
.filter_map(|node| {
board
.layout()
.center_of_compoundless_node(node)
.map(|pos| (node, pos))
})
.map(|(idx, pos)| TrianVertex {
idx,
pos: spade::mitigate_underflow(spade::Point2 {
x: pos.x(),
y: pos.y(),
}),
})
.collect(),
)
{
self.planar_incr_navmesh = Some(
planar_incr_embed::navmesh::NavmeshSer::<PieNavmeshBase>::from_triangulation(
&triangulation,
)
.into(),
);
}
}
pub fn drag_start( pub fn drag_start(
&mut self, &mut self,
_board: &Board<impl AccessMesadata>, _board: &Board<impl AccessMesadata>,
@ -167,6 +235,10 @@ impl Overlay {
&self.selection &self.selection
} }
pub fn planar_incr_navmesh(&self) -> Option<&PieNavmesh> {
self.planar_incr_navmesh.as_ref()
}
/// Returns the currently selected bounding box of a bounding-box reselect /// Returns the currently selected bounding box of a bounding-box reselect
pub fn get_bbox_reselect( pub fn get_bbox_reselect(
&self, &self,

View File

@ -274,6 +274,78 @@ impl Viewport {
} }
} }
if menu_bar.show_topo_navmesh {
if let Some(navmesh) = workspace.overlay.planar_incr_navmesh() {
// calculate dual node position approximations
use std::collections::BTreeMap;
use topola::geometry::shape::AccessShape;
use topola::router::planar_incr_embed::NavmeshIndex;
let mut map = BTreeMap::new();
let resolve_primal = |p: &topola::layout::NodeIndex| {
board.layout().node_shape(*p).center()
};
let root_bbox3d =
board.layout().drawing().rtree().root().envelope();
let root_bbox = AABB::<[f64; 2]>::from_corners(
[root_bbox3d.lower()[0], root_bbox3d.lower()[1]].into(),
[root_bbox3d.upper()[0], root_bbox3d.upper()[1]].into(),
);
for (nidx, node) in &*navmesh.nodes {
if let NavmeshIndex::Dual(didx) = nidx {
map.insert(
didx,
geo::point! { x: node.pos.x, y: node.pos.y },
);
}
}
for (eidx, edge) in &*navmesh.edges {
// TODO: display edge contents, too
let (a, b) = eidx.clone().into();
let mut got_primal = false;
let a_pos = match a {
NavmeshIndex::Primal(p) => {
got_primal = true;
resolve_primal(&p)
}
NavmeshIndex::Dual(d) => match map.get(&d) {
None => continue,
Some(x) => x.clone(),
},
};
let b_pos = match b {
NavmeshIndex::Primal(p) => {
got_primal = true;
resolve_primal(&p)
}
NavmeshIndex::Dual(d) => match map.get(&d) {
None => continue,
Some(x) => x.clone(),
},
};
let edge_len = navmesh.edge_paths[edge.1].len();
use egui::Color32;
let stroke = if edge_len == 0 {
egui::Stroke::new(
1.0,
if got_primal {
Color32::from_rgb(255, 175, 0)
} else {
Color32::from_rgb(159, 255, 33)
},
)
} else {
egui::Stroke::new(
1.5 + (edge_len as f32).atan(),
Color32::from_rgb(250, 250, 0),
)
};
painter.paint_edge(a_pos, b_pos, stroke);
}
}
}
if menu_bar.show_bboxes { if menu_bar.show_bboxes {
let root_bbox3d = board.layout().drawing().rtree().root().envelope(); let root_bbox3d = board.layout().drawing().rtree().root().envelope();

View File

@ -15,12 +15,14 @@ tr-menu-edit-redo = Redo
tr-menu-edit-abort = Abort tr-menu-edit-abort = Abort
tr-menu-edit-select-all = Select All tr-menu-edit-select-all = Select All
tr-menu-edit-unselect-all = Unselect All tr-menu-edit-unselect-all = Unselect All
tr-menu-edit-recalculate-topo-navmesh = Recalculate Topological Navmesh
tr-menu-edit-remove-bands = Remove Bands tr-menu-edit-remove-bands = Remove Bands
tr-menu-view = View tr-menu-view = View
tr-menu-view-zoom-to-fit = Zoom to Fit tr-menu-view-zoom-to-fit = Zoom to Fit
tr-menu-view-show-ratsnest = Show Ratsnest tr-menu-view-show-ratsnest = Show Ratsnest
tr-menu-view-show-navmesh = Show Navmesh tr-menu-view-show-navmesh = Show Navmesh
tr-menu-view-show-topo-navmesh = Show Topological Navmesh
tr-menu-view-show-bboxes = Show BBoxes tr-menu-view-show-bboxes = Show BBoxes
tr-menu-view-show-origin-destination = Show OriginDestination tr-menu-view-show-origin-destination = Show OriginDestination
tr-menu-view-show-layer-manager = Show Layer Manager tr-menu-view-show-layer-manager = Show Layer Manager

View File

@ -12,3 +12,5 @@ mod router;
pub use route::RouteStepper; pub use route::RouteStepper;
pub use router::*; pub use router::*;
pub use planar_incr_embed;