Lines 522, 526 and 536 are simply changes to prompts to refer to spheres as well as circles. To actually support the selection of spheres, we now make an additional call to add Solid3d as an allowed class (line 529).

On lines 621-622 we change the criteria for auto-linking, to support objects of type Solid3d. It would be nice to also filter specifically on the solid type, but unfortunately this information is currently not exposed through .NET (you can get it from C++, should you really need to... this is a technique we used in the Component Technologies demo we've been presenting at our annual regional conferences). And in any case, you can get some really fun results if you select different types of solids... :-)

And finally, we have the code to get the centre and radius of spheres (lines 772-787 and line 794). We simply use the centroid of the solid for the centre (which is a fair guess for solids, but may be slightly off for pyramids, for instance), and we retrieve the radius by working backwards from the surface area, which for sphere is 4 x PI x r^2. Again - this isn't quite going to right for non-spherical solids, so be prepared.

OK, that's it... here's the C# code:

1using System;

2using System.Collections;

3using System.Collections.Generic;

4using Autodesk.AutoCAD.Runtime;

5using Autodesk.AutoCAD.ApplicationServices;

6using Autodesk.AutoCAD.DatabaseServices;

7using Autodesk.AutoCAD.EditorInput;

8using Autodesk.AutoCAD.Geometry;

9

10 [assembly:

11 CommandClass(

12typeof(

13 AsdkLinkingLibrary.LinkingCommands

14 )

15 )

16 ]

17

18namespace AsdkLinkingLibrary

19 {

20///<summary>

21/// Utility class to manage and save links

22/// between objects

23///</summary>

24publicclass LinkedObjectManager

25 {

26conststring kCompanyDict =

27"AsdkLinks";

28conststring kApplicationDict =

29"AsdkLinkedObjects";

30conststring kXrecPrefix =

31"LINKXREC";

32

33Dictionary<ObjectId, ObjectIdCollection> m_dict;

34

35// Constructor

36public LinkedObjectManager()

37 {

38 m_dict =

39newDictionary<ObjectId,ObjectIdCollection>();

40 }

41

42// Create a bi-directional link between two objects

43publicvoid LinkObjects(ObjectId from, ObjectId to)

44 {

45 CreateLink(from, to);

46 CreateLink(to, from);

47 }

48

49// Helper function to create a one-way

50// link between objects

51privatevoid CreateLink(ObjectId from, ObjectId to)

52 {

53 ObjectIdCollection existingList;

54if (m_dict.TryGetValue(from, out existingList))

55 {

56if (!existingList.Contains(to))

57 {

58 existingList.Add(to);

59 m_dict.Remove(from);

60 m_dict.Add(from, existingList);

61 }

62 }

63else

64 {

65 ObjectIdCollection newList =

66new ObjectIdCollection();

67 newList.Add(to);

68 m_dict.Add(from, newList);

69 }

70 }

71

72// Remove bi-directional links from an object

73publicvoid RemoveLinks(ObjectId from)

74 {

75 ObjectIdCollection existingList;

76if (m_dict.TryGetValue(from, out existingList))

77 {

78 m_dict.Remove(from);

79foreach (ObjectId id in existingList)

80 {

81 RemoveFromList(id, from);

82 }

83 }

84 }

85

86// Helper function to remove an object reference

87// from a list (assumes the overall list should

88// remain)

89privatevoid RemoveFromList(

90 ObjectId key,

91 ObjectId toremove

92 )

93 {

94 ObjectIdCollection existingList;

95if (m_dict.TryGetValue(key, out existingList))

96 {

97if (existingList.Contains(toremove))

98 {

99 existingList.Remove(toremove);

100 m_dict.Remove(key);

101 m_dict.Add(key, existingList);

102 }

103 }

104 }

105

106// Return the list of objects linked to

107// the one passed in

108public ObjectIdCollection GetLinkedObjects(

109 ObjectId from

110 )

111 {

112 ObjectIdCollection existingList;

113 m_dict.TryGetValue(from, out existingList);

114return existingList;

115 }

116

117// Check whether the dictionary contains

118// a particular key

119publicbool Contains(ObjectId key)

120 {

121return m_dict.ContainsKey(key);

122 }

123

124// Save the link information to a special

125// dictionary in the database

126publicvoid SaveToDatabase(Database db)

127 {

128 Transaction tr =

129 db.TransactionManager.StartTransaction();

130using (tr)

131 {

132 ObjectId dictId =

133 GetLinkDictionaryId(db, true);

134 DBDictionary dict =

135 (DBDictionary)tr.GetObject(

136 dictId,

137 OpenMode.ForWrite

138 );

139int xrecCount = 0;

140

141foreach (

142KeyValuePair<ObjectId, ObjectIdCollection> kv

143in m_dict

144 )

145 {

146// Prepare the result buffer with our data

147 ResultBuffer rb =

148new ResultBuffer(

149new TypedValue(

150 (int)DxfCode.SoftPointerId,

151 kv.Key

152 )

153 );

154int i = 1;

155foreach (ObjectId id in kv.Value)

156 {

157 rb.Add(

158new TypedValue(

159 (int)DxfCode.SoftPointerId + i,

160 id

161 )

162 );

163 i++;

164 }

165

166// Update or create an xrecord to store the data

167 Xrecord xrec;

168bool newXrec = false;

169if (dict.Contains(

170 kXrecPrefix + xrecCount.ToString()

171 )

172 )

173 {

174// Open the existing object

175 DBObject obj =

176 tr.GetObject(

177 dict.GetAt(

178 kXrecPrefix + xrecCount.ToString()

179 ),

180 OpenMode.ForWrite

181 );

182// Check whether it's an xrecord

183 xrec = obj as Xrecord;

184if (xrec == null)

185 {

186// Should never happen

187// We only store xrecords in this dict

188 obj.Erase();

189 xrec = new Xrecord();

190 newXrec = true;

191 }

192 }

193// No object existed - create a new one

194else

195 {

196 xrec = new Xrecord();

197 newXrec = true;

198 }

199 xrec.XlateReferences = true;

200 xrec.Data = (ResultBuffer)rb;

201if (newXrec)

202 {

203 dict.SetAt(

204 kXrecPrefix + xrecCount.ToString(),

205 xrec

206 );

207 tr.AddNewlyCreatedDBObject(xrec, true);

208 }

209 xrecCount++;

210 }

211

212// Now erase the left-over xrecords

213bool finished = false;

214do

215 {

216if (dict.Contains(

217 kXrecPrefix + xrecCount.ToString()

218 )

219 )

220 {

221 DBObject obj =

222 tr.GetObject(

223 dict.GetAt(

224 kXrecPrefix + xrecCount.ToString()

225 ),

226 OpenMode.ForWrite

227 );

228 obj.Erase();

229 }

230else

231 {

232 finished = true;

233 }

234 xrecCount++;

235 } while (!finished);

236 tr.Commit();

237 }

238 }

239

240// Load the link information from a special

241// dictionary in the database

242publicvoid LoadFromDatabase(Database db)

243 {

244 Document doc =

245 Application.DocumentManager.MdiActiveDocument;

246 Editor ed = doc.Editor;

247 Transaction tr =

248 db.TransactionManager.StartTransaction();

249using (tr)

250 {

251// Try to find the link dictionary, but

252// do not create it if one isn't there

253 ObjectId dictId =

254 GetLinkDictionaryId(db, false);

255if (dictId.IsNull)

256 {

257 ed.WriteMessage(

258"\nCould not find link dictionary."

259 );

260return;

261 }

262

263// By this stage we can assume the dictionary exists

264 DBDictionary dict =

265 (DBDictionary)tr.GetObject(

266 dictId, OpenMode.ForRead

267 );

268int xrecCount = 0;

269bool done = false;

270

271// Loop, reading the xrecords one-by-one

272while (!done)

273 {

274if (dict.Contains(

275 kXrecPrefix + xrecCount.ToString()

276 )

277 )

278 {

279 ObjectId recId =

280 dict.GetAt(

281 kXrecPrefix + xrecCount.ToString()

282 );

283 DBObject obj =

284 tr.GetObject(recId, OpenMode.ForRead);

285 Xrecord xrec = obj as Xrecord;

286if (xrec == null)

287 {

288 ed.WriteMessage(

289"\nDictionary contains non-xrecords."

290 );

291return;

292 }

293int i = 0;

294 ObjectId from = new ObjectId();

295 ObjectIdCollection to =

296new ObjectIdCollection();

297foreach (TypedValue val in xrec.Data)

298 {

299if (i == 0)

300 from = (ObjectId)val.Value;

301else

302 {

303 to.Add((ObjectId)val.Value);

304 }

305 i++;

306 }

307// Validate the link info and add it to our

308// internal data structure

309 AddValidatedLinks(db, from, to);

310 xrecCount++;

311 }

312else

313 {

314 done = true;

315 }

316 }

317 tr.Commit();

318 }

319 }

320

321// Helper function to validate links before adding

322// them to the internal data structure

323privatevoid AddValidatedLinks(

324 Database db,

325 ObjectId from,

326 ObjectIdCollection to

327 )

328 {

329 Document doc =

330 Application.DocumentManager.MdiActiveDocument;

331 Editor ed = doc.Editor;

332 Transaction tr =

333 db.TransactionManager.StartTransaction();

334using (tr)

335 {

336try

337 {

338 ObjectIdCollection newList =

339new ObjectIdCollection();

340

341// Open the "from" object

342 DBObject obj =

343 tr.GetObject(from, OpenMode.ForRead, false);

344if (obj != null)

345 {

346// Open each of the "to" objects

347foreach (ObjectId id in to)

348 {

349 DBObject obj2;

350try

351 {

352 obj2 =

353 tr.GetObject(id, OpenMode.ForRead, false);

354// Filter out the erased "to" objects

355if (obj2 != null)

356 {

357 newList.Add(id);

358 }

359 }

360catch (System.Exception)

361 {

362 ed.WriteMessage(

363"\nFiltered out link to an erased object."

364 );

365 }

366 }

367// Only if the "from" object and at least

368// one "to" object exist (and are unerased)

369// do we add an entry for them

370if (newList.Count > 0)

371 {

372 m_dict.Add(from, newList);

373 }

374 }

375 }

376catch (System.Exception)

377 {

378 ed.WriteMessage(

379"\nFiltered out link from an erased object."

380 );

381 }

382 tr.Commit();

383 }

384 }

385

386// Helper function to get (optionally create)

387// the nested dictionary for our xrecord objects

388private ObjectId GetLinkDictionaryId(

389 Database db,

390bool createIfNotExisting

391 )

392 {

393 ObjectId appDictId = ObjectId.Null;

394

395 Transaction tr =

396 db.TransactionManager.StartTransaction();

397using (tr)

398 {

399 DBDictionary nod =

400 (DBDictionary)tr.GetObject(

401 db.NamedObjectsDictionaryId,

402 OpenMode.ForRead

403 );

404// Our outer level ("company") dictionary

405// does not exist

406if (!nod.Contains(kCompanyDict))

407 {

408if (!createIfNotExisting)

409return ObjectId.Null;

410

411// Create both the "company" dictionary...

412 DBDictionary compDict = new DBDictionary();

413 nod.UpgradeOpen();

414 nod.SetAt(kCompanyDict, compDict);

415 tr.AddNewlyCreatedDBObject(compDict, true);

416

417// ... and the inner "application" dictionary.

418 DBDictionary appDict = new DBDictionary();

419 appDictId =

420 compDict.SetAt(kApplicationDict, appDict);

421 tr.AddNewlyCreatedDBObject(appDict, true);

422 }

423else

424 {

425// Our "company" dictionary exists...

426 DBDictionary compDict =

427 (DBDictionary)tr.GetObject(

428 nod.GetAt(kCompanyDict),

429 OpenMode.ForRead

430 );

431/// So check for our "application" dictionary

432if (!compDict.Contains(kApplicationDict))

433 {

434if (!createIfNotExisting)

435return ObjectId.Null;

436

437// Create the "application" dictionary

438 DBDictionary appDict = new DBDictionary();

439 compDict.UpgradeOpen();

440 appDictId =

441 compDict.SetAt(kApplicationDict, appDict);

442 tr.AddNewlyCreatedDBObject(appDict, true);

443 }

444else

445 {

446// Both dictionaries already exist...

447 appDictId = compDict.GetAt(kApplicationDict);

448 }

449 }

450 tr.Commit();

451 }

452return appDictId;

453 }

454 }

455

456///<summary>

457/// This class defines our commands and event callbacks.

458///</summary>

459publicclass LinkingCommands

460 {

461 LinkedObjectManager m_linkManager;

462 ObjectIdCollection m_entitiesToUpdate;

463bool m_autoLink = false;

464 ObjectId m_lastEntity = ObjectId.Null;

465

466public LinkingCommands()

467 {

468 Document doc =

469 Application.DocumentManager.MdiActiveDocument;

470 Database db = doc.Database;

471 db.ObjectModified +=

472new ObjectEventHandler(OnObjectModified);

473 db.ObjectErased +=

474new ObjectErasedEventHandler(OnObjectErased);

475 db.ObjectAppended +=

476new ObjectEventHandler(OnObjectAppended);

477 db.BeginSave +=

478new DatabaseIOEventHandler(OnBeginSave);

479 doc.CommandEnded +=

480new CommandEventHandler(OnCommandEnded);

481

482 m_linkManager = new LinkedObjectManager();

483 m_entitiesToUpdate = new ObjectIdCollection();

484 }

485

486 ~LinkingCommands()

487 {

488try

489 {

490 Document doc =

491 Application.DocumentManager.MdiActiveDocument;

492 Database db = doc.Database;

493 db.ObjectModified -=

494new ObjectEventHandler(OnObjectModified);

495 db.ObjectErased -=

496new ObjectErasedEventHandler(OnObjectErased);

497 db.ObjectAppended -=

498new ObjectEventHandler(OnObjectAppended);

499 db.BeginSave -=

500new DatabaseIOEventHandler(OnBeginSave);

501 doc.CommandEnded +=

502new CommandEventHandler(OnCommandEnded);

503 }

504catch(System.Exception)

505 {

506// The document or database may no longer

507// be available on unload

508 }

509 }

510

511// Define "LINK" command

512 [CommandMethod("LINK")]

513publicvoid LinkEntities()

514 {

515 Document doc =

516 Application.DocumentManager.MdiActiveDocument;

517 Database db = doc.Database;

518 Editor ed = doc.Editor;

519

520 PromptEntityOptions opts =

521new PromptEntityOptions(

522"\nSelect first circle or sphere to link: "

523 );

524 opts.AllowNone = true;

525 opts.SetRejectMessage(

526"\nOnly circles or 3D solids can be selected."

527 );

528 opts.AddAllowedClass(typeof(Circle), false);

529 opts.AddAllowedClass(typeof(Solid3d), false);

530

531 PromptEntityResult res = ed.GetEntity(opts);

532if (res.Status == PromptStatus.OK)

533 {

534 ObjectId from = res.ObjectId;

535 opts.Message =

536"\nSelect second circle or sphere to link: ";

537 res = ed.GetEntity(opts);

538if (res.Status == PromptStatus.OK)

539 {

540 ObjectId to = res.ObjectId;

541 m_linkManager.LinkObjects(from, to);

542 m_lastEntity = to;

543 m_entitiesToUpdate.Add(from);

544 }

545 }

546 }

547

548// Define "AUTOLINK" command

549 [CommandMethod("AUTOLINK")]

550publicvoid ToggleAutoLink()

551 {

552 Document doc =

553 Application.DocumentManager.MdiActiveDocument;

554 Editor ed = doc.Editor;

555 m_autoLink = !m_autoLink;

556if (m_autoLink)

557 {

558 ed.WriteMessage("\nAutomatic linking turned on.");

559 }

560else

561 {

562 ed.WriteMessage("\nAutomatic linking turned off.");

563 }

564 }

565

566// Define "LOADLINKS" command

567 [CommandMethod("LOADLINKS")]

568publicvoid LoadLinkSettings()

569 {

570 Document doc =

571 Application.DocumentManager.MdiActiveDocument;

572 Database db = doc.Database;

573 m_linkManager.LoadFromDatabase(db);

574 }

575

576// Define "SAVELINKS" command

577 [CommandMethod("SAVELINKS")]

578publicvoid SaveLinkSettings()

579 {

580 Document doc =

581 Application.DocumentManager.MdiActiveDocument;

582 Database db = doc.Database;

583 m_linkManager.SaveToDatabase(db);

584 }

585

586// Define callback for Database.ObjectModified event

587privatevoid OnObjectModified(

588object sender, ObjectEventArgs e)

589 {

590 ObjectId id = e.DBObject.ObjectId;

591if (m_linkManager.Contains(id) &&

592 !m_entitiesToUpdate.Contains(id))

593 {

594 m_entitiesToUpdate.Add(id);

595 }

596 }

597

598// Define callback for Database.ObjectErased event

599privatevoid OnObjectErased(

600object sender, ObjectErasedEventArgs e)

601 {

602if (e.Erased)

603 {

604 ObjectId id = e.DBObject.ObjectId;

605 m_linkManager.RemoveLinks(id);

606if (m_lastEntity == id)

607 {

608 m_lastEntity = ObjectId.Null;

609 }

610 }

611 }

612

613// Define callback for Database.ObjectAppended event

614void OnObjectAppended(object sender, ObjectEventArgs e)

615 {

616 Database db = sender as Database;

617if (db != null)

618 {

619if (m_autoLink)

620 {

621if (e.DBObject.GetType() == typeof(Circle) ||

622 e.DBObject.GetType() == typeof(Solid3d))

623 {

624 ObjectId from = e.DBObject.ObjectId;

625if (m_lastEntity == ObjectId.Null)

626 {

627 m_lastEntity = from;

628 }

629else

630 {

631 m_linkManager.LinkObjects(from, m_lastEntity);

632 m_lastEntity = from;

633 m_entitiesToUpdate.Add(from);

634 }

635 }

636 }

637 }

638 }

639

640// Define callback for Database.BeginSave event

641void OnBeginSave(object sender, DatabaseIOEventArgs e)

642 {

643 Database db = sender as Database;

644if (db != null)

645 {

646 m_linkManager.SaveToDatabase(db);

647 }

648 }

649

650// Define callback for Document.CommandEnded event

651privatevoid OnCommandEnded(

652object sender, CommandEventArgs e)

653 {

654foreach (ObjectId id in m_entitiesToUpdate)

655 {

656 UpdateLinkedEntities(id);

657 }

658 m_entitiesToUpdate.Clear();

659 }

660

661// Helper function for OnCommandEnded

662privatevoid UpdateLinkedEntities(ObjectId from)

663 {

664 Document doc =

665 Application.DocumentManager.MdiActiveDocument;

666 Editor ed = doc.Editor;

667 Database db = doc.Database;

668

669 ObjectIdCollection linked =

670 m_linkManager.GetLinkedObjects(from);

671

672 Transaction tr =

673 db.TransactionManager.StartTransaction();

674using (tr)

675 {

676try

677 {

678 Point3d firstCenter;

679 Point3d secondCenter;

680double firstRadius;

681double secondRadius;

682

683 Entity ent =

684 (Entity)tr.GetObject(from, OpenMode.ForRead);

685

686if (GetCenterAndRadius(

687 ent,

688out firstCenter,

689out firstRadius

690 )

691 )

692 {

693foreach (ObjectId to in linked)

694 {

695 Entity ent2 =

696 (Entity)tr.GetObject(to, OpenMode.ForRead);

697if (GetCenterAndRadius(

698 ent2,

699out secondCenter,

700out secondRadius

701 )

702 )

703 {

704 Vector3d vec = firstCenter - secondCenter;

705if (!vec.IsZeroLength())

706 {

707// Only move the linked circle if it's not

708// already near enough

709double apart =

710 vec.Length - (firstRadius + secondRadius);

711if (apart < 0.0)

712 apart = -apart;

713

714if (apart > 0.00001)

715 {

716 ent2.UpgradeOpen();

717 ent2.TransformBy(

718 Matrix3d.Displacement(

719 vec.GetNormal() * apart

720 )

721 );

722 }

723 }

724 }

725 }

726 }

727 }

728catch (System.Exception ex)

729 {

730 Autodesk.AutoCAD.Runtime.Exception ex2 =

731 ex as Autodesk.AutoCAD.Runtime.Exception;

732if (ex2 != null &&

733 ex2.ErrorStatus !=

734 ErrorStatus.WasOpenForUndo &&

735 ex2.ErrorStatus !=

736 ErrorStatus.WasErased

737 )

738 {

739 ed.WriteMessage(

740"\nAutoCAD exception: {0}", ex2

741 );

742 }

743elseif (ex2 == null)

744 {

745 ed.WriteMessage(

746"\nSystem exception: {0}", ex

747 );

748 }

749 }

750 tr.Commit();

751 }

752 }

753

754// Helper function to get the center and radius

755// for all supported circular objects

756privatebool GetCenterAndRadius(

757 Entity ent,

758out Point3d center,

759outdouble radius

760 )

761 {

762// For circles it's easy...

763 Circle circle = ent as Circle;

764if (circle != null)

765 {

766 center = circle.Center;

767 radius = circle.Radius;

768returntrue;

769 }

770else

771 {

772// For solids (spheres) return the centroid

773// and the radius, derived from the area

774 Solid3d solid = ent as Solid3d;

775if (solid != null)

776 {

777 center =

778 solid.MassProperties.Centroid;

779// Surface area of a sphere is 4 * pi * r^2

780 radius =

781 System.Math.Sqrt(

782 solid.Area / System.Math.PI

783 ) / 2;

784returntrue;

785 }

786else

787 {

788// Throw in some empty values...

789// Returning false indicates the object

790// passed in was not useable

791 center = Point3d.Origin;

792 radius = 0.0;

793returnfalse;

794 }

795 }

796 }

797 }

798 }

Just to prove it all works, here are a couple of screenshots of some linked spheres with pretty materials attached. By the way, the links will also work when the movement is in full 3D - not just planar to the current UCS:

That's almost it for the "Linking Circles" series, unless someone makes some interesting suggestions in the comments. I have an old LISP file I used to demo the original sample: it simply moves the "head of the snake" through a sine wave a number of times across the screen (just using the MOVE command - nothing very fancy). I'll post the code next time, in case anyone's interested.