Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
use crate::DemoLensApp;
use crate::project::constants::*;
use crate::ui;
use eframe::emath::{Rect, Vec2};
use eframe::epaint::Color32;
use egui::{Painter, Pos2, Stroke};
use egui_dock::{SurfaceIndex, NodeIndex};
use serde::{Serialize, Deserialize};
use egui_lens::ReactiveEventLogger;
use gerber_viewer::{
draw_arrow, draw_crosshair, GerberRenderer,
draw_marker, ViewState
};
use crate::drc_operations::types::Position;
use crate::display::manager::ToPosition;
use nalgebra::Vector2;
/// Define the tabs for the DockArea
#[derive(Clone, Serialize, Deserialize)]
pub enum TabKind {
ViewSettings,
DRC,
GerberView,
EventLog,
Project,
Settings,
}
pub struct TabParams<'a> {
pub app: &'a mut DemoLensApp,
}
/// Tab container struct for DockArea
#[derive(Clone, Serialize, Deserialize)]
pub struct Tab {
pub kind: TabKind,
#[serde(skip)]
#[allow(dead_code)]
pub surface: Option<SurfaceIndex>,
#[serde(skip)]
#[allow(dead_code)]
pub node: Option<NodeIndex>,
}
impl Tab {
pub fn new(kind: TabKind, surface: SurfaceIndex, node: NodeIndex) -> Self {
Self {
kind,
surface: Some(surface),
node: Some(node),
}
}
pub fn title(&self) -> String {
match self.kind {
TabKind::ViewSettings => "View Settings".to_string(),
TabKind::DRC => "DRC".to_string(),
TabKind::GerberView => "Gerber View".to_string(),
TabKind::EventLog => "Event Log".to_string(),
TabKind::Project => "Project".to_string(),
TabKind::Settings => "Settings".to_string(),
}
}
pub fn content(&self, ui: &mut egui::Ui, params: &mut TabParams<'_>) {
match self.kind {
TabKind::ViewSettings => {
// Use vertical layout like diskforge
ui.vertical(|ui| {
let logger_state_clone = params.app.logger_state.clone();
let log_colors_clone = params.app.log_colors.clone();
// Layer Controls Section
ui.heading("Layer Controls");
ui.separator();
ui::show_layers_panel(ui, params.app, &logger_state_clone, &log_colors_clone);
ui.add_space(20.0);
// Orientation Section
ui.heading("Orientation");
ui.separator();
ui::show_orientation_panel(ui, params.app, &logger_state_clone, &log_colors_clone);
ui.add_space(20.0);
// Grid Settings Section
ui.heading("Grid Settings");
ui.separator();
ui::show_grid_panel(ui, params.app, &logger_state_clone, &log_colors_clone);
});
}
TabKind::DRC => {
let logger_state_clone = params.app.logger_state.clone();
let log_colors_clone = params.app.log_colors.clone();
ui::show_drc_panel(ui, params.app, &logger_state_clone, &log_colors_clone);
}
TabKind::GerberView => {
ui.vertical(|ui| {
// Rendering mode toggle
ui.horizontal(|ui| {
ui.label("Rendering mode:");
ui.radio_value(&mut params.app.use_wgpu_rendering, false, "Traditional (CPU)");
ui.radio_value(&mut params.app.use_wgpu_rendering, true, "WGPU (GPU)");
if params.app.use_wgpu_rendering {
if params.app.wgpu_widget.is_none() {
if ui.button("Initialize WGPU").clicked() {
let mut widget = crate::renderer::WgpuWidget::new(egui::Vec2::new(800.0, 600.0));
widget.try_initialize();
params.app.wgpu_widget = Some(widget);
}
} else if let Some(ref widget) = params.app.wgpu_widget {
if widget.is_initialized() {
ui.label("✅ WGPU Ready");
// 3D PCB Generation Controls
ui.horizontal(|ui| {
if ui.button("🔧 Generate 3D PCB").clicked() {
self.generate_3d_pcb(params.app);
}
if params.app.pcb3d_generated {
ui.label("✅ 3D PCB Generated");
if ui.button("🔄 Regenerate").clicked() {
params.app.pcb3d_system.clear_cache();
params.app.pcb3d_generated = false;
self.generate_3d_pcb(params.app);
}
}
});
// Show 3D PCB statistics if generated
if params.app.pcb3d_generated {
ui.collapsing("📊 3D PCB Statistics", |ui| {
let stats = params.app.pcb3d_system.get_statistics();
ui.label(format!("Layers processed: {}", stats.layers_processed));
ui.label(format!("Total vertices: {}", stats.total_vertices));
ui.label(format!("Total triangles: {}", stats.total_triangles));
ui.label(format!("Cached primitives: {}", stats.cached_primitives));
if let Some((min, max)) = stats.bounds {
ui.label(format!("Bounds: ({:.2}, {:.2}, {:.2}) to ({:.2}, {:.2}, {:.2})",
min.x, min.y, min.z, max.x, max.y, max.z));
}
});
}
} else if let Some(error) = widget.initialization_error() {
ui.colored_label(egui::Color32::RED, format!("❌ WGPU Error: {}", error));
if ui.button("Retry").clicked() {
let mut new_widget = crate::renderer::WgpuWidget::new(egui::Vec2::new(800.0, 600.0));
new_widget.try_initialize();
params.app.wgpu_widget = Some(new_widget);
}
} else {
ui.label("⏳ Initializing...");
}
}
}
});
ui.separator();
// Render based on selected mode
if params.app.use_wgpu_rendering {
self.render_wgpu_view(ui, params.app);
} else {
self.render_gerber_view(ui, params.app);
}
});
}
TabKind::EventLog => {
let logger = ReactiveEventLogger::with_colors(¶ms.app.logger_state, ¶ms.app.log_colors);
logger.show(ui);
}
TabKind::Project => {
let logger_state_clone = params.app.logger_state.clone();
let log_colors_clone = params.app.log_colors.clone();
ui::show_project_panel(ui, params.app, &logger_state_clone, &log_colors_clone);
}
TabKind::Settings => {
let logger_state_clone = params.app.logger_state.clone();
let log_colors_clone = params.app.log_colors.clone();
ui::show_settings_panel(ui, params.app, &logger_state_clone, &log_colors_clone);
}
}
}
fn render_wgpu_view(&self, ui: &mut egui::Ui, app: &mut DemoLensApp) {
if let Some(ref mut wgpu_widget) = app.wgpu_widget {
// Calculate the size for the WGPU widget based on available space
let available_size = ui.available_size();
let widget_size = egui::Vec2::new(
available_size.x.max(400.0),
available_size.y.max(300.0)
);
// Update the widget size and show it
wgpu_widget.set_size(widget_size);
wgpu_widget.show(ui);
} else {
// Show initialization message
ui.centered_and_justified(|ui| {
ui.label("WGPU widget not initialized. Click 'Initialize WGPU' above.");
});
}
}
fn render_gerber_view(&self, ui: &mut egui::Ui, app: &mut DemoLensApp) {
// Fill all available space in the panel
ui.ctx().request_repaint(); // Ensure continuous updates
// Use allocate_response to ensure we fill the entire available area
let available_size = ui.available_size();
// Ensure minimum size to avoid zero-sized allocations
let size = egui::Vec2::new(
available_size.x.max(100.0),
available_size.y.max(100.0)
);
let response = ui.allocate_response(size, egui::Sense::click_and_drag());
let viewport = response.rect;
// Handle double-click to center (same as Center button)
if response.double_clicked() {
// Apply the same logic as the "Center" button in orientation panel
app.display_manager.center_offset = crate::display::VectorOffset { x: 0.0, y: 0.0 };
app.display_manager.design_offset = crate::display::VectorOffset { x: 0.0, y: 0.0 };
app.needs_initial_view = true;
}
// Get mouse position for cursor tracking
let mouse_pos_screen = ui.input(|i| i.pointer.hover_pos());
// Handle right-click drag zoom window
let right_button = egui::PointerButton::Secondary;
if response.contains_pointer() {
if ui.input(|i| i.pointer.button_pressed(right_button)) {
// Start zoom window
if let Some(pos) = mouse_pos_screen {
app.zoom_window_start = Some(pos);
app.zoom_window_dragging = true;
}
}
}
if app.zoom_window_dragging && ui.input(|i| i.pointer.button_released(right_button)) {
// Complete zoom window
if let (Some(start), Some(end)) = (app.zoom_window_start, mouse_pos_screen) {
// Calculate zoom rectangle
let zoom_rect = Rect::from_two_pos(start, end);
// Only zoom if the rectangle is large enough
if zoom_rect.width() > 10.0 && zoom_rect.height() > 10.0 {
// Convert screen coordinates to gerber coordinates
let gerber_start = app.view_state.screen_to_gerber_coords(zoom_rect.min);
let gerber_end = app.view_state.screen_to_gerber_coords(zoom_rect.max);
// Calculate new scale to fit the selected region
let gerber_width = (gerber_end.x - gerber_start.x).abs() as f32;
let gerber_height = (gerber_end.y - gerber_start.y).abs() as f32;
let scale_x = viewport.width() / gerber_width;
let scale_y = viewport.height() / gerber_height;
let new_scale = scale_x.min(scale_y) * 0.9; // 90% to add some padding
// Calculate center of zoom rectangle in gerber coordinates
let gerber_center_x = (gerber_start.x + gerber_end.x) / 2.0;
let gerber_center_y = (gerber_start.y + gerber_end.y) / 2.0;
// Update view state
app.view_state.scale = new_scale;
// Calculate translation to center the zoomed region
let viewport_center = viewport.center();
app.view_state.translation = Vec2::new(
viewport_center.x - (gerber_center_x * new_scale as f64) as f32,
viewport_center.y + (gerber_center_y * new_scale as f64) as f32 // Y is flipped
);
}
}
app.zoom_window_dragging = false;
app.zoom_window_start = None;
}
// Cancel zoom window on escape
if app.zoom_window_dragging && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
app.zoom_window_dragging = false;
app.zoom_window_start = None;
}
// Fill the background with the panel color to ensure no black gaps
let painter = ui.painter_at(viewport);
painter.rect_filled(viewport, 0.0, ui.visuals().extreme_bg_color);
if app.needs_initial_view {
app.reset_view(viewport)
}
// Only update normal mouse handling if not doing zoom window
if !app.zoom_window_dragging {
app.ui_state.update(ui, &viewport, &response, &mut app.view_state);
}
let painter = ui.painter().with_clip_rect(viewport);
// Draw grid if enabled (before other elements so it appears underneath)
crate::display::draw_grid(&painter, &viewport, &app.view_state, &app.grid_settings);
// Draw quadrant axes if quadrant view is enabled
if app.display_manager.quadrant_view_enabled {
draw_quadrant_axes(&painter, &viewport, &app.view_state, app.ui_state.center_screen_pos);
}
// In quadrant view, show blue crosshair at viewport center instead of origin
if app.display_manager.quadrant_view_enabled {
draw_crosshair(&painter, app.ui_state.center_screen_pos, Color32::BLUE);
} else {
draw_crosshair(&painter, app.ui_state.origin_screen_pos, Color32::BLUE);
draw_crosshair(&painter, app.ui_state.center_screen_pos, Color32::LIGHT_GRAY);
}
// Get mechanical outline layer for quadrant view
let mechanical_outline_layer = if app.display_manager.quadrant_view_enabled {
app.layer_manager.layers.get(&crate::layer_operations::LayerType::MechanicalOutline)
.and_then(|info| if info.visible { info.gerber_layer.as_ref() } else { None })
} else {
None
};
// Render all visible layers based on showing_top
for layer_type in crate::layer_operations::LayerType::all() {
if let Some(layer_info) = app.layer_manager.layers.get(&layer_type) {
if layer_info.visible {
// Skip mechanical outline in quadrant view (it will be rendered with each layer)
if app.display_manager.quadrant_view_enabled && layer_type == crate::layer_operations::LayerType::MechanicalOutline {
continue;
}
// Filter based on showing_top
let should_render = layer_type.should_render(app.display_manager.showing_top);
if should_render {
// Use the layer's specific gerber data if available, otherwise fall back to demo
let gerber_to_render = layer_info.gerber_layer.as_ref()
.unwrap_or(&app.gerber_layer);
// Get quadrant offset for this layer type
let quadrant_offset = app.display_manager.get_quadrant_offset(&layer_type);
// The key is to offset from center_offset (which positions the viewport center)
// rather than from design_offset
let combined_offset = if app.display_manager.quadrant_view_enabled {
// When quadrant view is enabled, position relative to center_offset
crate::display::VectorOffset {
x: app.display_manager.center_offset.x + quadrant_offset.x,
y: app.display_manager.center_offset.y + quadrant_offset.y,
}
} else {
// Normal mode: use design offset
app.display_manager.design_offset.clone()
};
// Render the main layer
let render_config = gerber_viewer::RenderConfiguration {
use_unique_shape_colors: false,
use_shape_numbering: false,
use_vertex_numbering: false,
};
let transform = gerber_viewer::GerberTransform {
rotation_radians: app.rotation_degrees.to_radians(),
mirroring: gerber_viewer::Mirroring::from(app.display_manager.mirroring.clone()),
origin: nalgebra::Vector2::from(app.display_manager.center_offset.clone()),
offset: nalgebra::Vector2::from(combined_offset.clone()),
scale: 1.0,
};
GerberRenderer::default().paint_layer(
&painter,
app.view_state,
gerber_to_render,
layer_type.color(),
&render_config,
&transform,
);
// In quadrant view, also render mechanical outline with this layer
if app.display_manager.quadrant_view_enabled {
if let Some(mechanical_layer) = mechanical_outline_layer {
let mechanical_render_config = gerber_viewer::RenderConfiguration {
use_unique_shape_colors: false,
use_shape_numbering: false,
use_vertex_numbering: false,
};
let mechanical_transform = gerber_viewer::GerberTransform {
rotation_radians: app.rotation_degrees.to_radians(),
mirroring: gerber_viewer::Mirroring::from(app.display_manager.mirroring.clone()),
origin: nalgebra::Vector2::from(app.display_manager.center_offset.clone()),
offset: nalgebra::Vector2::from(combined_offset),
scale: 1.0,
};
GerberRenderer::default().paint_layer(
&painter,
app.view_state,
mechanical_layer,
crate::layer_operations::LayerType::MechanicalOutline.color(),
&mechanical_render_config,
&mechanical_transform,
);
}
}
}
}
}
}
let screen_radius = MARKER_RADIUS * app.view_state.scale;
let design_offset_screen_position = app.view_state.gerber_to_screen_coords(Vector2::from(app.display_manager.design_offset.clone()).to_position().to_point2());
draw_arrow(&painter, design_offset_screen_position, app.ui_state.origin_screen_pos, Color32::ORANGE);
draw_marker(&painter, design_offset_screen_position, Color32::ORANGE, Color32::YELLOW, screen_radius);
let design_origin_screen_position = app.view_state.gerber_to_screen_coords((Vector2::from(app.display_manager.center_offset.clone()) - Vector2::from(app.display_manager.design_offset.clone())).to_position().to_point2());
draw_marker(&painter, design_origin_screen_position, Color32::PURPLE, Color32::MAGENTA, screen_radius);
// Render corner overlay shapes (rounded corners)
if !app.drc_manager.corner_overlay_shapes.is_empty() {
// Use a different color for the overlay (bright green for visibility)
let overlay_color = Color32::from_rgb(0, 255, 0); // Bright green
for shape in &app.drc_manager.corner_overlay_shapes {
// Transform all polygon vertices
let mut transformed_vertices = Vec::new();
for point in &shape.points {
let mut vertex_pos = *point;
// Apply rotation if any
if app.rotation_degrees != 0.0 {
let rotation_radians = app.rotation_degrees.to_radians();
let (sin_theta, cos_theta) = (rotation_radians.sin(), rotation_radians.cos());
let rotated_x = vertex_pos.x * cos_theta as f64 - vertex_pos.y * sin_theta as f64;
let rotated_y = vertex_pos.x * sin_theta as f64 + vertex_pos.y * cos_theta as f64;
vertex_pos = Position { x: rotated_x, y: rotated_y };
}
// Apply mirroring if any
if app.display_manager.mirroring.x {
vertex_pos = vertex_pos.invert_x();
}
if app.display_manager.mirroring.y {
vertex_pos = vertex_pos.invert_y();
}
// Apply center and design offsets
let origin = Vector2::from(app.display_manager.center_offset.clone()) - Vector2::from(app.display_manager.design_offset.clone());
vertex_pos = vertex_pos + origin.to_position();
let vertex_screen = app.view_state.gerber_to_screen_coords(vertex_pos.to_point2());
transformed_vertices.push(vertex_screen);
}
// Draw filled polygon for the entire rounded corner
if transformed_vertices.len() >= 3 {
painter.add(egui::Shape::convex_polygon(
transformed_vertices,
overlay_color,
Stroke::NONE
));
}
}
}
// Draw DRC violation markers
for violation in &app.drc_manager.violations {
let violation_pos = Position::new(violation.x as f64, violation.y as f64);
// Apply the same transformation pipeline as GerberRenderer::paint_layer()
let mut transformed_pos = violation_pos;
// Apply rotation if any
if app.rotation_degrees != 0.0 {
let rotation_radians = app.rotation_degrees.to_radians();
let (sin_theta, cos_theta) = (rotation_radians.sin(), rotation_radians.cos());
let rotated_x = transformed_pos.x * cos_theta as f64 - transformed_pos.y * sin_theta as f64;
let rotated_y = transformed_pos.x * sin_theta as f64 + transformed_pos.y * cos_theta as f64;
transformed_pos = Position::new(rotated_x, rotated_y);
}
// Apply mirroring if any
if app.display_manager.mirroring.x { // X mirroring
transformed_pos = transformed_pos.invert_x();
}
if app.display_manager.mirroring.y { // Y mirroring
transformed_pos = transformed_pos.invert_y();
}
// Apply center and design offsets
let origin = Vector2::from(app.display_manager.center_offset.clone()) - Vector2::from(app.display_manager.design_offset.clone());
transformed_pos = transformed_pos + origin.to_position();
let screen_pos = app.view_state.gerber_to_screen_coords(transformed_pos.to_point2());
// All markers now represent trace areas (1 per trace)
let base_size = 3.0; // Small but visible markers
let marker_size = base_size * app.view_state.scale.max(0.5); // Scale with zoom but not too small
let color = Color32::RED;
draw_violation_marker(&painter, screen_pos, marker_size, color);
}
// Draw board dimensions at the bottom
if let Some(layer_info) = app.layer_manager.layers.get(&crate::layer_operations::LayerType::MechanicalOutline) {
if let Some(ref outline_layer) = layer_info.gerber_layer {
let bbox = outline_layer.bounding_box();
let width_mm = bbox.width();
let height_mm = bbox.height();
let dimension_text = if app.global_units_mils {
let width_mils = width_mm / 0.0254;
let height_mils = height_mm / 0.0254;
format!("{:.0} x {:.0} mils", width_mils, height_mils)
} else {
format!("{:.1} x {:.1} mm", width_mm, height_mm)
};
let text_pos = viewport.max - Vec2::new(10.0, 50.0);
painter.text(
text_pos,
egui::Align2::RIGHT_BOTTOM,
dimension_text,
egui::FontId::default(),
Color32::from_rgb(200, 200, 200),
);
}
}
// Draw zoom window rectangle if dragging
if app.zoom_window_dragging {
if let (Some(start), Some(current)) = (app.zoom_window_start, mouse_pos_screen) {
let zoom_rect = Rect::from_two_pos(start, current);
// Draw semi-transparent fill
painter.rect_filled(
zoom_rect,
0.0,
Color32::from_rgba_unmultiplied(100, 150, 255, 50)
);
// Draw border with lines instead of rect_stroke
let stroke = Stroke::new(2.0, Color32::from_rgb(100, 150, 255));
painter.line_segment([zoom_rect.min, Pos2::new(zoom_rect.max.x, zoom_rect.min.y)], stroke);
painter.line_segment([Pos2::new(zoom_rect.max.x, zoom_rect.min.y), zoom_rect.max], stroke);
painter.line_segment([zoom_rect.max, Pos2::new(zoom_rect.min.x, zoom_rect.max.y)], stroke);
painter.line_segment([Pos2::new(zoom_rect.min.x, zoom_rect.max.y), zoom_rect.min], stroke);
// Draw corner markers
let corner_size = 5.0;
let corners = [zoom_rect.min,
Pos2::new(zoom_rect.max.x, zoom_rect.min.y),
zoom_rect.max,
Pos2::new(zoom_rect.min.x, zoom_rect.max.y)];
for corner in &corners {
painter.circle_filled(*corner, corner_size, Color32::from_rgb(100, 150, 255));
}
// Show dimensions of selection
if zoom_rect.width() > 50.0 && zoom_rect.height() > 30.0 {
let gerber_start = app.view_state.screen_to_gerber_coords(zoom_rect.min);
let gerber_end = app.view_state.screen_to_gerber_coords(zoom_rect.max);
let width_mm = (gerber_end.x - gerber_start.x).abs() as f32;
let height_mm = (gerber_end.y - gerber_start.y).abs() as f32;
let dimension_text = if app.global_units_mils {
let width_mils = width_mm / 0.0254;
let height_mils = height_mm / 0.0254;
format!("{:.0} x {:.0} mils", width_mils, height_mils)
} else {
format!("{:.1} x {:.1} mm", width_mm, height_mm)
};
let text_pos = zoom_rect.center() + Vec2::new(0.0, -20.0);
// Background for text
let text_galley = painter.layout_no_wrap(
dimension_text.clone(),
egui::FontId::default(),
Color32::WHITE
);
let text_rect = egui::Rect::from_min_size(
text_pos - text_galley.size() / 2.0 - Vec2::new(4.0, 2.0),
text_galley.size() + Vec2::new(8.0, 4.0)
);
painter.rect_filled(
text_rect,
3.0,
Color32::from_rgba_unmultiplied(0, 0, 0, 200)
);
// Draw text
painter.text(
text_pos,
egui::Align2::CENTER_CENTER,
dimension_text,
egui::FontId::default(),
Color32::WHITE
);
}
}
}
// Draw mouse position cursor indicator
if let Some(mouse_screen_pos) = mouse_pos_screen {
if viewport.contains(mouse_screen_pos) {
// Convert screen position to gerber coordinates
let gerber_pos = app.view_state.screen_to_gerber_coords(mouse_screen_pos);
// Apply the same transformation as other elements for consistency
let origin = Vector2::from(app.display_manager.center_offset.clone()) - Vector2::from(app.display_manager.design_offset.clone());
let adjusted_pos = Position::new(
gerber_pos.x - origin.to_position().x,
gerber_pos.y - origin.to_position().y
);
// Format coordinates based on user preference
let cursor_text = if app.global_units_mils {
// Display in mils
let x_mils = adjusted_pos.x / 0.0254;
let y_mils = adjusted_pos.y / 0.0254;
format!("({:.0}, {:.0}) mils", x_mils, y_mils)
} else {
// Display in mm
format!("({:.2}, {:.2}) mm", adjusted_pos.x, adjusted_pos.y)
};
// Position cursor text near mouse
let text_offset = Vec2::new(15.0, -15.0);
let cursor_text_pos = mouse_screen_pos + text_offset;
// Draw background box for better readability
let text_size = painter.text(
cursor_text_pos,
egui::Align2::LEFT_TOP,
"",
egui::FontId::monospace(12.0),
Color32::WHITE,
).size();
let background_rect = egui::Rect::from_min_size(
cursor_text_pos - Vec2::new(2.0, 2.0),
text_size + Vec2::new(4.0, 4.0)
);
painter.rect_filled(
background_rect,
3.0,
Color32::from_rgba_unmultiplied(0, 0, 0, 180)
);
// Draw the actual cursor text
painter.text(
cursor_text_pos,
egui::Align2::LEFT_TOP,
cursor_text,
egui::FontId::monospace(12.0),
Color32::WHITE,
);
// Draw crosshair at mouse position
let crosshair_size = 8.0;
painter.line_segment(
[
mouse_screen_pos - Vec2::new(crosshair_size, 0.0),
mouse_screen_pos + Vec2::new(crosshair_size, 0.0)
],
Stroke::new(1.0, Color32::WHITE)
);
painter.line_segment(
[
mouse_screen_pos - Vec2::new(0.0, crosshair_size),
mouse_screen_pos + Vec2::new(0.0, crosshair_size)
],
Stroke::new(1.0, Color32::WHITE)
);
}
}
// Add unit toggle in top-right corner
let unit_toggle_pos = viewport.max - Vec2::new(10.0, 30.0);
let unit_text = if app.global_units_mils { "mils" } else { "mm" };
painter.text(
unit_toggle_pos,
egui::Align2::RIGHT_BOTTOM,
format!("Mouse: {}", unit_text),
egui::FontId::default(),
Color32::from_rgb(150, 150, 150),
);
}
/// Generate 3D PCB from current layer data
fn generate_3d_pcb(&self, app: &mut DemoLensApp) {
log::info!("Starting 3D PCB generation...");
match app.pcb3d_system.generate_3d_pcb(&app.layer_manager) {
Ok(result) => {
app.pcb3d_generated = true;
log::info!("3D PCB generation successful: {} layers, {} vertices, {} triangles",
result.layers_processed, result.total_vertices, result.total_triangles);
if !result.warnings.is_empty() {
for warning in &result.warnings {
log::warn!("3D PCB generation warning: {}", warning);
}
}
// Log detailed statistics
let stats = app.pcb3d_system.get_statistics();
log::info!("3D PCB Statistics - Layers: {}, Vertices: {}, Triangles: {}, Cached: {}",
stats.layers_processed, stats.total_vertices, stats.total_triangles, stats.cached_primitives);
if let Some((min, max)) = stats.bounds {
log::info!("3D PCB Bounds: ({:.2}, {:.2}, {:.2}) to ({:.2}, {:.2}, {:.2})",
min.x, min.y, min.z, max.x, max.y, max.z);
}
}
Err(e) => {
app.pcb3d_generated = false;
log::error!("3D PCB generation failed: {}", e);
}
}
}
}
pub struct TabViewer<'a> {
pub app: &'a mut DemoLensApp,
}
impl<'a> egui_dock::TabViewer for TabViewer<'a> {
type Tab = Tab;
fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
tab.title().into()
}
fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
let mut params = TabParams {
app: self.app,
};
tab.content(ui, &mut params);
}
}
/// Draw a red X marker for DRC violations
fn draw_violation_marker(painter: &Painter, center: Pos2, size: f32, color: Color32) {
let half_size = size / 2.0;
let stroke = Stroke::new(2.0, color);
// Draw X shape
painter.line_segment([
Pos2::new(center.x - half_size, center.y - half_size),
Pos2::new(center.x + half_size, center.y + half_size)
], stroke);
painter.line_segment([
Pos2::new(center.x - half_size, center.y + half_size),
Pos2::new(center.x + half_size, center.y - half_size)
], stroke);
}
/// Draw quadrant axes when quadrant view is enabled
fn draw_quadrant_axes(painter: &Painter, viewport: &Rect, _view_state: &ViewState, center_screen_pos: Pos2) {
let stroke = Stroke::new(2.0, Color32::from_rgba_unmultiplied(100, 100, 100, 150));
// Use the viewport center (white crosshair) as the origin for quadrant axes
let origin_screen = center_screen_pos;
// Draw vertical axis
if origin_screen.x >= viewport.min.x && origin_screen.x <= viewport.max.x {
painter.line_segment(
[
Pos2::new(origin_screen.x, viewport.min.y),
Pos2::new(origin_screen.x, viewport.max.y)
],
stroke
);
}
// Draw horizontal axis
if origin_screen.y >= viewport.min.y && origin_screen.y <= viewport.max.y {
painter.line_segment(
[
Pos2::new(viewport.min.x, origin_screen.y),
Pos2::new(viewport.max.x, origin_screen.y)
],
stroke
);
}
// Add quadrant labels
let label_offset = 20.0;
let font_id = egui::FontId::default();
let label_color = Color32::from_rgba_unmultiplied(150, 150, 150, 200);
// Only draw labels if axes are visible
if origin_screen.x > viewport.min.x + label_offset * 2.0 &&
origin_screen.x < viewport.max.x - label_offset * 2.0 &&
origin_screen.y > viewport.min.y + label_offset * 2.0 &&
origin_screen.y < viewport.max.y - label_offset * 2.0 {
// Quadrant 1 (top-right): Copper
painter.text(
origin_screen + Vec2::new(label_offset, -label_offset),
egui::Align2::LEFT_BOTTOM,
"Copper",
font_id.clone(),
label_color,
);
// Quadrant 2 (top-left): Silkscreen
painter.text(
origin_screen + Vec2::new(-label_offset, -label_offset),
egui::Align2::RIGHT_BOTTOM,
"Silkscreen",
font_id.clone(),
label_color,
);
// Quadrant 3 (bottom-left): Soldermask
painter.text(
origin_screen + Vec2::new(-label_offset, label_offset),
egui::Align2::RIGHT_TOP,
"Soldermask",
font_id.clone(),
label_color,
);
// Quadrant 4 (bottom-right): Paste
painter.text(
origin_screen + Vec2::new(label_offset, label_offset),
egui::Align2::LEFT_TOP,
"Paste",
font_id,
label_color,
);
}
}e57474237This file is pinned to the selected release. Download the original to open it in your design tool.