various changes

This commit is contained in:
Hlars 2024-08-23 10:52:33 +02:00
parent 63203ee717
commit 7913541282
50 changed files with 2459 additions and 1312 deletions

View File

@ -1,5 +1,7 @@
{
"cSpell.words": [
"centered",
"cmds",
"color",
"Color",
"consts",
@ -8,11 +10,14 @@
"egui",
"emath",
"epaint",
"evalexpr",
"excellon",
"Excellon",
"excellons",
"gerbers",
"Heatsink",
"itertools",
"Itertools",
"linepath",
"obround",
"Obround",
@ -21,6 +26,7 @@
"rect",
"Rect",
"regmatch",
"Soldermask"
"Soldermask",
"winresource"
]
}

856
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,16 +6,24 @@ edition = "2021"
[dependencies]
eframe = "0.28.1"
egui_plot = { version = "0.28.1", features = ["serde"] }
egui_extras = { version = "0.28.1", features = ["all_loaders"] }
rfd = "0.14"
image = "0.25"
clipper2 = "0.4.1"
gerber-types = "0.3"
# gerber-types = "0.3"
gerber-types = { path = "./gerber-types-rs" }
svg = "0.17"
dxf = "0.5"
lazy-regex = "3.1.0"
itertools = "0.13"
evalexpr = "11.3.0"
tracing = "0.1"
tracing-subscriber = "0.3"
error-stack = "0.5"
[build-dependencies]
winresource = "0.1"

14
build.rs Normal file
View File

@ -0,0 +1,14 @@
use {
std::{env, io},
winresource::WindowsResource,
};
fn main() -> io::Result<()> {
if env::var_os("CARGO_CFG_WINDOWS").is_some() {
WindowsResource::new()
// This path can be absolute, or relative to your crate root.
.set_icon("resources/icon.ico")
.compile()?;
}
Ok(())
}

Binary file not shown.

BIN
resources/excellon_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

BIN
resources/geometry_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

BIN
resources/gerber_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@ -1,15 +1,8 @@
use eframe::{
egui::{Color32, Ui},
epaint::CircleShape,
};
use egui_plot::PlotUi;
use crate::{
application::Application,
geometry::{elements::Element, DrawableRaw},
};
use crate::{application::Application, geometry::elements::Element};
use super::{draw_floating_area_on_canvas, draw_on_plot_canvas, CanvasColour, Drawer, PlotDrawer};
use super::{draw_on_plot_canvas, CanvasColour, PlotDrawer};
pub fn draw_excellons_(ui: &mut PlotUi, app: &mut Application) {
for (file_name, (_, excellon)) in &app.excellons {
@ -18,7 +11,6 @@ pub fn draw_excellons_(ui: &mut PlotUi, app: &mut Application) {
for circle in excellon.holes.iter() {
draw_on_plot_canvas(
ui,
app,
PlotDrawer::Drawable((
&Element::Circle(circle.to_owned()),
CanvasColour::Excellon,
@ -28,29 +20,3 @@ pub fn draw_excellons_(ui: &mut PlotUi, app: &mut Application) {
}
}
}
pub fn draw_excellons(ui: &mut Ui, app: &mut Application) {
for (file_name, (ex_name, excellon)) in &app.excellons {
let selected = &app.selection == file_name;
for (i, circle) in excellon.holes.iter().enumerate() {
draw_floating_area_on_canvas(
ui,
app,
circle.canvas_pos(),
("ExcellonArea", ex_name, i),
Drawer::Closure(&|ui| {
ui.painter().add(CircleShape::filled(
circle.position.invert_y().into(),
circle.diameter as f32 / 2.,
if selected {
Color32::BROWN.gamma_multiply(0.5)
} else {
Color32::BROWN
},
));
}),
);
}
}
}

View File

@ -1,7 +1,4 @@
use eframe::{
egui::{Color32, Pos2, Ui},
epaint::{CircleShape, PathShape, PathStroke},
};
use eframe::egui::Pos2;
use egui_plot::{Line, PlotPoint, PlotPoints, PlotUi};
use crate::{
@ -9,7 +6,7 @@ use crate::{
geometry::{elements::circle::Circle, DrawableRaw},
};
use super::{draw_floating_area_on_canvas, draw_on_plot_canvas, CanvasColour, Drawer, PlotDrawer};
use super::{draw_on_plot_canvas, CanvasColour, PlotDrawer};
pub fn draw_geometries_(ui: &mut PlotUi, app: &mut Application) {
for (file_name, geo) in &app.outlines {
@ -19,7 +16,6 @@ pub fn draw_geometries_(ui: &mut PlotUi, app: &mut Application) {
for path in geo.paths().iter() {
draw_on_plot_canvas(
ui,
app,
PlotDrawer::Closure(&|ui| {
// draw outline path
let mut points = path
@ -40,7 +36,7 @@ pub fn draw_geometries_(ui: &mut PlotUi, app: &mut Application) {
points.push(points[1]);
let line = Line::new(PlotPoints::from(points))
.width(width)
.color(CanvasColour::Outline.to_colour32(selected));
.color(CanvasColour::Outline.as_colour32(selected));
ui.line(line)
}),
@ -51,99 +47,11 @@ pub fn draw_geometries_(ui: &mut PlotUi, app: &mut Application) {
for point in geo.points().iter() {
draw_on_plot_canvas(
ui,
app,
PlotDrawer::Closure(&|ui| {
let circle = Circle::new(*point, geo.stroke.into(), None);
circle.draw_egui_plot(ui, CanvasColour::Outline, selected);
}),
);
// draw_floating_area_on_canvas(
// ui,
// app,
// point,
// ("GeometryAreaPoint", name, i),
// Drawer::Closure(&|ui| {
// // draw point shape
// ui.painter().add(CircleShape::filled(
// point.invert_y().into(),
// geo.stroke,
// if selected {
// Color32::DARK_BLUE.gamma_multiply(0.5)
// } else {
// Color32::DARK_BLUE
// },
// ));
// }),
// );
}
}
// for circle in excellon.holes.iter() {
// draw_on_plot_canvas(
// ui,
// app,
// PlotDrawer::Drawable((
// &Element::Circle(circle.to_owned()),
// CanvasColour::Excellon,
// selected,
// )),
// );
// }
}
pub fn draw_geometries(ui: &mut Ui, app: &mut Application) {
for (name, geo) in &app.outlines {
let selected = &app.selection == name;
// draw outline path
for (i, path) in geo.paths().iter().enumerate() {
draw_floating_area_on_canvas(
ui,
app,
{
let origin = path.iter().next().unwrap();
Pos2::new(origin.x() as f32, origin.y() as f32)
},
("GeometryAreaOutline", name, i),
Drawer::Closure(&|ui| {
// draw outline path
ui.painter().add(PathShape::closed_line(
path.iter()
.map(|v| Pos2::new(v.x() as f32, -v.y() as f32))
.collect::<Vec<Pos2>>(),
PathStroke::new(
geo.stroke,
if selected {
Color32::DARK_BLUE.gamma_multiply(0.5)
} else {
Color32::DARK_BLUE
},
),
));
}),
);
}
// draw point shapes
for (i, point) in geo.points().iter().enumerate() {
draw_floating_area_on_canvas(
ui,
app,
point,
("GeometryAreaPoint", name, i),
Drawer::Closure(&|ui| {
// draw point shape
ui.painter().add(CircleShape::filled(
point.invert_y().into(),
geo.stroke,
if selected {
Color32::DARK_BLUE.gamma_multiply(0.5)
} else {
Color32::DARK_BLUE
},
));
}),
);
}
}
}

View File

@ -1,12 +1,11 @@
use eframe::{
egui::{Color32, Pos2, Ui},
epaint::{PathShape, PathStroke},
};
use egui_plot::{Line, PlotPoints, PlotUi};
use crate::{application::Application, geometry::DrawableRaw};
use crate::{
application::Application,
geometry::{ClipperPaths, DrawableRaw},
};
use super::{draw_floating_area_on_canvas, draw_on_plot_canvas, CanvasColour, Drawer, PlotDrawer};
use super::{draw_on_plot_canvas, CanvasColour, PlotDrawer};
pub fn draw_gerbers_(ui: &mut PlotUi, app: &mut Application) {
for (file_name, (_, geo)) in &app.gerbers {
@ -16,7 +15,6 @@ pub fn draw_gerbers_(ui: &mut PlotUi, app: &mut Application) {
for geometry in geo.apertures.iter() {
draw_on_plot_canvas(
ui,
app,
PlotDrawer::Drawable((geometry, CanvasColour::Copper, selected)),
);
}
@ -25,13 +23,25 @@ pub fn draw_gerbers_(ui: &mut PlotUi, app: &mut Application) {
for line in geo.paths.iter() {
draw_on_plot_canvas(
ui,
app,
PlotDrawer::Closure(&|ui| line.draw_egui_plot(ui, CanvasColour::Copper, selected)),
);
}
// draw outline union path
for path in geo.outline_union.iter() {
// draw conductor outlines
for net in &geo.united_nets.conductors {
draw_paths_outline(ui, &net.outline, selected);
}
// draw isolated aperture outlines
for element in &geo.united_nets.isolated_apertures {
draw_paths_outline(ui, &element.to_paths(), selected);
}
}
}
fn draw_paths_outline(ui: &mut PlotUi, paths: &ClipperPaths, selected: bool) {
for path in paths.iter() {
if !path.is_empty() {
let mut points = path
.iter()
.map(|p| [p.x(), p.y()])
@ -39,63 +49,9 @@ pub fn draw_gerbers_(ui: &mut PlotUi, app: &mut Application) {
points.push(points[0]);
let line = Line::new(PlotPoints::from(points))
.color(CanvasColour::CopperOutline.to_colour32(selected));
.color(CanvasColour::CopperOutline.as_colour32(selected));
ui.line(line)
}
}
}
pub fn draw_gerbers(ui: &mut Ui, app: &mut Application) {
for (file_name, (name, geo)) in &app.gerbers {
let selected = &app.selection == file_name;
for (i, geometry) in geo.apertures.iter().enumerate() {
draw_floating_area_on_canvas(
ui,
app,
geometry.canvas_pos(),
("GerberArea", name, i),
Drawer::Drawable((geometry, selected)),
);
}
for (i, line) in geo.paths.iter().enumerate() {
draw_floating_area_on_canvas(
ui,
app,
line.canvas_pos(),
("LinePath", name, i),
Drawer::Closure(&|ui| line.draw_egui(ui, selected)),
);
}
// draw union path
for (i, path) in geo.outline_union.iter().enumerate() {
draw_floating_area_on_canvas(
ui,
app,
{
let origin = path.iter().next().unwrap();
Pos2::new(origin.x() as f32, origin.y() as f32)
},
("UnionOutline", name, i),
Drawer::Closure(&|ui| {
ui.painter().add(PathShape::closed_line(
path.iter()
.map(|v| Pos2::new(v.x() as f32, -v.y() as f32))
.collect::<Vec<Pos2>>(),
PathStroke::new(
0.1_f32,
if selected {
Color32::LIGHT_RED
} else {
Color32::BLACK
},
),
));
}),
);
}
}
}

View File

@ -1,63 +0,0 @@
use eframe::egui::{self, Color32, Pos2, TextWrapMode, Ui, Vec2};
use crate::application::Application;
pub fn draw_live_position(ui: &mut Ui, app: &mut Application) {
if let Some(cursor) = cursor_canvas_position(app, ui) {
let _id = egui::Area::new(app.canvas.0.with("cursor_position_box"))
.fixed_pos(app.canvas.1.min)
// .order(egui::Order::Middle)
.default_size(Vec2::new(80., 80.))
.show(ui.ctx(), |ui| {
// let painter = ui.painter();
// painter.add(RectShape::filled(Rect::from_min_size(rect.min, Vec2::new(80., 80.)), Rounding::ZERO, Color32::LIGHT_BLUE));
egui::Frame::default()
.rounding(egui::Rounding::same(4.0))
.inner_margin(egui::Margin::same(8.0))
.stroke(ui.ctx().style().visuals.window_stroke)
.fill(Color32::from_rgba_premultiplied(0xAD, 0xD8, 0xE6, 200))
.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
let cursor_position = app.transform.inverse() * cursor; // + rect.min.to_vec2() - app.canvas_size.min;
let cursor_position2 = app.test_transform.inverse() * cursor; // + rect.min.to_vec2() - app.canvas_size.min;
ui.label(format!(
"x: {} {}",
(-app.transform.translation.x + cursor.x) / app.transform.scaling,
app.variables.units
));
ui.label(format!(
"y: {} {}",
// cursor.y / app.transform.scaling + app.test_transform.translation.y,
(-app.transform.translation.y + cursor.y) / app.transform.scaling,
app.variables.units
));
ui.label(format!(
"cursor: {:?} {}",
cursor_position, app.variables.units
));
ui.label(format!(
"cursor2: {:?} {}",
cursor_position2, app.variables.units
));
ui.label(format!("{:?} - {:?}", app.transform, app.test_transform))
});
});
}
}
fn cursor_canvas_position(app: &Application, ui: &mut Ui) -> Option<Pos2> {
let pointer_pos = ui.ctx().input(|i| i.pointer.hover_pos());
if let Some(cursor) = pointer_pos {
let pos = cursor - app.canvas.1.min;
match (pos.x, pos.y) {
(x, y) if x > 0. && y > 0. => Some(pos.to_pos2()),
(_, _) => None,
}
} else {
None
}
// pointer_pos.map(|pos| (pos - app.canvas_size.min).to_pos2())
}

View File

@ -1,19 +1,12 @@
pub mod excellons;
pub mod geometries;
pub mod gerbers;
mod live_position;
use std::hash::Hash;
use eframe::{
egui::{self, Color32, Pos2, Rounding, Stroke, Ui},
epaint::RectShape,
};
use eframe::egui::{Color32, Ui};
use egui_plot::PlotUi;
use excellons::{draw_excellons, draw_excellons_};
use geometries::{draw_geometries, draw_geometries_};
use gerbers::{draw_gerbers, draw_gerbers_};
use live_position::draw_live_position;
use excellons::draw_excellons_;
use geometries::draw_geometries_;
use gerbers::draw_gerbers_;
use crate::geometry::elements::Element;
@ -28,6 +21,7 @@ const EXCELLON_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(65, 42
const OUTLINE_COLOR: Color32 = Color32::DARK_BLUE;
const OUTLINE_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(0, 0, 139, 128);
#[derive(Debug, Clone, Copy)]
pub enum CanvasColour {
Copper,
CopperOutline,
@ -36,7 +30,7 @@ pub enum CanvasColour {
}
impl CanvasColour {
pub fn to_colour32(&self, selected: bool) -> Color32 {
pub fn as_colour32(&self, selected: bool) -> Color32 {
match (self, selected) {
(CanvasColour::Copper, true) => COPPER_COLOR_SELECTED,
(CanvasColour::Copper, false) => COPPER_COLOR,
@ -59,48 +53,6 @@ pub fn draw_canvas(ui: &mut Ui, app: &mut Application) {
draw_excellons_(plot_ui, app);
draw_geometries_(plot_ui, app);
});
// draw_live_position(ui, app);
// ui.painter().add(RectShape::stroke(
// app.canvas.1,
// Rounding::same(0.),
// Stroke::new(0.2, Color32::BLACK),
// ));
// draw_gerbers(ui, app);
// draw_excellons(ui, app);
// draw_geometries(ui, app);
}
pub enum Drawer<'a> {
Drawable((&'a Element, bool)),
Closure(&'a dyn Fn(&mut Ui)),
}
fn draw_floating_area_on_canvas(
ui: &mut Ui,
app: &Application,
pos: impl Into<Pos2>,
name: impl Hash,
drawer: Drawer,
) {
let window_layer = ui.layer_id();
let id = egui::Area::new(app.canvas.0.with(name))
.current_pos(pos)
// .order(egui::Order::Middle)
.show(ui.ctx(), |ui| {
ui.set_clip_rect(app.test_transform.inverse() * app.canvas.1);
match drawer {
Drawer::Drawable((t, selected)) => t.draw_egui(ui, selected),
Drawer::Closure(fun) => fun(ui),
}
})
.response
.layer_id;
ui.ctx().set_transform_layer(id, app.test_transform);
ui.ctx().set_sublayer(window_layer, id);
}
pub enum PlotDrawer<'a> {
@ -108,7 +60,7 @@ pub enum PlotDrawer<'a> {
Closure(&'a dyn Fn(&mut PlotUi)),
}
fn draw_on_plot_canvas(ui: &mut PlotUi, app: &Application, drawer: PlotDrawer) {
fn draw_on_plot_canvas(ui: &mut PlotUi, drawer: PlotDrawer) {
match drawer {
PlotDrawer::Drawable((t, colour, selected)) => t.draw_egui_plot(ui, colour, selected),
PlotDrawer::Closure(fun) => fun(ui),

View File

@ -1,7 +1,4 @@
use eframe::{
egui::{self, Vec2},
emath::TSTransform,
};
use eframe::egui;
use super::{
canvas::draw_canvas,
@ -9,230 +6,17 @@ use super::{
Application,
};
impl eframe::App for Application {
impl<'a> eframe::App for Application<'a> {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// let window_size = ctx.screen_rect().size();
draw_header(ctx, self);
draw_sidebar(ctx, self);
egui::CentralPanel::default().show(ctx, |ui| {
// ui.horizontal(|ui| {
// let name_label = ui.label("Your name: ");
// ui.text_edit_singleline(&mut self.name)
// .labelled_by(name_label.id);
// });
// ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
// if ui.button("Increment").clicked() {
// self.age += 1;
// }
// ui.label(format!("Hello '{}', age {}", self.name, self.age));
// // ui.image(egui::include_image!(
// // "../../../crates/egui/assets/ferris.png"
// // ));
let pointer_pos = ui.ctx().input(|i| i.pointer.hover_pos());
draw_action_panel(ui, self);
// ui.label(
// "Pan, zoom in, and zoom out with scrolling (see the plot demo for more instructions). \
// Double click on the background to reset.",
// );
// ui.vertical_centered(|ui| {
// ui.add(crate::egui_github_link_file!());
// });
// if let Some(pos) = pointer_pos {
// ui.label(format!("Translation: {:?}", self.transform.translation));
// ui.label(format!("ScLING: {}", self.transform.scaling));
// ui.label(format!("Canvas start: {}", self.canvas.1.min));
// }
ui.separator();
// ui.allocate_ui(ui.available_size(), |ui| {
// egui::TopBottomPanel::bottom("BottomCanvas")
// .show_separator_line(false)
// .exact_height(15.)
// .show_inside(ui, |ui| {});
// egui::SidePanel::left("LeftCanvas")
// .show_separator_line(false)
// .exact_width(15.)
// .show_inside(ui, |ui| {});
// egui::CentralPanel::default()
// .frame(egui::Frame::none().inner_margin(0.).outer_margin(0.))
// .show_inside(ui, |ui| {
// let sin: egui_plot::PlotPoints = (0..1000)
// .map(|i| {
// let x = i as f64 * 0.01;
// [x, x.sin()]
// })
// .collect();
// let line = egui_plot::Line::new(sin);
// egui_plot::Plot::new("my_plot")
// .view_aspect(2.0)
// .data_aspect(1.0)
// .show(ui, |plot_ui| {
// super::canvas::gerbers::draw_gerbers_(plot_ui, self);
// super::canvas::excellons::draw_excellons_(plot_ui, self);
// super::canvas::geometries::draw_geometries_(plot_ui, self)
// // plot_ui.line(line);
// // for (_, (name, geo)) in &self.gerbers {
// // let path = geo.outline_union.iter().map(|path| {
// // let mut points = path
// // .iter()
// // .map(|p| [p.x(), p.y()])
// // .collect::<Vec<[f64; 2]>>();
// // points.push(points[0]);
// // (
// // egui_plot::Line::new(egui_plot::PlotPoints::from(
// // points.clone(),
// // ))
// // .color(egui::Color32::DARK_BLUE),
// // egui_plot::Polygon::new(egui_plot::PlotPoints::from(
// // points,
// // ))
// // .fill_color(egui::Color32::LIGHT_GREEN),
// // )
// // });
// // for line in path {
// // plot_ui.line(line.0);
// // plot_ui.polygon(line.1);
// // }
// // let circle_points: egui_plot::PlotPoints = (0..=400)
// // .map(|i| {
// // let t = egui::remap(
// // i as f64,
// // 0.0..=(400 as f64),
// // 0.0..=std::f64::consts::TAU,
// // );
// // let r = 10.;
// // [r * t.cos() + 20. as f64, r * t.sin() + 20. as f64]
// // })
// // .collect();
// // let poly = egui_plot::Polygon::new(
// // egui_plot::PlotPoints::from(circle_points),
// // )
// // .fill_color(egui::Color32::LIGHT_GREEN);
// // plot_ui.polygon(poly);
// // }
// });
// // let (id, rect) = ui.allocate_space(ui.available_size());
// // let response = ui.interact(rect, id, egui::Sense::click_and_drag());
// // self.canvas = (id, rect);
// // // Allow dragging the background as well.
// // if response.dragged() {
// // self.transform.translation += response.drag_delta();
// // }
// // // Plot-like reset
// // if response.double_clicked() {
// // self.transform = TSTransform::default();
// // self.transform = TSTransform::from_translation(Vec2::new(
// // rect.width() / 2.,
// // rect.height() / 2.,
// // ));
// // }
// // let transform =
// // TSTransform::from_translation(ui.min_rect().left_top().to_vec2())
// // * self.transform;
// // if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
// // // Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered.
// // if response.hovered() {
// // let pointer_in_layer = transform.inverse() * pointer;
// // let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
// // let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
// // // Zoom in on pointer:
// // self.transform = self.transform
// // * TSTransform::from_translation(pointer_in_layer.to_vec2())
// // * TSTransform::from_scaling(zoom_delta)
// // * TSTransform::from_translation(-pointer_in_layer.to_vec2());
// // // Pan:
// // self.transform =
// // TSTransform::from_translation(pan_delta) * self.transform;
// // }
// // }
// // self.test_transform = transform;
// })
// });
// let (id, rect) = ui.allocate_space(Vec2::new(ui.available_width(), 15.));
// ui.painter().add(RectShape::stroke(
// rect,
// Rounding::same(0.),
// egui::Stroke::new(0.2, Color32::BLACK),
// ));
// let (id, rect) = ui.allocate_space(Vec2::new(10., ui.available_height()));
// ui.painter().add(RectShape::stroke(
// rect,
// Rounding::same(0.),
// egui::Stroke::new(0.2, Color32::BLACK),
// ));
// let (id, rect) = ui.allocate_space(ui.available_size());
// let response = ui.interact(rect, id, egui::Sense::click_and_drag());
// self.canvas = (id, rect);
// // Allow dragging the background as well.
// if response.dragged() {
// self.transform.translation += response.drag_delta();
// }
// // Plot-like reset
// if response.double_clicked() {
// self.transform = TSTransform::default();
// self.transform =
// TSTransform::from_translation(Vec2::new(rect.width() / 2., rect.height() / 2.));
// }
// let transform =
// TSTransform::from_translation(ui.min_rect().left_top().to_vec2()) * self.transform;
// if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
// // Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered.
// if response.hovered() {
// let pointer_in_layer = transform.inverse() * pointer;
// let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
// let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
// // Zoom in on pointer:
// self.transform = self.transform
// * TSTransform::from_translation(pointer_in_layer.to_vec2())
// * TSTransform::from_scaling(zoom_delta)
// * TSTransform::from_translation(-pointer_in_layer.to_vec2());
// // Pan:
// self.transform = TSTransform::from_translation(pan_delta) * self.transform;
// }
// }
// self.test_transform = transform;
// let p = ui.painter_at(rect);
// p.add(RectShape::filled(
// Rect::from_center_size(rect.center(), Vec2::new(rect.width(), 1.)),
// Rounding::ZERO,
// Color32::LIGHT_RED,
// ));
// p.add(RectShape::filled(
// Rect::from_center_size(rect.center(), Vec2::new(1., rect.height())),
// Rounding::ZERO,
// Color32::LIGHT_RED,
// ));
draw_canvas(ui, self);
});
}

20
src/application/errors.rs Normal file
View File

@ -0,0 +1,20 @@
use std::fmt;
use error_stack::{Context, Report};
#[derive(Debug)]
pub struct FileError;
impl FileError {
pub fn new(text: &str) -> Report<Self> {
Report::new(Self).attach_printable(text.to_string())
}
}
impl fmt::Display for FileError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("Error opening file")
}
}
impl Context for FileError {}

View File

@ -1,30 +1,24 @@
mod canvas;
mod egui;
mod errors;
pub mod panels;
use std::collections::HashMap;
use eframe::{
egui::{Id, Pos2, Rect, Vec2},
emath::TSTransform,
};
use crate::{
excellon::drills::Drills,
geometry::{Geometry, Unit},
outline_geometry::OutlineGeometry,
resources::ResourceLoader,
};
pub use canvas::CanvasColour;
pub struct Application {
pub struct Application<'a> {
resources: ResourceLoader<'a>,
gerbers: HashMap<String, (String, Geometry)>,
outlines: HashMap<String, OutlineGeometry>,
excellons: HashMap<String, (String, Drills)>,
// geometry: Geometry,
transform: TSTransform,
test_transform: TSTransform,
canvas: (Id, Rect),
selection: String,
variables: Variables,
}
@ -35,19 +29,13 @@ pub struct Variables {
units: Unit,
}
impl Application {
impl<'a> Application<'a> {
pub fn new() -> Self {
Self {
resources: ResourceLoader::new(),
gerbers: HashMap::new(),
outlines: HashMap::new(),
excellons: HashMap::new(),
// geometry,
transform: TSTransform::default(),
test_transform: TSTransform::default(),
canvas: (
Id::new("0"),
Rect::from_center_size(Pos2::new(0., 0.), Vec2::default()),
),
selection: "".into(),
variables: Variables::default(),
}

View File

@ -1,51 +1,51 @@
use std::collections::hash_map::Entry;
use eframe::egui::Ui;
use crate::{
application::Application,
geometry::{ClipperPath, ClipperPaths, DrawableRaw},
outline_geometry::OutlineGeometry,
};
use crate::{application::Application, geometry::ClipperPath, outline_geometry::OutlineGeometry};
use super::HOLE_MARK_DIAMETER;
pub fn show_excellon_actions(ui: &mut Ui, app: &mut Application) {
if let Some((name, drill)) = app.excellons.get(&app.selection) {
if ui.button("Generate cut out").clicked() {
let path = drill
.holes
.iter()
.map(|hole| hole.outline.clone())
.collect::<Vec<ClipperPath>>()
.into();
app.outlines.insert(
format!("{name}-CutOut"),
OutlineGeometry::new_no_inflate(
&path,
0.1,
app.variables.units,
name,
path.bounds(),
),
);
}
if ui.button("Generate hole mark").clicked() {
app.outlines.insert(
format!("{name}-HoleMark"),
OutlineGeometry::point_marker(
drill.holes.iter().map(|c| c.canvas_pos()).collect(),
HOLE_MARK_DIAMETER,
app.variables.units,
name,
ClipperPaths::from(
drill
if app.excellons.contains_key(&app.selection) {
match app.excellons.entry(app.selection.to_string()) {
Entry::Occupied(c) => {
let mut deletion_request = false;
let (name, drill) = c.get();
if ui.button("Delete").clicked() {
deletion_request = true;
}
ui.horizontal(|ui| {
if ui.button("Generate cut out").clicked() {
let path = drill
.holes
.iter()
.map(|hole| hole.outline.clone())
.collect::<Vec<ClipperPath>>(),
)
.bounds(),
),
);
.collect::<Vec<ClipperPath>>()
.into();
app.outlines.insert(
format!("{name}-CutOut"),
OutlineGeometry::new_no_inflate(&path, 0.1, app.variables.units, name),
);
}
if ui.button("Generate hole mark").clicked() {
app.outlines.insert(
format!("{name}-HoleMark"),
OutlineGeometry::drill_marker(
drill,
HOLE_MARK_DIAMETER,
app.variables.units,
name,
),
);
}
});
if deletion_request {
c.remove();
}
}
Entry::Vacant(_) => unreachable!(),
}
}
}

View File

@ -1,34 +1,59 @@
use std::collections::hash_map::Entry;
use eframe::egui::{ComboBox, Ui};
use crate::{application::Application, export::svg::SVGConverter};
pub fn show_geometry_actions(ui: &mut Ui, app: &mut Application) {
if let Some(outline) = app.outlines.get_mut(&app.selection) {
ui.label("BoundingBox");
ComboBox::from_label("Select one!")
.selected_text(format!("{:?}", outline.bounds_from))
.show_ui(ui, |ui| {
for (key, (name, _)) in app.gerbers.iter() {
if ui
.selectable_value(&mut outline.bounds_from, name.into(), name)
.clicked()
{
if let Some((_, box_geo)) = app.gerbers.get(key) {
outline.bounding_box = box_geo.outline_union.bounds();
if app.outlines.contains_key(&app.selection) {
match app.outlines.entry(app.selection.to_string()) {
Entry::Occupied(mut c) => {
let mut deletion_request = false;
let isolated_tracks = c.get_mut();
if ui.button("Delete").clicked() {
deletion_request = true;
}
ui.horizontal(|ui| {
ui.label("BoundingBox");
let id = ui.make_persistent_id("BoundingBoxSelection");
ComboBox::new(id, "")
.selected_text(format!("{:?}", isolated_tracks.bounds_from))
.show_ui(ui, |ui| {
for (key, (name, _)) in app.gerbers.iter() {
if ui
.selectable_value(
&mut isolated_tracks.bounds_from,
name.into(),
name,
)
.clicked()
{
if let Some((_, box_geo)) = app.gerbers.get(key) {
isolated_tracks.bounding_box = box_geo.united_nets.bounds();
}
}
}
});
if ui.button("Save as SVG").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_title("Save as SVG")
.set_file_name(app.selection.to_string())
.add_filter("SVG", &["svg", "SVG"])
.save_file()
{
SVGConverter::export(isolated_tracks, &path);
}
}
}
});
});
if ui.button("Save as SVG").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_title("Save as SVG")
.set_file_name(app.selection.to_string())
.add_filter("SVG", &["svg", "SVG"])
.save_file()
{
SVGConverter::export(outline, &path);
if deletion_request {
c.remove();
}
}
Entry::Vacant(_) => unreachable!(),
}
}
}

View File

@ -1,28 +1,45 @@
use std::collections::hash_map::Entry;
use eframe::egui::{DragValue, Ui};
use crate::{application::Application, outline_geometry::OutlineGeometry};
pub fn show_gerber_actions(ui: &mut Ui, app: &mut Application) {
if let Some((name, geo)) = app.gerbers.get(&app.selection) {
ui.horizontal(|ui| {
ui.add(
DragValue::new(&mut app.variables.laser_line_width)
.range(0.1..=10.)
.suffix(geo.units),
);
if app.gerbers.contains_key(&app.selection) {
match app.gerbers.entry(app.selection.to_string()) {
Entry::Occupied(c) => {
let mut deletion_request = false;
let (name, gerber) = c.get();
if ui.button("Generate Isolation").clicked() {
app.outlines.insert(
format!("{name}-Iso"),
OutlineGeometry::new(
&geo.outline_union,
app.variables.laser_line_width,
geo.units,
name,
geo.outline_union.bounds(),
),
);
if ui.button("Delete").clicked() {
deletion_request = true;
}
ui.horizontal(|ui| {
ui.add(
DragValue::new(&mut app.variables.laser_line_width)
.range(0.1..=10.)
.speed(0.1)
.suffix(gerber.units),
);
if ui.button("Generate Isolation").clicked() {
app.outlines.insert(
format!("{name}-Iso"),
OutlineGeometry::new(
&gerber.united_nets,
app.variables.laser_line_width,
gerber.units,
name,
),
);
}
});
if deletion_request {
c.remove();
}
}
});
Entry::Vacant(_) => unreachable!(),
}
}
}

View File

@ -1,62 +1,192 @@
use std::{fs::File, io::BufReader};
use std::{
fs::File,
io::{BufReader, Read},
};
use eframe::egui;
use error_stack::{Report, ResultExt};
use tracing::error;
use crate::{
application::Application,
excellon::{drills::Drills, parse_excellon},
application::{errors::FileError, Application},
excellon::{doc::ExcellonDoc, drills::Drills, parse_excellon},
geometry::Geometry,
gerber::parse_gerber,
gerber::{doc::GerberDoc, parse_gerber},
};
const GERBER_EXTENSIONS: &[&str] = &["GBR ", "gbr", "GB", "geb"];
const EXCELLON_EXTENSIONS: &[&str] = &["DRL ", "drl"];
pub fn draw_header(ctx: &egui::Context, app: &mut Application) {
egui::TopBottomPanel::top("top_panel")
.exact_height(40.)
.show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("Open Gerber").clicked() {
if let Some(paths) = rfd::FileDialog::new()
.add_filter("Gerber", &["GBR ", "gbr", "GB", "geb"])
.pick_files()
{
// self.picked_path = Some(path.display().to_string());
for path in paths {
// TODO remove all unwraps
if let Ok(file) = File::open(&path) {
let gerber = parse_gerber(BufReader::new(file));
let name = path.file_name().unwrap().to_str().unwrap().to_string();
app.gerbers.insert(
path.to_str().unwrap().into(),
(name, Geometry::from(gerber).to_unit(app.variables.units)),
);
} else {
// TODO show error
};
ui.horizontal_centered(|ui| {
if let Some(opened_files) = ui
.menu_button("📝 Open File", open_file_menu)
.inner
.flatten()
{
match opened_files {
Ok(files) => {
for (path, name, content) in files {
match content {
AppFileContent::Gerber(gerber) => {
app.gerbers.insert(
path,
(
name,
Geometry::from(gerber)
.into_unit(app.variables.units),
),
);
}
AppFileContent::Excellon(excellon) => {
let drills: Drills = excellon.into();
app.excellons.insert(
path,
(name, drills.into_unit(app.variables.units)),
);
}
}
}
}
Err(e) => {
// TODO Show error
error!("{e:?}");
}
}
}
if ui.button("Open Excellon").clicked() {
if let Some(paths) = rfd::FileDialog::new()
.add_filter("Excellon", &["DRL ", "drl"])
.pick_files()
{
for path in paths {
// TODO remove all unwraps
if let Ok(file) = File::open(&path) {
let excellon = parse_excellon(BufReader::new(file)).unwrap();
let drills: Drills = excellon.into();
let name = path.file_name().unwrap().to_str().unwrap().to_string();
app.excellons.insert(
path.to_str().unwrap().into(),
(name, drills.to_unit(app.variables.units)),
);
} else {
// TODO show error
};
}
}
}
};
})
});
}
type FileOpenResponse = Result<Vec<(String, String, AppFileContent)>, Report<FileError>>;
fn open_file_menu(ui: &mut egui::Ui) -> Option<FileOpenResponse> {
let mut response = None;
ui.set_max_width(200.0); // To make sure we wrap long text
if ui.button("Gerber").clicked() {
response = Some(open_file(AppFileType::Gerber));
ui.close_menu();
}
if ui.button("Excellon").clicked() {
ui.close_menu();
response = Some(open_file(AppFileType::Excellon));
}
// ui.menu_button("SubMenu", |ui| {
// ui.menu_button("SubMenu", |ui| {
// if ui.button("Open…").clicked() {
// ui.close_menu();
// }
// let _ = ui.button("Item");
// });
// ui.menu_button("SubMenu", |ui| {
// if ui.button("Open…").clicked() {
// ui.close_menu();
// }
// let _ = ui.button("Item");
// });
// let _ = ui.button("Item");
// if ui.button("Open…").clicked() {
// ui.close_menu();
// }
// });
// ui.menu_button("SubMenu", |ui| {
// let _ = ui.button("Item1");
// let _ = ui.button("Item2");
// let _ = ui.button("Item3");
// let _ = ui.button("Item4");
// if ui.button("Open…").clicked() {
// ui.close_menu();
// }
// });
// let _ = ui.button("Very long text for this item that should be wrapped");
response
}
#[derive(Debug, Clone, Copy)]
enum AppFileType {
Gerber,
Excellon,
}
impl std::fmt::Display for AppFileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
AppFileType::Gerber => "Gerber",
AppFileType::Excellon => "Excellon",
}
)
}
}
impl AppFileType {
pub fn extensions(&self) -> &[&str] {
match self {
AppFileType::Gerber => GERBER_EXTENSIONS,
AppFileType::Excellon => EXCELLON_EXTENSIONS,
}
}
}
pub enum AppFileContent {
Gerber(GerberDoc),
Excellon(ExcellonDoc),
}
impl AppFileContent {
fn from_file<T: Read>(
file_type: AppFileType,
file: BufReader<T>,
) -> Result<Self, Report<FileError>> {
Ok(match file_type {
AppFileType::Gerber => Self::Gerber(parse_gerber(file)),
AppFileType::Excellon => Self::Excellon(
parse_excellon(file)
.change_context(FileError)
.attach_printable("Could not parse Excellon file")?,
),
})
}
}
fn open_file(
file_type: AppFileType,
) -> Result<Vec<(String, String, AppFileContent)>, Report<FileError>> {
let mut result = Vec::new();
if let Some(paths) = rfd::FileDialog::new()
.add_filter(file_type.to_string(), file_type.extensions())
.pick_files()
{
for path in paths {
// get file content
let file = File::open(&path).change_context(FileError)?;
// parse file format
let content = AppFileContent::from_file(file_type, BufReader::new(file))?;
// get file path
let path_str = path
.to_str()
.ok_or(FileError::new("Could not get file path string"))?;
// get file name
let name_str = path
.file_name()
.ok_or(FileError::new(
"Could not determine file name from selection",
))?
.to_str()
.ok_or(FileError::new("Could not convert OS String"))?;
result.push((path_str.into(), name_str.into(), content));
}
}
Ok(result)
}

View File

@ -1,4 +1,4 @@
use eframe::egui::{self, CollapsingHeader};
use eframe::egui::{self, RichText};
use crate::{application::Application, APP_NAME};
@ -6,30 +6,74 @@ pub fn draw_sidebar(ctx: &egui::Context, app: &mut Application) {
egui::SidePanel::left("left_panel")
.exact_width(230.)
.show(ctx, |ui| {
ui.add_space(6.0);
ui.heading(APP_NAME);
CollapsingHeader::new("Gerber")
.default_open(true)
.show(ui, |ui| {
ui.add_space(12.0);
let id = ui.make_persistent_id("GerberContainer");
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
.show_header(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.image(app.resources.icons().gerber_file.clone());
ui.label(
RichText::new("Gerber")
.extra_letter_spacing(1.2)
.size(17.)
.strong(),
);
});
})
.body(|ui| {
for (key, (name, _)) in app.gerbers.iter() {
ui.selectable_value(&mut app.selection, key.to_string(), name);
ui.selectable_value(
&mut app.selection,
key.to_string(),
RichText::new(name).size(11.),
);
}
});
CollapsingHeader::new("Excellon")
.default_open(true)
.show(ui, |ui| {
let id = ui.make_persistent_id("ExcellonContainer");
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
.show_header(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.image(app.resources.icons().excellon_file.clone());
ui.label(
RichText::new("Excellon")
.extra_letter_spacing(1.2)
.size(17.)
.strong(),
);
});
})
.body(|ui| {
for (key, (name, _)) in app.excellons.iter() {
ui.selectable_value(&mut app.selection, key.to_string(), name);
}
});
CollapsingHeader::new("Geometry")
.default_open(true)
.show(ui, |ui| {
let id = ui.make_persistent_id("GeometryContainer");
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
.show_header(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.image(app.resources.icons().geometry_file.clone());
ui.label(
RichText::new("Geometry")
.extra_letter_spacing(1.2)
.size(17.)
.strong(),
);
});
})
.body(|ui| {
for (key, _) in app.outlines.iter() {
ui.selectable_value(&mut app.selection, key.to_string(), key);
}
});
// allocate rest of the container
let (id, rect) = ui.allocate_space(ui.available_size());
let response = ui.interact(rect, id, egui::Sense::click_and_drag());
if response.clicked() {

View File

@ -41,13 +41,13 @@ impl From<ExcellonDoc> for Drills {
}
impl Drills {
pub fn to_unit(&self, unit: Unit) -> Self {
pub fn into_unit(self, unit: Unit) -> Self {
Self {
units: unit,
holes: self
.holes
.iter()
.map(|hole| hole.to_unit(self.units, unit))
.into_iter()
.map(|hole| hole.into_unit(self.units, unit))
.collect(),
}
}

View File

@ -1,4 +1,4 @@
mod doc;
pub mod doc;
pub mod drills;
mod errors;
@ -271,8 +271,6 @@ mod tests {
let file = File::open("./FirstPCB-PTH.drl").unwrap();
let _reader = BufReader::new(file);
let excellon = parse_excellon(BufReader::new(excellon.as_bytes()));
// let excellon = parse_excellon(reader);
println!("{:#?}", excellon);
let _excellon = parse_excellon(BufReader::new(excellon.as_bytes()));
}
}

View File

@ -8,9 +8,9 @@ use crate::geometry::Geometry;
pub struct DXFConverter;
impl DXFConverter {
pub fn export(geometry: &Geometry, file: &str) {
pub fn export(_geometry: &Geometry, _file: &str) {
let mut drawing = Drawing::new();
let added_entity_ref = drawing.add_entity(Entity::new(EntityType::Line(Line::default())));
let _added_entity_ref = drawing.add_entity(Entity::new(EntityType::Line(Line::default())));
// `added_entity_ref` is a reference to the newly added entity
drawing.save_file("./file.dxf").unwrap();
}

View File

@ -43,8 +43,8 @@ impl SVGConverter {
let points = geo.points().into_iter().map(|p| {
Circle::new()
.set("r", HOLE_MARK_DIAMETER)
.set("cx", p.x)
.set("cy", p.invert_y().y)
.set("cx", p.x())
.set("cy", p.invert_y().y())
.set("fill", "black")
});

View File

@ -1,20 +1,21 @@
use std::f64::consts::{PI, TAU};
use eframe::{
egui::{remap, Stroke, Ui},
epaint::CircleShape,
};
use eframe::egui::{remap, Stroke};
use egui_plot::{PlotPoints, PlotUi, Polygon};
use gerber_types::CirclePrimitive;
use crate::{
application::CanvasColour,
geometry::{ClipperPath, ClipperPaths},
};
use super::super::{
helpers::{create_circular_path, CircleSegment},
point::{convert_to_unit, Point},
DrawableRaw, Unit,
use super::{
super::{
helpers::{create_circular_path, CircleSegment},
point::{convert_to_unit, Point},
DrawableRaw, Unit,
},
macro_decimal_to_f64,
};
#[derive(Debug, Clone)]
@ -29,7 +30,7 @@ impl Circle {
pub fn new(position: impl Into<Point>, diameter: f64, hole_diameter: Option<f64>) -> Self {
let position = position.into();
Self {
position: position,
position,
diameter,
hole_diameter,
outline: create_circular_path(&position, diameter, CircleSegment::Full).into(),
@ -39,8 +40,19 @@ impl Circle {
Self::new(position, aperture.diameter, aperture.hole_diameter)
}
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
let position = self.position.to_unit(origin, to);
pub fn from_macro(ap_macro: &CirclePrimitive, variables: &[f64], target: Point) -> Self {
Self::new(
Point::new(
macro_decimal_to_f64(&ap_macro.center.0, variables) + target.x(),
macro_decimal_to_f64(&ap_macro.center.1, variables) + target.y(),
),
macro_decimal_to_f64(&ap_macro.diameter, variables),
None,
)
}
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
let position = self.position.into_unit(origin, to);
let diameter = convert_to_unit(self.diameter, origin, to);
Self {
position,
@ -51,17 +63,6 @@ impl Circle {
}
fn draw_circle(&self, ui: &mut PlotUi, colour: CanvasColour, selected: bool) {
// let n = 512;
// let circle_points: PlotPoints = (0..=n)
// .map(|i| {
// let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU);
// let r = self.diameter / 2.;
// [
// r * t.cos() + self.position.x as f64,
// r * t.sin() + self.position.y as f64,
// ]
// })
// .collect();
let circle_points = PlotPoints::from(Self::circle_segment_points(
self.position,
self.diameter,
@ -71,7 +72,7 @@ impl Circle {
ui.polygon(
Polygon::new(circle_points)
.fill_color(colour.to_colour32(selected))
.fill_color(colour.as_colour32(selected))
.stroke(Stroke::NONE),
);
}
@ -84,16 +85,15 @@ impl Circle {
) -> Vec<[f64; 2]> {
let segment_width = segment_width.clamp(0.0, 1.0);
let n = (512. * segment_width) as i32;
let circle_points = (0..=n)
(0..=n)
.map(|i| {
let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU * segment_width)
+ rotation * (PI / 180.);
let r = diameter / 2.;
[r * t.cos() + position.x, r * t.sin() + position.y]
[r * t.cos() + position.x(), r * t.sin() + position.y()]
})
.collect();
circle_points
.collect()
}
}
@ -101,31 +101,7 @@ impl DrawableRaw for Circle {
fn canvas_pos(&self) -> Point {
self.position
}
fn draw_egui(&self, ui: &mut Ui, selected: bool) {
ui.painter().add(CircleShape::filled(
self.position.invert_y().into(),
self.diameter as f32 / 2.,
CanvasColour::Copper.to_colour32(selected),
));
}
fn draw_egui_plot(&self, ui: &mut egui_plot::PlotUi, colour: CanvasColour, selected: bool) {
// let circle_points: Vec<[f64; 2]> =
// create_circular_path(&self.position, self.diameter, CircleSegment::Full)
// .iter()
// .map(|(x, y)| [*x, *y])
// .collect();
// let polygon = Polygon::new(PlotPoints::from(circle_points))
// .fill_color(if selected {
// COPPER_COLOR_SELECTED
// } else {
// COPPER_COLOR
// })
// .stroke(Stroke::new(0., Color32::TRANSPARENT));
// ui.polygon(polygon);
self.draw_circle(ui, colour, selected);
}

View File

@ -0,0 +1,117 @@
use std::collections::HashMap;
use clipper2::PointInPolygonResult;
use itertools::Itertools;
use crate::{
application::CanvasColour,
geometry::{point::Point, union::union_function, ClipperPath, ClipperPaths, DrawableRaw, Unit},
};
use super::Element;
#[derive(Debug, Clone)]
pub struct Composite {
elements: Vec<Element>,
outline: ClipperPaths,
}
impl Composite {
pub fn new() -> Self {
Self {
elements: Vec::new(),
outline: ClipperPaths::new(vec![]),
}
}
pub fn add(&mut self, element: Element) {
self.elements.push(element);
}
pub fn finalize(&mut self) {
let mut intersection_map: HashMap<usize, Vec<usize>> = HashMap::new();
for part in self.elements.iter().enumerate().combinations(2) {
let (index1, element1) = part[0];
let (index2, element2) = part[1];
// check if element1 and element2 intersect
if element1
.outline()
.iter()
.any(|p| element2.outline().is_point_inside(*p) != PointInPolygonResult::IsOutside)
{
// add intersection result for both indices
let entry1 = intersection_map.entry(index1).or_default();
entry1.push(index2);
let entry2 = intersection_map.entry(index2).or_default();
entry2.push(index1);
}
}
// go through all aperture components
for i in 0..intersection_map.len() {
// remove component from map
if let Some(mut intersections) = intersection_map.remove(&i) {
// take outline of element as base
let mut geo = self.elements[i].to_paths();
// union with intersecting aperture components until done
while let Some(other) = intersections.pop() {
// union with intersecting aperture component
let intersecting_element = &self.elements[other];
if let Some(union) = union_function(&geo, &intersecting_element.to_paths()) {
geo = union;
}
// get intersections of united aperture component and add them if not already in own
if let Some(new_intersections) = intersection_map.remove(&other) {
for o in new_intersections {
// add to original line if not self and not already inside
if o != i // ensure to not intersect with itself
&& !intersections.contains(&o) // do not add if already in intersection list
&& intersection_map.contains_key(&o)
// check if intersection was already performed
{
intersections.push(o);
}
}
}
}
self.outline.push(geo);
}
}
}
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
Self {
elements: self
.elements
.into_iter()
.map(|el| el.into_unit(origin, to))
.collect(),
outline: self.outline,
}
}
}
impl DrawableRaw for Composite {
fn canvas_pos(&self) -> Point {
// self.position
todo!()
}
fn draw_egui_plot(&self, ui: &mut egui_plot::PlotUi, colour: CanvasColour, selected: bool) {
for element in &self.elements {
element.draw_egui_plot(ui, colour, selected);
}
}
fn to_paths(&self) -> ClipperPaths {
self.outline.clone()
}
fn outline(&self) -> ClipperPath {
self.outline.first().unwrap().clone()
}
}

View File

@ -1,14 +1,12 @@
use std::f64::consts::PI;
use eframe::{
egui::{Pos2, Stroke, Ui},
epaint::{CircleShape, PathShape, PathStroke},
};
use clipper2::PointInPolygonResult;
use eframe::egui::Stroke;
use egui_plot::{PlotPoints, Polygon};
use crate::{
application::CanvasColour,
geometry::{helpers::semi_circle, ClipperPath, ClipperPaths},
geometry::{helpers::semi_circle, ClipperPath, ClipperPaths, ClipperPoint},
};
use super::super::{
@ -47,11 +45,16 @@ impl LinePath {
pub fn finalize(&mut self, stroke_width: f64) {
self.diameter = stroke_width;
self.outline = self.create_outline();
println!("Line with diameter: {stroke_width}");
}
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
let mut converted = Self {
points: self.points.iter().map(|p| p.to_unit(origin, to)).collect(),
points: self
.points
.iter()
.map(|p| p.into_unit(origin, to))
.collect(),
diameter: convert_to_unit(self.diameter, origin, to),
outline: ClipperPaths::new(vec![]),
};
@ -60,6 +63,14 @@ impl LinePath {
converted
}
pub fn outline_includes_points(&self, points: &[Point]) -> bool {
self.outline.iter().any(|path| {
points.iter().any(|point| {
path.is_point_inside(ClipperPoint::from(point)) != PointInPolygonResult::IsOutside
})
})
}
fn create_outline(&self) -> ClipperPaths {
let mut paths: Vec<ClipperPath> = Vec::new();
@ -91,33 +102,12 @@ impl DrawableRaw for &LinePath {
points.push(points[0]);
let poly = Polygon::new(PlotPoints::from(points))
.fill_color(colour.to_colour32(selected))
.fill_color(colour.as_colour32(selected))
.stroke(Stroke::NONE);
ui.polygon(poly);
}
}
fn draw_egui(&self, ui: &mut Ui, selected: bool) {
ui.painter().add(PathShape::line(
self.points
.iter()
.map(|v| (*v).invert_y().into())
.collect::<Vec<Pos2>>(),
PathStroke::new(
self.diameter as f32,
CanvasColour::Copper.to_colour32(selected),
),
));
for point in &self.points {
ui.painter().add(CircleShape::filled(
point.invert_y().into(),
self.diameter as f32 / 2.,
CanvasColour::Copper.to_colour32(selected),
));
}
}
fn to_paths(&self) -> ClipperPaths {
self.outline.clone()
}
@ -132,7 +122,7 @@ fn create_outline_between_points(point1: Point, point2: Point, width: f64) -> Ve
let normalized = line_vec.normalize();
let angle =
(normalized.x).acos() * (180. / PI) * if normalized.y < 0. { -1. } else { 1. } + 90.;
(normalized.x()).acos() * (180. / PI) * if normalized.y() < 0. { -1. } else { 1. } + 90.;
let mut outline: Vec<(f64, f64)> = Vec::new();

View File

@ -1,79 +1,126 @@
use circle::Circle;
use eframe::egui::Ui;
use composite::Composite;
use egui_plot::PlotUi;
use evalexpr::eval_number;
use gerber_types::MacroDecimal;
use lazy_regex::regex;
use linepath::LinePath;
use obround::Obround;
use polygon::Polygon;
use rectangle::Rectangle;
use tracing::error;
use crate::application::CanvasColour;
use super::{point::Point, ClipperPath, ClipperPaths, DrawableRaw, Unit};
pub mod circle;
pub mod composite;
pub mod linepath;
pub mod obround;
pub mod polygon;
pub mod rectangle;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Element {
Circle(Circle),
Composite(Composite),
Rectangle(Rectangle),
Line(LinePath),
_Line(LinePath),
Obround(Obround),
Polygon(Polygon),
}
impl Element {
pub fn draw_egui_plot(&self, ui: &mut PlotUi, colour: CanvasColour, selected: bool) {
match self {
Element::Circle(c) => c.draw_egui_plot(ui, colour, selected),
Element::Composite(co) => co.draw_egui_plot(ui, colour, selected),
Element::Rectangle(r) => r.draw_egui_plot(ui, colour, selected),
Element::Line(l) => l.draw_egui_plot(ui, colour, selected),
Element::_Line(l) => l.draw_egui_plot(ui, colour, selected),
Element::Obround(o) => o.draw_egui_plot(ui, colour, selected),
}
}
pub fn draw_egui(&self, ui: &mut Ui, selected: bool) {
match self {
Element::Circle(c) => c.draw_egui(ui, selected),
Element::Rectangle(r) => r.draw_egui(ui, selected),
Element::Line(l) => l.draw_egui(ui, selected),
Element::Obround(o) => o.draw_egui(ui, selected),
Element::Polygon(p) => p.draw_egui_plot(ui, colour, selected),
}
}
pub fn canvas_pos(&self) -> Point {
match self {
Element::Circle(c) => c.canvas_pos(),
Element::Composite(c) => c.canvas_pos(),
Element::Rectangle(r) => r.canvas_pos(),
Element::Line(l) => l.canvas_pos(),
Element::_Line(l) => l.canvas_pos(),
Element::Obround(o) => o.canvas_pos(),
Element::Polygon(p) => p.canvas_pos(),
}
}
pub fn to_paths(&self) -> ClipperPaths {
match self {
Element::Circle(c) => c.to_paths(),
Element::Composite(c) => c.to_paths(),
Element::Rectangle(r) => r.to_paths(),
Element::Line(l) => l.to_paths(),
Element::_Line(l) => l.to_paths(),
Element::Obround(o) => o.to_paths(),
Element::Polygon(p) => p.to_paths(),
}
}
pub fn outline(&self) -> ClipperPath {
match self {
Element::Circle(c) => c.outline(),
Element::Composite(c) => c.outline(),
Element::Rectangle(r) => r.outline(),
Element::Line(l) => l.outline(),
Element::_Line(l) => l.outline(),
Element::Obround(o) => o.outline(),
Element::Polygon(p) => p.outline(),
}
}
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
match self {
Element::Circle(c) => Element::Circle(c.to_unit(origin, to)),
Element::Rectangle(r) => Element::Rectangle(r.to_unit(origin, to)),
Element::Line(l) => Element::Line(l.to_unit(origin, to)),
Element::Obround(o) => Element::Obround(o.to_unit(origin, to)),
Element::Circle(c) => Element::Circle(c.into_unit(origin, to)),
Element::Composite(c) => Element::Composite(c.into_unit(origin, to)),
Element::Rectangle(r) => Element::Rectangle(r.into_unit(origin, to)),
Element::_Line(l) => Element::_Line(l.into_unit(origin, to)),
Element::Obround(o) => Element::Obround(o.into_unit(origin, to)),
Element::Polygon(p) => Element::Polygon(p.into_unit(origin, to)),
}
}
}
pub fn macro_decimal_to_f64(macro_decimal: &MacroDecimal, variables: &[f64]) -> f64 {
let re_units = regex!(r#"(\$\d+)"#);
match macro_decimal {
MacroDecimal::Value(v) => *v,
MacroDecimal::Variable(v) => *variables.get(*v as usize).unwrap_or(&0.),
MacroDecimal::Expression(ex) => {
// search for variables (eg. $1)
if let Some(captures) = re_units.captures(ex) {
// copy expression
let mut expr = ex.clone();
// go through all found variable names
for c in captures.iter().skip(1).flatten() {
// get variable name
let matched = c.as_str();
// create index out of variable
let index = matched.replace('$', "").parse::<usize>().unwrap();
// replace variable name with value
expr = expr.replace(
matched,
&variables.get(index - 1).unwrap_or(&0.).to_string(),
);
}
match eval_number(&expr) {
Ok(res) => res,
Err(e) => {
error!("{e:?}");
0.
}
}
} else {
0.
}
}
}
}

View File

@ -1,7 +1,4 @@
use eframe::{
egui::{Rect, Rounding, Stroke, Ui, Vec2},
epaint::RectShape,
};
use eframe::egui::Stroke;
use egui_plot::{PlotPoints, Polygon};
use crate::{
@ -9,24 +6,21 @@ use crate::{
geometry::{ClipperPath, ClipperPaths},
};
use super::rectangle::Rectangle;
use super::{
super::{
helpers::{create_circular_path, semi_circle, CircleSegment},
point::{convert_to_unit, Point},
DrawableRaw, Unit,
},
circle::Circle,
use super::super::{
helpers::{create_circular_path, semi_circle, CircleSegment},
point::{convert_to_unit, Point},
DrawableRaw, Unit,
};
use super::rectangle::Rectangle;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Obround {
position: Point,
x: f64,
y: f64,
rounding: f64,
_rounding: f64,
outline: ClipperPath,
rectangle: Rectangle,
_rectangle: Rectangle,
hole_diameter: Option<f64>,
}
@ -40,12 +34,27 @@ impl Obround {
// check if obround is round to x or y
if x < y {
// round on y axis
path.append(&mut semi_circle(position - Point::new(0., y / 4.), x, 180.));
path.append(&mut semi_circle(position + Point::new(0., y / 4.), x, 0.));
path.append(&mut semi_circle(
position - Point::new(0., y / 2. - x / 2.),
x,
180.,
));
path.append(&mut semi_circle(
position + Point::new(0., y / 2. - x / 2.),
x,
0.,
));
} else {
// TODO round on x axis -> check for correctness!!!!!!!
path.append(&mut semi_circle(position - Point::new(0., x / 4.), y, 270.));
path.append(&mut semi_circle(position + Point::new(0., x / 4.), y, 90.));
path.append(&mut semi_circle(
position + Point::new(x / 2. - y / 2., 0.),
y,
270.,
));
path.append(&mut semi_circle(
position - Point::new(x / 2. - y / 2., 0.),
y,
90.,
));
}
path
@ -53,12 +62,12 @@ impl Obround {
Self {
position,
x: x,
y: y,
rounding: diameter,
x,
y,
_rounding: diameter,
outline: outline.into(),
rectangle: Rectangle::new(position, x, y, hole_diameter),
hole_diameter: hole_diameter,
_rectangle: Rectangle::new(position, x, y, hole_diameter),
hole_diameter,
}
}
@ -66,9 +75,9 @@ impl Obround {
Self::new(position, aperture.x, aperture.y, aperture.hole_diameter)
}
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
Self::new(
self.position.to_unit(origin, to),
self.position.into_unit(origin, to),
convert_to_unit(self.x, origin, to),
convert_to_unit(self.y, origin, to),
self.hole_diameter.map(|d| convert_to_unit(d, origin, to)),
@ -86,22 +95,11 @@ impl DrawableRaw for Obround {
points.push(points[0]);
let poly = Polygon::new(PlotPoints::from(points))
.fill_color(colour.to_colour32(selected))
.fill_color(colour.as_colour32(selected))
.stroke(Stroke::NONE);
ui.polygon(poly);
}
fn draw_egui(&self, ui: &mut Ui, selected: bool) {
ui.painter().add(RectShape::filled(
Rect::from_center_size(
self.position.invert_y().into(),
Vec2::new(self.rectangle.width as f32, self.rectangle.height as f32),
),
Rounding::same(self.rounding as f32 / 2.),
CanvasColour::Copper.to_colour32(selected),
));
}
fn to_paths(&self) -> ClipperPaths {
ClipperPaths::new(vec![self.outline.clone()])
}

View File

@ -0,0 +1,72 @@
use eframe::egui::Stroke;
use gerber_types::OutlinePrimitive;
use crate::{
application::CanvasColour,
geometry::{
elements::macro_decimal_to_f64, point::Point, ClipperPath, ClipperPaths, DrawableRaw, Unit,
},
};
#[derive(Debug, Clone)]
pub struct Polygon {
points: Vec<Point>,
}
impl Polygon {
pub fn new(points: &[Point]) -> Self {
Self {
points: points.to_vec(),
}
}
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
Self {
points: self
.points
.iter()
.map(|p| p.into_unit(origin, to))
.collect(),
}
}
pub fn from_macro(ap_macro: &OutlinePrimitive, variables: &[f64], target: Point) -> Self {
Self {
points: ap_macro
.points
.iter()
.map(|(x, y)| {
Point::new(
macro_decimal_to_f64(x, variables),
macro_decimal_to_f64(y, variables),
) + target
})
.collect(),
}
}
}
impl DrawableRaw for Polygon {
fn canvas_pos(&self) -> Point {
// self.position
todo!()
}
fn draw_egui_plot(&self, ui: &mut egui_plot::PlotUi, colour: CanvasColour, selected: bool) {
let points: Vec<[f64; 2]> = self.points.iter().map(|p| [p.x(), p.y()]).collect();
ui.polygon(
egui_plot::Polygon::new(points)
.fill_color(colour.as_colour32(selected))
.stroke(Stroke::NONE),
);
}
fn to_paths(&self) -> ClipperPaths {
ClipperPaths::new(vec![self.outline()])
}
fn outline(&self) -> ClipperPath {
ClipperPath::new(self.points.iter().map(|p| p.into()).collect())
}
}

View File

@ -1,20 +1,21 @@
use eframe::{
egui::{Rect, Rounding, Stroke, Ui, Vec2},
epaint::RectShape,
};
use eframe::egui::Stroke;
use egui_plot::{PlotPoints, Polygon};
use gerber_types::{CenterLinePrimitive, VectorLinePrimitive};
use crate::{
application::CanvasColour,
geometry::{ClipperPath, ClipperPaths},
};
use super::super::{
point::{convert_to_unit, Point},
DrawableRaw, Unit,
use super::{
super::{
point::{convert_to_unit, Point},
DrawableRaw, Unit,
},
macro_decimal_to_f64,
};
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Rectangle {
position: Point,
pub width: f64,
@ -34,11 +35,26 @@ impl Rectangle {
height: aperture.y,
hole_diameter: aperture.hole_diameter,
outline: vec![
(position.x - aperture.x / 2., position.y - aperture.y / 2.),
(position.x - aperture.x / 2., position.y + aperture.y / 2.),
(position.x + aperture.x / 2., position.y + aperture.y / 2.),
(position.x + aperture.x / 2., position.y - aperture.y / 2.),
(position.x - aperture.x / 2., position.y - aperture.y / 2.),
(
position.x() - aperture.x / 2.,
position.y() - aperture.y / 2.,
),
(
position.x() - aperture.x / 2.,
position.y() + aperture.y / 2.,
),
(
position.x() + aperture.x / 2.,
position.y() + aperture.y / 2.,
),
(
position.x() + aperture.x / 2.,
position.y() - aperture.y / 2.,
),
(
position.x() - aperture.x / 2.,
position.y() - aperture.y / 2.,
),
]
.into(),
}
@ -51,18 +67,66 @@ impl Rectangle {
height,
hole_diameter,
outline: vec![
(position.x - width / 2., position.y - height / 2.),
(position.x - width / 2., position.y + height / 2.),
(position.x + width / 2., position.y + height / 2.),
(position.x + width / 2., position.y - height / 2.),
(position.x() - width / 2., position.y() - height / 2.),
(position.x() - width / 2., position.y() + height / 2.),
(position.x() + width / 2., position.y() + height / 2.),
(position.x() + width / 2., position.y() - height / 2.),
]
.into(),
}
}
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
pub fn from_macro_vector_line(
ap_macro: &VectorLinePrimitive,
variables: &[f64],
target: Point,
) -> Self {
// TODO angle
let x_start = macro_decimal_to_f64(&ap_macro.start.0, variables);
let x_end = macro_decimal_to_f64(&ap_macro.end.0, variables);
let y_start = macro_decimal_to_f64(&ap_macro.start.1, variables);
let y_end = macro_decimal_to_f64(&ap_macro.end.1, variables);
let height = if x_end - x_start == 0. {
macro_decimal_to_f64(&ap_macro.width, variables)
} else {
x_end - x_start
};
let width = if y_end - y_start == 0. {
macro_decimal_to_f64(&ap_macro.width, variables)
} else {
y_end - y_start
};
Self::new(
self.position.to_unit(origin, to),
Point::new(x_start, y_start)
+ (Point::new(x_end, y_end) - Point::new(x_start, y_start)).div(2.)
+ target,
height,
width,
None,
)
}
pub fn from_macro_center_line(
ap_macro: &CenterLinePrimitive,
variables: &[f64],
target: Point,
) -> Self {
// TODO angle
Self::new(
Point::new(
macro_decimal_to_f64(&ap_macro.center.0, variables),
macro_decimal_to_f64(&ap_macro.center.1, variables),
) + target,
macro_decimal_to_f64(&ap_macro.dimensions.0, variables),
macro_decimal_to_f64(&ap_macro.dimensions.1, variables),
None,
)
}
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
Self::new(
self.position.into_unit(origin, to),
convert_to_unit(self.width, origin, to),
convert_to_unit(self.height, origin, to),
self.hole_diameter.map(|d| convert_to_unit(d, origin, to)),
@ -73,8 +137,8 @@ impl Rectangle {
impl DrawableRaw for Rectangle {
fn canvas_pos(&self) -> Point {
self.position
.shift_x(self.position.x / 2.)
.shift_y(self.position.y / 2.)
.shift_x(self.position.x() / 2.)
.shift_y(self.position.y() / 2.)
}
fn draw_egui_plot(&self, ui: &mut egui_plot::PlotUi, colour: CanvasColour, selected: bool) {
@ -82,22 +146,11 @@ impl DrawableRaw for Rectangle {
points.push(points[0]);
let poly = Polygon::new(PlotPoints::from(points))
.fill_color(colour.to_colour32(selected))
.fill_color(colour.as_colour32(selected))
.stroke(Stroke::NONE);
ui.polygon(poly);
}
fn draw_egui(&self, ui: &mut Ui, selected: bool) {
ui.painter().add(RectShape::filled(
Rect::from_center_size(
self.position.invert_y().into(),
Vec2::new(self.width as f32, self.height as f32),
),
Rounding::ZERO,
CanvasColour::Copper.to_colour32(selected),
));
}
fn to_paths(&self) -> ClipperPaths {
ClipperPaths::new(vec![self.outline.clone()])
}

View File

@ -1,9 +1,10 @@
use clipper2::Paths;
use std::collections::HashMap;
use gerber_types::{
Aperture, Command, Coordinates, DCode, FunctionCode, GCode, InterpolationMode, MCode,
Operation, Unit,
Aperture, ApertureMacro, Command, Coordinates, DCode, FunctionCode, GCode, InterpolationMode,
MCode, MacroContent, Operation, Unit,
};
use tracing::{debug, error, info};
use tracing::{error, info};
use crate::{
geometry::{
@ -16,6 +17,7 @@ use crate::{
};
use super::{
elements::{composite::Composite, polygon::Polygon},
union::{union_lines, union_with_apertures},
Geometry,
};
@ -32,7 +34,6 @@ impl From<GerberDoc> for Geometry {
// create geometries by applying all gerber commands
for command in gerber.commands {
println!("{command:?}");
match command {
Command::FunctionCode(f) => {
match f {
@ -83,6 +84,7 @@ impl From<GerberDoc> for Geometry {
Operation::Flash(f) => {
Self::add_geometry(
&mut added_apertures,
&gerber.aperture_macros,
&current_position,
&f,
&selected_aperture,
@ -93,6 +95,19 @@ impl From<GerberDoc> for Geometry {
},
// select an aperture
DCode::SelectAperture(ap) => {
// check if a aperture is selected and if it's circular
if let Some(Aperture::Circle(c)) = selected_aperture.as_ref() {
// check if a path is currently active
if !active_path.is_empty() {
// finish active path if there is an active one before selecting new aperture
active_path.finalize(c.diameter);
path_container.push(active_path);
// create new active path
active_path = LinePath::new();
}
}
selected_aperture = Some(
gerber
.apertures
@ -127,53 +142,13 @@ impl From<GerberDoc> for Geometry {
}
}
// create empty path set
let mut result = clipper2::Paths::new(vec![]);
// if path_container.len() > 1 {
// let mut clipper = path_container[1]
// .outline
// // .to_paths()
// .to_clipper_subject()
// .add_clip(path_container[2].outline.clone());
// // .add_clip(path_container[3].outline.clone())
// // .add_clip(path_container[4].outline.clone());
// // for clip in added_apertures.iter().skip(2) {
// // clipper = clipper.add_clip(clip.to_paths());
// // }
// // for line in path_container.iter().skip(2) {
// // clipper = clipper.add_clip(line.to_paths())
// // }
// result = clipper.union(clipper2::FillRule::default()).unwrap();
// result = result
// .to_clipper_subject()
// .add_clip(path_container[3].outline.clone())
// .add_clip(path_container[4].outline.clone())
// .union(clipper2::FillRule::default())
// .unwrap();
// }
// let mut geo = Paths::new(vec![]);
// union all drawn lines into nets of conductors
let conductor_net = union_lines(&path_container);
// union conductors with apertures
let united_nets = union_with_apertures(&added_apertures, conductor_net).unwrap();
// for outline in &conductor_net {
// println!("{:?}", outline.included_points);
// geo.push(outline.outline.clone());
// }
// println!("Number of conductor net paths: {}", geo.len());
if let Some(geo) = union_with_apertures(&added_apertures, conductor_net) {
println!("Number of finalized net paths: {}", geo.len());
result = geo;
}
Self {
outline_union: result,
united_nets,
apertures: added_apertures,
paths: path_container,
units: gerber.units.unwrap_or(Unit::Millimeters).into(),
@ -182,7 +157,7 @@ impl From<GerberDoc> for Geometry {
}
impl Geometry {
fn move_position(coord: &Coordinates, position: &mut Point) -> () {
fn move_position(coord: &Coordinates, position: &mut Point) {
if let Ok(pos) = Point::try_from(coord) {
*position = pos;
};
@ -190,6 +165,7 @@ impl Geometry {
fn add_geometry(
geometries: &mut Vec<Element>,
aperture_macros: &HashMap<String, ApertureMacro>,
position: &Point,
coordinates: &Coordinates,
aperture: &Option<Aperture>,
@ -216,7 +192,53 @@ impl Geometry {
error!("Unsupported Polygon aperture:\r\n{:#?}", p);
}
Aperture::Other(o) => {
error!("Unsupported Other aperture:\r\n{:#?}", o);
// split at '/' -> name/arguments
if let Some((name, args)) = o.split_once("/") {
// parse variables from args
let variables: Vec<f64> = args
.split(",")
.map(|s| s.parse::<f64>().unwrap_or(0.))
.collect();
if let Some(ap_macro) = aperture_macros.get(name) {
let mut composite = Composite::new();
// add elements from macro
for aperture in &ap_macro.content {
match aperture {
MacroContent::Circle(c) => composite.add(Element::Circle(
Circle::from_macro(c, &variables, target),
)),
MacroContent::VectorLine(vl) => composite.add(Element::Rectangle(
Rectangle::from_macro_vector_line(vl, &variables, target),
)),
MacroContent::CenterLine(cl) => composite.add(Element::Rectangle(
Rectangle::from_macro_center_line(cl, &variables, target),
)),
MacroContent::Outline(ol) => composite.add(Element::Polygon(
Polygon::from_macro(ol, &variables, target),
)),
MacroContent::Polygon(p) => {
error!("Polygon Not implemented yet")
}
MacroContent::Moire(m) => {
error!("Moire Not implemented yet")
}
MacroContent::Thermal(t) => {
error!("Thermal Not implemented yet")
}
MacroContent::VariableDefinition(_) => {}
MacroContent::Comment(_) => {}
};
}
composite.finalize();
geometries.push(Element::Composite(composite));
} else {
error!("Unsupported Other aperture:\r\n{:#?}", o);
}
} else {
error!("Unsupported Other aperture:\r\n{:#?}", o);
}
}
}
}

View File

@ -10,13 +10,14 @@ pub fn semi_circle(center: Point, diameter: f64, tilt: f64) -> Vec<(f64, f64)> {
.map(|i| {
let angle = (i as f64 / (CIRCLE_SEGMENTS / 2) as f64) * PI + tilt * (PI / 180.);
(
angle.cos() * diameter / 2. + center.x,
angle.sin() * diameter / 2. + center.y,
angle.cos() * diameter / 2. + center.x(),
angle.sin() * diameter / 2. + center.y(),
)
})
.collect()
}
#[allow(unused)]
pub enum CircleSegment {
North,
East,
@ -49,106 +50,106 @@ pub fn create_circular_path(
.map(|&i| {
let angle = (i as f64 / CIRCLE_SEGMENTS as f64) * 2.0 * PI;
(
angle.sin() * diameter / 2. + position.x,
angle.cos() * diameter / 2. + position.y,
angle.sin() * diameter / 2. + position.x(),
angle.cos() * diameter / 2. + position.y(),
)
})
.collect()
}
#[derive(Debug, PartialEq)]
pub enum Orientation {
Clockwise,
CounterClockwise,
CoLinear,
}
// #[derive(Debug, PartialEq)]
// pub enum Orientation {
// Clockwise,
// CounterClockwise,
// CoLinear,
// }
// To find orientation of ordered triplet (p, q, r).
// https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
pub fn orientation(p: impl Into<Point>, q: impl Into<Point>, r: impl Into<Point>) -> Orientation {
let (p, q, r) = (p.into(), q.into(), r.into());
// // To find orientation of ordered triplet (p, q, r).
// // https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
// pub fn orientation(p: impl Into<Point>, q: impl Into<Point>, r: impl Into<Point>) -> Orientation {
// let (p, q, r) = (p.into(), q.into(), r.into());
// See https://www.geeksforgeeks.org/orientation-3-ordered-points/
// for details of below formula.
let val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
// // See https://www.geeksforgeeks.org/orientation-3-ordered-points/
// // for details of below formula.
// let val = (q.y() - p.y()) * (r.x() - q.x()) - (q.x() - p.x()) * (r.y() - q.y());
match val {
0. => Orientation::CoLinear,
x if x > 0. => Orientation::Clockwise,
_ => Orientation::CounterClockwise,
}
}
// match val {
// 0. => Orientation::CoLinear,
// x if x > 0. => Orientation::Clockwise,
// _ => Orientation::CounterClockwise,
// }
// }
// Given three collinear points p, q, r, the function checks if
// point q lies on line segment 'pr'
pub fn on_segment(p: impl Into<Point>, q: impl Into<Point>, r: impl Into<Point>) -> bool {
let (p, q, r) = (p.into(), q.into(), r.into());
// // Given three collinear points p, q, r, the function checks if
// // point q lies on line segment 'pr'
// pub fn on_segment(p: impl Into<Point>, q: impl Into<Point>, r: impl Into<Point>) -> bool {
// let (p, q, r) = (p.into(), q.into(), r.into());
q.x <= greater_val(p.x, r.x)
&& q.x >= lower_val(p.x, r.x)
&& q.y <= greater_val(p.y, r.y)
&& q.y >= lower_val(p.y, r.y)
}
// q.x() <= greater_val(p.x(), r.x())
// && q.x() >= lower_val(p.x(), r.x())
// && q.y() <= greater_val(p.y(), r.y())
// && q.y() >= lower_val(p.y(), r.y())
// }
fn greater_val(a: f64, b: f64) -> f64 {
if a > b {
a
} else {
b
}
}
// fn greater_val(a: f64, b: f64) -> f64 {
// if a > b {
// a
// } else {
// b
// }
// }
fn lower_val(a: f64, b: f64) -> f64 {
if a < b {
a
} else {
b
}
}
// fn lower_val(a: f64, b: f64) -> f64 {
// if a < b {
// a
// } else {
// b
// }
// }
// The main function that returns true if line segment 'p1q1'
// and 'p2q2' intersect.
// https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
pub fn do_intersect(
p1: impl Into<Point>,
q1: impl Into<Point>,
p2: impl Into<Point>,
q2: impl Into<Point>,
) -> bool {
let (p1, q1, p2, q2) = (p1.into(), q1.into(), p2.into(), q2.into());
// // The main function that returns true if line segment 'p1q1'
// // and 'p2q2' intersect.
// // https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
// pub fn do_intersect(
// p1: impl Into<Point>,
// q1: impl Into<Point>,
// p2: impl Into<Point>,
// q2: impl Into<Point>,
// ) -> bool {
// let (p1, q1, p2, q2) = (p1.into(), q1.into(), p2.into(), q2.into());
// Find the four orientations needed for general and
// special cases
let o1 = orientation(p1, q1, p2);
let o2 = orientation(p1, q1, q2);
let o3 = orientation(p2, q2, p1);
let o4 = orientation(p2, q2, q1);
// // Find the four orientations needed for general and
// // special cases
// let o1 = orientation(p1, q1, p2);
// let o2 = orientation(p1, q1, q2);
// let o3 = orientation(p2, q2, p1);
// let o4 = orientation(p2, q2, q1);
// General case
if o1 != o2 && o3 != o4 {
return true;
}
// // General case
// if o1 != o2 && o3 != o4 {
// return true;
// }
// Special Cases
// p1, q1 and p2 are collinear and p2 lies on segment p1q1
if o1 == Orientation::CoLinear && on_segment(p1, p2, q1) {
return true;
}
// // Special Cases
// // p1, q1 and p2 are collinear and p2 lies on segment p1q1
// if o1 == Orientation::CoLinear && on_segment(p1, p2, q1) {
// return true;
// }
// p1, q1 and q2 are collinear and q2 lies on segment p1q1
if o2 == Orientation::CoLinear && on_segment(p1, q2, q1) {
return true;
};
// // p1, q1 and q2 are collinear and q2 lies on segment p1q1
// if o2 == Orientation::CoLinear && on_segment(p1, q2, q1) {
// return true;
// };
// p2, q2 and p1 are collinear and p1 lies on segment p2q2
if o3 == Orientation::CoLinear && on_segment(p2, p1, q2) {
return true;
};
// // p2, q2 and p1 are collinear and p1 lies on segment p2q2
// if o3 == Orientation::CoLinear && on_segment(p2, p1, q2) {
// return true;
// };
// p2, q2 and q1 are collinear and q1 lies on segment p2q2
if o4 == Orientation::CoLinear && on_segment(p2, q1, q2) {
return true;
};
// // p2, q2 and q1 are collinear and q1 lies on segment p2q2
// if o4 == Orientation::CoLinear && on_segment(p2, q1, q2) {
// return true;
// };
false // Doesn't fall in any of the above cases
}
// false // Doesn't fall in any of the above cases
// }

View File

@ -2,47 +2,39 @@ pub mod elements;
pub mod gerber;
mod helpers;
pub mod point;
mod union;
pub mod union;
use std::fmt::{Debug, Display};
use clipper2::{Bounds, Path, Paths, PointScaler};
use eframe::egui::Ui;
use egui_plot::PlotUi;
use elements::{linepath::LinePath, Element};
use point::Point;
use union::UnitedNets;
use crate::application::CanvasColour;
pub struct Geometry {
pub outline_union: ClipperPaths,
pub united_nets: UnitedNets,
pub apertures: Vec<Element>,
pub paths: Vec<LinePath>,
pub units: Unit,
}
impl Geometry {
pub fn to_unit(self, to: Unit) -> Self {
pub fn into_unit(self, to: Unit) -> Self {
Self {
outline_union: self
.outline_union
.iter()
.map(|p| {
p.iter()
.map(|p| (&Point::from(p).to_unit(self.units, to)).into())
.collect()
})
.collect(),
united_nets: self.united_nets.into_unit(self.units, to),
apertures: self
.apertures
.iter()
.map(|a| a.to_unit(self.units, to))
.into_iter()
.map(|a| a.into_unit(self.units, to))
.collect(),
paths: self
.paths
.iter()
.map(|l| l.to_unit(self.units, to))
.into_iter()
.map(|l| l.into_unit(self.units, to))
.collect(),
units: to,
}
@ -98,12 +90,7 @@ pub type ClipperBounds = Bounds<Micro>;
pub trait DrawableRaw {
fn canvas_pos(&self) -> Point;
fn draw_egui(&self, ui: &mut Ui, selected: bool);
fn draw_egui_plot(&self, ui: &mut PlotUi, colour: CanvasColour, selected: bool);
fn to_paths(&self) -> ClipperPaths;
fn outline(&self) -> ClipperPath;
}
fn canvas_position_from_gerber(gerber_position: &Point, offset: Point) -> Point {
gerber_position.shift_x(offset.x).shift_y(offset.y)
}

View File

@ -1,80 +1,115 @@
use std::ops::{Add, Sub};
use std::{
fmt,
marker::PhantomData,
ops::{Add, Sub},
};
use clipper2::PointScaler;
use eframe::egui::{Pos2, Vec2};
use egui_plot::PlotPoint;
use gerber_types::Coordinates;
use super::{ClipperPoint, Unit};
use super::{ClipperPoint, Micro, Unit};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Point<P: PointScaler = Micro> {
x: i64,
y: i64,
phantom_data: PhantomData<P>,
}
impl Point {
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x())
.field("y", &self.y())
.finish()
}
}
impl<P: PointScaler> Point<P> {
pub fn new(x: f64, y: f64) -> Self {
Point { x, y }
// Point { x, y }
Self {
x: P::scale(x) as i64,
y: P::scale(y) as i64,
phantom_data: PhantomData,
}
}
pub fn shift_x(&self, shift: f64) -> Self {
Self {
x: self.x + shift,
x: self.x + P::scale(shift) as i64,
y: self.y,
phantom_data: PhantomData,
}
}
pub fn shift_y(&self, shift: f64) -> Self {
Self {
x: self.x,
y: self.y + shift,
y: self.y + P::scale(shift) as i64,
phantom_data: PhantomData,
}
}
pub fn div(&self, divisor: f64) -> Self {
Self {
x: (self.x as f64 / divisor) as i64,
y: (self.y as f64 / divisor) as i64,
phantom_data: PhantomData,
}
}
pub fn len(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
P::descale(((self.x.pow(2) + self.y.pow(2)) as f64).sqrt())
}
pub fn normalize(&self) -> Self {
Self {
x: self.x / self.len(),
y: self.y / self.len(),
}
Self::new(self.x() / self.len(), self.y() / self.len())
}
pub fn invert_y(&self) -> Self {
Self {
x: self.x,
y: -self.y,
phantom_data: PhantomData,
}
}
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
Self {
x: convert_to_unit(self.x, origin, to),
y: convert_to_unit(self.y, origin, to),
}
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
Self::new(
convert_to_unit(self.x(), origin, to),
convert_to_unit(self.y(), origin, to),
)
}
/// Returns the x coordinate of the point.
pub fn x(&self) -> f64 {
P::descale(self.x as f64)
}
/// Returns the y coordinate of the point.
pub fn y(&self) -> f64 {
P::descale(self.y as f64)
}
}
impl From<Point> for (f64, f64) {
fn from(value: Point) -> Self {
(value.x, value.y)
(value.x(), value.y())
}
}
impl From<Point> for [f64; 2] {
fn from(value: Point) -> Self {
[value.x, value.y]
[value.x(), value.y()]
}
}
impl From<[f64; 2]> for Point {
fn from(value: [f64; 2]) -> Self {
Point {
x: value[0],
y: value[1],
}
Self::new(value[0], value[1])
}
}
@ -85,6 +120,7 @@ impl Add for Point {
Self {
x: self.x + other.x,
y: self.y + other.y,
phantom_data: PhantomData,
}
}
}
@ -96,6 +132,7 @@ impl Sub for Point {
Self {
x: self.x - other.x,
y: self.y - other.y,
phantom_data: PhantomData,
}
}
}
@ -103,8 +140,8 @@ impl Sub for Point {
impl From<Point> for Pos2 {
fn from(value: Point) -> Self {
Self {
x: value.x as f32,
y: value.y as f32,
x: value.x() as f32,
y: value.y() as f32,
}
}
}
@ -112,33 +149,30 @@ impl From<Point> for Pos2 {
impl From<&Point> for Pos2 {
fn from(value: &Point) -> Self {
Self {
x: value.x as f32,
y: value.y as f32,
x: value.x() as f32,
y: value.y() as f32,
}
}
}
impl From<Pos2> for Point {
fn from(value: Pos2) -> Self {
Self {
x: value.x.into(),
y: value.y.into(),
}
Self::new(value.x.into(), value.y.into())
}
}
impl From<Point> for Vec2 {
fn from(value: Point) -> Self {
Self {
x: value.x as f32,
y: value.y as f32,
x: value.x() as f32,
y: value.y() as f32,
}
}
}
impl From<&Point> for ClipperPoint {
fn from(value: &Point) -> Self {
Self::new(value.x, value.y)
Self::from_scaled(value.x, value.y)
}
}
@ -150,7 +184,7 @@ impl From<&ClipperPoint> for Point {
impl From<Point> for PlotPoint {
fn from(value: Point) -> Self {
Self::new(value.x, value.y)
Self::new(value.x(), value.y())
}
}

View File

@ -1,30 +1,110 @@
use std::collections::HashMap;
use clipper2::{FillRule, One, Paths, PointInPolygonResult};
use crate::geometry::helpers::do_intersect;
use clipper2::{FillRule, PointInPolygonResult};
use itertools::Itertools;
use super::{
elements::{linepath::LinePath, Element},
point::Point,
ClipperPaths,
ClipperBounds, ClipperPaths, ClipperPoint, Unit,
};
#[derive(Debug, Clone, Default)]
pub struct UnitedNets {
pub conductors: Vec<ConductorNet>,
pub isolated_apertures: Vec<Element>,
}
impl UnitedNets {
pub fn into_unit(self, origin: Unit, to: Unit) -> Self {
Self {
conductors: self
.conductors
.iter()
.map(|net| net.to_unit(origin, to))
.collect(),
isolated_apertures: self
.isolated_apertures
.into_iter()
.map(|el| el.into_unit(origin, to))
.collect(),
}
}
pub fn bounds(&self) -> ClipperBounds {
let conductor_net = self
.conductors
.iter()
.map(|net| net.outline.clone())
.reduce(|mut acc, v| {
acc.push(v);
acc
});
let isolated_apertures_bound = ClipperPaths::new(
self.isolated_apertures
.iter()
.map(|net| net.outline())
.collect(),
)
.bounds();
if let Some(net) = conductor_net {
let net_bounds = net.bounds();
ClipperBounds {
min: ClipperPoint::new(
net_bounds.min.x().min(isolated_apertures_bound.min.x()),
net_bounds.min.y().min(isolated_apertures_bound.min.y()),
),
max: ClipperPoint::new(
net_bounds.max.x().max(isolated_apertures_bound.max.x()),
net_bounds.max.y().max(isolated_apertures_bound.max.y()),
),
}
} else {
isolated_apertures_bound
}
}
}
#[derive(Debug, Clone)]
pub struct ConductorNet {
pub outline: ClipperPaths,
pub included_points: Vec<Point>,
}
impl ConductorNet {
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
Self {
outline: self
.outline
.iter()
.map(|p| {
p.iter()
.map(|p| (&Point::from(p).into_unit(origin, to)).into())
.collect()
})
.collect(),
included_points: self
.included_points
.iter()
.map(|el| el.into_unit(origin, to))
.collect(),
}
}
}
pub fn union_lines(lines: &[LinePath]) -> Vec<ConductorNet> {
let mut intersection_map = HashMap::new();
println!(
"START LINE UNION of {:?}",
lines
.iter()
.map(|l| l.points.clone())
.collect::<Vec<Vec<Point>>>()
);
// TODO prevent double checking same combination
let combinations = lines.iter().enumerate().combinations(2);
for co in combinations {
let (index, line) = co[0];
// print!("{} mit {};", combi[0].0, combi[1].0);
}
for (index, line) in lines.iter().enumerate() {
if !line.is_empty() {
// create list of intersecting lines
@ -36,16 +116,28 @@ pub fn union_lines(lines: &[LinePath]) -> Vec<ConductorNet> {
// do not check for intersection with itself
if index != i {
// check for all lines in path
let intersect = l.points.windows(2).any(|w| {
let (p2, q2) = (w[0], w[1]);
// println!("LINE 2 {p2:?}, {q2:?}");
do_intersect(p1, q1, p2, q2)
});
// let intersect = l.points.windows(2).any(|w| {
// let (p2, q2) = (w[0], w[1]);
// // println!("LINE 2 {p2:?}, {q2:?}");
// // do_intersect(p1, q1, p2, q2)
// line.outline
// .get(0)
// .unwrap()
// .is_point_inside(ClipperPoint::from(&p2))
// != PointInPolygonResult::IsOutside
// || line
// .outline
// .get(0)
// .unwrap()
// .is_point_inside(ClipperPoint::from(&q2))
// != PointInPolygonResult::IsOutside
// });
let intersect = line.outline_includes_points(&l.points);
// println!("INTERSECTING: {intersect}");
if intersect {
// let entry = intersection_map.entry(index).or_insert(Vec::new());
// entry.push(i);
println!("INTERSECTING {:?} and {:?}", line.points, l.points);
// println!("INTERSECTING {:?} and {:?}", line.points, l.points);
intersections.push(i);
}
}
@ -56,34 +148,20 @@ pub fn union_lines(lines: &[LinePath]) -> Vec<ConductorNet> {
}
}
println!("{intersection_map:?}");
let mut final_geo = Vec::new();
// go through all line segments
for i in 0..intersection_map.len() {
if let Some(mut intersections) = intersection_map.remove(&i) {
// if no intersections are given, add line as own conductor net
// if intersections.is_empty() {
// if let Some(line) = lines.get(i) {
// final_geo.push(ConductorNet {
// outline: line.outline.clone(),
// included_points: line.points.clone(),
// })
// }
// }
println!("--- Taking line segment {i}");
// get current line path
let mut geo = lines[i].outline.clone();
let mut included_points = lines[i].points.clone();
// union with intersecting lines until done
println!("Intersection points: {intersections:?}");
while let Some(other) = intersections.pop() {
println!("Intersecting with line # {other:?}");
// union with intersecting line
let intersecting_line = &lines[other];
if let Some(union) = union(&geo, &intersecting_line.outline) {
if let Some(union) = union_function(&geo, &intersecting_line.outline) {
geo = union;
}
// add points of added line to included points
@ -115,23 +193,16 @@ pub fn union_lines(lines: &[LinePath]) -> Vec<ConductorNet> {
}
}
// if final_geo.is_empty() {
// if let Some(line) = lines.get(0) {
// final_geo.push(ConductorNet {
// outline: line.outline.clone(),
// included_points: line.points.clone(),
// })
// }
// }
final_geo
}
pub fn union_with_apertures(
apertures: &Vec<Element>,
conductors: Vec<ConductorNet>,
) -> Option<ClipperPaths> {
let mut finalized_paths = Vec::new(); // handle apertures without connection
) -> Option<UnitedNets> {
let mut isolated_apertures = Vec::new();
// let mut finalized_paths = Vec::new(); // handle apertures without connection
let mut current_conductors = conductors;
// go through all apertures
@ -152,7 +223,7 @@ pub fn union_with_apertures(
.any(|c| ap.outline().is_point_inside(c.into()) != PointInPolygonResult::IsOutside)
{
// union aperture with conductor net
let geo = union(&geo, &conductor.outline)?;
let geo = union_function(&geo, &conductor.outline)?;
let mut cond = conductor;
cond.outline = geo;
isolated = false;
@ -164,24 +235,21 @@ pub fn union_with_apertures(
// add aperture to extra container if isolated
if isolated {
finalized_paths.push(geo);
// finalized_paths.push(geo);
isolated_apertures.push(ap.clone());
}
// update current conductors
current_conductors = new_conductors;
}
for conductor in current_conductors {
finalized_paths.push(conductor.outline);
}
finalized_paths.into_iter().reduce(|mut all, paths| {
all.push(paths);
all
Some(UnitedNets {
conductors: current_conductors,
isolated_apertures,
})
}
fn union(path1: &ClipperPaths, path2: &ClipperPaths) -> Option<ClipperPaths> {
pub fn union_function(path1: &ClipperPaths, path2: &ClipperPaths) -> Option<ClipperPaths> {
path1
.to_clipper_subject()
.add_clip(path2.clone())

View File

@ -1,7 +1,170 @@
use gerber_types::MacroContent;
use std::str::Chars;
pub fn parse(data: &str) -> Option<MacroContent> {
todo!()
use gerber_types::{
ApertureMacro, CenterLinePrimitive, CirclePrimitive, MacroContent, MacroDecimal,
OutlinePrimitive, PolygonPrimitive, VectorLinePrimitive,
};
use super::doc::GerberDoc;
pub fn start_macro(doc: &mut GerberDoc, macro_lines: &mut Option<Vec<String>>, line: &str) {
*macro_lines = Some(vec![line.replacen("%AM", "", 1)]);
if line.ends_with("*%") {
if let Some(aperture_macro) = parse(macro_lines.as_ref()) {
doc.aperture_macros
.insert(aperture_macro.name.to_string(), aperture_macro);
};
*macro_lines = None;
}
}
pub fn continue_macro(doc: &mut GerberDoc, macro_lines: &mut Option<Vec<String>>, line: &str) {
if let Some(lines) = macro_lines {
lines.push(line.to_string());
if line.ends_with("*%") {
if let Some(aperture_macro) = parse(macro_lines.as_ref()) {
doc.aperture_macros
.insert(aperture_macro.name.to_string(), aperture_macro);
};
*macro_lines = None;
}
} else {
start_macro(doc, macro_lines, line);
}
}
pub fn parse(data: Option<&Vec<String>>) -> Option<ApertureMacro> {
let mut name = "";
let mut macro_content = Vec::new();
let lines = data?;
// Go through every very part in the macro
for (index, line) in lines.iter().enumerate() {
// get name
if index == 0 {
name = line.strip_suffix("*")?;
continue;
}
let mut chars = line.chars();
// remove % char of last line
if index == lines.len() - 1 {
chars.next_back();
}
match chars.next()? {
// Comment
'0' => macro_content.push(MacroContent::Comment(line.to_string())),
// Circle
'1' => {
let args = get_attr_args(chars)?;
macro_content.push(MacroContent::Circle(CirclePrimitive {
exposure: args.first()? == &MacroDecimal::Value(1.0),
diameter: args.get(1)?.clone(),
center: (args.get(2)?.clone(), args.get(3)?.clone()),
angle: match args.get(4) {
Some(arg) => Some(arg.clone()),
None => Some(MacroDecimal::Value(0.)),
},
}));
}
'2' => {
match chars.next()? {
// Vector Line
'0' => {
let args = get_attr_args(chars)?;
macro_content.push(MacroContent::VectorLine(VectorLinePrimitive {
exposure: args.first()? == &MacroDecimal::Value(1.0),
end: (args.get(4)?.clone(), args.get(5)?.clone()),
width: args.get(1)?.clone(),
start: (args.get(2)?.clone(), args.get(3)?.clone()),
angle: match args.get(6) {
Some(arg) => arg.clone(),
None => MacroDecimal::Value(0.),
},
}));
}
// Center Line
'1' => {
let args = get_attr_args(chars)?;
macro_content.push(MacroContent::CenterLine(CenterLinePrimitive {
exposure: args.first()? == &MacroDecimal::Value(1.0),
dimensions: (args.get(1)?.clone(), args.get(2)?.clone()),
center: (args.get(3)?.clone(), args.get(4)?.clone()),
angle: match args.get(6) {
Some(arg) => arg.clone(),
None => MacroDecimal::Value(0.),
},
}));
}
_ => panic!("Unknown command in macro line:\n{} | {}", index, line),
}
}
// Outline
'4' => {
let args = get_attr_args(chars)?;
macro_content.push(MacroContent::Outline(OutlinePrimitive {
exposure: args.first()? == &MacroDecimal::Value(1.0),
points: (args)
.chunks_exact(2)
.map(|c| (c[0].clone(), c[1].clone()))
.collect(),
angle: args.last()?.clone(),
}));
}
// Polygon
'5' => {
let args = get_attr_args(chars)?;
macro_content.push(MacroContent::Polygon(PolygonPrimitive {
exposure: args.first()? == &MacroDecimal::Value(1.0),
vertices: match args.get(1)? {
MacroDecimal::Value(v) => *v as u8,
_ => 0,
},
center: (args.get(2)?.clone(), args.get(3)?.clone()),
diameter: args.get(4)?.clone(),
angle: match args.get(5) {
Some(arg) => arg.clone(),
None => MacroDecimal::Value(0.),
},
}));
}
'6' => todo!(), // Moiré
'7' => todo!(), // Thermal
'$' => todo!(), // Define variable
_ => panic!("Unknown command in macro line:\n{} | {}", index, line),
}
}
Some(ApertureMacro {
name: name.to_string(),
content: macro_content,
})
}
fn get_attr_args(mut attribute_chars: Chars) -> Option<Vec<MacroDecimal>> {
attribute_chars.next_back()?;
attribute_chars.next()?;
Some(
attribute_chars
.as_str()
.split(",")
.map(|el| el.trim())
.map(|el| {
if el.starts_with("$") {
MacroDecimal::Expression(el.to_string())
} else {
MacroDecimal::Value(el.parse::<f64>().unwrap())
}
})
.collect(),
)
}
// def parse_content(self):

View File

@ -1,7 +1,8 @@
use ::std::collections::HashMap;
use gerber_types::{Aperture, ApertureDefinition, Command, CoordinateFormat, ExtendedCode, Unit};
use gerber_types::{
Aperture, ApertureDefinition, ApertureMacro, Command, CoordinateFormat, ExtendedCode, Unit,
};
use std::fmt;
use std::iter::repeat;
#[derive(Debug, PartialEq)]
// Representation of Gerber document
@ -12,6 +13,8 @@ pub struct GerberDoc {
pub format_specification: Option<CoordinateFormat>,
/// map of apertures which can be used in draw commands later on in the document.
pub apertures: HashMap<i32, Aperture>,
/// map of aperture macro which can be used to create aperture definitions
pub aperture_macros: HashMap<String, ApertureMacro>,
// Anything else, draw commands, comments, attributes
pub commands: Vec<Command>,
}
@ -23,6 +26,7 @@ impl GerberDoc {
units: None,
format_specification: None,
apertures: HashMap::new(),
aperture_macros: HashMap::new(),
commands: Vec::new(),
}
}
@ -33,26 +37,24 @@ impl GerberDoc {
/// in the gerber-types rust crate. Note that aperture definitions will be sorted by code number
/// with lower codes being at the top of the command. This is independent of their order during
/// parsing.
pub fn to_commands(mut self) -> Vec<Command> {
pub fn into_commands(self) -> Vec<Command> {
let mut gerber_doc = self;
let mut gerber_cmds: Vec<Command> = Vec::new();
gerber_cmds.push(ExtendedCode::CoordinateFormat(self.format_specification.unwrap()).into());
gerber_cmds.push(ExtendedCode::Unit(self.units.unwrap()).into());
gerber_cmds
.push(ExtendedCode::CoordinateFormat(gerber_doc.format_specification.unwrap()).into());
gerber_cmds.push(ExtendedCode::Unit(gerber_doc.units.unwrap()).into());
// we add the apertures to the list, but we sort by code. This means the order of the output
// is reproducible every time.
let mut apertures = self.apertures.into_iter().collect::<Vec<_>>();
let mut apertures = gerber_doc.apertures.into_iter().collect::<Vec<_>>();
apertures.sort_by_key(|tup| tup.0);
for (code, aperture) in apertures {
gerber_cmds.push(
ExtendedCode::ApertureDefinition(ApertureDefinition {
code: code,
aperture: aperture,
})
.into(),
ExtendedCode::ApertureDefinition(ApertureDefinition { code, aperture }).into(),
)
}
gerber_cmds.append(&mut self.commands);
gerber_cmds.append(&mut gerber_doc.commands);
// TODO implement for units
gerber_cmds
}

View File

@ -1,5 +1,7 @@
mod aperture_macros;
pub mod doc;
use aperture_macros::{continue_macro, start_macro};
use doc::GerberDoc;
use gerber_types::{
Aperture, ApertureAttribute, ApertureFunction, Circle, Command, CoordinateFormat,
@ -25,12 +27,14 @@ pub fn parse_gerber<T: Read>(reader: BufReader<T>) -> GerberDoc {
// By default the 'last coordinate' can be taken to be (0,0)
let mut last_coords = (0i64, 0i64);
let mut aperture_macro: Option<Vec<String>> = None;
// naively define some regex terms
// TODO see which ones can be done without regex for better performance?
let re_units = regex!(r#"%MO(.*)\*%"#);
let re_comment = regex!(r#"G04 (.*)\*"#);
let re_formatspec = regex!(r#"%FSLAX(.*)Y(.*)\*%"#);
let re_aperture = regex!(r#"%ADD([0-9]+)([A-Z]),(.*)\*%"#);
let re_aperture = regex!(r#"%ADD([0-9]+)(\w*),(.*)\*%"#);
let re_interpolation = regex!(r#"X?(-?[0-9]+)?Y?(-?[0-9]+)?I?(-?[0-9]+)?J?(-?[0-9]+)?D01\*"#);
let re_move_or_flash = regex!(r#"X?(-?[0-9]+)?Y?(-?[0-9]+)?D0[2-3]*"#);
// TODO: handle escaped characters for attributes
@ -46,6 +50,12 @@ pub fn parse_gerber<T: Read>(reader: BufReader<T>) -> GerberDoc {
debug!("{}. {}", index + 1, &line);
if !line.is_empty() {
// continue aperture macro if one is active
if aperture_macro.is_some() {
continue_macro(&mut gerber_doc, &mut aperture_macro, line);
continue;
}
let mut linechars = line.chars();
match linechars.next().unwrap() {
@ -114,7 +124,8 @@ pub fn parse_gerber<T: Read>(reader: BufReader<T>) -> GerberDoc {
'A' => match linechars.next().unwrap() {
'D' => parse_aperture_defs(line, re_aperture, &mut gerber_doc), // AD
'M' => {
panic!("Aperture Macros (AM) are not supported yet.")
start_macro(&mut gerber_doc, &mut aperture_macro, line);
// panic!("Aperture Macros (AM) are not supported yet.")
} // AM
_ => line_parse_failure(line, index),
},
@ -348,7 +359,14 @@ fn parse_aperture_defs(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
}),
),
_ => {
panic!("Encountered unknown aperture definition statement")
if let Some(ap_macro) = gerber_doc.aperture_macros.get(aperture_type) {
gerber_doc.apertures.insert(
code,
Aperture::Other(format!("{}/{}", ap_macro.name, aperture_args.join(","))),
)
} else {
panic!("Encountered unknown aperture definition statement")
}
}
};
@ -511,7 +529,7 @@ fn parse_move_or_flash(
}
}
fn parse_load_mirroring(mut linechars: Chars, gerber_doc: &mut GerberDoc) {
fn parse_load_mirroring(mut _linechars: Chars, _gerber_doc: &mut GerberDoc) {
// match linechars.next().unwrap() {
// 'N' => gerber_doc.commands.push(value), //LMN
// 'Y' => gerber_doc.commands.push(value), // LMY
@ -528,7 +546,7 @@ fn parse_load_mirroring(mut linechars: Chars, gerber_doc: &mut GerberDoc) {
// a step and repeat open statement has four (required) parameters that we need to extract
// X (pos int) Y (pos int), I (decimal), J (decimal)
fn parse_step_repeat_open(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
println!("SR line: {}", &line);
// println!("SR line: {}", &line);
if let Some(regmatch) = re.captures(line) {
gerber_doc.commands.push(
ExtendedCode::StepAndRepeat(StepAndRepeat::Open {
@ -577,7 +595,7 @@ fn parse_step_repeat_open(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
/// ⚠️ Any other Attributes (which seem to be valid within the gerber spec) we will **fail** to parse!
///
/// ⚠️ This parsing statement needs a lot of tests and validation at the current stage!
fn parse_file_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc) {
fn _parse_file_attribute(line: Chars, _re: &Regex, gerber_doc: &mut GerberDoc) {
let attr_args = get_attr_args(line);
if attr_args.len() >= 2 {
// we must have at least 1 field
@ -626,7 +644,7 @@ fn parse_file_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc) {
/// ⚠️ Any other Attributes (which seem to be valid within the gerber spec) we will **fail** to parse!
///
/// ⚠️ This parsing statement needs a lot of tests and validation at the current stage!
fn parse_aperture_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc) {
fn parse_aperture_attribute(line: Chars, _re: &Regex, gerber_doc: &mut GerberDoc) {
let attr_args = get_attr_args(line);
if attr_args.len() >= 2 {
// we must have at least 1 field
@ -708,7 +726,7 @@ fn parse_aperture_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc)
}
}
fn parse_object_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc) {
fn parse_object_attribute(line: Chars, _re: &Regex, _gerber_doc: &mut GerberDoc) {
let attr_args = get_attr_args(line);
if attr_args.len() >= 2 {
// gerber_doc.commands.push(
@ -728,7 +746,7 @@ fn parse_object_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc) {
}
}
fn parse_delete_attribute(line: Chars, re: &Regex, gerber_doc: &mut GerberDoc) {
fn parse_delete_attribute(line: Chars, _re: &Regex, gerber_doc: &mut GerberDoc) {
let attr_args = get_attr_args(line);
match attr_args.len() {
1 => gerber_doc

View File

@ -6,13 +6,15 @@ mod export;
mod geometry;
mod gerber;
mod outline_geometry;
mod resources;
use application::Application;
use eframe::egui;
use eframe::egui::{self, IconData};
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer};
const APP_NAME: &str = "Outlinify";
const ICON: &[u8] = include_bytes!("../resources/icon.png");
fn main() -> eframe::Result {
let stdout_log = tracing_subscriber::fmt::layer()
@ -28,17 +30,38 @@ fn main() -> eframe::Result {
let application = Application::new();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([900.0, 700.0]),
viewport: egui::ViewportBuilder::default()
.with_inner_size([900.0, 700.0])
.with_icon(load_icon()),
..Default::default()
};
eframe::run_native(
APP_NAME,
options,
Box::new(|cc| {
// This gives us image support:
// egui_extras::install_image_loaders(&cc.egui_ctx);
egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::new(application))
}),
)
}
fn load_icon() -> IconData {
let (icon_rgba, icon_width, icon_height) = {
let icon = ICON;
let image = image::load_from_memory(icon)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}

View File

@ -1,13 +1,11 @@
use clipper2::{Bounds, EndType, JoinType, One, Paths};
use eframe::{
egui::{Rect, Shape, Stroke},
epaint::{PathShape, PathStroke},
};
use egui_plot::{PlotBounds, PlotItem, PlotPoint, Polygon};
use clipper2::{EndType, JoinType};
use crate::{
application::CanvasColour,
geometry::{elements::circle::Circle, point::Point, ClipperBounds, ClipperPaths, Unit},
excellon::drills::Drills,
geometry::{
point::Point, union::UnitedNets, ClipperBounds, ClipperPath, ClipperPaths, DrawableRaw,
Unit,
},
};
#[derive(Debug, Clone)]
@ -16,10 +14,8 @@ pub enum GeometryType {
Points(Vec<Point>),
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct OutlineGeometry {
// pub path: ClipperPaths,
// pub points: Vec<Point>,
items: GeometryType,
pub stroke: f32,
pub unit: Unit,
@ -28,26 +24,31 @@ pub struct OutlineGeometry {
}
impl OutlineGeometry {
pub fn new(
outline: &ClipperPaths,
stroke: f32,
unit: Unit,
bounds_from: &str,
bounds: ClipperBounds,
) -> Self {
// inflate given path
let outline = outline
.clone()
.inflate((stroke / 2.).into(), JoinType::Miter, EndType::Polygon, 30.)
.simplify(0.01, false);
pub fn new(united_nets: &UnitedNets, stroke: f32, unit: Unit, bounds_from: &str) -> Self {
let mut outline_paths = ClipperPaths::new(vec![]);
// inflate conductor net paths
for net in &united_nets.conductors {
outline_paths.push(
net.outline
.inflate((stroke / 2.).into(), JoinType::Round, EndType::Polygon, 2.)
.simplify(0.001, false),
)
}
// inflate isolated apertures
for ap in &united_nets.isolated_apertures {
outline_paths.push(
ap.to_paths()
.inflate((stroke / 2.).into(), JoinType::Round, EndType::Polygon, 2.)
.simplify(0.001, false),
);
}
Self {
// path: outline,
// points: Vec::new(),
items: GeometryType::Paths(outline),
items: GeometryType::Paths(outline_paths),
stroke,
unit,
bounds_from: bounds_from.into(),
bounding_box: bounds,
bounding_box: united_nets.bounds(),
}
}
@ -56,34 +57,30 @@ impl OutlineGeometry {
stroke: f32,
unit: Unit,
bounds_from: &str,
bounds: ClipperBounds,
) -> Self {
Self {
// path: outline.clone(),
// points: Vec::new(),
items: GeometryType::Paths(outline.clone()),
stroke,
unit,
bounds_from: bounds_from.into(),
bounding_box: bounds,
bounding_box: outline.bounds(),
}
}
pub fn point_marker(
points: Vec<Point>,
stroke: f32,
unit: Unit,
bounds_from: &str,
bounds: ClipperBounds,
) -> Self {
pub fn drill_marker(drills: &Drills, stroke: f32, unit: Unit, bounds_from: &str) -> Self {
Self {
// path: Paths::new(vec![]),
// points,
items: GeometryType::Points(points),
items: GeometryType::Points(drills.holes.iter().map(|c| c.canvas_pos()).collect()),
stroke,
unit,
bounds_from: bounds_from.into(),
bounding_box: bounds,
bounding_box: ClipperPaths::from(
drills
.holes
.iter()
.map(|hole| hole.outline.clone())
.collect::<Vec<ClipperPath>>(),
)
.bounds(),
}
}
@ -101,114 +98,3 @@ impl OutlineGeometry {
}
}
}
pub struct OutlineShape {
stroke: f32,
selected: bool,
colour: CanvasColour,
items: GeometryType,
bounds: ClipperBounds,
}
// impl OutlineShape {
// pub fn new_from_geometry(
// geometry: &OutlineGeometry,
// colour: CanvasColour,
// selected: bool,
// ) -> Self {
// Self {
// stroke: geometry.stroke,
// selected,
// items: geometry.items.clone(),
// bounds: geometry.bounding_box,
// colour,
// }
// }
// }
// impl PlotItem for OutlineShape {
// fn shapes(
// &self,
// _ui: &eframe::egui::Ui,
// transform: &egui_plot::PlotTransform,
// shapes: &mut Vec<eframe::egui::Shape>,
// ) {
// match &self.items {
// GeometryType::Paths(paths) => {
// for path in paths.iter() {
// let values_tf: Vec<_> = path
// .iter()
// .map(|v| transform.position_from_point(&PlotPoint::from(Point::from(v))))
// .collect();
// let shape = PathShape::closed_line(
// values_tf.clone(),
// PathStroke::new(20., self.colour.to_colour32(self.selected)),
// );
// shapes.push(shape.into());
// }
// }
// GeometryType::Points(points) => {
// for point in points {
// // draw outline of hole
// let points = Circle::circle_segment_points(
// Point::from(transform.position_from_point(&PlotPoint::from(*point))),
// self.stroke.into(),
// 1.,
// 0.,
// )
// .into_iter()
// .map(|p| Point::from(p).into())
// .collect();
// let polygon = Shape::convex_polygon(
// points,
// self.colour.to_colour32(self.selected),
// Stroke::NONE,
// );
// shapes.push(polygon);
// }
// // TODO draw point circles
// }
// }
// // todo!()
// }
// fn initialize(&mut self, x_range: std::ops::RangeInclusive<f64>) {
// {}
// }
// fn name(&self) -> &str {
// "Test"
// }
// fn color(&self) -> eframe::egui::Color32 {
// self.colour.to_colour32(self.selected)
// }
// fn highlight(&mut self) {
// {}
// }
// fn highlighted(&self) -> bool {
// false
// }
// fn allow_hover(&self) -> bool {
// false
// }
// fn geometry(&self) -> egui_plot::PlotGeometry<'_> {
// todo!()
// }
// fn bounds(&self) -> PlotBounds {
// PlotBounds::from_min_max(self.bounds.min.into(), self.bounds.max.into())
// }
// fn id(&self) -> Option<eframe::egui::Id> {
// None
// }
// }

20
src/resources/errors.rs Normal file
View File

@ -0,0 +1,20 @@
use std::fmt;
use error_stack::{Context, Report};
#[derive(Debug)]
pub struct ResourceError;
impl ResourceError {
pub fn new(text: &str) -> Report<Self> {
Report::new(Self).attach_printable(text.to_string())
}
}
impl fmt::Display for ResourceError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("Error loading resource")
}
}
impl Context for ResourceError {}

37
src/resources/fonts.rs Executable file
View File

@ -0,0 +1,37 @@
use eframe::egui::{FontData, FontDefinitions};
// const FONT_ROBOTO: &[u8] = include_bytes!("../../resources/Roboto-Light.ttf");
// const FONT_ROBOTO_MONO: &[u8] = include_bytes!("../../resources/RobotoMono-Light.ttf");
const FONT_OPEN_SANS: &[u8] = include_bytes!("../../resources/OpenSans-Light.ttf");
// pub fn register_fonts(style: &mut Style) {
// use eframe::epaint::FontFamily::{Monospace, Proportional};
// style.text_styles = [
// (TextStyle::Body, FontId::new(21.0, Proportional)),
// (TextStyle::Button, FontId::new(18.0, Monospace)),
// (TextStyle::Monospace, FontId::new(18.0, Monospace)),
// (TextStyle::Small, FontId::new(16.0, Proportional)),
// (TextStyle::Heading, FontId::new(48.0, Proportional)),
// ]
// .into();
// }
pub fn load_fonts() -> FontDefinitions {
use eframe::epaint::FontFamily::{Monospace, Proportional};
let mut fonts = FontDefinitions::default();
// let roboto = FontData::from_static(FONT_ROBOTO);
// let roboto_mono = FontData::from_static(FONT_ROBOTO_MONO);
let open_sans = FontData::from_static(FONT_OPEN_SANS);
// fonts.font_data.insert("roboto".into(), roboto);
// fonts.font_data.insert("roboto_mono".into(), roboto_mono);
fonts.font_data.insert("OpenSans".into(), open_sans);
// fonts.families.insert(Monospace, vec!["roboto_mono".into()]);
// fonts.families.insert(Monospace, vec!["roboto".into()]);
fonts.families.insert(Proportional, vec!["OpenSans".into()]);
fonts.families.insert(Monospace, vec!["OpenSans".into()]);
fonts
}

17
src/resources/icons.rs Normal file
View File

@ -0,0 +1,17 @@
use eframe::egui::{include_image, ImageSource};
pub struct Icons<'a> {
pub gerber_file: ImageSource<'a>,
pub excellon_file: ImageSource<'a>,
pub geometry_file: ImageSource<'a>,
}
impl<'a> Icons<'a> {
pub fn preload() -> Self {
Self {
gerber_file: include_image!("../../resources/gerber_file.png"),
excellon_file: include_image!("../../resources/excellon_file.png"),
geometry_file: include_image!("../../resources/geometry_file.png"),
}
}
}

25
src/resources/mod.rs Executable file
View File

@ -0,0 +1,25 @@
// mod fonts;
mod errors;
mod icons;
pub use self::icons::Icons;
pub struct ResourceLoader<'a> {
icons: Icons<'a>,
}
impl<'a> ResourceLoader<'a> {
pub fn new() -> Self {
Self {
icons: Icons::preload(),
}
}
// pub fn fonts(&self) -> FontDefinitions {
// fonts::load_fonts()
// }
pub fn icons(&self) -> &Icons {
&self.icons
}
}