Compare commits

...

3 Commits
0.1 ... master

Author SHA1 Message Date
Hlars ff45b5ef03 added local fork of gerber-types-rs 2024-08-23 12:02:52 +02:00
Hlars 7913541282 various changes 2024-08-23 10:52:33 +02:00
Larsiiii 63203ee717 comments and inflate change 2024-08-16 19:54:54 +02:00
68 changed files with 5918 additions and 1321 deletions

View File

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

856
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

14
build.rs Normal file
View File

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

3
gerber-types-rs/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
target
Cargo.lock
*.swp

View File

@ -0,0 +1,44 @@
# Changelog
This project follows semantic versioning.
Possible log types:
- `[added]` for new features.
- `[changed]` for changes in existing functionality.
- `[deprecated]` for once-stable features removed in upcoming releases.
- `[removed]` for deprecated features removed in this release.
- `[fixed]` for any bug fixes.
- `[security]` to invite users to upgrade in case of vulnerabilities.
### v0.3.0 (2022-07-05)
- [fixed] Fix whitespace in G04 comment serialization (#33)
- [changed] Updated dependencies
- [changed] A fixed MSRV was dropped
Thanks @NemoAndrea for contributions!
### v0.2.0 (2021-01-06)
This release requires at least Rust 1.31 (2018 edition).
- [added] Implement constructors for `Circle` and `Rectangular`
- [added] Derive Clone for all structs and enums (#16)
- [added] Derive PartialEq and Eq where possible
- [added] Implement `From<FunctionCode>` and `From<ExtendedCode>` for Command
- [added] Impl `From<>` for Command, FunctionCode and ExtendedCode
- [added] New builder-style constructors (#22)
- [added] Support for more FileFunction and FileAttribute variants (#26, #30)
- [changed] Derive Copy for some trivial enums
- [changed] Create new internal `PartialGerberCode` trait (#18)
- [changed] Split up code into more modules
- [changed] Upgraded all dependencies
- [changed] Require Rust 1.31+
Thanks @connorkuehl and @twitchyliquid64 for contributions!
### v0.1.1 (2017-06-10)
- First crates.io release

View File

@ -0,0 +1,24 @@
[package]
name = "gerber-types"
version = "0.3.0"
documentation = "https://docs.rs/gerber-types/"
repository = "https://github.com/dbrgn/gerber-types-rs"
license = "MIT OR Apache-2.0"
authors = ["Danilo Bargen <mail@dbrgn.ch>"]
description = "Types and code generation for Gerber files (RS-274X)."
readme = "README.md"
keywords = ["gerber", "pcb", "rs274x", "cad", "cam"]
include = [
"**/*.rs",
"Cargo.toml",
"README.md",
"LICENSE-*",
]
edition = "2018"
[dependencies]
chrono = "0.4"
conv = "0.3"
num-rational = "0.4"
thiserror = "1"
uuid = "1"

View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,19 @@
Copyright (c) 2016-2021 Danilo Bargen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
gerber-types-rs/README.md Normal file
View File

@ -0,0 +1,47 @@
# Rust Gerber Library
[![Build status][build-status-badge]][build-status]
[![Crates.io][crates-io-badge]][crates-io]
- [Docs (released)](https://docs.rs/gerber-types/)
This crate implements the basic building blocks of Gerber X2 (compatible with
Gerber RS-274X) code. It focusses on the low level types (to be used like an
AST) and code generation and does not do any semantic checking.
For example, you can use an aperture without defining it. This will generate
syntactically valid but semantially invalid Gerber code, but this module won't
complain.
The plan is to write a high-level wrapper library on top of this. Early drafts
[are in progress](https://github.com/dbrgn/gerber-rs) but the design isn't
fixed yet.
Current Gerber X2 spec: https://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf
## Example
You can find an example in the [`examples`
directory](https://github.com/dbrgn/gerber-types-rs/blob/main/examples/polarities-apertures.rs).
It's still quite verbose, the goal is to make the API a bit more ergonomic in
the future. (This library has a low-level focus though, so it will never get a
high-level API. That is the task of other libraries.)
To generate Gerber code for that example:
$ cargo run --example polarities-apertures
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
<!-- Badges -->
[build-status]: https://github.com/dbrgn/gerber-types-rs/actions?query=workflow%3ACI
[build-status-badge]: https://img.shields.io/github/workflow/status/dbrgn/gerber-types-rs/CI/main
[crates-io]: https://crates.io/crates/gerber-types
[crates-io-badge]: https://img.shields.io/crates/v/gerber-types.svg

View File

@ -0,0 +1,24 @@
# Releasing
Set variables:
$ export VERSION=X.Y.Z
$ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA
Update version numbers:
$ vim -p Cargo.toml
Update changelog:
$ vim CHANGELOG.md
Commit & tag:
$ git commit -S${GPG_KEY} -m "Release v${VERSION}"
$ git tag -s -u ${GPG_KEY} v${VERSION} -m "Version ${VERSION}"
Publish:
$ cargo publish
$ git push && git push --tags

View File

@ -0,0 +1,410 @@
//! Attributes.
use std::io::Write;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::errors::GerberResult;
use crate::traits::PartialGerberCode;
// FileAttribute
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileAttribute {
Part(Part),
FileFunction(FileFunction),
FilePolarity(FilePolarity),
GenerationSoftware(GenerationSoftware),
CreationDate(DateTime<Utc>),
ProjectId {
id: String,
guid: Uuid,
revision: String,
},
Md5(String),
UserDefined {
name: String,
value: Vec<String>,
},
}
impl<W: Write> PartialGerberCode<W> for FileAttribute {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
FileAttribute::Part(ref part) => {
write!(writer, "Part,")?;
part.serialize_partial(writer)?;
}
FileAttribute::FileFunction(ref function) => {
write!(writer, "FileFunction,")?;
match function {
FileFunction::Copper {
ref layer,
ref pos,
ref copper_type,
} => {
write!(writer, "Copper,L{},", layer)?;
pos.serialize_partial(writer)?;
if let Some(ref t) = *copper_type {
write!(writer, ",")?;
t.serialize_partial(writer)?;
}
}
FileFunction::Profile(ref plating) => {
write!(writer, "Profile,")?;
plating.serialize_partial(writer)?;
}
FileFunction::Soldermask { ref pos, ref index } => {
write!(writer, "Soldermask,")?;
pos.serialize_partial(writer)?;
if let Some(ref i) = index {
write!(writer, ",{}", *i)?;
}
}
FileFunction::Legend { ref pos, ref index } => {
write!(writer, "Legend,")?;
pos.serialize_partial(writer)?;
if let Some(ref i) = index {
write!(writer, ",{}", *i)?;
}
}
_ => unimplemented!(),
}
}
FileAttribute::GenerationSoftware(ref gs) => {
write!(writer, "GenerationSoftware,")?;
gs.serialize_partial(writer)?;
}
FileAttribute::FilePolarity(ref p) => {
write!(writer, "FilePolarity,")?;
p.serialize_partial(writer)?;
}
FileAttribute::Md5(ref hash) => write!(writer, "MD5,{}", hash)?,
_ => unimplemented!(),
};
Ok(())
}
}
// ApertureAttribute
#[derive(Debug, Clone, PartialEq)]
pub enum ApertureAttribute {
ApertureFunction(ApertureFunction),
DrillTolerance { plus: f64, minus: f64 },
}
// Part
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Part {
/// Single PCB
Single,
/// A.k.a. customer panel, assembly panel, shipping panel, biscuit
Array,
/// A.k.a. working panel, production panel
FabricationPanel,
/// A test coupon
Coupon,
/// None of the above
Other(String),
}
impl<W: Write> PartialGerberCode<W> for Part {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Part::Single => write!(writer, "Single")?,
Part::Array => write!(writer, "Array")?,
Part::FabricationPanel => write!(writer, "FabricationPanel")?,
Part::Coupon => write!(writer, "Coupon")?,
Part::Other(ref description) => write!(writer, "Other,{}", description)?,
};
Ok(())
}
}
// Position
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Position {
Top,
Bottom,
}
impl<W: Write> PartialGerberCode<W> for Position {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Position::Top => write!(writer, "Top")?,
Position::Bottom => write!(writer, "Bot")?,
};
Ok(())
}
}
// ExtendedPosition
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtendedPosition {
Top,
Inner,
Bottom,
}
impl<W: Write> PartialGerberCode<W> for ExtendedPosition {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
ExtendedPosition::Top => write!(writer, "Top")?,
ExtendedPosition::Inner => write!(writer, "Inr")?,
ExtendedPosition::Bottom => write!(writer, "Bot")?,
};
Ok(())
}
}
// CopperType
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CopperType {
Plane,
Signal,
Mixed,
Hatched,
}
impl<W: Write> PartialGerberCode<W> for CopperType {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
CopperType::Plane => write!(writer, "Plane")?,
CopperType::Signal => write!(writer, "Signal")?,
CopperType::Mixed => write!(writer, "Mixed")?,
CopperType::Hatched => write!(writer, "Hatched")?,
};
Ok(())
}
}
// Drill
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Drill {
ThroughHole,
Blind,
Buried,
}
// DrillRouteType
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DrillRouteType {
Drill,
Route,
Mixed,
}
// Profile
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Profile {
Plated,
NonPlated,
}
impl<W: Write> PartialGerberCode<W> for Profile {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Profile::Plated => write!(writer, "P")?,
Profile::NonPlated => write!(writer, "NP")?,
};
Ok(())
}
}
// FileFunction
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileFunction {
Copper {
layer: i32,
pos: ExtendedPosition,
copper_type: Option<CopperType>,
},
Soldermask {
pos: Position,
index: Option<i32>,
},
Legend {
pos: Position,
index: Option<i32>,
},
Goldmask {
pos: Position,
index: Option<i32>,
},
Silvermask {
pos: Position,
index: Option<i32>,
},
Tinmask {
pos: Position,
index: Option<i32>,
},
Carbonmask {
pos: Position,
index: Option<i32>,
},
Peelablesoldermask {
pos: Position,
index: Option<i32>,
},
Glue {
pos: Position,
index: Option<i32>,
},
Viatenting(Position),
Viafill,
Heatsink(Position),
Paste(Position),
KeepOut(Position),
Pads(Position),
Scoring(Position),
Plated {
from_layer: i32,
to_layer: i32,
drill: Drill,
label: Option<DrillRouteType>,
},
NonPlated {
from_layer: i32,
to_layer: i32,
drill: Drill,
label: Option<DrillRouteType>,
},
Profile(Profile),
Drillmap,
FabricationDrawing,
ArrayDrawing,
AssemblyDrawing(Position),
Drawing(String),
Other(String),
}
// FilePolarity
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilePolarity {
Positive,
Negative,
}
impl<W: Write> PartialGerberCode<W> for FilePolarity {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
FilePolarity::Positive => write!(writer, "Positive")?,
FilePolarity::Negative => write!(writer, "Negative")?,
};
Ok(())
}
}
// GenerationSoftware
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationSoftware {
pub vendor: String,
pub application: String,
pub version: Option<String>,
}
impl GenerationSoftware {
pub fn new<S: Into<String>>(vendor: S, application: S, version: Option<S>) -> Self {
GenerationSoftware {
vendor: vendor.into(),
application: application.into(),
version: version.map(|s| s.into()),
}
}
}
impl<W: Write> PartialGerberCode<W> for GenerationSoftware {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match self.version {
Some(ref v) => write!(writer, "{},{},{}", self.vendor, self.application, v)?,
None => write!(writer, "{},{}", self.vendor, self.application)?,
};
Ok(())
}
}
// ApertureFunction
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApertureFunction {
// Only valid for layers with file function plated or non-plated
ViaDrill,
BackDrill,
ComponentDrill {
press_fit: Option<bool>, // TODO is this bool?
},
CastellatedDrill,
MechanicalDrill {
function: Option<DrillFunction>,
},
Slot,
CutOut,
Cavity,
OtherDrill(String),
// Only valid for layers with file function copper
ComponentPad {
press_fit: Option<bool>, // TODO is this bool?
},
SmdPad(SmdPadType),
BgaPad(SmdPadType),
ConnectorPad,
HeatsinkPad,
ViaPad,
TestPad,
CastellatedPad,
FiducialPad(FiducialScope),
ThermalReliefPad,
WasherPad,
AntiPad,
OtherPad(String),
Conductor,
NonConductor,
CopperBalancing,
Border,
OtherCopper(String),
// All layers
Profile,
NonMaterial,
Material,
Other(String),
}
// DrillFunction
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DrillFunction {
BreakOut,
Tooling,
Other,
}
// SmdPadType
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SmdPadType {
CopperDefined,
SoldermaskDefined,
}
// FiducialScope
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FiducialScope {
Global,
Local,
}

View File

@ -0,0 +1,106 @@
//! Generic code generation, e.g. implementations of `PartialGerberCode` for
//! bool or Vec<G: GerberCode>.
use std::io::Write;
use crate::errors::GerberResult;
use crate::traits::{GerberCode, PartialGerberCode};
use crate::types::*;
/// Implement `PartialGerberCode` for booleans
impl<W: Write> PartialGerberCode<W> for bool {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
if *self {
write!(writer, "1")?;
} else {
write!(writer, "0")?;
};
Ok(())
}
}
/// Implement `GerberCode` for Vectors of types that are `GerberCode`.
impl<W: Write, G: GerberCode<W>> GerberCode<W> for Vec<G> {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
for item in self.iter() {
item.serialize(writer)?;
}
Ok(())
}
}
/// Implement `PartialGerberCode` for `Option<T: PartialGerberCode>`
impl<T: PartialGerberCode<W>, W: Write> PartialGerberCode<W> for Option<T> {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
if let Some(ref val) = *self {
val.serialize_partial(writer)?;
}
Ok(())
}
}
impl<W: Write> GerberCode<W> for Command {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Command::FunctionCode(ref code) => code.serialize(writer)?,
Command::ExtendedCode(ref code) => code.serialize(writer)?,
};
Ok(())
}
}
impl<W: Write> GerberCode<W> for FunctionCode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
FunctionCode::DCode(ref code) => code.serialize(writer)?,
FunctionCode::GCode(ref code) => code.serialize(writer)?,
FunctionCode::MCode(ref code) => code.serialize(writer)?,
};
Ok(())
}
}
impl<W: Write> GerberCode<W> for ExtendedCode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
ExtendedCode::CoordinateFormat(ref cf) => {
writeln!(writer, "%FSLAX{0}{1}Y{0}{1}*%", cf.integer, cf.decimal)?;
}
ExtendedCode::Unit(ref unit) => {
write!(writer, "%MO")?;
unit.serialize_partial(writer)?;
writeln!(writer, "*%")?;
}
ExtendedCode::ApertureDefinition(ref def) => {
write!(writer, "%ADD")?;
def.serialize_partial(writer)?;
writeln!(writer, "*%")?;
}
ExtendedCode::ApertureMacro(ref am) => {
write!(writer, "%")?;
am.serialize_partial(writer)?;
writeln!(writer, "%")?;
}
ExtendedCode::LoadPolarity(ref polarity) => {
write!(writer, "%LP")?;
polarity.serialize_partial(writer)?;
writeln!(writer, "*%")?;
}
ExtendedCode::StepAndRepeat(ref sar) => {
write!(writer, "%SR")?;
sar.serialize_partial(writer)?;
writeln!(writer, "*%")?;
}
ExtendedCode::FileAttribute(ref attr) => {
write!(writer, "%TF.")?;
attr.serialize_partial(writer)?;
writeln!(writer, "*%")?;
}
ExtendedCode::DeleteAttribute(ref attr) => {
writeln!(writer, "%TD{}*%", attr)?;
}
_ => unimplemented!(),
};
Ok(())
}
}

View File

@ -0,0 +1,479 @@
//! Types for Gerber code generation related to coordinates.
use std::convert::{From, Into};
use std::i64;
use std::io::Write;
use std::num::FpCategory;
use conv::TryFrom;
use num_rational::Ratio;
use crate::errors::{GerberError, GerberResult};
use crate::traits::PartialGerberCode;
// Helper macros
/// Automatically implement `PartialGerberCode` trait for struct types
/// that are based on `x` and `y` attributes.
macro_rules! impl_xy_partial_gerbercode {
($class:ty, $x:expr, $y: expr) => {
impl<W: Write> PartialGerberCode<W> for $class {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
if let Some(x) = self.x {
write!(writer, "{}{}", $x, x.gerber(&self.format)?)?;
}
if let Some(y) = self.y {
write!(writer, "{}{}", $y, y.gerber(&self.format)?)?;
}
Ok(())
}
}
};
}
// Types
/// The coordinate format specifies the number of integer and decimal places in
/// a coordinate number. For example, the `24` format specifies 2 integer and 4
/// decimal places. The number of decimal places must be 4, 5 or 6. The number
/// of integer places must be not more than 6. Thus the longest representable
/// coordinate number is `nnnnnn.nnnnnn`.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct CoordinateFormat {
pub integer: u8,
pub decimal: u8,
}
impl CoordinateFormat {
pub fn new(integer: u8, decimal: u8) -> Self {
CoordinateFormat { integer, decimal }
}
}
/// Coordinate numbers are integers conforming to the rules set by the FS
/// command.
///
/// Coordinate numbers are integers. Explicit decimal points are not allowed.
///
/// A coordinate number must have at least one character. Zero therefore must
/// be encoded as `0`.
///
/// The value is stored as a 64 bit integer with 6 decimal places.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct CoordinateNumber {
nano: i64,
}
impl CoordinateNumber {
pub fn new(nano: i64) -> Self {
CoordinateNumber { nano }
}
}
const DECIMAL_PLACES_CHARS: u8 = 6;
const DECIMAL_PLACES_FACTOR: i64 = 1_000_000;
impl TryFrom<f64> for CoordinateNumber {
type Err = GerberError;
fn try_from(val: f64) -> Result<Self, Self::Err> {
match val.classify() {
FpCategory::Nan => Err(GerberError::ConversionError("Value is NaN".into())),
FpCategory::Infinite => Err(GerberError::ConversionError("Value is infinite".into())),
FpCategory::Zero | FpCategory::Subnormal => Ok(CoordinateNumber { nano: 0 }),
FpCategory::Normal => {
let multiplied = val * DECIMAL_PLACES_FACTOR as f64;
if (multiplied > i64::MAX as f64) || (multiplied < i64::MIN as f64) {
Err(GerberError::ConversionError(
"Value is out of bounds".into(),
))
} else {
Ok(CoordinateNumber {
nano: multiplied as i64,
})
}
}
}
}
}
impl Into<f64> for CoordinateNumber {
fn into(self) -> f64 {
(self.nano as f64) / DECIMAL_PLACES_FACTOR as f64
}
}
macro_rules! impl_from_integer {
($class:ty) => {
impl From<$class> for CoordinateNumber {
fn from(val: $class) -> Self {
CoordinateNumber {
nano: val as i64 * DECIMAL_PLACES_FACTOR,
}
}
}
};
}
// These are the types we can safely multiply with DECIMAL_PLACES_FACTOR
// without the risk of an overflow.
impl_from_integer!(i8);
impl_from_integer!(i16);
impl_from_integer!(i32);
impl_from_integer!(u8);
impl_from_integer!(u16);
impl CoordinateNumber {
pub fn gerber(&self, format: &CoordinateFormat) -> Result<String, GerberError> {
if format.decimal > DECIMAL_PLACES_CHARS {
return Err(GerberError::CoordinateFormatError(
"Invalid precision: Too high!".into(),
));
}
if self.nano.abs() >= 10_i64.pow((format.integer + DECIMAL_PLACES_CHARS) as u32) {
return Err(GerberError::CoordinateFormatError(
"Number is too large for chosen format!".into(),
));
}
let divisor: i64 = 10_i64.pow((DECIMAL_PLACES_CHARS - format.decimal) as u32);
let number: i64 = Ratio::new(self.nano, divisor).round().to_integer();
Ok(number.to_string())
}
}
/// Coordinates are part of an operation.
///
/// Coordinates are modal. If an X is omitted, the X coordinate of the
/// current point is used. Similar for Y.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Coordinates {
pub x: Option<CoordinateNumber>,
pub y: Option<CoordinateNumber>,
pub format: CoordinateFormat,
}
impl Coordinates {
pub fn new<T, U>(x: T, y: U, format: CoordinateFormat) -> Self
where
T: Into<CoordinateNumber>,
U: Into<CoordinateNumber>,
{
Coordinates {
x: Some(x.into()),
y: Some(y.into()),
format,
}
}
pub fn at_x<T>(x: T, format: CoordinateFormat) -> Self
where
T: Into<CoordinateNumber>,
{
Coordinates {
x: Some(x.into()),
y: None,
format,
}
}
pub fn at_y<T>(y: T, format: CoordinateFormat) -> Self
where
T: Into<CoordinateNumber>,
{
Coordinates {
x: None,
y: Some(y.into()),
format,
}
}
}
impl_xy_partial_gerbercode!(Coordinates, "X", "Y");
/// Coordinate offsets can be used for interpolate operations in circular
/// interpolation mode.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoordinateOffset {
pub x: Option<CoordinateNumber>,
pub y: Option<CoordinateNumber>,
pub format: CoordinateFormat,
}
impl CoordinateOffset {
pub fn new<T, U>(x: T, y: U, format: CoordinateFormat) -> Self
where
T: Into<CoordinateNumber>,
U: Into<CoordinateNumber>,
{
CoordinateOffset {
x: Some(x.into()),
y: Some(y.into()),
format,
}
}
pub fn at_x<T>(x: T, format: CoordinateFormat) -> Self
where
T: Into<CoordinateNumber>,
{
CoordinateOffset {
x: Some(x.into()),
y: None,
format,
}
}
pub fn at_y<T>(y: T, format: CoordinateFormat) -> Self
where
T: Into<CoordinateNumber>,
{
CoordinateOffset {
x: None,
y: Some(y.into()),
format,
}
}
}
impl_xy_partial_gerbercode!(CoordinateOffset, "I", "J");
#[cfg(test)]
mod test {
use super::*;
use std::f64;
use std::io::BufWriter;
use conv::TryFrom;
use crate::traits::PartialGerberCode;
#[test]
/// Test integer to coordinate number conversion
fn test_from_i8() {
let a = CoordinateNumber { nano: 13000000 };
let b = CoordinateNumber::from(13i8);
assert_eq!(a, b);
let c = CoordinateNumber { nano: -99000000 };
let d = CoordinateNumber::from(-99i8);
assert_eq!(c, d);
}
#[test]
/// Test integer to coordinate number conversion
fn test_from_i32() {
let a = CoordinateNumber { nano: 13000000 };
let b = CoordinateNumber::from(13);
assert_eq!(a, b);
let c = CoordinateNumber { nano: -998000000 };
let d = CoordinateNumber::from(-998);
assert_eq!(c, d);
}
#[test]
/// Test float to coordinate number conversion
fn test_try_from_f64_success() {
let a = CoordinateNumber { nano: 1375000i64 };
let b = CoordinateNumber::try_from(1.375f64).unwrap();
assert_eq!(a, b);
let c = CoordinateNumber {
nano: 123456888888i64,
};
let d = CoordinateNumber::try_from(123456.888888f64).unwrap();
assert_eq!(c, d);
let e = CoordinateNumber { nano: 0i64 };
let f = CoordinateNumber::try_from(0f64).unwrap();
assert_eq!(e, f);
let g = CoordinateNumber { nano: -12345678 };
let h = CoordinateNumber::try_from(-12.345678).unwrap();
assert_eq!(g, h);
}
#[test]
/// Test failing float to coordinate number conversion
fn test_try_from_f64_fail() {
let cn1 = CoordinateNumber::try_from(f64::NAN);
assert!(cn1.is_err());
let cn2 = CoordinateNumber::try_from(f64::INFINITY);
assert!(cn2.is_err());
let cn3 = CoordinateNumber::try_from(f64::MAX - 1.0);
assert!(cn3.is_err());
let cn4 = CoordinateNumber::try_from(f64::MIN + 1.0);
assert!(cn4.is_err());
}
#[test]
/// Test coordinate number to float conversion
fn test_into_f64() {
let a: f64 = CoordinateNumber { nano: 1375000i64 }.into();
let b = 1.375f64;
assert_eq!(a, b);
let c: f64 = CoordinateNumber {
nano: 123456888888i64,
}
.into();
let d = 123456.888888f64;
assert_eq!(c, d);
let e: f64 = CoordinateNumber { nano: 0i64 }.into();
let f = 0f64;
assert_eq!(e, f);
}
#[test]
/// Test the coordinate number constructor creates correct
/// coordinate numbers.
fn test_coordinate_number_new() {
let nano = 5;
let cn1 = CoordinateNumber::new(nano);
assert_eq!(cn1.nano, nano);
}
#[test]
/// Test coordinate number to string conversion when it's 0
fn test_formatted_zero() {
let cf1 = CoordinateFormat::new(6, 6);
let cf2 = CoordinateFormat::new(2, 4);
let a = CoordinateNumber { nano: 0 }.gerber(&cf1).unwrap();
let b = CoordinateNumber { nano: 0 }.gerber(&cf2).unwrap();
assert_eq!(a, "0".to_string());
assert_eq!(b, "0".to_string());
}
#[test]
/// Test coordinate number to string conversion when the decimal part is 0
fn test_formatted_decimal_zero() {
let cf1 = CoordinateFormat::new(6, 6);
let cf2 = CoordinateFormat::new(2, 4);
let a = CoordinateNumber { nano: 10000000 }.gerber(&cf1).unwrap();
let b = CoordinateNumber { nano: 20000000 }.gerber(&cf2).unwrap();
assert_eq!(a, "10000000".to_string());
assert_eq!(b, "200000".to_string());
}
#[test]
/// Test coordinate number to string conversion
fn test_formatted_65() {
let cf = CoordinateFormat::new(6, 5);
let d = CoordinateNumber { nano: 123456789012 }.gerber(&cf).unwrap();
assert_eq!(d, "12345678901".to_string());
}
#[test]
/// Test coordinate number to string conversion
fn test_formatted_54() {
let cf = CoordinateFormat::new(5, 4);
let d = CoordinateNumber { nano: 12345678901 }.gerber(&cf).unwrap();
assert_eq!(d, "123456789".to_string());
}
#[test]
/// Test coordinate number to string conversion failure
fn test_formatted_number_too_large() {
let cf = CoordinateFormat::new(4, 5);
let d = CoordinateNumber { nano: 12345000000 }.gerber(&cf);
assert!(d.is_err());
}
#[test]
/// Test coordinate number to string conversion failure
fn test_formatted_negative_number_too_large() {
let cf = CoordinateFormat::new(4, 5);
let d = CoordinateNumber { nano: -12345000000 }.gerber(&cf);
assert!(d.is_err());
}
#[test]
/// Test coordinate number to string conversion (rounding of decimal part)
fn test_formatted_44_rounding() {
let cf = CoordinateFormat::new(4, 4);
let d = CoordinateNumber { nano: 1234432199 }.gerber(&cf).unwrap();
assert_eq!(d, "12344322".to_string());
}
#[test]
/// Test negative coordinate number to string conversion
fn test_formatted_negative_rounding() {
let cf = CoordinateFormat::new(6, 4);
let d = CoordinateNumber {
nano: -123456789099,
}
.gerber(&cf)
.unwrap();
assert_eq!(d, "-1234567891".to_string());
}
#[test]
fn test_coordinates_into() {
let cf = CoordinateFormat::new(2, 4);
let c1 = Coordinates::new(CoordinateNumber::from(1), CoordinateNumber::from(2), cf);
let c2 = Coordinates::new(1, 2, cf);
assert_eq!(c1, c2);
}
#[test]
fn test_coordinates_into_mixed() {
let cf = CoordinateFormat::new(2, 4);
let c1 = Coordinates::new(CoordinateNumber::from(1), 2, cf);
let c2 = Coordinates::new(1, 2, cf);
assert_eq!(c1, c2);
}
#[test]
fn test_coordinates() {
macro_rules! assert_coords {
($coords:expr, $result:expr) => {{
assert_partial_code!($coords, $result);
}};
}
let cf44 = CoordinateFormat::new(4, 4);
let cf46 = CoordinateFormat::new(4, 6);
assert_coords!(Coordinates::new(10, 20, cf44), "X100000Y200000");
assert_coords!(
Coordinates {
x: None,
y: None,
format: cf44
},
""
); // TODO should we catch this?
assert_coords!(Coordinates::at_x(10, cf44), "X100000");
assert_coords!(Coordinates::at_y(20, cf46), "Y20000000");
assert_coords!(Coordinates::new(0, -400, cf44), "X0Y-4000000");
}
#[test]
fn test_offset() {
macro_rules! assert_coords {
($coords:expr, $result:expr) => {{
assert_partial_code!($coords, $result);
}};
}
let cf44 = CoordinateFormat::new(4, 4);
let cf55 = CoordinateFormat::new(5, 5);
let cf66 = CoordinateFormat::new(6, 6);
assert_coords!(CoordinateOffset::new(10, 20, cf44), "I100000J200000");
assert_coords!(
CoordinateOffset {
x: None,
y: None,
format: cf44
},
""
); // TODO should we catch this?
assert_coords!(CoordinateOffset::at_x(10, cf66), "I10000000");
assert_coords!(CoordinateOffset::at_y(20, cf55), "J2000000");
assert_coords!(CoordinateOffset::new(0, -400, cf44), "I0J-4000000");
}
}

View File

@ -0,0 +1,39 @@
//! Error types used in the gerber-types library.
use std::io::Error as IoError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GerberError {
#[error("Conversion between two types failed: {0}")]
ConversionError(String),
#[error("Bad coordinate format: {0}")]
CoordinateFormatError(String),
#[error("A value is out of range: {0}")]
RangeError(String),
#[error("Required data is missing: {0}")]
MissingDataError(String),
#[error("I/O error during code generation")]
IoError(#[from] IoError),
}
pub type GerberResult<T> = Result<T, GerberError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_msg() {
let err = GerberError::CoordinateFormatError("Something went wrong".into());
assert_eq!(
err.to_string(),
"Bad coordinate format: Something went wrong"
);
}
}

View File

@ -0,0 +1,315 @@
//! Extended code types.
use std::io::Write;
use crate::errors::GerberResult;
use crate::traits::PartialGerberCode;
// Unit
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
Inches,
Millimeters,
}
impl<W: Write> PartialGerberCode<W> for Unit {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Unit::Millimeters => write!(writer, "MM")?,
Unit::Inches => write!(writer, "IN")?,
};
Ok(())
}
}
// ApertureDefinition
#[derive(Debug, Clone, PartialEq)]
pub struct ApertureDefinition {
pub code: i32,
pub aperture: Aperture,
}
impl ApertureDefinition {
pub fn new(code: i32, aperture: Aperture) -> Self {
ApertureDefinition { code, aperture }
}
}
impl<W: Write> PartialGerberCode<W> for ApertureDefinition {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
write!(writer, "{}", self.code)?;
self.aperture.serialize_partial(writer)?;
Ok(())
}
}
// Aperture
#[derive(Debug, Clone, PartialEq)]
pub enum Aperture {
Circle(Circle),
Rectangle(Rectangular),
Obround(Rectangular),
Polygon(Polygon),
Other(String),
}
impl<W: Write> PartialGerberCode<W> for Aperture {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Aperture::Circle(ref circle) => {
write!(writer, "C,")?;
circle.serialize_partial(writer)?;
}
Aperture::Rectangle(ref rectangular) => {
write!(writer, "R,")?;
rectangular.serialize_partial(writer)?;
}
Aperture::Obround(ref rectangular) => {
write!(writer, "O,")?;
rectangular.serialize_partial(writer)?;
}
Aperture::Polygon(ref polygon) => {
write!(writer, "P,")?;
polygon.serialize_partial(writer)?;
}
Aperture::Other(ref string) => write!(writer, "{}", string)?,
};
Ok(())
}
}
// Circle
#[derive(Debug, Clone, PartialEq)]
pub struct Circle {
pub diameter: f64,
pub hole_diameter: Option<f64>,
}
impl Circle {
pub fn new(diameter: f64) -> Self {
Circle {
diameter,
hole_diameter: None,
}
}
pub fn with_hole(diameter: f64, hole_diameter: f64) -> Self {
Circle {
diameter,
hole_diameter: Some(hole_diameter),
}
}
}
impl<W: Write> PartialGerberCode<W> for Circle {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match self.hole_diameter {
Some(hole_diameter) => {
write!(writer, "{}X{}", self.diameter, hole_diameter)?;
}
None => write!(writer, "{}", self.diameter)?,
};
Ok(())
}
}
// Rectangular
#[derive(Debug, Clone, PartialEq)]
pub struct Rectangular {
pub x: f64,
pub y: f64,
pub hole_diameter: Option<f64>,
}
impl Rectangular {
pub fn new(x: f64, y: f64) -> Self {
Rectangular {
x,
y,
hole_diameter: None,
}
}
pub fn with_hole(x: f64, y: f64, hole_diameter: f64) -> Self {
Rectangular {
x,
y,
hole_diameter: Some(hole_diameter),
}
}
}
impl<W: Write> PartialGerberCode<W> for Rectangular {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match self.hole_diameter {
Some(hole_diameter) => write!(writer, "{}X{}X{}", self.x, self.y, hole_diameter)?,
None => write!(writer, "{}X{}", self.x, self.y)?,
};
Ok(())
}
}
// Polygon
#[derive(Debug, Clone, PartialEq)]
pub struct Polygon {
pub diameter: f64,
pub vertices: u8, // 3--12
pub rotation: Option<f64>,
pub hole_diameter: Option<f64>,
}
impl Polygon {
pub fn new(diameter: f64, vertices: u8) -> Self {
Polygon {
diameter,
vertices,
rotation: None,
hole_diameter: None,
}
}
pub fn with_rotation(mut self, angle: f64) -> Self {
self.rotation = Some(angle);
self
}
pub fn with_diameter(mut self, diameter: f64) -> Self {
self.diameter = diameter;
self
}
}
impl<W: Write> PartialGerberCode<W> for Polygon {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match (self.rotation, self.hole_diameter) {
(Some(rot), Some(hd)) => {
write!(writer, "{}X{}X{}X{}", self.diameter, self.vertices, rot, hd)?
}
(Some(rot), None) => write!(writer, "{}X{}X{}", self.diameter, self.vertices, rot)?,
(None, Some(hd)) => write!(writer, "{}X{}X0X{}", self.diameter, self.vertices, hd)?,
(None, None) => write!(writer, "{}X{}", self.diameter, self.vertices)?,
};
Ok(())
}
}
// Polarity
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Polarity {
Clear,
Dark,
}
impl<W: Write> PartialGerberCode<W> for Polarity {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Polarity::Clear => write!(writer, "C")?,
Polarity::Dark => write!(writer, "D")?,
};
Ok(())
}
}
// StepAndRepeat
#[derive(Debug, Clone, PartialEq)]
pub enum StepAndRepeat {
Open {
repeat_x: u32,
repeat_y: u32,
distance_x: f64,
distance_y: f64,
},
Close,
}
impl<W: Write> PartialGerberCode<W> for StepAndRepeat {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
match *self {
StepAndRepeat::Open {
repeat_x: rx,
repeat_y: ry,
distance_x: dx,
distance_y: dy,
} => write!(writer, "X{}Y{}I{}J{}", rx, ry, dx, dy)?,
StepAndRepeat::Close => {}
};
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_aperture_definition_new() {
let ad1 = ApertureDefinition::new(10, Aperture::Circle(Circle::new(3.0)));
let ad2 = ApertureDefinition {
code: 10,
aperture: Aperture::Circle(Circle::new(3.0)),
};
assert_eq!(ad1, ad2);
}
#[test]
fn test_rectangular_new() {
let r1 = Rectangular::new(2.0, 3.0);
let r2 = Rectangular {
x: 2.0,
y: 3.0,
hole_diameter: None,
};
assert_eq!(r1, r2);
}
#[test]
fn test_rectangular_with_hole() {
let r1 = Rectangular::with_hole(3.0, 2.0, 1.0);
let r2 = Rectangular {
x: 3.0,
y: 2.0,
hole_diameter: Some(1.0),
};
assert_eq!(r1, r2);
}
#[test]
fn test_circle_new() {
let c1 = Circle::new(3.0);
let c2 = Circle {
diameter: 3.0,
hole_diameter: None,
};
assert_eq!(c1, c2);
}
#[test]
fn test_circle_with_hole() {
let c1 = Circle::with_hole(3.0, 1.0);
let c2 = Circle {
diameter: 3.0,
hole_diameter: Some(1.0),
};
assert_eq!(c1, c2);
}
#[test]
fn test_polygon_new() {
let p1 = Polygon::new(3.0, 4).with_rotation(45.0);
let p2 = Polygon {
diameter: 3.0,
vertices: 4,
rotation: Some(45.0),
hole_diameter: None,
};
assert_eq!(p1, p2);
}
}

View File

@ -0,0 +1,143 @@
//! Function code types.
use std::io::Write;
use crate::coordinates::{CoordinateOffset, Coordinates};
use crate::errors::GerberResult;
use crate::traits::{GerberCode, PartialGerberCode};
// DCode
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DCode {
Operation(Operation),
SelectAperture(i32),
}
impl<W: Write> GerberCode<W> for DCode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
DCode::Operation(ref operation) => operation.serialize(writer)?,
DCode::SelectAperture(code) => writeln!(writer, "D{}*", code)?,
};
Ok(())
}
}
// GCode
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GCode {
InterpolationMode(InterpolationMode),
RegionMode(bool),
QuadrantMode(QuadrantMode),
Comment(String),
}
impl<W: Write> GerberCode<W> for GCode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
GCode::InterpolationMode(ref mode) => mode.serialize(writer)?,
GCode::RegionMode(enabled) => {
if enabled {
writeln!(writer, "G36*")?;
} else {
writeln!(writer, "G37*")?;
}
}
GCode::QuadrantMode(ref mode) => mode.serialize(writer)?,
GCode::Comment(ref comment) => writeln!(writer, "G04 {}*", comment)?,
};
Ok(())
}
}
// MCode
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MCode {
EndOfFile,
}
impl<W: Write> GerberCode<W> for MCode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
MCode::EndOfFile => writeln!(writer, "M02*")?,
};
Ok(())
}
}
// Operation
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operation {
/// D01 Command
Interpolate(Coordinates, Option<CoordinateOffset>),
/// D02 Command
Move(Coordinates),
/// D03 Command
Flash(Coordinates),
}
impl<W: Write> GerberCode<W> for Operation {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
Operation::Interpolate(ref coords, ref offset) => {
coords.serialize_partial(writer)?;
offset.serialize_partial(writer)?;
writeln!(writer, "D01*")?;
}
Operation::Move(ref coords) => {
coords.serialize_partial(writer)?;
writeln!(writer, "D02*")?;
}
Operation::Flash(ref coords) => {
coords.serialize_partial(writer)?;
writeln!(writer, "D03*")?;
}
};
Ok(())
}
}
// InterpolationMode
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationMode {
Linear,
ClockwiseCircular,
CounterclockwiseCircular,
}
impl<W: Write> GerberCode<W> for InterpolationMode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
InterpolationMode::Linear => writeln!(writer, "G01*")?,
InterpolationMode::ClockwiseCircular => writeln!(writer, "G02*")?,
InterpolationMode::CounterclockwiseCircular => writeln!(writer, "G03*")?,
};
Ok(())
}
}
// QuadrantMode
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuadrantMode {
Single,
Multi,
}
impl<W: Write> GerberCode<W> for QuadrantMode {
fn serialize(&self, writer: &mut W) -> GerberResult<()> {
match *self {
QuadrantMode::Single => writeln!(writer, "G74*")?,
QuadrantMode::Multi => writeln!(writer, "G75*")?,
};
Ok(())
}
}
#[cfg(test)]
mod test {}

289
gerber-types-rs/src/lib.rs Normal file
View File

@ -0,0 +1,289 @@
//! # Gerber commands
//!
//! This crate implements the basic building blocks of Gerber (RS-274X, aka
//! Extended Gerber version 2) code. It focusses on the low level types and does
//! not do any semantic checking.
//!
//! For example, you can use an aperture without defining it. This will
//! generate syntactically valid but semantially invalid Gerber code, but this
//! module won't complain.
//!
//! ## Traits: GerberCode and PartialGerberCode
//!
//! There are two main traits that are used for code generation:
//!
//! - [`GerberCode`](trait.GerberCode.html) generates a full Gerber code line,
//! terminated with a newline character.
//! - `PartialGerberCode` (internal only) generates Gerber representation of a
//! value, but does not represent a full line of code.
#![allow(clippy::new_without_default)]
#[cfg(test)]
#[macro_use]
mod test_macros;
mod attributes;
mod codegen;
mod coordinates;
mod errors;
mod extended_codes;
mod function_codes;
mod macros;
mod traits;
mod types;
pub use crate::attributes::*;
#[allow(unused)]
pub use crate::codegen::*;
pub use crate::coordinates::*;
pub use crate::errors::*;
pub use crate::extended_codes::*;
pub use crate::function_codes::*;
pub use crate::macros::*;
pub use crate::traits::GerberCode;
pub use crate::types::*;
#[cfg(test)]
mod test {
use std::io::BufWriter;
use super::traits::PartialGerberCode;
use super::*;
#[test]
fn test_serialize() {
//! The serialize method of the GerberCode trait should generate strings.
let comment = GCode::Comment("testcomment".to_string());
assert_code!(comment, "G04 testcomment*\n");
}
#[test]
fn test_vec_serialize() {
//! A `Vec<T: GerberCode>` should also implement `GerberCode`.
let mut v = Vec::new();
v.push(GCode::Comment("comment 1".to_string()));
v.push(GCode::Comment("another one".to_string()));
assert_code!(v, "G04 comment 1*\nG04 another one*\n");
}
#[test]
fn test_command_serialize() {
//! A `Command` should implement `GerberCode`
let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment("comment".to_string())));
assert_code!(c, "G04 comment*\n");
}
#[test]
fn test_interpolation_mode() {
let mut commands = Vec::new();
let c1 = GCode::InterpolationMode(InterpolationMode::Linear);
let c2 = GCode::InterpolationMode(InterpolationMode::ClockwiseCircular);
let c3 = GCode::InterpolationMode(InterpolationMode::CounterclockwiseCircular);
commands.push(c1);
commands.push(c2);
commands.push(c3);
assert_code!(commands, "G01*\nG02*\nG03*\n");
}
#[test]
fn test_region_mode() {
let mut commands = Vec::new();
commands.push(GCode::RegionMode(true));
commands.push(GCode::RegionMode(false));
assert_code!(commands, "G36*\nG37*\n");
}
#[test]
fn test_quadrant_mode() {
let mut commands = Vec::new();
commands.push(GCode::QuadrantMode(QuadrantMode::Single));
commands.push(GCode::QuadrantMode(QuadrantMode::Multi));
assert_code!(commands, "G74*\nG75*\n");
}
#[test]
fn test_end_of_file() {
let c = MCode::EndOfFile;
assert_code!(c, "M02*\n");
}
#[test]
fn test_operation_interpolate() {
let cf = CoordinateFormat::new(2, 5);
let c1 = Operation::Interpolate(
Coordinates::new(1, 2, cf),
Some(CoordinateOffset::new(5, 10, cf)),
);
assert_code!(c1, "X100000Y200000I500000J1000000D01*\n");
let c2 = Operation::Interpolate(Coordinates::at_y(-2, CoordinateFormat::new(4, 4)), None);
assert_code!(c2, "Y-20000D01*\n");
let cf = CoordinateFormat::new(4, 4);
let c3 = Operation::Interpolate(
Coordinates::at_x(1, cf),
Some(CoordinateOffset::at_y(2, cf)),
);
assert_code!(c3, "X10000J20000D01*\n");
}
#[test]
fn test_operation_move() {
let c = Operation::Move(Coordinates::new(23, 42, CoordinateFormat::new(6, 4)));
assert_code!(c, "X230000Y420000D02*\n");
}
#[test]
fn test_operation_flash() {
let c = Operation::Flash(Coordinates::new(23, 42, CoordinateFormat::new(4, 4)));
assert_code!(c, "X230000Y420000D03*\n");
}
#[test]
fn test_select_aperture() {
let c1 = DCode::SelectAperture(10);
assert_code!(c1, "D10*\n");
let c2 = DCode::SelectAperture(2147483647);
assert_code!(c2, "D2147483647*\n");
}
#[test]
fn test_coordinate_format() {
let c = ExtendedCode::CoordinateFormat(CoordinateFormat::new(2, 5));
assert_code!(c, "%FSLAX25Y25*%\n");
}
#[test]
fn test_unit() {
let c1 = ExtendedCode::Unit(Unit::Millimeters);
let c2 = ExtendedCode::Unit(Unit::Inches);
assert_code!(c1, "%MOMM*%\n");
assert_code!(c2, "%MOIN*%\n");
}
#[test]
fn test_aperture_circle_definition() {
let ad1 = ApertureDefinition {
code: 10,
aperture: Aperture::Circle(Circle {
diameter: 4.0,
hole_diameter: Some(2.0),
}),
};
let ad2 = ApertureDefinition {
code: 11,
aperture: Aperture::Circle(Circle {
diameter: 4.5,
hole_diameter: None,
}),
};
assert_partial_code!(ad1, "10C,4X2");
assert_partial_code!(ad2, "11C,4.5");
}
#[test]
fn test_aperture_rectangular_definition() {
let ad1 = ApertureDefinition {
code: 12,
aperture: Aperture::Rectangle(Rectangular {
x: 1.5,
y: 2.25,
hole_diameter: Some(3.8),
}),
};
let ad2 = ApertureDefinition {
code: 13,
aperture: Aperture::Rectangle(Rectangular {
x: 1.0,
y: 1.0,
hole_diameter: None,
}),
};
let ad3 = ApertureDefinition {
code: 14,
aperture: Aperture::Obround(Rectangular {
x: 2.0,
y: 4.5,
hole_diameter: None,
}),
};
assert_partial_code!(ad1, "12R,1.5X2.25X3.8");
assert_partial_code!(ad2, "13R,1X1");
assert_partial_code!(ad3, "14O,2X4.5");
}
#[test]
fn test_aperture_polygon_definition() {
let ad1 = ApertureDefinition {
code: 15,
aperture: Aperture::Polygon(Polygon {
diameter: 4.5,
vertices: 3,
rotation: None,
hole_diameter: None,
}),
};
let ad2 = ApertureDefinition {
code: 16,
aperture: Aperture::Polygon(Polygon {
diameter: 5.0,
vertices: 4,
rotation: Some(30.6),
hole_diameter: None,
}),
};
let ad3 = ApertureDefinition {
code: 17,
aperture: Aperture::Polygon(Polygon {
diameter: 5.5,
vertices: 5,
rotation: None,
hole_diameter: Some(1.8),
}),
};
assert_partial_code!(ad1, "15P,4.5X3");
assert_partial_code!(ad2, "16P,5X4X30.6");
assert_partial_code!(ad3, "17P,5.5X5X0X1.8");
}
#[test]
fn test_polarity_serialize() {
let d = ExtendedCode::LoadPolarity(Polarity::Dark);
let c = ExtendedCode::LoadPolarity(Polarity::Clear);
assert_code!(d, "%LPD*%\n");
assert_code!(c, "%LPC*%\n");
}
#[test]
fn test_step_and_repeat_serialize() {
let o = ExtendedCode::StepAndRepeat(StepAndRepeat::Open {
repeat_x: 2,
repeat_y: 3,
distance_x: 2.0,
distance_y: 3.0,
});
let c = ExtendedCode::StepAndRepeat(StepAndRepeat::Close);
assert_code!(o, "%SRX2Y3I2J3*%\n");
assert_code!(c, "%SR*%\n");
}
#[test]
fn test_delete_attribute_serialize() {
let d = ExtendedCode::DeleteAttribute("foo".into());
assert_code!(d, "%TDfoo*%\n");
}
#[test]
fn test_file_attribute_serialize() {
let part = ExtendedCode::FileAttribute(FileAttribute::Part(Part::Other("foo".into())));
assert_code!(part, "%TF.Part,Other,foo*%\n");
let gensw1 = ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware(
GenerationSoftware::new("Vend0r", "superpcb", None),
));
assert_code!(gensw1, "%TF.GenerationSoftware,Vend0r,superpcb*%\n");
let gensw2 = ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware(
GenerationSoftware::new("Vend0r", "superpcb", Some("1.2.3")),
));
assert_code!(gensw2, "%TF.GenerationSoftware,Vend0r,superpcb,1.2.3*%\n");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
/// Assert that serializing the object generates
/// the specified gerber code.
macro_rules! assert_code {
($obj:expr, $expected:expr) => {
let mut buf = BufWriter::new(Vec::new());
$obj.serialize(&mut buf)
.expect("Could not generate Gerber code");
let bytes = buf.into_inner().unwrap();
let code = String::from_utf8(bytes).unwrap();
assert_eq!(&code, $expected);
};
}
/// Assert that partially serializing the object generates
/// the specified gerber code.
macro_rules! assert_partial_code {
($obj:expr, $expected:expr) => {
let mut buf = BufWriter::new(Vec::new());
$obj.serialize_partial(&mut buf)
.expect("Could not generate Gerber code");
let bytes = buf.into_inner().unwrap();
let code = String::from_utf8(bytes).unwrap();
assert_eq!(&code, $expected);
};
}

View File

@ -0,0 +1,19 @@
//! Traits used in gerber-types.
use std::io::Write;
use crate::GerberResult;
/// All types that implement this trait can be converted to a complete Gerber
/// Code line. Generated code should end with a newline.
pub trait GerberCode<W: Write> {
fn serialize(&self, writer: &mut W) -> GerberResult<()>;
}
/// All types that implement this trait can be converted to a Gerber Code
/// representation.
///
/// This is a crate-internal trait.
pub trait PartialGerberCode<W: Write> {
fn serialize_partial(&self, writer: &mut W) -> GerberResult<()>;
}

View File

@ -0,0 +1,188 @@
//! Types for Gerber code generation.
//!
//! All types are stateless, meaning that they contain all information in order
//! to render themselves. This means for example that each `Coordinates`
//! instance contains a reference to the coordinate format to be used.
use std::convert::From;
use crate::attributes;
use crate::coordinates;
use crate::extended_codes;
use crate::function_codes;
use crate::macros;
// Helper macros
macro_rules! impl_from {
($from:ty, $target:ty, $variant:expr) => {
impl From<$from> for $target {
fn from(val: $from) -> Self {
$variant(val)
}
}
};
}
// Root type
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
FunctionCode(FunctionCode),
ExtendedCode(ExtendedCode),
}
impl_from!(FunctionCode, Command, Command::FunctionCode);
impl_from!(ExtendedCode, Command, Command::ExtendedCode);
macro_rules! impl_command_fromfrom {
($from:ty, $inner:path) => {
impl From<$from> for Command {
fn from(val: $from) -> Self {
Command::from($inner(val))
}
}
};
}
// Main categories
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FunctionCode {
DCode(function_codes::DCode),
GCode(function_codes::GCode),
MCode(function_codes::MCode),
}
impl_from!(function_codes::DCode, FunctionCode, FunctionCode::DCode);
impl_from!(function_codes::GCode, FunctionCode, FunctionCode::GCode);
impl_from!(function_codes::MCode, FunctionCode, FunctionCode::MCode);
impl_command_fromfrom!(function_codes::DCode, FunctionCode::from);
impl_command_fromfrom!(function_codes::GCode, FunctionCode::from);
impl_command_fromfrom!(function_codes::MCode, FunctionCode::from);
#[derive(Debug, Clone, PartialEq)]
pub enum ExtendedCode {
/// FS
CoordinateFormat(coordinates::CoordinateFormat),
/// MO
Unit(extended_codes::Unit),
/// AD
ApertureDefinition(extended_codes::ApertureDefinition),
/// AM
ApertureMacro(macros::ApertureMacro),
/// LP
LoadPolarity(extended_codes::Polarity),
/// SR
StepAndRepeat(extended_codes::StepAndRepeat),
/// TF
FileAttribute(attributes::FileAttribute),
/// TA
ApertureAttribute(attributes::ApertureAttribute),
/// TD
DeleteAttribute(String),
}
impl_from!(
coordinates::CoordinateFormat,
ExtendedCode,
ExtendedCode::CoordinateFormat
);
impl_from!(extended_codes::Unit, ExtendedCode, ExtendedCode::Unit);
impl_from!(
extended_codes::ApertureDefinition,
ExtendedCode,
ExtendedCode::ApertureDefinition
);
impl_from!(
macros::ApertureMacro,
ExtendedCode,
ExtendedCode::ApertureMacro
);
impl_from!(
extended_codes::Polarity,
ExtendedCode,
ExtendedCode::LoadPolarity
);
impl_from!(
extended_codes::StepAndRepeat,
ExtendedCode,
ExtendedCode::StepAndRepeat
);
impl_from!(
attributes::FileAttribute,
ExtendedCode,
ExtendedCode::FileAttribute
);
impl_from!(
attributes::ApertureAttribute,
ExtendedCode,
ExtendedCode::ApertureAttribute
);
impl_command_fromfrom!(coordinates::CoordinateFormat, ExtendedCode::from);
impl_command_fromfrom!(extended_codes::Unit, ExtendedCode::from);
impl_command_fromfrom!(extended_codes::ApertureDefinition, ExtendedCode::from);
impl_command_fromfrom!(macros::ApertureMacro, ExtendedCode::from);
impl_command_fromfrom!(extended_codes::Polarity, ExtendedCode::from);
impl_command_fromfrom!(extended_codes::StepAndRepeat, ExtendedCode::from);
impl_command_fromfrom!(attributes::FileAttribute, ExtendedCode::from);
impl_command_fromfrom!(attributes::ApertureAttribute, ExtendedCode::from);
#[cfg(test)]
mod test {
use super::*;
use std::io::BufWriter;
use crate::extended_codes::Polarity;
use crate::function_codes::GCode;
use crate::traits::GerberCode;
#[test]
fn test_debug() {
//! The debug representation should work properly.
let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment("test".to_string())));
let debug = format!("{:?}", c);
assert_eq!(debug, "FunctionCode(GCode(Comment(\"test\")))");
}
#[test]
fn test_function_code_serialize() {
//! A `FunctionCode` should implement `GerberCode`
let c = FunctionCode::GCode(GCode::Comment("comment".to_string()));
assert_code!(c, "G04 comment*\n");
}
#[test]
fn test_function_code_from_gcode() {
let comment = GCode::Comment("hello".into());
let f1: FunctionCode = FunctionCode::GCode(comment.clone());
let f2: FunctionCode = comment.into();
assert_eq!(f1, f2);
}
#[test]
fn test_command_from_function_code() {
let comment = FunctionCode::GCode(GCode::Comment("hello".into()));
let c1: Command = Command::FunctionCode(comment.clone());
let c2: Command = comment.into();
assert_eq!(c1, c2);
}
#[test]
fn test_command_from_extended_code() {
let delete_attr = ExtendedCode::DeleteAttribute("hello".into());
let c1: Command = Command::ExtendedCode(delete_attr.clone());
let c2: Command = delete_attr.into();
assert_eq!(c1, c2);
}
#[test]
fn test_extended_code_from_polarity() {
let e1: ExtendedCode = ExtendedCode::LoadPolarity(Polarity::Dark);
let e2: ExtendedCode = Polarity::Dark.into();
assert_eq!(e1, e2);
}
}

Binary file not shown.

BIN
resources/excellon_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

BIN
resources/geometry_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

BIN
resources/gerber_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,12 @@
pub mod excellons; pub mod excellons;
pub mod geometries; pub mod geometries;
pub mod gerbers; pub mod gerbers;
mod live_position;
use std::hash::Hash; use eframe::egui::{Color32, Ui};
use eframe::{
egui::{self, Color32, Pos2, Rounding, Stroke, Ui},
epaint::RectShape,
};
use egui_plot::PlotUi; use egui_plot::PlotUi;
use excellons::{draw_excellons, draw_excellons_}; use excellons::draw_excellons_;
use geometries::{draw_geometries, draw_geometries_}; use geometries::draw_geometries_;
use gerbers::{draw_gerbers, draw_gerbers_}; use gerbers::draw_gerbers_;
use live_position::draw_live_position;
use crate::geometry::elements::Element; use crate::geometry::elements::Element;
@ -28,6 +21,7 @@ const EXCELLON_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(65, 42
const OUTLINE_COLOR: Color32 = Color32::DARK_BLUE; const OUTLINE_COLOR: Color32 = Color32::DARK_BLUE;
const OUTLINE_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(0, 0, 139, 128); const OUTLINE_COLOR_SELECTED: Color32 = Color32::from_rgba_premultiplied(0, 0, 139, 128);
#[derive(Debug, Clone, Copy)]
pub enum CanvasColour { pub enum CanvasColour {
Copper, Copper,
CopperOutline, CopperOutline,
@ -36,7 +30,7 @@ pub enum CanvasColour {
} }
impl CanvasColour { impl CanvasColour {
pub fn to_colour32(&self, selected: bool) -> Color32 { pub fn as_colour32(&self, selected: bool) -> Color32 {
match (self, selected) { match (self, selected) {
(CanvasColour::Copper, true) => COPPER_COLOR_SELECTED, (CanvasColour::Copper, true) => COPPER_COLOR_SELECTED,
(CanvasColour::Copper, false) => COPPER_COLOR, (CanvasColour::Copper, false) => COPPER_COLOR,
@ -59,48 +53,6 @@ pub fn draw_canvas(ui: &mut Ui, app: &mut Application) {
draw_excellons_(plot_ui, app); draw_excellons_(plot_ui, app);
draw_geometries_(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> { pub enum PlotDrawer<'a> {
@ -108,7 +60,7 @@ pub enum PlotDrawer<'a> {
Closure(&'a dyn Fn(&mut PlotUi)), Closure(&'a dyn Fn(&mut PlotUi)),
} }
fn draw_on_plot_canvas(ui: &mut PlotUi, app: &Application, drawer: PlotDrawer) { fn draw_on_plot_canvas(ui: &mut PlotUi, drawer: PlotDrawer) {
match drawer { match drawer {
PlotDrawer::Drawable((t, colour, selected)) => t.draw_egui_plot(ui, colour, selected), PlotDrawer::Drawable((t, colour, selected)) => t.draw_egui_plot(ui, colour, selected),
PlotDrawer::Closure(fun) => fun(ui), PlotDrawer::Closure(fun) => fun(ui),

View File

@ -1,7 +1,4 @@
use eframe::{ use eframe::egui;
egui::{self, Vec2},
emath::TSTransform,
};
use super::{ use super::{
canvas::draw_canvas, canvas::draw_canvas,
@ -9,230 +6,17 @@ use super::{
Application, Application,
}; };
impl eframe::App for Application { impl<'a> eframe::App for Application<'a> {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// let window_size = ctx.screen_rect().size();
draw_header(ctx, self); draw_header(ctx, self);
draw_sidebar(ctx, self); draw_sidebar(ctx, self);
egui::CentralPanel::default().show(ctx, |ui| { 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); 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.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); draw_canvas(ui, self);
}); });
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,10 @@
use clipper2::Paths; use std::collections::HashMap;
use gerber_types::{ use gerber_types::{
Aperture, Command, Coordinates, DCode, FunctionCode, GCode, InterpolationMode, MCode, Aperture, ApertureMacro, Command, Coordinates, DCode, FunctionCode, GCode, InterpolationMode,
Operation, Unit, MCode, MacroContent, Operation, Unit,
}; };
use tracing::{debug, error, info}; use tracing::{error, info};
use crate::{ use crate::{
geometry::{ geometry::{
@ -16,6 +17,7 @@ use crate::{
}; };
use super::{ use super::{
elements::{composite::Composite, polygon::Polygon},
union::{union_lines, union_with_apertures}, union::{union_lines, union_with_apertures},
Geometry, Geometry,
}; };
@ -30,8 +32,8 @@ impl From<GerberDoc> for Geometry {
let mut path_container: Vec<LinePath> = Vec::new(); let mut path_container: Vec<LinePath> = Vec::new();
let mut added_apertures: Vec<Element> = Vec::new(); let mut added_apertures: Vec<Element> = Vec::new();
// create geometries by applying all gerber commands
for command in gerber.commands { for command in gerber.commands {
println!("{command:?}");
match command { match command {
Command::FunctionCode(f) => { Command::FunctionCode(f) => {
match f { match f {
@ -39,14 +41,15 @@ impl From<GerberDoc> for Geometry {
match code { match code {
DCode::Operation(op) => match op { DCode::Operation(op) => match op {
Operation::Interpolate(coordinates, offset) => { Operation::Interpolate(coordinates, offset) => {
// create line by interpolating from current position to new position
if selected_interpolation_mode == InterpolationMode::Linear if selected_interpolation_mode == InterpolationMode::Linear
{ {
// self.add_draw_segment(coord); // add current position as starting position if active path is empty (=> start new one)
let point = Point::try_from(&coordinates);
if active_path.is_empty() { if active_path.is_empty() {
active_path.add(current_position); active_path.add(current_position);
} }
match point { // add point (from gerber coordinates) to active path
match Point::try_from(&coordinates) {
Ok(point) => { Ok(point) => {
active_path.add(point); active_path.add(point);
} }
@ -56,27 +59,32 @@ impl From<GerberDoc> for Geometry {
// TODO // TODO
// self.add_arc_segment(coord, offset.as_ref().expect(format!("No offset coord with 'Circular' state\r\n{:#?}", c).as_str())) // self.add_arc_segment(coord, offset.as_ref().expect(format!("No offset coord with 'Circular' state\r\n{:#?}", c).as_str()))
} }
// move current coordinates to new position
Self::move_position(&coordinates, &mut current_position); Self::move_position(&coordinates, &mut current_position);
} }
// move current coordinates to new position
Operation::Move(m) => { Operation::Move(m) => {
debug!("Move to {:?}, create path.", &m); // check if a aperture is selected and if it's circular
// self.create_path_from_data();
if let Some(Aperture::Circle(c)) = if let Some(Aperture::Circle(c)) =
selected_aperture.as_ref() selected_aperture.as_ref()
{ {
// check if a path is currently active
if !active_path.is_empty() { if !active_path.is_empty() {
// finish active path if there is an active one
active_path.finalize(c.diameter); active_path.finalize(c.diameter);
path_container.push(active_path); path_container.push(active_path);
} }
} }
// create new active path and move position tp current position
active_path = LinePath::new(); active_path = LinePath::new();
Geometry::move_position(&m, &mut current_position); Self::move_position(&m, &mut current_position);
} }
// add selected Aperture
Operation::Flash(f) => { Operation::Flash(f) => {
// self.create_path_from_data();
Self::add_geometry( Self::add_geometry(
&mut added_apertures, &mut added_apertures,
&gerber.aperture_macros,
&current_position, &current_position,
&f, &f,
&selected_aperture, &selected_aperture,
@ -85,8 +93,21 @@ impl From<GerberDoc> for Geometry {
Self::move_position(&f, &mut current_position); Self::move_position(&f, &mut current_position);
} }
}, },
// select an aperture
DCode::SelectAperture(ap) => { DCode::SelectAperture(ap) => {
// self.create_path_from_data(); // check if a aperture is selected and if it's circular
if let Some(Aperture::Circle(c)) = selected_aperture.as_ref() {
// check if a path is currently active
if !active_path.is_empty() {
// finish active path if there is an active one before selecting new aperture
active_path.finalize(c.diameter);
path_container.push(active_path);
// create new active path
active_path = LinePath::new();
}
}
selected_aperture = Some( selected_aperture = Some(
gerber gerber
.apertures .apertures
@ -121,50 +142,13 @@ impl From<GerberDoc> for Geometry {
} }
} }
let mut result = clipper2::Paths::new(vec![]); // union all drawn lines into nets of conductors
// 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); let conductor_net = union_lines(&path_container);
// union conductors with apertures
let united_nets = union_with_apertures(&added_apertures, conductor_net).unwrap();
for outline in &conductor_net {
println!("{:?}", outline.included_points);
geo.push(outline.outline.clone());
}
println!("Number of conductor net paths: {}", geo.len());
if let Some(geo) = union_with_apertures(&added_apertures, conductor_net) {
println!("Number of finalized net paths: {}", geo.len());
result = geo;
}
Self { Self {
outline_union: result, united_nets,
apertures: added_apertures, apertures: added_apertures,
paths: path_container, paths: path_container,
units: gerber.units.unwrap_or(Unit::Millimeters).into(), units: gerber.units.unwrap_or(Unit::Millimeters).into(),
@ -173,15 +157,15 @@ impl From<GerberDoc> for Geometry {
} }
impl Geometry { impl Geometry {
fn move_position(coord: &Coordinates, position: &mut Point) -> () { fn move_position(coord: &Coordinates, position: &mut Point) {
if let Ok(pos) = Point::try_from(coord) { if let Ok(pos) = Point::try_from(coord) {
debug!("Moved position to {pos:?}");
*position = pos; *position = pos;
}; };
} }
fn add_geometry( fn add_geometry(
geometries: &mut Vec<Element>, geometries: &mut Vec<Element>,
aperture_macros: &HashMap<String, ApertureMacro>,
position: &Point, position: &Point,
coordinates: &Coordinates, coordinates: &Coordinates,
aperture: &Option<Aperture>, aperture: &Option<Aperture>,
@ -201,14 +185,60 @@ impl Geometry {
))); )));
} }
Aperture::Obround(o) => { Aperture::Obround(o) => {
// error!("Unsupported Obround aperture:\r\n{:#?}", o);
geometries.push(Element::Obround(Obround::from_aperture_obround(o, target))); geometries.push(Element::Obround(Obround::from_aperture_obround(o, target)));
} }
Aperture::Polygon(p) => { Aperture::Polygon(p) => {
// TODO add polygon
error!("Unsupported Polygon aperture:\r\n{:#?}", p); error!("Unsupported Polygon aperture:\r\n{:#?}", p);
} }
Aperture::Other(o) => { Aperture::Other(o) => {
error!("Unsupported Other aperture:\r\n{:#?}", o); // split at '/' -> name/arguments
if let Some((name, args)) = o.split_once("/") {
// parse variables from args
let variables: Vec<f64> = args
.split(",")
.map(|s| s.parse::<f64>().unwrap_or(0.))
.collect();
if let Some(ap_macro) = aperture_macros.get(name) {
let mut composite = Composite::new();
// add elements from macro
for aperture in &ap_macro.content {
match aperture {
MacroContent::Circle(c) => composite.add(Element::Circle(
Circle::from_macro(c, &variables, target),
)),
MacroContent::VectorLine(vl) => composite.add(Element::Rectangle(
Rectangle::from_macro_vector_line(vl, &variables, target),
)),
MacroContent::CenterLine(cl) => composite.add(Element::Rectangle(
Rectangle::from_macro_center_line(cl, &variables, target),
)),
MacroContent::Outline(ol) => composite.add(Element::Polygon(
Polygon::from_macro(ol, &variables, target),
)),
MacroContent::Polygon(p) => {
error!("Polygon Not implemented yet")
}
MacroContent::Moire(m) => {
error!("Moire Not implemented yet")
}
MacroContent::Thermal(t) => {
error!("Thermal Not implemented yet")
}
MacroContent::VariableDefinition(_) => {}
MacroContent::Comment(_) => {}
};
}
composite.finalize();
geometries.push(Element::Composite(composite));
} else {
error!("Unsupported Other aperture:\r\n{:#?}", o);
}
} else {
error!("Unsupported Other aperture:\r\n{:#?}", o);
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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