mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-01 01:01:35 -04:00
feat(cad): add radius and diameter dimensions
Holes and radii need sizes on the drawing as well as the overall extents. Select circles or arcs, right-click and choose Add Radius Dimension or Add Diameter Dimension. The value is measured live from the curve, found by its entity ID, so it follows edits; only the leader angle and label offset are saved. A radius on an arc stays on its sweep, and labels are kept clear of the curve and center. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
05d5c56ac0
commit
ca693285bb
14 files changed
+689
-35
No files matched your search
@@ -1,17 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Dimensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves dimensions against the entities being cut (visible, with roles, breakoffs and
|
||||
/// tabbed cutouts applied), so a dimension measures the part as it will come off the table.
|
||||
/// Radius and diameter dimensions find their circle or arc by ID among the visible entities.
|
||||
/// </summary>
|
||||
public static class DimensionBuilder
|
||||
{
|
||||
private const double DefaultOffsetFactor = 0.1;
|
||||
private const double RadialOffsetFactor = 0.05;
|
||||
|
||||
/// <summary>
|
||||
/// The extents of the geometry that bounds the part, or null if there is none. Source marks
|
||||
@@ -35,6 +39,9 @@ namespace OpenNest.Dimensions
|
||||
if (dimension.Offset == 0 || !double.IsFinite(dimension.Offset))
|
||||
return new DimensionLayout { Kind = kind, Error = "its offset is not usable" };
|
||||
|
||||
if (dimension.IsRadial)
|
||||
return RadialLayout(entities, dimension);
|
||||
|
||||
var measured = Measured(entities);
|
||||
if (measured.Count == 0)
|
||||
return new DimensionLayout { Kind = kind, Error = "there is no cut geometry to measure" };
|
||||
@@ -102,5 +109,126 @@ namespace OpenNest.Dimensions
|
||||
added.Add(new DrawingDimension { Kind = DimensionKind.OverallVertical, Offset = offset });
|
||||
return added;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <paramref name="kind"/> dimension for each circle or arc in <paramref name="curves"/>
|
||||
/// that does not have one yet. On a circle the leader points up and right for a radius and
|
||||
/// up and left for a diameter, so the two do not overlap; on an arc it points through the
|
||||
/// middle. The label sits outside the curve by half its radius, or a twentieth of
|
||||
/// the part's larger size when that is more.
|
||||
/// </summary>
|
||||
public static List<DrawingDimension> MissingRadial(IEnumerable<Entity> curves, DimensionKind kind,
|
||||
IEnumerable<Entity> part, IEnumerable<DrawingDimension> existing)
|
||||
{
|
||||
var added = new List<DrawingDimension>();
|
||||
if (!DrawingDimension.IsRadialKind(kind))
|
||||
return added;
|
||||
|
||||
var box = Extents(part);
|
||||
var partSize = box == null ? 0 : System.Math.Max(box.Length, box.Width);
|
||||
var taken = existing.Where(d => d.Kind == kind).Select(d => d.EntityId).ToHashSet();
|
||||
foreach (var curve in curves)
|
||||
{
|
||||
if (curve.Id == Guid.Empty || !taken.Add(curve.Id) || !TryCurve(curve, out _, out var radius, out var arc))
|
||||
continue;
|
||||
added.Add(new DrawingDimension
|
||||
{
|
||||
Kind = kind,
|
||||
EntityId = curve.Id,
|
||||
Angle = arc != null ? MidAngle(arc)
|
||||
: kind == DimensionKind.Radius ? Angle.HalfPI / 2 : Angle.HalfPI * 1.5,
|
||||
Offset = System.Math.Max(radius / 2, partSize * RadialOffsetFactor),
|
||||
});
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/// <summary>Whether <paramref name="entity"/> is a circle or arc a radius or diameter can measure.</summary>
|
||||
public static bool IsRadialTarget(Entity entity) => TryCurve(entity, out _, out _, out _);
|
||||
|
||||
private static bool TryCurve(Entity entity, out Vector center, out double radius, out Arc arc)
|
||||
{
|
||||
arc = null;
|
||||
switch (entity)
|
||||
{
|
||||
case Circle circle:
|
||||
center = circle.Center;
|
||||
radius = circle.Radius;
|
||||
break;
|
||||
case Arc a:
|
||||
center = a.Center;
|
||||
radius = a.Radius;
|
||||
arc = a.IsFullCircle() ? null : a;
|
||||
break;
|
||||
default:
|
||||
center = default;
|
||||
radius = 0;
|
||||
return false;
|
||||
}
|
||||
return radius > 0 && double.IsFinite(radius);
|
||||
}
|
||||
|
||||
private static DimensionLayout RadialLayout(IEnumerable<Entity> entities, DrawingDimension dimension)
|
||||
{
|
||||
var kind = dimension.Kind;
|
||||
if (!double.IsFinite(dimension.Angle))
|
||||
return new DimensionLayout { Kind = kind, Error = "its leader angle is not usable" };
|
||||
|
||||
var entity = dimension.EntityId == Guid.Empty
|
||||
? null
|
||||
: entities.FirstOrDefault(e => e.Id == dimension.EntityId);
|
||||
if (entity == null)
|
||||
return new DimensionLayout { Kind = kind, Error = "its circle or arc is not cut" };
|
||||
if (!TryCurve(entity, out var center, out var radius, out var arc))
|
||||
return new DimensionLayout { Kind = kind, Error = "it no longer measures a circle or arc" };
|
||||
|
||||
var reach = radius + dimension.Offset;
|
||||
if (reach <= 0)
|
||||
return new DimensionLayout { Kind = kind, Error = "its offset is not usable" };
|
||||
|
||||
var angle = arc != null && kind == DimensionKind.Radius
|
||||
? ClampToSweep(arc, dimension.Angle)
|
||||
: dimension.Angle;
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
var arrow = center + direction * radius;
|
||||
var far = center - direction * radius;
|
||||
return new DimensionLayout
|
||||
{
|
||||
Kind = kind,
|
||||
Value = kind == DimensionKind.Diameter ? radius * 2 : radius,
|
||||
Center = center,
|
||||
Radius = radius,
|
||||
Angle = angle,
|
||||
Arc = arc,
|
||||
ArrowPoint = arrow,
|
||||
FarArrowPoint = far,
|
||||
LineStart = kind == DimensionKind.Diameter ? far : center,
|
||||
LineEnd = center + direction * reach,
|
||||
Extents = new Box(center.X - radius, center.Y - radius, radius * 2, radius * 2),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary><paramref name="angle"/> if it is on the arc's sweep, else the nearer end's angle.</summary>
|
||||
public static double ClampToSweep(Arc arc, double angle)
|
||||
{
|
||||
if (Angle.IsBetweenRad(angle, arc.StartAngle, arc.EndAngle, arc.IsReversed))
|
||||
return angle;
|
||||
var toStart = AngularDistance(angle, arc.StartAngle);
|
||||
var toEnd = AngularDistance(angle, arc.EndAngle);
|
||||
return toStart <= toEnd ? arc.StartAngle : arc.EndAngle;
|
||||
}
|
||||
|
||||
private static double AngularDistance(double a, double b)
|
||||
{
|
||||
var d = Angle.NormalizeRad(a - b);
|
||||
return System.Math.Min(d, Angle.TwoPI - d);
|
||||
}
|
||||
|
||||
private static double MidAngle(Arc arc)
|
||||
{
|
||||
// SweepAngle runs counterclockwise from the start, or from the end on a reversed arc.
|
||||
var from = arc.IsReversed ? arc.EndAngle : arc.StartAngle;
|
||||
return Angle.NormalizeRad(from + arc.SweepAngle() / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,61 @@ namespace OpenNest.Dimensions
|
||||
|
||||
public string ValueText => Value.ToString("0.000", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>The value as drawn: radius and diameter carry their drafting prefix.</summary>
|
||||
public string DisplayText => Kind switch
|
||||
{
|
||||
DimensionKind.Radius => "R" + ValueText,
|
||||
DimensionKind.Diameter => "\u00D8" + ValueText,
|
||||
_ => ValueText,
|
||||
};
|
||||
|
||||
public Vector MidPoint => new((LineStart.X + LineEnd.X) / 2, (LineStart.Y + LineEnd.Y) / 2);
|
||||
|
||||
public bool IsRadial => DrawingDimension.IsRadialKind(Kind);
|
||||
|
||||
// Radius and diameter: the leader runs from LineStart (the center for a radius, the far
|
||||
// side of the curve for a diameter) through ArrowPoint on the curve to LineEnd, the label.
|
||||
|
||||
/// <summary>Radius and diameter: the measured curve's center.</summary>
|
||||
public Vector Center { get; init; }
|
||||
|
||||
/// <summary>Radius and diameter: the measured curve's radius.</summary>
|
||||
public double Radius { get; init; }
|
||||
|
||||
/// <summary>Radius and diameter: the leader's direction from the center, in radians.</summary>
|
||||
public double Angle { get; init; }
|
||||
|
||||
/// <summary>Radius and diameter: where the leader meets the curve on the label's side.</summary>
|
||||
public Vector ArrowPoint { get; init; }
|
||||
|
||||
/// <summary>Diameter: where the leader meets the curve opposite <see cref="ArrowPoint"/>.</summary>
|
||||
public Vector FarArrowPoint { get; init; }
|
||||
|
||||
/// <summary>Radius and diameter on an arc: the measured arc; null for a circle.</summary>
|
||||
public Arc Arc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Radius and diameter: the leader angle and offset that put the label at
|
||||
/// <paramref name="point"/>, with the label at least <paramref name="minimum"/> from both the
|
||||
/// curve and the center. A radius on an arc stays on the arc's sweep.
|
||||
/// </summary>
|
||||
public (double Offset, double Angle) RadialPlacementAt(Vector point, double minimum)
|
||||
{
|
||||
var dx = point.X - Center.X;
|
||||
var dy = point.Y - Center.Y;
|
||||
var distance = System.Math.Sqrt(dx * dx + dy * dy);
|
||||
var angle = distance > 0 ? System.Math.Atan2(dy, dx) : Angle;
|
||||
if (Arc != null && Kind == DimensionKind.Radius)
|
||||
angle = DimensionBuilder.ClampToSweep(Arc, angle);
|
||||
|
||||
var offset = distance - Radius;
|
||||
if (offset > -minimum && offset < minimum)
|
||||
offset = offset >= 0 ? minimum : -minimum;
|
||||
if (offset < minimum - Radius)
|
||||
offset = minimum - Radius <= -minimum ? minimum - Radius : minimum;
|
||||
return (offset, Math.Angle.NormalizeRad(angle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where an extension line leaving <paramref name="foot"/> on the dimension line first
|
||||
/// meets the profile offset by <paramref name="clearance"/>, walking toward the part by the
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Dimensions
|
||||
{
|
||||
public enum DimensionKind
|
||||
@@ -7,6 +9,12 @@ namespace OpenNest.Dimensions
|
||||
|
||||
/// <summary>The part's overall size along Y, drawn right or left of it.</summary>
|
||||
OverallVertical,
|
||||
|
||||
/// <summary>A circle's or arc's radius, on a leader from its center.</summary>
|
||||
Radius,
|
||||
|
||||
/// <summary>A circle's or arc's diameter, on a leader across it.</summary>
|
||||
Diameter,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -18,11 +26,23 @@ namespace OpenNest.Dimensions
|
||||
public DimensionKind Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Distance from the part's extents to the dimension line; never zero. Positive is above a
|
||||
/// horizontal dimension's part or right of a vertical one's, negative the opposite side.
|
||||
/// Never zero. Overall dimensions: distance from the part's extents to the dimension line,
|
||||
/// positive above a horizontal dimension's part or right of a vertical one's. Radius and
|
||||
/// diameter: distance from the curve to the label along the leader, positive outside.
|
||||
/// </summary>
|
||||
public double Offset { get; set; }
|
||||
|
||||
public DrawingDimension Clone() => new() { Kind = Kind, Offset = Offset };
|
||||
/// <summary>The circle or arc a radius or diameter measures; empty for overall dimensions.</summary>
|
||||
public Guid EntityId { get; set; }
|
||||
|
||||
/// <summary>Radius and diameter: the leader's direction from the center, in radians.</summary>
|
||||
public double Angle { get; set; }
|
||||
|
||||
public bool IsRadial => IsRadialKind(Kind);
|
||||
|
||||
public static bool IsRadialKind(DimensionKind kind) =>
|
||||
kind is DimensionKind.Radius or DimensionKind.Diameter;
|
||||
|
||||
public DrawingDimension Clone() => new() { Kind = Kind, Offset = Offset, EntityId = EntityId, Angle = Angle };
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,56 @@ public class DimensionPersistenceTests
|
||||
Assert.Contains(warnings, w => w.Contains("dimension 1 could not be read"));
|
||||
}
|
||||
|
||||
private static Drawing WithRadial()
|
||||
{
|
||||
var drawing = DrawingPackageFixture.Build();
|
||||
drawing.Dimensions.Add(new DrawingDimension { Kind = DimensionKind.OverallHorizontal, Offset = -1.25 });
|
||||
drawing.Dimensions.Add(new DrawingDimension
|
||||
{
|
||||
Kind = DimensionKind.Diameter,
|
||||
EntityId = RadialId,
|
||||
Angle = 0.75,
|
||||
Offset = 0.5,
|
||||
});
|
||||
return drawing;
|
||||
}
|
||||
|
||||
private static readonly Guid RadialId = Guid.Parse("6d6f0d55-2f5b-4e83-9d1e-5a0b8f1c2a11");
|
||||
|
||||
[Fact]
|
||||
public void RadialDimensions_RoundTripTheirCurveAndAngle_AndOverallOnesOmitThem()
|
||||
{
|
||||
var archive = Write(WithRadial());
|
||||
var json = NestJson(archive)["drawings"]![0]!["dimensions"]!;
|
||||
|
||||
Assert.False(json[0]!.AsObject().ContainsKey("entityId"));
|
||||
Assert.False(json[0]!.AsObject().ContainsKey("angle"));
|
||||
|
||||
var (restored, warnings) = RoundTrip(archive);
|
||||
Assert.Empty(warnings);
|
||||
var radial = Assert.Single(restored.Drawings).Dimensions[1];
|
||||
Assert.Equal(DimensionKind.Diameter, radial.Kind);
|
||||
Assert.Equal(RadialId, radial.EntityId);
|
||||
Assert.Equal(0.75, radial.Angle);
|
||||
Assert.Equal(0.5, radial.Offset);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("entityId")]
|
||||
[InlineData("angle")]
|
||||
public void RadialDimensionWithoutItsCurveOrAngle_IsDroppedWithAWarning(string field)
|
||||
{
|
||||
var archive = Write(WithRadial());
|
||||
var json = NestJson(archive);
|
||||
json["drawings"]![0]!["dimensions"]![1]!.AsObject().Remove(field);
|
||||
|
||||
var (restored, warnings) = RoundTrip(ReplaceNestJson(archive, json));
|
||||
|
||||
var dimension = Assert.Single(Assert.Single(restored.Drawings).Dimensions);
|
||||
Assert.Equal(DimensionKind.OverallHorizontal, dimension.Kind);
|
||||
Assert.Contains(warnings, w => w.Contains("dimension 2 could not be read"));
|
||||
}
|
||||
|
||||
private static byte[] Write(Drawing drawing)
|
||||
{
|
||||
var nest = new Nest(drawing.Name) { Units = Units.Inches };
|
||||
|
||||
@@ -495,6 +495,14 @@ namespace OpenNest.IO
|
||||
{
|
||||
public string Kind { get; init; }
|
||||
public double Offset { get; init; }
|
||||
|
||||
/// <summary>Radius and diameter: the measured circle or arc. Omitted for overall dimensions.</summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Guid? EntityId { get; init; }
|
||||
|
||||
/// <summary>Radius and diameter: the leader angle in radians. Omitted for overall dimensions.</summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public double? Angle { get; init; }
|
||||
}
|
||||
|
||||
public record TabbedCutoutDto
|
||||
|
||||
@@ -468,9 +468,18 @@ namespace OpenNest.IO
|
||||
{
|
||||
var dto = d.Dimensions[i];
|
||||
if (dto != null && TryParseName<DimensionKind>(dto.Kind, out var kind)
|
||||
&& dto.Offset != 0 && double.IsFinite(dto.Offset))
|
||||
&& dto.Offset != 0 && double.IsFinite(dto.Offset)
|
||||
&& (!DrawingDimension.IsRadialKind(kind)
|
||||
|| (dto.EntityId is { } id && id != Guid.Empty
|
||||
&& dto.Angle is { } angle && double.IsFinite(angle))))
|
||||
{
|
||||
dimensions.Add(new DrawingDimension { Kind = kind, Offset = dto.Offset });
|
||||
dimensions.Add(new DrawingDimension
|
||||
{
|
||||
Kind = kind,
|
||||
Offset = dto.Offset,
|
||||
EntityId = DrawingDimension.IsRadialKind(kind) ? dto.EntityId!.Value : Guid.Empty,
|
||||
Angle = DrawingDimension.IsRadialKind(kind) ? dto.Angle!.Value : 0,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -269,6 +269,8 @@ namespace OpenNest.IO
|
||||
{
|
||||
Kind = m.Kind.ToString(),
|
||||
Offset = m.Offset,
|
||||
EntityId = m.IsRadial ? m.EntityId : null,
|
||||
Angle = m.IsRadial ? m.Angle : null,
|
||||
}).ToList()
|
||||
: null,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
using OpenNest.Dimensions;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Tests.Dimensions;
|
||||
|
||||
public class RadialDimensionTests
|
||||
{
|
||||
private static readonly Layer Cut = new("0") { IsVisible = true };
|
||||
|
||||
private static Circle Hole(double x = 5, double y = 3, double radius = 1) =>
|
||||
new(x, y, radius) { Layer = Cut, Id = Guid.NewGuid() };
|
||||
|
||||
private static DimensionLayout Layout(Entity curve, DimensionKind kind, double angle, double offset = 0.5) =>
|
||||
DimensionBuilder.Layout([curve], new DrawingDimension
|
||||
{
|
||||
Kind = kind,
|
||||
EntityId = curve.Id,
|
||||
Angle = angle,
|
||||
Offset = offset,
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void Radius_MeasuresTheCircle_WithTheLeaderFromTheCenter()
|
||||
{
|
||||
var hole = Hole(radius: 1.25);
|
||||
|
||||
var layout = Layout(hole, DimensionKind.Radius, 0, 0.5);
|
||||
|
||||
Assert.True(layout.IsValid, layout.Error);
|
||||
Assert.Equal(1.25, layout.Value, 9);
|
||||
Assert.Equal("R1.250", layout.DisplayText);
|
||||
Assert.Equal(0, layout.LineStart.DistanceTo(new Vector(5, 3)), 12);
|
||||
Assert.Equal(0, layout.ArrowPoint.DistanceTo(new Vector(6.25, 3)), 12);
|
||||
Assert.Equal(0, layout.LineEnd.DistanceTo(new Vector(6.75, 3)), 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diameter_MeasuresTwiceTheRadius_WithTheLeaderAcrossTheCircle()
|
||||
{
|
||||
var hole = Hole(radius: 1.25);
|
||||
|
||||
var layout = Layout(hole, DimensionKind.Diameter, Angle.HalfPI, 0.5);
|
||||
|
||||
Assert.True(layout.IsValid, layout.Error);
|
||||
Assert.Equal(2.5, layout.Value, 9);
|
||||
Assert.Equal("\u00D82.500", layout.DisplayText);
|
||||
Assert.Equal(0, layout.LineStart.DistanceTo(new Vector(5, 1.75)), 12);
|
||||
Assert.Equal(0, layout.ArrowPoint.DistanceTo(new Vector(5, 4.25)), 12);
|
||||
Assert.Equal(0, layout.LineEnd.DistanceTo(new Vector(5, 4.75)), 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadiusOnAnArc_StaysOnItsSweep()
|
||||
{
|
||||
// A quarter arc from 0 to 90 degrees; a leader asked for at 180 degrees snaps to the
|
||||
// nearer end, 90 degrees.
|
||||
var arc = new Arc(new Vector(0, 0), 2, 0, Angle.HalfPI) { Layer = Cut, Id = Guid.NewGuid() };
|
||||
|
||||
var layout = Layout(arc, DimensionKind.Radius, System.Math.PI * 0.9);
|
||||
|
||||
Assert.True(layout.IsValid, layout.Error);
|
||||
Assert.Equal(2, layout.Value, 9);
|
||||
Assert.Equal(Angle.HalfPI, layout.Angle, 9);
|
||||
Assert.Equal(0, layout.ArrowPoint.DistanceTo(new Vector(0, 2)), 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AMissingCurve_IsNotShown()
|
||||
{
|
||||
var layout = DimensionBuilder.Layout([Hole()], new DrawingDimension
|
||||
{
|
||||
Kind = DimensionKind.Radius,
|
||||
EntityId = Guid.NewGuid(),
|
||||
Angle = 0,
|
||||
Offset = 1,
|
||||
});
|
||||
|
||||
Assert.False(layout.IsValid);
|
||||
Assert.Contains("not cut", layout.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ALine_CannotBeMeasuredAsARadius()
|
||||
{
|
||||
var line = new Line(0, 0, 4, 0) { Layer = Cut, Id = Guid.NewGuid() };
|
||||
|
||||
Assert.False(DimensionBuilder.IsRadialTarget(line));
|
||||
Assert.Empty(DimensionBuilder.MissingRadial([line], DimensionKind.Radius, [line], []));
|
||||
Assert.False(Layout(line, DimensionKind.Radius, 0).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingRadial_AddsOncePerCurveAndKind_OutsideTheCurve()
|
||||
{
|
||||
var hole = Hole(radius: 1);
|
||||
var arc = new Arc(new Vector(0, 0), 2, 0, System.Math.PI) { Layer = Cut, Id = Guid.NewGuid() };
|
||||
|
||||
var first = DimensionBuilder.MissingRadial([hole, arc], DimensionKind.Radius, [hole, arc], []);
|
||||
|
||||
Assert.Equal(2, first.Count);
|
||||
Assert.All(first, d => Assert.True(d.Offset > 0));
|
||||
Assert.Equal(Angle.HalfPI, first[1].Angle, 9); // The middle of the half arc.
|
||||
Assert.Empty(DimensionBuilder.MissingRadial([hole, arc], DimensionKind.Radius, [hole, arc], first));
|
||||
Assert.Equal(2, DimensionBuilder.MissingRadial([hole, arc], DimensionKind.Diameter, [hole, arc], first).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingRadial_OnAReversedArc_PointsThroughItsMiddle()
|
||||
{
|
||||
// Clockwise from 90 down to 0 degrees covers the first quadrant; its middle is 45.
|
||||
var arc = new Arc(new Vector(0, 0), 2, Angle.HalfPI, 0, reversed: true) { Layer = Cut, Id = Guid.NewGuid() };
|
||||
|
||||
var added = Assert.Single(DimensionBuilder.MissingRadial([arc], DimensionKind.Radius, [arc], []));
|
||||
|
||||
Assert.Equal(Angle.HalfPI / 2, added.Angle, 9);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(8, 3, 2, 0)] // Outside to the right: 2 past the curve.
|
||||
[InlineData(5, 3.5, -0.5, 0)] // Inside, near the center: kept the minimum clear of it.
|
||||
[InlineData(6.02, 3, 0.1, 0)] // Hugging the curve: pushed the minimum out.
|
||||
public void Dragging_PutsTheLabelUnderTheCursor_KeptClearOfCurveAndCenter(double x, double y,
|
||||
double expectedOffset, double expectedAngle)
|
||||
{
|
||||
var layout = Layout(Hole(radius: 1), DimensionKind.Radius, 0);
|
||||
|
||||
var (offset, angle) = layout.RadialPlacementAt(new Vector(x, y), 0.1);
|
||||
|
||||
Assert.Equal(expectedOffset, offset, 9);
|
||||
if (y == 3)
|
||||
Assert.Equal(expectedAngle, angle, 9);
|
||||
else
|
||||
Assert.Equal(Angle.HalfPI, angle, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dragging_ASmallHolesLabelInside_KeepsItOutsideInstead()
|
||||
{
|
||||
// Radius 0.15 with a 0.1 minimum: no inside spot is clear of both curve and center.
|
||||
var layout = Layout(Hole(radius: 0.15), DimensionKind.Diameter, 0);
|
||||
|
||||
var (offset, _) = layout.RadialPlacementAt(new Vector(5.05, 3), 0.1);
|
||||
|
||||
Assert.Equal(0.1, offset, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clone_KeepsTheCurveAndAngle()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var clone = new DrawingDimension { Kind = DimensionKind.Diameter, EntityId = id, Angle = 1.5, Offset = 2 }.Clone();
|
||||
|
||||
Assert.Equal((DimensionKind.Diameter, id, 1.5, 2.0), (clone.Kind, clone.EntityId, clone.Angle, clone.Offset));
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,72 @@ public class CadConverterDimensionTests
|
||||
form.Close();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void RadiusAndDiameter_FromTheEntityMenu_DragAroundTheHole_AndSurviveReopening() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
var part = Part();
|
||||
var hole = new Circle(5, 3, 1) { Layer = part.SourceEntities![0].Layer };
|
||||
part.SourceEntities.Add(hole);
|
||||
|
||||
using var form = new DimensionTestForm();
|
||||
Show(form);
|
||||
form.LoadDrawings([part]);
|
||||
var view = Field<EntityView>(form, "entityView1");
|
||||
var item = Field<FileListControl>(form, "fileList").SelectedItem!;
|
||||
|
||||
Invoke(form, "OnEntityActionRequested", null, new EntityActionEventArgs(EntityAction.AddRadiusDimension, [hole]));
|
||||
Invoke(form, "OnEntityActionRequested", null, new EntityActionEventArgs(EntityAction.AddDiameterDimension, [hole]));
|
||||
// Each kind exists once per curve.
|
||||
Invoke(form, "OnEntityActionRequested", null, new EntityActionEventArgs(EntityAction.AddRadiusDimension, [hole]));
|
||||
|
||||
Assert.Equal(new[] { DimensionKind.Radius, DimensionKind.Diameter }, item.Dimensions.Select(d => d.Kind));
|
||||
Assert.Equal(new[] { 1.0, 2.0 }, view.DimensionLayouts.Select(l => l.Value));
|
||||
|
||||
// Drag the radius label to straight left of the hole, 2 past its edge.
|
||||
var press = view.PointWorldToControl(view.DimensionLayouts[0].LineEnd);
|
||||
var release = view.PointWorldToControl(new Vector(2, 3));
|
||||
Mouse(view, "OnMouseDown", press);
|
||||
Mouse(view, "OnMouseMove", release);
|
||||
Mouse(view, "OnMouseUp", release);
|
||||
Assert.InRange(item.Dimensions[0].Offset, 1.9, 2.1);
|
||||
Assert.InRange(view.DimensionLayouts[0].ArrowPoint.X, 3.99, 4.01);
|
||||
|
||||
var saved = Assert.Single(form.GetDrawings());
|
||||
Assert.All(saved.Dimensions, d => Assert.Equal(hole.Id, d.EntityId));
|
||||
form.Close();
|
||||
|
||||
// Reopened, both still measure the hole; excluded, they are listed as not shown.
|
||||
using var reopened = new DimensionTestForm();
|
||||
Show(reopened);
|
||||
reopened.LoadDrawings([saved]);
|
||||
var reopenedView = Field<EntityView>(reopened, "entityView1");
|
||||
Assert.Equal(new[] { 1.0, 2.0 }, reopenedView.DimensionLayouts.Select(l => l.Value));
|
||||
|
||||
var reopenedHole = Field<FileListControl>(reopened, "fileList").SelectedItem!.Entities.Single(e => e.Id == hole.Id);
|
||||
Invoke(reopened, "OnEntityActionRequested", null, new EntityActionEventArgs(EntityAction.Exclude, [reopenedHole]));
|
||||
Assert.All(reopenedView.DimensionLayouts, l => Assert.False(l.IsValid));
|
||||
reopened.Close();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void DisplayOnlyCircle_ExplainsHowToDimensionIt() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
var part = Part();
|
||||
var hole = new Circle(5, 3, 1) { Layer = part.SourceEntities![0].Layer };
|
||||
part.SourceEntities.Add(hole);
|
||||
|
||||
using var form = new DimensionTestForm();
|
||||
Show(form);
|
||||
form.LoadDrawings([part]);
|
||||
Invoke(form, "OnEntityActionRequested", null, new EntityActionEventArgs(EntityAction.Display, [hole]));
|
||||
|
||||
Invoke(form, "OnEntityActionRequested", null, new EntityActionEventArgs(EntityAction.AddRadiusDimension, [hole]));
|
||||
|
||||
Assert.Contains("Cut or Etch", Assert.Single(form.Messages));
|
||||
Assert.Empty(Field<FileListControl>(form, "fileList").SelectedItem!.Dimensions);
|
||||
form.Close();
|
||||
});
|
||||
|
||||
private static Drawing Part()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
|
||||
@@ -10,16 +10,23 @@ namespace OpenNest.Controls
|
||||
{
|
||||
public sealed class DimensionDragEventArgs : EventArgs
|
||||
{
|
||||
public DimensionDragEventArgs(int dimension, double offset)
|
||||
public DimensionDragEventArgs(int dimension, double offset, double? angle = null)
|
||||
{
|
||||
Dimension = dimension;
|
||||
Offset = offset;
|
||||
Angle = angle;
|
||||
}
|
||||
|
||||
public int Dimension { get; }
|
||||
|
||||
/// <summary>The new distance from the part's extents, already kept clear of the part.</summary>
|
||||
/// <summary>
|
||||
/// The new distance from the part's extents, or from the curve for a radius or diameter,
|
||||
/// already kept clear of the part.
|
||||
/// </summary>
|
||||
public double Offset { get; }
|
||||
|
||||
/// <summary>Radius and diameter: the new leader angle; null for overall dimensions.</summary>
|
||||
public double? Angle { get; }
|
||||
}
|
||||
|
||||
public partial class EntityView
|
||||
@@ -30,6 +37,7 @@ namespace OpenNest.Controls
|
||||
private const int DimensionOvershootPixels = 5;
|
||||
private const int ArrowLengthPixels = 9;
|
||||
private const int ArrowHalfWidthPixels = 3;
|
||||
private const int CenterMarkPixels = 4;
|
||||
|
||||
private readonly Font dimensionFont = new Font("Segoe UI", 9f);
|
||||
private int? draggedDimension;
|
||||
@@ -46,7 +54,15 @@ namespace OpenNest.Controls
|
||||
var layout = DimensionLayouts.ElementAtOrDefault(index);
|
||||
if (layout is not { IsValid: true })
|
||||
return;
|
||||
var offset = layout.OffsetAt(PointControlToWorld(location), LengthGuiToWorld(DimensionMinimumPixels));
|
||||
var point = PointControlToWorld(location);
|
||||
var minimum = LengthGuiToWorld(DimensionMinimumPixels);
|
||||
if (layout.IsRadial)
|
||||
{
|
||||
var (radialOffset, angle) = layout.RadialPlacementAt(point, minimum);
|
||||
DimensionDragged?.Invoke(this, new DimensionDragEventArgs(index, radialOffset, angle));
|
||||
return;
|
||||
}
|
||||
var offset = layout.OffsetAt(point, minimum);
|
||||
DimensionDragged?.Invoke(this, new DimensionDragEventArgs(index, offset));
|
||||
}
|
||||
|
||||
@@ -66,8 +82,12 @@ namespace OpenNest.Controls
|
||||
return null;
|
||||
}
|
||||
|
||||
private Cursor DimensionCursor(int index) =>
|
||||
DimensionLayouts[index].Kind == DimensionKind.OverallHorizontal ? Cursors.SizeNS : Cursors.SizeWE;
|
||||
private Cursor DimensionCursor(int index) => DimensionLayouts[index].Kind switch
|
||||
{
|
||||
DimensionKind.OverallHorizontal => Cursors.SizeNS,
|
||||
DimensionKind.OverallVertical => Cursors.SizeWE,
|
||||
_ => Cursors.SizeAll,
|
||||
};
|
||||
|
||||
// Dimensions sit outside the part, so fitting the part alone would leave them off screen.
|
||||
private Box WithDimensions(Box box)
|
||||
@@ -85,6 +105,11 @@ namespace OpenNest.Controls
|
||||
{
|
||||
points.Add(layout.LineStart);
|
||||
points.Add(layout.LineEnd);
|
||||
if (layout.IsRadial)
|
||||
{
|
||||
points.Add(layout.ArrowPoint);
|
||||
points.Add(layout.FarArrowPoint);
|
||||
}
|
||||
}
|
||||
return points.GetBoundingBox();
|
||||
}
|
||||
@@ -113,6 +138,12 @@ namespace OpenNest.Controls
|
||||
|
||||
private void DrawDimension(Graphics g, DimensionLayout layout, Pen pen, Brush brush)
|
||||
{
|
||||
if (layout.IsRadial)
|
||||
{
|
||||
DrawRadialDimension(g, layout, pen, brush);
|
||||
return;
|
||||
}
|
||||
|
||||
var from = PointWorldToGraph(layout.LineStart);
|
||||
var to = PointWorldToGraph(layout.LineEnd);
|
||||
var length = System.Math.Sqrt((to.X - from.X) * (to.X - from.X) + (to.Y - from.Y) * (to.Y - from.Y));
|
||||
@@ -138,6 +169,54 @@ namespace OpenNest.Controls
|
||||
g.Restore(state);
|
||||
}
|
||||
|
||||
// A leader from the center (radius) or the far side of the curve (diameter) out to the
|
||||
// label, with arrows on the curve pointing at it from the label's side.
|
||||
private void DrawRadialDimension(Graphics g, DimensionLayout layout, Pen pen, Brush brush)
|
||||
{
|
||||
var start = PointWorldToGraph(layout.LineStart);
|
||||
var label = PointWorldToGraph(layout.LineEnd);
|
||||
var arrow = PointWorldToGraph(layout.ArrowPoint);
|
||||
g.DrawLine(pen, start, label);
|
||||
|
||||
var center = PointWorldToGraph(layout.Center);
|
||||
if (layout.Kind == DimensionKind.Diameter)
|
||||
{
|
||||
// Both tips on the curve, tails across the circle, the label beyond one end.
|
||||
DrawArrowAt(g, brush, arrow, center);
|
||||
DrawArrowAt(g, brush, PointWorldToGraph(layout.FarArrowPoint), center);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The tail runs along the leader toward the label, outside or inside the curve.
|
||||
var outside = layout.LineEnd.DistanceTo(layout.Center) >= layout.Radius;
|
||||
DrawArrowAt(g, brush, arrow, outside ? label : center);
|
||||
g.DrawLine(pen, center.X - CenterMarkPixels, center.Y, center.X + CenterMarkPixels, center.Y);
|
||||
g.DrawLine(pen, center.X, center.Y - CenterMarkPixels, center.X, center.Y + CenterMarkPixels);
|
||||
}
|
||||
|
||||
// The label sits past the leader's end, on the side away from the curve's center.
|
||||
var text = layout.DisplayText;
|
||||
var size = g.MeasureString(text, dimensionFont);
|
||||
var dx = System.Math.Cos(layout.Angle);
|
||||
var dy = -System.Math.Sin(layout.Angle); // Screen Y runs down.
|
||||
var reach = (float)(System.Math.Abs(dx) * size.Width / 2 + System.Math.Abs(dy) * size.Height / 2) + 2;
|
||||
var cx = label.X + (float)dx * reach;
|
||||
var cy = label.Y + (float)dy * reach;
|
||||
g.FillRectangle(labelBackBrush, cx - size.Width / 2f, cy - size.Height / 2f, size.Width, size.Height);
|
||||
g.DrawString(text, dimensionFont, brush, cx - size.Width / 2f, cy - size.Height / 2f);
|
||||
}
|
||||
|
||||
// An arrow with its tip at tip and its tail toward toward.
|
||||
private static void DrawArrowAt(Graphics g, Brush brush, PointF tip, PointF toward)
|
||||
{
|
||||
var dx = toward.X - tip.X;
|
||||
var dy = toward.Y - tip.Y;
|
||||
var length = (float)System.Math.Sqrt(dx * dx + dy * dy);
|
||||
if (length < 1)
|
||||
return;
|
||||
DrawArrow(g, brush, tip, new PointF(dx / length, dy / length));
|
||||
}
|
||||
|
||||
// Runs a little past the dimension line, the way drafted extension lines do.
|
||||
private static void DrawExtensionLine(Graphics g, Pen pen, PointF from, PointF to)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace OpenNest.Controls
|
||||
Display,
|
||||
Exclude,
|
||||
Include,
|
||||
AddRadiusDimension,
|
||||
AddDiameterDimension,
|
||||
}
|
||||
|
||||
public sealed class EntityActionEventArgs : EventArgs
|
||||
@@ -199,6 +201,18 @@ namespace OpenNest.Controls
|
||||
menu.Items.Add(include);
|
||||
}
|
||||
|
||||
var curves = selection.Where(en => !IsExcluded(en) && Dimensions.DimensionBuilder.IsRadialTarget(en)).ToList();
|
||||
if (curves.Count > 0)
|
||||
{
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
var radius = new ToolStripMenuItem("Add Radius Dimension");
|
||||
radius.Click += (s, e) => RaiseEntityAction(EntityAction.AddRadiusDimension, curves);
|
||||
menu.Items.Add(radius);
|
||||
var diameter = new ToolStripMenuItem("Add Diameter Dimension");
|
||||
diameter.Click += (s, e) => RaiseEntityAction(EntityAction.AddDiameterDimension, curves);
|
||||
menu.Items.Add(diameter);
|
||||
}
|
||||
|
||||
menu.Closed += (s, e) => BeginInvoke(new Action(menu.Dispose));
|
||||
menu.Show(this, location);
|
||||
}
|
||||
|
||||
@@ -988,10 +988,14 @@ namespace OpenNest.Forms
|
||||
private static List<Entity> DimensionedEntities(FileListItem item) =>
|
||||
TabbedCutoutBuilder.Apply(CutoutEntities(item), item.TabbedCutouts, out _);
|
||||
|
||||
// Radius and diameter find their curve by ID before tabbed cutouts split it into pieces.
|
||||
private void RefreshDimensions(FileListItem item)
|
||||
{
|
||||
var entities = item.Dimensions.Count > 0 ? DimensionedEntities(item) : new List<Entity>();
|
||||
var layouts = item.Dimensions.Select(d => DimensionBuilder.Layout(entities, d)).ToList();
|
||||
var entities = item.Dimensions.Any(d => !d.IsRadial) ? DimensionedEntities(item) : new List<Entity>();
|
||||
var curves = item.Dimensions.Any(d => d.IsRadial) ? VisibleEntities(item) : new List<Entity>();
|
||||
var layouts = item.Dimensions
|
||||
.Select(d => DimensionBuilder.Layout(d.IsRadial ? curves : entities, d))
|
||||
.ToList();
|
||||
entityView1.DimensionLayouts = layouts;
|
||||
filterPanel.LoadDimensions(layouts
|
||||
.Select(l => DimensionLabel(l))
|
||||
@@ -1000,7 +1004,13 @@ namespace OpenNest.Forms
|
||||
|
||||
private static string DimensionLabel(DimensionLayout layout)
|
||||
{
|
||||
var name = layout.Kind == DimensionKind.OverallHorizontal ? "Horizontal" : "Vertical";
|
||||
var name = layout.Kind switch
|
||||
{
|
||||
DimensionKind.OverallHorizontal => "Horizontal",
|
||||
DimensionKind.OverallVertical => "Vertical",
|
||||
DimensionKind.Radius => "Radius",
|
||||
_ => "Diameter",
|
||||
};
|
||||
return layout.IsValid ? $"{name}: {layout.ValueText}" : $"{name} (not shown): {layout.Error}";
|
||||
}
|
||||
|
||||
@@ -1022,6 +1032,25 @@ namespace OpenNest.Forms
|
||||
entityView1.ZoomToFit();
|
||||
}
|
||||
|
||||
private void AddRadialDimensions(FileListItem item, IEnumerable<Entity> curves, DimensionKind kind)
|
||||
{
|
||||
var visible = VisibleEntities(item).Select(en => en.Id).ToHashSet();
|
||||
var cut = curves.Where(c => visible.Contains(c.Id)).ToList();
|
||||
if (cut.Count == 0)
|
||||
{
|
||||
ShowDimensionMessage("Only a circle or arc that is cut or etched can be dimensioned. Right-click it and choose Cut or Etch first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var added = DimensionBuilder.MissingRadial(cut, kind, DimensionedEntities(item), item.Dimensions);
|
||||
if (added.Count == 0)
|
||||
return;
|
||||
item.Dimensions.AddRange(added);
|
||||
entityView1.SelectedDimensionIndex = item.Dimensions.Count - 1;
|
||||
RefreshDimensions(item);
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
private void OnDimensionRemoved(object sender, int index)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
@@ -1047,6 +1076,8 @@ namespace OpenNest.Forms
|
||||
return;
|
||||
|
||||
item.Dimensions[e.Dimension].Offset = e.Offset;
|
||||
if (e.Angle is { } angle)
|
||||
item.Dimensions[e.Dimension].Angle = angle;
|
||||
entityView1.SelectedDimensionIndex = e.Dimension;
|
||||
RefreshDimensions(item);
|
||||
entityView1.Invalidate();
|
||||
@@ -1522,6 +1553,12 @@ namespace OpenNest.Forms
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case EntityAction.AddRadiusDimension:
|
||||
case EntityAction.AddDiameterDimension:
|
||||
AddRadialDimensions(item, e.Entities,
|
||||
e.Action == EntityAction.AddRadiusDimension ? DimensionKind.Radius : DimensionKind.Diameter);
|
||||
return;
|
||||
|
||||
case EntityAction.Exclude:
|
||||
foreach (var entity in e.Entities)
|
||||
item.ExcludedEntityIds.Add(entity.Id);
|
||||
|
||||
+42
-17
@@ -1,28 +1,53 @@
|
||||
# Dimensions
|
||||
|
||||
A drawing can carry overall dimensions: the part's size along X and along Y, shown in the
|
||||
CAD Converter. In the **Dimensions** panel, **Add Overall Dimensions** adds whichever of the
|
||||
two is missing, below and to the right of the part. Drag a dimension line to move it, across
|
||||
the part to its other side if wanted; **Remove** deletes the selected one.
|
||||
A drawing can carry dimensions, shown in the CAD Converter:
|
||||
|
||||
- Overall dimensions: the part's size along X and along Y. In the **Dimensions** panel,
|
||||
**Add Overall Dimensions** adds whichever of the two is missing, below and to the right of
|
||||
the part. Drag a dimension line to move it, across the part to its other side if wanted.
|
||||
- Radius and diameter dimensions on circles and arcs. Select one or more circles or arcs,
|
||||
right-click and choose **Add Radius Dimension** or **Add Diameter Dimension**. Each curve
|
||||
gets at most one of each. Drag the leader to swing the label around the curve and move it
|
||||
in or out; a radius on an arc stays on the arc's sweep.
|
||||
|
||||
**Remove** in the Dimensions panel deletes the selected dimension.
|
||||
|
||||
## Contract
|
||||
|
||||
Dimensions are annotations. They never affect the program, nesting, cut time or posts, and
|
||||
they do not appear on plates, in PDFs or in DXF export.
|
||||
|
||||
- `Drawing.Dimensions` stores only a `DrawingDimension`'s kind and signed offset from the
|
||||
part's extents, so a value cannot go stale. Positive is above (horizontal) or right
|
||||
(vertical); zero is not a valid offset.
|
||||
- `DimensionBuilder` (`OpenNest.Core/Dimensions`) measures the entities as they will be cut:
|
||||
visible, with roles, breakoffs and tabbed cutouts applied, as the Program tab builds them.
|
||||
`FileListItem.Bounds` is not used because it includes hidden layers.
|
||||
- Marks do not count: anything `ScribeLayerRepair.IsMarkLayer` matches (scribe, etch and
|
||||
engrave, by layer name) and anything `SpecialLayers.IsMaterial` rejects (rapids, break
|
||||
cuts). `IsMaterial` compares layer objects, so it alone would count source marks on their
|
||||
own layers. Tabs and a breakoff's scrap count: both are material of the part as cut.
|
||||
- A dimension with nothing to measure, or a part with no size along its axis, is listed as
|
||||
not shown and keeps its placement until the geometry returns.
|
||||
- Values show three decimals with no unit, in the drawing's units.
|
||||
- `Drawing.Dimensions` stores only placement, so a value cannot go stale:
|
||||
- Overall: the kind and the signed offset from the part's extents. Positive is above
|
||||
(horizontal) or right (vertical).
|
||||
- Radius and diameter: the kind, the measured curve's entity ID, the leader angle from the
|
||||
curve's center (radians) and the signed offset from the curve to the label along the
|
||||
leader, positive outside.
|
||||
- Zero is never a valid offset.
|
||||
- `DimensionBuilder` (`OpenNest.Core/Dimensions`) measures overall dimensions from the
|
||||
entities as they will be cut: visible, with roles, breakoffs and tabbed cutouts applied, as
|
||||
the Program tab builds them. `FileListItem.Bounds` is not used because it includes hidden
|
||||
layers.
|
||||
- Marks do not count toward overall dimensions: anything `ScribeLayerRepair.IsMarkLayer`
|
||||
matches (scribe, etch and engrave, by layer name) and anything `SpecialLayers.IsMaterial`
|
||||
rejects (rapids, break cuts). `IsMaterial` compares layer objects, so it alone would count
|
||||
source marks on their own layers. Tabs and a breakoff's scrap count: both are material of
|
||||
the part as cut.
|
||||
- Radius and diameter find their curve by ID among the visible entities with roles applied,
|
||||
before breakoffs and tabbed cutouts, which split a curve into new pieces. A cut or etched
|
||||
curve can be dimensioned; an excluded, hidden or display-only one is not shown. Adding one
|
||||
to a display-only curve explains that it must be set to Cut or Etch first. An arc that is
|
||||
a full circle is measured as a circle.
|
||||
- New radius and diameter labels sit outside the curve by half its radius, or a twentieth of
|
||||
the part's larger size when that is more. On a circle the radius leader points up and right
|
||||
and the diameter leader up and left, so the two do not overlap; on an arc both point
|
||||
through the middle of its sweep. Dragging keeps the label a few pixels clear of both the
|
||||
curve and the center; a curve too small for that keeps the label outside.
|
||||
- A dimension with nothing to measure, a part with no size along its axis, or a curve that
|
||||
is gone or no longer cut is listed as not shown and keeps its placement until the geometry
|
||||
returns.
|
||||
- Values show three decimals with no unit, in the drawing's units; radius labels are drawn
|
||||
with an `R` prefix and diameter labels with `Ø`.
|
||||
- Split pieces start with no dimensions, like breakoffs and tabbed cutouts.
|
||||
|
||||
File format: see [drawing dimensions](nest-file-format.md#drawing-dimensions).
|
||||
@@ -398,11 +398,18 @@ versions without tabbed cutout support read them as ordinary cut motions.
|
||||
## Drawing dimensions
|
||||
|
||||
A drawing may carry a `dimensions` array, omitted when empty. Each entry has `kind`
|
||||
(`OverallHorizontal` or `OverallVertical`) and `offset`, the signed distance from the part's
|
||||
extents to the dimension line (positive is above or right); see [dimensions](dimensions.md).
|
||||
Values are not stored: they are measured when shown. An entry with an unknown kind or an
|
||||
offset that is zero or not finite is dropped with a reader warning; the drawing and its
|
||||
program are unaffected.
|
||||
(`OverallHorizontal`, `OverallVertical`, `Radius` or `Diameter`) and `offset`. For the
|
||||
overall kinds, `offset` is the signed distance from the part's extents to the dimension line
|
||||
(positive is above or right). `Radius` and `Diameter` also have `entityId`, the source entity
|
||||
ID of the measured circle or arc, and `angle`, the leader's direction from its center in
|
||||
radians; their `offset` is the signed distance from the curve to the label along the leader
|
||||
(positive is outside). Overall entries omit `entityId` and `angle`. See
|
||||
[dimensions](dimensions.md).
|
||||
|
||||
Values are not stored: they are measured when shown. An entry with an unknown kind, an
|
||||
offset that is zero or not finite, or a radius or diameter without a non-empty `entityId` and
|
||||
a finite `angle` is dropped with a reader warning; the drawing and its program are
|
||||
unaffected. Versions without radius and diameter support drop those entries the same way.
|
||||
|
||||
## Drawing entity roles
|
||||
|
||||
|
||||
Reference in new issue
Block a user