How to Retrieve Per-Scale Dimension Value Position

We have received following query from an A D N partner.

I’m trying to use Object ARX to obtain information about annotative scale objects in a drawing.
I know how to obtain the dimension value size of annotative dimensions, but I can’t obtain the coordinates of the dimension value position if it changes depending on the scale.
I know that AutoCAD allows you to change the position for each annotation scale and to reset the position with ANNORESET.
Please tell me how to obtain this position information (at least coordinate information, which exists for each scale).

The dimension with a value of 2490 in the attached drawing is an example.
There are multiple scales (1/5 and 1/30), and you can see that the position of the dimension value differs for each scale.

This is a great question that hits a key complexity of annotative objects in AutoCAD: per-scale data storage.

Technical drawing showing dimensions in different scales (1:5 and 1:30) with a highlighted dimension value of 2490.

The coordinate information you’re looking for (the position of the dimension value or text text location) when it is overridden for a specific annotation scale is not stored directly on the main AcDbDimension entity. This data is stored within a specialized companion object associated with that scale, typically found in the dimension’s Extension Dictionary.

The mechanism involves:

  1. Extension Dictionary: The dimension entity owns a dictionary (AcDbDictionary).
  2. Context Data: This dictionary contains AcDbObjectContextData objects (or derived classes) for each scale override.
  3. Property Storage: The scale-specific override data, including the text position, is stored within the derived Context Data object, this can retireved from get DXF data of the object, typically using acdbEntGetX

Here’s how to obtain the overridden text position for a specific scale using ObjectARX:

You need to perform a three-step lookup to find the overridden coordinates.

Step 1: Get the Current Annotation Scale

You must first identify the AcDbAnnotationScale object that represents the view you are inspecting (e.g., the 1:5 or 1:30 scale).
This is same as CANNONSCALE


// 1. Get the current database
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();

 AcDbAnnotationScale* curAnnoScale = pDb->cannoscale();
 if (curAnnoScale != nullptr) {
     AcString scaleName;
     curAnnoScale->getName(scaleName);
     acutPrintf(_T("\nCurrent annotation scale is %s."), scaleName.c_str());
 }

Step 2: Traverse the Dimension’s Extension Dictionary

You need to look inside the dimension entity’s extension dictionary for an entry matching the targetScaleId.

  1. Open the dimension’s extension dictionary.
  2. Search the dictionary using the Handle of the target scale as the key.

Since the dimension’s extension dictionary keys are strings (the scale name, in this case), the lookup is often done on the handle of the scale object.


AcDbDimension* pDim = ...; // Your dimension entity

// 1. Get the Extension Dictionary ID
AcDbObjectId dictId = pDim->extensionDictionary();
if (dictId.isNull()) return;

AcDbDictionaryPointer pExtDict(dictId, AcDb::kForRead);
if (pExtDict.openStatus() != Acad::eOk) return;


Step 3: Extract the Position from the Context Data

The overridden data is stored in the AcDbObjectContextData object found in the dictionary. You need to open this object and retrieve the coordinates.

The coordinates are stored as a 3D point (usually DXF group code 10 or similar for location overrides). Since the Context Data object is an AcDbObject, you can use your existing DXF extraction logic (getDxfDataFromId) on it.


// Assuming contextDataId is the ObjectId of the AcDbObjectContextData object
// that matches the target annotation scale.

// Call the function to get all DXF data:
DxfDataContainer dxfData = getDxfDataFromId(contextDataId);

AcGePoint3d position = AcGePoint3d::kOrigin;

// Iterate through the results to find the specific coordinate code (e.g., DXF 10)
for (const auto& entry : dxfData) {
    // Dimension text position is often stored in DXF group 10 (or a high-range code 1010+) 
    // when stored as XDATA/ContextData. 
    // DXF code 10 typically holds the text location.
    
    if (entry.code == 10) { 
        // NOTE: You would need to refine your resbufToDxfEntry to correctly parse
        // the coordinate values back into an AcGePoint3d, not just a string.
        // For now, we rely on the string, but a dedicated parser is better.
        
        acutPrintf(_T("\nOverridden Text Position Found: %s"), entry.value.c_str());
        break;
    }
}


Because Context Data objects are internal, their specific DXF code layout (whether they use group 10, or a private XRECORD/XDATA code) can vary. You must inspect the DXF dump of the object to find the exact code used for the dimension text position override. It will typically be a standard point code (10-17) or a custom XDATA point code (1010-1017).

Here is the Full Code.


const AcString CONTEXT_DATA = _T("ContextData");
std::vector g_ContextDataIds;

// Data Structures
struct DxfEntry {
    int code;            // DXF Group Code (e.g., 10, 330)
    std::wstring value;  // String representation of the value (e.g., "(1.0, 2.0)", "MyLayer")
    int dataType;        // The ADS data type (e.g., RTREAL, RT3DPOINT)
};
using DxfDataContainer = std::vector;

// Dictionary Node Structure
struct Node {
    AcString name;
    AcDbObjectId id;
    std::vector children;
};

int dxfCodeToDataType(int resType) {
    if ((resType >= 0) && (resType <= 9))
        return RTSTR;
    else if ((resType >= 10) && (resType <= 17))
        return RT3DPOINT;
    else if ((resType >= 38) && (resType <= 59))
        return RTREAL;
    else if ((resType >= 60) && (resType <= 79))
        return RTSHORT;
    else if ((resType >= 90) && (resType <= 99))
        return RTLONG;
    else if ((resType == 100) || (resType == 101) || (resType == 102) || (resType == 105))
        return RTSTR;
    else if ((resType >= 110) && (resType <= 119))
        return RT3DPOINT;
    else if ((resType >= 140) && (resType <= 149))
        return RTREAL;
    else if ((resType >= 170) && (resType <= 179))
        return RTSHORT;
    else if ((resType >= 210) && (resType <= 219))
        return RT3DPOINT;
    else if ((resType >= 270) && (resType <= 299))
        return RTSHORT;
    else if ((resType >= 300) && (resType <= 309))
        return RTSTR;
    else if ((resType >= 310) && (resType <= 369))
        return RTENAME;
    else if ((resType >= 370) && (resType <= 379))
        return RTSHORT;
    else if ((resType >= 380) && (resType <= 389))
        return RTSHORT;
    else if ((resType >= 390) && (resType <= 399))
        return RTENAME;
    else if ((resType >= 400) && (resType <= 409))
        return RTSHORT;
    else if ((resType >= 410) && (resType <= 419))
        return RTSTR;
    else if (resType == 1004)
        return resType; // binary chunk
    else if ((resType >= 999) && (resType <= 1009))
        return RTSTR;
    else if ((resType >= 1010) && (resType <= 1013))
        return RT3DPOINT;
    else if ((resType >= 1038) && (resType <= 1059))
        return RTREAL;
    else if ((resType >= 1060) && (resType <= 1070))
        return RTSHORT;
    else if ((resType == 1071))
        return RTLONG;
    else if ((resType < 0) || (resType > 4999))
        return resType;
    else
        return RTNONE;
}



void resbufToDxfEntry(const resbuf* rb, DxfEntry& entry) {


    if (!rb) {
        entry.code = 0;
        entry.dataType = RTNONE;
        entry.value = L"";
        return;
    }
    entry.code = rb->restype;
    entry.dataType = dxfCodeToDataType(rb->restype);

    AcString valueStr; 

    switch (entry.dataType) {
    case RTSHORT:
        valueStr.appendFormat(_T("%d"), rb->resval.rint);
        break;
    case RTLONG:
        valueStr.appendFormat(_T("%lld"), static_cast(rb->resval.rlong));
        break;
    case RTREAL:
        valueStr.appendFormat(_T("%.16f"), rb->resval.rreal);
        break;
    case RTSTR:
        if (rb->resval.rstring == nullptr)
            valueStr = _T("(NULL)");
        else
            valueStr.appendFormat(_T("\"%s\""), rb->resval.rstring);
        break;
    case RT3DPOINT:
        valueStr.appendFormat(_T("(%.16f, %.16f, %.16f)"), rb->resval.rpoint[X], rb->resval.rpoint[Y], rb->resval.rpoint[Z]);
        break;
    case RTPOINT:
        valueStr.appendFormat(_T("(%.16f, %.16f, %.16f)"), rb->resval.rpoint[X], rb->resval.rpoint[Y]);
        break;
    case 1004: { // Binary chunk
        int len = rb->resval.rbinary.clen;
        const char* data = rb->resval.rbinary.buf;
        AcString hexStr;
        for (int j = 0; j < len; j++) {
            unsigned char k = static_cast(data[j]);
            hexStr.appendFormat(_T("%02hX"), k);
        }
        valueStr.appendFormat(_T("Binary Chunk: \"%s\""), hexStr.kTCharPtr());
        break;
    }
    case -6:
        valueStr = _T("Extension Dictionary");
        break;
    case -5:
        valueStr = _T("Persistent Reactors");
        break;
    case -4:
        if (rb->resval.rstring)
            valueStr.appendFormat(_T("Conditional Operator: \"%s\""), rb->resval.rstring);
        else
            valueStr = _T("Conditional Operator: (NULL)");
        break;
    case -3:
        valueStr = _T("Start of Xdata");
        break;
    case -2:
        valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        break;
        break;
    case -1:
    case RTENAME: {
        if ((rb->restype >= 330) && (rb->restype < 340))
            valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        else if ((rb->restype >= 340) && (rb->restype < 350))
            valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        else if ((rb->restype >= 350) && (rb->restype < 360))
            valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        else if ((rb->restype >= 360) && (rb->restype < 370))
            valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        else if ((rb->restype >= 390) && (rb->restype < 399))
            valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        else
            valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        break;
    }
    case RTPICKS:
        valueStr.appendFormat(_T(""), static_cast(rb->resval.rlname[0]));
        break;
    case RTLB:
        valueStr = _T("List Begin");
        break;
    case RTLE:
        valueStr = _T("List End");
        break;
    case RTNIL:
        valueStr = _T("NIL");
        break;
    case RTT:
        valueStr = _T("T");
        break;
    default:
        valueStr = _T("*Unknown*");
        break;
    }

    // Store into std::wstring
    entry.value.assign(valueStr.kTCharPtr());
}

DxfDataContainer getDxfDataFromId(const AcDbObjectId id) {
    DxfDataContainer dataList;
    ads_name ent;

    if (acdbGetAdsName(ent, id) != Acad::eOk) {
        return dataList;
    }

    resbuf* apps = acutNewRb(RTSTR);
    acutNewString(_T("*"), apps->resval.rstring);

    resbuf* entdata = acdbEntGetX(ent, apps);
    acutRelRb(apps);

    resbuf* tmp = entdata;
    while (tmp) {
        DxfEntry entry;
        resbufToDxfEntry(tmp, entry);
        dataList.push_back(std::move(entry));
        tmp = tmp->rbnext;
    }

    acutRelRb(entdata);

    return dataList;
}

void printNodeRecursive(const Node& node, int depth = 0) {
    AcString indent;
    for (int i = 0; i < depth; ++i) {
        indent += _T("  ");
    }

    TCHAR tmpStr[256];

    if(node.id.handle().getIntoAsciiBuffer(tmpStr)) {
        // Successfully converted handle to string
    } else {
        _tcscpy_s(tmpStr, _T("Invalid Handle"));
	}

    // Using AcString's C-string access for acutPrintf
    acutPrintf(_T("\n%s- Name: %s | Handle: %s\n"),
        indent.c_str(),
        node.name.c_str(),
        tmpStr
    );

    for (const auto& child : node.children) {
        printNodeRecursive(child, depth + 1);
    }
}

void walkExtensionDictionary(const AcDbObjectId& dictId, Node& parentNode, AcRxClass* classType = nullptr) {
    AcDbDictionaryPointer dict(dictId, AcDb::kForRead);
    if (dict.openStatus() != Acad::eOk) {
        return;
    }

    AcDbDictionaryIterator* dictIter = dict->newIterator();
    if (dictIter == nullptr) {
        return;
    }

    for (; !dictIter->done(); dictIter->next()) {
        const AcString entryName = dictIter->name();
        const AcDbObjectId entryId = dictIter->objectId();

        if (entryId.objectClass() != AcDbDictionary::desc()) {

            AcString contxtClass = entryId.objectClass()->name();

            // Check for ContextData type (The special handling for non-dictionary items)
            if (contxtClass.find(CONTEXT_DATA) != -1) {

                // Add the ContextData item to the tree structure
                parentNode.children.emplace_back();
				Node& contextNode = parentNode.children.back();
				contextNode.name = entryName;
				contextNode.id = entryId;

                acutPrintf(_T("\n-- Context Data Found: %s --"), entryName.c_str());
				g_ContextDataIds.push_back(entryId);               
            }
            continue; // Skip non-dictionary items that are not recursible
        }

        bool shouldRecurse = true;

        // Class Type Filtering Logic
        if (classType != nullptr) {
            AcDbObject* pEntryObj = nullptr;
            shouldRecurse = false;

            if (acdbOpenObject(pEntryObj, entryId, AcDb::kForRead) == Acad::eOk) {
                if (pEntryObj->isKindOf(classType)) {
                    shouldRecurse = true;
                }
                pEntryObj->close();
            }
        }

        if (shouldRecurse) {
            // Create and populate the child node
            parentNode.children.emplace_back();
            Node& currentChild = parentNode.children.back();

            currentChild.name = entryName;
            currentChild.id = entryId;

            // Recursive call
            walkExtensionDictionary(entryId, currentChild, classType);
        }
    }

    delete dictIter;
}

void run2() {
    AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
    if (pDb == nullptr) {
        return;
    }

    // --- Output Current Annotation Scale ---
    AcDbAnnotationScale* curAnnoScale = pDb->cannoscale();
    if (curAnnoScale != nullptr) {
        AcString scaleName;
        curAnnoScale->getName(scaleName);
        acutPrintf(_T("\nCurrent annotation scale is %s."), scaleName.c_str());
    }

    // --- Entity Selection ---
    AcDbEntity* pEnt = nullptr;
    ads_name ename = {};
    ads_point ePt = {};
    if (acedEntSel(_T("\nSelect an entity to inspect its dictionary: "), ename, ePt) != RTNORM) {
        acutPrintf(_T("\nSelection cancelled or error."));
        return;
    }

    // --- Open Entity ---
    AcDbObjectId entId;
    Acad::ErrorStatus es = acdbGetObjectId(entId, ename);
    es = acdbOpenObject(pEnt, entId, AcDb::kForRead);
    if (es != Acad::eOk) {
        acutPrintf(_T("\nError in opening entity."));
        return;
    }

    // --- Dictionary Walk and Print ---
    acutPrintf(_T("\n\n--- Extension Dictionary Tree ---"));
    Node rootNode;
    rootNode.name = _T("Selected Entity Dictionary");

    // Start the recursive walk
    walkExtensionDictionary(pEnt->extensionDictionary(), rootNode);

    // Print the results
    printNodeRecursive(rootNode);

	// Print Dimension Text Value Position extracted from DXF data
    for (const auto& contextId : g_ContextDataIds) {
         acutPrintf(_T("\n\n--- DXF Data for ContextData Object: %s ---"), contextId.objectClass()->name());
        DxfDataContainer contextDxfData = getDxfDataFromId(contextId);
        for (const auto& entry : contextDxfData) {
			if (entry.code == 10)        
            acutPrintf(_T("\n      Dimension Text Value Position %d: %s"), entry.code, entry.value.c_str());
			if (entry.code == 11) 
				acutPrintf(_T("\n     Dimension Line Position (End Point 1) %d: %s"), entry.code, entry.value.c_str());
            /*else
				acutPrintf(_T("\n      DXF Code %d: %s"), entry.code, entry.value.c_str());*/

        }
	}
    

    // --- Cleanup ---
    pEnt->close();
    acutPrintf(_T("\n--- End of Dictionary Tree ---\n"));
}

When you execute the run2 from custom command say ANSCALE, this is the expected output.


Command: ANSCALE
Current annotation scale is 1:30.
Select an entity to inspect its dictionary:
--- Extension Dictionary Tree ---
-- Context Data Found: *A1 --
-- Context Data Found: *A2 --
- Name: Selected Entity Dictionary | Handle: 0
  - Name: AcDbContextDataManager | Handle: 4189C
    - Name: ACDB_ANNOTATIONSCALES | Handle: 4189D
      - Name: *A1 | Handle: 4189E
      - Name: *A2 | Handle: 418AB
--- DXF Data for ContextData Object: AcDbAlignedDimensionObjectContextData ---
      Dimension Text Value Position 10: (677605.0297795322, 185202.7687388095, 0.000000000000000)
     Dimension Line Position (End Point 1) 11: (678850.0310509118, 185135.2687388095, 0.000000000000000)
--- DXF Data for ContextData Object: AcDbAlignedDimensionObjectContextData ---
      Dimension Text Value Position 10: (676580.7945011582, 186752.3620226077, 0.000000000000000)
     Dimension Line Position (End Point 1) 11: (678850.0310509118, 186741.1120226077, 0.000000000000000)
--- End of Dictionary Tree ---



Comments

Leave a Reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading