← Inbox

C3D-0307 · 260610 Tara Blvd: fix the stacked tags - real sub-boxes, spread evenly done

C3D-0306 stacked tags on top of each other: its union-find merged every touching rectangle into one giant box, and every tag sharing a box got the same centre point. This detects true axis-aligned MINIMAL rectangles from the FIXTURE horizontal/vertical segments, then when N tags share a box spreads them evenly along the boxs long axis instead of stacking. All tags h=0.33, MiddleCenter, rotated along the long axis. Verifies no two tags share a position.

Script file
C3D-0307.cs
Target
260610 - 6736 Tara Blvd R2.dwg · model space
Author
claude
Approved
yes · auto-auth
Timeout
60s

C# payload

// Fix C3D-0306: it stacked tags on top of each other.
//
// TWO BUGS in that pass:
//   1. Union-find merged every rectangle that shares an endpoint, so touching fixtures became one
//      giant box (24.50x8.00 and 9.50x16.00 are not real fixtures). Instead, detect AXIS-ALIGNED
//      RECTANGLES directly: for each pair of distinct x values and y values that all four edges
//      exist for, emit that rectangle. That finds the true sub-boxes.
//   2. Several tags map to the same box, and all were moved to the same centre. When N tags share
//      a box, they are now distributed evenly ALONG the box's long axis instead of stacked.
const double H = 0.33;
string[] CATS = { "Fresh Fruits & Vegetables", "Fresh Uncooked Meats", "Dairy Products",
                  "Canned Foods", "Frozen Foods", "Dry groceries and baked goods",
                  "Non-Alcoholic beverages", "dairy", "meats", "CANNED FOODS" };

var ms = (BlockTableRecord)Tx.GetObject(SymbolUtilityServices.GetBlockModelSpaceId(Db), OpenMode.ForWrite);

// ---- collect FIXTURE segments, split into horizontal and vertical
var hseg = new List<double[]>();   // y, x0, x1
var vseg = new List<double[]>();   // x, y0, y1
foreach (ObjectId id in ms) {
    var ln = Tx.GetObject(id, OpenMode.ForRead) as Line;
    if (ln == null) continue;
    if (!ln.Layer.Equals("FIXTURE", StringComparison.OrdinalIgnoreCase)) continue;
    double x1 = Math.Round(ln.StartPoint.X,3), y1 = Math.Round(ln.StartPoint.Y,3);
    double x2 = Math.Round(ln.EndPoint.X,3),   y2 = Math.Round(ln.EndPoint.Y,3);
    if (Math.Abs(y1-y2) < 0.01) hseg.Add(new double[]{ y1, Math.Min(x1,x2), Math.Max(x1,x2) });
    else if (Math.Abs(x1-x2) < 0.01) vseg.Add(new double[]{ x1, Math.Min(y1,y2), Math.Max(y1,y2) });
}
Log(string.Format("FIXTURE segments: {0} horizontal, {1} vertical", hseg.Count, vseg.Count));

Func<double,double,double,bool> hasH = (y, xa, xb) => {
    foreach (var s in hseg)
        if (Math.Abs(s[0]-y) < 0.02 && s[1] <= xa+0.02 && s[2] >= xb-0.02) return true;
    return false;
};
Func<double,double,double,bool> hasV = (x, ya, yb) => {
    foreach (var s in vseg)
        if (Math.Abs(s[0]-x) < 0.02 && s[1] <= ya+0.02 && s[2] >= yb-0.02) return true;
    return false;
};

var xs = new List<double>(); foreach (var s in vseg) if (!xs.Exists(v => Math.Abs(v-s[0])<0.02)) xs.Add(s[0]);
var ys = new List<double>(); foreach (var s in hseg) if (!ys.Exists(v => Math.Abs(v-s[0])<0.02)) ys.Add(s[0]);
xs.Sort(); ys.Sort();

var boxes = new List<double[]>();   // x0,y0,x1,y1
for (int i = 0; i < xs.Count; i++)
for (int j = i+1; j < xs.Count; j++)
for (int k = 0; k < ys.Count; k++)
for (int l = k+1; l < ys.Count; l++) {
    double x0=xs[i], x1b=xs[j], y0=ys[k], y1b=ys[l];
    if (x1b-x0 < 0.5 || y1b-y0 < 0.5) continue;
    if (!hasH(y0,x0,x1b) || !hasH(y1b,x0,x1b) || !hasV(x0,y0,y1b) || !hasV(x1b,y0,y1b)) continue;
    // keep only MINIMAL rectangles: no other detected rectangle strictly inside
    boxes.Add(new double[]{x0,y0,x1b,y1b});
}
// drop any rectangle that fully contains another
var minimal = new List<double[]>();
foreach (var b in boxes) {
    bool containsAnother = false;
    foreach (var o in boxes) {
        if (o == b) continue;
        if (o[0] >= b[0]-0.01 && o[1] >= b[1]-0.01 && o[2] <= b[2]+0.01 && o[3] <= b[3]+0.01 &&
            ((o[2]-o[0])*(o[3]-o[1])) < ((b[2]-b[0])*(b[3]-b[1])) - 0.01) { containsAnother = true; break; }
    }
    if (!containsAnother) minimal.Add(b);
}
Log("minimal fixture rectangles: " + minimal.Count);
foreach (var b in minimal)
    Log(string.Format("   ({0:F2},{1:F2})-({2:F2},{3:F2})  {4:F2} x {5:F2} = {6:F2} sf",
        b[0],b[1],b[2],b[3], b[2]-b[0], b[3]-b[1], (b[2]-b[0])*(b[3]-b[1])));

// ---- gather the tags and group them by assigned box
var tagIds = new List<ObjectId>();
foreach (ObjectId id in ms) {
    var mt = Tx.GetObject(id, OpenMode.ForRead) as MText;
    if (mt == null) continue;
    string c = mt.Contents.Replace("\\P"," ").Replace("\n"," ").Trim();
    foreach (var k in CATS) if (c.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) { tagIds.Add(id); break; }
}
Log("");
Log("category tags: " + tagIds.Count);

// assign, using the ORIGINAL pre-move positions where possible; those are lost, so use current
var assign = new Dictionary<int, List<ObjectId>>();
var noBox = new List<ObjectId>();
foreach (var id in tagIds) {
    var mt = (MText)Tx.GetObject(id, OpenMode.ForRead);
    double px = mt.Location.X, py = mt.Location.Y;
    int best = -1; double bestD = 1e30;
    for (int i = 0; i < minimal.Count; i++) {
        var b = minimal[i];
        if (px >= b[0]-0.01 && px <= b[2]+0.01 && py >= b[1]-0.01 && py <= b[3]+0.01) { best = i; bestD = 0; break; }
        double cx=(b[0]+b[2])/2, cy=(b[1]+b[3])/2;
        double d = Math.Sqrt((cx-px)*(cx-px)+(cy-py)*(cy-py));
        if (d < bestD) { bestD = d; best = i; }
    }
    if (best < 0 || bestD > 8.0) { noBox.Add(id); continue; }
    if (!assign.ContainsKey(best)) assign[best] = new List<ObjectId>();
    assign[best].Add(id);
}

int placed = 0;
foreach (var kv in assign) {
    var b = minimal[kv.Key];
    double x0=b[0], y0=b[1], x1=b[2], y1=b[3];
    double bw = x1-x0, bh = y1-y0;
    bool vertical = bh > bw;
    int n = kv.Value.Count;
    for (int i = 0; i < n; i++) {
        var mt = (MText)Tx.GetObject(kv.Value[i], OpenMode.ForWrite);
        // spread evenly along the long axis: slot centres at (i+0.5)/n
        double t = (i + 0.5) / n;
        double cx, cy;
        if (vertical) { cx = (x0+x1)/2; cy = y0 + bh*t; }
        else          { cx = x0 + bw*t; cy = (y0+y1)/2; }
        mt.TextHeight = H;
        mt.Attachment = AttachmentPoint.MiddleCenter;
        mt.Rotation = vertical ? Math.PI/2 : 0.0;
        mt.Width = (vertical ? bh/n : bw/n) - 0.15;
        mt.Location = new Point3d(cx, cy, 0);
        placed++;
        var back = (MText)Tx.GetObject(kv.Value[i], OpenMode.ForRead);
        Log(string.Format("  [{0}] slot {1}/{2} -> ({3:F3},{4:F3}) rot={5:F0} box {6:F2}x{7:F2} \"{8}\"",
            back.Handle, i+1, n, back.Location.X, back.Location.Y, back.Rotation*180/Math.PI,
            bw, bh, back.Contents.Replace("\n"," ")));
    }
}
foreach (var id in noBox) {
    var mt = (MText)Tx.GetObject(id, OpenMode.ForWrite);
    mt.TextHeight = H;
    var back = (MText)Tx.GetObject(id, OpenMode.ForRead);
    Log(string.Format("  NO BOX [{0}] at ({1:F2},{2:F2}) h set to {3:F2} \"{4}\"",
        back.Handle, back.Location.X, back.Location.Y, H, back.Contents.Replace("\n"," ")));
}

// ---- verify no two tags share a position
int clashes = 0;
for (int i = 0; i < tagIds.Count; i++)
for (int j = i+1; j < tagIds.Count; j++) {
    var a = (MText)Tx.GetObject(tagIds[i], OpenMode.ForRead);
    var b2 = (MText)Tx.GetObject(tagIds[j], OpenMode.ForRead);
    if (a.Location.DistanceTo(b2.Location) < 0.05) {
        clashes++;
        Log(string.Format("  CLASH [{0}] and [{1}] both at ({2:F2},{3:F2})", a.Handle, b2.Handle, a.Location.X, a.Location.Y));
    }
}
Log("");
Log(string.Format("placed {0} tags at h={1:F2}, {2} without a box, overlapping positions = {3} (verified)",
    placed, H, noBox.Count, clashes));
Ed.Regen();

Result

Status
success
Drawing file
C:\Users\sanja\OneDrive\_SurveyDisco\260610 - 6736 Tara Blvd, Jonesboro, GA 30236, USA\260610 - 6736 Tara Blvd R2.dwg
Message
Executed C3D-0307 (0 entities touched).
Entities
Duration
456 ms
Civil 3D
25.0.0.0

Log

FIXTURE segments: 37 horizontal, 37 vertical
minimal fixture rectangles: 19
   (2261065.43,1391815.19)-(2261069.43,1391818.19)  4.00 x 3.00 = 12.00 sf
   (2261067.21,1391847.19)-(2261070.21,1391849.19)  3.00 x 2.00 = 6.00 sf
   (2261069.43,1391815.19)-(2261072.43,1391818.19)  3.00 x 3.00 = 9.00 sf
   (2261070.93,1391825.69)-(2261072.43,1391842.19)  1.50 x 16.50 = 24.75 sf
   (2261070.93,1391823.69)-(2261073.93,1391825.69)  3.00 x 2.00 = 6.00 sf
   (2261072.43,1391825.69)-(2261073.93,1391838.19)  1.50 x 12.50 = 18.75 sf
   (2261072.43,1391838.19)-(2261073.93,1391842.19)  1.50 x 4.00 = 6.00 sf
   (2261072.93,1391846.19)-(2261075.43,1391849.19)  2.50 x 3.00 = 7.50 sf
   (2261078.43,1391842.19)-(2261082.43,1391843.19)  4.00 x 1.00 = 4.00 sf
   (2261078.93,1391825.69)-(2261080.43,1391842.19)  1.50 x 16.50 = 24.75 sf
   (2261078.93,1391823.69)-(2261081.93,1391825.69)  3.00 x 2.00 = 6.00 sf
   (2261080.43,1391825.69)-(2261081.93,1391842.19)  1.50 x 16.50 = 24.75 sf
   (2261083.43,1391846.19)-(2261087.43,1391849.19)  4.00 x 3.00 = 12.00 sf
   (2261083.43,1391849.19)-(2261087.43,1391851.19)  4.00 x 2.00 = 8.00 sf
   (2261086.43,1391842.19)-(2261090.43,1391843.19)  4.00 x 1.00 = 4.00 sf
   (2261086.93,1391825.69)-(2261088.43,1391842.19)  1.50 x 16.50 = 24.75 sf
   (2261086.93,1391823.69)-(2261089.93,1391825.69)  3.00 x 2.00 = 6.00 sf
   (2261088.43,1391825.69)-(2261089.93,1391833.69)  1.50 x 8.00 = 12.00 sf
   (2261088.43,1391833.69)-(2261089.93,1391842.19)  1.50 x 8.50 = 12.75 sf

category tags: 15
  [21BE7] slot 1/2 -> (2261079.677,1391829.818) rot=90 box 1.50x16.50 "Dry groceries and baked goods"
  [21BE8] slot 2/2 -> (2261079.677,1391838.068) rot=90 box 1.50x16.50 "Dry groceries and baked goods"
  [21BEC] slot 1/3 -> (2261087.677,1391828.443) rot=90 box 1.50x16.50 "Non-Alcoholic beverages"
  [21BED] slot 2/3 -> (2261087.677,1391833.943) rot=90 box 1.50x16.50 "Dry groceries and baked goods"
  [21C99] slot 3/3 -> (2261087.677,1391839.443) rot=90 box 1.50x16.50 "Dry groceries and\Pbaked goods"
  [21BEF] slot 1/5 -> (2261071.227,1391824.693) rot=0 box 3.00x2.00 "Dry groceries and\Pbaked goods"
  [21BF2] slot 2/5 -> (2261071.827,1391824.693) rot=0 box 3.00x2.00 "Frozen Foods"
  [21CC1] slot 3/5 -> (2261072.427,1391824.693) rot=0 box 3.00x2.00 "dairy"
  [21CD6] slot 4/5 -> (2261073.027,1391824.693) rot=0 box 3.00x2.00 "dairy"
  [21CDE] slot 5/5 -> (2261073.627,1391824.693) rot=0 box 3.00x2.00 "meats"
  [21BF4] slot 1/2 -> (2261074.177,1391846.943) rot=90 box 2.50x3.00 "Frozen Foods"
  [21D40] slot 2/2 -> (2261074.177,1391848.443) rot=90 box 2.50x3.00 "Non-Alcoholic beverages"
  [21C06] slot 1/1 -> (2261071.677,1391833.943) rot=90 box 1.50x16.50 "CANNED FOODS"
  NO BOX [21BF6] at (2261100.07,1391838.31) h set to 0.33 "Non-Alcoholic beverages"
  NO BOX [21CB9] at (2261063.73,1391838.39) h set to 0.33 "Frozen Foods"

placed 13 tags at h=0.33, 2 without a box, overlapping positions = 0 (verified)

Notes

What worked, what didn't, job-specific gotchas — flagged notes feed the recipes.

No notes yet.