1808 lines
63 KiB
C#
1808 lines
63 KiB
C#
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Win32;
|
|
|
|
var options = Options.Parse(args);
|
|
if (!File.Exists(options.InputJsonl))
|
|
throw new FileNotFoundException($"ownership JSONL not found: {options.InputJsonl}");
|
|
if (!File.Exists(options.SourceDwg))
|
|
throw new FileNotFoundException($"source DWG not found: {options.SourceDwg}");
|
|
if (string.IsNullOrWhiteSpace(options.OutputDwg))
|
|
throw new ArgumentException("--output-dwg is required.");
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(options.OutputDwg))!);
|
|
File.Copy(options.SourceDwg, options.OutputDwg, overwrite: true);
|
|
|
|
var records = ReadPointRecords(options.InputJsonl).ToList();
|
|
Console.OutputEncoding = Encoding.UTF8;
|
|
|
|
if (options.ColorEntities)
|
|
{
|
|
var colorResult = ColorDwgEntities(options.OutputDwg, records, options);
|
|
WriteEntityColorReport(options, records.Count, colorResult);
|
|
WriteEntityColorLegend(options, records.Count, colorResult);
|
|
Console.WriteLine($"input_points={records.Count}");
|
|
Console.WriteLine($"entity_candidates={colorResult.CandidateCount}");
|
|
Console.WriteLine($"colored_entities={colorResult.Colored.Count}");
|
|
Console.WriteLine($"skipped_entities={colorResult.SkippedCount}");
|
|
Console.WriteLine($"output_dwg={Path.GetFullPath(options.OutputDwg)}");
|
|
Console.WriteLine($"report={Path.GetFullPath(options.ReportPath)}");
|
|
Console.WriteLine($"legend={Path.GetFullPath(options.LegendPath)}");
|
|
foreach (var item in colorResult.Colored.Take(30))
|
|
Console.WriteLine($"color={item.ColorIndex} handle={item.Handle} kind={item.ObjectName} sample=({item.SampleX:F3},{item.SampleY:F3}) distance={item.DistanceMm:F3} {item.DisplayName}");
|
|
}
|
|
else
|
|
{
|
|
var groups = BuildGroups(records, options).ToList();
|
|
var contours = BuildContours(groups, options).ToList();
|
|
|
|
DrawContours(options.OutputDwg, contours, options);
|
|
WriteReport(options, records.Count, groups.Count, contours);
|
|
|
|
Console.WriteLine($"input_points={records.Count}");
|
|
Console.WriteLine($"groups={groups.Count}");
|
|
Console.WriteLine($"contours={contours.Count}");
|
|
Console.WriteLine($"output_dwg={Path.GetFullPath(options.OutputDwg)}");
|
|
Console.WriteLine($"report={Path.GetFullPath(options.ReportPath)}");
|
|
foreach (var contour in contours.Take(30))
|
|
{
|
|
Console.WriteLine(
|
|
$"color={contour.ColorIndex} pts={contour.PointCount} bbox_mm=({contour.MinX:F3},{contour.MinY:F3},{contour.MaxX:F3},{contour.MaxY:F3}) {contour.DisplayName}");
|
|
}
|
|
}
|
|
|
|
static IEnumerable<PointRecord> ReadPointRecords(string path)
|
|
{
|
|
foreach (var line in File.ReadLines(path, Encoding.UTF8))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
continue;
|
|
|
|
using var doc = JsonDocument.Parse(line);
|
|
var root = doc.RootElement;
|
|
if (!string.Equals(ReadString(root, "Kind", "kind"), "point", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
if (!ReadBool(root, "Hit", "hit"))
|
|
continue;
|
|
|
|
var ownership = root.TryGetProperty("Ownership", out var pascal)
|
|
? pascal
|
|
: root.TryGetProperty("ownership", out var camel) ? camel : default;
|
|
if (ownership.ValueKind == JsonValueKind.Undefined)
|
|
continue;
|
|
|
|
var rawKey = ReadString(ownership, "EntityComponent", "entity_component");
|
|
if (string.IsNullOrWhiteSpace(rawKey) || string.Equals(rawKey, "null", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
// Section lines and other drawing annotations can be IEntity but do not
|
|
// represent a component ownership region.
|
|
var selectedObject = ReadString(root, "SelectedObject", "selected_object");
|
|
if (selectedObject.Contains("DrSectionLine", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
var x = ReadDouble(root, "SheetXMm", "sheet_x_mm", "SheetXMM");
|
|
var y = ReadDouble(root, "SheetYMm", "sheet_y_mm", "SheetYMM");
|
|
if (!double.IsFinite(x) || !double.IsFinite(y))
|
|
continue;
|
|
|
|
var row = ReadInt(root, "Row", "row");
|
|
var column = ReadInt(root, "Column", "column");
|
|
var key = NormalizeOwnershipKey(rawKey);
|
|
yield return new PointRecord(key, DisplayNameFromKey(key), x, y, row, column);
|
|
}
|
|
}
|
|
|
|
static IEnumerable<ComponentGroup> BuildGroups(List<PointRecord> records, Options options)
|
|
{
|
|
var colorIndex = 0;
|
|
foreach (var byOwner in records.GroupBy(r => r.Key, StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(g => g.Count()))
|
|
{
|
|
var points = byOwner.ToList();
|
|
if (points.Count < options.MinGroupPoints)
|
|
continue;
|
|
|
|
var clusters = SplitConnectedClusters(points, options.StepMm * 1.55)
|
|
.Where(cluster => cluster.Count >= options.MinClusterPoints)
|
|
.OrderByDescending(cluster => cluster.Count)
|
|
.ToList();
|
|
|
|
var ownerColor = options.Colors[colorIndex % options.Colors.Length];
|
|
colorIndex++;
|
|
for (var i = 0; i < clusters.Count; i++)
|
|
{
|
|
yield return new ComponentGroup(
|
|
byOwner.Key,
|
|
points[0].DisplayName,
|
|
i + 1,
|
|
clusters[i],
|
|
ownerColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
static List<List<PointRecord>> SplitConnectedClusters(List<PointRecord> points, double maxDistanceMm)
|
|
{
|
|
var clusters = new List<List<PointRecord>>();
|
|
var visited = new bool[points.Count];
|
|
var maxDistanceSquared = maxDistanceMm * maxDistanceMm;
|
|
|
|
for (var i = 0; i < points.Count; i++)
|
|
{
|
|
if (visited[i])
|
|
continue;
|
|
|
|
var cluster = new List<PointRecord>();
|
|
var queue = new Queue<int>();
|
|
visited[i] = true;
|
|
queue.Enqueue(i);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
var point = points[current];
|
|
cluster.Add(point);
|
|
|
|
for (var j = 0; j < points.Count; j++)
|
|
{
|
|
if (visited[j])
|
|
continue;
|
|
|
|
var other = points[j];
|
|
var dx = point.XMm - other.XMm;
|
|
var dy = point.YMm - other.YMm;
|
|
if (dx * dx + dy * dy > maxDistanceSquared)
|
|
continue;
|
|
|
|
visited[j] = true;
|
|
queue.Enqueue(j);
|
|
}
|
|
}
|
|
|
|
clusters.Add(cluster);
|
|
}
|
|
|
|
return clusters;
|
|
}
|
|
|
|
static IEnumerable<Contour> BuildContours(List<ComponentGroup> groups, Options options)
|
|
{
|
|
foreach (var group in groups)
|
|
{
|
|
var unique = group.Points
|
|
.GroupBy(point => (X: Round(point.XMm), Y: Round(point.YMm)))
|
|
.Select(g => new HullPoint(g.Key.X, g.Key.Y))
|
|
.OrderBy(point => point.X)
|
|
.ThenBy(point => point.Y)
|
|
.ToList();
|
|
|
|
var hull = unique.Count >= 3 ? ConvexHull(unique) : unique;
|
|
var padded = PadPolygonFromCentroid(hull, options.PadMm);
|
|
if (padded.Count == 1)
|
|
padded = SmallRectangle(padded[0], options.PadMm);
|
|
else if (padded.Count == 2)
|
|
padded = SegmentRectangle(padded[0], padded[1], options.PadMm);
|
|
|
|
var minX = group.Points.Min(point => point.XMm) - options.PadMm;
|
|
var minY = group.Points.Min(point => point.YMm) - options.PadMm;
|
|
var maxX = group.Points.Max(point => point.XMm) + options.PadMm;
|
|
var maxY = group.Points.Max(point => point.YMm) + options.PadMm;
|
|
|
|
yield return new Contour(
|
|
group.Key,
|
|
group.DisplayName,
|
|
group.ClusterIndex,
|
|
group.Points.Count,
|
|
group.ColorIndex,
|
|
minX,
|
|
minY,
|
|
maxX,
|
|
maxY,
|
|
padded);
|
|
}
|
|
}
|
|
|
|
static List<HullPoint> ConvexHull(List<HullPoint> points)
|
|
{
|
|
if (points.Count <= 1)
|
|
return points;
|
|
|
|
var lower = new List<HullPoint>();
|
|
foreach (var point in points)
|
|
{
|
|
while (lower.Count >= 2 && Cross(lower[^2], lower[^1], point) <= 0)
|
|
lower.RemoveAt(lower.Count - 1);
|
|
lower.Add(point);
|
|
}
|
|
|
|
var upper = new List<HullPoint>();
|
|
for (var i = points.Count - 1; i >= 0; i--)
|
|
{
|
|
var point = points[i];
|
|
while (upper.Count >= 2 && Cross(upper[^2], upper[^1], point) <= 0)
|
|
upper.RemoveAt(upper.Count - 1);
|
|
upper.Add(point);
|
|
}
|
|
|
|
lower.RemoveAt(lower.Count - 1);
|
|
upper.RemoveAt(upper.Count - 1);
|
|
lower.AddRange(upper);
|
|
return lower;
|
|
}
|
|
|
|
static double Cross(HullPoint origin, HullPoint a, HullPoint b)
|
|
{
|
|
return (a.X - origin.X) * (b.Y - origin.Y) - (a.Y - origin.Y) * (b.X - origin.X);
|
|
}
|
|
|
|
static List<HullPoint> PadPolygonFromCentroid(List<HullPoint> points, double padMm)
|
|
{
|
|
if (points.Count == 0)
|
|
return points;
|
|
|
|
var cx = points.Average(point => point.X);
|
|
var cy = points.Average(point => point.Y);
|
|
return points.Select(point =>
|
|
{
|
|
var dx = point.X - cx;
|
|
var dy = point.Y - cy;
|
|
var length = Math.Sqrt(dx * dx + dy * dy);
|
|
if (length < 1e-9)
|
|
return point;
|
|
return new HullPoint(point.X + dx / length * padMm, point.Y + dy / length * padMm);
|
|
}).ToList();
|
|
}
|
|
|
|
static List<HullPoint> SmallRectangle(HullPoint point, double padMm)
|
|
{
|
|
var pad = Math.Max(1.0, padMm);
|
|
return
|
|
[
|
|
new(point.X - pad, point.Y - pad),
|
|
new(point.X + pad, point.Y - pad),
|
|
new(point.X + pad, point.Y + pad),
|
|
new(point.X - pad, point.Y + pad)
|
|
];
|
|
}
|
|
|
|
static List<HullPoint> SegmentRectangle(HullPoint a, HullPoint b, double padMm)
|
|
{
|
|
var pad = Math.Max(1.0, padMm);
|
|
var dx = b.X - a.X;
|
|
var dy = b.Y - a.Y;
|
|
var length = Math.Sqrt(dx * dx + dy * dy);
|
|
if (length < 1e-9)
|
|
return SmallRectangle(a, pad);
|
|
|
|
var nx = -dy / length * pad;
|
|
var ny = dx / length * pad;
|
|
return
|
|
[
|
|
new(a.X + nx, a.Y + ny),
|
|
new(b.X + nx, b.Y + ny),
|
|
new(b.X - nx, b.Y - ny),
|
|
new(a.X - nx, a.Y - ny)
|
|
];
|
|
}
|
|
|
|
static void DrawContours(string dwgPath, List<Contour> contours, Options options)
|
|
{
|
|
var app = StartOrConnectAutoCad(options.ProgId);
|
|
SetProperty(app, "Visible", true);
|
|
|
|
object? doc = null;
|
|
try
|
|
{
|
|
var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD documents collection unavailable.");
|
|
doc = Invoke(documents, "Open", Path.GetFullPath(dwgPath))
|
|
?? throw new InvalidOperationException($"AutoCAD failed to open DWG: {dwgPath}");
|
|
var modelSpace = GetProperty(doc, "ModelSpace") ?? throw new InvalidOperationException("AutoCAD ModelSpace unavailable.");
|
|
|
|
var textStyleName = options.DrawLabels ? EnsureChineseTextStyle(doc, options) : "";
|
|
foreach (var contour in contours)
|
|
{
|
|
var layerName = $"AG4_OWN_{contour.ColorIndex:00}_{contour.ClusterIndex:00}";
|
|
EnsureLayer(doc, layerName, contour.ColorIndex);
|
|
var coordinates = ToPolylineCoordinates(contour.Points);
|
|
var polyline = Invoke(modelSpace, "AddLightWeightPolyline", coordinates);
|
|
if (polyline == null)
|
|
continue;
|
|
|
|
SetProperty(polyline, "Closed", true);
|
|
SetProperty(polyline, "Layer", layerName);
|
|
SetProperty(polyline, "Color", contour.ColorIndex);
|
|
SetProperty(polyline, "Lineweight", options.Lineweight);
|
|
Safe(() => Invoke(polyline, "Update"));
|
|
|
|
if (options.DrawLabels)
|
|
DrawLabel(modelSpace, contour, layerName, textStyleName, options);
|
|
}
|
|
|
|
Invoke(doc, "SaveAs", Path.GetFullPath(dwgPath));
|
|
}
|
|
finally
|
|
{
|
|
if (doc != null && options.CloseDocument)
|
|
Safe(() => Invoke(doc, "Close", false));
|
|
}
|
|
}
|
|
|
|
static EntityColorResult ColorDwgEntities(string dwgPath, List<PointRecord> records, Options options)
|
|
{
|
|
if (records.Count == 0)
|
|
throw new InvalidOperationException("No ownership records are available for entity coloring.");
|
|
|
|
var owners = BuildOwnerStyles(records, options).ToDictionary(owner => owner.Key, StringComparer.OrdinalIgnoreCase);
|
|
var app = StartOrConnectAutoCad(options.ProgId);
|
|
SetProperty(app, "Visible", true);
|
|
|
|
object? doc = null;
|
|
var colored = new List<EntityColorRecord>();
|
|
var candidateCount = 0;
|
|
var skipped = new List<EntitySkipRecord>();
|
|
var pending = new List<PendingEntityColor>();
|
|
var unassigned = new List<UnassignedEntityColor>();
|
|
var fillChanges = new List<FillChangeRecord>();
|
|
var continuityChanges = new List<ContinuityChangeRecord>();
|
|
|
|
try
|
|
{
|
|
var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD documents collection unavailable.");
|
|
doc = Invoke(documents, "Open", Path.GetFullPath(dwgPath))
|
|
?? throw new InvalidOperationException($"AutoCAD failed to open DWG: {dwgPath}");
|
|
foreach (var owner in owners.Values)
|
|
EnsureLayer(doc, owner.LayerName, owner.ColorIndex);
|
|
|
|
var modelSpace = GetProperty(doc, "ModelSpace") ?? throw new InvalidOperationException("AutoCAD ModelSpace unavailable.");
|
|
var count = Convert.ToInt32(GetProperty(modelSpace, "Count"), CultureInfo.InvariantCulture);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var entity = Safe(() => Invoke(modelSpace, "Item", i));
|
|
if (entity == null)
|
|
continue;
|
|
|
|
var objectName = ReadComString(entity, "ObjectName");
|
|
var layer = ReadComString(entity, "Layer");
|
|
var handle = ReadComString(entity, "Handle");
|
|
if (layer.StartsWith("AG4_", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
skipped.Add(new EntitySkipRecord(handle, objectName, layer, "ag4_helper_layer", null, null, null));
|
|
continue;
|
|
}
|
|
|
|
if (!IsSupportedGeometry(objectName))
|
|
{
|
|
skipped.Add(new EntitySkipRecord(handle, objectName, layer, "unsupported_entity_type", null, null, null));
|
|
continue;
|
|
}
|
|
|
|
var samples = GetEntitySamplePoints(entity, objectName);
|
|
if (samples.Count == 0)
|
|
{
|
|
skipped.Add(new EntitySkipRecord(handle, objectName, layer, "no_sample_points", null, null, null));
|
|
continue;
|
|
}
|
|
|
|
if (!samples.Any(point => options.ContainsViewPoint(point.X, point.Y)))
|
|
{
|
|
var sample = samples[0];
|
|
skipped.Add(new EntitySkipRecord(handle, objectName, layer, "outside_view_bounds", sample.X, sample.Y, null));
|
|
continue;
|
|
}
|
|
|
|
candidateCount++;
|
|
var assignment = FindMajorityOwnership(records, samples, options.MaxEntityAssignDistanceMm);
|
|
if (assignment == null)
|
|
{
|
|
var nearest = FindBestSampleNearestOwnership(records, samples);
|
|
unassigned.Add(new UnassignedEntityColor(entity, handle, objectName, layer, samples, nearest));
|
|
continue;
|
|
}
|
|
|
|
pending.Add(new PendingEntityColor(entity, handle, objectName, layer, samples, assignment));
|
|
}
|
|
|
|
if (options.FillUnassignedByContinuity)
|
|
fillChanges = FillUnassignedByContinuity(pending, unassigned, options);
|
|
|
|
foreach (var item in unassigned.Where(item => !item.Filled))
|
|
{
|
|
skipped.Add(new EntitySkipRecord(
|
|
item.Handle,
|
|
item.ObjectName,
|
|
item.OriginalLayer,
|
|
"no_near_ownership_point",
|
|
item.Nearest?.Sample.X,
|
|
item.Nearest?.Sample.Y,
|
|
item.Nearest?.Nearest.DistanceMm));
|
|
}
|
|
|
|
continuityChanges = options.SmoothContinuity
|
|
? SmoothContinuityAssignments(pending, options)
|
|
: [];
|
|
|
|
foreach (var item in pending)
|
|
{
|
|
var owner = owners[item.Assignment.Key];
|
|
SetProperty(item.Entity, "Layer", owner.LayerName);
|
|
SetProperty(item.Entity, "Color", owner.ColorIndex);
|
|
SetProperty(item.Entity, "Lineweight", options.EntityLineweight);
|
|
Safe(() => Invoke(item.Entity, "Update"));
|
|
colored.Add(new EntityColorRecord(
|
|
item.Handle,
|
|
item.ObjectName,
|
|
owner.Key,
|
|
owner.DisplayName,
|
|
owner.ColorIndex,
|
|
owner.LayerName,
|
|
item.Assignment.SampleX,
|
|
item.Assignment.SampleY,
|
|
item.Assignment.AverageDistanceMm,
|
|
item.OriginalKey,
|
|
item.UnassignedFilled,
|
|
item.ContinuityAdjusted));
|
|
}
|
|
|
|
Invoke(doc, "SaveAs", Path.GetFullPath(dwgPath));
|
|
}
|
|
finally
|
|
{
|
|
if (doc != null && options.CloseDocument)
|
|
Safe(() => Invoke(doc, "Close", false));
|
|
}
|
|
|
|
return new EntityColorResult(candidateCount, skipped.Count, colored, skipped, fillChanges, continuityChanges);
|
|
}
|
|
|
|
static List<FillChangeRecord> FillUnassignedByContinuity(
|
|
List<PendingEntityColor> pending,
|
|
List<UnassignedEntityColor> unassigned,
|
|
Options options)
|
|
{
|
|
var changes = new List<FillChangeRecord>();
|
|
foreach (var item in unassigned)
|
|
{
|
|
if (item.Samples.Count < 1 || EstimatePathLength(item.Samples) > options.FillMaxEntityLengthMm)
|
|
continue;
|
|
|
|
var connected = pending
|
|
.Where(other => other.Samples.Count >= 1 &&
|
|
AreEntitiesConnected(item.Samples, other.Samples, options.FillToleranceMm))
|
|
.ToList();
|
|
if (connected.Count == 0)
|
|
continue;
|
|
|
|
var best = connected
|
|
.GroupBy(entity => entity.Assignment.Key, StringComparer.OrdinalIgnoreCase)
|
|
.Select(group => new
|
|
{
|
|
Key = group.Key,
|
|
Count = group.Count(),
|
|
DisplayName = group.First().Assignment.DisplayName,
|
|
AverageDistance = group.Average(entity => entity.Assignment.AverageDistanceMm)
|
|
})
|
|
.OrderByDescending(group => group.Count)
|
|
.ThenBy(group => group.AverageDistance)
|
|
.First();
|
|
|
|
if (best.Count < options.FillMinSameNeighbors)
|
|
continue;
|
|
|
|
var sampleX = item.Samples.Average(point => point.X);
|
|
var sampleY = item.Samples.Average(point => point.Y);
|
|
var assignment = new EntityAssignment(
|
|
best.Key,
|
|
best.DisplayName,
|
|
sampleX,
|
|
sampleY,
|
|
item.Nearest?.Nearest.DistanceMm ?? best.AverageDistance,
|
|
best.Count,
|
|
item.Samples.Count);
|
|
var pendingItem = new PendingEntityColor(item.Entity, item.Handle, item.ObjectName, item.OriginalLayer, item.Samples, assignment)
|
|
{
|
|
UnassignedFilled = true
|
|
};
|
|
pending.Add(pendingItem);
|
|
item.Filled = true;
|
|
changes.Add(new FillChangeRecord(
|
|
item.Handle,
|
|
item.ObjectName,
|
|
best.Key,
|
|
best.DisplayName,
|
|
best.Count,
|
|
connected.Count,
|
|
EstimatePathLength(item.Samples),
|
|
item.Nearest?.Nearest.DistanceMm));
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
static List<ContinuityChangeRecord> SmoothContinuityAssignments(List<PendingEntityColor> pending, Options options)
|
|
{
|
|
var changes = new List<ContinuityChangeRecord>();
|
|
foreach (var current in pending)
|
|
{
|
|
if (current.Samples.Count < 2 || EstimatePathLength(current.Samples) > options.ContinuityMaxEntityLengthMm)
|
|
continue;
|
|
|
|
var connected = pending
|
|
.Where(other => !ReferenceEquals(other, current) &&
|
|
other.Samples.Count >= 2 &&
|
|
AreEntitiesConnected(current.Samples, other.Samples, options.ContinuityToleranceMm))
|
|
.ToList();
|
|
if (connected.Count == 0)
|
|
continue;
|
|
|
|
var best = connected
|
|
.GroupBy(item => item.Assignment.Key, StringComparer.OrdinalIgnoreCase)
|
|
.Select(group => new { Key = group.Key, Count = group.Count(), DisplayName = group.First().Assignment.DisplayName })
|
|
.OrderByDescending(group => group.Count)
|
|
.First();
|
|
|
|
var currentNeighborCount = connected.Count(item =>
|
|
string.Equals(item.Assignment.Key, current.Assignment.Key, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (best.Count < options.ContinuityMinSameNeighbors ||
|
|
string.Equals(best.Key, current.Assignment.Key, StringComparison.OrdinalIgnoreCase) ||
|
|
currentNeighborCount > 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var before = current.Assignment;
|
|
current.Assignment = before with { Key = best.Key, DisplayName = best.DisplayName };
|
|
current.ContinuityAdjusted = true;
|
|
changes.Add(new ContinuityChangeRecord(
|
|
current.Handle,
|
|
current.ObjectName,
|
|
before.Key,
|
|
best.Key,
|
|
before.DisplayName,
|
|
best.DisplayName,
|
|
best.Count,
|
|
connected.Count,
|
|
EstimatePathLength(current.Samples)));
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
static bool AreEntitiesConnected(List<HullPoint> a, List<HullPoint> b, double toleranceMm)
|
|
{
|
|
var aTouches = TouchPoints(a);
|
|
var bTouches = TouchPoints(b);
|
|
var toleranceSquared = toleranceMm * toleranceMm;
|
|
foreach (var pa in aTouches)
|
|
{
|
|
foreach (var pb in bTouches)
|
|
{
|
|
var dx = pa.X - pb.X;
|
|
var dy = pa.Y - pb.Y;
|
|
if (dx * dx + dy * dy <= toleranceSquared)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static List<HullPoint> TouchPoints(List<HullPoint> samples)
|
|
{
|
|
if (samples.Count == 0)
|
|
return [];
|
|
if (samples.Count == 1)
|
|
return [samples[0]];
|
|
return [samples[0], samples[^1]];
|
|
}
|
|
|
|
static double EstimatePathLength(List<HullPoint> samples)
|
|
{
|
|
var length = 0.0;
|
|
for (var i = 0; i < samples.Count - 1; i++)
|
|
length += Distance(samples[i].X, samples[i].Y, samples[i + 1].X, samples[i + 1].Y);
|
|
return length;
|
|
}
|
|
|
|
static IEnumerable<OwnerStyle> BuildOwnerStyles(List<PointRecord> records, Options options)
|
|
{
|
|
var index = 0;
|
|
foreach (var group in records.GroupBy(record => record.Key, StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(group => group.Count()))
|
|
{
|
|
var color = options.Colors[index % options.Colors.Length];
|
|
yield return new OwnerStyle(
|
|
group.Key,
|
|
group.First().DisplayName,
|
|
color,
|
|
$"AG4_ENTITY_{color:00}_{index + 1:00}");
|
|
index++;
|
|
}
|
|
}
|
|
|
|
static NearestOwnership? FindNearestOwnership(List<PointRecord> records, double xMm, double yMm)
|
|
{
|
|
PointRecord? best = null;
|
|
var bestDistanceSquared = double.PositiveInfinity;
|
|
foreach (var record in records)
|
|
{
|
|
var dx = record.XMm - xMm;
|
|
var dy = record.YMm - yMm;
|
|
var distanceSquared = dx * dx + dy * dy;
|
|
if (distanceSquared >= bestDistanceSquared)
|
|
continue;
|
|
|
|
best = record;
|
|
bestDistanceSquared = distanceSquared;
|
|
}
|
|
|
|
return best == null ? null : new NearestOwnership(best, Math.Sqrt(bestDistanceSquared));
|
|
}
|
|
|
|
static EntityAssignment? FindMajorityOwnership(List<PointRecord> records, List<HullPoint> samples, double maxDistanceMm)
|
|
{
|
|
var votes = new Dictionary<string, AssignmentAccumulator>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var sample in samples)
|
|
{
|
|
var nearest = FindNearestOwnership(records, sample.X, sample.Y);
|
|
if (nearest == null || nearest.DistanceMm > maxDistanceMm)
|
|
continue;
|
|
|
|
if (!votes.TryGetValue(nearest.Point.Key, out var accumulator))
|
|
{
|
|
accumulator = new AssignmentAccumulator(nearest.Point.Key, nearest.Point.DisplayName);
|
|
votes[nearest.Point.Key] = accumulator;
|
|
}
|
|
|
|
accumulator.Count++;
|
|
accumulator.DistanceSum += nearest.DistanceMm;
|
|
accumulator.SampleXSum += sample.X;
|
|
accumulator.SampleYSum += sample.Y;
|
|
}
|
|
|
|
var best = votes.Values
|
|
.OrderByDescending(vote => vote.Count)
|
|
.ThenBy(vote => vote.DistanceSum / Math.Max(1, vote.Count))
|
|
.FirstOrDefault();
|
|
if (best == null)
|
|
return null;
|
|
|
|
return new EntityAssignment(
|
|
best.Key,
|
|
best.DisplayName,
|
|
best.SampleXSum / best.Count,
|
|
best.SampleYSum / best.Count,
|
|
best.DistanceSum / best.Count,
|
|
best.Count,
|
|
samples.Count);
|
|
}
|
|
|
|
static SampleNearestOwnership? FindBestSampleNearestOwnership(List<PointRecord> records, List<HullPoint> samples)
|
|
{
|
|
SampleNearestOwnership? best = null;
|
|
foreach (var sample in samples)
|
|
{
|
|
var nearest = FindNearestOwnership(records, sample.X, sample.Y);
|
|
if (nearest == null)
|
|
continue;
|
|
|
|
if (best == null || nearest.DistanceMm < best.Nearest.DistanceMm)
|
|
best = new SampleNearestOwnership(sample, nearest);
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
static bool IsSupportedGeometry(string objectName)
|
|
{
|
|
return objectName.Contains("Line", StringComparison.OrdinalIgnoreCase) ||
|
|
objectName.Contains("Polyline", StringComparison.OrdinalIgnoreCase) ||
|
|
objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase) ||
|
|
objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase) ||
|
|
objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static List<HullPoint> GetEntitySamplePoints(object entity, string objectName)
|
|
{
|
|
if (objectName.Contains("Polyline", StringComparison.OrdinalIgnoreCase))
|
|
return GetPolylineSamplePoints(entity);
|
|
|
|
if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
|
|
return GetSplineSamplePoints(entity);
|
|
|
|
if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase))
|
|
return GetCircleSamplePoints(entity);
|
|
|
|
if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase))
|
|
return GetArcSamplePoints(entity);
|
|
|
|
if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase) ||
|
|
objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
|
|
return GetLineLikeSamplePoints(entity);
|
|
|
|
return TryGetBoundingBoxCenter(entity, out var x, out var y) ? [new HullPoint(x, y)] : [];
|
|
}
|
|
|
|
static List<HullPoint> GetSplineSamplePoints(object entity)
|
|
{
|
|
var fitPoints = ToDoubleArray(Safe(() => GetProperty(entity, "FitPoints")));
|
|
if (fitPoints.Count >= 4)
|
|
return GetCoordinatePathSamples(fitPoints);
|
|
|
|
var controlPoints = ToDoubleArray(Safe(() => GetProperty(entity, "ControlPoints")));
|
|
if (controlPoints.Count >= 4)
|
|
return GetCoordinatePathSamples(controlPoints);
|
|
|
|
return GetLineLikeSamplePoints(entity);
|
|
}
|
|
|
|
static List<HullPoint> GetLineLikeSamplePoints(object entity)
|
|
{
|
|
if (!TryGetPointProperty(entity, "StartPoint", out var sx, out var sy) ||
|
|
!TryGetPointProperty(entity, "EndPoint", out var ex, out var ey))
|
|
return TryGetBoundingBoxCenter(entity, out var bx, out var by) ? [new HullPoint(bx, by)] : [];
|
|
|
|
var length = Distance(sx, sy, ex, ey);
|
|
var count = Math.Clamp((int)Math.Ceiling(length / 8.0) + 1, 3, 11);
|
|
return Enumerable.Range(0, count)
|
|
.Select(i =>
|
|
{
|
|
var t = count == 1 ? 0.5 : i / (double)(count - 1);
|
|
return new HullPoint(sx + (ex - sx) * t, sy + (ey - sy) * t);
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
static List<HullPoint> GetCircleSamplePoints(object entity)
|
|
{
|
|
if (!TryGetPointProperty(entity, "Center", out var cx, out var cy))
|
|
return TryGetBoundingBoxCenter(entity, out var bx, out var by) ? [new HullPoint(bx, by)] : [];
|
|
|
|
var radius = ReadComDouble(entity, "Radius");
|
|
if (!double.IsFinite(radius) || radius <= 0)
|
|
return [new HullPoint(cx, cy)];
|
|
|
|
return Enumerable.Range(0, 8)
|
|
.Select(i =>
|
|
{
|
|
var angle = Math.PI * 2.0 * i / 8.0;
|
|
return new HullPoint(cx + Math.Cos(angle) * radius, cy + Math.Sin(angle) * radius);
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
static List<HullPoint> GetArcSamplePoints(object entity)
|
|
{
|
|
if (!TryGetPointProperty(entity, "Center", out var cx, out var cy))
|
|
return TryGetBoundingBoxCenter(entity, out var bx, out var by) ? [new HullPoint(bx, by)] : [];
|
|
|
|
var radius = ReadComDouble(entity, "Radius");
|
|
var start = ReadComDouble(entity, "StartAngle");
|
|
var end = ReadComDouble(entity, "EndAngle");
|
|
if (!double.IsFinite(radius) || !double.IsFinite(start) || !double.IsFinite(end) || radius <= 0)
|
|
return TryGetBoundingBoxCenter(entity, out var bx, out var by) ? [new HullPoint(bx, by)] : [];
|
|
|
|
var span = end - start;
|
|
if (span < 0)
|
|
span += Math.PI * 2.0;
|
|
var count = Math.Clamp((int)Math.Ceiling(radius * span / 6.0) + 1, 3, 9);
|
|
return Enumerable.Range(0, count)
|
|
.Select(i =>
|
|
{
|
|
var t = count == 1 ? 0.5 : i / (double)(count - 1);
|
|
var angle = start + span * t;
|
|
return new HullPoint(cx + Math.Cos(angle) * radius, cy + Math.Sin(angle) * radius);
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
static List<HullPoint> GetPolylineSamplePoints(object entity)
|
|
{
|
|
var raw = Safe(() => GetProperty(entity, "Coordinates"));
|
|
var values = ToDoubleArray(raw);
|
|
if (values.Count < 4)
|
|
return TryGetBoundingBoxCenter(entity, out var bx, out var by) ? [new HullPoint(bx, by)] : [];
|
|
|
|
return GetCoordinatePathSamples(values);
|
|
}
|
|
|
|
static List<HullPoint> GetCoordinatePathSamples(List<double> values)
|
|
{
|
|
var dimensions = values.Count % 3 == 0 ? 3 : 2;
|
|
var pointCount = values.Count / dimensions;
|
|
var samples = new List<HullPoint>();
|
|
for (var i = 0; i < pointCount - 1; i++)
|
|
{
|
|
var ax = values[i * dimensions];
|
|
var ay = values[i * dimensions + 1];
|
|
var bx = values[(i + 1) * dimensions];
|
|
var by = values[(i + 1) * dimensions + 1];
|
|
var length = Distance(ax, ay, bx, by);
|
|
var count = Math.Clamp((int)Math.Ceiling(length / 8.0) + 1, 2, 8);
|
|
for (var j = 0; j < count; j++)
|
|
{
|
|
var t = count == 1 ? 0.5 : j / (double)(count - 1);
|
|
samples.Add(new HullPoint(ax + (bx - ax) * t, ay + (by - ay) * t));
|
|
}
|
|
}
|
|
|
|
return samples.Count > 0 ? samples : [new HullPoint(values[0], values[1])];
|
|
}
|
|
|
|
static bool TryGetPointProperty(object entity, string propertyName, out double xMm, out double yMm)
|
|
{
|
|
xMm = double.NaN;
|
|
yMm = double.NaN;
|
|
var values = ToDoubleArray(Safe(() => GetProperty(entity, propertyName)));
|
|
if (values.Count < 2)
|
|
return false;
|
|
|
|
xMm = values[0];
|
|
yMm = values[1];
|
|
return true;
|
|
}
|
|
|
|
static bool TryGetBoundingBoxCenter(object entity, out double xMm, out double yMm)
|
|
{
|
|
xMm = double.NaN;
|
|
yMm = double.NaN;
|
|
object? minObj = null;
|
|
object? maxObj = null;
|
|
try
|
|
{
|
|
var args = new object?[] { minObj, maxObj };
|
|
var modifiers = new ParameterModifier(2);
|
|
modifiers[0] = true;
|
|
modifiers[1] = true;
|
|
entity.GetType().InvokeMember(
|
|
"GetBoundingBox",
|
|
BindingFlags.InvokeMethod,
|
|
null,
|
|
entity,
|
|
args,
|
|
[modifiers],
|
|
CultureInfo.InvariantCulture,
|
|
null);
|
|
minObj = args[0];
|
|
maxObj = args[1];
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var min = ToDoubleArray(minObj);
|
|
var max = ToDoubleArray(maxObj);
|
|
if (min.Count < 2 || max.Count < 2)
|
|
return false;
|
|
|
|
xMm = (min[0] + max[0]) / 2.0;
|
|
yMm = (min[1] + max[1]) / 2.0;
|
|
return true;
|
|
}
|
|
|
|
static List<double> ToDoubleArray(object? value)
|
|
{
|
|
if (value == null)
|
|
return [];
|
|
|
|
if (value is Array array)
|
|
{
|
|
var values = new List<double>();
|
|
foreach (var item in array)
|
|
{
|
|
if (item == null)
|
|
continue;
|
|
values.Add(Convert.ToDouble(item, CultureInfo.InvariantCulture));
|
|
}
|
|
return values;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
static string ReadComString(object target, string propertyName)
|
|
{
|
|
return Safe(() => GetProperty(target, propertyName)?.ToString()) ?? "";
|
|
}
|
|
|
|
static double ReadComDouble(object target, string propertyName)
|
|
{
|
|
var value = Safe(() => GetProperty(target, propertyName));
|
|
if (value == null)
|
|
return double.NaN;
|
|
return Convert.ToDouble(value, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
static double Distance(double ax, double ay, double bx, double by)
|
|
{
|
|
var dx = ax - bx;
|
|
var dy = ay - by;
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
static double[] ToPolylineCoordinates(List<HullPoint> points)
|
|
{
|
|
var values = new double[points.Count * 2];
|
|
for (var i = 0; i < points.Count; i++)
|
|
{
|
|
values[i * 2] = points[i].X;
|
|
values[i * 2 + 1] = points[i].Y;
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
static void EnsureLayer(object doc, string layerName, int colorIndex)
|
|
{
|
|
var layers = GetProperty(doc, "Layers") ?? throw new InvalidOperationException("AutoCAD Layers unavailable.");
|
|
object? layer = null;
|
|
try
|
|
{
|
|
layer = Invoke(layers, "Item", layerName);
|
|
}
|
|
catch
|
|
{
|
|
layer = Invoke(layers, "Add", layerName);
|
|
}
|
|
|
|
if (layer != null)
|
|
SetProperty(layer, "Color", colorIndex);
|
|
}
|
|
|
|
static string EnsureChineseTextStyle(object doc, Options options)
|
|
{
|
|
var styles = GetProperty(doc, "TextStyles") ?? throw new InvalidOperationException("AutoCAD TextStyles unavailable.");
|
|
object? style = null;
|
|
try
|
|
{
|
|
style = Invoke(styles, "Item", options.TextStyleName);
|
|
}
|
|
catch
|
|
{
|
|
style = Invoke(styles, "Add", options.TextStyleName);
|
|
}
|
|
|
|
if (style == null)
|
|
return options.TextStyleName;
|
|
|
|
var fontPath = ResolveChineseFont(options);
|
|
if (!string.IsNullOrWhiteSpace(fontPath))
|
|
SafeAction(() => SetProperty(style, "FontFile", fontPath));
|
|
|
|
// TrueType Chinese fonts are more reliable through SetFont than through
|
|
// whatever Standard style the source DWG happens to use.
|
|
Safe(() => Invoke(style, "SetFont", options.TextTypeface, false, false, 134, 49));
|
|
SafeAction(() => SetProperty(style, "Height", 0.0));
|
|
SafeAction(() => SetProperty(style, "Width", 1.0));
|
|
return options.TextStyleName;
|
|
}
|
|
|
|
static string ResolveChineseFont(Options options)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(options.TextFontFile) && File.Exists(options.TextFontFile))
|
|
return options.TextFontFile;
|
|
|
|
var candidates = new[]
|
|
{
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "simsun.ttc"),
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "msyh.ttc"),
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "simhei.ttf")
|
|
};
|
|
return candidates.FirstOrDefault(File.Exists) ?? "";
|
|
}
|
|
|
|
static void DrawLabel(object modelSpace, Contour contour, string layerName, string textStyleName, Options options)
|
|
{
|
|
var point = new[] { contour.MinX, contour.MaxY + options.LabelOffsetMm, 0.0 };
|
|
var text = Invoke(modelSpace, "AddText", ShortLabel(contour.DisplayName, 24), point, options.LabelHeightMm);
|
|
if (text == null)
|
|
return;
|
|
|
|
SetProperty(text, "Layer", layerName);
|
|
SetProperty(text, "Color", contour.ColorIndex);
|
|
if (!string.IsNullOrWhiteSpace(textStyleName))
|
|
SafeAction(() => SetProperty(text, "StyleName", textStyleName));
|
|
Safe(() => Invoke(text, "Update"));
|
|
}
|
|
|
|
static void WriteReport(Options options, int inputPointCount, int groupCount, List<Contour> contours)
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(options.ReportPath))!);
|
|
File.WriteAllText(options.ReportPath, JsonSerializer.Serialize(new
|
|
{
|
|
input_jsonl = Path.GetFullPath(options.InputJsonl),
|
|
source_dwg = Path.GetFullPath(options.SourceDwg),
|
|
output_dwg = Path.GetFullPath(options.OutputDwg),
|
|
input_point_count = inputPointCount,
|
|
group_count = groupCount,
|
|
contour_count = contours.Count,
|
|
step_mm = options.StepMm,
|
|
pad_mm = options.PadMm,
|
|
min_group_points = options.MinGroupPoints,
|
|
min_cluster_points = options.MinClusterPoints,
|
|
contours = contours.Select(contour => new
|
|
{
|
|
contour.Key,
|
|
contour.DisplayName,
|
|
contour.ClusterIndex,
|
|
contour.PointCount,
|
|
contour.ColorIndex,
|
|
contour.MinX,
|
|
contour.MinY,
|
|
contour.MaxX,
|
|
contour.MaxY,
|
|
points = contour.Points.Select(point => new { x = point.X, y = point.Y })
|
|
})
|
|
}, new JsonSerializerOptions { WriteIndented = true }));
|
|
}
|
|
|
|
static void WriteEntityColorReport(Options options, int inputPointCount, EntityColorResult result)
|
|
{
|
|
var skipSummary = result.Skipped
|
|
.GroupBy(item => item.Reason, StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(group => group.Count())
|
|
.Select(group => new
|
|
{
|
|
reason = group.Key,
|
|
count = group.Count()
|
|
})
|
|
.ToList();
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(options.ReportPath))!);
|
|
File.WriteAllText(options.ReportPath, JsonSerializer.Serialize(new
|
|
{
|
|
input_jsonl = Path.GetFullPath(options.InputJsonl),
|
|
source_dwg = Path.GetFullPath(options.SourceDwg),
|
|
output_dwg = Path.GetFullPath(options.OutputDwg),
|
|
color_legend_text = Path.GetFullPath(options.LegendPath),
|
|
mode = "color_entities",
|
|
input_point_count = inputPointCount,
|
|
entity_candidate_count = result.CandidateCount,
|
|
colored_entity_count = result.Colored.Count,
|
|
skipped_entity_count = result.SkippedCount,
|
|
skipped_summary = skipSummary,
|
|
skipped_samples = result.Skipped.Take(options.MaxSkippedSamples),
|
|
fill_unassigned_by_continuity = new
|
|
{
|
|
enabled = options.FillUnassignedByContinuity,
|
|
tolerance_mm = options.FillToleranceMm,
|
|
min_same_neighbors = options.FillMinSameNeighbors,
|
|
max_entity_length_mm = options.FillMaxEntityLengthMm,
|
|
change_count = result.FillChanges.Count,
|
|
changes = result.FillChanges
|
|
},
|
|
continuity_smoothing = new
|
|
{
|
|
enabled = options.SmoothContinuity,
|
|
tolerance_mm = options.ContinuityToleranceMm,
|
|
min_same_neighbors = options.ContinuityMinSameNeighbors,
|
|
max_entity_length_mm = options.ContinuityMaxEntityLengthMm,
|
|
change_count = result.ContinuityChanges.Count,
|
|
changes = result.ContinuityChanges
|
|
},
|
|
max_entity_assign_distance_mm = options.MaxEntityAssignDistanceMm,
|
|
view_bounds_mm = options.HasViewBounds
|
|
? new { min_x = options.ViewMinX, min_y = options.ViewMinY, max_x = options.ViewMaxX, max_y = options.ViewMaxY }
|
|
: null,
|
|
view_bounds_list_mm = options.ViewBoundsMm
|
|
.Select(box => new { min_x = box.MinX, min_y = box.MinY, max_x = box.MaxX, max_y = box.MaxY })
|
|
.ToList(),
|
|
colored = result.Colored
|
|
}, new JsonSerializerOptions { WriteIndented = true }));
|
|
}
|
|
|
|
static void WriteEntityColorLegend(Options options, int inputPointCount, EntityColorResult result)
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(options.LegendPath))!);
|
|
|
|
var groups = result.Colored
|
|
.GroupBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
|
|
.Select(group =>
|
|
{
|
|
var first = group.First();
|
|
return new
|
|
{
|
|
first.Key,
|
|
first.DisplayName,
|
|
first.ColorIndex,
|
|
first.LayerName,
|
|
EntityCount = group.Count(),
|
|
FilledCount = group.Count(item => item.UnassignedFilled),
|
|
AdjustedCount = group.Count(item => item.ContinuityAdjusted),
|
|
ObjectTypes = group
|
|
.GroupBy(item => item.ObjectName, StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(typeGroup => typeGroup.Count())
|
|
.Select(typeGroup => $"{typeGroup.Key}={typeGroup.Count()}")
|
|
.ToList()
|
|
};
|
|
})
|
|
.OrderBy(item => item.ColorIndex)
|
|
.ThenByDescending(item => item.EntityCount)
|
|
.ToList();
|
|
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("上色二维图线条归属说明");
|
|
sb.AppendLine($"原始DWG: {Path.GetFullPath(options.SourceDwg)}");
|
|
sb.AppendLine($"上色DWG: {Path.GetFullPath(options.OutputDwg)}");
|
|
sb.AppendLine($"归属采样JSONL: {Path.GetFullPath(options.InputJsonl)}");
|
|
sb.AppendLine($"归属采样点数量: {inputPointCount}");
|
|
sb.AppendLine($"已上色可绘制线条数量: {result.Colored.Count}");
|
|
sb.AppendLine();
|
|
sb.AppendLine("使用方式:");
|
|
sb.AppendLine("上色后的零件位置说明图中,同一种线条颜色对应下面列出的同一个零件/组件归属。分析时请把本说明、上色位置图、学生原始标注图一起使用。");
|
|
sb.AppendLine();
|
|
sb.AppendLine("颜色归属:");
|
|
|
|
if (groups.Count == 0)
|
|
{
|
|
sb.AppendLine("- 没有成功上色的可绘制线条。");
|
|
}
|
|
else
|
|
{
|
|
foreach (var group in groups)
|
|
{
|
|
sb.AppendLine(
|
|
$"- {AciColorDescription(group.ColorIndex)} | 图层={group.LayerName} | 上色线条数={group.EntityCount} | 归属={group.DisplayName}");
|
|
sb.AppendLine($" 归属键={group.Key}");
|
|
if (group.FilledCount > 0 || group.AdjustedCount > 0)
|
|
sb.AppendLine($" 连续性补色={group.FilledCount}, 连续性修正={group.AdjustedCount}");
|
|
if (group.ObjectTypes.Count > 0)
|
|
sb.AppendLine($" 图元类型={string.Join(", ", group.ObjectTypes)}");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("备注:");
|
|
sb.AppendLine("- ACI 是 AutoCAD Color Index,即 DWG 图元使用的颜色编号。");
|
|
sb.AppendLine("- Hex 是用于 AI 读图理解的近似屏幕颜色,不要求与 CAD 显示完全逐像素一致。");
|
|
|
|
File.WriteAllText(options.LegendPath, sb.ToString(), new UTF8Encoding(false));
|
|
}
|
|
|
|
static string AciColorDescription(int colorIndex)
|
|
{
|
|
var color = KnownAciColor(colorIndex);
|
|
if (color == null)
|
|
return $"ACI {colorIndex}";
|
|
|
|
return $"ACI {colorIndex} {color.Value.Name} 近似 {color.Value.Hex}";
|
|
}
|
|
|
|
static (string Name, string Hex)? KnownAciColor(int colorIndex) => colorIndex switch
|
|
{
|
|
1 => ("红色", "#FF0000"),
|
|
2 => ("黄色", "#FFFF00"),
|
|
3 => ("绿色", "#00FF00"),
|
|
4 => ("青色", "#00FFFF"),
|
|
5 => ("蓝色", "#0000FF"),
|
|
6 => ("品红色", "#FF00FF"),
|
|
7 => ("白色/黑色", "#FFFFFF"),
|
|
8 => ("灰色", "#808080"),
|
|
9 => ("浅灰色", "#C0C0C0"),
|
|
10 => ("红橙色", "#FF7F00"),
|
|
30 => ("橙色", "#FFBF00"),
|
|
94 => ("黄绿色", "#99CC00"),
|
|
140 => ("蓝绿色", "#00CC99"),
|
|
170 => ("蓝紫色", "#3366CC"),
|
|
200 => ("紫色", "#9933CC"),
|
|
_ => null
|
|
};
|
|
|
|
static object StartOrConnectAutoCad(string progId)
|
|
{
|
|
object? app = null;
|
|
try
|
|
{
|
|
app = GetActiveObjectFromProgId(progId);
|
|
}
|
|
catch
|
|
{
|
|
app = null;
|
|
}
|
|
|
|
if (app != null)
|
|
return app;
|
|
|
|
StartAutoCadFromRegistry(progId);
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(60);
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
try
|
|
{
|
|
return GetActiveObjectFromProgId(progId);
|
|
}
|
|
catch
|
|
{
|
|
Thread.Sleep(500);
|
|
}
|
|
}
|
|
|
|
throw new InvalidOperationException($"AutoCAD startup or COM connection failed: {progId}");
|
|
}
|
|
|
|
static object GetActiveObjectFromProgId(string progId)
|
|
{
|
|
var hr = CLSIDFromProgID(progId, out var clsid);
|
|
if (hr < 0)
|
|
Marshal.ThrowExceptionForHR(hr);
|
|
return GetActiveObject(ref clsid, IntPtr.Zero);
|
|
}
|
|
|
|
static void StartAutoCadFromRegistry(string progId)
|
|
{
|
|
var clsid = ReadDefaultValue($@"{progId}\CLSID");
|
|
var command = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32");
|
|
var exe = ExtractExePath(command);
|
|
if (string.IsNullOrWhiteSpace(exe) || !File.Exists(exe))
|
|
exe = @"D:\Program Files\Autodesk\AutoCAD 2020\acad.exe";
|
|
if (!File.Exists(exe))
|
|
throw new FileNotFoundException($"acad.exe not found. ProgID={progId}, LocalServer32={command}");
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = exe,
|
|
Arguments = "/Automation",
|
|
UseShellExecute = true,
|
|
WorkingDirectory = Path.GetDirectoryName(exe) ?? Environment.CurrentDirectory,
|
|
WindowStyle = ProcessWindowStyle.Hidden
|
|
});
|
|
}
|
|
|
|
static string ReadDefaultValue(string subKey)
|
|
{
|
|
using var key = Registry.ClassesRoot.OpenSubKey(subKey);
|
|
return key?.GetValue("")?.ToString() ?? "";
|
|
}
|
|
|
|
static string ExtractExePath(string command)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(command))
|
|
return "";
|
|
var trimmed = command.Trim();
|
|
if (trimmed.StartsWith('"'))
|
|
{
|
|
var end = trimmed.IndexOf('"', 1);
|
|
return end > 1 ? trimmed[1..end] : "";
|
|
}
|
|
|
|
var exeIndex = trimmed.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
|
|
return exeIndex >= 0 ? trimmed[..(exeIndex + 4)] : trimmed;
|
|
}
|
|
|
|
[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
|
|
static extern int CLSIDFromProgID(string progId, out Guid clsid);
|
|
|
|
[DllImport("oleaut32.dll", PreserveSig = false)]
|
|
[return: MarshalAs(UnmanagedType.IUnknown)]
|
|
static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);
|
|
|
|
static object? Invoke(object target, string name, params object?[] args)
|
|
{
|
|
return RetryCom(() => target.GetType().InvokeMember(
|
|
name,
|
|
BindingFlags.InvokeMethod,
|
|
null,
|
|
target,
|
|
args));
|
|
}
|
|
|
|
static object? GetProperty(object target, string name)
|
|
{
|
|
return RetryCom(() => target.GetType().InvokeMember(
|
|
name,
|
|
BindingFlags.GetProperty,
|
|
null,
|
|
target,
|
|
[]));
|
|
}
|
|
|
|
static void SetProperty(object target, string name, object? value)
|
|
{
|
|
RetryCom(() => target.GetType().InvokeMember(
|
|
name,
|
|
BindingFlags.SetProperty,
|
|
null,
|
|
target,
|
|
[value]));
|
|
}
|
|
|
|
static object? RetryCom(Func<object?> action)
|
|
{
|
|
Exception? last = null;
|
|
for (var attempt = 0; attempt < 60; attempt++)
|
|
{
|
|
try
|
|
{
|
|
return action();
|
|
}
|
|
catch (TargetInvocationException ex) when (IsRetryLater(ex.InnerException))
|
|
{
|
|
last = ex;
|
|
Thread.Sleep(250);
|
|
}
|
|
catch (COMException ex) when (IsRetryLater(ex))
|
|
{
|
|
last = ex;
|
|
Thread.Sleep(250);
|
|
}
|
|
}
|
|
|
|
throw last ?? new InvalidOperationException("COM call failed.");
|
|
}
|
|
|
|
static bool IsRetryLater(Exception? ex)
|
|
{
|
|
return ex is COMException com && unchecked((uint)com.HResult) == 0x8001010A;
|
|
}
|
|
|
|
static T? Safe<T>(Func<T> action)
|
|
{
|
|
try
|
|
{
|
|
return action();
|
|
}
|
|
catch
|
|
{
|
|
return default;
|
|
}
|
|
}
|
|
|
|
static void SafeAction(Action action)
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
static double ReadDouble(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!element.TryGetProperty(name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number))
|
|
return number;
|
|
if (value.ValueKind == JsonValueKind.String &&
|
|
double.TryParse(value.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out number))
|
|
return number;
|
|
}
|
|
|
|
return double.NaN;
|
|
}
|
|
|
|
static bool ReadBool(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!element.TryGetProperty(name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.True)
|
|
return true;
|
|
if (value.ValueKind == JsonValueKind.False)
|
|
return false;
|
|
if (value.ValueKind == JsonValueKind.String && bool.TryParse(value.GetString(), out var parsed))
|
|
return parsed;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int ReadInt(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!element.TryGetProperty(name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number))
|
|
return number;
|
|
if (value.ValueKind == JsonValueKind.String &&
|
|
int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out number))
|
|
return number;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static string ReadString(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!element.TryGetProperty(name, out var value))
|
|
continue;
|
|
return value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : value.ToString();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
static string DisplayNameFromKey(string key)
|
|
{
|
|
const string prefix = "name=";
|
|
var start = key.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
|
|
if (start < 0)
|
|
return key;
|
|
start += prefix.Length;
|
|
var end = key.IndexOf(';', start);
|
|
return end > start ? key[start..end] : key[start..];
|
|
}
|
|
|
|
static string NormalizeOwnershipKey(string key)
|
|
{
|
|
var name = ReadKeyField(key, "name");
|
|
var path = ReadKeyField(key, "path");
|
|
var config = ReadKeyField(key, "config");
|
|
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(path))
|
|
return key;
|
|
|
|
var leafName = name
|
|
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.LastOrDefault();
|
|
if (string.IsNullOrWhiteSpace(leafName))
|
|
leafName = name;
|
|
|
|
return $"name={leafName}; path={path}; config={config}";
|
|
}
|
|
|
|
static string ReadKeyField(string key, string fieldName)
|
|
{
|
|
var prefix = fieldName + "=";
|
|
var start = key.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
|
|
if (start < 0)
|
|
return "";
|
|
|
|
start += prefix.Length;
|
|
var end = key.IndexOf(';', start);
|
|
return end > start ? key[start..end].Trim() : key[start..].Trim();
|
|
}
|
|
|
|
static string ShortLabel(string text, int maxLength)
|
|
{
|
|
return text.Length <= maxLength ? text : text[..maxLength];
|
|
}
|
|
|
|
static double Round(double value)
|
|
{
|
|
return Math.Round(value, 6);
|
|
}
|
|
|
|
sealed record PointRecord(string Key, string DisplayName, double XMm, double YMm, int Row, int Column);
|
|
|
|
sealed record ComponentGroup(
|
|
string Key,
|
|
string DisplayName,
|
|
int ClusterIndex,
|
|
List<PointRecord> Points,
|
|
int ColorIndex);
|
|
|
|
sealed record HullPoint(double X, double Y);
|
|
|
|
sealed record Contour(
|
|
string Key,
|
|
string DisplayName,
|
|
int ClusterIndex,
|
|
int PointCount,
|
|
int ColorIndex,
|
|
double MinX,
|
|
double MinY,
|
|
double MaxX,
|
|
double MaxY,
|
|
List<HullPoint> Points);
|
|
|
|
sealed record OwnerStyle(string Key, string DisplayName, int ColorIndex, string LayerName);
|
|
|
|
sealed record ViewBoundsMm(double MinX, double MinY, double MaxX, double MaxY);
|
|
|
|
sealed record NearestOwnership(PointRecord Point, double DistanceMm);
|
|
|
|
sealed record SampleNearestOwnership(HullPoint Sample, NearestOwnership Nearest);
|
|
|
|
sealed record EntityAssignment(
|
|
string Key,
|
|
string DisplayName,
|
|
double SampleX,
|
|
double SampleY,
|
|
double AverageDistanceMm,
|
|
int Votes,
|
|
int SampleCount);
|
|
|
|
sealed class AssignmentAccumulator(string key, string displayName)
|
|
{
|
|
public string Key { get; } = key;
|
|
public string DisplayName { get; } = displayName;
|
|
public int Count { get; set; }
|
|
public double DistanceSum { get; set; }
|
|
public double SampleXSum { get; set; }
|
|
public double SampleYSum { get; set; }
|
|
}
|
|
|
|
sealed record EntityColorResult(
|
|
int CandidateCount,
|
|
int SkippedCount,
|
|
List<EntityColorRecord> Colored,
|
|
List<EntitySkipRecord> Skipped,
|
|
List<FillChangeRecord> FillChanges,
|
|
List<ContinuityChangeRecord> ContinuityChanges);
|
|
|
|
sealed record EntityColorRecord(
|
|
string Handle,
|
|
string ObjectName,
|
|
string Key,
|
|
string DisplayName,
|
|
int ColorIndex,
|
|
string LayerName,
|
|
double SampleX,
|
|
double SampleY,
|
|
double DistanceMm,
|
|
string OriginalKey,
|
|
bool UnassignedFilled,
|
|
bool ContinuityAdjusted);
|
|
|
|
sealed record EntitySkipRecord(
|
|
string Handle,
|
|
string ObjectName,
|
|
string LayerName,
|
|
string Reason,
|
|
double? SampleX,
|
|
double? SampleY,
|
|
double? NearestDistanceMm);
|
|
|
|
sealed record FillChangeRecord(
|
|
string Handle,
|
|
string ObjectName,
|
|
string ToKey,
|
|
string ToDisplayName,
|
|
int SameNeighborCount,
|
|
int ConnectedNeighborCount,
|
|
double EntityLengthMm,
|
|
double? NearestDistanceMm);
|
|
|
|
sealed record ContinuityChangeRecord(
|
|
string Handle,
|
|
string ObjectName,
|
|
string FromKey,
|
|
string ToKey,
|
|
string FromDisplayName,
|
|
string ToDisplayName,
|
|
int SameNeighborCount,
|
|
int ConnectedNeighborCount,
|
|
double EntityLengthMm);
|
|
|
|
sealed class PendingEntityColor(
|
|
object entity,
|
|
string handle,
|
|
string objectName,
|
|
string originalLayer,
|
|
List<HullPoint> samples,
|
|
EntityAssignment assignment)
|
|
{
|
|
public object Entity { get; } = entity;
|
|
public string Handle { get; } = handle;
|
|
public string ObjectName { get; } = objectName;
|
|
public string OriginalLayer { get; } = originalLayer;
|
|
public List<HullPoint> Samples { get; } = samples;
|
|
public string OriginalKey { get; } = assignment.Key;
|
|
public EntityAssignment Assignment { get; set; } = assignment;
|
|
public bool UnassignedFilled { get; set; }
|
|
public bool ContinuityAdjusted { get; set; }
|
|
}
|
|
|
|
sealed class UnassignedEntityColor(
|
|
object entity,
|
|
string handle,
|
|
string objectName,
|
|
string originalLayer,
|
|
List<HullPoint> samples,
|
|
SampleNearestOwnership? nearest)
|
|
{
|
|
public object Entity { get; } = entity;
|
|
public string Handle { get; } = handle;
|
|
public string ObjectName { get; } = objectName;
|
|
public string OriginalLayer { get; } = originalLayer;
|
|
public List<HullPoint> Samples { get; } = samples;
|
|
public SampleNearestOwnership? Nearest { get; } = nearest;
|
|
public bool Filled { get; set; }
|
|
}
|
|
|
|
sealed class Options
|
|
{
|
|
public string InputJsonl { get; private set; } = "";
|
|
public string SourceDwg { get; private set; } = "";
|
|
public string OutputDwg { get; private set; } = "";
|
|
public string ReportPath { get; private set; } = "";
|
|
public string LegendPath { get; private set; } = "";
|
|
public string ProgId { get; private set; } = "AutoCAD.Application.23.1";
|
|
public double StepMm { get; private set; } = 4.0;
|
|
public double PadMm { get; private set; } = 2.0;
|
|
public int MinGroupPoints { get; private set; } = 2;
|
|
public int MinClusterPoints { get; private set; } = 2;
|
|
public int Lineweight { get; private set; } = 35;
|
|
public int EntityLineweight { get; private set; } = 25;
|
|
public bool DrawLabels { get; private set; } = true;
|
|
public bool ColorEntities { get; private set; }
|
|
public bool CloseDocument { get; private set; }
|
|
public double LabelHeightMm { get; private set; } = 3.0;
|
|
public double LabelOffsetMm { get; private set; } = 2.5;
|
|
public string TextStyleName { get; private set; } = "AG4_CN_TEXT";
|
|
public string TextTypeface { get; private set; } = "SimSun";
|
|
public string TextFontFile { get; private set; } = "";
|
|
public double MaxEntityAssignDistanceMm { get; private set; } = 8.0;
|
|
public double ViewMinX { get; private set; } = double.NegativeInfinity;
|
|
public double ViewMinY { get; private set; } = double.NegativeInfinity;
|
|
public double ViewMaxX { get; private set; } = double.PositiveInfinity;
|
|
public double ViewMaxY { get; private set; } = double.PositiveInfinity;
|
|
public bool HasViewBounds { get; private set; }
|
|
public List<ViewBoundsMm> ViewBoundsMm { get; } = new();
|
|
public int MaxSkippedSamples { get; private set; } = 200;
|
|
public bool SmoothContinuity { get; private set; } = true;
|
|
public double ContinuityToleranceMm { get; private set; } = 1.0;
|
|
public int ContinuityMinSameNeighbors { get; private set; } = 2;
|
|
public double ContinuityMaxEntityLengthMm { get; private set; } = 40.0;
|
|
public bool FillUnassignedByContinuity { get; private set; } = true;
|
|
public double FillToleranceMm { get; private set; } = 2.0;
|
|
public int FillMinSameNeighbors { get; private set; } = 1;
|
|
public double FillMaxEntityLengthMm { get; private set; } = 60.0;
|
|
public int[] Colors { get; private set; } = [1, 3, 5, 6, 2, 4, 30, 94, 140, 200, 10, 170];
|
|
|
|
public bool ContainsViewPoint(double xMm, double yMm)
|
|
{
|
|
if (ViewBoundsMm.Count == 0)
|
|
return xMm >= ViewMinX && xMm <= ViewMaxX && yMm >= ViewMinY && yMm <= ViewMaxY;
|
|
|
|
return ViewBoundsMm.Any(box =>
|
|
xMm >= box.MinX &&
|
|
xMm <= box.MaxX &&
|
|
yMm >= box.MinY &&
|
|
yMm <= box.MaxY);
|
|
}
|
|
|
|
public static Options Parse(string[] args)
|
|
{
|
|
var options = new Options();
|
|
for (var i = 0; i < args.Length; i++)
|
|
{
|
|
var arg = args[i];
|
|
string Next() => i + 1 < args.Length ? args[++i] : throw new ArgumentException($"Missing value after {arg}");
|
|
|
|
switch (arg.ToLowerInvariant())
|
|
{
|
|
case "--input":
|
|
case "--input-jsonl":
|
|
options.InputJsonl = Next();
|
|
break;
|
|
case "--source-dwg":
|
|
options.SourceDwg = Next();
|
|
break;
|
|
case "--output-dwg":
|
|
options.OutputDwg = Next();
|
|
break;
|
|
case "--report":
|
|
case "--output":
|
|
options.ReportPath = Next();
|
|
break;
|
|
case "--legend":
|
|
case "--color-legend":
|
|
case "--legend-text":
|
|
options.LegendPath = Next();
|
|
break;
|
|
case "--prog-id":
|
|
options.ProgId = Next();
|
|
break;
|
|
case "--step-mm":
|
|
options.StepMm = double.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--pad-mm":
|
|
options.PadMm = double.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--min-group-points":
|
|
options.MinGroupPoints = int.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--min-cluster-points":
|
|
options.MinClusterPoints = int.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--lineweight":
|
|
options.Lineweight = int.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--entity-lineweight":
|
|
options.EntityLineweight = int.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--colors":
|
|
options.Colors = Next()
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Select(item => int.Parse(item, CultureInfo.InvariantCulture))
|
|
.ToArray();
|
|
if (options.Colors.Length == 0)
|
|
throw new ArgumentException("--colors requires at least one AutoCAD color index.");
|
|
break;
|
|
case "--color-entities":
|
|
options.ColorEntities = true;
|
|
break;
|
|
case "--max-entity-assign-distance-mm":
|
|
options.MaxEntityAssignDistanceMm = double.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--view-bounds-mm":
|
|
var parts = Next().Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
if (parts.Length != 4)
|
|
throw new ArgumentException("--view-bounds-mm requires minX,minY,maxX,maxY.");
|
|
options.ViewMinX = double.Parse(parts[0], CultureInfo.InvariantCulture);
|
|
options.ViewMinY = double.Parse(parts[1], CultureInfo.InvariantCulture);
|
|
options.ViewMaxX = double.Parse(parts[2], CultureInfo.InvariantCulture);
|
|
options.ViewMaxY = double.Parse(parts[3], CultureInfo.InvariantCulture);
|
|
options.HasViewBounds = true;
|
|
options.ViewBoundsMm.Add(new ViewBoundsMm(options.ViewMinX, options.ViewMinY, options.ViewMaxX, options.ViewMaxY));
|
|
break;
|
|
case "--max-skipped-samples":
|
|
options.MaxSkippedSamples = Math.Max(0, int.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--smooth-continuity":
|
|
options.SmoothContinuity = true;
|
|
break;
|
|
case "--no-smooth-continuity":
|
|
options.SmoothContinuity = false;
|
|
break;
|
|
case "--continuity-tolerance-mm":
|
|
options.ContinuityToleranceMm = Math.Max(0, double.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--continuity-min-same-neighbors":
|
|
options.ContinuityMinSameNeighbors = Math.Max(1, int.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--continuity-max-entity-length-mm":
|
|
options.ContinuityMaxEntityLengthMm = Math.Max(0, double.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--fill-unassigned-by-continuity":
|
|
options.FillUnassignedByContinuity = true;
|
|
break;
|
|
case "--no-fill-unassigned-by-continuity":
|
|
options.FillUnassignedByContinuity = false;
|
|
break;
|
|
case "--fill-tolerance-mm":
|
|
options.FillToleranceMm = Math.Max(0, double.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--fill-min-same-neighbors":
|
|
options.FillMinSameNeighbors = Math.Max(1, int.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--fill-max-entity-length-mm":
|
|
options.FillMaxEntityLengthMm = Math.Max(0, double.Parse(Next(), CultureInfo.InvariantCulture));
|
|
break;
|
|
case "--no-labels":
|
|
options.DrawLabels = false;
|
|
break;
|
|
case "--close-document":
|
|
options.CloseDocument = true;
|
|
break;
|
|
case "--label-height-mm":
|
|
options.LabelHeightMm = double.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--label-offset-mm":
|
|
options.LabelOffsetMm = double.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--text-style":
|
|
options.TextStyleName = Next();
|
|
break;
|
|
case "--text-typeface":
|
|
options.TextTypeface = Next();
|
|
break;
|
|
case "--text-font-file":
|
|
options.TextFontFile = Next();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(options.ReportPath) && !string.IsNullOrWhiteSpace(options.OutputDwg))
|
|
options.ReportPath = Path.ChangeExtension(options.OutputDwg, ".ownership-contours.json");
|
|
if (string.IsNullOrWhiteSpace(options.LegendPath) && !string.IsNullOrWhiteSpace(options.ReportPath))
|
|
options.LegendPath = Path.ChangeExtension(options.ReportPath, ".color-legend.txt");
|
|
|
|
return options;
|
|
}
|
|
}
|