From be84172729bde902cfb6efec130b4f268a2954a9 Mon Sep 17 00:00:00 2001 From: Wouter Horlings Date: Sun, 27 Jul 2025 17:31:11 +0200 Subject: [PATCH] Build v2 and refactor code base --- .golangci.yml | 2 +- cmd/gopatterns/gopatterns.go | 10 +- pkg/config/config.go | 36 ++ pkg/{pattern => dimensions}/dimensions.go | 9 +- pkg/path/path.go | 62 +++ pkg/{pattern => }/path/splines.go | 17 +- pkg/{pattern => }/path/style.go | 0 pkg/pattern/panel/panel.go | 35 ++ pkg/pattern/path/path.go | 36 -- pkg/pattern/pattern.go | 24 +- .../fixtures/classic_trouser_block.yaml | 119 ----- pkg/pattern/template/fixtures/trouser.yaml | 9 - pkg/pattern/template/line.go | 69 --- pkg/pattern/template/renderer.go | 124 ----- pkg/pattern/template/template.go | 56 -- pkg/pattern/text/text.go | 2 +- pkg/{pattern => }/point/absolute_point.go | 7 +- pkg/{pattern => }/point/between_point.go | 7 +- pkg/{pattern => }/point/extend_point.go | 9 +- pkg/{pattern => }/point/point.go | 6 +- pkg/{pattern => }/point/relative_point.go | 19 +- pkg/position/position.go | 6 - pkg/position/position_test.go | 41 ++ pkg/position/testutil/testutil.go | 15 + pkg/renderer/renderer.go | 164 ++++++ pkg/{pattern/template => storage}/storage.go | 30 +- pkg/template/fixtures/absolute_points.yaml | 13 + pkg/template/fixtures/between_points.yaml | 20 + .../fixtures/evaluation_functions.yaml | 19 + pkg/template/fixtures/extend_points.yaml | 18 + pkg/template/fixtures/functions.yaml | 52 ++ pkg/template/fixtures/polar_points.yaml | 17 + pkg/template/fixtures/references.yaml | 12 + pkg/template/fixtures/relative_points.yaml | 15 + pkg/{pattern => }/template/information.go | 0 pkg/template/line.go | 157 ++++++ pkg/{pattern => }/template/panel.go | 17 +- pkg/{pattern => }/template/point.go | 286 +++++----- pkg/template/point_test.go | 289 ++++++++++ pkg/{pattern => }/template/position.go | 0 pkg/template/template.go | 79 +++ pkg/{pattern => }/template/value.go | 0 pkg/util/id.go | 28 + pkg/util/id_test.go | 33 ++ spec/pattern.v2.yaml | 125 +++++ spec/pattern.yaml | 4 + templates/dimension_names.yaml | 2 + templates/tailored_shirt_block.v2.yaml | 498 ++++++++++++++++++ 48 files changed, 1991 insertions(+), 607 deletions(-) create mode 100644 pkg/config/config.go rename pkg/{pattern => dimensions}/dimensions.go (83%) create mode 100644 pkg/path/path.go rename pkg/{pattern => }/path/splines.go (91%) rename pkg/{pattern => }/path/style.go (100%) create mode 100644 pkg/pattern/panel/panel.go delete mode 100644 pkg/pattern/path/path.go delete mode 100644 pkg/pattern/template/fixtures/classic_trouser_block.yaml delete mode 100644 pkg/pattern/template/fixtures/trouser.yaml delete mode 100644 pkg/pattern/template/line.go delete mode 100644 pkg/pattern/template/renderer.go delete mode 100644 pkg/pattern/template/template.go rename pkg/{pattern => }/point/absolute_point.go (91%) rename pkg/{pattern => }/point/between_point.go (92%) rename pkg/{pattern => }/point/extend_point.go (89%) rename pkg/{pattern => }/point/point.go (97%) rename pkg/{pattern => }/point/relative_point.go (86%) create mode 100644 pkg/position/testutil/testutil.go create mode 100644 pkg/renderer/renderer.go rename pkg/{pattern/template => storage}/storage.go (54%) create mode 100644 pkg/template/fixtures/absolute_points.yaml create mode 100644 pkg/template/fixtures/between_points.yaml create mode 100644 pkg/template/fixtures/evaluation_functions.yaml create mode 100644 pkg/template/fixtures/extend_points.yaml create mode 100644 pkg/template/fixtures/functions.yaml create mode 100644 pkg/template/fixtures/polar_points.yaml create mode 100644 pkg/template/fixtures/references.yaml create mode 100644 pkg/template/fixtures/relative_points.yaml rename pkg/{pattern => }/template/information.go (100%) create mode 100644 pkg/template/line.go rename pkg/{pattern => }/template/panel.go (57%) rename pkg/{pattern => }/template/point.go (51%) create mode 100644 pkg/template/point_test.go rename pkg/{pattern => }/template/position.go (100%) create mode 100644 pkg/template/template.go rename pkg/{pattern => }/template/value.go (100%) create mode 100644 pkg/util/id.go create mode 100644 pkg/util/id_test.go create mode 100644 spec/pattern.v2.yaml create mode 100644 templates/tailored_shirt_block.v2.yaml diff --git a/.golangci.yml b/.golangci.yml index e29c38b..3a361f3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,7 +32,7 @@ linters-settings: allow: - $gostd - git.wtrh.nl/patterns/gopatterns/pkg/pattern - - git.wtrh.nl/patterns/gopatterns/pkg/pattern/point + - git.wtrh.nl/patterns/gopatterns/pkg/point - git.wtrh.nl/patterns/gopatterns/pkg/pattern/template - git.wtrh.nl/patterns/gopatterns/pkg/position - git.wtrh.nl/patterns/gopatterns/pkg/vector diff --git a/cmd/gopatterns/gopatterns.go b/cmd/gopatterns/gopatterns.go index 89913ab..c841936 100644 --- a/cmd/gopatterns/gopatterns.go +++ b/cmd/gopatterns/gopatterns.go @@ -8,7 +8,9 @@ import ( "os" gotemplate "text/template" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/template" + "git.wtrh.nl/patterns/gopatterns/pkg/config" + "git.wtrh.nl/patterns/gopatterns/pkg/renderer" + storage2 "git.wtrh.nl/patterns/gopatterns/pkg/storage" "gitlab.com/slxh/go/env" ) @@ -51,7 +53,7 @@ gopatterns [-templates ] [-out ] input-file os.MkdirAll(outputDir, 0o770) - storage, err := template.NewStorage(templateDir) + storage, err := storage2.NewStorage(templateDir) if err != nil { slog.Error("failed to open template directory", "err", err, "dir", templateDir) return @@ -60,13 +62,13 @@ gopatterns [-templates ] [-out ] input-file files := make([]string, 0) for _, arg := range args { - pattern, err := template.LoadPattern(arg) + pattern, err := config.LoadConfig(arg) if err != nil { slog.Error("failed to load pattern", "err", err) return } - filenames, err := storage.RenderPatterns(pattern, outputDir, debug) + filenames, err := renderer.RenderPatterns(storage, pattern, outputDir, debug) if err != nil { slog.Error("error occurred while creating pattern", "pattern", arg, "err", err) return diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..aadd536 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// Request contains the information to draw a pattern for a specific owner, with corresponding +// sizes and the template name. +type Request struct { + Sizes Sizes `yaml:"sizes,omitempty"` + Owner string `yaml:"owner"` + Template string `yaml:"template,omitempty"` +} + +// Sizes defines a map with the size name and the size value. +type Sizes map[string]float64 + +// LoadConfig reads and decodes a [Request] from a yaml file. +func LoadConfig(name string) (Request, error) { + fh, err := os.Open(name) + if err != nil { + return Request{}, fmt.Errorf("open pattern file %q: %w", name, err) + } + + pat := Request{} + + err = yaml.NewDecoder(fh).Decode(&pat) + if err != nil { + return Request{}, fmt.Errorf("decode content of file %q as yaml: %w", name, err) + } + + return pat, nil +} diff --git a/pkg/pattern/dimensions.go b/pkg/dimensions/dimensions.go similarity index 83% rename from pkg/pattern/dimensions.go rename to pkg/dimensions/dimensions.go index c967574..895906f 100644 --- a/pkg/pattern/dimensions.go +++ b/pkg/dimensions/dimensions.go @@ -1,4 +1,4 @@ -package pattern +package dimensions import ( "fmt" @@ -15,7 +15,7 @@ type DimensionID string // Dimensions is a map with dimensions. type Dimensions map[DimensionID]Dimension -// Parameters returns a govaluate.MapParameters object based on the dimensions. +// Parameters return a govaluate.MapParameters object based on the dimensions. func (d Dimensions) Parameters() govaluate.MapParameters { parameters := govaluate.MapParameters{} parameters["pi"] = math.Pi @@ -60,8 +60,3 @@ type Dimension struct { Name string Value float64 } - -// AddDimension adds a dimension to a pattern. -func (p *Pattern) AddDimension(id DimensionID, dimension Dimension) { - p.dimensions[id] = dimension -} diff --git a/pkg/path/path.go b/pkg/path/path.go new file mode 100644 index 0000000..e1be8c3 --- /dev/null +++ b/pkg/path/path.go @@ -0,0 +1,62 @@ +// Package path provides objects to define lines on a sewing pattern. +package path + +import ( + "git.wtrh.nl/patterns/gopatterns/pkg/point" + "git.wtrh.nl/patterns/gopatterns/pkg/util" + "github.com/tdewolff/canvas" +) + +// Polygon defines a set of straight lines through points. +type Polygon struct { + points []point.Point + style Style + id util.ID +} + +func (p *Polygon) Through() []point.Point { + return p.points +} + +// NewPolygon returns a new [Polygon]. +func NewPolygon(points []point.Point, style Style, id util.ID) *Polygon { + return &Polygon{points: points, style: style} +} + +// WithStyle updates the style of the Polygon. +func (p *Polygon) WithStyle(Style Style) *Polygon { + p.style = Style + return p +} + +// Draw the path to the provided [canvas.Canvas]. +func (p *Polygon) Draw(c *canvas.Canvas) error { + polyline := canvas.Polyline{} + for _, next := range p.points { + polyline.Add(next.Vector().Values()) + } + + c.RenderPath(polyline.ToPath(), p.style.ToCanvas(), canvas.Identity) + + return nil +} + +func (p *Polygon) Length() (float64, error) { + length := 0.0 + + for i := range p.points[1:] { + length += p.points[i].Position().Distance(p.points[i+1].Position()) + } + + return length, nil +} + +func (p *Polygon) ID() util.ID { + return p.id +} + +type Path interface { + Draw(c *canvas.Canvas) error + Length() (float64, error) + Through() []point.Point +} diff --git a/pkg/pattern/path/splines.go b/pkg/path/splines.go similarity index 91% rename from pkg/pattern/path/splines.go rename to pkg/path/splines.go index ecf1c67..aa80281 100644 --- a/pkg/pattern/path/splines.go +++ b/pkg/path/splines.go @@ -2,17 +2,19 @@ package path import ( "fmt" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" + "git.wtrh.nl/patterns/gopatterns/pkg/util" + "log/slog" + + "git.wtrh.nl/patterns/gopatterns/pkg/point" "github.com/tdewolff/canvas" splines "gitlab.com/Achilleshiel/gosplines" - "log/slog" ) const resolution = 40 // Spline defines a smooth curved path through points. type Spline struct { - *Path + *Polygon start, end point.Point } @@ -20,6 +22,7 @@ type SplineOpts struct { Start, End point.Point Points []point.Point Style Style + ID util.ID } // NewSpline returns a new spline through points. Start and end points can be provided as @@ -27,9 +30,9 @@ type SplineOpts struct { // are no constraints on the direction. func NewSpline(opts SplineOpts) Spline { s := Spline{ - Path: NewPath(opts.Points, opts.Style), - start: opts.Start, - end: opts.End, + Polygon: NewPolygon(opts.Points, opts.Style, opts.ID), + start: opts.Start, + end: opts.End, } if opts.Start == nil && len(opts.Points) > 1 { @@ -50,7 +53,7 @@ func (s Spline) Draw(c *canvas.Canvas) error { return fmt.Errorf("generating spline: %w", err) } - path := NewPath(points, s.style) + path := NewPolygon(points, s.style, s.id) if err = path.Draw(c); err != nil { return fmt.Errorf("draw spline points to canvas: %w", err) diff --git a/pkg/pattern/path/style.go b/pkg/path/style.go similarity index 100% rename from pkg/pattern/path/style.go rename to pkg/path/style.go diff --git a/pkg/pattern/panel/panel.go b/pkg/pattern/panel/panel.go new file mode 100644 index 0000000..c186d57 --- /dev/null +++ b/pkg/pattern/panel/panel.go @@ -0,0 +1,35 @@ +package panel + +import ( + "git.wtrh.nl/patterns/gopatterns/pkg/dimensions" + "git.wtrh.nl/patterns/gopatterns/pkg/path" + "git.wtrh.nl/patterns/gopatterns/pkg/point" + "git.wtrh.nl/patterns/gopatterns/pkg/util" + "github.com/tdewolff/canvas" +) + +type Panel struct { + Name string + Lines map[util.ID]path.Path + Points map[util.ID]point.Point + Dimensions dimensions.Dimensions +} + +func (p Panel) Draw(c *canvas.Canvas, face *canvas.FontFace, debug bool) error { + for _, line := range p.Lines { + err := line.Draw(c) + if err != nil { + return err + } + + for _, throughPoint := range line.Through() { + throughPoint.SetDraw() + } + } + + for _, drawPoints := range p.Points { + point.Draw(c, drawPoints, face, debug) + } + + return nil +} diff --git a/pkg/pattern/path/path.go b/pkg/pattern/path/path.go deleted file mode 100644 index 647723e..0000000 --- a/pkg/pattern/path/path.go +++ /dev/null @@ -1,36 +0,0 @@ -// Package path provides objects to define lines on a sewing pattern. -package path - -import ( - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" - "github.com/tdewolff/canvas" -) - -// Path defines a set of straight lines through points. -type Path struct { - points []point.Point - style Style -} - -// NewPath returns a new [Path]. -func NewPath(points []point.Point, style Style) *Path { - return &Path{points: points, style: style} -} - -// WithStyle updates the style of the Path. -func (p *Path) WithStyle(Style Style) *Path { - p.style = Style - return p -} - -// Draw the path to the provided [canvas.Canvas]. -func (p *Path) Draw(c *canvas.Canvas) error { - polyline := canvas.Polyline{} - for _, next := range p.points { - polyline.Add(next.Vector().Values()) - } - - c.RenderPath(polyline.ToPath(), p.style.ToCanvas(), canvas.Identity) - - return nil -} diff --git a/pkg/pattern/pattern.go b/pkg/pattern/pattern.go index e066893..b3ee4d3 100644 --- a/pkg/pattern/pattern.go +++ b/pkg/pattern/pattern.go @@ -3,9 +3,11 @@ package pattern import ( "fmt" + "git.wtrh.nl/patterns/gopatterns/pkg/util" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" + "git.wtrh.nl/patterns/gopatterns/pkg/dimensions" "git.wtrh.nl/patterns/gopatterns/pkg/pattern/text" + "git.wtrh.nl/patterns/gopatterns/pkg/point" "github.com/tdewolff/canvas" "golang.org/x/image/font/gofont/goregular" "gopkg.in/Knetic/govaluate.v3" @@ -13,13 +15,14 @@ import ( // The Pattern contains all the points, lines and dimensions to draw a pattern to a canvas. type Pattern struct { - points map[point.ID]point.Point + points map[util.ID]point.Point lines []pathDrawer - dimensions Dimensions + dimensions dimensions.Dimensions texts []*text.Text } type pathDrawer interface { + Length() (float64, error) Draw(c *canvas.Canvas) error } @@ -34,7 +37,7 @@ func (p *Pattern) AddLine(line pathDrawer) { } // GetPoints returns a slice with points for the given IDs. -func (p *Pattern) GetPoints(id []point.ID) []point.Point { +func (p *Pattern) GetPoints(id []util.ID) []point.Point { points := make([]point.Point, 0, len(id)) for _, i := range id { points = append(points, p.GetPoint(i)) @@ -44,16 +47,16 @@ func (p *Pattern) GetPoints(id []point.ID) []point.Point { } // GetPoint returns the point for the given ID. -func (p *Pattern) GetPoint(id point.ID) point.Point { //nolint:ireturn +func (p *Pattern) GetPoint(id util.ID) point.Point { //nolint:ireturn return p.points[id] } // NewPattern returns a new Pattern. func NewPattern() *Pattern { return &Pattern{ - points: make(map[point.ID]point.Point, 32), + points: make(map[util.ID]point.Point, 32), lines: make([]pathDrawer, 0, 32), - dimensions: make(Dimensions), + dimensions: make(dimensions.Dimensions), texts: make([]*text.Text, 0), } } @@ -94,6 +97,11 @@ func (p *Pattern) AddText(t *text.Text) { p.texts = append(p.texts, t) } -func (p *Pattern) SetDimensions(dimensions Dimensions) { +func (p *Pattern) SetDimensions(dimensions dimensions.Dimensions) { p.dimensions = dimensions } + +// AddDimension adds a dimension to a pattern. +func (p *Pattern) AddDimension(id dimensions.DimensionID, dimension dimensions.Dimension) { + p.dimensions[id] = dimension +} diff --git a/pkg/pattern/template/fixtures/classic_trouser_block.yaml b/pkg/pattern/template/fixtures/classic_trouser_block.yaml deleted file mode 100644 index 3d645f4..0000000 --- a/pkg/pattern/template/fixtures/classic_trouser_block.yaml +++ /dev/null @@ -1,119 +0,0 @@ ---- -points: - 0: - position: - 1: - position: - y: -(body_rise + 10) - relativeTo: 0 - 2: - position: - y: -inside_leg - relativeTo: 1 - 3: - position: - y: inside_leg/2+50 - relativeTo: 2 - 4: - position: - y: body_rise/4 - relativeTo: 1 - 5: - position: - x: -(seat/8 - 10) - relativeTo: 1 - 6: - position: - x: -(seat/8 - 10) - relativeTo: 4 - 7: - position: - x: -(seat/8 - 10) - relativeTo: 0 - 8: - position: - x: seat/4 + 20 - relativeTo: 6 - 9: - position: - x: -(seat/16 + 5) - relativeTo: 5 - 10: - position: - x: 10 - relativeTo: 7 - 11: - position: - x: trouser_waist/4 + 25 - relativeTo: 10 - 12: - position: - x: trouser_bottom_width/2 - relativeTo: 2 - 13: - position: - x: -trouser_bottom_width/2 - relativeTo: 2 - 14: - position: - x: trouser_bottom_width/2 + 15 - relativeTo: 3 - 15: - position: - x: -(trouser_bottom_width/2 + 15) - relativeTo: 3 - -panels: - front: - points: - extend12-14: - between: - from: 12 - to: 14 - offset: 1.3 - between11-8: - between: - from: 8 - to: 11 - offset: 0.5 - between9-15: - between: - from: 15 - to: 9 - offset: 0.5 - offset_between11-8: - position: - x: 5 - relativeTo: between11-8 - offset_between9-15: - position: - x: 5 - relativeTo: between9-15 - extend13-15: - between: - from: 13 - to: 15 - offset: 1.5 - r5: - position: - rotation: 3*pi/4 - relativeTo: 5 - h5: - position: - x: 30 - relativeTo: r5 - hide: true - lines: - - through: [0,4,1,3,2] - - through: [14,12,13,15] - - through: [14,8,11] - curve: - start: extend12-14 - end: offset_between11-8 - - through: [15,9] - curve: - start: extend13-15 - - through: [9, h5, 6] - curve: {} - - through: [6, 10, 11] - diff --git a/pkg/pattern/template/fixtures/trouser.yaml b/pkg/pattern/template/fixtures/trouser.yaml deleted file mode 100644 index 2b35c53..0000000 --- a/pkg/pattern/template/fixtures/trouser.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -owner: Wouter -sizes: - body_rise: 281 - inside_leg: 800 - seat: 1020 - trouser_waist: 900 - trouser_bottom_width: 226 -template: fixtures/classic_trouser_block.yaml \ No newline at end of file diff --git a/pkg/pattern/template/line.go b/pkg/pattern/template/line.go deleted file mode 100644 index 1c7c21d..0000000 --- a/pkg/pattern/template/line.go +++ /dev/null @@ -1,69 +0,0 @@ -package template - -import ( - "git.wtrh.nl/patterns/gopatterns/pkg/pattern" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/path" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" -) - -// Lines contains one Line or more in a slice. -type Lines []Line - -// Line describes a pattern line. -type Line struct { - Through []point.ID `yaml:"through"` - Curve *Curve `yaml:"curve,omitempty"` - Style *Style `yaml:"style,omitempty"` -} - -type Style struct { - Thickness *float64 `yaml:"thickness,omitempty"` -} - -// Curve describes if a Line curves and if it has start and end constraints. -type Curve struct { - Start point.ID `yaml:"start,omitempty"` - End point.ID `yaml:"end,omitempty"` -} - -// Build adds the line to the provided [pattern.Pattern]. -func (l Line) Build(pat *pattern.Pattern) error { - points := pat.GetPoints(l.Through) - for _, p := range points { - p.SetDraw() - } - - style := path.NewDefaultStyle() - - if l.Style != nil && l.Style.Thickness != nil { - style.Thickness = *l.Style.Thickness - } - - switch { - case l.Curve != nil: - pat.AddLine( - path.NewSpline(path.SplineOpts{ - Start: pat.GetPoint(l.Curve.Start), - End: pat.GetPoint(l.Curve.End), - Points: points, - Style: style, - }), - ) - default: - pat.AddLine(path.NewPath(points, style)) - } - - return nil -} - -// Build adds all the lines to the provided [pattern.Pattern]. -func (l Lines) Build(pat *pattern.Pattern) error { - for _, line := range l { - err := line.Build(pat) - if err != nil { - return err - } - } - - return nil -} diff --git a/pkg/pattern/template/renderer.go b/pkg/pattern/template/renderer.go deleted file mode 100644 index 27328db..0000000 --- a/pkg/pattern/template/renderer.go +++ /dev/null @@ -1,124 +0,0 @@ -package template - -import ( - "fmt" - "path/filepath" - "slices" - "strings" - - "git.wtrh.nl/patterns/gopatterns/pkg/pattern" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/text" - "github.com/stoewer/go-strcase" - "github.com/tdewolff/canvas" - "github.com/tdewolff/canvas/renderers" -) - -// RenderPatterns loads a [Request] from yaml file and renders the pattern to an SVG. -func (s Storage) RenderPatterns(request Request, outputDir string, debug bool) ([]string, error) { - template, err := s.LoadTemplate(request.Template) - if err != nil { - return nil, fmt.Errorf("load pattern %q: %w", request.Template, err) - } - - filenames := make([]string, 0, len(template.Panels)) - - dim, err := s.Dimensions(request.Sizes) - if err != nil { - return nil, fmt.Errorf("load dimensions: %w", err) - } - - renderer := Renderer{dimensions: dim, owner: request.Owner, pattern: request.Template} - - for name, panel := range template.Panels { - pat := pattern.NewPattern() - pat.SetDimensions(dim) - - err = template.Points.AddToPattern(pat) - if err != nil { - return nil, fmt.Errorf("add generic points to pattern: %w", err) - } - - err = renderer.BuildPanel(panel, pat) - if err != nil { - return nil, fmt.Errorf("constructing %s panel: %w", name, err) - } - - c := canvas.New(200, 200) - - err = pat.ToCanvas(c, debug) - if err != nil { - return nil, fmt.Errorf("write pattern to canvas: %w", err) - } - - c.Fit(10) - - filename := filepath.Join(outputDir, strings.Join([]string{ - request.Template, name, - strcase.SnakeCase(request.Owner), - }, "_")+".pdf") - - filenames = append(filenames, filename) - - err = renderers.Write(filename, c) - if err != nil { - return nil, fmt.Errorf("write canvas to file: %w", err) - } - } - - return filenames, nil -} - -type Renderer struct { - dimensions pattern.Dimensions - owner string - pattern string -} - -// BuildPanel translates the panel to the provided [pattern.Pattern]. -func (r Renderer) BuildPanel(panel Panel, pat *pattern.Pattern) error { - err := panel.Points.AddToPattern(pat) - if err != nil { - return err - } - - err = panel.Lines.Build(pat) - if err != nil { - return err - } - - err = r.GenerateInformation(panel, pat) - if err != nil { - return err - } - - return nil -} - -func (r Renderer) GenerateInformation(p Panel, pat *pattern.Pattern) error { - err := Points{"_information": p.Information.Point}.AddToPattern(pat) - if err != nil { - return err - } - - dimensions := make([]string, 0, len(r.dimensions)) - for _, dimension := range r.dimensions { - dimensions = append(dimensions, - fmt.Sprintf(" %s: %.1f cm", dimension.Name, dimension.Value/10)) - } - - slices.Sort(dimensions) - dimensions = append([]string{ - "For: " + r.owner, - "Pattern: " + startCase(r.pattern), - "Panel: " + p.Name, - "Hem allowance: " + p.Allowances.Hem, - "Seam allowance: " + p.Allowances.Seam, - "\nMeasurements:", - }, dimensions...) - - point := pat.GetPoint("_information") - point.SetHide() - pat.AddText(text.NewText(point, "", strings.Join(dimensions, "\n"))) - - return nil -} diff --git a/pkg/pattern/template/template.go b/pkg/pattern/template/template.go deleted file mode 100644 index 2e6e671..0000000 --- a/pkg/pattern/template/template.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package template makes it possible to describe templates and patterns in yaml files and -// to render them to SVG. -package template - -import ( - "fmt" - "os" - "unicode" - - "gopkg.in/yaml.v3" -) - -// Sizes defines a map with the size name and the size value. -type Sizes map[string]float64 - -// Template contains the generic information to draw a specific clothing pattern. -type Template struct { - Points `yaml:"points"` - Panels `yaml:"panels"` -} - -// LoadPattern reads and decodes a [Request] from a yaml file. -func LoadPattern(name string) (Request, error) { - fh, err := os.Open(name) - if err != nil { - return Request{}, fmt.Errorf("open pattern file %q: %w", name, err) - } - - pat := Request{} - - err = yaml.NewDecoder(fh).Decode(&pat) - if err != nil { - return Request{}, fmt.Errorf("decode content of file %q as yaml: %w", name, err) - } - - return pat, nil -} - -func startCase(text string) string { - output := make([]rune, len(text)) - - for i, val := range text { - switch { - case i == 0: - output[i] = unicode.ToUpper(val) - case val == '_': - output[i] = ' ' - case output[i-1] == ' ': - output[i] = unicode.ToUpper(val) - default: - output[i] = val - } - } - - return string(output) -} diff --git a/pkg/pattern/text/text.go b/pkg/pattern/text/text.go index 3d92c3e..5628659 100644 --- a/pkg/pattern/text/text.go +++ b/pkg/pattern/text/text.go @@ -1,7 +1,7 @@ package text import ( - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" + "git.wtrh.nl/patterns/gopatterns/pkg/point" "github.com/tdewolff/canvas" ) diff --git a/pkg/pattern/point/absolute_point.go b/pkg/point/absolute_point.go similarity index 91% rename from pkg/pattern/point/absolute_point.go rename to pkg/point/absolute_point.go index 5b9a587..6b4e912 100644 --- a/pkg/pattern/point/absolute_point.go +++ b/pkg/point/absolute_point.go @@ -2,6 +2,7 @@ package point import ( "git.wtrh.nl/patterns/gopatterns/pkg/position" + "git.wtrh.nl/patterns/gopatterns/pkg/util" "git.wtrh.nl/patterns/gopatterns/pkg/vector" "github.com/tdewolff/canvas" ) @@ -9,7 +10,7 @@ import ( // AbsolutePoint implements Point and defines an absolute position. // All other points should eventually be relative to one or more absolute point(s). type AbsolutePoint struct { - id ID + id util.ID position position.Position name string draw bool @@ -22,7 +23,7 @@ func (a *AbsolutePoint) Matrix() canvas.Matrix { } // ID returns the point ID. -func (a *AbsolutePoint) ID() ID { +func (a *AbsolutePoint) ID() util.ID { return a.id } @@ -42,7 +43,7 @@ func (a *AbsolutePoint) Vector() vector.Vector { } // NewAbsolutePoint returns a new absolute point. -func NewAbsolutePoint(x, y, r float64, id ID) *AbsolutePoint { +func NewAbsolutePoint(x, y, r float64, id util.ID) *AbsolutePoint { return &AbsolutePoint{ position: position.Position{ Vector: vector.Vector{ diff --git a/pkg/pattern/point/between_point.go b/pkg/point/between_point.go similarity index 92% rename from pkg/pattern/point/between_point.go rename to pkg/point/between_point.go index a4551c6..33168aa 100644 --- a/pkg/pattern/point/between_point.go +++ b/pkg/point/between_point.go @@ -1,6 +1,7 @@ package point import ( + "git.wtrh.nl/patterns/gopatterns/pkg/util" "math" "git.wtrh.nl/patterns/gopatterns/pkg/position" @@ -10,7 +11,7 @@ import ( // BetweenPoint defines a point on the line between two other points. type BetweenPoint struct { - id ID + id util.ID p Point q Point offset float64 @@ -23,7 +24,7 @@ type BetweenPoint struct { // The given offset defines where the new point is. // With offset = 0 the new point is a p, offset = 0.5 results in a point exactly in the middle. // Offset can be <0 (extending from p side) or >1 (extending from the q side). -func NewBetweenPoint(p, q Point, offset float64, id ID) *BetweenPoint { +func NewBetweenPoint(p, q Point, offset float64, id util.ID) *BetweenPoint { return &BetweenPoint{ id: id, p: p, @@ -57,7 +58,7 @@ func (b *BetweenPoint) Matrix() canvas.Matrix { } // ID returns the point ID. -func (b *BetweenPoint) ID() ID { +func (b *BetweenPoint) ID() util.ID { return b.id } diff --git a/pkg/pattern/point/extend_point.go b/pkg/point/extend_point.go similarity index 89% rename from pkg/pattern/point/extend_point.go rename to pkg/point/extend_point.go index e6b903b..d8c1f6f 100644 --- a/pkg/pattern/point/extend_point.go +++ b/pkg/point/extend_point.go @@ -1,6 +1,7 @@ package point import ( + "git.wtrh.nl/patterns/gopatterns/pkg/util" "math" "git.wtrh.nl/patterns/gopatterns/pkg/position" @@ -10,7 +11,7 @@ import ( // ExtendPoint defines a point on the line between two other points. type ExtendPoint struct { - id ID + id util.ID from Point to Point extend float64 @@ -22,7 +23,7 @@ type ExtendPoint struct { // NewExtendPoint returns a new ExtendPoint relative to two other points from and to. // The given offset defines where the new point is. // With offset = 0 the new point is a from, offset = 0.5 results in a point exactly in the middle. -func NewExtendPoint(from, to Point, extend float64, id ID) *ExtendPoint { +func NewExtendPoint(from, to Point, extend float64, id util.ID) *ExtendPoint { return &ExtendPoint{ id: id, from: from, @@ -36,7 +37,7 @@ func NewExtendPoint(from, to Point, extend float64, id ID) *ExtendPoint { func (b *ExtendPoint) Position() position.Position { return position.Position{ Vector: b.to.Vector().Add(b.extendedVector()), - Rotation: b.to.Vector().AngleBetween(b.to.Vector()) - math.Pi/2, + Rotation: b.from.Vector().AngleBetween(b.to.Vector()) - math.Pi/2, } } @@ -56,7 +57,7 @@ func (b *ExtendPoint) Matrix() canvas.Matrix { } // ID returns the point ID. -func (b *ExtendPoint) ID() ID { +func (b *ExtendPoint) ID() util.ID { return b.id } diff --git a/pkg/pattern/point/point.go b/pkg/point/point.go similarity index 97% rename from pkg/pattern/point/point.go rename to pkg/point/point.go index d77eb86..3cf0dee 100644 --- a/pkg/pattern/point/point.go +++ b/pkg/point/point.go @@ -5,17 +5,15 @@ package point import ( "git.wtrh.nl/patterns/gopatterns/pkg/position" + "git.wtrh.nl/patterns/gopatterns/pkg/util" "git.wtrh.nl/patterns/gopatterns/pkg/vector" "github.com/tdewolff/canvas" ) -// ID defines a point id. -type ID string - // Point defines the interface for different types of points. type Point interface { // ID returns the point ID. - ID() ID + ID() util.ID // Position calculates and returns the absolute [position.Position]. Position() position.Position diff --git a/pkg/pattern/point/relative_point.go b/pkg/point/relative_point.go similarity index 86% rename from pkg/pattern/point/relative_point.go rename to pkg/point/relative_point.go index d570ba7..0dbab40 100644 --- a/pkg/pattern/point/relative_point.go +++ b/pkg/point/relative_point.go @@ -1,6 +1,7 @@ package point import ( + "git.wtrh.nl/patterns/gopatterns/pkg/util" "math" "git.wtrh.nl/patterns/gopatterns/pkg/position" @@ -14,7 +15,7 @@ type RelativePoint struct { name string point Point relativeOffset position.Position - id ID + id util.ID hide bool } @@ -29,7 +30,7 @@ func (r *RelativePoint) Done() Point { //nolint:ireturn } // MarkWith sets the ID of the point. -func (r *RelativePoint) MarkWith(n ID) *RelativePoint { +func (r *RelativePoint) MarkWith(n util.ID) *RelativePoint { r.id = n if r.name == "" { @@ -56,12 +57,12 @@ func (r *RelativePoint) Vector() vector.Vector { } // ID returns the point ID. -func (r *RelativePoint) ID() ID { +func (r *RelativePoint) ID() util.ID { return r.id } // NewRelativePointWithVector returns a new RelativePoint. -func NewRelativePointWithVector(point Point, p vector.Vector, id ID) *RelativePoint { +func NewRelativePointWithVector(point Point, p vector.Vector, id util.ID) *RelativePoint { return &RelativePoint{ point: point, relativeOffset: position.Position{Vector: p}, @@ -78,7 +79,7 @@ func NewRelativePoint(point Point) *RelativePoint { } // NewRotationPoint returns a new RelativePoint with a specific rotation. -func NewRotationPoint(point Point, a float64, id ID) *RelativePoint { +func NewRotationPoint(point Point, a float64, id util.ID) *RelativePoint { p := &RelativePoint{ point: point, relativeOffset: position.Position{ @@ -115,22 +116,22 @@ func (r *RelativePoint) WithRotationOffset(value float64) *RelativePoint { } // NewRelativePointBelow returns a RelativePoint distance f below another Point. -func NewRelativePointBelow(point Point, f float64, id ID) *RelativePoint { +func NewRelativePointBelow(point Point, f float64, id util.ID) *RelativePoint { return NewRelativePointWithVector(point, vector.Vector{X: 0, Y: -f}, id) } // NewRelativePointAbove returns a RelativePoint distance f above another Point. -func NewRelativePointAbove(point Point, f float64, id ID) *RelativePoint { +func NewRelativePointAbove(point Point, f float64, id util.ID) *RelativePoint { return NewRelativePointWithVector(point, vector.Vector{X: 0, Y: f}, id) } // NewRelativePointLeft returns a RelativePoint distance f left of another Point. -func NewRelativePointLeft(point Point, f float64, id ID) *RelativePoint { +func NewRelativePointLeft(point Point, f float64, id util.ID) *RelativePoint { return NewRelativePointWithVector(point, vector.Vector{X: -f, Y: 0}, id) } // NewRelativePointRight returns a RelativePoint distance f right of another Point. -func NewRelativePointRight(point Point, f float64, id ID) *RelativePoint { +func NewRelativePointRight(point Point, f float64, id util.ID) *RelativePoint { return NewRelativePointWithVector(point, vector.Vector{X: f, Y: 0}, id) } diff --git a/pkg/position/position.go b/pkg/position/position.go index 78a5fb4..e15dcc4 100644 --- a/pkg/position/position.go +++ b/pkg/position/position.go @@ -22,12 +22,6 @@ func (p Position) Add(q Position) Position { } } -// Translate the position to a new reference frame. -func (p Position) Translate(q Position) Position { - q.Vector = q.Vector.Span(q.Rotation) - return p.Add(q) -} - // Distance returns the distance between two positions. func (p Position) Distance(q Position) float64 { return p.Vector.Distance(q.Vector) diff --git a/pkg/position/position_test.go b/pkg/position/position_test.go index 4e7cf74..b95900a 100644 --- a/pkg/position/position_test.go +++ b/pkg/position/position_test.go @@ -98,3 +98,44 @@ func TestPosition_Add(t *testing.T) { }) } } + +func TestPosition_Translate(t *testing.T) { + tests := map[string]struct { + p, q position.Position + result float64 + }{ + "origin to 1,1": { + p: position.Position{ + Vector: vector.Vector{ + X: 1, Y: 1, + }, + }, + q: position.Position{ + Vector: vector.Vector{ + X: 0, Y: 0, + }, + }, + result: math.Sqrt(2), + }, + "0,2 to 4,5 -> 5": { + p: position.Position{ + Vector: vector.Vector{ + X: 0, Y: 2, + }, + }, + q: position.Position{ + Vector: vector.Vector{ + X: 4, Y: 5, + }, + }, + result: 5, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + translateResult := tt.p.Distance(tt.q) + require.InDelta(t, tt.result, translateResult, 1e-10) + }) + } +} diff --git a/pkg/position/testutil/testutil.go b/pkg/position/testutil/testutil.go new file mode 100644 index 0000000..71e161b --- /dev/null +++ b/pkg/position/testutil/testutil.go @@ -0,0 +1,15 @@ +package testutil + +import ( + "testing" + + "git.wtrh.nl/patterns/gopatterns/pkg/position" + "github.com/stretchr/testify/require" +) + +func EqualPosition(tb testing.TB, expected, actual position.Position, delta float64, msgAndArgs ...interface{}) { + tb.Helper() + require.InDelta(tb, expected.Vector.X, actual.Vector.X, delta, msgAndArgs) + require.InDelta(tb, expected.Vector.Y, actual.Vector.Y, delta, msgAndArgs) + require.InDelta(tb, expected.Rotation, actual.Rotation, delta, msgAndArgs) +} diff --git a/pkg/renderer/renderer.go b/pkg/renderer/renderer.go new file mode 100644 index 0000000..e30b7dc --- /dev/null +++ b/pkg/renderer/renderer.go @@ -0,0 +1,164 @@ +package renderer + +import ( + "fmt" + "path/filepath" + "strings" + "unicode" + + "git.wtrh.nl/patterns/gopatterns/pkg/config" + "git.wtrh.nl/patterns/gopatterns/pkg/dimensions" + "git.wtrh.nl/patterns/gopatterns/pkg/template" + "github.com/stoewer/go-strcase" + "github.com/tdewolff/canvas" + "github.com/tdewolff/canvas/renderers" + "golang.org/x/image/font/gofont/goregular" +) + +type Storage interface { + LoadDimensions(sizes config.Sizes) (dimensions.Dimensions, error) + LoadTemplate(name string) (template.Template, error) +} + +// RenderPatterns loads a [Request] from yaml file and renders the pattern to an SVG. +func RenderPatterns(s Storage, request config.Request, outputDir string, debug bool) ([]string, error) { + loadedTemplate, err := s.LoadTemplate(request.Template) + if err != nil { + return nil, fmt.Errorf("load pattern %q: %w", request.Template, err) + } + + filenames := make([]string, 0, len(loadedTemplate.Panels)) + + dim, err := s.LoadDimensions(request.Sizes) + if err != nil { + return nil, fmt.Errorf("load dimensions: %w", err) + } + + // renderer := Renderer{dimensions: dim, owner: request.Owner, pattern: request.Template} + + for name := range loadedTemplate.Panels { + newPanel, err := loadedTemplate.GetPanel(template.Request{Dims: dim, Panel: name}) + if err != nil { + return nil, err + } + // + //pat := pattern.NewPattern() + //pat.SetDimensions(dim) + // + //err = loadedTemplate.Points.AddToPattern(pat) + //if err != nil { + // return nil, fmt.Errorf("add generic points to pattern: %w", err) + //} + // + //err = renderer.BuildPanel(panel, pat) + //if err != nil { + // return nil, fmt.Errorf("constructing %s panel: %w", name, err) + //} + + c := canvas.New(200, 200) + + err = newPanel.Draw(c, loadFont(), debug) + if err != nil { + return nil, fmt.Errorf("write pattern to canvas: %w", err) + } + + c.Fit(10) + + filename := filepath.Join(outputDir, strings.Join([]string{ + request.Template, name, + strcase.SnakeCase(request.Owner), + }, "_")+".pdf") + + filenames = append(filenames, filename) + + err = renderers.Write(filename, c) + if err != nil { + return nil, fmt.Errorf("write canvas to file: %w", err) + } + } + + return filenames, nil +} + +func loadFont() *canvas.FontFace { + fontDejaVu := canvas.NewFontFamily("latin") + + if err := fontDejaVu.LoadFont(goregular.TTF, 0, canvas.FontRegular); err != nil { + panic(err) + } + + return fontDejaVu.Face(12.0, canvas.Black, canvas.FontRegular) +} + +type Renderer struct { + dimensions dimensions.Dimensions + owner string + pattern string +} + +//// BuildPanel translates the panel to the provided [pattern.Pattern]. +//func (r Renderer) BuildPanel(panel template.Panel, pat *pattern.Pattern) error { +// err := panel.Points.AddToPattern(pat) +// if err != nil { +// return err +// } +// +// err = panel.Lines.Build(pat) +// if err != nil { +// return err +// } +// +// err = r.GenerateInformation(panel, pat) +// if err != nil { +// return err +// } +// +// return nil +//} + +//func (r Renderer) GenerateInformation(p panel.Panel, pat *pattern.Pattern) error { +// err := template.Points{"_information": p.Information.Point} +// if err != nil { +// return err +// } +// +// dims := make([]string, 0, len(r.dimensions)) +// for _, dimension := range r.dimensions { +// dims = append(dims, fmt.Sprintf(" %s: %.1f cm", dimension.Name, dimension.Value/10)) +// } +// +// slices.Sort(dims) +// dims = append([]string{ +// "For: " + r.owner, +// "Pattern: " + startCase(r.pattern), +// "Panel: " + p.Name, +// "Hem allowance: " + p.Allowances.Hem, +// "Seam allowance: " + p.Allowances.Seam, +// "\nMeasurements:", +// }, dims...) +// +// point := pat.GetPoint("_information") +// point.SetHide() +// pat.AddText(text.NewText(point, "", strings.Join(dims, "\n"))) +// +// return nil +//} + +func startCase(text string) string { + output := make([]rune, len(text)) + + for i, val := range text { + switch { + case i == 0: + output[i] = unicode.ToUpper(val) + case val == '_': + output[i] = ' ' + case output[i-1] == ' ': + output[i] = unicode.ToUpper(val) + default: + output[i] = val + } + } + + return string(output) +} diff --git a/pkg/pattern/template/storage.go b/pkg/storage/storage.go similarity index 54% rename from pkg/pattern/template/storage.go rename to pkg/storage/storage.go index df8e7d7..23f0837 100644 --- a/pkg/pattern/template/storage.go +++ b/pkg/storage/storage.go @@ -1,11 +1,13 @@ -package template +package storage import ( "fmt" "io/fs" "os" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern" + "git.wtrh.nl/patterns/gopatterns/pkg/config" + "git.wtrh.nl/patterns/gopatterns/pkg/dimensions" + "git.wtrh.nl/patterns/gopatterns/pkg/template" "gopkg.in/yaml.v3" ) @@ -22,13 +24,13 @@ func NewStorage(dir string) (Storage, error) { return Storage{dir: os.DirFS(dir)}, nil } -func (s Storage) Dimensions(sizes Sizes) (pattern.Dimensions, error) { +func (s Storage) LoadDimensions(sizes config.Sizes) (dimensions.Dimensions, error) { f, err := s.dir.Open("dimension_names.yaml") if err != nil { return nil, fmt.Errorf("open \"dimension_names.yaml\": %w", err) } - namedDimensions := pattern.Dimensions{} + namedDimensions := dimensions.Dimensions{} err = yaml.NewDecoder(f).Decode(&namedDimensions) if err != nil { @@ -49,27 +51,19 @@ func (s Storage) Dimensions(sizes Sizes) (pattern.Dimensions, error) { return namedDimensions, nil } -// Request contains the information to draw a pattern for a specific owner, with corresponding -// sizes and the template name. -type Request struct { - Sizes Sizes `yaml:"sizes,omitempty"` - Owner string `yaml:"owner"` - Template string `yaml:"template,omitempty"` -} - // LoadTemplate reads and decodes a [Template] from a yaml file. -func (s Storage) LoadTemplate(name string) (Template, error) { +func (s Storage) LoadTemplate(name string) (template.Template, error) { fh, err := s.dir.Open(name + ".yaml") if err != nil { - return Template{}, fmt.Errorf("open template file %q: %w", name, err) + return template.Template{}, fmt.Errorf("open template file %q: %w", name, err) } - template := Template{} + t := template.Template{} - err = yaml.NewDecoder(fh).Decode(&template) + err = yaml.NewDecoder(fh).Decode(&t) if err != nil { - return Template{}, fmt.Errorf("decode content of file %q as yaml: %w", name, err) + return template.Template{}, fmt.Errorf("decode content of file %q as yaml: %w", name, err) } - return template, nil + return t, nil } diff --git a/pkg/template/fixtures/absolute_points.yaml b/pkg/template/fixtures/absolute_points.yaml new file mode 100644 index 0000000..709114e --- /dev/null +++ b/pkg/template/fixtures/absolute_points.yaml @@ -0,0 +1,13 @@ +--- +points: + 1: + position: + x: test + y: 14.3 +panels: + body: + points: + 2: + position: + x: test*2 + y: -3 diff --git a/pkg/template/fixtures/between_points.yaml b/pkg/template/fixtures/between_points.yaml new file mode 100644 index 0000000..3c144a0 --- /dev/null +++ b/pkg/template/fixtures/between_points.yaml @@ -0,0 +1,20 @@ +--- +points: + 1: + position: + x: 4 + y: 14.3 +panels: + body: + points: + 2: + relativeTo: + 1 + position: + x: 2 + y: -3 + 3: + between: + from: 1 + to: 2 + offset: test diff --git a/pkg/template/fixtures/evaluation_functions.yaml b/pkg/template/fixtures/evaluation_functions.yaml new file mode 100644 index 0000000..29e795c --- /dev/null +++ b/pkg/template/fixtures/evaluation_functions.yaml @@ -0,0 +1,19 @@ +--- +points: + acos: + position: + x: acos(1/2) + asin: + position: + x: asin(pi/2) + atan2: + position: + x: atan2(1,1) + abs1: + position: + x: abs(-1.4) + abs2: + position: + x: abs(3.5) +panels: + test: {} \ No newline at end of file diff --git a/pkg/template/fixtures/extend_points.yaml b/pkg/template/fixtures/extend_points.yaml new file mode 100644 index 0000000..ce1cbfe --- /dev/null +++ b/pkg/template/fixtures/extend_points.yaml @@ -0,0 +1,18 @@ +--- +points: + 1: {} +panels: + body: + points: + 2: + relativeTo: + 1 + position: + x: 4 + y: 3 + 3: + extend: + from: 1 + to: 2 + offset: 5 + diff --git a/pkg/template/fixtures/functions.yaml b/pkg/template/fixtures/functions.yaml new file mode 100644 index 0000000..efd48b7 --- /dev/null +++ b/pkg/template/fixtures/functions.yaml @@ -0,0 +1,52 @@ +--- +points: + 1: {} + 2: + position: + x: 4 + y: 3 + distance: + position: + x: DistanceBetween("1","2") + angle: + position: + x: AngleBetween("1","2") + yDistance: + position: + x: YDistanceBetween("1","2") + xDistance: + position: + x: XDistanceBetween("1","2") + lineLength: + position: + x: LineLength("test.1") + lineLength2: + position: + x: LineLength("test.2") +panels: + test: + points: + 3: + relativeTo: 1 + position: + y: 1 + 4: + relativeTo: 2 + position: + y: 1 + 5: + position: + x: 1 + 6: + position: + y: 1 + x: 1 + 7: + position: + y: 1 + lines: + 1: + through: [1,2,4,3,1] + 2: + through: [1,5,6,7,1] + diff --git a/pkg/template/fixtures/polar_points.yaml b/pkg/template/fixtures/polar_points.yaml new file mode 100644 index 0000000..b579297 --- /dev/null +++ b/pkg/template/fixtures/polar_points.yaml @@ -0,0 +1,17 @@ +--- +points: + 1: {} +panels: + body: + points: + 2: + relativeTo: 1 + polar: + rotation: 0 + length: 1 + 3: + relativeTo: 2 + polar: + rotation: pi/2 + length: 1 + diff --git a/pkg/template/fixtures/references.yaml b/pkg/template/fixtures/references.yaml new file mode 100644 index 0000000..04d82f2 --- /dev/null +++ b/pkg/template/fixtures/references.yaml @@ -0,0 +1,12 @@ +--- +points: + 1: {} + 3: + relativeTo: body.2 +panels: + body: + points: + 2: + relativeTo: 1 + position: + x: 3 diff --git a/pkg/template/fixtures/relative_points.yaml b/pkg/template/fixtures/relative_points.yaml new file mode 100644 index 0000000..52eae9a --- /dev/null +++ b/pkg/template/fixtures/relative_points.yaml @@ -0,0 +1,15 @@ +--- +points: + 1: + position: + x: test + y: 14.3 +panels: + body: + points: + 2: + relativeTo: + 1 + position: + x: test*2 + y: -3 diff --git a/pkg/pattern/template/information.go b/pkg/template/information.go similarity index 100% rename from pkg/pattern/template/information.go rename to pkg/template/information.go diff --git a/pkg/template/line.go b/pkg/template/line.go new file mode 100644 index 0000000..3058cc0 --- /dev/null +++ b/pkg/template/line.go @@ -0,0 +1,157 @@ +package template + +import ( + "errors" + "git.wtrh.nl/patterns/gopatterns/pkg/util" + + "git.wtrh.nl/patterns/gopatterns/pkg/path" + "git.wtrh.nl/patterns/gopatterns/pkg/pattern" + "git.wtrh.nl/patterns/gopatterns/pkg/point" +) + +var ErrLineNotFound = errors.New("required path not found") + +// Lines contain named lines. +type Lines map[util.ID]Line + +// Line describes a pattern line. +type Line struct { + Through []util.ID `yaml:"through"` + Curve *Curve `yaml:"curve,omitempty"` + Style *Style `yaml:"style,omitempty"` +} + +type Style struct { + Thickness *float64 `yaml:"thickness,omitempty"` +} + +// Curve describes if a Line curves and if it has start and end constraints. +type Curve struct { + Start util.ID `yaml:"start,omitempty"` + End util.ID `yaml:"end,omitempty"` +} + +func (t Template) templateLine(panelName string, id util.ID) (Line, error) { + if id.Panel() != "" { + panelName = id.Panel() + } + + panel, err := t.Panel(panelName) + if !errors.Is(err, ErrPanelNotFound) { + l, ok := panel.Lines[id.Deref()] + if ok { + return l, nil + } + } + + line, ok := t.Lines[id.Deref()] + if !ok { + return Line{}, ErrLineNotFound + } + + return line, nil +} + +func (t Template) getOrCreateLine(id util.ID, req request, depth int) (path.Path, error) { + l, ok := req.lines[id] + if ok { + return l, nil + } + + newLine, err := t.createLine(id, req, depth+1) + if err != nil { + return nil, err + } + + req.lines[util.ID(id)] = newLine + + return newLine, nil +} + +func (t Template) createLine(id util.ID, req request, depth int) (path.Path, error) { + line, err := t.templateLine(req.name, id) + if err != nil { + return nil, err + } + + throughPoints, err := t.getOrCreatePoints(line.Through, req, depth+1) + if err != nil { + return nil, err + } + + style := path.NewDefaultStyle() + + if line.Style != nil && line.Style.Thickness != nil { + style.Thickness = *line.Style.Thickness + } + + switch { + case line.Curve != nil: + var start, end point.Point + if line.Curve.Start != "" { + start, err = t.getOrCreatePoint(line.Curve.Start, req, depth+1) + if err != nil { + return nil, err + } + } + + if line.Curve.End != "" { + end, err = t.getOrCreatePoint(line.Curve.End, req, depth+1) + if err != nil { + return nil, err + } + } + + return path.NewSpline(path.SplineOpts{ + Start: start, + End: end, + Points: throughPoints, + Style: style, + ID: util.ID(id), + }), nil + default: + return path.NewPolygon(throughPoints, style, util.ID(id)), nil + } +} + +// Build adds the line to the provided [pattern.Pattern]. +func (l Line) Build(pat *pattern.Pattern) error { + points := pat.GetPoints(l.Through) + for _, p := range points { + p.SetDraw() + } + + style := path.NewDefaultStyle() + + if l.Style != nil && l.Style.Thickness != nil { + style.Thickness = *l.Style.Thickness + } + + switch { + case l.Curve != nil: + pat.AddLine( + path.NewSpline(path.SplineOpts{ + Start: pat.GetPoint(l.Curve.Start), + End: pat.GetPoint(l.Curve.End), + Points: points, + Style: style, + }), + ) + default: + pat.AddLine(path.NewPolygon(points, style, "")) + } + + return nil +} + +// Build adds all the lines to the provided [pattern.Pattern]. +func (l Lines) Build(pat *pattern.Pattern) error { + for _, line := range l { + err := line.Build(pat) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/pattern/template/panel.go b/pkg/template/panel.go similarity index 57% rename from pkg/pattern/template/panel.go rename to pkg/template/panel.go index 7ea0352..b6701b0 100644 --- a/pkg/pattern/template/panel.go +++ b/pkg/template/panel.go @@ -1,9 +1,15 @@ package template +import ( + "errors" +) + +var ErrPanelNotFound = errors.New("panel does not exist") + // Panels contains a map with named panels. type Panels map[string]Panel -// Panel contains all the lines and extra points to draw a panel. +// The Panel contains all the lines and extra points to draw a panel. type Panel struct { Points Points `yaml:"points"` Lines Lines `yaml:"lines"` @@ -16,3 +22,12 @@ type Allowances struct { Hem string `yaml:"hem"` Seam string `yaml:"seam"` } + +func (t Template) Panel(name string) (Panel, error) { + p, ok := t.Panels[name] + if !ok { + return Panel{}, ErrPanelNotFound + } + + return p, nil +} diff --git a/pkg/pattern/template/point.go b/pkg/template/point.go similarity index 51% rename from pkg/pattern/template/point.go rename to pkg/template/point.go index 3621f7b..622661d 100644 --- a/pkg/pattern/template/point.go +++ b/pkg/template/point.go @@ -3,12 +3,13 @@ package template import ( "errors" "fmt" + "git.wtrh.nl/patterns/gopatterns/pkg/path" + "git.wtrh.nl/patterns/gopatterns/pkg/util" "maps" "math" "strconv" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern" - "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" + "git.wtrh.nl/patterns/gopatterns/pkg/point" "gopkg.in/Knetic/govaluate.v3" ) @@ -25,12 +26,12 @@ var ( ) // Points contains a map with points. -type Points map[point.ID]Point +type Points map[util.ID]Point // Point contains the template information for a point. type Point struct { Position Position `yaml:"position"` - RelativeTo *point.ID `yaml:"relativeTo,omitempty"` + RelativeTo *util.ID `yaml:"relativeTo,omitempty"` Description string `yaml:"description"` Between *BetweenPoint `yaml:"between"` Extend *ExtendPoint `yaml:"extend"` @@ -38,10 +39,31 @@ type Point struct { Polar *PolarPoint `yaml:"polar"` } +func (t Template) templatePoint(panelName string, id util.ID) (Point, error) { + if id.Panel() != "" { + panelName = id.Panel() + } + + panel, err := t.Panel(panelName) + if !errors.Is(err, ErrPanelNotFound) { + p, ok := panel.Points[id.Deref()] + if ok { + return p, nil + } + } + + p, ok := t.Points[id.Deref()] + if !ok { + return Point{}, ErrPointNotFound + } + + return p, nil +} + var ErrInvalidPointID = errors.New("type cannot be converted to a PointID") -func (p Points) Functions(pat *pattern.Pattern) map[string]govaluate.ExpressionFunction { - functions := p.evaluationFunctions() +func (t Template) functions(req request) map[string]govaluate.ExpressionFunction { + functions := t.evaluationFunctions() maps.Copy(functions, map[string]govaluate.ExpressionFunction{ "DistanceBetween": func(args ...interface{}) (interface{}, error) { @@ -50,7 +72,7 @@ func (p Points) Functions(pat *pattern.Pattern) map[string]govaluate.ExpressionF ErrInvalidArguments) } - points, err := p.getOrCreateFromArgs(pat, args...) + points, err := t.getOrCreatePointsFromArgs(req, args...) if err != nil { return nil, err } @@ -62,7 +84,7 @@ func (p Points) Functions(pat *pattern.Pattern) map[string]govaluate.ExpressionF ErrInvalidArguments) } - points, err := p.getOrCreateFromArgs(pat, args...) + points, err := t.getOrCreatePointsFromArgs(req, args...) if err != nil { return nil, err } @@ -75,12 +97,12 @@ func (p Points) Functions(pat *pattern.Pattern) map[string]govaluate.ExpressionF ErrInvalidArguments) } - points, err := p.getOrCreateFromArgs(pat, args...) + points, err := t.getOrCreatePointsFromArgs(req, args...) if err != nil { return nil, err } - return points[0].Vector().Y - points[1].Vector().Y, nil + return points[1].Vector().Y - points[0].Vector().Y, nil }, "XDistanceBetween": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { @@ -88,28 +110,40 @@ func (p Points) Functions(pat *pattern.Pattern) map[string]govaluate.ExpressionF ErrInvalidArguments) } - points, err := p.getOrCreateFromArgs(pat, args...) + points, err := t.getOrCreatePointsFromArgs(req, args...) + if err != nil { + return nil, err + } + + return points[1].Vector().X - points[0].Vector().X, nil + }, + "LineLength": func(args ...interface{}) (interface{}, error) { + if len(args) != 1 { + return nil, fmt.Errorf("function LineLength() requires 2 arguments: %w", ErrInvalidArguments) + } + + line, err := t.getOrCreateLinesFromArgs(req, args...) if err != nil { return nil, err } - return points[0].Vector().X - points[1].Vector().X, nil + return line[0].Length() }, }) return functions } -func (p Points) getOrCreateFromArgs(pat *pattern.Pattern, args ...interface{}) ([]point.Point, error) { +func (t Template) getOrCreatePointsFromArgs(req request, args ...interface{}) ([]point.Point, error) { points := make([]point.Point, 0, len(args)) for i, arg := range args { - id, err := toPointID(arg) + id, err := toID(arg) if err != nil { - return nil, fmt.Errorf("parsing args[%d] to pointID: %w", i, err) + return nil, fmt.Errorf("parsing args[%d] to ID: %w", i, err) } - newPoint, err := p.getOrCreate(id, pat, 0) + newPoint, err := t.getOrCreatePoint(id, req, 0) if err != nil { return nil, fmt.Errorf("get or create point %q: %w", id, err) } @@ -120,7 +154,27 @@ func (p Points) getOrCreateFromArgs(pat *pattern.Pattern, args ...interface{}) ( return points, nil } -func toPointID(arg interface{}) (point.ID, error) { +func (t Template) getOrCreateLinesFromArgs(req request, args ...interface{}) ([]path.Path, error) { + points := make([]path.Path, 0, len(args)) + + for i, arg := range args { + id, err := toID(arg) + if err != nil { + return nil, fmt.Errorf("parsing args[%d] to ID: %w", i, err) + } + + newPoint, err := t.getOrCreateLine(id, req, 0) + if err != nil { + return nil, fmt.Errorf("get or create line %q: %w", id, err) + } + + points = append(points, newPoint) + } + + return points, nil +} + +func toID(arg interface{}) (util.ID, error) { v1, ok := arg.(string) if !ok { f, ok := arg.(float64) @@ -131,63 +185,66 @@ func toPointID(arg interface{}) (point.ID, error) { v1 = strconv.FormatFloat(f, 'f', -1, 64) } - id1 := point.ID(v1) - - return id1, nil + return util.ID(v1), nil } // BetweenPoint contains the template information for a point in between two other points. type BetweenPoint struct { - From point.ID `yaml:"from"` - To point.ID `yaml:"to"` - Offset *Value `yaml:"offset"` + From util.ID `yaml:"from"` + To util.ID `yaml:"to"` + Offset *Value `yaml:"offset"` } -func (p Points) evaluationFunctions() map[string]govaluate.ExpressionFunction { +func (t Template) evaluationFunctions() map[string]govaluate.ExpressionFunction { return map[string]govaluate.ExpressionFunction{ "acos": func(args ...interface{}) (interface{}, error) { if len(args) != 1 { - return nil, fmt.Errorf("function acos() requires 1 argument: %w", - ErrInvalidArguments) + return nil, fmt.Errorf("function acos() requires 1 argument: %w", ErrInvalidArguments) } x, ok := args[0].(float64) if !ok { - return nil, fmt.Errorf("evaluate acos(): parsing %q as float64: %w", args[0], - ErrInvalidArguments) + return nil, fmt.Errorf("evaluate acos(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } return math.Acos(x), nil }, "asin": func(args ...interface{}) (interface{}, error) { if len(args) != 1 { - return nil, fmt.Errorf("function asin() requires 1 argument: %w", - ErrInvalidArguments) + return nil, fmt.Errorf("function asin() requires 1 argument: %w", ErrInvalidArguments) } x, ok := args[0].(float64) if !ok { - return nil, fmt.Errorf("evaluate asin(): parsing %q as float64: %w", args[0], - ErrInvalidArguments) + return nil, fmt.Errorf("evaluate asin(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } return math.Asin(x), nil }, + "abs": func(args ...interface{}) (interface{}, error) { + if len(args) != 1 { + return nil, fmt.Errorf("function abs() requires 1 argument: %w", ErrInvalidArguments) + } + + x, ok := args[0].(float64) + if !ok { + return nil, fmt.Errorf("evaluate abs(): parsing %q as float64: %w", args[0], ErrInvalidArguments) + } + + return math.Abs(x), nil + }, "atan2": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { - return nil, fmt.Errorf("function atan2() requires 2 arguments: %w", - ErrInvalidArguments) + return nil, fmt.Errorf("function atan2() requires 2 arguments: %w", ErrInvalidArguments) } x, ok := args[0].(float64) if !ok { - return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], - ErrInvalidArguments) + return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } y, ok := args[1].(float64) if !ok { - return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], - ErrInvalidArguments) + return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } return math.Atan2(x, y), nil @@ -195,53 +252,39 @@ func (p Points) evaluationFunctions() map[string]govaluate.ExpressionFunction { } } -// AddToPattern will add all points to the provided [pattern.Pattern]. -func (p Points) AddToPattern(pat *pattern.Pattern) error { - for id := range p { - if pat.GetPoint(id) == nil { - err := p.addSingleToPattern(id, pat, 0) - if err != nil { - return err - } - } - } - - return nil -} - -func (p Points) addSingleToPattern(id point.ID, pat *pattern.Pattern, depth int) (err error) { - templatePoint, ok := p[id] - if !ok { - return ErrPointNotFound +func (t Template) createPoint(id util.ID, req request, depth int) (p point.Point, err error) { + templatePoint, err := t.templatePoint(req.name, id) + if err != nil { + return nil, fmt.Errorf("creating point: %w", err) } var newPoint point.Point switch { case templatePoint.RelativeTo != nil && templatePoint.Polar != nil: - newPoint, err = p.createPolar(id, pat, depth) + newPoint, err = t.createPolar(id, req, depth) if err != nil { - return err + return nil, err } case templatePoint.RelativeTo != nil: - newPoint, err = p.createRelative(id, pat, depth) + newPoint, err = t.createRelative(id, req, depth) if err != nil { - return err + return nil, err } case templatePoint.Between != nil: - newPoint, err = p.createBetween(id, pat, depth) + newPoint, err = t.createBetween(id, req, depth) if err != nil { - return err + return nil, err } case templatePoint.Extend != nil: - newPoint, err = p.createExtend(id, pat, depth) + newPoint, err = t.createExtend(id, req, depth) if err != nil { - return err + return nil, err } default: - x, y, r, err := templatePoint.Position.evaluate(pat.Parameters(), p.Functions(pat)) + x, y, r, err := templatePoint.Position.evaluate(req.dimensions.Parameters(), t.functions(req)) if err != nil { - return err + return nil, err } newPoint = point.NewAbsolutePoint(x, y, r, id) @@ -251,19 +294,13 @@ func (p Points) addSingleToPattern(id point.ID, pat *pattern.Pattern, depth int) newPoint.SetHide() } - pat.AddPoint(newPoint) - - return nil + return newPoint, nil } -func (p Points) createRelative( - id point.ID, - pat *pattern.Pattern, - depth int, -) (*point.RelativePoint, error) { - templatePoint, ok := p[id] - if !ok { - return nil, ErrPointNotFound +func (t Template) createRelative(id util.ID, req request, depth int) (*point.RelativePoint, error) { + templatePoint, err := t.templatePoint(req.name, id) + if err != nil { + return nil, err } relativePointID := *templatePoint.RelativeTo @@ -271,12 +308,12 @@ func (p Points) createRelative( return nil, ErrRelativePointRecursion } - relativePoint, err := p.getOrCreate(relativePointID, pat, depth) + relativePoint, err := t.getOrCreatePoint(relativePointID, req, depth) if err != nil { return nil, err } - x, y, r, err := templatePoint.Position.evaluate(pat.Parameters(), p.Functions(pat)) + x, y, r, err := templatePoint.Position.evaluate(req.dimensions.Parameters(), t.functions(req)) if err != nil { return nil, err } @@ -287,29 +324,29 @@ func (p Points) createRelative( } //nolint:ireturn,dupl -func (p Points) createBetween(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { - newPoint, ok := p[id] - if !ok { - return nil, ErrPointNotFound +func (t Template) createBetween(id util.ID, req request, depth int) (point.Point, error) { + newPoint, err := t.templatePoint(req.name, id) + if err != nil { + return nil, err } if newPoint.Between.To == id || newPoint.Between.From == id || depth > maxRecursionDepth { return nil, ErrRelativePointRecursion } - fromPoint, err := p.getOrCreate(newPoint.Between.From, pat, depth) + fromPoint, err := t.getOrCreatePoint(newPoint.Between.From, req, depth) if err != nil { return nil, err } - toPoint, err := p.getOrCreate(newPoint.Between.To, pat, depth) + toPoint, err := t.getOrCreatePoint(newPoint.Between.To, req, depth) if err != nil { return nil, err } - params := pat.Parameters() + params := req.dimensions.Parameters() - offset, err := newPoint.Between.Offset.Evaluate(params, p.Functions(pat)) + offset, err := newPoint.Between.Offset.Evaluate(params, t.functions(req)) if err != nil { return nil, err } @@ -317,47 +354,60 @@ func (p Points) createBetween(id point.ID, pat *pattern.Pattern, depth int) (poi return point.NewBetweenPoint(fromPoint, toPoint, offset, id), nil } -//nolint:ireturn -func (p Points) getOrCreate(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { - if pat.GetPoint(id) == nil { - err := p.addSingleToPattern(id, pat, depth+1) +func (t Template) getOrCreatePoints(ids []util.ID, req request, depth int) ([]point.Point, error) { + points := make([]point.Point, 0, len(ids)) + + for _, id := range ids { + createPoint, err := t.getOrCreatePoint(id, req, depth) if err != nil { return nil, err } + + points = append(points, createPoint) } - createdPoint := pat.GetPoint(id) - if createdPoint == nil { - panic("getPoint cannot be nil") + return points, nil +} + +//nolint:ireturn +func (t Template) getOrCreatePoint(id util.ID, req request, depth int) (point.Point, error) { + p, ok := req.points[id] + if ok { + return p, nil } - return createdPoint, nil + newPoint, err := t.createPoint(id, req, depth+1) + if err != nil { + return nil, fmt.Errorf("creating point %q: %w", id, err) + } + + req.points[id] = newPoint + + return newPoint, nil } //nolint:ireturn,dupl -func (p Points) createExtend(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { - newPoint, ok := p[id] - if !ok { - return nil, ErrPointNotFound +func (t Template) createExtend(id util.ID, req request, depth int) (point.Point, error) { + newPoint, err := t.templatePoint(req.name, id) + if err != nil { + return nil, err } if newPoint.Extend.To == id || newPoint.Extend.From == id || depth > maxRecursionDepth { return nil, ErrRelativePointRecursion } - fromPoint, err := p.getOrCreate(newPoint.Extend.From, pat, depth) + fromPoint, err := t.getOrCreatePoint(newPoint.Extend.From, req, depth) if err != nil { return nil, err } - toPoint, err := p.getOrCreate(newPoint.Extend.To, pat, depth) + toPoint, err := t.getOrCreatePoint(newPoint.Extend.To, req, depth) if err != nil { return nil, err } - params := pat.Parameters() - - offset, err := newPoint.Extend.Offset.Evaluate(params, p.Functions(pat)) + offset, err := newPoint.Extend.Offset.Evaluate(req.dimensions.Parameters(), t.functions(req)) if err != nil { return nil, err } @@ -366,10 +416,10 @@ func (p Points) createExtend(id point.ID, pat *pattern.Pattern, depth int) (poin } //nolint:ireturn -func (p Points) createPolar(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { - templatePoint, ok := p[id] - if !ok { - return nil, ErrPointNotFound +func (t Template) createPolar(id util.ID, req request, depth int) (point.Point, error) { + templatePoint, err := t.templatePoint(req.name, id) + if err != nil { + return nil, err } relativePointID := *templatePoint.RelativeTo @@ -377,25 +427,25 @@ func (p Points) createPolar(id point.ID, pat *pattern.Pattern, depth int) (point return nil, ErrRelativePointRecursion } - relativePoint, err := p.getOrCreate(relativePointID, pat, depth) + relativePoint, err := t.getOrCreatePoint(relativePointID, req, depth) if err != nil { return nil, err } - x, y, err := templatePoint.Polar.evaluate(pat.Parameters(), p.Functions(pat)) + x, y, r, err := templatePoint.Polar.evaluate(req.dimensions.Parameters(), t.functions(req)) if err != nil { return nil, err } return point.NewRelativePoint(relativePoint). - WithXOffset(x).WithYOffset(y).MarkWith(id), nil + WithXOffset(x).WithYOffset(y).MarkWith(id).WithRotationOffset(r), nil } // ExtendPoint describes how to draw a new point that extends in line with two points. type ExtendPoint struct { - From point.ID `yaml:"from"` - To point.ID `yaml:"to"` - Offset *Value `yaml:"offset"` + From util.ID `yaml:"from"` + To util.ID `yaml:"to"` + Offset *Value `yaml:"offset"` } // PolarPoint describes how to draw a new point with a direction and a distance from the current @@ -408,16 +458,16 @@ type PolarPoint struct { func (p PolarPoint) evaluate( params govaluate.MapParameters, funcs map[string]govaluate.ExpressionFunction, -) (x, y float64, err error) { +) (x, y, r float64, err error) { rotation, err := p.Rotation.Evaluate(params, funcs) if err != nil { - return 0, 0, err + return 0, 0, 0, err } length, err := p.Length.Evaluate(params, funcs) if err != nil { - return 0, 0, err + return 0, 0, 0, err } - return math.Sin(rotation) * -length, math.Cos(rotation) * -length, nil + return math.Cos(rotation) * length, math.Sin(rotation) * length, rotation, nil } diff --git a/pkg/template/point_test.go b/pkg/template/point_test.go new file mode 100644 index 0000000..99d5943 --- /dev/null +++ b/pkg/template/point_test.go @@ -0,0 +1,289 @@ +package template_test + +import ( + _ "embed" + "git.wtrh.nl/patterns/gopatterns/pkg/util" + "math" + "testing" + + "git.wtrh.nl/patterns/gopatterns/pkg/dimensions" + "git.wtrh.nl/patterns/gopatterns/pkg/position" + "git.wtrh.nl/patterns/gopatterns/pkg/position/testutil" + "git.wtrh.nl/patterns/gopatterns/pkg/template" + "git.wtrh.nl/patterns/gopatterns/pkg/vector" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +//go:embed fixtures/absolute_points.yaml +var absolutePoints []byte + +//go:embed fixtures/relative_points.yaml +var relativePoints []byte + +//go:embed fixtures/between_points.yaml +var betweenPoints []byte + +//go:embed fixtures/extend_points.yaml +var extendPoints []byte + +//go:embed fixtures/polar_points.yaml +var polarPoints []byte + +//go:embed fixtures/functions.yaml +var functions []byte + +//go:embed fixtures/evaluation_functions.yaml +var evaluationFunctions []byte + +//go:embed fixtures/references.yaml +var references []byte + +func TestAbsolutePoint(t *testing.T) { + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(absolutePoints, temp)) + + panel, err := temp.GetPanel(template.Request{ + Dims: dimensions.Dimensions{"test": dimensions.Dimension{ + Name: "Test", + Value: 1.3, + }}, + Panel: "body", + }) + require.NoError(t, err) + + p1, ok := panel.Points["1"] + require.True(t, ok) + require.Equal(t, position.Position{ + Vector: vector.Vector{ + X: 1.3, + Y: 14.3, + }, + Rotation: 0, + }, p1.Position()) + + p2, ok := panel.Points["2"] + require.True(t, ok) + require.Equal(t, position.Position{ + Vector: vector.Vector{ + X: 2.6, + Y: -3, + }, + Rotation: 0, + }, p2.Position()) +} + +func TestBetweenPoints(t *testing.T) { + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(betweenPoints, temp)) + + panel, err := temp.GetPanel(template.Request{ + Dims: dimensions.Dimensions{"test": dimensions.Dimension{ + Name: "Test", + Value: 0.5, + }}, + Panel: "body", + }) + require.NoError(t, err) + + p1, ok := panel.Points["1"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 4, + Y: 14.3, + }, + Rotation: 0, + }, p1.Position(), 1e-10) + + p2, ok := panel.Points["2"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 6, + Y: 11.3, + }, + Rotation: 0, + }, p2.Position(), 1e-10) + + p3, ok := panel.Points["3"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 5, + Y: 12.8, + }, + Rotation: math.Atan2(-3, 2) - math.Pi/2, + }, p3.Position(), 1e-10) +} + +func TestRelativePoints(t *testing.T) { + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(relativePoints, temp)) + + panel, err := temp.GetPanel(template.Request{ + Dims: dimensions.Dimensions{"test": dimensions.Dimension{ + Name: "Test", + Value: 1.3, + }}, + Panel: "body", + }) + require.NoError(t, err) + + p1, ok := panel.Points["1"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 1.3, + Y: 14.3, + }, + Rotation: 0, + }, p1.Position(), 1e-10) + + p2, ok := panel.Points["2"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 3.9, + Y: 11.3, + }, + Rotation: 0, + }, p2.Position(), 1e-10) +} + +func TestExtendPoints(t *testing.T) { + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(extendPoints, temp)) + + panel, err := temp.GetPanel(template.Request{ + Dims: dimensions.Dimensions{"test": dimensions.Dimension{ + Name: "Test", + Value: 1.3, + }}, + Panel: "body", + }) + require.NoError(t, err) + + p1, ok := panel.Points["1"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{}, p1.Position(), 1e-10) + + p2, ok := panel.Points["2"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 4, + Y: 3, + }, + Rotation: 0, + }, p2.Position(), 1e-10) + + p3, ok := panel.Points["3"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 8, + Y: 6, + }, + Rotation: math.Atan2(3, 4) - math.Pi/2, + }, p3.Position(), 1e-10) +} + +func TestPolarPoints(t *testing.T) { + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(polarPoints, temp)) + + panel, err := temp.GetPanel(template.Request{ + Dims: dimensions.Dimensions{"test": dimensions.Dimension{ + Name: "Test", + Value: 1.3, + }}, + Panel: "body", + }) + require.NoError(t, err) + + p1, ok := panel.Points["1"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{}, p1.Position(), 1e-10) + + p2, ok := panel.Points["2"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 1, + Y: 0, + }, + Rotation: 0, + }, p2.Position(), 1e-10) + + p3, ok := panel.Points["3"] + require.True(t, ok) + testutil.EqualPosition(t, position.Position{ + Vector: vector.Vector{ + X: 1, + Y: 1, + }, + Rotation: math.Pi / 2, + }, p3.Position(), 1e-10) +} + +func TestFunctions(t *testing.T) { + tests := map[string]struct { + result float64 + }{ + "distance": {result: 5}, + "angle": {result: math.Atan2(3, 4)}, + "yDistance": {result: 3}, + "xDistance": {result: 4}, + "lineLength": {result: 12}, + "lineLength2": {result: 4}, + } + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(functions, temp)) + + panel, err := temp.GetPanel(template.Request{Panel: "test"}) + require.NoError(t, err) + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p, ok := panel.Points[util.ID(name)] + require.True(t, ok) + require.InDelta(t, test.result, p.Vector().X, 1e-10) + }) + } +} + +func TestEvaluationFunctions(t *testing.T) { + tests := map[string]struct { + result float64 + }{ + "acos": {result: math.Acos(0.5)}, + "asin": {result: math.Asin(math.Pi / 2)}, + "atan2": {result: math.Atan2(1, 1)}, + "abs1": {result: math.Abs(-1.4)}, + "abs2": {result: math.Abs(3.5)}, + } + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(evaluationFunctions, temp)) + + panel, err := temp.GetPanel(template.Request{Panel: "test"}) + require.NoError(t, err) + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p, ok := panel.Points[util.ID(name)] + require.True(t, ok) + require.InDelta(t, test.result, p.Vector().X, 1e-10) + }) + } +} + +func TestReferences(t *testing.T) { + temp := &template.Template{} + require.NoError(t, yaml.Unmarshal(references, temp)) + + panel, err := temp.GetPanel(template.Request{Panel: "body"}) + require.NoError(t, err) + + _ = panel +} diff --git a/pkg/pattern/template/position.go b/pkg/template/position.go similarity index 100% rename from pkg/pattern/template/position.go rename to pkg/template/position.go diff --git a/pkg/template/template.go b/pkg/template/template.go new file mode 100644 index 0000000..fcc5c28 --- /dev/null +++ b/pkg/template/template.go @@ -0,0 +1,79 @@ +package template + +import ( + "git.wtrh.nl/patterns/gopatterns/pkg/dimensions" + "git.wtrh.nl/patterns/gopatterns/pkg/path" + "git.wtrh.nl/patterns/gopatterns/pkg/pattern/panel" + "git.wtrh.nl/patterns/gopatterns/pkg/point" + "git.wtrh.nl/patterns/gopatterns/pkg/util" +) + +type Template struct { + Name string `yaml:"name"` + Points Points `yaml:"points"` + Lines Lines `yaml:"lines"` + Panels Panels `yaml:"panels"` + Version string `yaml:"version"` +} + +type Request struct { + Dims dimensions.Dimensions + Panel string +} + +type request struct { + dimensions dimensions.Dimensions + name string + lines map[util.ID]path.Path + points map[util.ID]point.Point +} + +func (t Template) GetPanel(req Request) (panel.Panel, error) { + p, ok := t.Panels[req.Panel] + if !ok { + return panel.Panel{}, ErrPanelNotFound + } + + r := request{ + dimensions: req.Dims, + name: req.Panel, + lines: make(map[util.ID]path.Path), + points: make(map[util.ID]point.Point), + } + + result := panel.Panel{ + Name: req.Panel, + Lines: make(map[util.ID]path.Path), + Points: make(map[util.ID]point.Point), + Dimensions: req.Dims, + } + + for id := range p.Lines { + line, err := t.getOrCreateLine(id, r, 0) + if err != nil { + return panel.Panel{}, err + } + + result.Lines[id] = line + } + + for id := range p.Points { + newPoint, err := t.getOrCreatePoint(id, r, 0) + if err != nil { + return panel.Panel{}, err + } + + result.Points[id] = newPoint + } + + for id := range t.Points { + newPoint, err := t.getOrCreatePoint(id, r, 0) + if err != nil { + return panel.Panel{}, err + } + + result.Points[id] = newPoint + } + + return result, nil +} diff --git a/pkg/pattern/template/value.go b/pkg/template/value.go similarity index 100% rename from pkg/pattern/template/value.go rename to pkg/template/value.go diff --git a/pkg/util/id.go b/pkg/util/id.go new file mode 100644 index 0000000..3a7fe4f --- /dev/null +++ b/pkg/util/id.go @@ -0,0 +1,28 @@ +package util + +import "strings" + +// ID defines a point id. +type ID string + +func (i ID) Panel() string { + split := strings.Split(string(i), ".") + if len(split) < 2 { + return "" + } + + return split[0] +} + +func (i ID) Name() string { + split := strings.Split(string(i), ".") + if len(split) < 2 { + return split[0] + } + + return split[1] +} + +func (i ID) Deref() ID { + return ID(i.Name()) +} diff --git a/pkg/util/id_test.go b/pkg/util/id_test.go new file mode 100644 index 0000000..dad652b --- /dev/null +++ b/pkg/util/id_test.go @@ -0,0 +1,33 @@ +package util_test + +import ( + "git.wtrh.nl/patterns/gopatterns/pkg/util" + "github.com/stretchr/testify/require" + "testing" +) + +func TestID(t *testing.T) { + tests := map[string]struct { + panel, name string + }{ + "body.2": { + panel: "body", + name: "2", + }, + "2": { + panel: "", + name: "2", + }, + "1.test": { + panel: "1", + name:"test", + }, + } + for testName, tt := range tests { + t.Run(testName, func(t *testing.T) { + id := util.ID(testName) + require.Equal(t, tt.panel, id.Panel()) + require.Equal(t, tt.name, id.Name()) + }) + } +} \ No newline at end of file diff --git a/spec/pattern.v2.yaml b/spec/pattern.v2.yaml new file mode 100644 index 0000000..76fc030 --- /dev/null +++ b/spec/pattern.v2.yaml @@ -0,0 +1,125 @@ +--- +$schema: "https://json-schema.org/draft-04/schema" +id: "https://stsci.edu/schemas/yaml-schema/draft-01" +title: + YAML Schema + +type: object +properties: + version: + type: string + points: + $ref: '#/components/schemas/points' + lines: + $ref: '#/components/schemas/lines' + panels: + type: object + additionalProperties: + type: object + properties: + allowances: + type: object + properties: + hem: + type: string + seam: + type: string + points: + $ref: '#/components/schemas/points' + lines: + $ref: '#/components/schemas/lines' + information: + allOf: + - $ref: '#/components/schemas/point' + - type: object + properties: + anchor: + type: string + enum: [top, center, bottom, left, right] + +components: + schemas: + point: + type: object + properties: + position: + $ref: '#/components/schemas/position' + relativeTo: + $ref: '#/components/schemas/pointID' + polar: + $ref: '#/components/schemas/polar' + description: + type: string + between: + $ref: '#/components/schemas/between' + hide: + type: bool + extend: + $ref: '#/components/schemas/between' + points: + type: object + additionalProperties: + $ref: '#/components/schemas/point' + position: + type: object + properties: + y: + $ref: '#/components/schemas/expression' + x: + $ref: '#/components/schemas/expression' + rotation: + $ref: '#/components/schemas/expression' + polar: + type: object + properties: + length: + $ref: '#/components/schemas/expression' + rotation: + $ref: '#/components/schemas/expression' + between: + type: object + properties: + from: + $ref: '#/components/schemas/pointID' + to: + $ref: '#/components/schemas/pointID' + offset: + $ref: '#/components/schemas/expression' + line: + type: object + properties: + through: + type: array + items: + $ref: '#/components/schemas/pointID' + curve: + type: object + properties: + start: + $ref: '#/components/schemas/pointID' + end: + $ref: '#/components/schemas/pointID' + style: + type: object + properties: + thickness: + type: number + hide: + type: bool + reference: + type: string + lines: + type: object + additionalProperties: + $ref: '#/components/schemas/line' + pointID: + oneOf: + - type: integer + - type: string + expression: + oneOf: + - type: number + - type: string + + + diff --git a/spec/pattern.yaml b/spec/pattern.yaml index 0737295..8d9d296 100644 --- a/spec/pattern.yaml +++ b/spec/pattern.yaml @@ -6,6 +6,8 @@ title: type: object properties: + version: + type: string points: $ref: '#/components/schemas/points' panels: @@ -102,6 +104,8 @@ components: properties: thickness: type: number + reference: + type: string pointID: oneOf: - type: integer diff --git a/templates/dimension_names.yaml b/templates/dimension_names.yaml index e7dee77..54aa40a 100644 --- a/templates/dimension_names.yaml +++ b/templates/dimension_names.yaml @@ -33,6 +33,8 @@ sleeve_length_shirt: name: Sleeve length for shirts cuff_size: name: Cuff size +cuff_depth: + name: Cuff Depth bovenwijdte: name: Bovenwijdte diff --git a/templates/tailored_shirt_block.v2.yaml b/templates/tailored_shirt_block.v2.yaml new file mode 100644 index 0000000..5ac9866 --- /dev/null +++ b/templates/tailored_shirt_block.v2.yaml @@ -0,0 +1,498 @@ +--- +name: Basic Trouser Block +panels: + body: + allowances: + hem: 1cm + seam: 1cm + information: + position: + y: -10 + x: 10 + lines: + 1: + through: [14,8] + style: + thickness: 1 + 2: + through: [8, 7, 0, 1, 2, 3, 37, 19, 35, 6, 22] + 3: + through: [1,11,17,4] + 4: + through: [7,12,10] + 5: + through: [9,15,10,23,11] + 6: + through: [15,16] + curve: + start: 10 + 7: + through: [0,7a,8] + curve: + start: 7 + style: + thickness: 1 + 8: + through: [17,18,19] + 9: + through: [24,21] + style: + thickness: 1 + 10: + through: [21,22] + curve: + start: 20b + end: 20a + style: + thickness: 1 + 11: + through: [14,10,11a,17,25a,26,27a,24] + curve: + start: 14 + style: + thickness: 1 + 12: + through: [28,28a] + 13: + through: [22,29,29a] + 14: + through: [37,34] + curve: + start: 19 + end: 34a + style: + thickness: 1 + 15: + through: [33,36] + curve: + start: 33a + end: 36a + style: + thickness: 1 + 16: + through: [17,31,34] + curve: {} + style: + thickness: 0.6 + 17: + through: [17,30,33] + curve: {} + style: + thickness: 0.6 + 18: + through: [36,29b] + 19: + through: [34,33] + style: + thickness: 1 + 20: + through: [39,43,41] + curve: {} + style: + thickness: 0.6 + 21: + through: [39,42,41] + curve: {} + style: + thickness: 0.6 + 22: + through: [0,3,37] + style: + thickness: 1 + 23: + through: [22,29,29b,36] + style: + thickness: 1 + + points: + 0: + position: {} + 1: + position: + y: -(scye_depth) - 60 + 2: + position: + y: -(back_waist + 25 ) + 3: + position: + y: -(shirt_length) - 40 + 4: + relativeTo: 1 + position: + x: chest/2 + 100 + 5: + relativeTo: 0 + position: + x: DistanceBetween("1","4") + 6: + relativeTo: 3 + position: + x: DistanceBetween("1","4") + 7: + relativeTo: 0 + position: + x: neck_size/5 - 5 + 8: + relativeTo: 7 + position: + y: 45 + 9: + position: + y: -(DistanceBetween("0","1")/5 + 40) + 10: + relativeTo: 9 + position: + x: half_back + 40 + 11: + relativeTo: 1 + position: + x: half_back + 40 + 12: + relativeTo: 0 + position: + x: half_back + 40 + 14: + relativeTo: 12 + position: + x: 15 + y: 20 + 15: + relativeTo: 10 + position: + x: -100 + 16: + relativeTo: 10 + position: + y: -7.5 + 17: + relativeTo: 1 + position: + x: DistanceBetween("1","4")/2 + 5 + 18: + relativeTo: 17 + position: + y: -(DistanceBetween("1","2")+25) + 19: + relativeTo: 17 + position: + y: -DistanceBetween("1","3") + 20: + relativeTo: 5 + position: + y: -45 + 21: + relativeTo: 20 + position: + x: -(neck_size/5-10) + 22: + relativeTo: 20 + position: + y: -(neck_size/5-25) + 23: + relativeTo: 10 + position: + y: -15 + 24: + relativeTo: 21 + polar: + length: -DistanceBetween("8","14") + rotation: asin(abs(YDistanceBetween("21","23"))/abs(DistanceBetween("8","14"))) + 25: + relativeTo: 1 + position: + x: chest/3+40 + 26: + relativeTo: 25 + position: + y: 40 + 27: + between: + from: 26 + to: 24 + offset: 0.5 + 28: + relativeTo: 22 + position: + x: 15 + 28a: + relativeTo: 28 + position: + y: YDistanceBetween("28","3") + hide: true + 29: + relativeTo: 28 + position: + x: 35 + 29a: + relativeTo: 29 + position: + y: YDistanceBetween("29","3") + hide: true + 29b: + relativeTo: 29a + position: + y: DistanceBetween("35","36") + hide: true + 30: + relativeTo: 18 + position: + x: 25 + 31: + relativeTo: 18 + position: + x: -25 + 32: + relativeTo: 19 + position: + y: 80 + 33: + relativeTo: 32 + position: + x: 15 + 33a: + relativeTo: 33 + position: + x: DistanceBetween("33","36") + 34: + relativeTo: 32 + position: + x: -15 + 34a: + relativeTo: 34 + position: + x: -DistanceBetween("19","37") + 35: + between: + from: 6 + to: 19 + offset: 0.5 + 36: + relativeTo: 35 + position: + x: 30 + rotation: -pi/2 + 36a: + relativeTo: 36 + position: + x: -DistanceBetween("33","36") + 37: + between: + from: 3 + to: 19 + offset: 0.5 + 38: + relativeTo: 1 + position: + x: DistanceBetween("1","11")/2 + 20 + 39: + relativeTo: 38 + position: + y: -40 + 40: + relativeTo: 2 + position: + x: DistanceBetween("1","38") + y: -25 + 41: + relativeTo: 40 + position: + y: -160 + 42: + relativeTo: 40 + position: + x: 7.5 + 43: + relativeTo: 40 + position: + x: -7.5 + 7a: + relativeTo: 7 + polar: + length: 20 + rotation: 3*pi/4 + 11a: + relativeTo: 11 + position: + y: 30 + x: 10 + 20a: + relativeTo: 22 + position: + x: -DistanceBetween("21","20")*2 + 20b: + relativeTo: 21 + position: + y: -DistanceBetween("22","20")*2 + 27a: + relativeTo: 27 + position: + x: 10 + 25a: + relativeTo: 25 + position: + y: 7 + x: -30 + sleeve: + points: + 0: {} + 1: + relativeTo: 0 + position: + y: -(502.6 / 4 + 15) + 2: + relativeTo: 0 + position: + y: -(sleeve_length_shirt+60-cuff_depth) + 3: + between: + from: 2 + to: 1 + offset: 0.5 + 4: + relativeTo: 1 + position: + x: -(502.6/2 -5) + 5: + relativeTo: 4 + position: + y: -DistanceBetween("1","2") + 6: + relativeTo: 1 + position: + x: (502.6/2 -5) + 7: + relativeTo: 6 + position: + y: -DistanceBetween("1","2") + 8a: + between: + from: 4 + to: 0 + offset: 0.25 + 8: + relativeTo: 8a + position: + x: 5 + 9a: + between: + from: 4 + to: 0 + offset: 0.5 + 9: + relativeTo: 9a + position: + x: -12.5 + 10a: + between: + from: 4 + to: 0 + offset: 0.75 + 10: + relativeTo: 10a + position: + x: -22.5 + 11a: + between: + from: 0 + to: 6 + offset: 0.25 + 11: + relativeTo: 11a + position: + x: -15 + 12: + between: + from: 0 + to: 6 + offset: 0.5 + 13a: + between: + from: 0 + to: 6 + offset: 0.75 + 13: + relativeTo: 13a + position: + x: 12.5 + 14: + relativeTo: 5 + position: + x: DistanceBetween("5","2")/3+7.5 + 15: + relativeTo: 7 + position: + x: -DistanceBetween("5","14") + 3a: + between: + from: 14 + to: 4 + offset: 0.5 + 3aa: + relativeTo: 3a + position: + x: -7 + 3b: + between: + from: 15 + to: 6 + offset: 0.5 + 3bb: + relativeTo: 3b + position: + x: 7 + A: + relativeTo: 0 + position: + y: -250 + B: + relativeTo: 4 + position: + y: -DistanceBetween("1","A") + C: + relativeTo: 6 + position: + y: -DistanceBetween("1","A") + 16a: + between: + from: 14 + to: 2 + offset: 0.5 + hide: true + 16: + relativeTo: 16a + position: + rotation: pi/2 + 17: + relativeTo: 16 + position: + y: 150 + lines: + scye: + through: [4,8,9,10,0,11,12,13,6] + curve: {} + style: + thickness: 1 + 1: + through: [4,3aa,14] + curve: {} + style: + thickness: 1 + 2: + through: [14,2,15] + style: + thickness: 1 + 3: + through: [6,3bb,15] + curve: {} + style: + thickness: 1 + 4: + through: [0,1,3,2] + 0: + through: [14,4,0,6,15] + abc: + through: [B,A,C] + 5: + through: [16,17] + +