<|start_filename|>pkg/format/intellijel/intellijel.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package intellijel

import (
	"github.com/jsleeio/go-eagle/pkg/format/eurorack"
	"github.com/jsleeio/go-eagle/pkg/panel"
)

// based on https://intellijel.com/support/1u-technical-specifications/

const (
	// PanelHeight1U represents the total height of an Intellijel 1U panel, in
	// millimetres
	PanelHeight1U = 39.65

	// MountingHolesLeftOffset represents the distance of the first mounting
	// hole from the left edge of the panel, in millimetres
	MountingHolesLeftOffset = eurorack.MountingHolesLeftOffset

	// MountingHoleTopY1U represents the Y value for the top row of 1U mounting
	// holes, in millimetres
	MountingHoleTopY1U = PanelHeight1U - 3.00

	// MountingHoleBottomY1U represents the Y value for the bottom row of 1U
	// mounting holes, in millimetres
	MountingHoleBottomY1U = 3.00

	// MountingHoleDiameter represents the diameter of a Eurorack system
	// mounting hole, in millimetres
	MountingHoleDiameter = eurorack.MountingHoleDiameter

	// HP represents horizontal pitch in a Eurorack frame, in millimetres
	HP = eurorack.HP

	// HorizontalFit indicates the panel tolerance adjustment for the format
	HorizontalFit = 0.25

	// CornerRadius indicates the corner radius for the format. Eurorack doesn't
	// believe in such things.
	CornerRadius = 0.0

	// RailHeightFromMountingHole is used to determine how much space exists.
	// See discussion in github.com/jsleeio/pkg/panel. 5mm is a good safe
	// figure for all known-used Eurorack rail types
	RailHeightFromMountingHole = eurorack.RailHeightFromMountingHole
)

// Intellijel implements the panel.Panel interface and encapsulates the physical
// characteristics of a Intellijel panel
type Intellijel struct {
	HP int
}

// NewIntellijel constructs a new Intellijel object
func NewIntellijel(hp int) *Intellijel {
	return &Intellijel{HP: hp}
}

// Width returns the width of a Intellijel panel, in millimetres
func (i Intellijel) Width() float64 {
	return HP * float64(i.HP)
}

// Height returns the height of a Intellijel panel, in millimetres
func (i Intellijel) Height() float64 {
	return PanelHeight1U
}

// MountingHoleDiameter returns the Intellijel system mounting hole size, in
// millimetres
func (i Intellijel) MountingHoleDiameter() float64 {
	return MountingHoleDiameter
}

// MountingHoles generates a set of Point objects representing the mounting
// hole locations of a Intellijel panel
func (i Intellijel) MountingHoles() []panel.Point {
	rhsx := MountingHolesLeftOffset + HP*(float64(i.HP-3))
	holes := []panel.Point{
		{X: MountingHolesLeftOffset, Y: MountingHoleBottomY1U},
		{X: MountingHolesLeftOffset, Y: MountingHoleTopY1U},
		{X: rhsx, Y: MountingHoleBottomY1U},
		{X: rhsx, Y: MountingHoleTopY1U},
	}
	return holes
}

// HorizontalFit indicates the panel tolerance adjustment for the format
func (i Intellijel) HorizontalFit() float64 {
	return HorizontalFit
}

// CornerRadius indicates the corner radius for the format
func (i Intellijel) CornerRadius() float64 {
	return CornerRadius
}

// RailHeightFromMountingHole is used to calculate space between rails
func (i Intellijel) RailHeightFromMountingHole() float64 {
	return RailHeightFromMountingHole
}

// MountingHoleTopY returns the Y coordinate for the top row of mounting
// holes
func (i Intellijel) MountingHoleTopY() float64 {
	return MountingHoleTopY1U
}

// MountingHoleBottomY returns the Y coordinate for the bottom row of
// mounting holes
func (i Intellijel) MountingHoleBottomY() float64 {
	return MountingHoleBottomY1U
}

// HeaderLocation returns the location of the header text. Intellijel 1U has
// mounting rails so this is typically aligned with the top mounting screw

func (i Intellijel) HeaderLocation() panel.Point {
	return panel.Point{X: i.Width() / 2.0, Y: i.MountingHoleTopY()}
}

// FooterLocation returns the location of the footer text. Intellijel 1U has
// mounting rails so this is typically aligned with the bottom mounting screw
func (i Intellijel) FooterLocation() panel.Point {
	return panel.Point{X: i.Width() / 2.0, Y: i.MountingHoleBottomY()}
}


<|start_filename|>pkg/panel/panel.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package panel

// Point defines a single metric coordinate in a 2D space.
type Point struct {
	X, Y float64
}

// Panel types encapsulate physical characteristics of a rail-mounted panel
// All coordinates, distances and sizes are indicated in millimetres
type Panel interface {
	// MountingHoles returns a list of Points indicating mounting hole locations
	MountingHoles() []Point

	// MountingHoleDiameter returns the appropriate mounting hole diameter for
	// the panel format
	MountingHoleDiameter() float64

	// Height returns the Y-dimension size for the panel, eg. 128.5mm for panels
	// in the Eurorack system. This does NOT include tolerance adjustments
	Height() float64

	// Height returns the X-dimension size for the panel. This does not include
	// tolerance adjustments
	Width() float64

	// HorizontalFit returns the panel tolerance amount in the horizontal axis.
	// When creating panel outlines, this tolerance amount should be added to
	// the left-edge X coordinate, and subtracted from the right-edge coordinate,
	// resulting in the panel being slightly narrower than the "correct" width.
	//
	// As this is intended to improve panel fit in a system with panels of varying
	// tolerances, this adjustment should only be applied to the left and right
	// edges of the outline, and NOT to the X coordinates of any other features
	// of the panel. (and especially not the mounting holes!)
	HorizontalFit() float64

	// CornerRadius returns the radius of the panel corners, if applicable. A
	// zero value will result in no corner segments being generated, and so the
	// board outline will consist of four straight lines.
	CornerRadius() float64

	// RailHeightFromMountingHole indicates how far up (from centre of bottom
	// mounting hole) or down (from centre of top mounting hole) the mounting
	// rail extends. This can be used to define KeepOut areas on the panel
	//
	// eg. For all Eurorack-related formats this is likely to be around 5mm,
	// though the exact figure will differ with rail type.
	//
	// This is primarily used to determine how much empty space there is between
	// the mounting rails, so best to err on the side of larger than smaller
	RailHeightFromMountingHole() float64

	// MountingHoleTopY returns the Y coordinate for the top row of mounting
	// holes
	MountingHoleTopY() float64

	// MountingHoleBottomY returns the Y coordinate for the bottom row of
	// mounting holes
	MountingHoleBottomY() float64

	// HeaderLocation returns the location of the header text
	HeaderLocation() Point

	// FooterLocation returns the location of the footer text
	FooterLocation() Point
}

func LeftX(spec Panel) float64 {
	return spec.HorizontalFit() / 2
}

func RightX(spec Panel) float64 {
	return spec.Width() - spec.HorizontalFit()/2
}

func TopY(spec Panel) float64 {
	return spec.Height()
}

func BottomY(spec Panel) float64 {
	return 0
}

func TopLeft(spec Panel) Point {
	return Point{X: LeftX(spec), Y: TopY(spec)}
}

func TopRight(spec Panel) Point {
	return Point{X: RightX(spec), Y: TopY(spec)}
}

func BottomLeft(spec Panel) Point {
	return Point{X: LeftX(spec), Y: BottomY(spec)}
}

func BottomRight(spec Panel) Point {
	return Point{X: RightX(spec), Y: BottomY(spec)}
}


<|start_filename|>pkg/eagle/utility.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package eagle

// BoardOutlineWires generates a rectangular outline out of wires, suitable
// for placing in Eagle's Dimension layer to create a board outline
func BoardOutlineWires(width, height float64, layer int) []Wire {
	return []Wire{
		// origin at bottom left.
		{X1: 0, Y1: height, X2: width, Y2: height, Width: 0, Layer: layer}, // top
		{X1: 0, Y1: 0, X2: width, Y2: 0, Width: 0, Layer: layer},           // bottom
		{X1: 0, Y1: 0, X2: 0, Y2: height, Width: 0, Layer: layer},          // left
		{X1: width, Y1: 0, X2: width, Y2: height, Width: 0, Layer: layer},  // right
	}
}


<|start_filename|>internal/boardops/standard/standard.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package standard

import (
	"github.com/jsleeio/go-eagle/internal/boardops"
	"github.com/jsleeio/go-eagle/internal/boardops/util"
	"github.com/jsleeio/go-eagle/pkg/eagle"
	"github.com/jsleeio/go-eagle/pkg/panel"
)

const (
	// CopperPullback indicates how far a copper pour will be "pulled
	// back" from the edge of a board. This shouldn't really need to
	// be configurable, so keep it simple and use a constant
	CopperPullback = 0.5
)

// ApplyStandardBoardOperations applies the minimal set of baseline
// board features to an Eagle board: an outline, some mounting holes
// and copper pours in the Top and Bottom copper layers.
func ApplyStandardBoardOperations(board *eagle.Eagle, spec panel.Panel) error {
	ops := []boardops.BoardOperation{
		outlineWiresOp,
		mountingHolesOp,
		copperFillOp,
		railKeepoutsOp,
	}
	return boardops.ApplyBoardOperations(board, spec, ops)
}

func outlineWiresOp(board *eagle.Eagle, spec panel.Panel) error {
	adjust := spec.HorizontalFit() / 2 // half on left edge, half on right edge
	outline := util.WireRectangle(
		0+adjust,
		0,
		spec.Width()-adjust,
		spec.Height(),
		board.LayerByName("Dimension"),
		0, // outline wires must be zero-width
		spec.CornerRadius(),
	)
	for _, wire := range outline {
		board.Board.Plain.Wires = append(board.Board.Plain.Wires, wire)
	}
	return nil
}

func mountingHolesOp(board *eagle.Eagle, spec panel.Panel) error {
	for _, hole := range spec.MountingHoles() {
		board.Board.Plain.Holes = append(board.Board.Plain.Holes, eagle.Hole{
			X:     hole.X,
			Y:     hole.Y,
			Drill: spec.MountingHoleDiameter(),
		})
	}
	return nil
}

func railKeepoutsOp(board *eagle.Eagle, spec panel.Panel) error {
	// format may not have rails.
	// FIXME: find a better way to do this now that custom formats are
	//        supported. Maybe add a new operation that creates keepouts
	//        around panel holes to account for mounting hole posts in
	//        typical off-the-shelf enclosures?
	if railheight := spec.RailHeightFromMountingHole(); railheight > 0 {
		layer := board.LayerByName("tKeepout")
		bRail := eagle.Rectangle{
			X1:    panel.LeftX(spec),
			Y1:    spec.MountingHoleBottomY(),
			X2:    panel.RightX(spec),
			Y2:    spec.MountingHoleBottomY() + spec.RailHeightFromMountingHole(),
			Layer: layer,
		}
		tRail := eagle.Rectangle{
			X1:    panel.LeftX(spec),
			Y1:    spec.MountingHoleTopY() - spec.RailHeightFromMountingHole(),
			X2:    panel.RightX(spec),
			Y2:    spec.MountingHoleTopY(),
			Layer: layer,
		}
		board.Board.Plain.Rectangles = append(board.Board.Plain.Rectangles, bRail)
		board.Board.Plain.Rectangles = append(board.Board.Plain.Rectangles, tRail)
	}
	return nil
}

func copperFillOp(board *eagle.Eagle, spec panel.Panel) error {
	// normally the horizontal fit value shouldn't be used anywhere except the
	// panel outline, but if we don't include it here, the pullback will be
	// wrong, or possibly even completely ineffective. So, include it.
	adjust := spec.HorizontalFit() / 2
	x1 := CopperPullback + adjust
	y1 := CopperPullback
	x2 := spec.Width() - (CopperPullback + adjust)
	y2 := spec.Height() - CopperPullback
	r := spec.CornerRadius()
	vertices := []eagle.Vertex{}
	if r < 0.01 { // effectively zero {
		vertices = append(vertices,
			eagle.Vertex{X: x1, Y: y1}, // bottom left,
			eagle.Vertex{X: x1, Y: y2}, // top left,
			eagle.Vertex{X: x2, Y: y2}, // top right,
			eagle.Vertex{X: x2, Y: y1}) // bottom right
	} else {
		vertices = append(vertices,
			eagle.Vertex{X: x1 + r, Y: y1, Curve: -90.0}, // bottom left corner radius start
			eagle.Vertex{X: x1, Y: y1 + r},               // bottom left corner radius end
			eagle.Vertex{X: x1, Y: y2 - r, Curve: -90.0}, // left edge edge end
			eagle.Vertex{X: x1 + r, Y: y2},               // top left corner radius end
			eagle.Vertex{X: x2 - r, Y: y2, Curve: -90.0}, // top edge end
			eagle.Vertex{X: x2, Y: y2 - r},               // top right corner radius end
			eagle.Vertex{X: x2, Y: y1 + r, Curve: -90.0}, // right edge end
			eagle.Vertex{X: x2 - r, Y: y1},               // bottom right corner radius end
			eagle.Vertex{X: x1 + r, Y: y1})               // bottom edge end
	}
	top := eagle.Polygon{
		Vertices: []eagle.Vertex{},
		Layer:    board.LayerByName("Top"),
	}
	top.Vertices = append(top.Vertices, vertices...) // copy to avoid later pass-by-reference traps
	bottom := eagle.Polygon{
		Vertices: []eagle.Vertex{},
		Layer:    board.LayerByName("Bottom"),
	}
	bottom.Vertices = append(bottom.Vertices, vertices...) // copy to avoid later pass-by-reference traps
	board.Board.Plain.Polygons = append(board.Board.Plain.Polygons, top)
	board.Board.Plain.Polygons = append(board.Board.Plain.Polygons, bottom)
	return nil
}


<|start_filename|>pkg/format/spec/spec.go<|end_filename|>
// Copyright 2020 <NAME> <<EMAIL>>
//
// 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.

package spec

import (
	"fmt"
	"io/ioutil"
	"sort"

	"gopkg.in/yaml.v2"

	"github.com/jsleeio/go-eagle/pkg/panel"
)

// Spec implements the panel.Panel interface and encapsulates the physical
// characteristics of a Spec panel
type Spec struct {
	SpecName                 string        `yaml:"name"`
	SpecWidth                float64       `yaml:"width"`
	SpecHeight               float64       `yaml:"height"`
	SpecMountingHoles        []panel.Point `yaml:"mountingHoles"`
	SpecMountingHoleDiameter float64       `yaml:"mountingHoleDiameter"`
	SpecHorizontalFit        float64       `yaml:"horizontalFit"`
	SpecCornerRadius         float64       `yaml:"cornerRadius"`
}

type PanelSpecError struct {
	s string
}

func (e *PanelSpecError) Error() string {
	return fmt.Sprintf("PanelSpecError: %s", e.s)
}

func NewPanelSpecError(s string) error {
	return &PanelSpecError{s: s}
}

// LoadSpec constructs a new Spec object according to a YAML file definition
func LoadSpec(filename string) (*Spec, error) {
	yamltext, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	var sp Spec
	if err := yaml.Unmarshal(yamltext, &sp); err != nil {
		return nil, err
	}
	if len(sp.SpecMountingHoles) < 1 {
		return nil, NewPanelSpecError("need at least one mounting hole")
	}
	sort.Slice(sp.SpecMountingHoles, func(i, j int) bool {
		return sp.SpecMountingHoles[i].Y < sp.SpecMountingHoles[j].Y
	})
	return &sp, nil
}

// Width returns the width of a Spec panel, in millimetres
func (s Spec) Width() float64 {
	return s.SpecWidth
}

// Height returns the height of a Spec panel, in millimetres
func (s Spec) Height() float64 {
	return s.SpecHeight
}

// MountingHoleDiameter returns the Spec system mounting hole size, in
// millimetres
func (s Spec) MountingHoleDiameter() float64 {
	return s.SpecMountingHoleDiameter
}

// MountingHoles generates a set of Point objects representing the mounting
// hole locations of a Spec panel
func (s Spec) MountingHoles() []panel.Point {
	return s.SpecMountingHoles
}

// HorizontalFit indicates the panel tolerance adjustment for the format
func (s Spec) HorizontalFit() float64 {
	return s.SpecHorizontalFit
}

// HorizontalFit indicates the corner radius for the format, as would be
// useful for snugly fitting jiffyboxes. Default is no radius.
func (s Spec) CornerRadius() float64 {
	return s.SpecCornerRadius
}

// RailHeightFromMountingHole doesn't really directly apply to YAML-spec
// panels where the keepout area is more likely to be a ring around each
// mounting hole, with the thickness of the ring likely varying with the
// mounting hole diameter. For now, keep it simple and return 0. The PCB
// designer will need to be careful regardless in order to fit their
// design within the given enclosure's envelope.
//
// FIXME: fix the interface design here to better facilitate non-system
//        panels (without making it unnecessarily awful!)
func (s Spec) RailHeightFromMountingHole() float64 {
	return 0.0
}

// MountingHoleTopY returns the Y coordinate for the top row of mounting
// holes
func (s Spec) MountingHoleTopY() float64 {
	return s.SpecMountingHoles[0].Y
}

// MountingHoleBottomY returns the Y coordinate for the bottom row of
// mounting holes
func (s Spec) MountingHoleBottomY() float64 {
	return s.SpecMountingHoles[len(s.SpecMountingHoles)-1].Y
}

// HeaderLocation returns the location of the header text. Spec panels
// may not have mounting rails so this is entirely arbitrary
func (s Spec) HeaderLocation() panel.Point {
	return panel.Point{X: s.Width() / 2, Y: s.MountingHoleTopY()}
}

// FooterLocation returns the location of the footer text. Spec panels
// may not have mounting rails so this is entirely arbitrary
func (s Spec) FooterLocation() panel.Point {
	return panel.Point{X: s.Width() / 2, Y: s.MountingHoleBottomY()}
}


<|start_filename|>internal/boardops/util/util.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package util

import (
	"github.com/jsleeio/go-eagle/pkg/eagle"
)

// WireRectangle generates a rectangle
func WireRectangle(x1, y1, x2, y2 float64, layer int, width float64, radius float64) []eagle.Wire {
	segments := []eagle.Wire{
		{X1: x1 + radius, Y1: y1, X2: x2 - radius, Y2: y1, Layer: layer, Width: width}, // bottom
		{X1: x1 + radius, Y1: y2, X2: x2 - radius, Y2: y2, Layer: layer, Width: width}, // top
		{X1: x1, Y1: y1 + radius, X2: x1, Y2: y2 - radius, Layer: layer, Width: width}, // left
		{X1: x2, Y1: y1 + radius, X2: x2, Y2: y2 - radius, Layer: layer, Width: width}, // right
	}
	if radius > 0.0 {
		segments = append(segments,
			eagle.Wire{Curve: -90.0, Layer: layer, X1: x1 + radius, Y1: y1, X2: x1, Y2: y1 + radius, Width: width}, // bottom-left
			eagle.Wire{Curve: -90.0, Layer: layer, X1: x1, Y1: y2 - radius, X2: x1 + radius, Y2: y2, Width: width}, // top-left
			eagle.Wire{Curve: -90.0, Layer: layer, X1: x2 - radius, Y1: y2, X2: x2, Y2: y2 - radius, Width: width}, // top-right
			eagle.Wire{Curve: -90.0, Layer: layer, X1: x2, Y1: y1 + radius, X2: x2 - radius, Y2: y1, Width: width}) // bottom-right
	}
	return segments
}


<|start_filename|>pkg/format/eurorack/eurorack.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.
package eurorack

import (
	"github.com/jsleeio/go-eagle/pkg/panel"
)

const (
	// PanelHeight3U represents the total height of a Eurorack panel. Note in
	// particular that this is NOT the same as the Eurocard standard, as the
	// latter does not use lipped rails
	PanelHeight3U = 128.5

	// ExtraMountingHolesThreshold represents the panel width threshold beyond
	// which additional mounting holes are required
	ExtraMountingHolesThreshold = 8

	// MountingHolesLeftOffset represents the distance of the first mounting
	// hole from the left edge of the panel
	MountingHolesLeftOffset = 7.5

	// MountingHoleTopY3U represents the Y value for the top row of 3U mounting
	// holes
	MountingHoleTopY3U = PanelHeight3U - 3.00

	// MountingHoleBottomY3U represents the Y value for the bottom row of 3U
	// mounting holes
	MountingHoleBottomY3U = 3.00

	// MountingHoleDiameter represents the diameter of a Eurorack system
	// mounting hole, in millimetres
	MountingHoleDiameter = 3.2

	// HP represents horizontal pitch in a Eurorack frame, in millimetres
	HP = 5.08

	// HorizontalFit indicates the panel tolerance adjustment for the format
	HorizontalFit = 0.25

	// CornerRadius indicates the corner radius for the format. Eurorack doesn't
	// believe in such things.
	CornerRadius = 0.0

	// RailHeightFromMountingHole is used to determine how much space exists.
	// See discussion in github.com/jsleeio/pkg/panel. 5mm is a good safe
	// figure for all known-used Eurorack rail types
	RailHeightFromMountingHole = 5.0
)

// Eurorack implements the panel.Panel interface and encapsulates the physical
// characteristics of a Eurorack panel
type Eurorack struct {
	HP int
}

// NewEurorack constructs a new Eurorack object
func NewEurorack(hp int) *Eurorack {
	return &Eurorack{HP: hp}
}

// Width returns the width of a Eurorack panel, in millimetres
func (e Eurorack) Width() float64 {
	return HP * float64(e.HP)
}

// Height returns the height of a Eurorack panel, in millimetres
func (e Eurorack) Height() float64 {
	return PanelHeight3U
}

// MountingHoleDiameter returns the Eurorack system mounting hole size, in
// millimetres
func (e Eurorack) MountingHoleDiameter() float64 {
	return MountingHoleDiameter
}

// MountingHoles generates a set of Point objects representing the mounting
// hole locations of a Eurorack panel
func (e Eurorack) MountingHoles() []panel.Point {
	holes := []panel.Point{
		{X: MountingHolesLeftOffset, Y: MountingHoleBottomY3U},
		{X: MountingHolesLeftOffset, Y: MountingHoleTopY3U},
	}
	if e.HP > ExtraMountingHolesThreshold {
		rhsx := MountingHolesLeftOffset + HP*(float64(e.HP-3))
		holes = append(holes, panel.Point{X: rhsx, Y: MountingHoleBottomY3U})
		holes = append(holes, panel.Point{X: rhsx, Y: MountingHoleTopY3U})
	}
	return holes
}

// HorizontalFit indicates the panel tolerance adjustment for the format
func (e Eurorack) HorizontalFit() float64 {
	return HorizontalFit
}

// CornerRadius indicates the corner radius for the format
func (e Eurorack) CornerRadius() float64 {
	return CornerRadius
}

// RailHeightFromMountingHole is used to calculate space between rails
func (e Eurorack) RailHeightFromMountingHole() float64 {
	return RailHeightFromMountingHole
}

// MountingHoleTopY returns the Y coordinate for the top row of mounting
// holes
func (e Eurorack) MountingHoleTopY() float64 {
	return MountingHoleTopY3U
}

// MountingHoleBottomY returns the Y coordinate for the bottom row of
// mounting holes
func (e Eurorack) MountingHoleBottomY() float64 {
	return MountingHoleBottomY3U
}

// HeaderLocation returns the location of the header text. Eurorack has
// mounting rails so this is typically aligned with the top mounting screw
func (e Eurorack) HeaderLocation() panel.Point {
	return panel.Point{X: e.Width() / 2, Y: e.MountingHoleTopY()}
}

// FooterLocation returns the location of the footer text. Eurorack has
// mounting rails so this is typically aligned with the bottom mounting screw
func (e Eurorack) FooterLocation() panel.Point {
	return panel.Point{X: e.Width() / 2, Y: e.MountingHoleBottomY()}
}


<|start_filename|>pkg/geometry/geometry.go<|end_filename|>
// Copyright 2020 <NAME> <<EMAIL>>
//
// 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.

package geometry

import "math"

// Point holds a Cartesian point and also an angle in degrees.
type RadialPoint struct {
	Angle float64
	X, Y  float64
}

// RadialPointGenerator generates a series of Cartesian points around a segment
// of a circle.
type RadialPointGenerator struct {
	// StartAngle and EndAngle indicate the span in degrees around the circle for
	// which points are generated. Zero degrees is at 9-o'clock and increases in
	// the clockwise direction.
	StartAngle, EndAngle float64
	// X and Y indicate the centre of the circle (origin)
	X, Y float64
	// Count indicates how many ticks are generated
	Count int
}

// GenerateAtRadius returns a set of evenly-distributed Cartesian points around
// a circle at a supplied radius.
func (rpg RadialPointGenerator) GenerateAtRadius(r float64) []RadialPoint {
	var points []RadialPoint
	interval := (rpg.EndAngle - rpg.StartAngle) / float64(rpg.Count-1)
	for i := 0; i < rpg.Count; i++ {
		angle := rpg.StartAngle + interval*float64(i)
		radians := angle * math.Pi / 180.0
		point := RadialPoint{
			Angle: angle,
			X:     rpg.X - r*math.Cos(radians),
			Y:     rpg.Y + r*math.Sin(radians),
		}
		points = append(points, point)
	}
	return points
}


<|start_filename|>pkg/eagle/attributes.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package eagle

import (
	"fmt"
	"strconv"
	"strings"
)

// AttributeCarrier allows applying attribute operations to any objects that
// contain attributes
type AttributeCarrier interface {
	GetAttributes() []Attribute
}

// AttributeString returns an attribute's string value, or if it isn't found,
// a provided default value. Whitespace is not trimmed as that would hinder
// supplying an attribute value of just whitespace (unlikely?)
func AttributeString(c AttributeCarrier, name string, def string) string {
	for _, attribute := range c.GetAttributes() {
		if attribute.Name == name {
			return attribute.Value
		}
	}
	return def
}

// AttributeFloat returns an attribute's numeric value as a float64, or if
// it isn't found, a provided default value.
func AttributeFloat(c AttributeCarrier, name string, def float64) (float64, error) {
	s := AttributeString(c, name, fmt.Sprint(def))
	f, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return 0.0, fmt.Errorf("unparseable numeric floating-point attribute value for %s: %v", name, err)
	}
	return f, nil
}

// AttributeBool returns an attribute's value as a boolean value, or if
// it isn't found, a provided default value. Valid values are "yes"/"true"
// or "no"/"false". Case insensitive.
func AttributeBool(c AttributeCarrier, name string, def bool) (bool, error) {
	s := strings.ToLower(strings.TrimSpace(AttributeString(c, name, fmt.Sprint(def))))
	switch {
	case s == "yes" || s == "true":
		return true, nil
	case s == "no" || s == "false":
		return false, nil
	default:
		return false, fmt.Errorf("unparseable boolean attribute value for %s: %q", name, s)
	}
}

// AttributeInt returns an attribute's numeric value as an int, or if
// it isn't found, a provided default value.
func AttributeInt(c AttributeCarrier, name string, def int) (int, error) {
	s := AttributeString(c, name, fmt.Sprint(def))
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("unparseable numeric integer attribute value for %s: %v", name, err)
	}
	return n, nil
}


<|start_filename|>Makefile<|end_filename|>
binaries: schroff panelgen

schroff:
	go build ./cmd/schroff

panelgen:
	go build ./cmd/panelgen

clean:
	$(RM) schroff panelgen


<|start_filename|>main.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package main

import (
	"flag"
	"fmt"
	"log"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/jsleeio/go-eagle/pkg/eagle"
	"github.com/jsleeio/go-eagle/pkg/format/eurorack"
	"github.com/jsleeio/go-eagle/pkg/format/intellijel"
	"github.com/jsleeio/go-eagle/pkg/format/pulplogic"
	filespec "github.com/jsleeio/go-eagle/pkg/format/spec"
	"github.com/jsleeio/go-eagle/pkg/geometry"
	"github.com/jsleeio/go-eagle/pkg/panel"

	"github.com/jsleeio/go-eagle/internal/boardops/standard"
	"github.com/jsleeio/go-eagle/internal/outline"
)

const (
	// FormatEurorack is the Doepfer-defined 3U specification. Not Eurocard!
	FormatEurorack = "eurorack"
	// FormatPulplogic is the PulpLogic-defined 1U specification
	FormatPulplogic = "pulplogic"
	// FormatIntellijel is the Intellijel-defined 1U specification
	FormatIntellijel = "intellijel"
	// FormatSpec is the YAML-derived panel specification
	FormatSpec = "spec"
)

// wrap up all of the context required for creating panel features
// into one place to simplify and reduce error
type panelLayoutContext struct {
	format string
	bc     outline.BoardCoords
	board  *eagle.Eagle
	panel  *eagle.Eagle
	cfg    config
	spec   panel.Panel
	// legendSkipRe is pulled from the board global attribute PANEL_LEGEND_SKIP_RE.
	// If a component name matches this regexp, it will NOT have a panel legend
	// text object created.
	legendSkipRe *regexp.Regexp
	legendLayer  string
	headerLayer  string
	footerLayer  string
}

func (plc *panelLayoutContext) panelSpecForFormat() (err error) {
	err = nil
	switch *plc.cfg.Format {
	case FormatEurorack:
		plc.spec = eurorack.NewEurorack(plc.bc.HP)
	case FormatPulplogic:
		plc.spec = pulplogic.NewPulplogic(plc.bc.HP)
	case FormatIntellijel:
		plc.spec = intellijel.NewIntellijel(plc.bc.HP)
	case FormatSpec:
		plc.spec, err = filespec.LoadSpec(*plc.cfg.SpecFile)
		if err != nil {
			err = fmt.Errorf("error loading YAML panel spec from '%v': %v", *plc.cfg.SpecFile, err)
		}
	default:
		err = fmt.Errorf("unsupported format: %s", *plc.cfg.Format)
	}
	return
}

func setupPanelLayoutContext(board *eagle.Eagle, c config) (panelLayoutContext, error) {
	plc := panelLayoutContext{
		cfg:          c,
		board:        board,
		bc:           outline.DeriveBoardCoords(board),
		legendLayer:  eagle.AttributeString(board.Board, "PANEL_LEGEND_LAYER", "tStop"),
		headerLayer:  eagle.AttributeString(board.Board, "PANEL_HEADER_LAYER", "tStop"),
		footerLayer:  eagle.AttributeString(board.Board, "PANEL_FOOTER_LAYER", "tStop"),
		legendSkipRe: nil,
	}
	if lsre := eagle.AttributeString(board.Board, "PANEL_LEGEND_SKIP_RE", ""); lsre != "" {
		plc.legendSkipRe = regexp.MustCompile(lsre)
	}
	err := plc.panelSpecForFormat()
	if err != nil {
		return panelLayoutContext{}, err
	}
	plc.panel = plc.board.CloneEmpty()
	if err := standard.ApplyStandardBoardOperations(plc.panel, plc.spec); err != nil {
		return panelLayoutContext{}, fmt.Errorf("error creating panel features: %v", err)
	}
	// centre the board on the panel
	plc.bc.XOffset += (plc.spec.Width()-plc.bc.Width())/2 + plc.spec.HorizontalFit()/2
	plc.bc.YOffset += (plc.spec.Height() - plc.bc.Height()) / 2
	return plc, nil
}

func headerOp(plc panelLayoutContext) {
	offsets := make(map[string]float64)
	offsets["PANEL_HEADER_OFFSET_X"] = 0.0
	offsets["PANEL_HEADER_OFFSET_Y"] = 0.0
	offsets["PANEL_FOOTER_OFFSET_X"] = 0.0
	offsets["PANEL_FOOTER_OFFSET_Y"] = 0.0
	for k, defval := range offsets {
		if v, err := eagle.AttributeFloat(plc.board.Board, k, defval); err == nil {
			offsets[k] = v
		} else {
			log.Fatalf("invalid global attribute numeric value: %s: %v", err)
		}
	}
	// add the header and footer
	headerloc := plc.spec.HeaderLocation()
	header := eagle.Text{
		X:     headerloc.X + offsets["PANEL_HEADER_OFFSET_X"],
		Y:     headerloc.Y + offsets["PANEL_HEADER_OFFSET_Y"],
		Align: "center",
		Size:  3.0,
		Text:  eagle.AttributeString(plc.board.Board, "PANEL_HEADER_TEXT", "<HEADER>"),
		Layer: plc.panel.LayerByName(plc.headerLayer),
	}
	footerloc := plc.spec.FooterLocation()
	plc.panel.Board.Plain.Texts = append(plc.panel.Board.Plain.Texts, header)
	footer := eagle.Text{
		X:     footerloc.X + offsets["PANEL_FOOTER_OFFSET_X"],
		Y:     footerloc.Y + offsets["PANEL_FOOTER_OFFSET_Y"],
		Align: "center",
		Size:  3.0,
		Text:  eagle.AttributeString(plc.board.Board, "PANEL_FOOTER_TEXT", "<FOOTER>"),
		Layer: plc.panel.LayerByName(plc.footerLayer),
	}
	plc.panel.Board.Plain.Texts = append(plc.panel.Board.Plain.Texts, footer)
}

type elementConfig struct {
	legendOffsetX, legendOffsetY   float64
	legendLocationFactor           float64
	legendAlign                    string
	legend                         string
	ticks, ticksLabels             bool
	ticksStartAngle, ticksEndAngle float64
	ticksLength, ticksWidth        float64
	ticksCount                     int
	ticksLabelsTexts               []string
}

// extract all the per-element config into a nice structure. Later this should help
// with refactoring the currently-ugly elementOp() into a bunch of separate operations
func elementConfigFromElement(elem eagle.Element) (elementConfig, error) {
	var err error
	ec := elementConfig{
		legend:           eagle.AttributeString(elem, "PANEL_LEGEND", elem.Name),
		ticksLabelsTexts: strings.Split(eagle.AttributeString(elem, "PANEL_LEGEND_TICKS_LABELS_TEXTS", ""), ","),
	}
	if ec.legendOffsetX, err = eagle.AttributeFloat(elem, "PANEL_LEGEND_OFFSET_X", 0.0); err != nil {
		return ec, err
	}
	if ec.legendOffsetY, err = eagle.AttributeFloat(elem, "PANEL_LEGEND_OFFSET_Y", 0.0); err != nil {
		return ec, err
	}
	legendlocation := eagle.AttributeString(elem, "PANEL_LEGEND_LOCATION", "above")
	switch legendlocation {
	case "above":
		ec.legendLocationFactor = 1
		ec.legendAlign = "bottom-center"
	case "below":
		ec.legendLocationFactor = -1
		ec.legendAlign = "top-center"
	default:
		return ec, fmt.Errorf("invalid value %q for attribute PANEL_LEGEND_LOCATION on object %q: must be 'above' or 'below'", legendlocation, elem.Name)
	}
	if ec.ticks, err = eagle.AttributeBool(elem, "PANEL_LEGEND_TICKS", false); err != nil {
		return ec, err
	}
	if ec.ticksLabels, err = eagle.AttributeBool(elem, "PANEL_LEGEND_TICKS_LABELS", false); err != nil {
		return ec, err
	}
	if ec.ticksLength, err = eagle.AttributeFloat(elem, "PANEL_LEGEND_TICKS_LENGTH", 1.5); err != nil {
		return ec, err
	}
	if ec.ticksWidth, err = eagle.AttributeFloat(elem, "PANEL_LEGEND_TICKS_WIDTH", 0.25); err != nil {
		return ec, err
	}
	// default values for start and end angles suit a typical single-turn potentiometer
	// with a 300-degree rotation, like Alpha 9mm vertical pots
	// https://www.thonk.co.uk/documents/alpha/9mm/Alpha%209mm%20Vertical%20-%20Linear%20Taper%20B1K-B500K.pdf
	if ec.ticksStartAngle, err = eagle.AttributeFloat(elem, "PANEL_LEGEND_TICKS_START_ANGLE", -60.0); err != nil {
		return ec, err
	}
	if ec.ticksEndAngle, err = eagle.AttributeFloat(elem, "PANEL_LEGEND_TICKS_END_ANGLE", 240.0); err != nil {
		return ec, err
	}
	// everything should go up to (at least) 11
	if ec.ticksCount, err = eagle.AttributeInt(elem, "PANEL_LEGEND_TICKS_COUNT", 11); err != nil {
		return ec, err
	}
	if ec.ticksLabels && len(ec.ticksLabelsTexts) != ec.ticksCount {
		return ec, fmt.Errorf("incorrect number of tick labels provided for object %q: ticks = %v, labels = %v", elem.Name, ec.ticksCount, len(ec.ticksLabelsTexts))
	}
	log.Printf("element config for %s: %+v", elem.Name, ec)
	return ec, nil
}

func elementOp(plc panelLayoutContext, elem eagle.Element) {
	hole, needHole, err := holeForPanelElement(elem)
	if err != nil {
		log.Fatalf("can't find drill size for element %q: %v", elem.Name, err)
	}
	if !needHole {
		return
	}
	// derive the per-element config
	elementConfig, err := elementConfigFromElement(elem)
	if err != nil {
		log.Fatalf("error extracting per-element config from attributes: %v", err)
	}
	// the hole was generated with coordinates from the source board, now
	// adjust them to be in the right place on the panel
	tstop := plc.panel.LayerByName("tStop")
	hole.X += plc.bc.XOffset
	hole.Y += plc.bc.YOffset
	plc.panel.Board.Plain.Holes = append(plc.panel.Board.Plain.Holes, hole)
	text := eagle.Text{
		X:     hole.X + elementConfig.legendOffsetX,
		Y:     hole.Y + ((elementConfig.legendOffsetY + (hole.Drill / 2.0) + *plc.cfg.TextSpacing) * elementConfig.legendLocationFactor),
		Size:  *plc.cfg.TextSize,
		Layer: plc.panel.LayerByName(plc.legendLayer),
		Text:  elementConfig.legend,
		Align: elementConfig.legendAlign,
		Font:  "vector",
	}
	if text.Text != "" && (plc.legendSkipRe == nil || !plc.legendSkipRe.MatchString(elem.Name)) {
		plc.panel.Board.Plain.Texts = append(plc.panel.Board.Plain.Texts, text)
	} else {
		log.Printf("%s: skipping legend\n", elem.Name)
	}
	hsw, err := eagle.AttributeFloat(elem, "PANEL_HOLE_STOP_WIDTH", *plc.cfg.HoleStopRadius)
	if err != nil {
		log.Fatal(err)
	}
	stop := eagle.Circle{
		X: hole.X, Y: hole.Y,
		Radius: hole.Drill / 2.0,
		Width:  hsw,
		Layer:  tstop,
	}
	plc.panel.Board.Plain.Circles = append(plc.panel.Board.Plain.Circles, stop)
	if elementConfig.ticks {
		rpg := geometry.RadialPointGenerator{
			X: hole.X, Y: hole.Y,
			StartAngle: elementConfig.ticksStartAngle,
			EndAngle:   elementConfig.ticksEndAngle,
			Count:      elementConfig.ticksCount,
		}
		tickstarts := rpg.GenerateAtRadius(hole.Drill/2.0 + *plc.cfg.HoleStopRadius)
		tickends := rpg.GenerateAtRadius(hole.Drill/2.0 + *plc.cfg.HoleStopRadius + elementConfig.ticksLength)
		textorigins := rpg.GenerateAtRadius(hole.Drill/2.0 + *plc.cfg.HoleStopRadius + elementConfig.ticksLength + 2.0)
		for index, inner := range tickstarts {
			plc.panel.Board.Plain.Wires = append(plc.panel.Board.Plain.Wires, eagle.Wire{
				X1: inner.X, Y1: inner.Y,
				X2: tickends[index].X, Y2: tickends[index].Y,
				Width: elementConfig.ticksWidth,
				Layer: tstop,
			})
			if elementConfig.ticksLabels {
				plc.panel.Board.Plain.Texts = append(plc.panel.Board.Plain.Texts, eagle.Text{
					X: textorigins[index].X, Y: textorigins[index].Y,
					Align: "center",
					Size:  1.5,
					Text:  strings.TrimSpace(elementConfig.ticksLabelsTexts[index]),
					Layer: tstop,
				})
			}
		}
	}
}

// generate a panel hole for a single element, if necessary
func holeForPanelElement(elem eagle.Element) (eagle.Hole, bool, error) {
	hole := eagle.Hole{X: elem.X, Y: elem.Y}
	// negative default drill size => no drill unless PANEL_DRILL_MM present
	drillmm, err := eagle.AttributeFloat(elem, "PANEL_DRILL_MM", -1.0)
	if err != nil {
		return eagle.Hole{}, false, err
	}
	if drillmm < 0.0 { // no drill size found, do nothing
		return eagle.Hole{}, false, nil
	}
	hole.Drill = drillmm
	log.Printf("%s: found PANEL_DRILL_MM attribute with value %v", elem.Name, drillmm)
	return hole, true, nil
}

type config struct {
	Format         *string
	TextSpacing    *float64
	TextSize       *float64
	HoleStopRadius *float64
	SpecFile       *string
}

func configureFromFlags() config {
	formatList := "(" + strings.Join([]string{FormatEurorack, FormatPulplogic, FormatIntellijel, FormatSpec}, ",") + ")"
	cfg := config{
		Format:         flag.String("format", FormatEurorack, "panel format to create "+formatList),
		TextSpacing:    flag.Float64("text-spacing", 3.5, "spacing between a hole and its related label"),
		TextSize:       flag.Float64("text-size", 2.25, "label text size"),
		HoleStopRadius: flag.Float64("hole-stop-radius", 2.0, "Radius to pull back soldermask around a hole"),
		SpecFile:       flag.String("spec-file", "", "filename to read YAML panel spec from"),
	}
	flag.Parse()
	return cfg
}

func main() {
	config := configureFromFlags()
	for _, filename := range flag.Args() {
		board, err := eagle.LoadEagleFile(filename)
		if err != nil {
			log.Fatalf("can't load input file %q: %v", filename, err)
		}
		plc, err := setupPanelLayoutContext(board, config)
		if err != nil {
			log.Fatalf("can't setup panel layout context: %v", err)
		}
		headerOp(plc)
		for _, elem := range plc.board.Board.Elements {
			elementOp(plc, elem)
		}
		outFilename := filepath.Base(filename) + ".panel.brd"
		if err := plc.panel.WriteFile(outFilename); err != nil {
			log.Fatalf("can't write output file %q: %v", outFilename, err)
		}
	}
}


<|start_filename|>pkg/eagle/types.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package eagle

// Grid object
type Grid struct {
	Distance        float64 `xml:"distance,attr"`
	UnitDistance    string  `xml:"unitdist,attr"`
	Unit            string  `xml:"unit,attr"`
	Style           string  `xml:"style,attr"`
	Multiple        float64 `xml:"multiple,attr"`
	Display         string  `xml:"display,attr"`
	AltDistance     float64 `xml:"altdistance,attr"`
	AltUnitDistance string  `xml:"altunitdist,attr"`
	AltUnit         string  `xml:"altunit,attr"`
}

// Layer object
type Layer struct {
	Number  int    `xml:"number,attr"`
	Name    string `xml:"name,attr"`
	Color   int    `xml:"color,attr"`
	Fill    int    `xml:"fill,attr"`
	Visible string `xml:"visible,attr"`
	Active  string `xml:"active,attr"`
}

// Wire object
type Wire struct {
	X1    float64 `xml:"x1,attr"`
	Y1    float64 `xml:"y1,attr"`
	X2    float64 `xml:"x2,attr"`
	Y2    float64 `xml:"y2,attr"`
	Width float64 `xml:"width,attr"`
	Layer int     `xml:"layer,attr"`
	Style string  `xml:"style,attr,omitempty"`
	Cap   string  `xml:"cap,attr,omitempty"`
	Curve float64 `xml:"curve,attr,omitempty"`
}

// Rectangle object
type Rectangle struct {
	X1     float64 `xml:"x1,attr"`
	Y1     float64 `xml:"y1,attr"`
	X2     float64 `xml:"x2,attr"`
	Y2     float64 `xml:"y2,attr"`
	Layer  int     `xml:"layer,attr"`
	Rotate string  `xml:"rot,attr,omitempty"`
}

// Vertex object, used only in Polygon
type Vertex struct {
	X     float64 `xml:"x,attr"`
	Y     float64 `xml:"y,attr"`
	Curve float64 `xml:"curve,attr,omitempty"`
}

// Polygon object
type Polygon struct {
	Vertices []Vertex `xml:"vertex"`
	Isolate  string   `xml:"isolate,omitempty"`
	Pour     string   `xml:"pour,omitempty"`
	Orphans  string   `xml:"orphans,omitempty"`
	Layer    int      `xml:"layer,attr"`
	Rank     int      `xml:"rank,attr,omitempty"`
	Spacing  string   `xml:"spacing,attr,omitempty"`
	Thermals string   `xml:"thermals,attr,omitempty"`
	Width    float64  `xml:"width,attr"`
}

// Hole object
type Hole struct {
	X     float64 `xml:"x,attr"`
	Y     float64 `xml:"y,attr"`
	Drill float64 `xml:"drill,attr"`
}

// Circle object
type Circle struct {
	X      float64 `xml:"x,attr"`
	Y      float64 `xml:"y,attr"`
	Radius float64 `xml:"radius,attr"`
	Width  float64 `xml:"width,attr"`
	Layer  int     `xml:"layer,attr"`
}

// Pad object
type Pad struct {
	Name   string  `xml:"name,attr"`
	X      float64 `xml:"x,attr"`
	Y      float64 `xml:"y,attr"`
	Drill  float64 `xml:"drill,attr"`
	Shape  string  `xml:"shape,attr,omitempty"`
	Rotate string  `xml:"rot,attr,omitempty"`
}

// Text object
type Text struct {
	Text     string  `xml:",chardata"`
	X        float64 `xml:"x,attr"`
	Y        float64 `xml:"y,attr"`
	Size     float64 `xml:"size,attr"`
	Layer    int     `xml:"layer,attr"`
	Ratio    int     `xml:"ratio,attr,omitempty"`
	Font     string  `xml:"font,attr,omitempty"`
	Align    string  `xml:"align,attr,omitempty"`
	Distance float64 `xml:"distance,attr,omitempty"`
	Rotate   string  `xml:"rot,attr,omitempty"`
}

// Package object
type Package struct {
	Name        string      `xml:"name,attr"`
	Urn         string      `xml:"urn,attr,omitempty"`
	Version     string      `xml:"library_version,attr,omitempty"`
	Description string      `xml:"description"`
	Pads        []Pad       `xml:"pad"`
	Rectangles  []Rectangle `xml:"rectangle"`
	Circles     []Circle    `xml:"circle"`
	Texts       []Text      `xml:"text"`
	Wires       []Wire      `xml:"wire"`
}

// PackageInstance object
// FIXME: should be able to eliminate this and directly include in
// Package3D object via a slice
type PackageInstance struct {
	Name string `xml:"name,attr"`
}

// Package3D object
type Package3D struct {
	Name        string            `xml:"name,attr"`
	Urn         string            `xml:"urn,attr,omitempty"`
	Type        string            `xml:"type,attr"`
	Version     string            `xml:"library_version,attr,omitempty"`
	Description string            `xml:"description"`
	Instances   []PackageInstance `xml:"packageinstances>packageinstance"`
}

// Library object
type Library struct {
	Name        string    `xml:"name,attr"`
	Urn         string    `xml:"urn,attr,omitempty"`
	Description string    `xml:"description"`
	Packages    []Package `xml:"packages>package"`
}

// Attribute object
type Attribute struct {
	Name     string  `xml:"name,attr"`
	Value    string  `xml:"value,attr"`
	X        float64 `xml:"x,attr,omitempty"`
	Y        float64 `xml:"y,attr,omitempty"`
	Size     float64 `xml:"size,attr,omitempty"`
	Display  string  `xml:"display,attr,omitempty"`
	Constant string  `xml:"constant,attr,omitempty"`
	Font     string  `xml:"font,attr,omitempty"`
	Ratio    int     `xml:"ratio,attr,omitempty"`
	Rotate   string  `xml:"rot,attr,omitempty"`
}

// Element object
type Element struct {
	Name         string      `xml:"name,attr"`
	Value        string      `xml:"value,attr"`
	X            float64     `xml:"x,attr"`
	Y            float64     `xml:"y,attr"`
	Library      string      `xml:"library,attr"`
	LibraryUrn   string      `xml:"library_urn,attr,omitempty"`
	Package      string      `xml:"package,attr"`
	Package3dUrn string      `xml:"package3d_urn,attr,omitempty"`
	Smashed      string      `xml:"smashed,attr"`
	Rotate       string      `xml:"rot,attr,omitempty"`
	Attributes   []Attribute `xml:"attribute"`
}

func (e Element) GetAttributes() []Attribute {
	return e.Attributes
}

// Plain object
type Plain struct {
	Holes      []Hole      `xml:"hole"`
	Circles    []Circle    `xml:"circle"`
	Rectangles []Rectangle `xml:"rectangle"`
	Polygons   []Polygon   `xml:"polygon"`
	Texts      []Text      `xml:"text"`
	Wires      []Wire      `xml:"wire"`
}

// NewPlain constructs a new Plain object with empty slices; this
// should be substantially more convenient to use.
func NewPlain() Plain {
	return Plain{
		Holes:      []Hole{},
		Circles:    []Circle{},
		Rectangles: []Rectangle{},
		Polygons:   []Polygon{},
		Texts:      []Text{},
		Wires:      []Wire{},
	}
}

// Board object
type Board struct {
	Libraries  []Library   `xml:"libraries>library"`
	Elements   []Element   `xml:"elements>element"`
	Plain      Plain       `xml:"plain"`
	Attributes []Attribute `xml:"attributes>attribute"`
}

func (b Board) GetAttributes() []Attribute {
	return b.Attributes
}

// NewBoard constructs a new empty Board object.
func NewBoard() Board {
	return Board{
		Libraries:  []Library{},
		Elements:   []Element{},
		Attributes: []Attribute{},
		Plain:      NewPlain(),
	}
}

// Eagle object
type Eagle struct {
	Version string  `xml:"version,attr"`
	Grid    Grid    `xml:"drawing>grid"`
	Layers  []Layer `xml:"drawing>layers>layer"`
	Board   Board   `xml:"drawing>board"`
}


<|start_filename|>pkg/format/pulplogic/pulplogic.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package pulplogic

import (
	"github.com/jsleeio/go-eagle/pkg/format/eurorack"
	"github.com/jsleeio/go-eagle/pkg/panel"
)

// based on http://pulplogic.com/1u_tiles/

const (
	inch = 25.4

	// PanelHeight1U represents the total height of a Pulplogic panel, in
	// millimetres
	PanelHeight1U = 1.70 * inch

	// MountingHolesLeftOffset represents the distance of the first mounting
	// hole from the left edge of the panel, in millimetres
	MountingHolesLeftOffset = 0.2 * inch

	// MountingHolesRightOffset represents the distance of the first mounting
	// hole from the right edge of the panel, in millimetres
	MountingHolesRightOffset = 0.2 * inch

	// MountingHoleTopY1U represents the Y value for the top row of 1U mounting
	// holes, in millimetres
	MountingHoleTopY1U = PanelHeight1U - (0.118 * inch)

	// MountingHoleBottomY1U represents the Y value for the bottom row of 1U
	// mounting holes, in millimetres
	MountingHoleBottomY1U = 0.118 * inch

	// MountingHoleDiameter represents the diameter of a Eurorack system
	// mounting hole, in millimetres
	MountingHoleDiameter = 0.125 * inch

	// HP represents horizontal pitch in a Eurorack frame, in millimetres
	HP = eurorack.HP

	// HorizontalFit indicates the panel tolerance adjustment for the format
	HorizontalFit = eurorack.HorizontalFit

	// CornerRadius indicates the corner radius for the format. Eurorack doesn't
	// believe in such things.
	CornerRadius = 0.0

	// RailHeightFromMountingHole is used to determine how much space exists.
	// See discussion in github.com/jsleeio/pkg/panel.
	//
	// NOT using the Eurorack figure here; see the Pulplogic spec. Instead we use
	// half the height of Vector T-strut rails. With this measurement, the
	// Pulplogic-recommended maximum PCB size (1.130") will fit between a pair of
	// keepout areas extending this distance beyond the mounting hole centres.
	RailHeightFromMountingHole = (0.291 / 2.0) * inch
)

// Pulplogic implements the panel.Panel interface and encapsulates the physical
// characteristics of a Pulplogic panel
type Pulplogic struct {
	HP int
}

// NewPulplogic constructs a new Pulplogic object
func NewPulplogic(hp int) *Pulplogic {
	return &Pulplogic{HP: hp}
}

// Width returns the width of a Pulplogic panel, in millimetres
func (p Pulplogic) Width() float64 {
	return HP * float64(p.HP)
}

// Height returns the height of a Pulplogic panel, in millimetres
func (p Pulplogic) Height() float64 {
	return PanelHeight1U
}

// MountingHoleDiameter returns thp Pulplogic system mounting hole size, in
// millimetres
func (p Pulplogic) MountingHoleDiameter() float64 {
	return MountingHoleDiameter
}

// MountingHoles generates a set of Point objects representing the mounting
// hole locations of a Pulplogic panel
func (p Pulplogic) MountingHoles() []panel.Point {
	holes := []panel.Point{
		{X: MountingHolesLeftOffset, Y: MountingHoleBottomY1U},
		{X: MountingHolesLeftOffset, Y: MountingHoleTopY1U},
		{X: p.Width() - MountingHolesRightOffset, Y: MountingHoleBottomY1U},
		{X: p.Width() - MountingHolesRightOffset, Y: MountingHoleTopY1U},
	}
	return holes
}

// HorizontalFit indicates the panel tolerance adjustment for the format
func (p Pulplogic) HorizontalFit() float64 {
	return HorizontalFit
}

// CornerRadius indicates the corner radius for the format
func (p Pulplogic) CornerRadius() float64 {
	return CornerRadius
}

// RailHeightFromMountingHole is used to calculate space between rails
func (p Pulplogic) RailHeightFromMountingHole() float64 {
	return RailHeightFromMountingHole
}

// MountingHoleTopY returns the Y coordinate for the top row of mounting
// holes
func (p Pulplogic) MountingHoleTopY() float64 {
	return MountingHoleTopY1U
}

// MountingHoleBottomY returns the Y coordinate for the bottom row of
// mounting holes
func (p Pulplogic) MountingHoleBottomY() float64 {
	return MountingHoleBottomY1U
}

// HeaderLocation returns the location of the header text. Pulplogic 1U has
// mounting rails so this is typically aligned with the top mounting screw
func (p Pulplogic) HeaderLocation() panel.Point {
	return panel.Point{X: p.Width() / 2.0, Y: p.MountingHoleTopY()}
}

// FooterLocation returns the location of the footer text. Pulplogic 1U has
// mounting rails so this is typically aligned with the bottom mounting screw
func (p Pulplogic) FooterLocation() panel.Point {
	return panel.Point{X: p.Width() / 2.0, Y: p.MountingHoleBottomY()}
}


<|start_filename|>cmd/panelgen/panelgen.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package main

import (
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/jsleeio/go-eagle/pkg/eagle"
	"github.com/jsleeio/go-eagle/pkg/format/eurorack"
	"github.com/jsleeio/go-eagle/pkg/format/intellijel"
	"github.com/jsleeio/go-eagle/pkg/format/pulplogic"
	filespec "github.com/jsleeio/go-eagle/pkg/format/spec"
	"github.com/jsleeio/go-eagle/pkg/panel"

	"github.com/jsleeio/go-eagle/internal/boardops/standard"
)

const (
	// FormatEurorack is the Doepfer-defined 3U specification. Not Eurocard!
	FormatEurorack = "eurorack"
	// FormatPulplogic is the PulpLogic-defined 1U specification
	FormatPulplogic = "pulplogic"
	// FormatIntellijel is the Intellijel-defined 1U specification
	FormatIntellijel = "intellijel"
	// FormatSpec is the YAML-derived panel specification
	FormatSpec = "spec"
)

type config struct {
	Width        *int
	Format       *string
	Output       *string
	RefBoard     *string
	OutlineLayer *string
	SpecFile     *string
}

func configureFromFlags() (*config, error) {
	formatList := "(" + strings.Join([]string{FormatEurorack, FormatPulplogic, FormatIntellijel, FormatSpec}, ",") + ")"
	c := &config{
		Width:        flag.Int("width", 4, "width of the panel, in integer units appropriate for the format"),
		Format:       flag.String("format", FormatEurorack, "panel format to create "+formatList),
		RefBoard:     flag.String("reference-board", "", "reference Eagle board file to read layer information from"),
		Output:       flag.String("output", "newpanel.brd", "filename to write new Eagle board file to"),
		OutlineLayer: flag.String("outline-layer", "Dimension", "layer to draw board outline in"),
		SpecFile:     flag.String("spec-file", "", "filename to read YAML panel spec from"),
	}
	flag.Parse()
	if *c.RefBoard == "" {
		return nil, fmt.Errorf("a reference board file (-reference-board option) is required to acquire a list of Eagle layers")
	}
	return c, nil
}

func generatePanelBoardFile(cfg *config, spec panel.Panel) error {
	// the user very likely already has an Eagle board file nearby, so use it to
	// acquire a list of layers --- avoids hardcoding them, lets users use their
	// own mix/subset of layers if desired
	ref, err := eagle.LoadEagleFile(*cfg.RefBoard)
	if err != nil {
		return fmt.Errorf("can't load reference board: %v", err)
	}
	panel := ref.CloneEmpty()
	if err := standard.ApplyStandardBoardOperations(panel, spec); err != nil {
		return fmt.Errorf("error creating panel features: %v", err)
	}
	if err := panel.WriteFile(*cfg.Output); err != nil {
		return fmt.Errorf("can't write output board: %v", err)
	}
	return nil
}

func main() {
	cfg, err := configureFromFlags()
	if err != nil {
		fmt.Printf("configuration error: %v\n", err)
		os.Exit(1)
	}
	var spec panel.Panel
	switch *cfg.Format {
	case FormatEurorack:
		spec = eurorack.NewEurorack(*cfg.Width)
	case FormatPulplogic:
		spec = pulplogic.NewPulplogic(*cfg.Width)
	case FormatIntellijel:
		spec = intellijel.NewIntellijel(*cfg.Width)
	case FormatSpec:
		spec, err = filespec.LoadSpec(*cfg.SpecFile)
		if err != nil {
			fmt.Printf("error loading YAML panel spec from '%v': %v", *cfg.SpecFile, err)
			os.Exit(1)
		}
	default:
		fmt.Printf("unsupported format: %s\n", *cfg.Format)
		os.Exit(3)
	}
	if err := generatePanelBoardFile(cfg, spec); err != nil {
		fmt.Printf("error generating panel: %v\n", err)
		os.Exit(2)
	}
}


<|start_filename|>internal/outline/outline.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package outline

import (
	"math"

	"github.com/jsleeio/go-eagle/pkg/eagle"
)

// FindBoardOutlineWires searches through Wires in the Plain section of
// the board for zero-width wires in the Dimension layer
func FindBoardOutlineWires(e *eagle.Eagle) []eagle.Wire {
	wires := []eagle.Wire{}
	dimension := e.LayerByName("Dimension")
	for _, wire := range e.Board.Plain.Wires {
		if wire.Layer == dimension && wire.Width == 0.0 {
			wires = append(wires, wire)
		}
	}
	return wires
}

// BoardCoords holds information about a board outline and its place in
// the coordinate space. This is used to determine panel width and to
// correctly align the board with the panel
type BoardCoords struct {
	XMin, XMax, YMin, YMax float64
	XOffset, YOffset       float64
	HP                     int
}

// Width returns the width of the board outline
func (bc BoardCoords) Width() float64 {
	return bc.XMax - bc.XMin
}

// Height returns the height of the board outline
func (bc BoardCoords) Height() float64 {
	return bc.YMax - bc.YMin
}

// DeriveBoardCoords creates a BoardCoords object from the discovered outline
// wires in the Plain section of a board
func DeriveBoardCoords(e *eagle.Eagle) BoardCoords {
	bc := BoardCoords{}
	for _, wire := range FindBoardOutlineWires(e) {
		txmin, txmax := fsort2(wire.X1, wire.X2)
		tymin, tymax := fsort2(wire.Y1, wire.Y2)
		bc.XMin = math.Min(bc.XMin, txmin)
		bc.XMax = math.Max(bc.XMax, txmax)
		bc.YMin = math.Min(bc.YMin, tymin)
		bc.YMax = math.Max(bc.YMax, tymax)
	}
	if bc.XMin != 0 {
		bc.XOffset = -bc.XMin
	}
	if bc.YMin != 0 {
		bc.YOffset = -bc.YMin
	}
	bc.HP = int(math.Ceil(math.Ceil(bc.XMax-bc.XMin) / 5.08))
	return bc
}

func fsort2(a, b float64) (float64, float64) {
	if a > b {
		return b, a
	}
	return a, b
}


<|start_filename|>pkg/eagle/eagle.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package eagle

import (
	"encoding/xml"
	"io/ioutil"
	"log"
	"os"
)

// LoadEagleFile attempts to read and unmarshal an Eagle XML file.
func LoadEagleFile(filename string) (*Eagle, error) {
	xmlText, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	var eagle Eagle
	if err := xml.Unmarshal(xmlText, &eagle); err != nil {
		return nil, err
	}
	return &eagle, nil
}

// WriteFile attempts to generate a valid Eagle XML board file from an
// Eagle data structure.
func (e *Eagle) WriteFile(filename string) error {
	wrapped := &struct {
		Eagle
		XMLName struct{} `xml:"eagle"`
	}{Eagle: *e}
	xml, err := xml.MarshalIndent(wrapped, "", "  ")
	if err != nil {
		return err
	}
	file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	defer file.Close()
	header := "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
	header += "<!DOCTYPE eagle SYSTEM \"eagle.dtd\">\n"
	if _, err := file.WriteString(header); err != nil {
		return err
	}
	if _, err := file.Write(xml); err != nil {
		return err
	}
	return nil
}

// CloneEmpty creates an empty shell based on an existing Eagle object, copying
// layer and grid definitions, and the Eagle version, but not copying any board
// elements, board outline, DRC, signals or libraries.
func (e *Eagle) CloneEmpty() *Eagle {
	clone := &Eagle{
		Version: e.Version,
		Grid:    e.Grid,
		Layers:  []Layer{},
		Board:   NewBoard(),
	}
	for _, layer := range e.Layers {
		clone.Layers = append(clone.Layers, layer)
	}
	return clone
}

// LayerByName attempts to find the layer number for a named layer. Eagle does
// appear to standardise these but it's easy to do a lookup, so let's be
// tolerant of future surprises. Aborts if the desired layer is not present,
// as it's extremely unlikely that any other action is desired in that case.
func (e *Eagle) LayerByName(name string) int {
	for _, layer := range e.Layers {
		if layer.Name == name {
			return layer.Number
		}
	}
	log.Fatalf("Requested layer %q not found!", name)
	return 0
}


<|start_filename|>internal/boardops/apply.go<|end_filename|>
// Copyright 2019 <NAME> <<EMAIL>>
//
// 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.

package boardops

import (
	"github.com/jsleeio/go-eagle/pkg/eagle"
	"github.com/jsleeio/go-eagle/pkg/panel"
)

// BoardOperation functions do ... something ... to an Eagle board
type BoardOperation func(*eagle.Eagle, panel.Panel) error

// ApplyBoardOperations works through a list of board operation
// functions and applies them to an Eagle board object
func ApplyBoardOperations(board *eagle.Eagle, spec panel.Panel, ops []BoardOperation) error {
	for _, op := range ops {
		if err := op(board, spec); err != nil {
			return err
		}
	}
	return nil
}
