MultiLine Attributes change to SingleLine when cloned

1 2    next >
FizixMan
5/2/2022 6:44 PM

Hi,

I've run into a peculiar issue trying to create a templating system. For context, we're trying to read in an AutoCAD model with named blocks and attributes as a template, copy its blocks/attributes to a new AutoCAD file's PaperSpace, then update the attribute texts with new values.

So far everything works fine, but the PrimaryMultiLine attributes do not seem to be transferring their multi-line aspects over as expected. Curiously, if we're using CadLib's PDF plotter, it works out fine because it seems to honour the newline special characters just fine. But if we instead open the same DWG model in AutoCAD, the resulting attribute seems to be converted to a single line and all lines are combined into that single line. After opening the Block Attribute Manager and hitting "Sync" (or making a small, irrelevant change to the block definition which forces a resync) the copied lines convert from single to multiline magically.

We've seem to have narrowed down to a minimal example that demonstrates the core problem.

We start with "SourceTest.dwg" that has a block named "Test" that's multiline and has "Line 1\PLine2\PLine3" as a default value for a "TEST" attribute: https://i.imgur.com/4I456rp.png (Here is its "Edit Attribute" dialog settings: https://i.imgur.com/ooBIRt7.png)

After copying over to a new model, it looks like this: https://i.imgur.com/hfCMhhZ.png (Here is its "Edit Attribute" dialog settings, which are identical to the source DWG: https://i.imgur.com/GydlE4f.png) (Ignore the bad styling, this minimal example doesn't bother to copy over styles.)

In the Block Attribute Manager, if we hit "Sync", it then updates to be the expected multi-line state: https://i.imgur.com/FivIGve.png

The below code that reads the dwg, grabs the "Test" block, copies it over to a new DxfModel, creates the new insert and attributes for it:

C# Code:
//Our template model from which we want to copy a block and insert
var sourceModel = DwgReader.Read("SourceTest.dwg"); 

//A new model that we want to insert the template block into
var destinationModel = new DxfModel(DxfVersion.Dxf32);

//The source template block we want to copy
var sourceBlock = sourceModel.Blocks["Test"];

//Get our multiline attribute from the source block
var sourceAttributeDefinition = sourceBlock.GetAttributeDefinition("TEST");

//Report the template source attribute, all these outputs are as expected
Console.WriteLine("Source Attribute MultiLineState: "+ sourceAttributeDefinition.MultiLineState); //"PrimaryMultiLine"
Console.WriteLine("Source Attribute Mtext: " + (sourceAttributeDefinition.Mtext == null ? "null" : sourceAttributeDefinition.Mtext.Text)); //MText.Text == "Line1\PLine2\PLine3"
Console.WriteLine("Source Attribute Text: " + sourceAttributeDefinition.Text); //Text is "Test", which is the original block definition text

//Copy the source block over to the new model
var cloneContext = new CloneContext(sourceModel, destinationModel, ReferenceResolutionType.CloneMissing);
var copiedBlock = (DxfBlock)sourceBlock.Clone(cloneContext);
destinationModel.Blocks.Add(copiedBlock);
cloneContext.ResolveReferences();

//Insert the block into the model space
var destinationInsert = new DxfInsert(copiedBlock);
destinationModel.ModelLayout.Entities.Add(destinationInsert);

//Now create a new attribute definition using our source attribute's definition and text.
//I have a suspicion this is the wrong way of doing this.
var copiedAttribute = destinationInsert.AddAttribute(sourceAttributeDefinition, sourceAttributeDefinition.Mtext.Text);

//Report the copied attribute
Console.WriteLine("Cloned Attribute MultiLineState: " + copiedAttribute.MultiLineState); //"PrimarySingleLine" was not expected
Console.WriteLine("Cloned Attribute Mtext: " + (copiedAttribute.Mtext == null ? "null" : copiedAttribute.Mtext.Text)); //Mtext is null
Console.WriteLine("Cloned Attribute Text: " + copiedAttribute.Text); //Text is "Line1\PLine2\PLine3"

DwgWriter.Write("Output.dwg", destinationModel); //Check the results in AutoCAD

Note that the source attribute definition here reports its MultiLineState as "PrimaryMultiLine" which is what we expect. After copying over, the new attribute now reports it as "PrimarySingleLine" which is not what we expected. In addition to the MultiLineState changes, the underyling "Mtext" entity becomes null, and the "Text" also changes from the original "Test" default to the new value. But I suspect part of this is coming from the use of "var copiedAttribute = destinationInsert.AddAttribute(sourceAttributeDefinition, sourceAttributeDefinition.Mtext.Text);" to "copy" the attribute over. I'm guessing it's converting to a single line here, but we couldn't see any overload or different way to create the attribute as multiline with an MText.

Admittedly, we aren't sure if this is the correct way of copying over blocks/attributes to a new DxfModel, but everything we have is working on our end except this MultiLine->SingleLine issue. If there is a more appropriate method that avoids the issue entirely, let me know and I'll give it a shot.

We see this behaviour happening on CadLib 4.0 for .NET Framework 4.0.38.243 and the latest 4.0.39.39 trial version.

Attached is the "SourceTest.dwg" input DWG we're using and an example "Output.dwg" that we generated using the above code and the 4.0.39.39 trial version.

Please let me know if you have any questions or need additional information. Thanks.

Wout
5/4/2022 9:32 AM

Hi,

I've added a new attribute constructor and a new DxfInsert.AddMultilineAttribute method in the latest CadLib 4.0 version. See updated code below, note that you also needed to use the copy of sourceAttributeDefinition instead of the source object.

C# Code:
            //Our template model from which we want to copy a block and insert
            var sourceModel = DwgReader.Read("SourceTest.dwg");

            //A new model that we want to insert the template block into
            var destinationModel = new DxfModel(DxfVersion.Dxf32);

            //The source template block we want to copy
            var sourceBlock = sourceModel.Blocks["Test"];

            //Get our multiline attribute from the source block
            var sourceAttributeDefinition = sourceBlock.GetAttributeDefinition("TEST");

            //Report the template source attribute, all these outputs are as expected
            Console.WriteLine("Source Attribute MultiLineState: " + sourceAttributeDefinition.MultiLineState); //"PrimaryMultiLine"
            Console.WriteLine("Source Attribute Mtext: " + (sourceAttributeDefinition.Mtext == null ? "null" : sourceAttributeDefinition.Mtext.Text)); //MText.Text == "Line1\PLine2\PLine3"
            Console.WriteLine("Source Attribute Text: " + sourceAttributeDefinition.Text); //Text is "Test", which is the original block definition text

            //Copy the source block over to the new model
            DxfBlock copiedBlock;
            {
                var cloneContext = new CloneContext(sourceModel, destinationModel, ReferenceResolutionType.CloneMissing);
                copiedBlock = (DxfBlock)sourceBlock.Clone(cloneContext);
                destinationModel.Blocks.Add(copiedBlock);
                cloneContext.ResolveReferences();
            }

            var copiedAttDef = copiedBlock.GetAttributeDefinition(sourceAttributeDefinition.TagString);
            DxfMText copiedMText;
            {
                var cloneContext = new CloneContext(sourceModel, destinationModel, ReferenceResolutionType.CloneMissing);
                copiedMText = (DxfMText)copiedAttDef.Mtext.Clone(cloneContext);
                cloneContext.ResolveReferences();
            }

            //Insert the block into the model space
            var destinationInsert = new DxfInsert(copiedBlock);
            destinationModel.ModelLayout.Entities.Add(destinationInsert);

            //Now create a new attribute definition using our source attribute's definition and text.
            //I have a suspicion this is the wrong way of doing this.
            var copiedAttribute = destinationInsert.AddMultilineAttribute(copiedAttDef, copiedMText);

            //Report the copied attribute
            Console.WriteLine("Cloned Attribute MultiLineState: " + copiedAttribute.MultiLineState); //"PrimarySingleLine" was not expected
            Console.WriteLine("Cloned Attribute Mtext: " + (copiedAttribute.Mtext == null ? "null" : copiedAttribute.Mtext.Text)); //Mtext is null
            Console.WriteLine("Cloned Attribute Text: " + copiedAttribute.Text); //Text is "Line1\PLine2\PLine3"

            DwgWriter.Write("Output.dwg", destinationModel); //Check the results in AutoCAD

- Wout

FizixMan
5/5/2022 6:36 PM

Hi Wout,

Thanks for the quick update, all makes sense. I tried adapting the code changes, but ran into another bug with 4.0.39.44 (and 4.0.39.39) that is interfering with another aspect of our templating. One of the first steps we do is remove all PaperSpace layouts from the DWG with the intent to later add our own new ones. It's now throwing an ArgumentOutOfRangeException when we try to remove a layout:

Code:
System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
   at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
   at WW.Collections.Generic.ActiveNonUniqueKeyedCollection`2.TryGetValue(TKey key, TItem& item)
   at WW.Cad.Model.DxfModel.()
   at WW.Cad.Model.DxfModel.set_ActiveLayout(DxfLayout value)
   at WW.Cad.Model.Objects.DxfLayout.(DxfModel )
   at WW.Cad.Model.DxfModel.(Object , Int32 , DxfLayout )
   at WW.Cad.Model.ActiveKeyedDxfHandledObjectCollection`2.OnRemoved(Int32 index, TItem item)
   at WW.Cad.Model.KeyedDxfHandledObjectCollection`2..RemoveItem(Int32 )
   at System.Collections.ObjectModel.Collection`1.Remove(T item)
   at WW.Cad.Model.KeyedDxfHandledObjectCollection`2.Remove(TItem item)
   at UserQuery.Main() in c:\Users\Chris\AppData\Local\Temp\LINQPad\_hvsvcyrg\query_ufwfbr.cs:line 40

The code used to generate this:

C# Code:
var model = DwgReader.Read(@"SourceTest.dwg"); 

var existingLayouts = model.Layouts.Where(l => l.PaperSpace).ToList(); //get all the Paper Space layouts
foreach (var existingLayout in existingLayouts)
    model.Layouts.Remove(existingLayout); //remove each Paper Space layout

This is using the same "SourceTest.dwg" that I attached in the original post.

The above code works fine in version 4.0.38.243 which is the latest version I have available to test with before 4.0.39.39, so sorry I can't narrow it down more than that.

It seems that I can still remove a non-PaperSpace (Model Space) layout though if that helps figure it out.

Wout
5/6/2022 9:41 AM

Hi,

This was a recently introduced bug indeed. I've just fixed it, thank you for the report.

Could you update your profile data (e-mail/company name) by the way? I can't see which company license you are using.

- Wout

FizixMan
5/6/2022 3:44 PM

Hi,

I added the company profile information there. I'll be seeing about getting them to purchase an updated license too.

The updated 4.0.39.45 version does fix the exception, but the resulting DWG might be corrupted or malformed. AutoCAD LT 2020 is complaining that it requires recovery, and when it does it adds back a default "Layout1" sheet.

Error message: https://i.imgur.com/qs9VBoG.png

Recovery message: https://i.imgur.com/rLEKHAD.png

Resulting unexpected "Layout1" sheet: https://i.imgur.com/tKvXipq.png

The code used to generate this result:

C# Code:
var model = DwgReader.Read("SourceTest.dwg"); //same SourceTest.dwg as before

Console.WriteLine(model.Layouts.Count); //2 layouts, the main model space layout + the original paper space sheet

var existingLayouts = model.Layouts.Where(l => l.PaperSpace).ToList(); //get all the Paper Space layouts
foreach (var existingLayout in existingLayouts)
    model.Layouts.Remove(existingLayout); //remove each Paper Space layout
    
Console.WriteLine(model.Layouts.Count); //1 layout, the main model space layout

var newLayout = new DxfLayout("test sheet");
model.Layouts.Add(newLayout);
    
Console.WriteLine(model.Layouts.Count); //2 layouts, the main model space layout + the new paper space sheet we just added
    
DwgWriter.Write("Output.dwg", model); //results in 3 layouts when opened by AutoCAD due to the recovery

The same code in 4.0.38.243 works fine with no error in AutoCAD or "Layout1" being recovered.

Wout
5/6/2022 4:53 PM

Hi,

I've fixed the problem. There was another issue in the anonymous block collection implementation. I failed to write unit tests for it, which I should have, but I've just remedied this. Thank you for yet another report.

- Wout

FizixMan
5/11/2022 5:36 PM

What you provided seems to be working fine for the test scenarios. Thanks!

I am running into some real pains adapting the code to our templating system. For whatever reason, the attributes are coming out blank in the PDF, but they're there in the DWG export but converted to single line again. None of this is happening with the minimal test code provided here, so I'm clearly doing something terribly wrong on my end. I'll probably figure it out though. Thanks again for the fixes.

FizixMan
5/12/2022 6:31 PM

Hi, I think I may have figured out part of my issue.

It seems that the multiline mtext attributes are not being affected by repositioning their associated insert. When I assign an InsertionPoint, ScaleFactor, and make a call to ApplyInsertionPointOffsetToAttributes(), the multiline attribute seems unaffected.

I updated the DWG block to include some random shapes so we can see where we would have expected the attribute text to transform to: https://i.imgur.com/cZHkccU.png

The resulting output has the multiline attribute cloned over, but it did not receive the transformations from its parents insert: https://i.imgur.com/TWPNH50.png

If I open up ATTMAN and hit Sync, the attribute updates over to the size and location we expected it: https://i.imgur.com/BiEJ1XC.png

Using the same example code you provided but with the extra transformations added at the end:

C# Code:
//Our template model from which we want to copy a block and insert
var sourceModel = DwgReader.Read("SourceTransformationTest.dwg"); 

//A new model that we want to insert the template block into
var destinationModel = new DxfModel(DxfVersion.Dxf32);

//The source template block we want to copy
var sourceBlock = sourceModel.Blocks["Test"];

//Get our multiline attribute from the source block
var sourceAttributeDefinition = sourceBlock.GetAttributeDefinition("TEST");

//Copy the source block over to the new model
DxfBlock copiedBlock;
{
    var cloneContext = new CloneContext(sourceModel, destinationModel, ReferenceResolutionType.CloneMissing);
    copiedBlock = (DxfBlock)sourceBlock.Clone(cloneContext);
    destinationModel.Blocks.Add(copiedBlock);
    cloneContext.ResolveReferences();
}

var copiedAttDef = copiedBlock.GetAttributeDefinition(sourceAttributeDefinition.TagString);
DxfMText copiedMText;
{
    var cloneContext = new CloneContext(sourceModel, destinationModel, ReferenceResolutionType.CloneMissing);
    copiedMText = (DxfMText)copiedAttDef.Mtext.Clone(cloneContext);
    cloneContext.ResolveReferences();
}

//Insert the block into the model space
var destinationInsert = new DxfInsert(copiedBlock);
destinationModel.ModelLayout.Entities.Add(destinationInsert);

//Now create a new attribute definition using our attribute's definition and text.
var copiedAttribute = destinationInsert.AddMultilineAttribute(copiedAttDef, copiedMText);

//Some arbitrary placement/scale of the insertion
destinationInsert.InsertionPoint = new WW.Math.Point3D(10, 5, 0);
destinationInsert.ScaleFactor = new Vector3D(10, 10, 0);
destinationInsert.ApplyInsertionPointOffsetToAttributes();

DwgWriter.Write(@"Output.dwg", destinationModel);

This is executing on the 4.0.39.46 version.

Note I have attached the new SourceTransformationTest.dwg to see the transformation.

Wout
6/14/2022 4:06 PM

Hi,

I've updated the DxfInsert.ApplyInsertionPointOffsetToAttributes() method to also change the underlying multiline attribute position in the latest CadLib 4.0 build.

Thank you for the report,

- Wout

FizixMan
6/15/2022 4:08 PM

Thanks for the update, Wout.

I think the translations are applying, but maybe not the scale? The text seems horizontally in the correct position, but the scaling and vertical location don't seem correct.

Here's what the same code produces when it's first opened in AutoCAD: https://i.imgur.com/lU8YBDk.png

After opening the Block Attribute Manager and hitting "Sync", it then updates the attribute to how you would expect: https://i.imgur.com/VfmkXO8.png

Am I missing a step or something in the example code I posted with handling the inserts?

This is using the same sample coded posted above and tested on 40.0.39.53.

1 2    next >