use super::*;
pub(super) fn raw_wheel_delta_y(ctx: &egui::Context) -> f32 {
const LINE_POINTS: f32 = 40.0;
ctx.input(|i| {
i.events
.iter()
.filter_map(|event| match event {
egui::Event::MouseWheel { unit, delta, .. } => Some(match unit {
egui::MouseWheelUnit::Point => delta.y,
egui::MouseWheelUnit::Line => delta.y * LINE_POINTS,
egui::MouseWheelUnit::Page => delta.y * LINE_POINTS * 20.0,
}),
_ => None,
})
.sum()
})
}
const DWELL_FLOOR_SECS: f64 = 0.25;
fn dwell_secs(ctx: &egui::Context) -> f64 {
(f64::from(ctx.global_style().interaction.tooltip_delay)).max(DWELL_FLOOR_SECS)
}
impl Viewport {
pub fn last_rect(&self) -> Option<egui::Rect> {
self.last_rect
}
pub fn viewport_rect_json(&self) -> String {
match self.last_rect {
Some(r) => {
serde_json::json!({ "x": r.min.x, "y": r.min.y, "w": r.width(), "h": r.height() })
.to_string()
}
None => "null".to_string(),
}
}
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
if crate::workbench::ecad::Target::of_workbench(&state.settings.workbench).is_some() {
if state.sketch_mode() {
let _ = state.exit_sketch_mode(true);
}
if state.sketch3d_mode() {
let _ = state.exit_sketch3d_mode(true);
}
}
let ppp = ui.ctx().pixels_per_point();
egui::containers::panel::CentralPanel::default()
.frame(egui::Frame::NONE)
.show(ui, |ui| {
let rect = ui.available_rect_before_wrap();
self.last_rect = Some(rect);
let response = ui.allocate_rect(rect, egui::Sense::click_and_drag());
let phys_w = (rect.width() * ppp).round().max(1.0) as u32;
let phys_h = (rect.height() * ppp).round().max(1.0) as u32;
self.ensure_offscreen(phys_w, phys_h);
state.resize(rect.width() as f64, rect.height() as f64);
if self.show_sheet(ui, rect, &response, state) {
return;
}
self.handle_viewport_input(ui, rect, &response, state);
let request = self.component_menu_request(rect, &response, state);
self.show_component_menu(ui.ctx(), state, request);
{
let _span = crate::perf::span(crate::perf::Phase::Overlays);
state.ensure_overlays_current();
}
let render = state.dirty || std::mem::take(&mut self.force_render);
if render {
self.render_viewport(phys_w, phys_h, ppp, state);
ui.ctx().request_repaint();
}
#[cfg(feature = "automation")]
self.check_stale_frame(render, phys_w, phys_h, ppp, state);
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
ViewportCallback,
));
if super::touch::enabled(ui.ctx()) && state.sketch_mode()
&& state.sketch_active_tool().is_some_and(|tool| tool != "pickEdges" && tool != "handdraw")
&& !self.touch_suppressed
{
if let Some(finger) = ui.input(|i| i.pointer.interact_pos()).filter(|p| rect.contains(*p)) {
let (x, y) = super::touch::sketch_target(state,
f64::from(finger.x - rect.left()), f64::from(finger.y - rect.top()), true);
let cursor = rect.min + egui::vec2(x as f32, y as f32);
let painter = ui.painter().with_clip_rect(rect);
painter.line_segment([finger, cursor], egui::Stroke::new(1.5, egui::Color32::LIGHT_BLUE));
painter.circle_stroke(cursor, 8.0, egui::Stroke::new(3.0, egui::Color32::BLACK));
painter.circle_stroke(cursor, 8.0, egui::Stroke::new(1.0, egui::Color32::WHITE));
for axis in [egui::vec2(11.0, 0.0), egui::vec2(0.0, 11.0)] {
painter.line_segment([cursor - axis, cursor + axis], egui::Stroke::new(1.0, egui::Color32::WHITE));
}
}
}
});
let ctx = ui.ctx().clone();
if self.chrome_obscured {
self.candidate_popup = None;
self.candidate_popup_rect = None;
} else { self.show_candidate_popup(&ctx, state); }
let sheet_open = state.sheet_open().is_some();
self.move_card.hits.clear();
if let Some(rect) = self.last_rect.filter(|_| !sheet_open && !self.chrome_obscured) {
self.draw_dimension_labels(&ctx, rect, state);
self.draw_feature_dimension_labels(&ctx, rect, state);
Self::draw_transform_axis_labels(&ctx, rect, state);
self.draw_transform_readout(&ctx, rect, state);
self.move_card.draw(&ctx, rect, state);
self.draw_constraint_labels(&ctx, rect, state);
self.draw_pmi_labels(&ctx, rect, state);
self.draw_gizmo_hit_areas(&ctx, rect, state);
}
if self.candidate_popup.is_some() || state.dirty {
ctx.request_repaint();
}
if crate::automation::registry::enabled() {
crate::automation::registry::publish("__brepHover", "hovered entity", &state.hovered_json());
crate::automation::registry::publish("__brepHoverDwell", "hover dwell {armed, age, threshold} — armed = a plain click opens the pick list instead of selecting the highlight", &self.hover_dwell_json(&ctx));
crate::automation::registry::publish("__brepCandidates", "open pick-candidate popup entries [{index,kind,name,solid,depth}]", &self.candidates_json(state));
crate::automation::registry::publish("__brepCandidateHit", "pick-candidate popup entry rects", &self.candidate_hits_json());
crate::automation::registry::publish("__brepSelection", "selection {solids, faces, edges, datums, vertices}; the name arrays are in pick order, oldest first", &state.selection_json());
crate::automation::registry::publish("__brepFeatureDim", "dimension gizmo state (mode, annotations)", &state.feature_dimension_state_json());
crate::automation::registry::publish("__brepGizmoReadout", "transform gizmo readout {active, kind, axis, value, unit, text, origin, dragging, refused, motion, reason, hint, kernel} \u{2014} the live drag's distance/angle and, on a refusal, why it refused", &state.transform_readout_json());
crate::automation::registry::publish("__brepConstraints", "assembly constraint overlay labels", &state.constraint_labels_json());
crate::automation::registry::publish("__brepFreedomGlyph", "free-motion glyph at the selected component {component, center, slides: [[x,y,z]], axes: [{point, direction}]} or null", &state.freedom_glyph_value().to_string());
crate::automation::registry::publish("__brepPmiLabels", "PMI label chips of the active view", &state.pmi_labels_json());
crate::automation::registry::publish("__brepPmiLabelHit", "PMI label chip rects {id: [x,y,w,h]}", &self.pmi_label_hits_json());
crate::automation::registry::publish("__brepPmiCenterMarks", "center marks the PMI overlay drew {viewDir, marks: [{id, arms: [[u, v] per mark]}]} \u{2014} unit world directions of each cross's arms (f64), and the view direction of that bake", &state.pmi_center_marks_json());
crate::automation::registry::publish("__brepMoveCardHit", "the Move gizmo's typed-value card rects in screen points (card, field:x|y|z|rx|ry|rz) while the component Move gizmo is armed", &crate::automation::hit_rects::hits_json(&self.move_card.hits));
crate::automation::registry::publish("__brepComponentMenu", "the component right-click menu: {open: ACOMP id or null, last: the last right-click's hit and owning component}", &serde_json::json!({ "open": self.component_menu.as_ref().map(|(id, _)| id), "last": self.component_menu_probe }).to_string());
crate::automation::registry::publish("__brepComponentMenuHit", "open component right-click menu entry rects (menuitem:<ACOMP id>:<action id>)", &crate::automation::hit_rects::hits_json(&self.component_menu_hits));
let chip_hits: std::collections::HashMap<String, egui::Rect> = self.constraint_chips_drawn.iter().cloned().collect();
crate::automation::registry::publish("__brepConstraintLabelHit", "assembly-constraint chip rects {id: [x,y,w,h]}", &crate::automation::hit_rects::hits_json(&chip_hits));
crate::automation::registry::publish("__brepConstraintMenuHit", "open assembly-constraint chip right-click menu entry rects (menuitem:<constraint id>:<action id>)", &crate::automation::hit_rects::hits_json(&self.constraint_menu_hits));
crate::automation::registry::publish("__brepPmiMenuHit", "open PMI label chip right-click menu entry rects (menuitem:<annotation id>:<action id>)", &crate::automation::hit_rects::hits_json(&self.pmi_menu_hits));
crate::automation::registry::publish("__brepGizmoHit", "armed gizmo handle rects in screen points (anchor)", &gizmo_anchor_hits_json(self.last_rect, state));
crate::automation::registry::publish("__brepCageHit", "control-cage dot rects in screen points (point:<index>) while a NURBS Solid's form is open", &cage_point_hits_json(self.last_rect, state));
crate::automation::registry::publish("__brepConstraintHit", "draggable assembly-constraint handle rects in screen points (by constraint id)", &constraint_handle_hits_json(self.last_rect, state));
crate::automation::registry::publish("__brepViewCube", "ViewCube rect (viewport-local)", &state.viewcube_rect_json());
crate::automation::registry::publish("__brepSheetHit", "open sheet's widget rects in screen points (paper, view:<id>, dim:<id>, ord:<id>; anchor:<reference> while the reference picker is up for a sheet object)", &self.sheet_hits_json());
}
self.publish_no_ecad();
}
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
fn sheet_hits_json(&self) -> String {
self.sheet.hits_json()
}
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
fn pmi_label_hits_json(&self) -> String {
let map: serde_json::Map<String, serde_json::Value> = self
.pmi_label_hits
.iter()
.map(|(id, rect)| {
(
id.clone(),
serde_json::json!([rect.min.x, rect.min.y, rect.width(), rect.height()]),
)
})
.collect();
serde_json::Value::Object(map).to_string()
}
fn viewcube_local(&self, state: &EngineState, x: f64, y: f64) -> Option<(f64, f64)> {
viewcube_local(state, x, y)
}
fn handle_sketch3d_input(
&mut self,
ui: &egui::Ui,
rect: egui::Rect,
response: &egui::Response,
state: &mut EngineState,
) {
let local = |p: egui::Pos2| ((p.x - rect.min.x) as f64, (p.y - rect.min.y) as f64);
let draw_mode = state.sketch3d_active_tool().is_some();
if draw_mode && response.secondary_clicked() {
state.sketch3d_tool_cancel();
}
if response.drag_started() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
if let Some((cx, cy)) = self.viewcube_local(state, lx, ly) {
state.viewcube_click(cx, cy);
} else if !draw_mode && state.sketch3d_drag_begin(lx, ly) {
self.sketch3d_dragging = true;
} else {
let btn = if response.dragged_by(egui::PointerButton::Secondary) {
BUTTON_RIGHT
} else if response.dragged_by(egui::PointerButton::Middle) {
BUTTON_MIDDLE
} else {
BUTTON_LEFT
};
state.pointer_down(lx, ly, btn);
self.dragging = true;
}
}
}
if response.dragged() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
if self.sketch3d_dragging {
state.sketch3d_drag_to(lx, ly);
} else if self.dragging {
state.pointer_move(lx, ly);
}
}
}
if response.drag_stopped() {
if self.sketch3d_dragging {
state.sketch3d_drag_end();
self.sketch3d_dragging = false;
}
if self.dragging {
state.pointer_up();
self.dragging = false;
}
}
if response.clicked() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
if let Some((cx, cy)) = self.viewcube_local(state, lx, ly) {
state.viewcube_click(cx, cy);
} else if draw_mode {
state.sketch3d_tool_click_at(lx, ly);
} else {
let mods = ui.ctx().input(|i| i.modifiers);
state.sketch3d_click_at(lx, ly, mods.command || mods.ctrl);
}
}
}
let primary_held = ui.input(|i| i.pointer.primary_down());
if !self.sketch3d_dragging && !self.dragging && !(primary_held && !draw_mode) {
match response.hover_pos() {
Some(pos) => {
let (lx, ly) = local(pos);
match self.viewcube_local(state, lx, ly) {
Some((cx, cy)) => {
state.viewcube_hover(cx, cy);
state.sketch3d_clear_hover();
}
None => {
state.viewcube_clear_hover();
state.sketch3d_hover_at(lx, ly);
if draw_mode {
state.hover_at(lx, ly);
}
}
}
}
None => {
state.viewcube_clear_hover();
state.sketch3d_clear_hover();
}
}
}
if response.hovered() {
let scroll_y = raw_wheel_delta_y(ui.ctx());
if scroll_y != 0.0 {
let cursor = response.hover_pos().map(|p| {
let (lx, ly) = local(p);
[lx, ly]
});
state.wheel(-(scroll_y as f64), cursor);
}
}
}
fn handle_sketch_input(
&mut self,
ui: &egui::Ui,
rect: egui::Rect,
response: &egui::Response,
state: &mut EngineState,
) {
let local = |p: egui::Pos2| ((p.x - rect.min.x) as f64, (p.y - rect.min.y) as f64);
let draw_mode = state.sketch_active_tool().is_some();
let finger = super::touch::enabled(ui.ctx());
let precision = finger && draw_mode
&& !matches!(state.sketch_active_tool(), Some("handdraw" | "pickEdges"));
if draw_mode && response.secondary_clicked() {
state.sketch_tool_cancel();
}
if response.drag_started() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
if let Some((cx, cy)) = self.viewcube_local(state, lx, ly) {
state.viewcube_click(cx, cy);
} else if state.sketch_active_tool() == Some("handdraw") {
state.sketch_handdraw_begin(lx, ly);
self.sketch_handdrawing = true;
} else if precision {
self.touch_drawing = true;
} else if !draw_mode && {
self.touch_drag_offset = (0.0, 0.0);
let (gx, gy) = if finger {
let point = super::touch::sketch_target(state, lx, ly, false);
state.sketch_hover_at(point.0, point.1);
self.touch_drag_offset = (point.0 - lx, point.1 - ly);
point
} else { (lx, ly) };
state.sketch_drag_begin(gx, gy)
} {
self.sketch_dragging = true;
} else {
let btn = if response.dragged_by(egui::PointerButton::Secondary) {
BUTTON_RIGHT
} else if response.dragged_by(egui::PointerButton::Middle) {
BUTTON_MIDDLE
} else {
BUTTON_LEFT
};
state.pointer_down(lx, ly, btn);
self.dragging = true;
}
}
}
if response.dragged() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
if self.sketch_handdrawing {
state.sketch_handdraw_move(lx, ly);
} else if self.touch_drawing {
let (x, y) = super::touch::sketch_target(state, lx, ly, true);
state.sketch_hover_at(x, y);
} else if self.sketch_dragging {
if finger {
state.sketch_drag_to(lx + self.touch_drag_offset.0, ly + self.touch_drag_offset.1);
} else {
state.sketch_drag_to(lx, ly);
}
} else if self.dragging {
state.pointer_move(lx, ly);
}
}
}
if response.drag_stopped() {
if self.touch_drawing {
if let Some(pos) = response.interact_pointer_pos() {
let (x, y) = local(pos);
let point = super::touch::sketch_target(state, x, y, true);
state.sketch_tool_click_at(point.0, point.1);
}
self.touch_drawing = false;
}
if self.sketch_handdrawing {
state.sketch_handdraw_end();
self.sketch_handdrawing = false;
}
if self.sketch_dragging {
state.sketch_drag_end();
self.sketch_dragging = false;
}
if self.dragging {
state.pointer_up();
self.dragging = false;
}
}
if response.clicked() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
if let Some((cx, cy)) = self.viewcube_local(state, lx, ly) {
state.viewcube_click(cx, cy);
} else if state.sketch_active_tool() == Some("handdraw") {
} else if state.sketch_active_tool() == Some("pickEdges") {
state.sketch_pick_edge_at(lx, ly);
} else if draw_mode {
let point = if precision { super::touch::sketch_target(state, lx, ly, true) } else { (lx, ly) };
state.sketch_tool_click_at(point.0, point.1);
} else {
let mods = ui.ctx().input(|i| i.modifiers);
let point = if finger { super::touch::sketch_target(state, lx, ly, false) } else { (lx, ly) };
state.sketch_click_at(point.0, point.1, mods.command || mods.ctrl);
}
}
}
let primary_held = ui.input(|i| i.pointer.primary_down());
if !self.sketch_dragging
&& !self.dragging
&& !self.sketch_handdrawing
&& !self.touch_drawing
&& !(primary_held && !draw_mode)
{
match response.hover_pos() {
Some(pos) => {
let (lx, ly) = local(pos);
match self.viewcube_local(state, lx, ly) {
Some((cx, cy)) => {
state.viewcube_hover(cx, cy);
state.sketch_clear_hover(); }
None => {
state.viewcube_clear_hover();
let point = if finger { super::touch::sketch_target(state, lx, ly, precision) } else { (lx, ly) };
state.sketch_hover_at(point.0, point.1);
if state.sketch_active_tool() == Some("pickEdges") {
state.hover_at(lx, ly);
}
}
}
}
None => {
state.viewcube_clear_hover();
if !state.take_sketch_list_hover() {
state.sketch_clear_hover();
}
}
}
}
if response.hovered() {
let scroll_y = raw_wheel_delta_y(ui.ctx());
if scroll_y != 0.0 {
let cursor = response.hover_pos().map(|p| {
let (lx, ly) = local(p);
[lx, ly]
});
state.wheel(-(scroll_y as f64), cursor);
}
}
}
fn component_menu_request(
&mut self,
rect: egui::Rect,
response: &egui::Response,
state: &mut EngineState,
) -> Option<(String, egui::Pos2)> {
if !response.secondary_clicked()
|| state.ref_select_active()
|| state.sketch_mode()
|| state.sketch3d_mode()
{
return None;
}
let pos = response.interact_pointer_pos()?;
let (lx, ly) = ((pos.x - rect.min.x) as f64, (pos.y - rect.min.y) as f64);
let hit: serde_json::Value = serde_json::from_str(&state.hover_json(lx, ly)).unwrap_or_default();
let component = hit["solid"]
.as_str()
.or_else(|| hit["name"].as_str())
.and_then(|solid| state.component_of_solid(solid));
self.component_menu_probe = serde_json::json!({ "at": [pos.x, pos.y], "hit": hit, "component": component });
let component = component?;
state.select_component(&component);
Some((component, pos))
}
fn show_component_menu(
&mut self,
ctx: &egui::Context,
state: &mut EngineState,
request: Option<(String, egui::Pos2)>,
) {
use crate::panels::component_actions::{component_menu, run_component_actions, ComponentAction, MenuSubject};
self.component_menu_hits.clear();
let mut chosen: Option<(String, ComponentAction)> = None;
if let Some((id, pos)) = self.component_menu.clone() {
let instance = brep_render::brep_kernel::pattern_component_instance(&id);
let seed = instance.as_ref().map_or(id.clone(), |(_, seed, _)| seed.clone());
if state.component_info(&seed).is_some() {
let actions = component_menu(&MenuSubject::of(state, std::slice::from_ref(&id)));
let hits = &mut self.component_menu_hits;
let (action, still_open) = crate::column_tree::action_menu(
ctx,
egui::LayerId::new(egui::Order::Foreground, egui::Id::new("brep-component-menu-layer")),
egui::Id::new("brep-component-menu"),
pos,
&actions,
|entry, rect| {
hits.insert(format!("menuitem:{id}:{}", entry.id), rect);
},
);
if !still_open {
self.component_menu = None;
}
chosen = action.and_then(|action| ComponentAction::from_id(&action)).map(|action| (id, action));
} else {
self.component_menu = None;
}
}
if let Some((id, action)) = chosen {
if let Some(request) = run_component_actions(state, action, &[id]) {
self.component_request = Some(request);
}
}
if let Some(request) = request {
self.component_menu = Some(request);
}
}
pub fn take_empty_double_click(&mut self) -> bool {
std::mem::take(&mut self.empty_double_click)
}
pub fn take_component_request(&mut self) -> Option<crate::panels::component_actions::ComponentActionRequest> {
self.component_request.take()
}
fn handle_viewport_input(
&mut self,
ui: &egui::Ui,
rect: egui::Rect,
response: &egui::Response,
state: &mut EngineState,
) {
let local = |p: egui::Pos2| ((p.x - rect.min.x) as f64, (p.y - rect.min.y) as f64);
let (events, focused, any_touches) =
ui.ctx().input(|i| (i.events.clone(), i.focused, i.any_touches()));
let touch = self.touch.update_for_frame(
ui.ctx().cumulative_frame_nr(),
&events,
focused,
any_touches,
|pos| {
ui.is_enabled()
&& rect.contains(pos)
&& ui.ctx().layer_id_at(pos) == Some(response.layer_id)
},
);
self.touch_suppressed = touch.suppress_pointer;
if touch.started {
self.finish_pointer_drag(state);
self.hover_lit_since = None;
state.clear_hover();
state.sketch_clear_hover();
state.viewcube_clear_hover();
}
if touch.suppress_pointer {
if let Some((from, to, scale)) = touch.motion {
let (fx, fy) = local(from);
let (tx, ty) = local(to);
state.touch_navigation([fx, fy], [tx, ty], scale);
}
return;
}
if response.long_touched() && !state.any_sketch_mode() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
let reference = state.ref_select_active();
let candidates = if reference {
state.ref_select_candidates_at(lx, ly)
} else {
state.candidates_filtered_at(lx, ly)
};
self.candidate_popup = (!candidates.is_empty())
.then(|| CandidatePopup { anchor: pos, candidates, reference });
self.candidate_popup_fresh = self.candidate_popup.is_some();
self.hover_lit_since = None;
}
return;
}
if state.sketch_mode() {
self.handle_sketch_input(ui, rect, response, state);
return;
}
if state.sketch3d_mode() {
self.handle_sketch3d_input(ui, rect, response, state);
return;
}
if response.drag_started() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
state.clear_hover();
self.hover_lit_since = None;
let (mut gx, mut gy) = drag_start_local(response, rect).unwrap_or((lx, ly));
self.touch_drag_offset = (0.0, 0.0);
if super::touch::enabled(ui.ctx()) && self.viewcube_local(state, gx, gy).is_none() {
if let Some((tx, ty)) = super::touch::handle_target(state, gx, gy) {
self.touch_drag_offset = (tx - gx, ty - gy);
(gx, gy) = (tx, ty);
}
}
match route_drag_start(state, gx, gy) {
DragStart::ViewCube => {}
DragStart::Gizmo => self.gizmo_dragging = true,
DragStart::Component => self.component_gizmo_dragging = true,
DragStart::Dimension(field) => self.dim_dragging = Some(field),
DragStart::Constraint => self.constraint_dragging = true,
DragStart::Camera => {
let btn = if response.dragged_by(egui::PointerButton::Secondary) {
BUTTON_RIGHT
} else if response.dragged_by(egui::PointerButton::Middle) {
BUTTON_MIDDLE
} else {
BUTTON_LEFT
};
state.pointer_down(lx, ly, btn);
self.dragging = true;
}
}
}
}
if response.dragged() {
if let Some(pos) = response.interact_pointer_pos() {
let (lx, ly) = local(pos);
let (lx, ly) = (lx + self.touch_drag_offset.0, ly + self.touch_drag_offset.1);
if self.gizmo_dragging {
state.transform_drag_to(lx, ly);
} else if self.component_gizmo_dragging {
state.component_drag_to(lx, ly);
} else if let Some(field) = self.dim_dragging.clone() {
let feature = state.dimension_armed_feature();
if !feature.is_empty() {
state.feature_dimension_drag(&feature, &field, lx, ly);
}
} else if self.constraint_dragging {
state.constraint_drag_to(lx, ly);
} else if self.dragging {
state.pointer_move(lx, ly);
}
}
}
if response.drag_stopped() {
if let (Some(field), Some(pos)) =
(self.dim_dragging.as_ref(), response.interact_pointer_pos())
{
let (lx, ly) = local(pos);
let (lx, ly) = (lx + self.touch_drag_offset.0, ly + self.touch_drag_offset.1);
let feature = state.dimension_armed_feature();
state.feature_dimension_drag(&feature, field, lx, ly);
}
self.finish_pointer_drag(state);
}
if response.clicked() {
if let Some(pos) = response.interact_pointer_pos() {
let on_popup = self.candidate_popup.is_some()
&& self
.candidate_popup_rect
.map(|r| r.contains(pos))
.unwrap_or(false);
let (lx, ly) = local(pos);
let mods = ui.ctx().input(|i| i.modifiers);
let (lx, ly) = if super::touch::enabled(ui.ctx()) && self.viewcube_local(state, lx, ly).is_none() {
super::touch::handle_target(state, lx, ly).unwrap_or((lx, ly))
} else { (lx, ly) };
if on_popup {
} else if self.candidate_popup.is_some() {
self.candidate_popup = None;
self.candidate_popup_rect = None;
} else if let Some((cx, cy)) = self.viewcube_local(state, lx, ly) {
state.viewcube_click(cx, cy);
} else if state.ref_select_active() {
if mods.alt || self.hover_dwell_armed(ui.ctx()) {
let cands = state.ref_select_candidates_at(lx, ly);
self.candidate_popup = (!cands.is_empty())
.then(|| CandidatePopup { anchor: pos, candidates: cands, reference: true });
self.candidate_popup_fresh = self.candidate_popup.is_some();
} else {
state.ref_select_click(lx, ly);
}
self.hover_lit_since = None;
} else if state.transform_center_pick(lx, ly) {
state.toggle_to_dimension();
} else if state.dimension_origin_pick(lx, ly) {
state.toggle_to_transform();
} else if state.dimension_arrow_pick(lx, ly).is_some() {
} else if state.constraint_arrow_pick(lx, ly).is_some() {
} else if (state.transform_armed() || state.component_move_armed())
&& state.transform_pick(lx, ly) != 0
{
} else if mods.alt {
let cands = state.candidates_filtered_at(lx, ly);
self.candidate_popup = (!cands.is_empty())
.then(|| CandidatePopup { anchor: pos, candidates: cands, reference: false });
self.candidate_popup_fresh = self.candidate_popup.is_some();
self.hover_lit_since = None;
} else if state.cage_edit_feature().is_some()
&& state
.cage_point_pick_at(lx, ly, mods.command || mods.ctrl || mods.shift)
.is_some()
{
} else if mods.command || mods.ctrl {
state.select_toggle_at(lx, ly);
} else if state.spline_anchor_pick_at(lx, ly).is_some() {
} else if self.hover_dwell_armed(ui.ctx()) {
let cands = state.candidates_filtered_at(lx, ly);
self.candidate_popup = (!cands.is_empty())
.then(|| CandidatePopup { anchor: pos, candidates: cands, reference: false });
self.candidate_popup_fresh = self.candidate_popup.is_some();
self.hover_lit_since = None;
} else {
let toggles =
state.settings.multi_select == MultiSelectMode::ClickToggles;
if let Some(context) = state.context_hit_at(lx, ly) {
state.clear_selection();
state.select_context(Some(context));
} else {
match state.nearest_candidate_at(lx, ly) {
Some(candidate) => {
state.select_context(None);
if toggles {
state.toggle_candidate(&candidate);
} else {
state.select_candidate(&candidate);
}
}
None => {
state.clear_selection();
self.empty_double_click |= response.double_clicked();
}
}
}
self.hover_lit_since = None;
}
}
}
let label_hover = state.take_constraint_label_hover() | state.take_pmi_label_hover();
let tree_hover = state.take_scene_tree_hover();
let dialog_hover = state.take_dialog_hover();
if !self.dragging
&& !self.gizmo_dragging
&& !self.component_gizmo_dragging
&& self.dim_dragging.is_none()
&& !self.constraint_dragging
&& self.pmi_label_dragging.is_none()
&& !label_hover
{
match response.hover_pos() {
Some(pos) => {
let (lx, ly) = local(pos);
match self.viewcube_local(state, lx, ly) {
Some((cx, cy)) => {
state.viewcube_hover(cx, cy);
state.clear_hover(); self.hover_lit_since = None;
}
None => {
state.viewcube_clear_hover();
let over_handle = (state.transform_armed()
|| state.component_move_armed())
&& state.transform_hover(lx, ly) != 0;
if over_handle {
state.clear_hover();
self.hover_lit_since = None;
} else if self.candidate_popup.is_none() {
let changed = state.hover_at(lx, ly);
let lit = state.hover_is_lit();
self.note_hover(ui.ctx(), pos, lit, changed);
} else {
self.hover_lit_since = None;
}
}
}
}
None => {
state.viewcube_clear_hover();
if self.candidate_popup.is_none() && !tree_hover && !dialog_hover {
state.clear_hover();
}
self.hover_lit_since = None;
}
}
} else {
self.hover_lit_since = None;
}
if response.hovered() {
let scroll_y = raw_wheel_delta_y(ui.ctx());
if scroll_y != 0.0 {
let cursor = response.hover_pos().map(|p| {
let (lx, ly) = local(p);
[lx, ly]
});
state.wheel(-(scroll_y as f64), cursor);
}
}
}
fn finish_pointer_drag(&mut self, state: &mut EngineState) {
self.touch_drag_offset = (0.0, 0.0);
self.touch_drawing = false;
if self.gizmo_dragging {
state.transform_release();
self.gizmo_dragging = false;
}
if self.component_gizmo_dragging {
state.component_release();
self.component_gizmo_dragging = false;
}
if self.dim_dragging.take().is_some() {
let feature = state.dimension_armed_feature();
state.feature_dimension_release(&feature);
}
if self.constraint_dragging {
state.constraint_drag_release();
self.constraint_dragging = false;
}
if self.sketch_handdrawing {
state.sketch_handdraw_end();
self.sketch_handdrawing = false;
}
if self.sketch_dragging {
state.sketch_drag_end();
self.sketch_dragging = false;
}
if self.dragging {
state.pointer_up();
self.dragging = false;
}
}
fn hover_dwell_armed(&self, ctx: &egui::Context) -> bool {
let Some(since) = self.hover_lit_since else {
return false; };
let dwell = dwell_secs(ctx);
ctx.input(|i| i.time - since >= dwell)
}
fn note_hover(&mut self, ctx: &egui::Context, pos: egui::Pos2, lit: bool, changed: bool) {
const SLOP: f32 = 2.0;
let moved = self
.hover_last_pos
.is_none_or(|last| last.distance(pos) > SLOP);
if moved {
self.hover_last_pos = Some(pos);
}
if !lit {
self.hover_lit_since = None;
return;
}
let now = ctx.input(|i| i.time);
if changed || moved || self.hover_lit_since.is_none() {
self.hover_lit_since = Some(now);
}
if self.hover_dwell_armed(ctx) {
ctx.set_cursor_icon(egui::CursorIcon::ContextMenu);
return;
}
let dwell = dwell_secs(ctx);
let since = self.hover_lit_since.unwrap_or(now);
let remaining = ctx.input(|i| dwell - (i.time - since));
if remaining > 0.0 {
ctx.request_repaint_after(std::time::Duration::from_secs_f64(remaining.min(dwell)));
}
}
fn show_candidate_popup(&mut self, ctx: &egui::Context, state: &mut EngineState) {
self.candidate_hits.clear();
let reference = state.ref_select_active();
if state.any_sketch_mode()
|| self.candidate_popup.as_ref().is_some_and(|p| p.reference != reference)
{
self.candidate_popup = None;
}
let Some(popup) = self.candidate_popup.as_ref() else {
self.candidate_popup_rect = None;
return;
};
let candidates = popup.candidates.clone();
let anchor = popup.anchor;
let mods = ctx.input(|i| i.modifiers);
let selected_rows: Vec<bool> = candidates
.iter()
.map(|c| if reference { state.ref_select_has_candidate(c) } else { state.candidate_is_selected(c) })
.collect();
let mut hits: Vec<egui::Rect> = Vec::with_capacity(candidates.len());
let mut hovered_index: Option<usize> = None;
let mut clicked_index: Option<usize> = None;
let mut clear_clicked = false;
let area = egui::Area::new(egui::Id::new("brep-candidate-popup"))
.order(egui::Order::Foreground)
.fixed_pos(anchor)
.constrain(true)
.show(ctx, |ui| {
let mut frame = egui::Frame::popup(ui.style());
frame.fill = frame.fill.gamma_multiply(0.85);
frame.show(ui, |ui| {
ui.set_max_width(280.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Select an object").weak().small());
if !reference {
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
if ui.small_button("Clear Selection").clicked() {
clear_clicked = true;
}
},
);
}
});
egui::ScrollArea::vertical()
.max_height(240.0)
.show(ui, |ui| {
ui.set_min_width(220.0);
for (i, candidate) in candidates.iter().enumerate() {
let label = format!(
"{} {}",
state.candidate_kind_label(candidate),
candidate_label(candidate)
);
let resp = ui
.add(
egui::Button::selectable(
selected_rows[i],
label.as_str(),
)
.truncate(),
)
.on_hover_text(&label);
hits.push(resp.rect);
if resp.hovered() {
hovered_index = Some(i);
}
if resp.clicked() {
clicked_index = Some(i);
}
}
});
});
});
self.candidate_hits = hits;
self.candidate_popup_rect = Some(area.response.rect);
if let Some(i) = hovered_index {
state.hover_candidate(&candidates[i]);
} else {
state.clear_hover();
}
let mut close = false;
if clear_clicked {
state.clear_selection();
close = true;
}
if let Some(i) = clicked_index {
let toggles = state.settings.multi_select == MultiSelectMode::ClickToggles;
if reference {
state.ref_select_take_candidate(&candidates[i]);
} else if toggles || mods.command || mods.ctrl {
state.toggle_candidate(&candidates[i]);
} else {
state.select_candidate(&candidates[i]);
}
close = true;
}
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
close = true;
}
if self.candidate_popup_fresh {
self.candidate_popup_fresh = false;
} else if area.response.clicked_elsewhere() {
close = true;
}
if close {
state.clear_hover();
self.candidate_popup = None;
self.candidate_popup_rect = None;
}
}
fn candidates_json(&self, state: &EngineState) -> String {
match self.candidate_popup.as_ref() {
Some(popup) => {
let out: Vec<serde_json::Value> = popup
.candidates
.iter()
.enumerate()
.map(|(i, c)| {
serde_json::json!({
"index": i,
"kind": state.candidate_kind_label(c),
"name": c.name,
"solid": c.solid,
"depth": c.depth,
})
})
.collect();
serde_json::Value::Array(out).to_string()
}
None => "[]".to_string(),
}
}
fn hover_dwell_json(&self, ctx: &egui::Context) -> String {
let age = match self.hover_lit_since {
Some(since) => ctx.input(|i| i.time - since),
None => -1.0,
};
serde_json::json!({
"armed": self.hover_dwell_armed(ctx),
"age": age,
"threshold": dwell_secs(ctx),
})
.to_string()
}
fn candidate_hits_json(&self) -> String {
let out: serde_json::Map<String, serde_json::Value> = self
.candidate_hits
.iter()
.enumerate()
.map(|(i, r)| {
(
i.to_string(),
serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
)
})
.collect();
serde_json::Value::Object(out).to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum DragStart {
ViewCube,
Gizmo,
Component,
Dimension(String),
Constraint,
Camera,
}
pub(super) fn gizmo_anchor_hits_json(view: Option<egui::Rect>, state: &EngineState) -> String {
let Some(view) = view else {
return "{}".to_string();
};
let at = |(x, y): (f64, f64)| {
egui::Rect::from_min_size(egui::pos2(view.min.x + x as f32, view.min.y + y as f32), egui::Vec2::ZERO)
};
let hits: Vec<(String, egui::Rect)> = state
.transform_gizmo_anchor()
.map(|point| ("anchor".to_string(), at(point)))
.into_iter()
.chain(
state
.transform_handle_points()
.into_iter()
.map(|(key, point)| (key.to_string(), at(point))),
)
.chain(
state
.dimension_handle_points()
.into_iter()
.map(|(key, point)| (format!("dim:{key}"), at(point))),
)
.chain(
state
.dimension_origin_points()
.into_iter()
.map(|(key, point)| (format!("ball:{key}"), at(point))),
)
.collect();
if hits.is_empty() {
return "{}".to_string();
}
crate::automation::hit_rects::hits_json(hits.iter().map(|(key, rect)| (key, rect)))
}
pub(super) fn cage_point_hits_json(view: Option<egui::Rect>, state: &EngineState) -> String {
let Some(view) = view else {
return "{}".to_string();
};
let hits: Vec<(String, egui::Rect)> = state
.cage_point_screen_points()
.into_iter()
.map(|(index, (x, y))| {
(
format!("point:{index}"),
egui::Rect::from_min_size(
egui::pos2(view.min.x + x as f32, view.min.y + y as f32),
egui::Vec2::ZERO,
),
)
})
.collect();
if hits.is_empty() {
return "{}".to_string();
}
crate::automation::hit_rects::hits_json(hits.iter().map(|(key, rect)| (key, rect)))
}
pub(super) fn constraint_handle_hits_json(view: Option<egui::Rect>, state: &EngineState) -> String {
let Some(view) = view else {
return "{}".to_string();
};
let Ok(serde_json::Value::Object(points)) =
serde_json::from_str::<serde_json::Value>(&state.constraint_handle_points_json())
else {
return "{}".to_string();
};
let hits: Vec<(String, egui::Rect)> = points
.iter()
.filter_map(|(id, point)| {
let point = point.as_array()?;
let x = point.first()?.as_f64()? as f32;
let y = point.get(1)?.as_f64()? as f32;
Some((
id.clone(),
egui::Rect::from_min_size(
egui::pos2(view.min.x + x, view.min.y + y),
egui::Vec2::ZERO,
),
))
})
.collect();
crate::automation::hit_rects::hits_json(hits.iter().map(|(key, rect)| (key, rect)))
}
fn drag_start_local(response: &egui::Response, rect: egui::Rect) -> Option<(f64, f64)> {
let p = response
.ctx
.input(|i| i.pointer.press_origin())
.or_else(|| response.interact_pointer_pos())?;
Some(((p.x - rect.min.x) as f64, (p.y - rect.min.y) as f64))
}
pub(super) fn route_drag_start(state: &mut EngineState, x: f64, y: f64) -> DragStart {
if let Some((cx, cy)) = viewcube_local(state, x, y) {
state.viewcube_click(cx, cy);
DragStart::ViewCube
} else if state.transform_press(x, y) {
DragStart::Gizmo
} else if state.component_press(x, y) {
DragStart::Component
} else if let Some(field) = state.dimension_arrow_pick(x, y) {
DragStart::Dimension(field)
} else if state.constraint_drag_begin(x, y) {
DragStart::Constraint
} else {
DragStart::Camera
}
}
fn viewcube_local(state: &EngineState, x: f64, y: f64) -> Option<(f64, f64)> {
let v: serde_json::Value = serde_json::from_str(&state.viewcube_rect_json()).ok()?;
let (rx, ry, rw, rh) = (
v["x"].as_f64()?,
v["y"].as_f64()?,
v["w"].as_f64()?,
v["h"].as_f64()?,
);
if rw > 0.0 && rh > 0.0 && x >= rx && x <= rx + rw && y >= ry && y <= ry + rh {
Some((x - rx, y - ry))
} else {
None
}
}
fn candidate_label(candidate: &PickCandidate) -> String {
if candidate.name.trim().is_empty() {
let p = candidate.position;
format!("({:.2}, {:.2}, {:.2})", p[0], p[1], p[2])
} else {
candidate.name.clone()
}
}