/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.Model/Drafting/DraftEvaluator.cs
239 строк
11 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 ClothesGraph.Geometry; using ClothesGraph.Model.Formulas; using ClothesGraph.Model.Grading; using ClothesGraph.Model.Measurements; namespace ClothesGraph.Model.Drafting; /// <summary> /// Пересчёт чертежа с сохранением результатов между вызовами. /// </summary> /// <remarks> /// Пересчитываются только построения, помеченные графом как затронутые, и всё, /// что от них зависит. Полный пересчёт при каждой правке сделал бы работу с /// крупной конструкцией ощутимо медленной без всякой на то причины. /// </remarks> public sealed class DraftEvaluator { private readonly DraftGraph _graph; private readonly Dictionary<NodeId, Point2> _points = new(); private readonly Dictionary<NodeId, ICurve> _curves = new(); private readonly Dictionary<NodeId, NodeFailure> _failures = new(); public DraftEvaluator(DraftGraph graph) => _graph = graph; /// <summary>Сколько построений пересчитано при последнем вызове.</summary> public int LastRecomputedCount { get; private set; } /// <summary> /// Признак набора входных данных, для которого построен кэш. /// </summary> /// <remarks> /// Кэш хранит геометрию одного размера. При пересчёте другого размера /// сохранённые значения относятся к чужим меркам, поэтому смена признака /// сбрасывает кэш целиком: иначе градация молча выдавала бы геометрию /// того размера, что считался прошлым. /// </remarks> private string? _cacheKey; /// <summary> /// Номер правки графа, до которого этот вычислитель всё пересчитал. /// </summary> /// <remarks> /// Своя граница у каждого вычислителя, а не общий набор пометок на граф. /// Вычислителей несколько — по одному на размер ряда, — и общий набор /// доставался пересчитавшемуся первым: он снимал пометки, а остальные /// размеры оставались с прежней геометрией. /// </remarks> private long _seenEpoch; public DraftEvaluation Evaluate( IReadOnlyDictionary<MeasurementId, Length> measurements, LengthUnit formulaUnit, IPointAdjustments? adjustments = null, string? cacheKey = null) { if (!string.Equals(_cacheKey, cacheKey, StringComparison.Ordinal)) { // Сохранённое относится к чужим меркам: считать заново придётся // всё, и пустой кэш сам к этому приведёт. _points.Clear(); _curves.Clear(); _failures.Clear(); _cacheKey = cacheKey; } DropRemoved(); var context = new Context(this, measurements, formulaUnit); var recomputed = 0; var epoch = _graph.Epoch; foreach (var node in _graph.TopologicalOrder()) { var needsRecompute = _graph.IsInvalidatedSince(node.Id, _seenEpoch) || (!_points.ContainsKey(node.Id) && !_curves.ContainsKey(node.Id) && !_failures.ContainsKey(node.Id)); if (!needsRecompute) continue; recomputed++; _failures.Remove(node.Id); // Построение, зависящее от неразрешимого, неразрешимо и само. // Причина указывает на исходный сбой, а не на ближайшее звено: // иначе пользователь шёл бы по цепочке вручную. if (FindBrokenDependency(node) is { } broken) { _points.Remove(node.Id); _curves.Remove(node.Id); _failures[node.Id] = new NodeFailure( node.Name, $"Зависит от неразрешимого построения «{_graph.Get(broken).Name}»", broken); continue; } try { switch (node) { case PointNode point: { var position = point.Evaluate(context); // Поправка применяется сразу после вычисления, а не // поверх готового чертежа: иначе построения, зависящие // от поправленной точки, остались бы на прежнем месте // и конструкция разошлась бы сама с собой. if (adjustments is not null && adjustments.TryGet(node.Id, out var offset)) position += offset; _points[node.Id] = position; break; } case CurveNode curve: _curves[node.Id] = curve.Evaluate(context); break; } } catch (Exception error) when (error is NodeUnresolvableException or FormulaEvaluationException or KeyNotFoundException) { _points.Remove(node.Id); _curves.Remove(node.Id); _failures[node.Id] = new NodeFailure(node.Name, error.Message); } } LastRecomputedCount = recomputed; // Граница сдвигается только после полного обхода: прерванный пересчёт // не должен оставить вычислитель в состоянии «всё пересчитано». // Берётся значение на начало обхода — правка, пришедшая во время него, // осталась бы иначе незамеченной. _seenEpoch = epoch; return new DraftEvaluation( new Dictionary<NodeId, Point2>(_points), new Dictionary<NodeId, ICurve>(_curves), new Dictionary<NodeId, NodeFailure>(_failures)); } /// <summary> /// Выбрасывает из кэша построения, которых больше нет в графе. /// </summary> /// <remarks> /// Обход считает только то, что в графе есть, и сам по себе удалённого /// построения не касается — его геометрия осталась бы в кэше навсегда и /// продолжала бы возвращаться в результате пересчёта. Удалённая линия /// рисовалась бы дальше, а попытка найти её в графе по идентификатору из /// того же результата не находила бы ничего. /// </remarks> private void DropRemoved() { Drop(_points); Drop(_curves); Drop(_failures); void Drop<TValue>(Dictionary<NodeId, TValue> cache) { List<NodeId>? removed = null; foreach (var id in cache.Keys) if (!_graph.Contains(id)) (removed ??= []).Add(id); if (removed is null) return; foreach (var id in removed) cache.Remove(id); } } private NodeId? FindBrokenDependency(DraftNode node) { foreach (var dependency in node.Dependencies) if (_failures.ContainsKey(dependency)) return dependency; return null; } private sealed class Context( DraftEvaluator evaluator, IReadOnlyDictionary<MeasurementId, Length> measurements, LengthUnit formulaUnit) : INodeEvaluationContext { public double GetMeasurement(MeasurementId id) => measurements.TryGetValue(id, out var value) ? value.To(formulaUnit) : throw new FormulaEvaluationException( "Мерка не задана в текущем размере"); public double GetNodeProperty(NodeId node, NodeProperty property) { if (evaluator._points.TryGetValue(node, out var point)) return property switch { NodeProperty.X => point.X.To(formulaUnit), NodeProperty.Y => point.Y.To(formulaUnit), _ => throw new FormulaEvaluationException( "У точки нет длины и угла") }; if (evaluator._curves.TryGetValue(node, out var curve)) return property switch { NodeProperty.Length => curve.Length.To(formulaUnit), NodeProperty.Angle => curve.DirectionAt(0), NodeProperty.X => curve.Start.X.To(formulaUnit), NodeProperty.Y => curve.Start.Y.To(formulaUnit), _ => throw new FormulaEvaluationException("Неизвестное свойство") }; throw new FormulaEvaluationException("Построение ещё не вычислено"); } public Point2 GetPoint(NodeId id) => evaluator._points.TryGetValue(id, out var point) ? point : throw new NodeUnresolvableException("Точка не вычислена"); public ICurve GetCurve(NodeId id) => evaluator._curves.TryGetValue(id, out var curve) ? curve : throw new NodeUnresolvableException("Линия не вычислена"); public Length ToLength(double value) => Length.From(value, formulaUnit); } }