Initial commit
This commit is contained in:
commit
e01cc0e39c
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/target
|
||||||
|
/tests
|
||||||
|
*.gbr
|
||||||
|
*.svg
|
||||||
|
*.drl
|
||||||
|
*.jpg
|
||||||
|
*.dxf
|
||||||
|
.DS_Store
|
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"color",
|
||||||
|
"Color",
|
||||||
|
"consts",
|
||||||
|
"cutout",
|
||||||
|
"eframe",
|
||||||
|
"egui",
|
||||||
|
"emath",
|
||||||
|
"epaint",
|
||||||
|
"excellon",
|
||||||
|
"Excellon",
|
||||||
|
"excellons",
|
||||||
|
"gerbers",
|
||||||
|
"Heatsink",
|
||||||
|
"linepath",
|
||||||
|
"obround",
|
||||||
|
"Obround",
|
||||||
|
"Outlinify",
|
||||||
|
"powi",
|
||||||
|
"rect",
|
||||||
|
"Rect",
|
||||||
|
"regmatch",
|
||||||
|
"Soldermask"
|
||||||
|
]
|
||||||
|
}
|
4576
Cargo.lock
generated
Normal file
4576
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "outlinify"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eframe = "0.28.1"
|
||||||
|
egui_plot = { version = "0.28.1", features = ["serde"] }
|
||||||
|
rfd = "0.14"
|
||||||
|
|
||||||
|
clipper2 = "0.4.1"
|
||||||
|
gerber-types = "0.3"
|
||||||
|
|
||||||
|
svg = "0.17"
|
||||||
|
dxf = "0.5"
|
||||||
|
|
||||||
|
lazy-regex = "3.1.0"
|
||||||
|
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
error-stack = "0.5"
|
56
src/application/canvas/excellons.rs
Normal file
56
src/application/canvas/excellons.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{Color32, Ui},
|
||||||
|
epaint::CircleShape,
|
||||||
|
};
|
||||||
|
use egui_plot::PlotUi;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::Application,
|
||||||
|
geometry::{elements::Element, DrawableRaw},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{draw_floating_area_on_canvas, draw_on_plot_canvas, CanvasColour, Drawer, PlotDrawer};
|
||||||
|
|
||||||
|
pub fn draw_excellons_(ui: &mut PlotUi, app: &mut Application) {
|
||||||
|
for (file_name, (_, excellon)) in &app.excellons {
|
||||||
|
let selected = &app.selection == file_name;
|
||||||
|
|
||||||
|
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_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
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
src/application/canvas/geometries.rs
Normal file
149
src/application/canvas/geometries.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{Color32, Pos2, Ui},
|
||||||
|
epaint::{CircleShape, PathShape, PathStroke},
|
||||||
|
};
|
||||||
|
use egui_plot::{Line, PlotPoint, PlotPoints, PlotUi};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::Application,
|
||||||
|
geometry::{elements::circle::Circle, DrawableRaw},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{draw_floating_area_on_canvas, draw_on_plot_canvas, CanvasColour, Drawer, PlotDrawer};
|
||||||
|
|
||||||
|
pub fn draw_geometries_(ui: &mut PlotUi, app: &mut Application) {
|
||||||
|
for (file_name, geo) in &app.outlines {
|
||||||
|
let selected = &app.selection == file_name;
|
||||||
|
|
||||||
|
// draw outline path
|
||||||
|
for path in geo.paths().iter() {
|
||||||
|
draw_on_plot_canvas(
|
||||||
|
ui,
|
||||||
|
app,
|
||||||
|
PlotDrawer::Closure(&|ui| {
|
||||||
|
// draw outline path
|
||||||
|
let mut points = path
|
||||||
|
.iter()
|
||||||
|
.map(|p| [p.x(), p.y()])
|
||||||
|
.collect::<Vec<[f64; 2]>>();
|
||||||
|
|
||||||
|
// calculate line width according to zoom factor
|
||||||
|
let transform = ui.transform();
|
||||||
|
let p1: Pos2 = transform.position_from_point(&PlotPoint::from(points[0]));
|
||||||
|
let p2: Pos2 = transform.position_from_point(&PlotPoint::from([
|
||||||
|
points[0][0] - geo.stroke as f64,
|
||||||
|
points[0][1],
|
||||||
|
]));
|
||||||
|
let width = (p1.x - p2.x).abs();
|
||||||
|
|
||||||
|
points.push(points[0]);
|
||||||
|
points.push(points[1]);
|
||||||
|
let line = Line::new(PlotPoints::from(points))
|
||||||
|
.width(width)
|
||||||
|
.color(CanvasColour::Outline.to_colour32(selected));
|
||||||
|
|
||||||
|
ui.line(line)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw point shapes
|
||||||
|
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
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
src/application/canvas/gerbers.rs
Normal file
99
src/application/canvas/gerbers.rs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{Color32, Pos2, Ui},
|
||||||
|
epaint::{PathShape, PathStroke},
|
||||||
|
};
|
||||||
|
use egui_plot::{Line, PlotPoints, PlotUi};
|
||||||
|
|
||||||
|
use crate::{application::Application, geometry::DrawableRaw};
|
||||||
|
|
||||||
|
use super::{draw_floating_area_on_canvas, draw_on_plot_canvas, CanvasColour, Drawer, PlotDrawer};
|
||||||
|
|
||||||
|
pub fn draw_gerbers_(ui: &mut PlotUi, app: &mut Application) {
|
||||||
|
for (file_name, (_, geo)) in &app.gerbers {
|
||||||
|
let selected = &app.selection == file_name;
|
||||||
|
|
||||||
|
for geometry in geo.apertures.iter() {
|
||||||
|
draw_on_plot_canvas(
|
||||||
|
ui,
|
||||||
|
app,
|
||||||
|
PlotDrawer::Drawable((geometry, CanvasColour::Copper, selected)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in geo.paths.iter() {
|
||||||
|
draw_on_plot_canvas(
|
||||||
|
ui,
|
||||||
|
app,
|
||||||
|
PlotDrawer::Closure(&|ui| line.draw_egui_plot(ui, CanvasColour::Copper, selected)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw union path
|
||||||
|
for path in geo.outline_union.iter() {
|
||||||
|
let mut points = path
|
||||||
|
.iter()
|
||||||
|
.map(|p| [p.x(), p.y()])
|
||||||
|
.collect::<Vec<[f64; 2]>>();
|
||||||
|
|
||||||
|
points.push(points[0]);
|
||||||
|
let line = Line::new(PlotPoints::from(points))
|
||||||
|
.color(CanvasColour::CopperOutline.to_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
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
src/application/canvas/live_position.rs
Normal file
63
src/application/canvas/live_position.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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())
|
||||||
|
}
|
116
src/application/canvas/mod.rs
Normal file
116
src/application/canvas/mod.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
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 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 crate::geometry::elements::Element;
|
||||||
|
|
||||||
|
use super::Application;
|
||||||
|
|
||||||
|
const COPPER_COLOR: Color32 = Color32::from_rgb(0xCA, 0xF6, 0x8F);
|
||||||
|
const COPPER_COLOR_SELECTED: Color32 = Color32::YELLOW;
|
||||||
|
const COPPER_OUTLINE: Color32 = Color32::BLACK;
|
||||||
|
const COPPER_OUTLINE_SELECTED: Color32 = Color32::LIGHT_RED;
|
||||||
|
const EXCELLON_COLOR: Color32 = Color32::BROWN;
|
||||||
|
const EXCELLON_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(65, 42, 42, 128);
|
||||||
|
const OUTLINE_COLOR: Color32 = Color32::DARK_BLUE;
|
||||||
|
const OUTLINE_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(0, 0, 139, 128);
|
||||||
|
|
||||||
|
pub enum CanvasColour {
|
||||||
|
Copper,
|
||||||
|
CopperOutline,
|
||||||
|
Excellon,
|
||||||
|
Outline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanvasColour {
|
||||||
|
pub fn to_colour32(&self, selected: bool) -> Color32 {
|
||||||
|
match (self, selected) {
|
||||||
|
(CanvasColour::Copper, true) => COPPER_COLOR_SELECTED,
|
||||||
|
(CanvasColour::Copper, false) => COPPER_COLOR,
|
||||||
|
(CanvasColour::CopperOutline, true) => COPPER_OUTLINE_SELECTED,
|
||||||
|
(CanvasColour::CopperOutline, false) => COPPER_OUTLINE,
|
||||||
|
(CanvasColour::Excellon, true) => EXCELLON_COLOR_SELECTED,
|
||||||
|
(CanvasColour::Excellon, false) => EXCELLON_COLOR,
|
||||||
|
(CanvasColour::Outline, true) => OUTLINE_COLOR_SELECTED,
|
||||||
|
(CanvasColour::Outline, false) => OUTLINE_COLOR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_canvas(ui: &mut Ui, app: &mut Application) {
|
||||||
|
egui_plot::Plot::new("ApplicationPlot")
|
||||||
|
// .view_aspect(2.0)
|
||||||
|
.data_aspect(1.0)
|
||||||
|
.show(ui, |plot_ui| {
|
||||||
|
draw_gerbers_(plot_ui, app);
|
||||||
|
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> {
|
||||||
|
Drawable((&'a Element, CanvasColour, bool)),
|
||||||
|
Closure(&'a dyn Fn(&mut PlotUi)),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_on_plot_canvas(ui: &mut PlotUi, app: &Application, drawer: PlotDrawer) {
|
||||||
|
match drawer {
|
||||||
|
PlotDrawer::Drawable((t, colour, selected)) => t.draw_egui_plot(ui, colour, selected),
|
||||||
|
PlotDrawer::Closure(fun) => fun(ui),
|
||||||
|
}
|
||||||
|
}
|
239
src/application/egui.rs
Normal file
239
src/application/egui.rs
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{self, Vec2},
|
||||||
|
emath::TSTransform,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
canvas::draw_canvas,
|
||||||
|
panels::{actions::draw_action_panel, header::draw_header, sidebar::draw_sidebar},
|
||||||
|
Application,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl eframe::App for Application {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
55
src/application/mod.rs
Normal file
55
src/application/mod.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
mod canvas;
|
||||||
|
mod egui;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use canvas::CanvasColour;
|
||||||
|
|
||||||
|
pub struct Application {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Variables {
|
||||||
|
laser_line_width: f32,
|
||||||
|
units: Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/application/panels/actions/excellon_actions.rs
Normal file
51
src/application/panels/actions/excellon_actions.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use eframe::egui::Ui;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::Application,
|
||||||
|
geometry::{ClipperPath, ClipperPaths, DrawableRaw},
|
||||||
|
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
|
||||||
|
.holes
|
||||||
|
.iter()
|
||||||
|
.map(|hole| hole.outline.clone())
|
||||||
|
.collect::<Vec<ClipperPath>>(),
|
||||||
|
)
|
||||||
|
.bounds(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/application/panels/actions/geometry_actions.rs
Normal file
34
src/application/panels/actions/geometry_actions.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/application/panels/actions/gerber_actions.rs
Normal file
28
src/application/panels/actions/gerber_actions.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
src/application/panels/actions/mod.rs
Normal file
28
src/application/panels/actions/mod.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
mod excellon_actions;
|
||||||
|
mod geometry_actions;
|
||||||
|
mod gerber_actions;
|
||||||
|
|
||||||
|
use eframe::egui::{self, Ui, Vec2};
|
||||||
|
use excellon_actions::show_excellon_actions;
|
||||||
|
use geometry_actions::show_geometry_actions;
|
||||||
|
use gerber_actions::show_gerber_actions;
|
||||||
|
|
||||||
|
use crate::application::Application;
|
||||||
|
|
||||||
|
pub const HOLE_MARK_DIAMETER: f32 = 0.1;
|
||||||
|
|
||||||
|
pub fn draw_action_panel(ui: &mut Ui, app: &mut Application) {
|
||||||
|
let (id, rect) = ui.allocate_space(Vec2::new(ui.available_width(), 100.));
|
||||||
|
|
||||||
|
egui::Area::new(id)
|
||||||
|
.default_width(rect.width())
|
||||||
|
.default_height(rect.height())
|
||||||
|
.movable(false)
|
||||||
|
.show(ui.ctx(), |ui| {
|
||||||
|
ui.heading("Actions");
|
||||||
|
|
||||||
|
show_gerber_actions(ui, app);
|
||||||
|
show_excellon_actions(ui, app);
|
||||||
|
show_geometry_actions(ui, app);
|
||||||
|
});
|
||||||
|
}
|
62
src/application/panels/header.rs
Normal file
62
src/application/panels/header.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use std::{fs::File, io::BufReader};
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::Application,
|
||||||
|
excellon::{drills::Drills, parse_excellon},
|
||||||
|
geometry::Geometry,
|
||||||
|
gerber::parse_gerber,
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
3
src/application/panels/mod.rs
Normal file
3
src/application/panels/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod actions;
|
||||||
|
pub mod header;
|
||||||
|
pub mod sidebar;
|
39
src/application/panels/sidebar.rs
Normal file
39
src/application/panels/sidebar.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use eframe::egui::{self, CollapsingHeader};
|
||||||
|
|
||||||
|
use crate::{application::Application, APP_NAME};
|
||||||
|
|
||||||
|
pub fn draw_sidebar(ctx: &egui::Context, app: &mut Application) {
|
||||||
|
egui::SidePanel::left("left_panel")
|
||||||
|
.exact_width(230.)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.heading(APP_NAME);
|
||||||
|
CollapsingHeader::new("Gerber")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (key, (name, _)) in app.gerbers.iter() {
|
||||||
|
ui.selectable_value(&mut app.selection, key.to_string(), name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CollapsingHeader::new("Excellon")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |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| {
|
||||||
|
for (key, _) in app.outlines.iter() {
|
||||||
|
ui.selectable_value(&mut app.selection, key.to_string(), key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (id, rect) = ui.allocate_space(ui.available_size());
|
||||||
|
let response = ui.interact(rect, id, egui::Sense::click_and_drag());
|
||||||
|
if response.clicked() {
|
||||||
|
app.selection = "".into();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
58
src/excellon/doc.rs
Normal file
58
src/excellon/doc.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::geometry::{point::Point, Unit};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
// Representation of Gerber document
|
||||||
|
pub struct ExcellonDoc {
|
||||||
|
// // unit type, defined once per document
|
||||||
|
pub units: Unit,
|
||||||
|
pub zeroes: Zeroes,
|
||||||
|
// // format specification for coordinates, defined once per document
|
||||||
|
// pub format_specification: Option<CoordinateFormat>,
|
||||||
|
// /// map of apertures which can be used in draw commands later on in the document.
|
||||||
|
/// map of tools which can be used in draw commands later on in the document.
|
||||||
|
pub tools: HashMap<u32, Tool>,
|
||||||
|
pub commands: Vec<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExcellonDoc {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
units: Unit::Inches,
|
||||||
|
zeroes: Zeroes::Leading,
|
||||||
|
tools: HashMap::new(),
|
||||||
|
commands: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Zeroes {
|
||||||
|
Leading,
|
||||||
|
Trailing,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Tool {
|
||||||
|
pub diameter: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tool {
|
||||||
|
pub fn new(diameter: f64) -> Self {
|
||||||
|
Self { diameter }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum Command {
|
||||||
|
FunctionCode(FunctionCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum FunctionCode {
|
||||||
|
EndOfFile,
|
||||||
|
SelectTool(u32),
|
||||||
|
Comment(String),
|
||||||
|
Drill(Point),
|
||||||
|
}
|
54
src/excellon/drills.rs
Normal file
54
src/excellon/drills.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
excellon::doc::{Command, FunctionCode},
|
||||||
|
geometry::{elements::circle::Circle, Unit},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::doc::ExcellonDoc;
|
||||||
|
|
||||||
|
pub struct Drills {
|
||||||
|
pub holes: Vec<Circle>,
|
||||||
|
pub units: Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExcellonDoc> for Drills {
|
||||||
|
fn from(doc: ExcellonDoc) -> Self {
|
||||||
|
// working variables
|
||||||
|
let mut selected_tool = None;
|
||||||
|
let mut holes = Vec::new();
|
||||||
|
|
||||||
|
for command in &doc.commands {
|
||||||
|
match command {
|
||||||
|
Command::FunctionCode(code) => match code {
|
||||||
|
FunctionCode::EndOfFile => {}
|
||||||
|
FunctionCode::SelectTool(id) => selected_tool = doc.tools.get(id),
|
||||||
|
FunctionCode::Comment(c) => debug!(c),
|
||||||
|
FunctionCode::Drill(p) => {
|
||||||
|
if let Some(tool) = selected_tool {
|
||||||
|
holes.push(Circle::new(*p, tool.diameter, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
units: doc.units,
|
||||||
|
holes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drills {
|
||||||
|
pub fn to_unit(&self, unit: Unit) -> Self {
|
||||||
|
Self {
|
||||||
|
units: unit,
|
||||||
|
holes: self
|
||||||
|
.holes
|
||||||
|
.iter()
|
||||||
|
.map(|hole| hole.to_unit(self.units, unit))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/excellon/errors.rs
Normal file
14
src/excellon/errors.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use error_stack::Context;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExcellonError;
|
||||||
|
|
||||||
|
impl fmt::Display for ExcellonError {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt.write_str("Error parsing Excellon document")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for ExcellonError {}
|
278
src/excellon/mod.rs
Normal file
278
src/excellon/mod.rs
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
mod doc;
|
||||||
|
pub mod drills;
|
||||||
|
mod errors;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader, Read},
|
||||||
|
str::Chars,
|
||||||
|
};
|
||||||
|
|
||||||
|
use doc::{Command, ExcellonDoc, FunctionCode, Tool, Zeroes};
|
||||||
|
use error_stack::{Report, ResultExt};
|
||||||
|
use errors::ExcellonError;
|
||||||
|
use lazy_regex::{regex, Regex};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use crate::geometry::{point::Point, Unit};
|
||||||
|
|
||||||
|
pub fn parse_excellon<T: Read>(reader: BufReader<T>) -> Result<ExcellonDoc, Report<ExcellonError>> {
|
||||||
|
let mut excellon_doc = ExcellonDoc::new();
|
||||||
|
|
||||||
|
// Number format and units
|
||||||
|
// INCH uses 6 digits
|
||||||
|
// METRIC uses 5/6
|
||||||
|
let re_units = regex!("^(INCH|METRIC)(?:,([TL])Z)?$");
|
||||||
|
|
||||||
|
let re_toolset = regex!(r#"^T(\d+)(?:.*C(\d*\.?\d*))?"#);
|
||||||
|
|
||||||
|
let re_coordinates = regex!(r#"X?(-?[0-9.]+)?Y?(-?[0-9.]+)?"#);
|
||||||
|
|
||||||
|
// Parse coordinates
|
||||||
|
let re_leading_zeroes = regex!(r#"^[-\+]?(0*)(\d*)"#);
|
||||||
|
|
||||||
|
let mut inside_header = false;
|
||||||
|
|
||||||
|
for (index, line) in reader.lines().enumerate() {
|
||||||
|
let raw_line = line.change_context(ExcellonError)?;
|
||||||
|
let line = raw_line.trim();
|
||||||
|
|
||||||
|
// Show the line
|
||||||
|
debug!("{}. {}", index + 1, &line);
|
||||||
|
|
||||||
|
if !line.is_empty() {
|
||||||
|
let mut line_chars = line.chars();
|
||||||
|
|
||||||
|
if inside_header {
|
||||||
|
match get_next_char(&mut line_chars)? {
|
||||||
|
'I' => parse_units(line, re_units, &mut excellon_doc),
|
||||||
|
'M' => match get_next_char(&mut line_chars)? {
|
||||||
|
'E' => parse_units(line, re_units, &mut excellon_doc),
|
||||||
|
'9' => inside_header = false, // End of header
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'T' => parse_toolset(line, re_toolset, &mut excellon_doc)?,
|
||||||
|
'%' => inside_header = false, // End of header
|
||||||
|
';' => parse_comment(line_chars, &mut excellon_doc),
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match get_next_char(&mut line_chars)? {
|
||||||
|
'G' => {}
|
||||||
|
'M' => match get_next_char(&mut line_chars)? {
|
||||||
|
'1' => {}
|
||||||
|
'3' => excellon_doc
|
||||||
|
.commands
|
||||||
|
.push(Command::FunctionCode(FunctionCode::EndOfFile)),
|
||||||
|
'4' => match get_next_char(&mut line_chars)? {
|
||||||
|
'8' => inside_header = true,
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'T' => {
|
||||||
|
// Select Tool
|
||||||
|
let id = line_chars
|
||||||
|
.as_str()
|
||||||
|
.parse::<u32>()
|
||||||
|
.change_context(ExcellonError)?;
|
||||||
|
excellon_doc
|
||||||
|
.commands
|
||||||
|
.push(Command::FunctionCode(FunctionCode::SelectTool(id)))
|
||||||
|
}
|
||||||
|
'X' | 'Y' => {
|
||||||
|
if let Some(reg_match) = re_coordinates.captures(line) {
|
||||||
|
let x = reg_match
|
||||||
|
.get(1)
|
||||||
|
.ok_or(ExcellonError)
|
||||||
|
.attach_printable("No x coordinate present")?
|
||||||
|
.as_str();
|
||||||
|
let y = reg_match
|
||||||
|
.get(2)
|
||||||
|
.ok_or(ExcellonError)
|
||||||
|
.attach_printable("No y coordinate present")?
|
||||||
|
.as_str();
|
||||||
|
|
||||||
|
let x = if x.contains(".") {
|
||||||
|
x.parse::<f64>().change_context(ExcellonError)?
|
||||||
|
} else {
|
||||||
|
parse_number(x, &excellon_doc, re_leading_zeroes)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let y = if y.contains(".") {
|
||||||
|
y.parse::<f64>().change_context(ExcellonError)?
|
||||||
|
} else {
|
||||||
|
parse_number(y, &excellon_doc, re_leading_zeroes)?
|
||||||
|
};
|
||||||
|
|
||||||
|
excellon_doc
|
||||||
|
.commands
|
||||||
|
.push(Command::FunctionCode(FunctionCode::Drill(Point::new(x, y))))
|
||||||
|
} else {
|
||||||
|
error!("Could not parse Coordinates")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
';' => parse_comment(line_chars, &mut excellon_doc),
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(excellon_doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next_char(chars: &mut Chars) -> Result<char, ExcellonError> {
|
||||||
|
chars.next().ok_or(ExcellonError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a Excellon Comment (e.g. '; This line is a comment and is ignored')
|
||||||
|
fn parse_comment(line: Chars, doc: &mut ExcellonDoc) {
|
||||||
|
let comment = line.as_str();
|
||||||
|
doc.commands
|
||||||
|
.push(Command::FunctionCode(FunctionCode::Comment(
|
||||||
|
comment.to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a Excellon unit statement (e.g. 'METRIC,LZ')
|
||||||
|
fn parse_units(line: &str, re: &Regex, doc: &mut ExcellonDoc) {
|
||||||
|
// Set the unit type
|
||||||
|
if let Some(reg_match) = re.captures(line) {
|
||||||
|
// Parse Unit
|
||||||
|
doc.units = match reg_match.get(1).map(|s| s.as_str()) {
|
||||||
|
Some("METRIC") => Unit::Millimeters,
|
||||||
|
Some("INCH") => Unit::Inches,
|
||||||
|
_ => {
|
||||||
|
error!("Incorrect excellon units format");
|
||||||
|
doc.units
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Parse Zeroes Type (Leading | Trailing)
|
||||||
|
doc.zeroes = match reg_match.get(2).map(|s| s.as_str()) {
|
||||||
|
Some("L") => doc::Zeroes::Leading,
|
||||||
|
Some("T") => doc::Zeroes::Trailing,
|
||||||
|
_ => {
|
||||||
|
error!("Incorrect excellon zeroes format");
|
||||||
|
doc.zeroes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_toolset(
|
||||||
|
line: &str,
|
||||||
|
re: &Regex,
|
||||||
|
doc: &mut ExcellonDoc,
|
||||||
|
) -> Result<(), Report<ExcellonError>> {
|
||||||
|
if let Some(reg_match) = re.captures(line) {
|
||||||
|
if let (Some(id), Some(size)) = (
|
||||||
|
reg_match.get(1).map(|s| s.as_str()),
|
||||||
|
reg_match.get(2).map(|s| s.as_str()),
|
||||||
|
) {
|
||||||
|
doc.tools.insert(
|
||||||
|
id.parse::<u32>().change_context(ExcellonError)?,
|
||||||
|
Tool::new(size.parse::<f64>().change_context(ExcellonError)?),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// print a simple message in case the parser hits a dead end
|
||||||
|
fn line_parse_failure(line: &str, index: usize) {
|
||||||
|
error!(
|
||||||
|
"## Excellon Parser ## Cannot parse line:\n{} | {}",
|
||||||
|
index, line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses coordinate numbers without period.
|
||||||
|
fn parse_number(
|
||||||
|
number_str: &str,
|
||||||
|
doc: &ExcellonDoc,
|
||||||
|
lz: &Regex,
|
||||||
|
) -> Result<f64, Report<ExcellonError>> {
|
||||||
|
match doc.zeroes {
|
||||||
|
Zeroes::Leading => {
|
||||||
|
// With leading zeros, when you type in a coordinate,
|
||||||
|
// the leading zeros must always be included. Trailing zeros
|
||||||
|
// are unneeded and may be left off. The CNC-7 will automatically add them.
|
||||||
|
// r'^[-\+]?(0*)(\d*)'
|
||||||
|
// 6 digits are divided by 10^4
|
||||||
|
// If less than size digits, they are automatically added,
|
||||||
|
// 5 digits then are divided by 10^3 and so on.
|
||||||
|
|
||||||
|
let re_match = lz
|
||||||
|
.captures(number_str)
|
||||||
|
.ok_or(ExcellonError)
|
||||||
|
.attach_printable("Leading zeroes regex does not match")?;
|
||||||
|
let (g1, g2) = (
|
||||||
|
re_match
|
||||||
|
.get(1)
|
||||||
|
.ok_or(ExcellonError)
|
||||||
|
.attach_printable("Regex MatchGroup 2")?
|
||||||
|
.as_str(),
|
||||||
|
re_match
|
||||||
|
.get(2)
|
||||||
|
.ok_or(ExcellonError)
|
||||||
|
.attach_printable("Regex MatchGroup 2")?
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(number_str.parse::<f64>().change_context(ExcellonError)?
|
||||||
|
/ (10_f64.powi(
|
||||||
|
g1.len() as i32 + g2.len() as i32
|
||||||
|
- match doc.units {
|
||||||
|
Unit::Inches => 2,
|
||||||
|
Unit::Millimeters => 3,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Zeroes::Trailing => {
|
||||||
|
// Trailing
|
||||||
|
// You must show all zeros to the right of the number and can omit
|
||||||
|
// all zeros to the left of the number. The CNC-7 will count the number
|
||||||
|
// of digits you typed and automatically fill in the missing zeros.
|
||||||
|
Ok(number_str.parse::<f64>().change_context(ExcellonError)?
|
||||||
|
/ match doc.units {
|
||||||
|
Unit::Millimeters => 1000., // Metric is 000.000
|
||||||
|
Unit::Inches => 10000., // Inches is 00.0000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_excellon_file() {
|
||||||
|
let excellon = r#"; This line is a comment and is ignored
|
||||||
|
; The next line starts the "header":
|
||||||
|
M48
|
||||||
|
; Units and number format:
|
||||||
|
INCH,LZ
|
||||||
|
; One tool is defined with diameter 0.04 inches,
|
||||||
|
; drill rate of 300 inches/minute and 55000 RPM.
|
||||||
|
T1C.04F300S55
|
||||||
|
; End of header, M95 or %, and beginning of body:
|
||||||
|
M95
|
||||||
|
|
||||||
|
; Use tool 1 defined in the header
|
||||||
|
T1
|
||||||
|
; Drill at points (123.45, 234.5) and (12.345, 234.5):
|
||||||
|
X12345Y23450
|
||||||
|
X012345Y234500
|
||||||
|
; End of program;
|
||||||
|
M30"#;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
17
src/export/dxf.rs
Normal file
17
src/export/dxf.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use dxf::{
|
||||||
|
entities::{Entity, EntityType, Line},
|
||||||
|
Drawing,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::geometry::Geometry;
|
||||||
|
|
||||||
|
pub struct DXFConverter;
|
||||||
|
|
||||||
|
impl DXFConverter {
|
||||||
|
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())));
|
||||||
|
// `added_entity_ref` is a reference to the newly added entity
|
||||||
|
drawing.save_file("./file.dxf").unwrap();
|
||||||
|
}
|
||||||
|
}
|
2
src/export/mod.rs
Normal file
2
src/export/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod dxf;
|
||||||
|
pub mod svg;
|
77
src/export/svg.rs
Normal file
77
src/export/svg.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use svg::{
|
||||||
|
node::element::{path::Data, Circle, Path},
|
||||||
|
Document,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{application::panels::actions::HOLE_MARK_DIAMETER, outline_geometry::OutlineGeometry};
|
||||||
|
|
||||||
|
pub struct SVGConverter;
|
||||||
|
|
||||||
|
impl SVGConverter {
|
||||||
|
pub fn export(geo: &OutlineGeometry, file: &std::path::Path) {
|
||||||
|
let view_box = geo.bounding_box;
|
||||||
|
|
||||||
|
let data: Vec<Data> = geo
|
||||||
|
.paths()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|path| {
|
||||||
|
if path.len() > 1 {
|
||||||
|
let mut iter = path.iter().map(|point| (point.x(), -point.y()));
|
||||||
|
let first = iter.next().unwrap();
|
||||||
|
let mut data = Data::new().move_to(first);
|
||||||
|
for point in iter {
|
||||||
|
data = data.line_to(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data.close();
|
||||||
|
|
||||||
|
Some(data)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let paths = data.into_iter().map(|data| {
|
||||||
|
Path::new()
|
||||||
|
.set("fill", "none")
|
||||||
|
.set("stroke", "black")
|
||||||
|
.set("stroke-width", geo.stroke)
|
||||||
|
.set("d", data)
|
||||||
|
});
|
||||||
|
|
||||||
|
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("fill", "black")
|
||||||
|
});
|
||||||
|
|
||||||
|
let document_width = view_box.max.x() - view_box.min.x();
|
||||||
|
let document_height = view_box.max.y() - view_box.min.y();
|
||||||
|
|
||||||
|
let mut document = Document::new()
|
||||||
|
.set(
|
||||||
|
"viewBox",
|
||||||
|
(
|
||||||
|
view_box.min.x(),
|
||||||
|
-view_box.min.y() - document_height,
|
||||||
|
document_width,
|
||||||
|
document_height,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.set("width", format!("{document_width}{}", geo.unit))
|
||||||
|
.set("height", format!("{document_height}{}", geo.unit));
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
document = document.add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
for point in points {
|
||||||
|
document = document.add(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg::save(file, &document).unwrap();
|
||||||
|
}
|
||||||
|
}
|
139
src/geometry/elements/circle.rs
Normal file
139
src/geometry/elements/circle.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use std::f64::consts::{PI, TAU};
|
||||||
|
|
||||||
|
use eframe::{
|
||||||
|
egui::{remap, Stroke, Ui},
|
||||||
|
epaint::CircleShape,
|
||||||
|
};
|
||||||
|
use egui_plot::{PlotPoints, PlotUi, Polygon};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::CanvasColour,
|
||||||
|
geometry::{ClipperPath, ClipperPaths},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::{
|
||||||
|
helpers::{create_circular_path, CircleSegment},
|
||||||
|
point::{convert_to_unit, Point},
|
||||||
|
DrawableRaw, Unit,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Circle {
|
||||||
|
pub position: Point,
|
||||||
|
pub diameter: f64,
|
||||||
|
pub hole_diameter: Option<f64>,
|
||||||
|
pub outline: ClipperPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Circle {
|
||||||
|
pub fn new(position: impl Into<Point>, diameter: f64, hole_diameter: Option<f64>) -> Self {
|
||||||
|
let position = position.into();
|
||||||
|
Self {
|
||||||
|
position: position,
|
||||||
|
diameter,
|
||||||
|
hole_diameter,
|
||||||
|
outline: create_circular_path(&position, diameter, CircleSegment::Full).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_aperture_circle(aperture: &gerber_types::Circle, position: Point) -> Self {
|
||||||
|
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);
|
||||||
|
let diameter = convert_to_unit(self.diameter, origin, to);
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
diameter,
|
||||||
|
outline: create_circular_path(&position, diameter, CircleSegment::Full).into(),
|
||||||
|
hole_diameter: self.hole_diameter.map(|hd| convert_to_unit(hd, origin, to)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
ui.polygon(
|
||||||
|
Polygon::new(circle_points)
|
||||||
|
.fill_color(colour.to_colour32(selected))
|
||||||
|
.stroke(Stroke::NONE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn circle_segment_points(
|
||||||
|
position: Point,
|
||||||
|
diameter: f64,
|
||||||
|
segment_width: f64,
|
||||||
|
rotation: f64,
|
||||||
|
) -> 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)
|
||||||
|
.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]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
circle_points
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_paths(&self) -> ClipperPaths {
|
||||||
|
ClipperPaths::new(vec![self.outline.clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outline(&self) -> ClipperPath {
|
||||||
|
self.outline.clone()
|
||||||
|
}
|
||||||
|
}
|
143
src/geometry/elements/linepath.rs
Normal file
143
src/geometry/elements/linepath.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
use eframe::{
|
||||||
|
egui::{Pos2, Stroke, Ui},
|
||||||
|
epaint::{CircleShape, PathShape, PathStroke},
|
||||||
|
};
|
||||||
|
use egui_plot::{PlotPoints, Polygon};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::CanvasColour,
|
||||||
|
geometry::{helpers::semi_circle, ClipperPath, ClipperPaths},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::{
|
||||||
|
point::{convert_to_unit, Point},
|
||||||
|
DrawableRaw, Unit,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LinePath {
|
||||||
|
pub points: Vec<Point>,
|
||||||
|
diameter: f64,
|
||||||
|
pub outline: ClipperPaths,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinePath {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
points: Vec::new(),
|
||||||
|
diameter: 0.,
|
||||||
|
outline: ClipperPaths::new(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.points.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, point: Point) {
|
||||||
|
self.points.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_stroke(&mut self, width: f64) {
|
||||||
|
self.diameter = width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finalize(&mut self, stroke_width: f64) {
|
||||||
|
self.diameter = stroke_width;
|
||||||
|
self.outline = self.create_outline();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
|
||||||
|
let mut converted = Self {
|
||||||
|
points: self.points.iter().map(|p| p.to_unit(origin, to)).collect(),
|
||||||
|
diameter: convert_to_unit(self.diameter, origin, to),
|
||||||
|
outline: ClipperPaths::new(vec![]),
|
||||||
|
};
|
||||||
|
converted.outline = converted.create_outline();
|
||||||
|
|
||||||
|
converted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_outline(&self) -> ClipperPaths {
|
||||||
|
let mut paths: Vec<ClipperPath> = Vec::new();
|
||||||
|
|
||||||
|
for (index, point) in self.points.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
paths.push(
|
||||||
|
create_outline_between_points(self.points[index - 1], *point, self.diameter)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipperPaths::new(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawableRaw for &LinePath {
|
||||||
|
fn canvas_pos(&self) -> Point {
|
||||||
|
if let Some(point) = self.points.first() {
|
||||||
|
point.to_owned()
|
||||||
|
} else {
|
||||||
|
Point::new(0., 0.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_egui_plot(&self, ui: &mut egui_plot::PlotUi, colour: CanvasColour, selected: bool) {
|
||||||
|
if let Some(path) = self.outline.get(0) {
|
||||||
|
let mut points: Vec<[f64; 2]> = path.iter().map(|p| [p.x(), p.y()]).collect();
|
||||||
|
points.push(points[0]);
|
||||||
|
|
||||||
|
let poly = Polygon::new(PlotPoints::from(points))
|
||||||
|
.fill_color(colour.to_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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outline(&self) -> ClipperPath {
|
||||||
|
self.outline.first().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_outline_between_points(point1: Point, point2: Point, width: f64) -> Vec<(f64, f64)> {
|
||||||
|
let line_vec = point2 - point1; // vector between start and end of line
|
||||||
|
let normalized = line_vec.normalize();
|
||||||
|
|
||||||
|
let angle =
|
||||||
|
(normalized.x).acos() * (180. / PI) * if normalized.y < 0. { -1. } else { 1. } + 90.;
|
||||||
|
|
||||||
|
let mut outline: Vec<(f64, f64)> = Vec::new();
|
||||||
|
|
||||||
|
outline.append(&mut semi_circle(point1, width, angle));
|
||||||
|
outline.append(&mut semi_circle(point2, width, angle + 180.));
|
||||||
|
|
||||||
|
outline
|
||||||
|
}
|
79
src/geometry/elements/mod.rs
Normal file
79
src/geometry/elements/mod.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use circle::Circle;
|
||||||
|
use eframe::egui::Ui;
|
||||||
|
use egui_plot::PlotUi;
|
||||||
|
use linepath::LinePath;
|
||||||
|
use obround::Obround;
|
||||||
|
use rectangle::Rectangle;
|
||||||
|
|
||||||
|
use crate::application::CanvasColour;
|
||||||
|
|
||||||
|
use super::{point::Point, ClipperPath, ClipperPaths, DrawableRaw, Unit};
|
||||||
|
|
||||||
|
pub mod circle;
|
||||||
|
pub mod linepath;
|
||||||
|
pub mod obround;
|
||||||
|
pub mod rectangle;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Element {
|
||||||
|
Circle(Circle),
|
||||||
|
Rectangle(Rectangle),
|
||||||
|
Line(LinePath),
|
||||||
|
Obround(Obround),
|
||||||
|
}
|
||||||
|
|
||||||
|
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::Rectangle(r) => r.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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canvas_pos(&self) -> Point {
|
||||||
|
match self {
|
||||||
|
Element::Circle(c) => c.canvas_pos(),
|
||||||
|
Element::Rectangle(r) => r.canvas_pos(),
|
||||||
|
Element::Line(l) => l.canvas_pos(),
|
||||||
|
Element::Obround(o) => o.canvas_pos(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_paths(&self) -> ClipperPaths {
|
||||||
|
match self {
|
||||||
|
Element::Circle(c) => c.to_paths(),
|
||||||
|
Element::Rectangle(r) => r.to_paths(),
|
||||||
|
Element::Line(l) => l.to_paths(),
|
||||||
|
Element::Obround(o) => o.to_paths(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn outline(&self) -> ClipperPath {
|
||||||
|
match self {
|
||||||
|
Element::Circle(c) => c.outline(),
|
||||||
|
Element::Rectangle(r) => r.outline(),
|
||||||
|
Element::Line(l) => l.outline(),
|
||||||
|
Element::Obround(o) => o.outline(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
src/geometry/elements/obround.rs
Normal file
112
src/geometry/elements/obround.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{Rect, Rounding, Stroke, Ui, Vec2},
|
||||||
|
epaint::RectShape,
|
||||||
|
};
|
||||||
|
use egui_plot::{PlotPoints, Polygon};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::CanvasColour,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Obround {
|
||||||
|
position: Point,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
rounding: f64,
|
||||||
|
outline: ClipperPath,
|
||||||
|
rectangle: Rectangle,
|
||||||
|
hole_diameter: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Obround {
|
||||||
|
pub fn new(position: Point, x: f64, y: f64, hole_diameter: Option<f64>) -> Self {
|
||||||
|
let diameter = if x < y { x } else { y };
|
||||||
|
let outline = if x == y {
|
||||||
|
create_circular_path(&position, x, CircleSegment::Full)
|
||||||
|
} else {
|
||||||
|
let mut path: Vec<(f64, f64)> = Vec::new();
|
||||||
|
// 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.));
|
||||||
|
} 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
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
rounding: diameter,
|
||||||
|
outline: outline.into(),
|
||||||
|
rectangle: Rectangle::new(position, x, y, hole_diameter),
|
||||||
|
hole_diameter: hole_diameter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_aperture_obround(aperture: &gerber_types::Rectangular, position: Point) -> Self {
|
||||||
|
Self::new(position, aperture.x, aperture.y, aperture.hole_diameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
|
||||||
|
Self::new(
|
||||||
|
self.position.to_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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawableRaw for Obround {
|
||||||
|
fn canvas_pos(&self) -> Point {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_egui_plot(&self, ui: &mut egui_plot::PlotUi, colour: CanvasColour, selected: bool) {
|
||||||
|
let mut points: Vec<[f64; 2]> = self.outline.iter().map(|p| [p.x(), p.y()]).collect();
|
||||||
|
points.push(points[0]);
|
||||||
|
|
||||||
|
let poly = Polygon::new(PlotPoints::from(points))
|
||||||
|
.fill_color(colour.to_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()])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outline(&self) -> ClipperPath {
|
||||||
|
self.outline.clone()
|
||||||
|
}
|
||||||
|
}
|
108
src/geometry/elements/rectangle.rs
Normal file
108
src/geometry/elements/rectangle.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use eframe::{
|
||||||
|
egui::{Rect, Rounding, Stroke, Ui, Vec2},
|
||||||
|
epaint::RectShape,
|
||||||
|
};
|
||||||
|
use egui_plot::{PlotPoints, Polygon};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::CanvasColour,
|
||||||
|
geometry::{ClipperPath, ClipperPaths},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::{
|
||||||
|
point::{convert_to_unit, Point},
|
||||||
|
DrawableRaw, Unit,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Rectangle {
|
||||||
|
position: Point,
|
||||||
|
pub width: f64,
|
||||||
|
pub height: f64,
|
||||||
|
hole_diameter: Option<f64>,
|
||||||
|
outline: ClipperPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rectangle {
|
||||||
|
pub fn from_aperture_rectangular(
|
||||||
|
aperture: &gerber_types::Rectangular,
|
||||||
|
position: Point,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
width: aperture.x,
|
||||||
|
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.),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(position: Point, width: f64, height: f64, hole_diameter: Option<f64>) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
width,
|
||||||
|
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.),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_unit(&self, origin: Unit, to: Unit) -> Self {
|
||||||
|
Self::new(
|
||||||
|
self.position.to_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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawableRaw for Rectangle {
|
||||||
|
fn canvas_pos(&self) -> Point {
|
||||||
|
self.position
|
||||||
|
.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) {
|
||||||
|
let mut points: Vec<[f64; 2]> = self.outline.iter().map(|p| [p.x(), p.y()]).collect();
|
||||||
|
points.push(points[0]);
|
||||||
|
|
||||||
|
let poly = Polygon::new(PlotPoints::from(points))
|
||||||
|
.fill_color(colour.to_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()])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outline(&self) -> ClipperPath {
|
||||||
|
self.outline.clone()
|
||||||
|
}
|
||||||
|
}
|
215
src/geometry/gerber.rs
Normal file
215
src/geometry/gerber.rs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
use clipper2::Paths;
|
||||||
|
use gerber_types::{
|
||||||
|
Aperture, Command, Coordinates, DCode, FunctionCode, GCode, InterpolationMode, MCode,
|
||||||
|
Operation, Unit,
|
||||||
|
};
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::{
|
||||||
|
elements::{
|
||||||
|
circle::Circle, linepath::LinePath, obround::Obround, rectangle::Rectangle, Element,
|
||||||
|
},
|
||||||
|
point::Point,
|
||||||
|
},
|
||||||
|
gerber::doc::GerberDoc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
union::{union_lines, union_with_apertures},
|
||||||
|
Geometry,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl From<GerberDoc> for Geometry {
|
||||||
|
fn from(gerber: GerberDoc) -> Self {
|
||||||
|
// working variables
|
||||||
|
let mut selected_aperture = None;
|
||||||
|
let mut selected_interpolation_mode = InterpolationMode::Linear;
|
||||||
|
let mut current_position = Point::new(0., 0.);
|
||||||
|
let mut active_path: LinePath = LinePath::new();
|
||||||
|
let mut path_container: Vec<LinePath> = Vec::new();
|
||||||
|
let mut added_apertures: Vec<Element> = Vec::new();
|
||||||
|
|
||||||
|
for command in gerber.commands {
|
||||||
|
println!("{command:?}");
|
||||||
|
match command {
|
||||||
|
Command::FunctionCode(f) => {
|
||||||
|
match f {
|
||||||
|
FunctionCode::DCode(code) => {
|
||||||
|
match code {
|
||||||
|
DCode::Operation(op) => match op {
|
||||||
|
Operation::Interpolate(coordinates, offset) => {
|
||||||
|
if selected_interpolation_mode == InterpolationMode::Linear
|
||||||
|
{
|
||||||
|
// self.add_draw_segment(coord);
|
||||||
|
let point = Point::try_from(&coordinates);
|
||||||
|
if active_path.is_empty() {
|
||||||
|
active_path.add(current_position);
|
||||||
|
}
|
||||||
|
match point {
|
||||||
|
Ok(point) => {
|
||||||
|
active_path.add(point);
|
||||||
|
}
|
||||||
|
Err(e) => error!("{e:?}"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
// self.add_arc_segment(coord, offset.as_ref().expect(format!("No offset coord with 'Circular' state\r\n{:#?}", c).as_str()))
|
||||||
|
}
|
||||||
|
Self::move_position(&coordinates, &mut current_position);
|
||||||
|
}
|
||||||
|
Operation::Move(m) => {
|
||||||
|
debug!("Move to {:?}, create path.", &m);
|
||||||
|
// self.create_path_from_data();
|
||||||
|
if let Some(Aperture::Circle(c)) =
|
||||||
|
selected_aperture.as_ref()
|
||||||
|
{
|
||||||
|
if !active_path.is_empty() {
|
||||||
|
active_path.finalize(c.diameter);
|
||||||
|
path_container.push(active_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
active_path = LinePath::new();
|
||||||
|
Geometry::move_position(&m, &mut current_position);
|
||||||
|
}
|
||||||
|
Operation::Flash(f) => {
|
||||||
|
// self.create_path_from_data();
|
||||||
|
Self::add_geometry(
|
||||||
|
&mut added_apertures,
|
||||||
|
¤t_position,
|
||||||
|
&f,
|
||||||
|
&selected_aperture,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::move_position(&f, &mut current_position);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DCode::SelectAperture(ap) => {
|
||||||
|
// self.create_path_from_data();
|
||||||
|
selected_aperture = Some(
|
||||||
|
gerber
|
||||||
|
.apertures
|
||||||
|
.get(&ap)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!("Unknown aperture id '{}'", ap)
|
||||||
|
})
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FunctionCode::GCode(code) => match code {
|
||||||
|
GCode::InterpolationMode(im) => selected_interpolation_mode = im,
|
||||||
|
GCode::Comment(c) => info!("[COMMENT] \"{}\"", c),
|
||||||
|
_ => error!("Unsupported GCode:\r\n{:#?}", code),
|
||||||
|
},
|
||||||
|
FunctionCode::MCode(m) => {
|
||||||
|
// check for end of file
|
||||||
|
if m == MCode::EndOfFile && !active_path.is_empty() {
|
||||||
|
// finish current path if one is present
|
||||||
|
if let Some(Aperture::Circle(c)) = selected_aperture.as_ref() {
|
||||||
|
active_path.finalize(c.diameter);
|
||||||
|
path_container.push(active_path);
|
||||||
|
break; // finish executing commands
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::ExtendedCode(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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![]);
|
||||||
|
let conductor_net = union_lines(&path_container);
|
||||||
|
|
||||||
|
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,
|
||||||
|
apertures: added_apertures,
|
||||||
|
paths: path_container,
|
||||||
|
units: gerber.units.unwrap_or(Unit::Millimeters).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Geometry {
|
||||||
|
fn move_position(coord: &Coordinates, position: &mut Point) -> () {
|
||||||
|
if let Ok(pos) = Point::try_from(coord) {
|
||||||
|
debug!("Moved position to {pos:?}");
|
||||||
|
*position = pos;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_geometry(
|
||||||
|
geometries: &mut Vec<Element>,
|
||||||
|
position: &Point,
|
||||||
|
coordinates: &Coordinates,
|
||||||
|
aperture: &Option<Aperture>,
|
||||||
|
) {
|
||||||
|
let target = match Point::try_from(coordinates) {
|
||||||
|
Ok(point) => point,
|
||||||
|
Err(_) => *position,
|
||||||
|
};
|
||||||
|
|
||||||
|
match aperture.as_ref().expect("No aperture selected") {
|
||||||
|
Aperture::Circle(c) => {
|
||||||
|
geometries.push(Element::Circle(Circle::from_aperture_circle(c, target)));
|
||||||
|
}
|
||||||
|
Aperture::Rectangle(r) => {
|
||||||
|
geometries.push(Element::Rectangle(Rectangle::from_aperture_rectangular(
|
||||||
|
r, target,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Aperture::Obround(o) => {
|
||||||
|
// error!("Unsupported Obround aperture:\r\n{:#?}", o);
|
||||||
|
geometries.push(Element::Obround(Obround::from_aperture_obround(o, target)));
|
||||||
|
}
|
||||||
|
Aperture::Polygon(p) => {
|
||||||
|
error!("Unsupported Polygon aperture:\r\n{:#?}", p);
|
||||||
|
}
|
||||||
|
Aperture::Other(o) => {
|
||||||
|
error!("Unsupported Other aperture:\r\n{:#?}", o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
src/geometry/helpers.rs
Normal file
154
src/geometry/helpers.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
use super::point::Point;
|
||||||
|
|
||||||
|
const CIRCLE_SEGMENTS: u32 = 512;
|
||||||
|
|
||||||
|
pub fn semi_circle(center: Point, diameter: f64, tilt: f64) -> Vec<(f64, f64)> {
|
||||||
|
(0..CIRCLE_SEGMENTS / 2)
|
||||||
|
.step_by(1)
|
||||||
|
.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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CircleSegment {
|
||||||
|
North,
|
||||||
|
East,
|
||||||
|
South,
|
||||||
|
West,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_circular_path(
|
||||||
|
position: &Point,
|
||||||
|
diameter: f64,
|
||||||
|
segment: CircleSegment,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
let segments: Vec<u32> = match segment {
|
||||||
|
CircleSegment::North => (CIRCLE_SEGMENTS / 4..CIRCLE_SEGMENTS - CIRCLE_SEGMENTS / 4)
|
||||||
|
.step_by(1)
|
||||||
|
.collect(),
|
||||||
|
CircleSegment::East => (0..CIRCLE_SEGMENTS / 2 + 1).step_by(1).collect(),
|
||||||
|
CircleSegment::South => (CIRCLE_SEGMENTS - CIRCLE_SEGMENTS / 4..CIRCLE_SEGMENTS)
|
||||||
|
.step_by(1)
|
||||||
|
.chain((0..CIRCLE_SEGMENTS / 4).step_by(1))
|
||||||
|
.collect(),
|
||||||
|
CircleSegment::West => (CIRCLE_SEGMENTS / 2 - 1..CIRCLE_SEGMENTS)
|
||||||
|
.step_by(1)
|
||||||
|
.collect(),
|
||||||
|
CircleSegment::Full => (0..CIRCLE_SEGMENTS).step_by(1).collect(),
|
||||||
|
};
|
||||||
|
segments
|
||||||
|
.iter()
|
||||||
|
.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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
}
|
109
src/geometry/mod.rs
Normal file
109
src/geometry/mod.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
pub mod elements;
|
||||||
|
pub mod gerber;
|
||||||
|
mod helpers;
|
||||||
|
pub mod point;
|
||||||
|
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 crate::application::CanvasColour;
|
||||||
|
|
||||||
|
pub struct Geometry {
|
||||||
|
pub outline_union: ClipperPaths,
|
||||||
|
pub apertures: Vec<Element>,
|
||||||
|
pub paths: Vec<LinePath>,
|
||||||
|
pub units: Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Geometry {
|
||||||
|
pub fn to_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(),
|
||||||
|
apertures: self
|
||||||
|
.apertures
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.to_unit(self.units, to))
|
||||||
|
.collect(),
|
||||||
|
paths: self
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.to_unit(self.units, to))
|
||||||
|
.collect(),
|
||||||
|
units: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Unit {
|
||||||
|
Millimeters,
|
||||||
|
Inches,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Unit {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Millimeters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gerber_types::Unit> for Unit {
|
||||||
|
fn from(value: gerber_types::Unit) -> Self {
|
||||||
|
match value {
|
||||||
|
gerber_types::Unit::Inches => Self::Inches,
|
||||||
|
gerber_types::Unit::Millimeters => Self::Millimeters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Unit {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Unit::Millimeters => "mm",
|
||||||
|
Unit::Inches => "in",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale by 10000.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
|
||||||
|
pub struct Micro;
|
||||||
|
|
||||||
|
impl PointScaler for Micro {
|
||||||
|
const MULTIPLIER: f64 = 10000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ClipperPoint = clipper2::Point<Micro>;
|
||||||
|
pub type ClipperPath = Path<Micro>;
|
||||||
|
pub type ClipperPaths = Paths<Micro>;
|
||||||
|
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)
|
||||||
|
}
|
197
src/geometry/point.rs
Normal file
197
src/geometry/point.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
use std::ops::{Add, Sub};
|
||||||
|
|
||||||
|
use eframe::egui::{Pos2, Vec2};
|
||||||
|
use egui_plot::PlotPoint;
|
||||||
|
use gerber_types::Coordinates;
|
||||||
|
|
||||||
|
use super::{ClipperPoint, Unit};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Point {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Point {
|
||||||
|
pub fn new(x: f64, y: f64) -> Self {
|
||||||
|
Point { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shift_x(&self, shift: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x + shift,
|
||||||
|
y: self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shift_y(&self, shift: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x,
|
||||||
|
y: self.y + shift,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> f64 {
|
||||||
|
(self.x.powi(2) + self.y.powi(2)).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x / self.len(),
|
||||||
|
y: self.y / self.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invert_y(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x,
|
||||||
|
y: -self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point> for (f64, f64) {
|
||||||
|
fn from(value: Point) -> Self {
|
||||||
|
(value.x, value.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point> for [f64; 2] {
|
||||||
|
fn from(value: Point) -> Self {
|
||||||
|
[value.x, value.y]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f64; 2]> for Point {
|
||||||
|
fn from(value: [f64; 2]) -> Self {
|
||||||
|
Point {
|
||||||
|
x: value[0],
|
||||||
|
y: value[1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Point {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x + other.x,
|
||||||
|
y: self.y + other.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Point {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, other: Self) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
x: self.x - other.x,
|
||||||
|
y: self.y - other.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point> for Pos2 {
|
||||||
|
fn from(value: Point) -> Self {
|
||||||
|
Self {
|
||||||
|
x: value.x as f32,
|
||||||
|
y: value.y as f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Point> for Pos2 {
|
||||||
|
fn from(value: &Point) -> Self {
|
||||||
|
Self {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point> for Vec2 {
|
||||||
|
fn from(value: Point) -> Self {
|
||||||
|
Self {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ClipperPoint> for Point {
|
||||||
|
fn from(value: &ClipperPoint) -> Self {
|
||||||
|
Self::new(value.x(), value.y())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point> for PlotPoint {
|
||||||
|
fn from(value: Point) -> Self {
|
||||||
|
Self::new(value.x, value.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CoordinateConversionError {
|
||||||
|
MissingXValue,
|
||||||
|
MissingYValue,
|
||||||
|
ParsingError(String),
|
||||||
|
FormattingError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Coordinates> for Point {
|
||||||
|
type Error = CoordinateConversionError;
|
||||||
|
|
||||||
|
fn try_from(value: &Coordinates) -> Result<Self, Self::Error> {
|
||||||
|
let x_coordinate = value
|
||||||
|
.x
|
||||||
|
.ok_or(CoordinateConversionError::MissingXValue)?
|
||||||
|
.gerber(&value.format)
|
||||||
|
.map_err(|_| CoordinateConversionError::FormattingError)?
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|e| CoordinateConversionError::ParsingError(e.to_string()))?;
|
||||||
|
let y_coordinate = value
|
||||||
|
.y
|
||||||
|
.ok_or(CoordinateConversionError::MissingYValue)?
|
||||||
|
.gerber(&value.format)
|
||||||
|
.map_err(|_| CoordinateConversionError::FormattingError)?
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|e| CoordinateConversionError::ParsingError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Point::new(
|
||||||
|
x_coordinate / 10_f64.powi(value.format.decimal as i32),
|
||||||
|
y_coordinate / 10_f64.powi(value.format.decimal as i32),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_to_unit(number: f64, from: Unit, to: Unit) -> f64 {
|
||||||
|
match (from, to) {
|
||||||
|
(Unit::Millimeters, Unit::Millimeters) | (Unit::Inches, Unit::Inches) => number,
|
||||||
|
(Unit::Millimeters, Unit::Inches) => number * 1. / 25.4,
|
||||||
|
(Unit::Inches, Unit::Millimeters) => number * 25.4,
|
||||||
|
}
|
||||||
|
}
|
189
src/geometry/union.rs
Normal file
189
src/geometry/union.rs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use clipper2::{FillRule, One, Paths, PointInPolygonResult};
|
||||||
|
|
||||||
|
use crate::geometry::helpers::do_intersect;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
elements::{linepath::LinePath, Element},
|
||||||
|
point::Point,
|
||||||
|
ClipperPaths,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConductorNet {
|
||||||
|
pub outline: ClipperPaths,
|
||||||
|
pub included_points: Vec<Point>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>>()
|
||||||
|
);
|
||||||
|
for (index, line) in lines.iter().enumerate() {
|
||||||
|
if !line.is_empty() {
|
||||||
|
// create list of intersecting lines
|
||||||
|
let mut intersections = Vec::new();
|
||||||
|
let (p1, q1) = (line.points[0], line.points[1]);
|
||||||
|
println!("LINE 1 {p1:?}, {q1:?}");
|
||||||
|
for (i, l) in lines.iter().enumerate() {
|
||||||
|
if !l.is_empty() {
|
||||||
|
// 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)
|
||||||
|
});
|
||||||
|
println!("INTERSECTING: {intersect}");
|
||||||
|
if intersect {
|
||||||
|
// let entry = intersection_map.entry(index).or_insert(Vec::new());
|
||||||
|
// entry.push(i);
|
||||||
|
intersections.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
intersection_map.insert(index, intersections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
geo = union;
|
||||||
|
}
|
||||||
|
// add points of added line to included points
|
||||||
|
for point in &intersecting_line.points {
|
||||||
|
if !included_points.contains(point) {
|
||||||
|
included_points.push(point.to_owned()); // show included points
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get intersections of united line 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create conductor net
|
||||||
|
final_geo.push(ConductorNet {
|
||||||
|
outline: geo,
|
||||||
|
included_points,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
let mut current_conductors = conductors;
|
||||||
|
|
||||||
|
// go through all apertures
|
||||||
|
for ap in apertures {
|
||||||
|
// get outline path
|
||||||
|
let geo = ap.to_paths();
|
||||||
|
// create empty set of conductors
|
||||||
|
let mut new_conductors = Vec::new();
|
||||||
|
// create indicator if aperture is isolated
|
||||||
|
let mut isolated = true;
|
||||||
|
|
||||||
|
// go through all available conductor nets
|
||||||
|
for conductor in current_conductors {
|
||||||
|
// check if any point of the conductor net is not outside of the aperture
|
||||||
|
if conductor
|
||||||
|
.included_points
|
||||||
|
.iter()
|
||||||
|
.any(|c| ap.outline().is_point_inside(c.into()) != PointInPolygonResult::IsOutside)
|
||||||
|
{
|
||||||
|
// union aperture with conductor net
|
||||||
|
let geo = union(&geo, &conductor.outline)?;
|
||||||
|
let mut cond = conductor;
|
||||||
|
cond.outline = geo;
|
||||||
|
isolated = false;
|
||||||
|
new_conductors.push(cond);
|
||||||
|
} else {
|
||||||
|
new_conductors.push(conductor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add aperture to extra container if isolated
|
||||||
|
if isolated {
|
||||||
|
finalized_paths.push(geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn union(path1: &ClipperPaths, path2: &ClipperPaths) -> Option<ClipperPaths> {
|
||||||
|
path1
|
||||||
|
.to_clipper_subject()
|
||||||
|
.add_clip(path2.clone())
|
||||||
|
.union(FillRule::default())
|
||||||
|
.ok()
|
||||||
|
}
|
76
src/gerber/aperture_macros.rs
Normal file
76
src/gerber/aperture_macros.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use gerber_types::MacroContent;
|
||||||
|
|
||||||
|
pub fn parse(data: &str) -> Option<MacroContent> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
// def parse_content(self):
|
||||||
|
// """
|
||||||
|
// Creates numerical lists for all primitives in the aperture
|
||||||
|
// macro (in ``self.raw``) by replacing all variables by their
|
||||||
|
// values iteratively and evaluating expressions. Results
|
||||||
|
// are stored in ``self.primitives``.
|
||||||
|
|
||||||
|
// :return: None
|
||||||
|
// """
|
||||||
|
// # Cleanup
|
||||||
|
// self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
|
||||||
|
// self.primitives = []
|
||||||
|
|
||||||
|
// # Separate parts
|
||||||
|
// parts = self.raw.split('*')
|
||||||
|
|
||||||
|
// #### Every part in the macro ####
|
||||||
|
// for part in parts:
|
||||||
|
// ### Comments. Ignored.
|
||||||
|
// match = ApertureMacro.amcomm_re.search(part)
|
||||||
|
// if match:
|
||||||
|
// continue
|
||||||
|
|
||||||
|
// ### Variables
|
||||||
|
// # These are variables defined locally inside the macro. They can be
|
||||||
|
// # numerical constant or defind in terms of previously define
|
||||||
|
// # variables, which can be defined locally or in an aperture
|
||||||
|
// # definition. All replacements ocurr here.
|
||||||
|
// match = ApertureMacro.amvar_re.search(part)
|
||||||
|
// if match:
|
||||||
|
// var = match.group(1)
|
||||||
|
// val = match.group(2)
|
||||||
|
|
||||||
|
// # Replace variables in value
|
||||||
|
// for v in self.locvars:
|
||||||
|
// val = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), val)
|
||||||
|
|
||||||
|
// # Make all others 0
|
||||||
|
// val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
|
||||||
|
|
||||||
|
// # Change x with *
|
||||||
|
// val = re.sub(r'[xX]', "*", val)
|
||||||
|
|
||||||
|
// # Eval() and store.
|
||||||
|
// self.locvars[var] = eval(val)
|
||||||
|
// continue
|
||||||
|
|
||||||
|
// ### Primitives
|
||||||
|
// # Each is an array. The first identifies the primitive, while the
|
||||||
|
// # rest depend on the primitive. All are strings representing a
|
||||||
|
// # number and may contain variable definition. The values of these
|
||||||
|
// # variables are defined in an aperture definition.
|
||||||
|
// match = ApertureMacro.amprim_re.search(part)
|
||||||
|
// if match:
|
||||||
|
// ## Replace all variables
|
||||||
|
// for v in self.locvars:
|
||||||
|
// part = re.sub(r'\$' + str(v) + r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
|
||||||
|
|
||||||
|
// # Make all others 0
|
||||||
|
// part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
|
||||||
|
|
||||||
|
// # Change x with *
|
||||||
|
// part = re.sub(r'[xX]', "*", part)
|
||||||
|
|
||||||
|
// ## Store
|
||||||
|
// elements = part.split(",")
|
||||||
|
// self.primitives.append([eval(x) for x in elements])
|
||||||
|
// continue
|
||||||
|
|
||||||
|
// log.warning("Unknown syntax of aperture macro part: %s" % str(part))
|
83
src/gerber/doc.rs
Normal file
83
src/gerber/doc.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use ::std::collections::HashMap;
|
||||||
|
use gerber_types::{Aperture, ApertureDefinition, Command, CoordinateFormat, ExtendedCode, Unit};
|
||||||
|
use std::fmt;
|
||||||
|
use std::iter::repeat;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
// Representation of Gerber document
|
||||||
|
pub struct GerberDoc {
|
||||||
|
// unit type, defined once per document
|
||||||
|
pub units: Option<Unit>,
|
||||||
|
// format specification for coordinates, defined once per document
|
||||||
|
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>,
|
||||||
|
// Anything else, draw commands, comments, attributes
|
||||||
|
pub commands: Vec<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GerberDoc {
|
||||||
|
// instantiate a empty gerber document ready for parsing
|
||||||
|
pub fn new() -> GerberDoc {
|
||||||
|
GerberDoc {
|
||||||
|
units: None,
|
||||||
|
format_specification: None,
|
||||||
|
apertures: HashMap::new(),
|
||||||
|
commands: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns a GerberDoc into a &vec of gerber-types Commands
|
||||||
|
///
|
||||||
|
/// Get a representation of a gerber document *purely* in terms of elements provided
|
||||||
|
/// 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> {
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 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<_>>();
|
||||||
|
apertures.sort_by_key(|tup| tup.0);
|
||||||
|
for (code, aperture) in apertures {
|
||||||
|
gerber_cmds.push(
|
||||||
|
ExtendedCode::ApertureDefinition(ApertureDefinition {
|
||||||
|
code: code,
|
||||||
|
aperture: aperture,
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
gerber_cmds.append(&mut self.commands);
|
||||||
|
// TODO implement for units
|
||||||
|
gerber_cmds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for GerberDoc {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let int_str: String = "_".repeat(self.format_specification.unwrap().integer as usize);
|
||||||
|
let dec_str: String = "_".repeat(self.format_specification.unwrap().decimal as usize);
|
||||||
|
|
||||||
|
writeln!(f, "GerberDoc").unwrap();
|
||||||
|
writeln!(f, "- units: {:?}", self.units).unwrap();
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"- format spec: {}.{} ({}|{})",
|
||||||
|
int_str,
|
||||||
|
dec_str,
|
||||||
|
self.format_specification.unwrap().integer,
|
||||||
|
self.format_specification.unwrap().decimal
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writeln!(f, "- apertures: ").unwrap();
|
||||||
|
for code in self.apertures.keys() {
|
||||||
|
writeln!(f, "\t {}", code).unwrap();
|
||||||
|
}
|
||||||
|
write!(f, "- commands: {}", &self.commands.len())
|
||||||
|
}
|
||||||
|
}
|
803
src/gerber/mod.rs
Normal file
803
src/gerber/mod.rs
Normal file
@ -0,0 +1,803 @@
|
|||||||
|
pub mod doc;
|
||||||
|
|
||||||
|
use doc::GerberDoc;
|
||||||
|
use gerber_types::{
|
||||||
|
Aperture, ApertureAttribute, ApertureFunction, Circle, Command, CoordinateFormat,
|
||||||
|
CoordinateNumber, CoordinateOffset, Coordinates, DCode, ExtendedCode, FiducialScope,
|
||||||
|
FileAttribute, FileFunction, FilePolarity, FunctionCode, GCode, InterpolationMode, MCode,
|
||||||
|
Operation, Part, Polarity, Polygon, QuadrantMode, Rectangular, SmdPadType, StepAndRepeat, Unit,
|
||||||
|
};
|
||||||
|
use lazy_regex::{regex, Regex};
|
||||||
|
use std::io::{BufRead, BufReader, Read};
|
||||||
|
use std::str::Chars;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
/// Parse a gerber string (in BufReader) to a GerberDoc
|
||||||
|
///
|
||||||
|
/// Take the contents of a Gerber (.gbr) file and parse it to a GerberDoc struct. The parsing does
|
||||||
|
/// some semantic checking, but is is certainly not exhaustive - so don't rely on it to check if
|
||||||
|
/// your Gerber file is valid according to the spec. Some of the parsing steps are greedy - they may
|
||||||
|
/// match something unexpected (rather than panicking) if there is a typo/fault in your file.
|
||||||
|
pub fn parse_gerber<T: Read>(reader: BufReader<T>) -> GerberDoc {
|
||||||
|
let mut gerber_doc = GerberDoc::new();
|
||||||
|
// The gerber spec allows omission of X or Y statements in D01/2/3 commands, where the omitted
|
||||||
|
// coordinate is to be taken as whatever was used in the previous coordinate-using command
|
||||||
|
// By default the 'last coordinate' can be taken to be (0,0)
|
||||||
|
let mut last_coords = (0i64, 0i64);
|
||||||
|
|
||||||
|
// 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_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
|
||||||
|
let re_attributes = regex!(r#"%T[A-Z].([A-Z]+?),?"#);
|
||||||
|
let re_step_repeat = regex!(r#"%SRX([0-9]+)Y([0-9]+)I(\d+\.?\d*)J(\d+\.?\d*)\*%"#);
|
||||||
|
|
||||||
|
for (index, line) in reader.lines().enumerate() {
|
||||||
|
let rawline = line.unwrap();
|
||||||
|
// TODO combine this with line above
|
||||||
|
let line = rawline.trim();
|
||||||
|
|
||||||
|
// Show the line
|
||||||
|
debug!("{}. {}", index + 1, &line);
|
||||||
|
|
||||||
|
if !line.is_empty() {
|
||||||
|
let mut linechars = line.chars();
|
||||||
|
|
||||||
|
match linechars.next().unwrap() {
|
||||||
|
'G' => {
|
||||||
|
match linechars.next().unwrap() {
|
||||||
|
'0' => match linechars.next().unwrap() {
|
||||||
|
'1' => gerber_doc.commands.push(
|
||||||
|
FunctionCode::GCode(GCode::InterpolationMode(
|
||||||
|
InterpolationMode::Linear,
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
), // G01
|
||||||
|
'2' => gerber_doc.commands.push(
|
||||||
|
FunctionCode::GCode(GCode::InterpolationMode(
|
||||||
|
InterpolationMode::ClockwiseCircular,
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
), // G02
|
||||||
|
'3' => gerber_doc.commands.push(
|
||||||
|
FunctionCode::GCode(GCode::InterpolationMode(
|
||||||
|
InterpolationMode::CounterclockwiseCircular,
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
), // G03
|
||||||
|
'4' => parse_comment(line, re_comment, &mut gerber_doc), // G04
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'3' => match linechars.next().unwrap() {
|
||||||
|
'6' => gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(FunctionCode::GCode(GCode::RegionMode(true)).into()), // G36
|
||||||
|
'7' => gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(FunctionCode::GCode(GCode::RegionMode(false)).into()), // G37
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'5' => match linechars.next().unwrap() {
|
||||||
|
// the G54 command (select aperture) is technically part of the Deprecated commands
|
||||||
|
'4' => {
|
||||||
|
// select aperture D<num>*
|
||||||
|
linechars.next_back(); // remove the trailing '*'
|
||||||
|
linechars.next(); // remove 'D'
|
||||||
|
parse_aperture_selection(linechars, &mut gerber_doc)
|
||||||
|
}
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'7' => match linechars.next().unwrap() {
|
||||||
|
// the G74 command is technically part of the Deprecated commands
|
||||||
|
'4' => gerber_doc.commands.push(
|
||||||
|
FunctionCode::GCode(GCode::QuadrantMode(QuadrantMode::Single))
|
||||||
|
.into(),
|
||||||
|
), // G74
|
||||||
|
'5' => gerber_doc.commands.push(
|
||||||
|
FunctionCode::GCode(GCode::QuadrantMode(QuadrantMode::Multi))
|
||||||
|
.into(),
|
||||||
|
), // G74
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}, // G75
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'%' => {
|
||||||
|
match linechars.next().unwrap() {
|
||||||
|
'M' => parse_units(line, re_units, &mut gerber_doc),
|
||||||
|
'F' => parse_format_spec(line, re_formatspec, &mut gerber_doc),
|
||||||
|
'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.")
|
||||||
|
} // AM
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'L' => match linechars.next().unwrap() {
|
||||||
|
'P' => match linechars.next().unwrap() {
|
||||||
|
'D' => gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(ExtendedCode::LoadPolarity(Polarity::Dark).into()), // LPD
|
||||||
|
'C' => gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(ExtendedCode::LoadPolarity(Polarity::Clear).into()), // LPC
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}, // LP
|
||||||
|
'M' => parse_load_mirroring(linechars, &mut gerber_doc), // LM
|
||||||
|
'R' => {
|
||||||
|
panic!("Load Mirroring (LM) command not supported yet.")
|
||||||
|
} // LR
|
||||||
|
'S' => {
|
||||||
|
panic!("Load Scaling (LS) command not supported yet.")
|
||||||
|
} // LS
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'T' => match linechars.next().unwrap() {
|
||||||
|
// TODO Turned off
|
||||||
|
// 'F' => parse_file_attribute(linechars, &re_attributes, &mut gerber_doc),
|
||||||
|
'A' => {
|
||||||
|
parse_aperture_attribute(linechars, re_attributes, &mut gerber_doc)
|
||||||
|
}
|
||||||
|
'O' => {
|
||||||
|
parse_object_attribute(linechars, re_attributes, &mut gerber_doc)
|
||||||
|
}
|
||||||
|
'D' => {
|
||||||
|
parse_delete_attribute(linechars, re_attributes, &mut gerber_doc)
|
||||||
|
}
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
'S' => match linechars.next().unwrap() {
|
||||||
|
'R' => match linechars.next().unwrap() {
|
||||||
|
'X' => {
|
||||||
|
parse_step_repeat_open(line, re_step_repeat, &mut gerber_doc)
|
||||||
|
}
|
||||||
|
// a statement %SR*% closes a step repeat command, which has no parameters
|
||||||
|
'*' => gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(ExtendedCode::StepAndRepeat(StepAndRepeat::Close).into()),
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
},
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'X' | 'Y' => {
|
||||||
|
linechars.next_back();
|
||||||
|
match linechars.next_back().unwrap() {
|
||||||
|
'1' => parse_interpolation(
|
||||||
|
line,
|
||||||
|
re_interpolation,
|
||||||
|
&mut gerber_doc,
|
||||||
|
&mut last_coords,
|
||||||
|
), // D01
|
||||||
|
'2' => parse_move_or_flash(
|
||||||
|
line,
|
||||||
|
re_move_or_flash,
|
||||||
|
&mut gerber_doc,
|
||||||
|
&mut last_coords,
|
||||||
|
false,
|
||||||
|
), // D02
|
||||||
|
'3' => parse_move_or_flash(
|
||||||
|
line,
|
||||||
|
re_move_or_flash,
|
||||||
|
&mut gerber_doc,
|
||||||
|
&mut last_coords,
|
||||||
|
true,
|
||||||
|
), // D03
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'D' => {
|
||||||
|
// select aperture D<num>*
|
||||||
|
linechars.next_back(); // remove the trailing '*'
|
||||||
|
parse_aperture_selection(linechars, &mut gerber_doc)
|
||||||
|
}
|
||||||
|
'M' => gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(FunctionCode::MCode(MCode::EndOfFile).into()),
|
||||||
|
_ => line_parse_failure(line, index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we ended with a gerber EOF command
|
||||||
|
assert_eq!(
|
||||||
|
gerber_doc.commands.last().unwrap(),
|
||||||
|
&Command::FunctionCode(FunctionCode::MCode(MCode::EndOfFile)),
|
||||||
|
"Missing M02 statement at end of file"
|
||||||
|
);
|
||||||
|
|
||||||
|
gerber_doc
|
||||||
|
}
|
||||||
|
|
||||||
|
// print a simple message in case the parser hits a dead end
|
||||||
|
fn line_parse_failure(line: &str, index: usize) {
|
||||||
|
error!("Cannot parse line:\n{} | {}", index, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a Gerber Comment (e.g. 'G04 This is a comment*')
|
||||||
|
fn parse_comment(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
let comment = regmatch.get(1).unwrap().as_str();
|
||||||
|
gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(FunctionCode::GCode(GCode::Comment(comment.to_string())).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a Gerber unit statement (e.g. '%MOMM*%')
|
||||||
|
fn parse_units(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
|
||||||
|
// Check that the units are not set yet (that would imply the set unit command is present twice)
|
||||||
|
if gerber_doc.units.is_some() {
|
||||||
|
panic! {"Cannot set unit type twice in the same document!"}
|
||||||
|
}
|
||||||
|
// Set the unit type
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
let units_str = regmatch.get(1).unwrap().as_str();
|
||||||
|
if units_str == "MM" {
|
||||||
|
gerber_doc.units = Some(Unit::Millimeters);
|
||||||
|
} else if units_str == "IN" {
|
||||||
|
gerber_doc.units = Some(Unit::Inches);
|
||||||
|
} else {
|
||||||
|
panic!("Incorrect gerber units format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a Gerber format spec statement (e.g. '%FSLAX23Y23*%')
|
||||||
|
fn parse_format_spec(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
|
||||||
|
// Ensure that FS was not set before, which would imply two FS statements in the same doc
|
||||||
|
if gerber_doc.format_specification.is_some() {
|
||||||
|
panic!("Cannot set format specification twice in the same document!")
|
||||||
|
}
|
||||||
|
// Set Format Specification
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
let mut fs_chars = regmatch.get(1).unwrap().as_str().chars();
|
||||||
|
let integer: u8 = fs_chars.next().unwrap().to_digit(10).unwrap() as u8;
|
||||||
|
let decimal: u8 = fs_chars.next().unwrap().to_digit(10).unwrap() as u8;
|
||||||
|
|
||||||
|
// the gerber spec states that the integer value can be at most 6
|
||||||
|
assert!(
|
||||||
|
(1..=6).contains(&integer),
|
||||||
|
"format spec integer value must be between 1 and 6"
|
||||||
|
);
|
||||||
|
|
||||||
|
let fs = CoordinateFormat::new(integer, decimal);
|
||||||
|
gerber_doc.format_specification = Some(fs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a Gerber aperture definition e.g. '%ADD44R, 2.0X3.0*%')
|
||||||
|
fn parse_aperture_defs(line: &str, re: &Regex, gerber_doc: &mut GerberDoc) {
|
||||||
|
// aperture definitions
|
||||||
|
// TODO: prevent the same aperture code being used twice
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
let code = regmatch
|
||||||
|
.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.parse::<i32>()
|
||||||
|
.expect("Failed to parse aperture code");
|
||||||
|
assert!(
|
||||||
|
code > 9,
|
||||||
|
"Aperture codes 0-9 cannot be used for custom apertures"
|
||||||
|
);
|
||||||
|
|
||||||
|
let aperture_type = regmatch.get(2).unwrap().as_str();
|
||||||
|
let aperture_args: Vec<&str> = regmatch.get(3).unwrap().as_str().split("X").collect();
|
||||||
|
|
||||||
|
//println!("The code is {}, and the aperture type is {} with params {:?}", code, aperture_type, aperture_args);
|
||||||
|
let insert_state = match aperture_type {
|
||||||
|
"C" => gerber_doc.apertures.insert(
|
||||||
|
code,
|
||||||
|
Aperture::Circle(Circle {
|
||||||
|
diameter: aperture_args[0].trim().parse::<f64>().unwrap(),
|
||||||
|
hole_diameter: if aperture_args.len() > 1 {
|
||||||
|
Some(aperture_args[1].trim().parse::<f64>().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
"R" => gerber_doc.apertures.insert(
|
||||||
|
code,
|
||||||
|
Aperture::Rectangle(Rectangular {
|
||||||
|
x: aperture_args[0].trim().parse::<f64>().unwrap(),
|
||||||
|
y: aperture_args[1].trim().parse::<f64>().unwrap(),
|
||||||
|
hole_diameter: if aperture_args.len() > 2 {
|
||||||
|
Some(aperture_args[2].trim().parse::<f64>().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
"O" => gerber_doc.apertures.insert(
|
||||||
|
code,
|
||||||
|
Aperture::Obround(Rectangular {
|
||||||
|
x: aperture_args[0].trim().parse::<f64>().unwrap(),
|
||||||
|
y: aperture_args[1].trim().parse::<f64>().unwrap(),
|
||||||
|
hole_diameter: if aperture_args.len() > 2 {
|
||||||
|
Some(aperture_args[2].trim().parse::<f64>().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
// note that for polygon we HAVE TO specify rotation if we want to add a hole
|
||||||
|
"P" => gerber_doc.apertures.insert(
|
||||||
|
code,
|
||||||
|
Aperture::Polygon(Polygon {
|
||||||
|
diameter: aperture_args[0].trim().parse::<f64>().unwrap(),
|
||||||
|
vertices: aperture_args[1].trim().parse::<u8>().unwrap(),
|
||||||
|
rotation: if aperture_args.len() > 2 {
|
||||||
|
Some(aperture_args[2].trim().parse::<f64>().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
hole_diameter: if aperture_args.len() > 3 {
|
||||||
|
Some(aperture_args[3].trim().parse::<f64>().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
panic!("Encountered unknown aperture definition statement")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// the insert state will be None if the key (i.e. aperture code) was not present yet,
|
||||||
|
// or a Some(Aperture) value if the key was already in use (see behaviour of HashMap.insert)
|
||||||
|
// If a key is already present we have to throw an error, as this is invalid
|
||||||
|
if insert_state.is_some() {
|
||||||
|
panic!("Cannot use the aperture code {} more than once!", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_aperture_selection(linechars: Chars, gerber_doc: &mut GerberDoc) {
|
||||||
|
let aperture_code = linechars
|
||||||
|
.as_str()
|
||||||
|
.parse::<i32>()
|
||||||
|
.expect("Failed to parse aperture selection");
|
||||||
|
assert!(
|
||||||
|
gerber_doc.apertures.contains_key(&aperture_code),
|
||||||
|
"Cannot select an aperture that is not defined"
|
||||||
|
);
|
||||||
|
gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(FunctionCode::DCode(DCode::SelectAperture(aperture_code)).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO clean up the except statements a bit
|
||||||
|
// parse a Gerber interpolation command (e.g. 'X2000Y40000I300J50000D01*')
|
||||||
|
fn parse_interpolation(
|
||||||
|
line: &str,
|
||||||
|
re: &Regex,
|
||||||
|
gerber_doc: &mut GerberDoc,
|
||||||
|
last_coords: &mut (i64, i64),
|
||||||
|
) {
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
let x_coord = match regmatch.get(1) {
|
||||||
|
Some(x) => {
|
||||||
|
let new_x = x.as_str().trim().parse::<i64>().unwrap();
|
||||||
|
last_coords.0 = new_x;
|
||||||
|
new_x
|
||||||
|
}
|
||||||
|
None => last_coords.0, // if match is None, then the coordinate must have been implicit
|
||||||
|
};
|
||||||
|
let y_coord = match regmatch.get(2) {
|
||||||
|
Some(y) => {
|
||||||
|
let new_y = y.as_str().trim().parse::<i64>().unwrap();
|
||||||
|
last_coords.1 = new_y;
|
||||||
|
new_y
|
||||||
|
}
|
||||||
|
None => last_coords.1, // if match is None, then the coordinate must have been implicit
|
||||||
|
};
|
||||||
|
|
||||||
|
if regmatch.get(3).is_some() {
|
||||||
|
// we have X,Y,I,J parameters and we are doing circular interpolation
|
||||||
|
let i_offset = regmatch
|
||||||
|
.get(3)
|
||||||
|
.expect("Unable to match I offset")
|
||||||
|
.as_str()
|
||||||
|
.trim()
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap();
|
||||||
|
let j_offset = regmatch
|
||||||
|
.get(4)
|
||||||
|
.expect("Unable to match J offset")
|
||||||
|
.as_str()
|
||||||
|
.trim()
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
gerber_doc.commands.push(
|
||||||
|
FunctionCode::DCode(DCode::Operation(Operation::Interpolate(
|
||||||
|
coordinates_from_gerber(
|
||||||
|
x_coord,
|
||||||
|
y_coord,
|
||||||
|
gerber_doc
|
||||||
|
.format_specification
|
||||||
|
.expect("Operation statement called before format specification"),
|
||||||
|
),
|
||||||
|
Some(coordinates_offset_from_gerber(
|
||||||
|
i_offset,
|
||||||
|
j_offset,
|
||||||
|
gerber_doc.format_specification.unwrap(),
|
||||||
|
)),
|
||||||
|
)))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// linear interpolation, only X,Y parameters
|
||||||
|
gerber_doc.commands.push(
|
||||||
|
FunctionCode::DCode(DCode::Operation(Operation::Interpolate(
|
||||||
|
coordinates_from_gerber(
|
||||||
|
x_coord,
|
||||||
|
y_coord,
|
||||||
|
gerber_doc
|
||||||
|
.format_specification
|
||||||
|
.expect("Operation statement called before format specification"),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse D01 (interpolate) command: {}", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO clean up the except statements a bit
|
||||||
|
// parse a Gerber move or flash command (e.g. 'X2000Y40000D02*')
|
||||||
|
fn parse_move_or_flash(
|
||||||
|
line: &str,
|
||||||
|
re: &Regex,
|
||||||
|
gerber_doc: &mut GerberDoc,
|
||||||
|
last_coords: &mut (i64, i64),
|
||||||
|
flash: bool,
|
||||||
|
) {
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
let x_coord = match regmatch.get(1) {
|
||||||
|
Some(x) => {
|
||||||
|
let new_x = x.as_str().trim().parse::<i64>().unwrap();
|
||||||
|
last_coords.0 = new_x;
|
||||||
|
new_x
|
||||||
|
}
|
||||||
|
None => last_coords.0, // if match is None, then the coordinate must have been implicit
|
||||||
|
};
|
||||||
|
let y_coord = match regmatch.get(2) {
|
||||||
|
Some(y) => {
|
||||||
|
let new_y = y.as_str().trim().parse::<i64>().unwrap();
|
||||||
|
last_coords.1 = new_y;
|
||||||
|
new_y
|
||||||
|
}
|
||||||
|
None => last_coords.1, // if match is None, then the coordinate must have been implicit
|
||||||
|
};
|
||||||
|
|
||||||
|
if flash {
|
||||||
|
gerber_doc.commands.push(
|
||||||
|
FunctionCode::DCode(DCode::Operation(Operation::Flash(coordinates_from_gerber(
|
||||||
|
x_coord,
|
||||||
|
y_coord,
|
||||||
|
gerber_doc
|
||||||
|
.format_specification
|
||||||
|
.expect("Operation statement called before format specification"),
|
||||||
|
))))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
gerber_doc.commands.push(
|
||||||
|
FunctionCode::DCode(DCode::Operation(Operation::Move(coordinates_from_gerber(
|
||||||
|
x_coord,
|
||||||
|
y_coord,
|
||||||
|
gerber_doc
|
||||||
|
.format_specification
|
||||||
|
.expect("Operation statement called before format specification"),
|
||||||
|
))))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse D02 (move) or D03 (flash) command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// 'X' => match linechars.next() {
|
||||||
|
// Some('Y') => {} //LMXY
|
||||||
|
// None => {} // LMX
|
||||||
|
// _ => panic!("Invalid load mirroring (LM) command: {}", linechars.as_str())
|
||||||
|
// }
|
||||||
|
// _ => panic!("Invalid load mirroring (LM) command: {}", linechars.as_str())
|
||||||
|
// }
|
||||||
|
panic!("Load Mirroring (LM) command not supported yet.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
if let Some(regmatch) = re.captures(line) {
|
||||||
|
gerber_doc.commands.push(
|
||||||
|
ExtendedCode::StepAndRepeat(StepAndRepeat::Open {
|
||||||
|
repeat_x: regmatch
|
||||||
|
.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.trim()
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap(),
|
||||||
|
repeat_y: regmatch
|
||||||
|
.get(2)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.trim()
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap(),
|
||||||
|
distance_x: regmatch
|
||||||
|
.get(3)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap(),
|
||||||
|
distance_y: regmatch
|
||||||
|
.get(4)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse Step and Repeat opening command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an Aperture Attribute (%TF.<AttributeName>[,<AttributeValue>]*%) into Command
|
||||||
|
///
|
||||||
|
/// For now we consider two types of TA statements:
|
||||||
|
/// 1. Aperture Function (AperFunction) with field: String
|
||||||
|
/// 2. Drill tolerance (DrillTolerance) with fields: [1] num [2] num
|
||||||
|
///
|
||||||
|
/// ⚠️ 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) {
|
||||||
|
let attr_args = get_attr_args(line);
|
||||||
|
if attr_args.len() >= 2 {
|
||||||
|
// we must have at least 1 field
|
||||||
|
//println!("TF args are: {:?}", attr_args);
|
||||||
|
let file_attr: FileAttribute = match attr_args[0] {
|
||||||
|
"Part" => match attr_args[1] {
|
||||||
|
"Single" => FileAttribute::Part(Part::Single),
|
||||||
|
"Array" => FileAttribute::Part(Part::Array),
|
||||||
|
"FabricationPanel" => FileAttribute::Part(Part::FabricationPanel),
|
||||||
|
"Coupon" => FileAttribute::Part(Part::Coupon),
|
||||||
|
"Other" => FileAttribute::Part(Part::Other(attr_args[2].to_string())),
|
||||||
|
_ => panic!("Unsupported Part type '{}' in TF statement", attr_args[1]),
|
||||||
|
},
|
||||||
|
// TODO do FileFunction properly, but needs changes in gerber-types
|
||||||
|
"FileFunction" => {
|
||||||
|
FileAttribute::FileFunction(FileFunction::Other(attr_args[1].to_string()))
|
||||||
|
}
|
||||||
|
"FilePolarity" => match attr_args[1] {
|
||||||
|
"Positive" => FileAttribute::FilePolarity(FilePolarity::Positive),
|
||||||
|
"Negative" => FileAttribute::FilePolarity(FilePolarity::Negative),
|
||||||
|
_ => panic!(
|
||||||
|
"Unsupported Polarity type '{}' in TF statement",
|
||||||
|
attr_args[1]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"Md5" => FileAttribute::Md5(attr_args[1].to_string()),
|
||||||
|
_ => panic!(
|
||||||
|
"The AttributeName '{}' is currently not supported for File Attributes",
|
||||||
|
attr_args[0]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
gerber_doc
|
||||||
|
.commands
|
||||||
|
.push(ExtendedCode::FileAttribute(file_attr).into())
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse file attribute (TF)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an Aperture Attribute (%TA.<AttributeName>[,<AttributeValue>]*%) into Command
|
||||||
|
///
|
||||||
|
/// For now we consider two types of TA statements:
|
||||||
|
/// 1. Aperture Function (AperFunction) with field: String
|
||||||
|
/// 2. Drill tolerance (DrillTolerance) with fields: [1] num [2] num
|
||||||
|
///
|
||||||
|
/// ⚠️ 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) {
|
||||||
|
let attr_args = get_attr_args(line);
|
||||||
|
if attr_args.len() >= 2 {
|
||||||
|
// we must have at least 1 field
|
||||||
|
//println!("TA args are: {:?}", attr_args);
|
||||||
|
match attr_args[0] {
|
||||||
|
"AperFunction" => {
|
||||||
|
let aperture_func: ApertureFunction = match attr_args[1] {
|
||||||
|
"ViaDrill" => ApertureFunction::ViaDrill,
|
||||||
|
"BackDrill" => ApertureFunction::BackDrill,
|
||||||
|
"ComponentDrill" => ApertureFunction::ComponentDrill { press_fit: None }, // TODO parse this
|
||||||
|
"CastellatedDrill" => ApertureFunction::CastellatedDrill,
|
||||||
|
"MechanicalDrill" => ApertureFunction::MechanicalDrill { function: None }, // TODO parse this
|
||||||
|
"Slot" => ApertureFunction::Slot,
|
||||||
|
"CutOut" => ApertureFunction::CutOut,
|
||||||
|
"Cavity" => ApertureFunction::Cavity,
|
||||||
|
"OtherDrill" => ApertureFunction::OtherDrill(attr_args[2].to_string()),
|
||||||
|
"ComponentPad" => ApertureFunction::ComponentPad { press_fit: None }, // TODO parse this
|
||||||
|
"SmdPad" => match attr_args[2] {
|
||||||
|
"CopperDefined" => ApertureFunction::SmdPad(SmdPadType::CopperDefined),
|
||||||
|
"SoldermaskDefined" => {
|
||||||
|
ApertureFunction::SmdPad(SmdPadType::SoldermaskDefined)
|
||||||
|
}
|
||||||
|
_ => panic!("Unsupported SmdPad type in TA statement"),
|
||||||
|
},
|
||||||
|
"BgaPad" => match attr_args[2] {
|
||||||
|
"CopperDefined" => ApertureFunction::BgaPad(SmdPadType::CopperDefined),
|
||||||
|
"SoldermaskDefined" => {
|
||||||
|
ApertureFunction::BgaPad(SmdPadType::SoldermaskDefined)
|
||||||
|
}
|
||||||
|
_ => panic!("Unsupported SmdPad type in TA statement"),
|
||||||
|
},
|
||||||
|
"HeatsinkPad" => ApertureFunction::HeatsinkPad,
|
||||||
|
"TestPad" => ApertureFunction::TestPad,
|
||||||
|
"CastellatedPad" => ApertureFunction::CastellatedPad,
|
||||||
|
"FiducialPad" => match attr_args[2] {
|
||||||
|
"Global" => ApertureFunction::FiducialPad(FiducialScope::Global),
|
||||||
|
"Local" => ApertureFunction::FiducialPad(FiducialScope::Local),
|
||||||
|
_ => panic!("Unsupported FiducialPad type in TA statement"),
|
||||||
|
},
|
||||||
|
"ThermalReliefPad" => ApertureFunction::ThermalReliefPad,
|
||||||
|
"WasherPad" => ApertureFunction::WasherPad,
|
||||||
|
"AntiPad" => ApertureFunction::AntiPad,
|
||||||
|
"OtherPad" => ApertureFunction::OtherPad(attr_args[2].to_string()),
|
||||||
|
"Conductor" => ApertureFunction::Conductor,
|
||||||
|
"NonConductor" => ApertureFunction::NonConductor,
|
||||||
|
"CopperBalancing" => ApertureFunction::CopperBalancing,
|
||||||
|
"Border" => ApertureFunction::Border,
|
||||||
|
"OtherCopper" => ApertureFunction::OtherCopper(attr_args[2].to_string()),
|
||||||
|
"Profile" => ApertureFunction::Profile,
|
||||||
|
"NonMaterial" => ApertureFunction::NonMaterial,
|
||||||
|
"Material" => ApertureFunction::Material,
|
||||||
|
"Other" => ApertureFunction::Other(attr_args[2].to_string()),
|
||||||
|
_ => panic!(
|
||||||
|
"The Aperture Function '{}' is currently not supported (/known)",
|
||||||
|
attr_args[1]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
gerber_doc.commands.push(
|
||||||
|
ExtendedCode::ApertureAttribute(ApertureAttribute::ApertureFunction(
|
||||||
|
aperture_func,
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"DrillTolerance" => gerber_doc.commands.push(
|
||||||
|
ExtendedCode::ApertureAttribute(ApertureAttribute::DrillTolerance {
|
||||||
|
plus: attr_args[1].parse::<f64>().unwrap(),
|
||||||
|
minus: attr_args[2].parse::<f64>().unwrap(),
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
_ => panic!(
|
||||||
|
"The AttributeName '{}' is currently not supported for Aperture Attributes",
|
||||||
|
attr_args[0]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse aperture attribute (TA)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
// ExtendedCode::ObjectAttribute(ObjectAttribute {
|
||||||
|
// attribute_name: attr_args[0].to_string(),
|
||||||
|
// values: attr_args[1..]
|
||||||
|
// .into_iter()
|
||||||
|
// .map(|val| val.to_string())
|
||||||
|
// .collect(),
|
||||||
|
// })
|
||||||
|
// .into(),
|
||||||
|
// )
|
||||||
|
} else if attr_args.len() == 1 {
|
||||||
|
panic!("Unable to add Object Attribute (TO) - TO statements need at least 1 field value on top of the name: '{}'", attr_args[0]);
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse object attribute (TO)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.commands
|
||||||
|
.push(ExtendedCode::DeleteAttribute(attr_args[0].to_string()).into()),
|
||||||
|
x if x > 1 => panic!(
|
||||||
|
"Unable to parse delete attribute (TD) - TD should not have any fields, got field '{}'",
|
||||||
|
attr_args[0]
|
||||||
|
),
|
||||||
|
_ => panic!("Unable to parse delete attribute (TD)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the individual elements (AttributeName and Fields) from Chars
|
||||||
|
///
|
||||||
|
/// The arguments of the attribute statement can have whitespace as this will be trimmed.
|
||||||
|
/// `attribute_chars` argument must be the **trimmed line** from the gerber file
|
||||||
|
/// with the **first three characters removed**. E.g. ".Part,single*%" not "%TF.Part,single*%"
|
||||||
|
/// ```
|
||||||
|
/// # use gerber_parser::parser::get_attr_args;
|
||||||
|
/// let attribute_chars = ".DrillTolerance, 0.02, 0.01 *%".chars();
|
||||||
|
///
|
||||||
|
/// let arguments = get_attr_args(attribute_chars);
|
||||||
|
/// assert_eq!(arguments, vec!["DrillTolerance","0.02","0.01"])
|
||||||
|
/// ```
|
||||||
|
pub fn get_attr_args(mut attribute_chars: Chars) -> Vec<&str> {
|
||||||
|
attribute_chars.next_back().unwrap();
|
||||||
|
attribute_chars.next_back().unwrap();
|
||||||
|
if attribute_chars.next().is_some() {
|
||||||
|
attribute_chars
|
||||||
|
.as_str()
|
||||||
|
.split(",")
|
||||||
|
.map(|el| el.trim())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![""]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn coordinates_from_gerber(
|
||||||
|
mut x_as_int: i64,
|
||||||
|
mut y_as_int: i64,
|
||||||
|
fs: CoordinateFormat,
|
||||||
|
) -> Coordinates {
|
||||||
|
// we have the raw gerber string as int but now have to convert it to nano precision format
|
||||||
|
// (i.e. 6 decimal precision) as this is what CoordinateNumber uses internally
|
||||||
|
let factor = (6u8 - fs.decimal) as u32;
|
||||||
|
x_as_int *= 10i64.pow(factor);
|
||||||
|
y_as_int *= 10i64.pow(factor);
|
||||||
|
Coordinates::new(
|
||||||
|
CoordinateNumber::new(x_as_int),
|
||||||
|
CoordinateNumber::new(y_as_int),
|
||||||
|
fs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn coordinates_offset_from_gerber(
|
||||||
|
mut x_as_int: i64,
|
||||||
|
mut y_as_int: i64,
|
||||||
|
fs: CoordinateFormat,
|
||||||
|
) -> CoordinateOffset {
|
||||||
|
// we have the raw gerber string as int but now have to convert it to nano precision format
|
||||||
|
// (i.e. 6 decimal precision) as this is what CoordinateNumber uses internally
|
||||||
|
let factor = (6u8 - fs.decimal) as u32;
|
||||||
|
x_as_int *= 10i64.pow(factor);
|
||||||
|
y_as_int *= 10i64.pow(factor);
|
||||||
|
CoordinateOffset::new(
|
||||||
|
CoordinateNumber::new(x_as_int),
|
||||||
|
CoordinateNumber::new(y_as_int),
|
||||||
|
fs,
|
||||||
|
)
|
||||||
|
}
|
44
src/main.rs
Normal file
44
src/main.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
mod application;
|
||||||
|
mod excellon;
|
||||||
|
mod export;
|
||||||
|
mod geometry;
|
||||||
|
mod gerber;
|
||||||
|
mod outline_geometry;
|
||||||
|
|
||||||
|
use application::Application;
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer};
|
||||||
|
|
||||||
|
const APP_NAME: &str = "Outlinify";
|
||||||
|
|
||||||
|
fn main() -> eframe::Result {
|
||||||
|
let stdout_log = tracing_subscriber::fmt::layer()
|
||||||
|
.pretty()
|
||||||
|
.with_filter(filter::filter_fn(|metadata| {
|
||||||
|
metadata.target().starts_with(env!("CARGO_CRATE_NAME"))
|
||||||
|
}))
|
||||||
|
.with_filter(filter::LevelFilter::DEBUG);
|
||||||
|
|
||||||
|
// Register subscriptions
|
||||||
|
tracing_subscriber::registry().with(stdout_log).init();
|
||||||
|
|
||||||
|
let application = Application::new();
|
||||||
|
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_inner_size([900.0, 700.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
APP_NAME,
|
||||||
|
options,
|
||||||
|
Box::new(|cc| {
|
||||||
|
// This gives us image support:
|
||||||
|
// egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
|
||||||
|
Ok(Box::new(application))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
214
src/outline_geometry/mod.rs
Normal file
214
src/outline_geometry/mod.rs
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
use clipper2::{Bounds, EndType, JoinType, One, Paths};
|
||||||
|
use eframe::{
|
||||||
|
egui::{Rect, Shape, Stroke},
|
||||||
|
epaint::{PathShape, PathStroke},
|
||||||
|
};
|
||||||
|
use egui_plot::{PlotBounds, PlotItem, PlotPoint, Polygon};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::CanvasColour,
|
||||||
|
geometry::{elements::circle::Circle, point::Point, ClipperBounds, ClipperPaths, Unit},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum GeometryType {
|
||||||
|
Paths(ClipperPaths),
|
||||||
|
Points(Vec<Point>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OutlineGeometry {
|
||||||
|
// pub path: ClipperPaths,
|
||||||
|
// pub points: Vec<Point>,
|
||||||
|
items: GeometryType,
|
||||||
|
pub stroke: f32,
|
||||||
|
pub unit: Unit,
|
||||||
|
pub bounds_from: String,
|
||||||
|
pub bounding_box: ClipperBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::Round, EndType::Polygon, 2.0)
|
||||||
|
.simplify(0.01, false);
|
||||||
|
Self {
|
||||||
|
// path: outline,
|
||||||
|
// points: Vec::new(),
|
||||||
|
items: GeometryType::Paths(outline),
|
||||||
|
stroke,
|
||||||
|
unit,
|
||||||
|
bounds_from: bounds_from.into(),
|
||||||
|
bounding_box: bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_no_inflate(
|
||||||
|
outline: &ClipperPaths,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn point_marker(
|
||||||
|
points: Vec<Point>,
|
||||||
|
stroke: f32,
|
||||||
|
unit: Unit,
|
||||||
|
bounds_from: &str,
|
||||||
|
bounds: ClipperBounds,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
// path: Paths::new(vec![]),
|
||||||
|
// points,
|
||||||
|
items: GeometryType::Points(points),
|
||||||
|
stroke,
|
||||||
|
unit,
|
||||||
|
bounds_from: bounds_from.into(),
|
||||||
|
bounding_box: bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paths(&self) -> ClipperPaths {
|
||||||
|
match &self.items {
|
||||||
|
GeometryType::Paths(paths) => paths.clone(),
|
||||||
|
GeometryType::Points(_) => ClipperPaths::new(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn points(&self) -> Vec<Point> {
|
||||||
|
match &self.items {
|
||||||
|
GeometryType::Paths(_) => vec![],
|
||||||
|
GeometryType::Points(p) => p.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// }
|
||||||
|
// }
|
Loading…
Reference in New Issue
Block a user