Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
5203ce801This file is pinned to the selected release. Download the original to open it in your design tool.
using GerberLibrary.Core;
using GerberLibrary.Core.Primitives;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GerberLibrary
{
public class ExcellonTool
{
public int ID;
public double Radius;
public List<PointD> Drills = new List<PointD>();
public class SlotInfo
{
public PointD Start = new PointD();
public PointD End = new PointD();
public override string ToString()
{
return $"({Start.X:N2},{Start.Y:N2})-({End.X:N2},{End.X:N2})";
}
}
public List<SlotInfo> Slots = new List<SlotInfo>();
};
public class ExcellonFile
{
public void Load(ProgressLog log, string filename, double drillscaler = 1.0)
{
var Load = log.PushActivity("Loading Excellon");
var lines = File.ReadAllLines(filename);
ParseExcellon(lines.ToList(), drillscaler,log);
log.PopActivity(Load);
}
public void Load(ProgressLog log, StreamReader stream, double drillscaler = 1.0)
{
List<string> lines = new List<string>();
while (!stream.EndOfStream)
{
lines.Add(stream.ReadLine());
}
ParseExcellon(lines, drillscaler, log);
}
public static void MergeAll(List<string> Files, string output, ProgressLog Log)
{
var LogDepth = Log.PushActivity("Excellon MergeAll");
if (Files.Count >= 2)
{
MultiMerge(Files[0], Files.Skip(1).ToList(), output, Log);
Log.PopActivity(LogDepth);
return;
}
if (Files.Count < 2)
{
if (Files.Count == 1)
{
Log.AddString("Merging 1 file is copying... doing so...");
if (File.Exists(output)) File.Delete(output);
File.Copy(Files[0], output);
}
else
{
Log.AddString("Need files to do anything??");
}
Log.PopActivity(LogDepth);
return;
}
string LastFile = Files[0];
List<string> TempFiles = new List<string>();
for (int i = 1; i < Files.Count - 1; i++)
{
string NewFile = Path.GetTempFileName();
TempFiles.Add(NewFile);
Merge(LastFile, Files[i], NewFile, Log);
LastFile = NewFile;
}
Merge(LastFile, Files.Last(), output, Log);
Log.AddString("Removing merge tempfiles");
foreach (string s in TempFiles)
{
File.Delete(s);
}
Log.PopActivity(LogDepth);
}
private static void MultiMerge(string file1, List<string> otherfiles, string output, ProgressLog Log)
{
int MM = Log.PushActivity("Excellon MultiMerge");
if (File.Exists(file1) == false)
{
Log.AddString(String.Format("{0} not found! stopping process!", file1));
Log.PopActivity(MM);
return;
}
foreach (var otherfile in otherfiles)
{
if (File.Exists(otherfile) == false)
{
Log.AddString(String.Format("{0} not found! stopping process!", otherfile));
Log.PopActivity(MM);
return;
}
}
Log.AddString(String.Format("Reading {0}:", file1));
ExcellonFile File1Parsed = new ExcellonFile();
File1Parsed.Load(Log, file1);
List<ExcellonFile> OtherFilesParsed = new List<ExcellonFile>();
foreach (var otherfile in otherfiles)
{
Log.AddString(String.Format("Reading {0}:", otherfile));
ExcellonFile OtherFileParsed = new ExcellonFile();
OtherFileParsed.Load(Log, otherfile);
OtherFilesParsed.Add(OtherFileParsed);
}
int MaxID = 0;
foreach (var D in File1Parsed.Tools)
{
if (D.Value.ID > MaxID) MaxID = D.Value.ID + 1;
}
foreach (var F in OtherFilesParsed)
{
foreach (var D in F.Tools)
{
File1Parsed.AddToolWithHoles(D.Value); ;
// D.Value.ID += MaxID;
// File1Parsed.Tools[D.Value.ID] = D.Value;
}
}
File1Parsed.Write(output, 0, 0, 0, 0);
Log.PopActivity(MM);
}
private void AddToolWithHoles(ExcellonTool d)
{
ExcellonTool T = FindMatchingTool(d);
foreach(var a in d.Drills)
{
T.Drills.Add(new PointD(a.X, a.Y));
}
foreach(var s in d.Slots)
{
T.Slots.Add(new ExcellonTool.SlotInfo() { Start = new PointD(s.Start.X, s.Start.Y), End = new PointD(s.End.X, s.End.Y) });
}
}
private ExcellonTool FindMatchingTool(ExcellonTool d)
{
int freeid = 10;
foreach(var t in Tools)
{
if (d.Radius == t.Value.Radius) return t.Value;
if (t.Key >= freeid) freeid = t.Key + 1;
}
var T = new ExcellonTool() { Radius = d.Radius , ID = freeid};
Tools[T.ID] = T;
return T;
}
public static void Merge(string file1, string file2, string outputfile, ProgressLog Log)
{
Log.PushActivity("Excellon Merge");
if (File.Exists(file1) == false)
{
Log.AddString(String.Format("{0} not found! stopping process!", file1));
Log.PopActivity();
return;
}
if (File.Exists(file2) == false)
{
Log.AddString(String.Format("{0} not found! stopping process!", file2));
Log.PopActivity();
return;
}
Log.AddString(String.Format("Reading {0}:", file1));
ExcellonFile File1Parsed = new ExcellonFile();
File1Parsed.Load(Log, file1);
Log.AddString(String.Format("Reading {0}:", file2));
ExcellonFile File2Parsed = new ExcellonFile();
File2Parsed.Load(Log, file2);
Log.AddString(String.Format("Merging {0} with {1}", file1, file2));
int MaxID = 0;
foreach (var D in File1Parsed.Tools)
{
if (D.Value.ID > MaxID) MaxID = D.Value.ID + 1;
}
foreach (var D in File2Parsed.Tools)
{
D.Value.ID += MaxID;
File1Parsed.Tools[D.Value.ID] = D.Value;
}
File1Parsed.Write(outputfile, 0, 0, 0, 0);
Log.PopActivity();
}
public void Write(string filename, double DX, double DY, double DXp, double DYp, double AngleInDeg = 0)
{
double Angle = AngleInDeg * (Math.PI * 2.0) / 360.0;
double CA = Math.Cos(Angle);
double SA = Math.Sin(Angle);
List<string> lines = new List<string>();
lines.Add("%");
lines.Add("M48");
lines.Add("METRIC,000.000");
//lines.Add("M71");
foreach (var a in Tools)
{
lines.Add(String.Format("T{0}C{1}", a.Key.ToString("D2"), (a.Value.Radius * 2).ToString("N2").Replace(',', '.')));
}
lines.Add("%");
GerberNumberFormat GNF = new GerberNumberFormat();
GNF.SetMetricMode();
GNF.OmitLeading = true;
GNF.DigitsAfter = 3;
GNF.DigitsBefore = 3;
foreach (var a in Tools)
{
lines.Add(String.Format("T{0}", a.Key.ToString("D2")));
double coordmultiplier = 1;
foreach (var d in a.Value.Drills)
{
double X = (d.X * coordmultiplier + DXp) / coordmultiplier;
double Y = (d.Y * coordmultiplier + DYp) / coordmultiplier;
if (Angle != 0)
{
double nX = X * CA - Y * SA;
double nY = X * SA + Y * CA;
X = nX;
Y = nY;
}
X = (X * coordmultiplier + DX) / coordmultiplier;
Y = (Y * coordmultiplier + DY) / coordmultiplier;
lines.Add(string.Format("X{0}Y{1}", GNF.Format(X), GNF.Format(Y).Replace(',', '.')));
}
foreach(var s in a.Value.Slots)
{
double XS = (s.Start.X * coordmultiplier + DXp) / coordmultiplier;
double YS = (s.Start.Y * coordmultiplier + DYp) / coordmultiplier;
double XE = (s.End.X * coordmultiplier + DXp) / coordmultiplier;
double YE = (s.End.Y * coordmultiplier + DYp) / coordmultiplier;
if (Angle != 0)
{
double nX = XS * CA - YS * SA;
double nY = XS * SA + YS * CA;
XS = nX;
YS = nY;
double neX = XE * CA - YE * SA;
double neY = XE * SA + YE * CA;
XE = neX;
YE = neY;
}
XS = (XS * coordmultiplier + DX) / coordmultiplier;
YS = (YS * coordmultiplier + DY) / coordmultiplier;
XE = (XE * coordmultiplier + DX) / coordmultiplier;
YE = (YE * coordmultiplier + DY) / coordmultiplier;
lines.Add(string.Format("X{0}Y{1}G85X{2}Y{3}", GNF.Format(XS), GNF.Format(YS).Replace(',', '.'),GNF.Format(XE), GNF.Format(YE).Replace(',', '.')));
}
}
lines.Add("M30");
Gerber.WriteAllLines(filename, lines);
}
public Dictionary<int, ExcellonTool> Tools = new Dictionary<int, ExcellonTool>();
public int TotalDrillCount()
{
int T = 0;
foreach(var Tool in Tools)
{
T += Tool.Value.Drills.Count;
}
return T;
}
private enum CutterCompensation
{
None = 0,
Left,
Right
}
private List<PointD> CutCompensation(List<PointD> path, CutterCompensation compensation, double offset)
{
if (compensation == CutterCompensation.None)
return path;
if (path.Count < 2)
return path;
/* remove contiguous duplicates */
var unique = new List<PointD>(path.Count);
PointD prev = null;
foreach (var point in path)
{
if (prev == point)
continue;
prev = point;
unique.Add(point);
}
path = unique;
/* create offset segments */
var SegmentsOffset = path.Zip(path.Skip(1), (A, B) =>
{
var angle = A.Angle(B);
if (compensation == CutterCompensation.Left)
angle += Math.PI / 2;
else
angle -= Math.PI / 2;
A += new PointD(offset * Math.Cos(angle), offset * Math.Sin(angle));
B += new PointD(offset * Math.Cos(angle), offset * Math.Sin(angle));
return new { A, B };
});
/* create segment pairs */
var SegmentPairs = SegmentsOffset
.Zip(SegmentsOffset.Skip(1), (First, Second) => new { First, Second })
.Zip(path.Skip(1), (pair, Center) => new { pair.First, pair.Second, Center });
var Path = new PolyLine();
Path.Vertices.Add(SegmentsOffset.First().A);
foreach (var segment in SegmentPairs)
{
/* segments are colinear */
if (segment.First.B == segment.Second.A)
continue;
var intersection = Helpers.SegmentSegmentIntersect(segment.First.A, segment.First.B, segment.Second.A, segment.Second.B);
/* if segments intersect, */
if (intersection != null)
{
/* the intersection point is what connects first and second segments */
Path.Vertices.Add(intersection);
}
else
{
/* otherwise connect segments with an arc */
var Center = segment.Center - segment.First.B;
var arc = Gerber.CreateCurvePoints(
segment.First.B.X, segment.First.B.Y,
segment.Second.A.X, segment.Second.A.Y,
Center.X, Center.Y,
compensation == CutterCompensation.Left ? InterpolationMode.ClockWise : InterpolationMode.CounterClockwise,
GerberQuadrantMode.Multi);
Path.Vertices.AddRange(arc);
}
}
Path.Vertices.Add(SegmentsOffset.Last().B);
return Path.Vertices;
}
bool ParseExcellon(List<string> lines, double drillscaler,ProgressLog log )
{
var LogID = log.PushActivity("Parse Excellon");
Tools.Clear();
bool headerdone = false;
int currentline = 0;
ExcellonTool CurrentTool = null;
GerberNumberFormat GNF = new GerberNumberFormat();
GNF.DigitsBefore = 3;
GNF.DigitsAfter = 3;
GNF.OmitLeading = true;
double Scaler = 1.0f;
bool FormatSpecified = false;
bool NumberSpecHad = false;
double LastX = 0;
double LastY = 0;
double WorkZeroX; // not sure what to do with these workzero coordinates? can these change multiple times per file?
double WorkZeroY;
CutterCompensation Compensation = CutterCompensation.None;
List<PointD> PathCompensation = new List<PointD>();
bool WarnIntersections = true;
while (currentline < lines.Count)
{
switch(lines[currentline])
{
// case "M70": GNF.Multiplier = 25.4; break; // inch mode
case "INCH":
if (Gerber.ExtremelyVerbose) log.AddString("Out of header INCH found!");
GNF.SetImperialMode();
break; // inch mode
case "METRIC":
if (Gerber.ExtremelyVerbose) log.AddString("Out of header METRIC found!");
GNF.SetMetricMode();
break;
case "M72":
if (Gerber.ExtremelyVerbose) log.AddString("Out of header M72 found!");
GNF.SetImperialMode();
break; // inch mode
case "M71":
if (Gerber.ExtremelyVerbose) log.AddString("Out of header M71 found!");
GNF.SetMetricMode();
break; // metric mode
}
if (lines[currentline] == "M48")
{
//Console.WriteLine("Excellon header starts at line {0}", currentline);
currentline++;
while ((lines[currentline] != "%" && lines[currentline] != "M95"))
{
headerdone = true;
//double InchMult = 1;// 0.010;
switch (lines[currentline])
{
// case "M70": GNF.Multiplier = 25.4; break; // inch mode
case "INCH":
GNF.SetImperialMode();
//Scaler = 0.01;
break; // inch mode
case "METRIC":
GNF.SetMetricMode();
break;
case "M72":
//GNF.Multiplier = 25.4 * InchMult;
GNF.SetImperialMode();
// Scaler = 0.01;
break; // inch mode
case "M71":
//GNF.Multiplier = 1.0;
GNF.SetMetricMode();
break; // metric mode
default:
{
var S = lines[currentline].Split(',');
if (S[0].IndexOf("INCH") == 0 || S[0].IndexOf("METRIC") == 0)
{
if (S[0].IndexOf("INCH") ==0)
{
GNF.SetImperialMode();
}
else
{
GNF.SetMetricMode();
}
if (S.Count() > 1)
{
for (int i = 1; i < S.Count(); i++)
{if (S[i][0] == '0')
{
log.AddString(String.Format("Number spec reading!: {0}", S[i]));
var A = S[i].Split('.');
if (A.Length == 2)
{
GNF.DigitsBefore = A[0].Length;
GNF.DigitsAfter = A[1].Length;
NumberSpecHad = true;
}
}
if (S[i] == "LZ")
{
GNF.OmitLeading = false;
}
if (S[i] == "TZ")
{
GNF.OmitLeading = true;
}
}
}
}
else
{
if (lines[currentline][0] == ';')
{
if (Gerber.ShowProgress) log.AddString(lines[currentline]);
if (lines[currentline].Contains(";FILE_FORMAT="))
{
var N = lines[currentline].Substring(13).Split(':');
GNF.DigitsBefore = int.Parse(N[0]);
GNF.DigitsAfter = int.Parse(N[1]);
FormatSpecified = true;
}
}
else
{
GCodeCommand GCC = new GCodeCommand();
GCC.Decode(lines[currentline], GNF);
if (GCC.charcommands.Count > 0)
switch (GCC.charcommands[0])
{
case 'T':
{
ExcellonTool ET = new ExcellonTool();
ET.ID = (int)GCC.numbercommands[0];
ET.Radius = GNF.ScaleFileToMM(GCC.GetNumber('C')) / 2.0f;
Tools[ET.ID] = ET;
}
break;
}
}
}
}
break;
}
currentline++;
}
// Console.WriteLine("Excellon header stops at line {0}", currentline);
if (FormatSpecified == false && NumberSpecHad == false)
{
if (GNF.CurrentNumberScale == GerberNumberFormat.NumberScale.Imperial)
{
// GNF.OmitLeading = true;
GNF.DigitsBefore = 2;
GNF.DigitsAfter = 4;
}
else
{
GNF.DigitsAfter = 3;
GNF.DigitsBefore = 3;
}
}
}
else
{
if (headerdone)
{
GCodeCommand GCC = new GCodeCommand();
GCC.Decode(lines[currentline], GNF);
if (GCC.charcommands.Count > 0)
{
switch (GCC.charcommands[0])
{
case 'T':
if ((int)GCC.numbercommands[0] > 0)
{
CurrentTool = Tools[(int)GCC.numbercommands[0]];
}
else
{
CurrentTool = null;
}
break;
case 'M':
default:
{
GerberSplitter GS = new GerberSplitter();
GS.Split(GCC.originalline, GNF, true);
if (GS.Has("G") && GS.Get("G") == 85 && (GS.Has("X") || GS.Has("Y")))
{
GerberListSplitter GLS = new GerberListSplitter();
GLS.Split(GCC.originalline, GNF, true);
double x1 = LastX;
double y1 = LastY;
if (GLS.HasBefore("G", "X")) {x1 = GNF.ScaleFileToMM(GLS.GetBefore("G", "X") * Scaler);LastX = x1;}
if (GLS.HasBefore("G", "Y")) {y1 = GNF.ScaleFileToMM(GLS.GetBefore("G", "Y") * Scaler); LastY = y1; }
double x2 = LastX;
double y2 = LastY;
if (GLS.HasAfter("G", "X")) { x2 = GNF.ScaleFileToMM(GLS.GetAfter("G", "X") * Scaler); LastX = x2; }
if (GLS.HasAfter("G", "Y")) { y2 = GNF.ScaleFileToMM(GLS.GetAfter("G", "Y") * Scaler); LastY = y2; }
CurrentTool.Slots.Add(new ExcellonTool.SlotInfo() { Start = new PointD(x1 * drillscaler, y1 * drillscaler), End = new PointD(x2 * drillscaler, y2 * drillscaler) });
LastX = x2;
LastY = y2;
}
else if (GS.Has("G") && GS.Get("G") == 00 && (GS.Has("X") || GS.Has("Y")))
{
GerberListSplitter GLS = new GerberListSplitter();
GLS.Split(GCC.originalline, GNF, true);
double x1 = LastX;
double y1 = LastY;
if (GLS.HasAfter("G", "X")) { x1 = GNF.ScaleFileToMM(GLS.GetAfter("G", "X") * Scaler); LastX = x1; }
if (GLS.HasAfter("G", "Y")) { y1 = GNF.ScaleFileToMM(GLS.GetAfter("G", "Y") * Scaler); LastY = y1; }
/* cancel cutter compensation */
Compensation = CutterCompensation.None;
PathCompensation.Clear();
}
else if (GS.Has("G") && GS.Get("G") == 01 && (GS.Has("X") || GS.Has("Y")))
{
GerberListSplitter GLS = new GerberListSplitter();
GLS.Split(GCC.originalline, GNF, true);
double x1 = LastX;
double y1 = LastY;
double x2 = LastX;
double y2 = LastY;
if (GLS.HasAfter("G", "X")) { x2 = GNF.ScaleFileToMM(GLS.GetAfter("G", "X") * Scaler); LastX = x2; }
if (GLS.HasAfter("G", "Y")) { y2 = GNF.ScaleFileToMM(GLS.GetAfter("G", "Y") * Scaler); LastY = y2; }
if (Compensation == CutterCompensation.None)
CurrentTool.Slots.Add(new ExcellonTool.SlotInfo() { Start = new PointD(x1 * drillscaler, y1 * drillscaler), End = new PointD(x2 * drillscaler, y2 * drillscaler) });
else
PathCompensation.Add(new PointD(x2 * drillscaler, y2 * drillscaler));
LastX = x2;
LastY = y2;
}
else if (GS.Has("G") && GS.Get("G") == 40) /* cutter compensation off */
{
var comp = CutCompensation(PathCompensation, Compensation, CurrentTool.Radius * drillscaler);
if (WarnIntersections)
{
/* warn about path intersections */
for (int i = 0; i < comp.Count - 1; i++)
{
for (int j = i + 2; j < comp.Count - 1; j++)
{
var intersection = Helpers.SegmentSegmentIntersect(comp[i], comp[i + 1], comp[j], comp[j + 1]);
if (intersection != null)
{
log.AddString("Path with intersections found on cut compensation! Inspect output for accuracy!");
WarnIntersections = false;
break;
}
}
if (!WarnIntersections)
break;
}
}
/* create line segments from set of points */
var array = comp.Zip(comp.Skip(1), Tuple.Create);
CurrentTool.Slots.AddRange(array.Select(i => new ExcellonTool.SlotInfo() { Start = i.Item1, End = i.Item2 }));
Compensation = CutterCompensation.None;
PathCompensation.Clear();
}
else if (GS.Has("G") && GS.Get("G") == 41) /* cutter compensation left: offset of the cutter radius is to the LEFT of contouring direction */
{
if (Compensation != CutterCompensation.None)
log.AddString("Unterminated cutter compensation block found! Inspect output for accuracy!");
Compensation = CutterCompensation.Left;
PathCompensation.Clear();
PathCompensation.Add(new PointD(LastX * drillscaler, LastY * drillscaler));
}
else if (GS.Has("G") && GS.Get("G") == 42) /* cutter compensation right: offset of the cutter radius is to the RIGHT of contouring direction */
{
if (Compensation != CutterCompensation.None)
log.AddString("Unterminated cutter compensation block found! Inspect output for accuracy!");
Compensation = CutterCompensation.Right;
PathCompensation.Clear();
PathCompensation.Add(new PointD(LastX * drillscaler, LastY * drillscaler));
}
else if (GS.Has("G") && GS.Get("G") == 93) /* excellon work zero? */
{
WorkZeroX = 0;
WorkZeroY = 0;
if (GS.Has("X"))
WorkZeroX = GNF.ScaleFileToMM(GS.Get("X") * Scaler);
if (GS.Has("Y"))
WorkZeroY = GNF.ScaleFileToMM(GS.Get("Y") * Scaler);
log.AddString("WorkZero set - unsupported feature so far.");
}
else
{
//Deal with the repeat code
if (GS.Has("R") && (GS.Has("X") || GS.Has("Y")))
{
double repeatX = 0;
double repeatY = 0;
if (GS.Has("X"))
repeatX = GNF.ScaleFileToMM(GS.Get("X") * Scaler);
if (GS.Has("Y"))
repeatY = GNF.ScaleFileToMM(GS.Get("Y") * Scaler);
for (int repeatIndex = 1; repeatIndex <= GS.Get("R"); repeatIndex++)
{
double X = LastX;
if (GS.Has("X"))
X += repeatX;
double Y = LastY;
if (GS.Has("Y"))
Y += repeatY;
CurrentTool.Drills.Add(new PointD(X * drillscaler, Y * drillscaler));
LastX = X;
LastY = Y;
}
}
else if (GS.Has("X") || GS.Has("Y"))
{
double X = LastX;
if (GS.Has("X")) X = GNF.ScaleFileToMM(GS.Get("X") * Scaler);
double Y = LastY;
if (GS.Has("Y")) Y = GNF.ScaleFileToMM(GS.Get("Y") * Scaler);
if (Compensation == CutterCompensation.None)
CurrentTool.Drills.Add(new PointD(X * drillscaler, Y * drillscaler));
else
PathCompensation.Add(new PointD(X * drillscaler, Y * drillscaler));
LastX = X;
LastY = Y;
}
}
}
break;
}
}
}
}
currentline++;
}
log.PopActivity(LogID);
return headerdone;
}
public static void WriteContainedOnly(string inputfile, PolyLine Boundary, string outputfilename, ProgressLog Log)
{
Log.PushActivity("Excellon Clipper");
if (File.Exists(inputfile) == false)
{
Log.AddString(String.Format("{0} not found! stopping process!", Path.GetFileName(inputfile)));
Log.PopActivity();
return;
}
Log.AddString(String.Format("Clipping {0} to {1}", Path.GetFileName(inputfile), Path.GetFileName(outputfilename)));
ExcellonFile EF = new ExcellonFile();
EF.Load(Log, inputfile);
EF.WriteContained(Boundary, outputfilename, Log);
Log.PopActivity();
}
private void WriteContained(PolyLine boundary, string outputfilename, ProgressLog log)
{
ExcellonFile Out = new ExcellonFile();
foreach(var T in Tools)
{
Out.Tools[T.Key] = new ExcellonTool() { ID = T.Value.ID, Radius = T.Value.Radius };
foreach(var d in T.Value.Drills)
{
if (boundary.PointInPoly(new PointD(d.X , d.Y)))
{
Out.Tools[T.Key].Drills.Add(d);
}
}
foreach (var d in T.Value.Slots)
{
if (boundary.PointInPoly(d.Start) || boundary.PointInPoly(d.End))
{
Out.Tools[T.Key].Slots.Add(d);
}
}
}
Out.Write(outputfilename, 0, 0, 0, 0);
}
}
}