Copying Groups between drawings

Copying Groups between drawings

SRSDS
Advisor Advisor
3,809 Views
25 Replies
Message 1 of 26

Copying Groups between drawings

SRSDS
Advisor
Advisor

Does anyone have any sample code for copying grouped entities between drawings?

I think I could do it but would prefer not to spend days of confusing frustration and potentially reinventing a wheel.

0 Likes
Accepted solutions (2)
3,810 Views
25 Replies
Replies (25)
Message 2 of 26

_gile
Consultant
Consultant
Accepted solution

Hi,

You should be able to do it by yourself.

The Group.GetAllEntityIds method returns an array of ObjectIds which can be used to create the ObjectIdcollection required by the WBlockCloneObjects method to copy these objects from one Database to another one.



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

Message 3 of 26

SRSDS
Advisor
Advisor

Gile, thanks for the tips.

I'll give it a try.

0 Likes
Message 4 of 26

Gepaha
Collaborator
Collaborator

Groups do not persist through different databases (they are hard pointer and not hard owned) and you are responsible for the implementation. The best way is probably to handle the "beginDeepCloneXlation" method. Maybe this link will help you (look in the comments):

https://adndevblog.typepad.com/autocad/2019/02/how-to-handle-references-to-other-entities.html 

I think it is dangerous terrain when a plug-in depends exclusively on groups. It is necessary that the plug-in is always loaded and when objects are copied / moved from one file to another, the plug-in handles this properly.

In addition, groups can be empty or contain only one entity, be nested or the same entity belongs to multiple groups. All options must be taken into account.

Until today, I never had the chance to analyze the implementation of a complete .NET code for a plug-in that makes extensive use of groups. So, if you can show your progress, it will be of great value to me and others.

0 Likes
Message 5 of 26

SRSDS
Advisor
Advisor

I never worked this out. I'd like to do it using the default AutoCAD copy/paste commands.

I got as far as collecting the ObjectIDs of all copied groups in the COPYBASE / COPYCLIP commands. 

Problem now is that entities pasted under the PASTECLIP command will be assigned new ObjectIDs, and don't know how to associate the old with the new.

0 Likes
Message 6 of 26

ActivistInvestor
Mentor
Mentor

You have to handle the deep clone events of the Database and do the ObjectId translation. When you find an IdPair in the IdMapping whose Key property is an Id that's in a group, you add the value of the Idpair's Value property to the group in the destination document.

0 Likes
Message 7 of 26

SRSDS
Advisor
Advisor

How do I access the idmapping? Could it be BeginDeepCloneTranslation?

I can see it has IdMappingEventArgs but not sure how to get it from the 'e' variable.

 

0 Likes
Message 8 of 26

ActivistInvestor
Mentor
Mentor

The IdMapping is passed into the BeginDeepClone/BeginWBlockClone events.

0 Likes
Message 9 of 26

JamesMaeding
Advisor
Advisor

I happened to see this just now.

I did a lisp implementation of this in the "WTIT" tools attached.

What you would do is run WTWG on entities in a drawing, then ITWG.

Test that in a session with one drawing, its not 100% fast or reliable as grouping can get tangled.

 

I would argue these tools are superior than cut and paste as they do not use the windows clipboard.

You also have several "clipboards" with these, WT2, WT3...you can add as many as you want in the lisp.

You also have WTN/ITN, if you edit the path to some company network location. Yes, a network clipboard!

I am remote and use it with people on teams calls all the time.

 

Here is a listing of key-ins, also in the .lsp if you open it:

WT - CUT TO LOCAL TEMP.DWG USING 0,0
WT2 - CUT TO LOCAL TEMP2.DWG USING 0,0
WT3 - CUT TO LOCAL TEMP3.DWG USING 0,0
WTX - CUT TO LOCAL <YOU GIVE NAME>.DWG USING 0,0
WTN - CUT TO NETWORK <YOU GIVE NAME>.DWG USING 0,0
WTWG - CUT TO LOCAL TEMP.DWG USING 0,0 AND TAG GROUPS
WTT - COPY TO LOCAL TEMP.DWG USING 0,0
WTTX - COPY TO LOCAL <YOU GIVE NAME>.DWG USING 0,0
WTTN - COPY TO NETWORK <YOU GIVE NAME>.DWG USING 0,0
WTP - CUT TO LOCAL TEMPP.DWG WITH PICK
WTPX - CUT TO LOCAL <YOU GIVE NAME>.DWG WITH PICK
WTPN - CUT TO NETWORK <YOU GIVE NAME>.DWG WITH PICK

IT - INSERT LOCAL TEMP.DWG USING 0,0
IT2 - INSERT LOCAL TEMP2.DWG USING 0,0
IT3 - INSERT LOCAL TEMP3.DWG USING 0,0
ITB - INSERT LOCAL TEMP.DWG AS BLOCK USING 0,0
ITG - INSERT LOCAL TEMP.DWG USING 0,0 AS A GROUP
ITWG - INSERT LOCAL TEMP.DWG USING 0,0 AND RESTORE GROUPS
ITP - INSERT LOCAL TEMPP.DWG WITH PICK
ITPX - INSERT LOCAL <YOU GIVE NAME>.DWG WITH PICK
ITPN - INSERT NETWORK <YOU GIVE NAME>.DWG WITH PICK
ITX - INSERT LOCAL <YOU GIVE NAME>.DWG USING 0,0
ITBX - INSERT LOCAL <YOU GIVE NAME>.DWG AS BLOCK USING 0,0
ITGX - INSERT LOCAL <YOU GIVE NAME>.DWG USING 0,0 AS A GROUP
ITN - INSERT NETWORK <YOU GIVE NAME>.DWG USING 0,0
ITBN - INSERT NETWORK TEMP.DWG AS BLOCK USING 0,0
ITGN - INSERT NETWORK <YOU GIVE NAME>.DWG USING 0,0 AS A GROUP

 enjoy.


internal protected virtual unsafe Human() : mostlyHarmless
I'm just here for the Shelties

0 Likes
Message 10 of 26

Gepaha
Collaborator
Collaborator

Firstly, thank you very much to everyone who responds here. For me, any comments are always welcome.
The subject is of interest to me too, I was hoping someone could give an answer with an example, apparently it's not that simple.
I may be wrong, and if I am, correct me, but when CRTL C (CopyClip) is used, entities are copied to a temporary database placed on the Clipboard and should not contain further group membership information.
If my point of view is correct, then you would have to capture the entities and group when the temporary database is created, possibly at a WBlock event.
With this information, when pressing CRLT V (PasteClip), using a deepclone event that contains IdMapping, it is possible to find the relationship between the original entity and the new entity. IdPar.Key is the ObjectId of the original entity, IdPar.Value is the ObjectId of the new entity and LookUp gets the ObjecId of the cloned object.
With the mapping between the original entity, the new entity and group, then in a DeepCloneEnded event you would create the groups and associate the new entities with them.

How do you capture and store the entities that are part of the group when CRTL C is used? Use GetPersistentReactorIds()? Which event do you monitor? Is it possible to share part of the code of how you do this?

Message 11 of 26

ActivistInvestor
Mentor
Mentor

Trying to handle wblock operations is extremely problematic using the managed API, due to long-standing defects that have never been fixed.

 

The show-stopper WRT to wblock operations is the Database.s WblockNotice event. 

 

According the the native ObjectARX docs:

 

This callback function indicates that a wblock operation is about to start. All that occurs when this notification is sent is the creation of the AcDbDatabase to which the wblockCloned objects are added. pDb points to this newly created AcDbDatabase. This callback is provided so that applications may attach a reactor to the destination database to monitor the appending of objects to it.

Parameters
pDb: Passed in pointer to target AcDbDatabase

Unfortunately, the Database that is passed into the managed event argument which is returned by the property 'To', is not the destination database as the excerpt above states. It is the source Database (it seems they got the 'sender' argument and the event argument 'To' property confused), and there is no way to access the destination database from that event. 

 

0 Likes
Message 12 of 26

Gepaha
Collaborator
Collaborator

Thank you for the clarification.
It turns out that I ruled out using the WBlockNotice event because it involved an entire database.
I looked for an event that involved IdMapping and ended up testing the BeginWblockObjects event. It just doesn't work, it never fires. It is implemented but has no use.
So, I started using a mixed solution involving ObjectOverrule and BeginDeepCloneTranslation.
It turns out that I didn't find a way to map the original entities with the new entities.
When CopyClip is used I can store the original entity through pair.Key (at least I think that is correct) and the destination entity through pair.Value. From what I can see, a temporary file based on a dwt is created, then the temporary file with the entities goes to the clipboard. Thus the original of pair.Value is lost.

0 Likes
Message 13 of 26

ActivistInvestor
Mentor
Mentor

When you WBLOCK * (entire file), groups should go along for the ride, but when wblocking selected objects (e.g., COPYCLIP), the problem becomes what should happen if not all objects in a group are included?

 

That's an application-specific question/answer, and I don't see any easy way to generalize it. In some cases, the actual underlying problem is the use of groups for establishing some type of associativity between objects, for application-specific purposes, rather than for the user's direct use of groups.

 

Regarding my observations about the issues with the WBlockNotice event, here is a workaround that will let you find the destination database for the WBLOCK operation. It's not pretty, but it works:

 

public static class WblockNoticeExample
{
   /// <summary>
   /// Adds a WblockNotice handler to the given Database:
   /// </summary>
   /// <param name="db"></param>

   public static void Add(Database db)
   {
      db.WblockNotice += wblockNotice;
   }

   private static void wblockNotice(object sender, WblockNoticeEventArgs e)
   {
      // Because the event args does not expose
      // the destination database, and because that
      // database is not constructed until after this
      // handler returns, we can find out what that
      // database is by handling the DatabaseContstructed
      // event, which will be raised immediately after
      // this handler returns:

      Database.DatabaseConstructed += databaseConstructed;
   }

   /// <summary>
   /// Handler for the DatabaseConstructed event that 
   /// is called immediately after the handler for the
   /// WBlockNotice event returns:
   /// </summary>

   private static void databaseConstructed(object sender, EventArgs e)
   {
      /// Remove this handler
      Database.DatabaseConstructed -= databaseConstructed;

      /// The sender is the database that was created,
      /// and is the destination database for the WBLOCK 
      /// clone operation:

      Database destinationDb = (Database) sender;

      /// Now that we have the destination database for
      /// the WBLOCK operation, we can add whatever event
      /// handlers are needed
      /// 
      /// Add a handler to the destination database's
      /// BeginDeepCloneTranslation event:

      destinationDb.BeginDeepCloneTranslation += beginDeepCloneTranslation;
   }

   private static void beginDeepCloneTranslation(object sender, IdMappingEventArgs e)
   {
      WriteMsg("\n*** destination database raised BeginDeepCloneTranslation event ***");
      ((Database) sender).BeginDeepCloneTranslation -= beginDeepCloneTranslation;
   }

   static void WriteMsg(string fmt, params object[] args)
   {
      var doc = Application.DocumentManager.MdiActiveDocument;
      doc?.Editor.WriteMessage("\n" + fmt, args);
   }

   [CommandMethod("TESTWBLOCKNOTICE")]
   public static void AddCurrentDb()
   {
      Add(HostApplicationServices.WorkingDatabase);
   }
}
Command: TESTWBLOCKNOTICE
Command: -WBLOCK
Current settings: Object conversion=Retain
Enter name of output file: TESTWBLOCKNOTICE.DWG
Enter name of existing block or
[= (block=output file)/* (whole drawing)] <define new drawing>:
Enter name of existing block or
[= (block=output file)/* (whole drawing)] <define new drawing>:
Specify insertion base point or [mOde]:
Select objects: Specify opposite corner: 5 found
Select objects:
*** destination database raised BeginDeepCloneTranslation event
Command:

 

0 Likes
Message 14 of 26

JamesMaeding
Advisor
Advisor

@ActivistInvestor 

Thanks for the insight, as always.

So my practical answer would be that only top level grouping needs to be maintained.

If there are nested groups, they get lost.

My lisp implementation tries to do that, and is slow. I don't even use it any more.

So .net would be 1000x faster and better, and I did not try it yet.

Hopefully dropping nested groups makes things doable.

thx


internal protected virtual unsafe Human() : mostlyHarmless
I'm just here for the Shelties

0 Likes
Message 15 of 26

Gepaha
Collaborator
Collaborator

Thanks for the workaround.
I tested this but it seems to have the same problem that occurs in WblockClone with ObjectOverrule.
I can't get target database name.
Under this condition, would it be possible to add the groups to the target database and associate the cloned entities in BeginDeepCloneTranslation? That's what I'm trying to do but so far I haven't made any progress.

 

private static void beginDeepCloneTranslation(object sender, IdMappingEventArgs e)
{
   WriteMsg("\n*** destination database raised BeginDeepCloneTranslation event ***");
   IdMappingWrite(e.IdMapping);
   // How to add the group and associate the entities using e.IdMapping.Add? Is this possible?
   ((Database)sender).BeginDeepCloneTranslation -= beginDeepCloneTranslation;
}
private static void IdMappingWrite(IdMapping idMapping)
{
  Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;          
  ed.WriteMessage("\nDeepCloneContext: " + idMapping.DeepCloneContext);
  ed.WriteMessage("\nOriginalDatabase: " + idMapping.OriginalDatabase.UnmanagedObject.ToInt64() + " FileName: " + idMapping.OriginalDatabase.Filename);
  ed.WriteMessage("\nDestinationDatabase: " + idMapping.DestinationDatabase.UnmanagedObject.ToInt64() + " FileName: " + idMapping.DestinationDatabase.Filename);

  List<IdPair> idPairList = idMapping.Cast<IdPair>().Where(item => item.IsCloned).ToList();
  ed.WriteMessage("\nIdPairs:");
  foreach (IdPair idPair in idPairList)
  {
     ed.WriteMessage("\nObjectType: {4}, KeyId: {0}, ValueId: {1}, KeyHandle: {2}, ValueHandle: {3}", idPair.Key, idPair.Value, idPair.Key.Handle, idPair.Value.Handle, idPair.Key.ObjectClass.Name);
  }
  ed.WriteMessage("\n");
}

 

 

Capturar.JPG

0 Likes
Message 16 of 26

ActivistInvestor
Mentor
Mentor

@Gepaha wrote:

Thanks for the workaround.
I tested this but it seems to have the same problem that occurs in WblockClone with ObjectOverrule.
I can't get target database name.

 


The destination database hasn't been saved at the point when the event handlers fire. Since its a new Database, it won't have a file name until its saved. If you need the file name you have to add a handler to its BeginSave event. That event might also be the best way to add the missing groups. You can use the IdMapping to translate the source database entities in each group to their corresponding clones, and add the clones to the groups.

0 Likes
Message 17 of 26

ActivistInvestor
Mentor
Mentor

@ActivistInvestor wrote:


The destination database hasn't been saved at the point when the event handlers fire. Since its a new Database, it won't have a file name until its saved. If you need the file name you have to add a handler to its BeginSave event. That event might also be the best way to add the missing groups. You can use the IdMapping to translate the source database entities in each group to their corresponding clones, and add the clones to the groups.


Disregard what I wrote above. In the BeginSave event, the IdMapping is completely unusable and any attempt to use it will just terminate AutoCAD.

 

From reading this entire thread, I think one might get the impression that this problem is much more difficult to solve than it actually is. 

 

I haven't tested this thoroughly, but when I use COPYCLIP and PASTECLIP, groups will go along for the ride. Ditto for any form of WBLOCK.

 

This code, and the file it requires can be found here and here.

 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.DatabaseServices.Extensions;
using Autodesk.AutoCAD.Runtime;

/// <summary>
/// An example showing the use of the WblockCloneHandler
/// base type, which automates the grunt work required to
/// intervene in a wblock clone operation. 
/// 
/// This example causes groups to be included in a WBLOCK or a
/// COPYCLIP operation when all of their member entities are 
/// included in the operation. 
/// 
/// To enable the functionality provided by this example, one 
/// only need to issue the WBLOCKGROUPS command defined below. 
/// 
/// In a realistic usage scenario, the initialization done by
/// the WBLOCKGROUPS command might instead by done within the
/// Initialize() method of an IExtensionApplication.
/// 
/// Once WBLOCKGROUPS is issued, you can use the COPYCLIP and
/// PASTECLIP commands to copy and paste entities, including 
/// any groups they belong to.
/// 
/// In order for a group to be included in a WBLOCK operation,
/// all of the member entities in the group must be included in
/// the operation. If only a subset of member entities of a group
/// are involved in a WBLOCK operation, the group(s) they belong 
/// to are not included.
/// 
/// If COPYCLIP is used to copy entities and groups to the 
/// clipboard, when pasted back into a drawing the groups will 
/// be pasted as anonymous/unnamed groups. 
/// 
/// Because this operation does not technically Clone existing
/// groups, if there is any type of application-data attached to 
/// a group (e.g., xdata or extension dictionary) it will not be
/// copied/cloned or transformed. Supporting that is beyond the
/// scope of this example.
/// </summary>

namespace AcMgdLib.Common.Examples
{
   public class WblockGroupHandler : WblockCloneHandler
   {
      public WblockGroupHandler(Database db) : base(db)
      {
      }

      protected override void OnDeepCloneEnded(Database sender)
      {
         CloneGroups(this.IdMap);
      }

      /// <summary>
      /// Clones all groups from the source database
      /// to the destination database, only if all of 
      /// the entities in the group were cloned. 
      /// </summary>
      /// <param name="map"></param>

      static void CloneGroups(IdMapping map)
      {
         if(map == null)
            throw new ArgumentNullException(nameof(map));
         try
         {
            using(var tr = new OpenCloseTransaction())
            {
               Database sourceDb = map.OriginalDatabase;
               Database destDb = map.DestinationDatabase;
               ObjectId destGroupDictionaryId = destDb.GroupDictionaryId;
               var groupDictionary = tr.GetObject<DBDictionary>(
                  destGroupDictionaryId, OpenMode.ForWrite);
               foreach(var srcGroup in sourceDb.GetGroups(tr))
               {
                  if(srcGroup.IsNotAccessible)
                     continue;
                  var cloneIds = GetCloneIds(srcGroup, map);
                  if(cloneIds != null)
                  {
                     Group group = new Group(srcGroup.Description, srcGroup.Selectable);
                     groupDictionary.SetAt(srcGroup.Name, group);
                     tr.AddNewlyCreatedDBObject(group, true);
                     group.Append(cloneIds);
                     DebugWrite($"Copied group {srcGroup.Name} ({cloneIds.Count} entities)");
                  }
               }
               tr.Commit();
            }
         }
         catch(System.Exception ex)
         {
            WriteMessage($"Exception in {nameof(CloneGroups)}(): {ex.ToString()}");
            return;
         }
      }

      /// <summary>
      /// If not all source entities exist in the map (e.g., they
      /// were not cloned), then this returns null and the group
      /// is not cloned.
      /// </summary>

      public static ObjectIdCollection GetCloneIds(Group source, IdMapping map)
      {
         var srcIds = source.GetAllEntityIds();
         var cloneIds = new ObjectId[srcIds.Length];
         for(int i = 0; i < srcIds.Length; i++)
         {
            var id = srcIds[i];
            if(!map.Contains(id))
               return null;
            cloneIds[i] = map[id].Value;
         }
         return new ObjectIdCollection(cloneIds);
      }

   }

   public static class WBlockGroupExtensions
   {
      public static IEnumerable<Group> GetGroups(this Database db, Transaction tr)
      {
         var groupDict = Unsafe.As<DBDictionary>(tr.GetObject(db.GroupDictionaryId, OpenMode.ForRead))
         foreach(DictionaryEntry entry in groupDict)
         {
            yield return Unsafe.As<Group>(tr.GetObject((ObjectId)entry.Value, OpenMode.ForRead));
         }
      }

      public static T GetObject<T>(this Transaction tr, ObjectId id, OpenMode mode = OpenMode.ForRead)
         where T: DBObject
      {
         return Unsafe.As<T>(tr.GetObject(id, mode));
      }


   }

   public static class WblockGroupHandlers
   {
      static bool initialized = false;

      public static void Initialize()
      {
         if(!initialized)
         {
            initialized = true;
            foreach(Document doc in Application.DocumentManager)
            {
               doc.UserData[typeof(WblockGroupHandler)] =
                  new WblockGroupHandler(doc.Database);
            }

            Application.DocumentManager.DocumentCreated += documentCreated;
         }
      }

      private static void documentCreated(object sender, DocumentCollectionEventArgs e)
      {
         e.Document.UserData[typeof(WblockGroupHandler)] =
            new WblockGroupHandler(e.Document.Database);
      }
   }

   public static class TestCommand
   {
      [CommandMethod("WBLOCKGROUPS")]
      public static void Initialize()
      {
         WblockGroupHandlers.Initialize();
      }
   }

}

 

 

Message 18 of 26

Gepaha
Collaborator
Collaborator

Wow, thanks for the code, it helps a lot, it's the first code I've seen on the subject.

👏👏👏...

0 Likes
Message 19 of 26

ActivistInvestor
Mentor
Mentor
Accepted solution

After I posted that code, I found a minor bug that was fixed, and the updated files were posted to the repo here and here.

 

The bug was preventing the inclusion of groups when doing a full WBLOCK of the entire drawing file. That was fixed and works now, but be advised that the fix requires setting a switch (Database.ForceWblockDatabaseCopy()) that tells AutoCAD to make a copy of the existing Database, which it normally doesn't do when doing a full WBLOCK of the file.

0 Likes
Message 20 of 26

SRSDS
Advisor
Advisor

Thank you so much for this. I'm relying more and more on grouping dynamic blocks I can't express how much this helps.

0 Likes