This is a problem that developers have been struggling with for some time, and it came up again at the recent Cloud Accelerator: how to control the display of AutoCAD geometry at a per-viewport level, perhaps to implement your own “isolate in a viewport” command.
It’s certainly possible to control layer visibility at the viewport level, of course, but this is sometimes at odds with how users wish to use layers for their own purposes. An application may want to isolate geometry in a certain location from a number of layers, for instance, and it becomes cumbersome to hijack the layer system and change everything back, afterwards.
So on the way back from Prague, I started work on an overrule that does this cleanly. Attempts have been made before – here’s one I found out about fairly recently that didn’t work for me – but I think the combination of tricks I put together here should be useful for people.
To start with, I decided to create a DrawableOverrule that does a few things: for all AutoCAD entities, it makes sure SetAttributes() indicates that the entities need to be drawn per-viewport. It also returns false from WorldDraw(), forcing ViewportDraw() to be called. Because a lot of AutoCAD entities don’t implement ViewportDraw(), we also need to capture the WorldDraw() graphics and send them through to ViewportDraw(). So we’ve derived a “pass through” class from WorldDraw (and a corresponding geometry object from WordGeometry) that encapsulates a ViewportDraw object (and its ViewportGeometry) and simply makes calls through to these, as needed.
There were a few nasty little gotchas, along the way, though…
Firstly, the Shell() call was failing, as one of the arguments passed in to it couldn’t be marshalled across to the equivalent ViewportGeometry call. Very strange. But creating a new collection and copying the contents into it addressed that.
Secondly, the regen-type specific geometry for solids and surfaces wasn’t being collected properly by the graphics system. Usually it makes two passes, one with “shaded” regen-type and the other with “standard” regen-type. This is enough to display the graphics appropriately for the various AutoCAD visual styles. In this case both calls were made with “standard”, so we needed some code to track the calls made for each drawable and force “shaded” for the first of each set of calls. I’m hopeful this is something that won’t be needed, in the future, but for now it’s a required workaround. Many thanks for Erik Larsen for his help identifying both the issue and how to deal with it.
I’ve only attached the event handler that cleans the cache needed by this workaround at the active document’s command boundaries. I was lazy: if you’re implementing this yourself, please make sure you attach the event for each open document (and any opened/created in the future).
Block attribute support was also pretty tricky to get working: I realised last night that I needed to attach the overrule at the DBObject level – not the Entity level – as BlockTableRecords are drawable and responsible for drawing constant attribute definitions. This was the “aha!” that allowed me to get this bit working (along with some trial and error with SetAttributes() flags).
Here’s the code in action against a test project. When the PVE command toggles the overrule on, the lower-left viewport will show everything while the other three should show a variety of object types. You can control this very easily by adjusting the logic in the IsVisible() function in the code: right now it works on object type but you could easily store a list of ObjectIds and use that, instead.
And here’s the C# code that enables the PVE command:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;
using AcGi = Autodesk.AutoCAD.GraphicsInterface;
using AcDb = Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
namespace PerViewportEntityDisplay
{
public static class Extensions
{
/// <summary>
/// Performs an operation on all entities found in this database.
/// </summary>
/// <param name="f">Function performing an operation on an entity.</param>
/// <param name="includeAttribs">Also include attributes.</param>
/// <returns>The number of times the function returned true.</returns>
public static int ForEachEntity(
this Database db, Func<Entity, bool> f, bool includeAttribs = false
)
{
int count = 0;
using (var tr = db.TransactionManager.StartTransaction())
{
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
foreach (ObjectId btrId in bt)
{
var btr = (BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead);
foreach (ObjectId entId in btr)
{
var ent = tr.GetObject(entId, OpenMode.ForRead) as Entity;
if (ent != null && f(ent))
{
count++;
}
if (includeAttribs)
{
var br = ent as BlockReference;
if (br != null)
{
foreach (ObjectId arId in br.AttributeCollection)
{
var ar =
tr.GetObject(arId, OpenMode.ForRead)
as AttributeReference;
if (ar != null && f(ar))
{
count++;
}
}
}
}
}
}
tr.Commit();
}
return count;
}
public static void TouchAllEntities(this Database db, bool incAttbs = false)
{
// Invalidating the views doesn't help: we touch all the entities to
// make sure things get updated
db.ForEachEntity(
e =>
{
try
{
e.UpgradeOpen();
e.RecordGraphicsModified(true);
e.DowngradeOpen();
return true;
}
catch { }
return false;
},
incAttbs
);
}
}
// A class that derives from WorldDraw and encapsulates ViewportDraw
// (so we can capture and use WorldDraw geometry calls from ViewportDraw)
public class PassthroughDraw : WorldDraw
{
private PassthroughGeometry _pg;
private ViewportDraw _vd;
private bool _forceShaded = false;
// Constructor
public PassthroughDraw(ViewportDraw vd, bool forceShaded = false)
{
_vd = vd;
_forceShaded = forceShaded;
_pg = new PassthroughGeometry(vd.Geometry);
}
// WorldDraw protocol
public override WorldGeometry Geometry { get { return _pg; } }
// CommonDraw protocol
public override Context Context { get { return _vd.Context; } }
public override bool IsDragging { get { return _vd.IsDragging; } }
public override int NumberOfIsolines
{
get { return _vd.NumberOfIsolines; }
}
public override Geometry RawGeometry { get { return _vd.RawGeometry; } }
public override bool RegenAbort { get { return _vd.RegenAbort; } }
public override RegenType RegenType
{
get { return _forceShaded ? RegenType.ShadedDisplay : _vd.RegenType; }
}
public override SubEntityTraits SubEntityTraits
{
get { return _vd.SubEntityTraits; }
}
public override double Deviation(
DeviationType deviationType, Point3d pointOnCurve
)
{
return _vd.Deviation(deviationType, pointOnCurve);
}
}
// A class that derives from WorldGeometry and encapsulates ViewportGeometry
// (so we can capture and use WorldDraw geometry calls from ViewportDraw)
public class PassthroughGeometry : WorldGeometry
{
private ViewportGeometry _vg = null;
// Constructor
public PassthroughGeometry(ViewportGeometry vg)
{
_vg = vg;
}
// WorldGeometry protocol
public override void SetExtents(Extents3d extents) { }
public override void StartAttributesSegment() { }
// Geometry protocol
public override Matrix3d ModelToWorldTransform
{
get { return _vg.ModelToWorldTransform; }
}
public override Matrix3d WorldToModelTransform
{
get { return _vg.WorldToModelTransform; }
}
public override bool Circle(Point3d center, double radius, Vector3d normal)
{
return _vg.Circle(center, radius, normal);
}
public override bool Circle(
Point3d firstPoint, Point3d secondPoint, Point3d thirdPoint
)
{
return _vg.Circle(firstPoint, secondPoint, thirdPoint);
}
public override bool CircularArc(
Point3d start, Point3d point, Point3d endingPoint, ArcType arcType
)
{
return _vg.CircularArc(start, point, endingPoint, arcType);
}
public override bool CircularArc(
Point3d center, double radius, Vector3d normal, Vector3d startVector,
double sweepAngle, ArcType arcType
)
{
return _vg.CircularArc(
center, radius, normal, startVector, sweepAngle, arcType
);
}
public override bool Draw(Drawable value)
{
return _vg.Draw(value);
}
public override bool Edge(Curve2dCollection e)
{
return _vg.Edge(e);
}
public override bool EllipticalArc(
Point3d center, Vector3d normal, double majorAxisLength,
double minorAxisLength, double startDegreeInRads, double endDegreeInRads,
double tiltDegreeInRads, ArcType arcType
)
{
return
_vg.EllipticalArc(
center, normal, majorAxisLength, minorAxisLength, startDegreeInRads,
endDegreeInRads, tiltDegreeInRads, arcType
);
}
public override bool Image(
ImageBGRA32 imageSource, Point3d position, Vector3d u, Vector3d v
)
{
return _vg.Image(imageSource, position, u, v);
}
public override bool Image(
ImageBGRA32 imageSource, Point3d position, Vector3d u, Vector3d v,
TransparencyMode transparencyMode
)
{
return _vg.Image(imageSource, position, u, v, transparencyMode);
}
public override bool Mesh(
int rows, int columns, Point3dCollection points, EdgeData edgeData,
FaceData faceData, VertexData vertexData, bool bAutoGenerateNormals
)
{
return
_vg.Mesh(
rows, columns, points, edgeData, faceData, vertexData,
bAutoGenerateNormals
);
}
public override bool OwnerDraw(
GdiDrawObject gdiDrawObject, Point3d position, Vector3d u, Vector3d v
)
{
return _vg.OwnerDraw(gdiDrawObject, position, u, v);
}
public override bool Polygon(Point3dCollection points)
{
return _vg.Polygon(points);
}
public override bool Polyline(AcGi.Polyline polylineObj)
{
return _vg.Polyline(polylineObj);
}
public override bool Polyline(
AcDb.Polyline value, int fromIndex, int segments
)
{
return _vg.Polyline(value, fromIndex, segments);
}
public override bool Polyline(
Point3dCollection points, Vector3d normal, IntPtr subEntityMarker
)
{
return _vg.Polyline(points, normal, subEntityMarker);
}
public override bool Polypoint(
Point3dCollection points, Vector3dCollection normals,
IntPtrCollection subentityMarkers
)
{
return _vg.Polypoint(points, normals, subentityMarkers);
}
public override bool PolyPolygon(
UInt32Collection numPolygonPositions, Point3dCollection polygonPositions,
UInt32Collection numPolygonPoints, Point3dCollection polygonPoints,
EntityColorCollection outlineColors, LinetypeCollection outlineTypes,
EntityColorCollection fillColors, TransparencyCollection fillOpacities
)
{
return _vg.PolyPolygon(
numPolygonPoints, polygonPoints, numPolygonPoints, polygonPoints,
outlineColors, outlineTypes, fillColors, fillOpacities
);
}
public override bool PolyPolyline(PolylineCollection polylineCollection)
{
return _vg.PolyPolyline(polylineCollection);
}
public override void PopClipBoundary()
{
_vg.PopClipBoundary();
}
public override bool PopModelTransform()
{
return _vg.PopModelTransform();
}
public override bool PushClipBoundary(ClipBoundary boundary)
{
return _vg.PushClipBoundary(boundary);
}
public override bool PushModelTransform(Matrix3d matrix)
{
return _vg.PushModelTransform(matrix);
}
public override bool PushModelTransform(Vector3d normal)
{
return _vg.PushModelTransform(normal);
}
public override Matrix3d PushOrientationTransform(
OrientationBehavior behavior
)
{
return _vg.PushOrientationTransform(behavior);
}
public override Matrix3d PushPositionTransform(
PositionBehavior behavior, Point2d offset
)
{
return _vg.PushPositionTransform(behavior, offset);
}
public override Matrix3d PushPositionTransform(
PositionBehavior behavior, Point3d offset
)
{
return _vg.PushPositionTransform(behavior, offset);
}
public override Matrix3d PushScaleTransform(
ScaleBehavior behavior, Point2d extents
)
{
return _vg.PushScaleTransform(behavior, extents);
}
public override Matrix3d PushScaleTransform(
ScaleBehavior behavior, Point3d extents
)
{
return _vg.PushScaleTransform(behavior, extents);
}
public override bool Ray(Point3d point1, Point3d point2)
{
return _vg.Ray(point1, point2);
}
public override bool RowOfDots(int count, Point3d start, Vector3d step)
{
return _vg.RowOfDots(count, start, step);
}
public override bool Shell(
Point3dCollection points, IntegerCollection faces, EdgeData edgeData,
FaceData faceData, VertexData vertexData, bool bAutoGenerateNormals
)
{
// To avoid a null argument exception we need to transfer the faces
// across to a new collection
var faces2 = new IntegerCollection(faces.Count);
foreach (int face in faces) faces2.Add(face);
return
_vg.Shell(
points, faces2, edgeData, faceData, vertexData, bAutoGenerateNormals
);
}
public override bool Text(
Point3d position, Vector3d normal, Vector3d direction, string message,
bool raw, TextStyle textStyle
)
{
return _vg.Text(position, normal, direction, message, raw, textStyle);
}
public override bool Text(
Point3d position, Vector3d normal, Vector3d direction, double height,
double width, double oblique, string message
)
{
return
_vg.Text(position, normal, direction, height, width, oblique, message);
}
public override bool WorldLine(Point3d startPoint, Point3d endPoint)
{
return _vg.WorldLine(startPoint, endPoint);
}
public override bool Xline(Point3d point1, Point3d point2)
{
return _vg.Xline(point1, point2);
}
}
public class PerViewportEntityDisplayOverrule : DrawableOverrule
{
// We currently have a few issues to work around: solid geometry (including
// surfaces) doesn't have ViewportDraw called with the right RegenType.
// We need to force the first ViewportDraw call (of two, usually) to have
// RegenType of ShadedDisplay. This should only be an issue with 2016 and
// earlier.
// This collection tracks the ObjectIds of entities we've seen
// (we effectively toggle the "seen" flag on and off per entity)
private ObjectIdCollection _seen = new ObjectIdCollection();
// We also need to track which viewports are 2D vs. 3D
private Dictionary<int, bool> _is3D = new Dictionary<int, bool>();
// We'll return the same attributes from both SetAttributes and
// ViewportDrawLogicalFlags
private int GetAttributes(Drawable d, int flags)
{
// All entities need to be per-viewport
if (d is Entity)
{
flags |=
(int)(
DrawableAttributes.IsAnEntity |
DrawableAttributes.RegenTypeDependentGeometry |
DrawableAttributes.ViewDependentViewportDraw
);
}
if (d is BlockReference)
{
// Block references shouldn't have their compound flag set if
// containing attributes
if ((flags & (int)DrawableAttributes.IsCompoundObject) != 0)
{
flags ^= (int)DrawableAttributes.IsCompoundObject;
}
var br = (BlockReference)d;
if (br.AttributeCollection.Count > 0)
{
flags |= (int)DrawableAttributes.HasAttributes;
}
}
else if (d is BlockTableRecord)
{
// Block definitions are drawn per-viewport and may have attributes
flags |=
(int)(
DrawableAttributes.RegenTypeDependentGeometry |
DrawableAttributes.ViewDependentViewportDraw
);
var btr = (BlockTableRecord)d;
if (btr.HasAttributeDefinitions)
{
flags |= (int)DrawableAttributes.HasAttributes;
}
}
return flags;
}
public override int SetAttributes(Drawable d, DrawableTraits t)
{
return GetAttributes(d, base.SetAttributes(d, t));
}
public override int ViewportDrawLogicalFlags(Drawable d, ViewportDraw vd)
{
return GetAttributes(d, base.ViewportDrawLogicalFlags(d, vd));
}
// This is where you implement your logic to decide which objects to
// display. You could tag with XData or maintain your own in-memory
// structure indicating which entities belong where. We're going to
// decide it based purely on the type of the object, for simplicity
private bool IsVisible(Drawable d, int vp)
{
if (vp == 2)
return d is Curve;
if (vp == 3)
return d is Solid3d;
if (vp == 4)
return d is Solid3d || d is AcDb.Surface;
if (vp == 5)
return true;
return false;
}
// We periodically need to clear and regenerate the "seen" and "is 3D"
// information, as it can change
public void ResetShading(Document doc)
{
try
{
_seen.Clear();
PopulateVpData(doc);
}
catch { }
}
// Refresh our cache of per-viewport "is 3D" data
private void PopulateVpData(Document doc)
{
var gm = doc.GraphicsManager;
var db = doc.Database;
// Do we need to regen at the end?
var needRegen = false;
// Use an open/close transaction
using (var tr = db.TransactionManager.StartOpenCloseTransaction())
{
var vt =
(ViewportTable)tr.GetObject(db.ViewportTableId, OpenMode.ForRead);
foreach (ObjectId vtrId in vt)
{
var isVp3D = false;
var vpNeedsRegen = false;
var vtr =
(ViewportTableRecord)tr.GetObject(vtrId, OpenMode.ForRead);
// Check the viewport's visual style
if (vtr.VisualStyleId != ObjectId.Null)
{
// As an added feature we're making 3D Wireframe look 2D, too
var vs =
(DBVisualStyle)tr.GetObject(vtr.VisualStyleId, OpenMode.ForRead);
isVp3D =
(vs.Type != VisualStyleType.Wireframe2D &&
vs.Type != VisualStyleType.Wireframe3D);
}
// Only update the data if it has changed: this way we only regen
// when needed
if (_is3D.ContainsKey(vtr.Number))
{
if (_is3D[vtr.Number] != isVp3D)
{
// If the value for this viewport has changed, we're going
// to invalidate the GS cache and force a regen
_is3D[vtr.Number] = isVp3D;
vpNeedsRegen = true;
}
}
else
{
_is3D.Add(vtr.Number, isVp3D);
}
if (vpNeedsRegen)
{
// Invalidate the vieport's GS cache and trigger a full regen
var v = gm.GetCurrent3dAcGsView(vtr.Number);
if (v != null)
v.InvalidateCachedViewportGeometry();
needRegen = true;
}
}
tr.Commit();
}
// Now we regen, if needed
if (needRegen)
doc.Editor.Regen();
}
public bool Is3D(int vp)
{
return _is3D.ContainsKey(vp) ? _is3D[vp] : false;
}
public override bool WorldDraw(Drawable d, WorldDraw wd)
{
// Defer all calls to ViewportDraw
return false;
}
public override void ViewportDraw(Drawable d, ViewportDraw vd)
{
// There's an issue with viewportDraw for solids/surfaces...
// It should be called twice, once with ShadedDisplay and then
// once with StandardDisplay. In AutoCAD 2016 and before it gets
// called twice with StandardDisplay. So we tell the passthrough
// object to force to ShadedDisplay the first time a solid or
// surface gets drawn. All being well.
var forceShaded = false;
// Do we have a solid or a surface in a 3D viewport?
var in3D = Is3D(vd.Viewport.AcadWindowId);
if (in3D && (d is Solid3d || d is AcDb.Surface))
{
// Check whether we have its ID in our "seen" list
var e = (Entity)d;
if (_seen.Contains(e.ObjectId))
{
// If yes, remove it from the list
_seen.Remove(e.ObjectId);
}
else
{
// If no, force to ShadedDisplay (and add the object, so
// the next call won't be shaded)
forceShaded = true;
_seen.Add(e.ObjectId);
}
}
// We shouldn't be getting exceptions here under normal circumstances...
// You will want to add soem kind of logging or output for when something
// bad happens
try
{
// Check whether the object is visible in this viewport
if (IsVisible(d, vd.Viewport.AcadWindowId))
{
// If so, create our passthrough object (which will take WorldDraw
// calls and pass them through to our ViewportDraw object)
var pd = new PassthroughDraw(vd, forceShaded);
// If we're dealing with a block or an attribute definition -
// or we can't use WorldDraw in this scenario with our passthrough
// object - then go ahead and call ViewportDraw, directly
if (
d is BlockReference ||
d is AttributeDefinition ||
d is Light ||
!base.WorldDraw(d, pd)
)
{
base.ViewportDraw(d, vd);
}
}
}
catch {}
}
}
public class Commands
{
// Shared member variable to store our Overrule instance
private static PerViewportEntityDisplayOverrule _orule;
[CommandMethod("PVE")]
public static void ToggleOverrule()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return;
var db = doc.Database;
// Initialize overrule if it doesn't exist
if (_orule == null)
{
// Turn overruling on
_orule = new PerViewportEntityDisplayOverrule();
Overrule.AddOverrule(RXObject.GetClass(typeof(DBObject)), _orule, false);
Overrule.Overruling = true;
_orule.ResetShading(doc);
}
else
{
// Remove the overrule
Overrule.RemoveOverrule(RXObject.GetClass(typeof(DBObject)), _orule);
_orule.Dispose();
_orule = null;
doc.Editor.Command("_.REGENALL");
}
db.TouchAllEntities(true);
// This is a kludge: for the current document, reset the shading tracking
// at the end of each command. The tracking will hopefully not be
// needed post 2016
doc.CommandWillStart += (s, e) => { _orule.ResetShading((Document)s); };
doc.CommandEnded += (s, e) => { _orule.ResetShading((Document)s); };
}
}
}
I wrote the code in C# but the technique certainly remains valid for native ObjectARX, too (in fact using C++ would almost certainly avoid the Shell() marshalling problem).
As you can see from the code and the introduction to this post, there were quite a few issues to work through to get it working. It’s also very possible I’ve missed some. I’d certainly appreciate feedback as people try the code out and work through scenarios I haven’t tested, as I’m very keen to get a solid, general solution to this problem in place.
Update:
I made a few minor adjustments to better cope with entities on locked layers and not crash when dealing with Light objects. I expect more of these cases to crop up… please keep them coming!