/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.Model/Storage/DocumentMapper.cs
481 строка
19 KB
docNemo
Запрос на слияние 'interface-to-spec' (
#1
) из interface-to-spec в main
09 авг 2026, 20:28
Верифицирован
09 авг 2026, 20:28
1bbc436
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Linq; using ClothesGraph.Geometry; using ClothesGraph.Model.Drafting; using ClothesGraph.Model.Measurements; using ClothesGraph.Model.Pieces; using ClothesGraph.Model.Seams; using ClothesGraph.Model.Underlay; namespace ClothesGraph.Model.Storage; /// <summary>Перевод документа между моделью и записью в файле.</summary> internal static class DocumentMapper { public static DocumentDto ToDto(PatternDocument document) { var dto = new DocumentDto { FormulaUnit = document.FormulaUnit.ToString(), DisplayUnit = document.DisplayUnit.ToString(), Measurements = document.Measurements.Measurements .Select(measurement => new MeasurementDto { Id = measurement.Id.Value, Name = measurement.DisplayName, Units = measurement.Value.Units, Description = measurement.Description }) .OrderBy(measurement => measurement.Name) .ToList() }; dto.Sizes = ToDto(document.Sizes, document.Measurements); // Построения пишутся в порядке зависимостей: при чтении узел нельзя // добавить раньше тех, на которые он ссылается. dto.Nodes = document.Draft.TopologicalOrder().Select(ToDto).ToList(); dto.Pieces = document.Pieces.Select(ToDto).ToList(); dto.Print = new PrintSettingsDto { PageSizeName = document.Print.PageSizeName, PageWidthUnits = document.Print.PageWidth.Units, PageHeightUnits = document.Print.PageHeight.Units, IsLandscape = document.Print.IsLandscape, MarginUnits = document.Print.Margin.Units, JoinWithOverlap = document.Print.JoinWithOverlap, OverlapUnits = document.Print.Overlap.Units, ExcludedFromPrint = document.Print.ExcludedFromPrint .Select(piece => piece.Value).ToList(), Placements = document.Print.Placements .Select(pair => new PlacementDto { Piece = pair.Key.Value, OffsetXUnits = pair.Value.Offset.X.Units, OffsetYUnits = pair.Value.Offset.Y.Units, RotationDegrees = pair.Value.RotationDegrees }) .ToList() }; dto.Adjustments = document.Adjustments.All() .Select(adjustment => new AdjustmentDto { Size = adjustment.Size, Node = adjustment.Node.Value, OffsetXUnits = adjustment.Offset.X.Units, OffsetYUnits = adjustment.Offset.Y.Units }) .ToList(); dto.Seams = document.Seams.Seams .Select(seam => new SeamDto { Id = seam.Id.Value, Name = seam.Name, FirstPiece = seam.First.Piece.Value, FirstSegment = seam.First.SegmentIndex, SecondPiece = seam.Second.Piece.Value, SecondSegment = seam.Second.SegmentIndex, Reversed = seam.Reversed, Ease = seam.Ease is null ? null : ExpressionMapper.ToDto(seam.Ease), ToleranceUnits = seam.Tolerance.Units }) .OrderBy(seam => seam.Name) .ToList(); dto.Underlays = document.Underlays .Select(underlay => new UnderlayDto { Id = underlay.Id.Value, Name = underlay.Name, Entry = FileFormat.ImagesFolder + underlay.Id.Value.ToString("N") + ".bin", PixelWidth = underlay.PixelWidth, PixelHeight = underlay.PixelHeight, OriginXUnits = underlay.Origin.X.Units, OriginYUnits = underlay.Origin.Y.Units, RotationDegrees = underlay.RotationDegrees, Opacity = underlay.Opacity, UnitsPerPixel = underlay.UnitsPerPixel, Order = underlay.Order, IsVisible = underlay.IsVisible, IsCalibrated = underlay.IsCalibrated }) .ToList(); return dto; } /// <summary>Восстанавливает подложки; содержимое изображений берётся из архива.</summary> internal static void RestoreUnderlays( PatternDocument document, DocumentDto dto, Func<string, byte[]?> readEntry) { foreach (var underlayDto in dto.Underlays) { var content = readEntry(underlayDto.Entry); // Запись изображения потеряна: подложка без картинки бессмысленна, // но остальной документ из-за этого терять нельзя. if (content is null) continue; var underlay = new UnderlayImage( UnderlayId.FromGuid(underlayDto.Id), underlayDto.Name, content, underlayDto.PixelWidth, underlayDto.PixelHeight) { Origin = new Point2( Length.FromUnits(underlayDto.OriginXUnits), Length.FromUnits(underlayDto.OriginYUnits)), RotationDegrees = underlayDto.RotationDegrees, Opacity = underlayDto.Opacity, Order = underlayDto.Order, IsVisible = underlayDto.IsVisible }; // Признака нет в файлах, записанных до разделения масштаба показа и // калибровки: там калиброванной считалась подложка с ненулевым // масштабом, и это правило сохраняется. underlay.Restore( underlayDto.UnitsPerPixel, underlayDto.IsCalibrated ?? underlayDto.UnitsPerPixel > 0); document.Underlays.Add(underlay); } } /// <summary>Имя записи архива для изображения подложки.</summary> internal static string EntryFor(UnderlayImage underlay) => FileFormat.ImagesFolder + underlay.Id.Value.ToString("N") + ".bin"; private static SizeChartDto ToDto(SizeChart chart, MeasurementSet measurements) { var dto = new SizeChartDto { BaseSize = chart.BaseSize, Sizes = chart.Sizes.ToList() }; foreach (var size in chart.Sizes) { var values = new List<SizeValueDto>(); foreach (var measurement in measurements.Measurements) if (chart.TryGetValue(size, measurement.Id, out var value)) values.Add(new SizeValueDto { Measurement = measurement.Id.Value, Units = value.Units }); if (values.Count > 0) dto.Values[size] = values; } return dto; } private static NodeDto ToDto(DraftNode node) { var dto = new NodeDto { Id = node.Id.Value, Name = node.Name }; switch (node) { case FreePoint free: dto.Kind = "free-point"; dto.First = ExpressionMapper.ToDto(free.X); dto.SecondFormula = ExpressionMapper.ToDto(free.Y); break; case OffsetPoint offset: dto.Kind = "offset-point"; dto.Origin = offset.Origin.Value; dto.First = ExpressionMapper.ToDto(offset.Distance); dto.SecondFormula = ExpressionMapper.ToDto(offset.AngleDegrees); break; case PointOnCurve onCurve: dto.Kind = "point-on-curve"; dto.Origin = onCurve.Curve.Value; dto.First = ExpressionMapper.ToDto(onCurve.Position); dto.Mode = onCurve.Mode.ToString(); break; case IntersectionPoint intersection: dto.Kind = "intersection-point"; dto.Origin = intersection.First.Value; dto.Second = intersection.Second.Value; break; case SegmentNode segment: dto.Kind = "segment"; dto.Origin = segment.StartPoint.Value; dto.Second = segment.EndPoint.Value; break; case CubicCurveNode cubic: dto.Kind = "cubic-curve"; dto.Origin = cubic.StartPoint.Value; dto.Second = cubic.Control1.Value; dto.Third = cubic.Control2.Value; dto.Fourth = cubic.EndPoint.Value; break; case TracedCurveNode traced: dto.Kind = "traced-curve"; dto.Anchored = traced.Points .Select(point => new AnchoredPointDto { AnchorFrom = point.AnchorFrom.Value, AnchorTo = point.AnchorTo.Value, Along = point.Along, Across = point.Across }) .ToList(); break; default: throw new InvalidPatternFileException( $"Построение {node.GetType().Name} не поддержано форматом файла"); } return dto; } private static PieceDto ToDto(PatternPiece piece) => new() { Id = piece.Id.Value, Name = piece.Name, Quantity = piece.Quantity, Note = piece.Note, FoldSegmentIndex = piece.FoldSegmentIndex, SeamAllowance = ExpressionMapper.ToDto(piece.SeamAllowance), GrainlineAngle = piece.Grainline is null ? null : ExpressionMapper.ToDto(piece.Grainline.AngleDegrees), Contour = piece.Contour .Select(segment => new SegmentDto { Curve = segment.Curve.Value, Reversed = segment.Reversed }) .ToList(), AllowanceOverrides = piece.SegmentAllowances .Select(pair => new AllowanceOverrideDto { SegmentIndex = pair.Key, Width = ExpressionMapper.ToDto(pair.Value) }) .OrderBy(item => item.SegmentIndex) .ToList(), Notches = piece.Notches .Select(notch => new NotchDto { Distance = ExpressionMapper.ToDto(notch.DistanceAlongContour), Label = notch.Label }) .ToList() }; public static PatternDocument ToDocument(DocumentDto dto) { var formulaUnit = ParseUnit(dto.FormulaUnit); var document = new PatternDocument(dto.Sizes.BaseSize, formulaUnit) { DisplayUnit = ParseUnit(dto.DisplayUnit) }; RestoreMeasurements(document, dto); RestoreSizes(document, dto); RestoreNodes(document, dto); RestorePieces(document, dto); document.Print.PageSizeName = dto.Print.PageSizeName; document.Print.PageWidth = Length.FromUnits(dto.Print.PageWidthUnits); document.Print.PageHeight = Length.FromUnits(dto.Print.PageHeightUnits); document.Print.IsLandscape = dto.Print.IsLandscape; document.Print.Margin = Length.FromUnits(dto.Print.MarginUnits); document.Print.JoinWithOverlap = dto.Print.JoinWithOverlap; document.Print.ExcludedFromPrint.Clear(); foreach (var piece in dto.Print.ExcludedFromPrint) document.Print.ExcludedFromPrint.Add(PieceId.FromGuid(piece)); document.Print.Overlap = Length.FromUnits(dto.Print.OverlapUnits); foreach (var placement in dto.Print.Placements) document.Print.Placements[PieceId.FromGuid(placement.Piece)] = new PiecePlacement( new Offset2( Length.FromUnits(placement.OffsetXUnits), Length.FromUnits(placement.OffsetYUnits)), placement.RotationDegrees); // Швы восстанавливаются после деталей: они ссылаются на участки их // контуров, которых до сборки деталей ещё нет. foreach (var seam in dto.Seams) document.Seams.Add(new Seam( SeamId.FromGuid(seam.Id), seam.Name, new SeamEdge(PieceId.FromGuid(seam.FirstPiece), seam.FirstSegment), new SeamEdge(PieceId.FromGuid(seam.SecondPiece), seam.SecondSegment), seam.Reversed) { Ease = seam.Ease is null ? null : ExpressionMapper.ToFormula(seam.Ease), Tolerance = Length.FromUnits(seam.ToleranceUnits) }); // Поправки восстанавливаются последними: они ссылаются на построения, // которых до восстановления графа ещё нет. foreach (var adjustment in dto.Adjustments) document.Adjustments.Set( adjustment.Size, NodeId.FromGuid(adjustment.Node), new Offset2( Length.FromUnits(adjustment.OffsetXUnits), Length.FromUnits(adjustment.OffsetYUnits))); return document; } private static void RestoreMeasurements(PatternDocument document, DocumentDto dto) { // Идентификаторы восстанавливаются из файла: на них ссылаются формулы, // и новые сделали бы весь сохранённый чертёж неразрешимым. foreach (var measurement in dto.Measurements) document.Measurements.AddWithId( MeasurementId.FromGuid(measurement.Id), measurement.Name, Length.FromUnits(measurement.Units), measurement.Description); } private static void RestoreSizes(PatternDocument document, DocumentDto dto) { foreach (var size in dto.Sizes.Sizes) if (!document.Sizes.ContainsSize(size)) document.Sizes.AddSize(size); foreach (var (size, values) in dto.Sizes.Values) foreach (var value in values) document.Sizes.SetValue( size, MeasurementId.FromGuid(value.Measurement), Length.FromUnits(value.Units)); if (document.Sizes.ContainsSize(dto.Sizes.BaseSize)) document.Sizes.SetBaseSize(dto.Sizes.BaseSize); } private static void RestoreNodes(PatternDocument document, DocumentDto dto) { foreach (var node in dto.Nodes) { var id = NodeId.FromGuid(node.Id); DraftNode restored = node.Kind switch { "free-point" => new FreePoint(id, node.Name, ExpressionMapper.ToFormula(node.First), ExpressionMapper.ToFormula(node.SecondFormula)), "offset-point" => new OffsetPoint(id, node.Name, NodeId.FromGuid(Required(node.Origin, node.Name)), ExpressionMapper.ToFormula(node.First), ExpressionMapper.ToFormula(node.SecondFormula)), "point-on-curve" => new PointOnCurve(id, node.Name, NodeId.FromGuid(Required(node.Origin, node.Name)), ExpressionMapper.ToFormula(node.First), Enum.TryParse<PointOnCurveMode>(node.Mode, out var mode) ? mode : PointOnCurveMode.Distance), "intersection-point" => new IntersectionPoint(id, node.Name, NodeId.FromGuid(Required(node.Origin, node.Name)), NodeId.FromGuid(Required(node.Second, node.Name))), "segment" => new SegmentNode(id, node.Name, NodeId.FromGuid(Required(node.Origin, node.Name)), NodeId.FromGuid(Required(node.Second, node.Name))), "cubic-curve" => new CubicCurveNode(id, node.Name, NodeId.FromGuid(Required(node.Origin, node.Name)), NodeId.FromGuid(Required(node.Second, node.Name)), NodeId.FromGuid(Required(node.Third, node.Name)), NodeId.FromGuid(Required(node.Fourth, node.Name))), "traced-curve" => RestoreTracedCurve(id, node), _ => throw new InvalidPatternFileException( $"Неизвестный вид построения «{node.Kind}»") }; document.Draft.Add(restored); } } private static void RestorePieces(PatternDocument document, DocumentDto dto) { foreach (var pieceDto in dto.Pieces) { var piece = new PatternPiece( PieceId.FromGuid(pieceDto.Id), pieceDto.Name, ExpressionMapper.ToFormula(pieceDto.SeamAllowance)) { Quantity = pieceDto.Quantity, Note = pieceDto.Note, Grainline = pieceDto.GrainlineAngle is null ? null : new Grainline(ExpressionMapper.ToFormula(pieceDto.GrainlineAngle)) }; foreach (var segment in pieceDto.Contour) piece.AddSegment(NodeId.FromGuid(segment.Curve), segment.Reversed); foreach (var over in pieceDto.AllowanceOverrides) if (over.SegmentIndex < piece.Contour.Count) piece.SetSegmentAllowance( over.SegmentIndex, ExpressionMapper.ToFormula(over.Width)); foreach (var notch in pieceDto.Notches) piece.AddNotch(new Notch( ExpressionMapper.ToFormula(notch.Distance), notch.Label)); // Признак сгиба ставится после сборки контура: он ссылается // на номер участка, которого до сборки ещё нет. piece.FoldSegmentIndex = pieceDto.FoldSegmentIndex; document.Pieces.Add(piece); } } private static TracedCurveNode RestoreTracedCurve(NodeId id, NodeDto node) { if (node.Anchored is not { Count: 4 }) throw new InvalidPatternFileException( $"У обведённой кривой «{node.Name}» должно быть четыре привязанные точки"); var points = node.Anchored .Select(point => new AnchoredPoint( NodeId.FromGuid(point.AnchorFrom), NodeId.FromGuid(point.AnchorTo), point.Along, point.Across)) .ToArray(); return new TracedCurveNode(id, node.Name, points[0], points[1], points[2], points[3]); } private static Guid Required(Guid? value, string nodeName) => value ?? throw new InvalidPatternFileException( $"У построения «{nodeName}» отсутствует обязательная ссылка"); private static LengthUnit ParseUnit(string value) => Enum.TryParse<LengthUnit>(value, out var unit) ? unit : LengthUnit.Centimetre; }