/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
tests/ClothesGraph.Model.Tests/PatternPieceTests.cs
239 строк
10 KB
docNemo
Запрос на слияние 'interface-to-spec' (
#1
) из interface-to-spec в main
09 авг 2026, 20:28
Верифицирован
09 авг 2026, 20:28
1bbc436
Код
Авторство
О чём код?
using System.Linq; using ClothesGraph.Geometry; using ClothesGraph.Model; using ClothesGraph.Model.Drafting; using ClothesGraph.Model.Formulas; using ClothesGraph.Model.Measurements; using ClothesGraph.Model.Pieces; using Xunit; namespace ClothesGraph.Model.Tests; public class PatternPieceTests { /// <summary> /// Каждый участок контура находится в ломаной по своим границам. /// </summary> /// <remarks> /// Ломаная склеена из всех участков подряд, и без границ по ней не сказать, /// где кончается один и начинается другой. Границы нужны, чтобы участок /// можно было показать на детали и выбрать указанием, а не вводом номера. /// </remarks> [Fact] public void Участки_контура_находятся_в_ломаной_по_границам() { var (document, piece, _) = BuildRectangle(); var geometry = Evaluate(document, piece); Assert.Equal(piece.Contour.Count, geometry.SegmentSpans.Count); for (var index = 0; index < piece.Contour.Count; index++) { var points = geometry.PointsOf(index); Assert.True(points.Count >= 2, $"участок {index + 1} пуст"); // Длина ломаной участка совпадает с длиной его линии на чертеже. var length = 0L; for (var step = 1; step < points.Count; step++) length += points[step - 1].DistanceTo(points[step]).Units; Assert.Equal( geometry.LengthOf(index).Millimetres, Length.FromUnits(length).Millimetres, precision: 1); } // Соседние участки стыкуются: конец одного есть начало следующего. for (var index = 0; index < piece.Contour.Count; index++) { var current = geometry.PointsOf(index); var next = geometry.PointsOf((index + 1) % piece.Contour.Count); Assert.Equal(current[^1], next[0]); } } /// <summary>Прямоугольная деталь со стороной, заданной меркой.</summary> private static (PatternDocument Document, PatternPiece Piece, NodeId Bottom) BuildRectangle() { var document = new PatternDocument("46"); document.Measurements.Add("Ш", Length.FromCentimetres(10)); var draft = document.Draft; var a = draft.Add(new FreePoint(NodeId.New(), "A", document.ParseFormula("0"), document.ParseFormula("0"))).Id; var b = draft.Add(new OffsetPoint(NodeId.New(), "B", a, document.ParseFormula("Ш"), document.ParseFormula("0"))).Id; var c = draft.Add(new OffsetPoint(NodeId.New(), "C", b, document.ParseFormula("6"), document.ParseFormula("90"))).Id; var d = draft.Add(new OffsetPoint(NodeId.New(), "D", a, document.ParseFormula("6"), document.ParseFormula("90"))).Id; var bottom = draft.Add(new SegmentNode(NodeId.New(), "AB", a, b)).Id; var right = draft.Add(new SegmentNode(NodeId.New(), "BC", b, c)).Id; var top = draft.Add(new SegmentNode(NodeId.New(), "CD", c, d)).Id; var left = draft.Add(new SegmentNode(NodeId.New(), "DA", d, a)).Id; var piece = new PatternPiece(PieceId.New(), "Полочка", document.ParseFormula("1")); piece.AddSegment(bottom); piece.AddSegment(right); piece.AddSegment(top); piece.AddSegment(left); return (document, piece, bottom); } private static PieceGeometry Evaluate(PatternDocument document, PatternPiece piece) { var evaluator = new DraftEvaluator(document.Draft); var values = document.ResolveMeasurements(); var evaluation = evaluator.Evaluate(values, document.FormulaUnit); var context = new Values(evaluation, values, document.FormulaUnit); return PieceBuilder.Build(piece, evaluation, context, document.FormulaUnit); } private sealed class Values( DraftEvaluation evaluation, System.Collections.Generic.IReadOnlyDictionary<MeasurementId, Length> measurements, LengthUnit unit) : IFormulaValues { public double GetMeasurement(MeasurementId id) => measurements[id].To(unit); public double GetNodeProperty(NodeId node, NodeProperty property) { if (evaluation.TryGetCurve(node, out var curve) && curve is not null) return property == NodeProperty.Length ? curve.Length.To(unit) : curve.DirectionAt(0); evaluation.TryGetPoint(node, out var point); return property == NodeProperty.X ? point.X.To(unit) : point.Y.To(unit); } } [Fact] public void Замкнутый_контур_строится_с_припуском() { var (document, piece, _) = BuildRectangle(); var geometry = Evaluate(document, piece); Assert.True(geometry.IsClosed); Assert.True(geometry.IsSound); Assert.NotEmpty(geometry.SeamLine); // Периметр прямоугольника 10 на 6 сантиметров. Assert.Equal(32, geometry.Perimeter.Centimetres, precision: 1); } [Fact] public void Разрыв_контура_обнаруживается_и_указывает_место() { var (document, piece, _) = BuildRectangle(); // Убираем нижний участок: контур перестаёт смыкаться. piece.RemoveSegmentAt(0); var geometry = Evaluate(document, piece); Assert.False(geometry.IsClosed); Assert.NotEmpty(geometry.Gaps); // Припуск по незамкнутому контуру не строится: он не определён. Assert.Empty(geometry.SeamLine); } [Fact] public void Контур_перестраивается_вслед_за_чертежом() { var (document, piece, _) = BuildRectangle(); var before = Evaluate(document, piece).Perimeter; var width = document.Measurements.FindByName("Ш")!; document.Measurements.SetValue(width.Id, Length.FromCentimetres(20)); document.MeasurementChanged(width.Id); var after = Evaluate(document, piece).Perimeter; // Ширина выросла на 10 сантиметров, периметр — на 20. Assert.Equal(before.Centimetres + 20, after.Centimetres, precision: 1); } [Fact] public void Припуск_переопределяется_на_отдельном_участке() { var (document, piece, _) = BuildRectangle(); piece.SetSegmentAllowance(0, document.ParseFormula("4")); var geometry = Evaluate(document, piece); var lowest = geometry.SeamLine.Min(point => point.Y.Centimetres); // По нижнему участку припуск 4 сантиметра вместо общего одного. Assert.True(lowest < -3, $"низ припуска должен уйти ниже трёх сантиметров, а ушёл на {lowest}"); } [Fact] public void Надсечка_сохраняет_положение_по_контуру_при_смене_размера() { var (document, piece, _) = BuildRectangle(); piece.AddNotch(new Notch(document.ParseFormula("5"), "боковой шов")); var before = Evaluate(document, piece); var notchBefore = Assert.Single(before.Notches); // Надсечка отложена на 5 сантиметров от начала контура, то есть // на нижней стороне; это положение не должно зависеть от размера. Assert.Equal(5, notchBefore.Position.X.Centimetres, precision: 1); Assert.Equal(0, notchBefore.Position.Y.Centimetres, precision: 1); var width = document.Measurements.FindByName("Ш")!; document.Measurements.SetValue(width.Id, Length.FromCentimetres(20)); document.MeasurementChanged(width.Id); var after = Evaluate(document, piece); var notchAfter = Assert.Single(after.Notches); Assert.Equal(5, notchAfter.Position.X.Centimetres, precision: 1); Assert.Equal(0, notchAfter.Position.Y.Centimetres, precision: 1); } [Fact] public void Деталь_со_сгибом_разворачивается_зеркально() { var (document, piece, _) = BuildRectangle(); // Левая сторона объявлена линией сгиба. piece.FoldSegmentIndex = 3; var evaluator = new DraftEvaluator(document.Draft); var values = document.ResolveMeasurements(); var evaluation = evaluator.Evaluate(values, document.FormulaUnit); var geometry = PieceBuilder.Build( piece, evaluation, new Values(evaluation, values, document.FormulaUnit), document.FormulaUnit); var unfolded = PieceBuilder.Unfold(piece, geometry, evaluation); Assert.True(piece.IsFolded); Assert.True(unfolded.Count > geometry.Outline.Count); // Отражение относительно левой стороны уводит полотно в отрицательные x. Assert.True(unfolded.Min(point => point.X.Centimetres) < -9); } [Fact] public void Деталь_объявляет_зависимости_для_перестроения() { var (document, piece, bottom) = BuildRectangle(); Assert.Contains(bottom, piece.Dependencies); Assert.Equal(4, piece.Dependencies.Count()); } }