Today is The Building Coder's ninth birthday.
The first welcome post was published August 22, 2008.
We'll celebrate by discussing the pretty fundamental issue of XYZ
points versus vectors, and how to distinguish different points:
This question was raised in
the Revit API discussion forum thread
on XYZ
question:
Question: I find the XYZ
class very confusing because it can be a point or a vector.
Does anybody know any good guide on how to use them? Or some documentation.
Also, I have a specific question: how does one extract start and end points of a XYZ vector?
Answer: The documentation is right there where it belongs, in the Revit API help file RevitAPI.chm
section on the XYZ
class.
You are perfectly right, the XYZ class can represent either a point or a vector, depending on how you use it.
If you have a XYZ
object and run an angle method, you're in fact handling a vector information... but if you just get a coordinate, then a point.
In some cases, a vector doesn't have a start or end point, for instance, if it is used to represent a pure direction, not a line. If you need a line, use the Curve/Line object.
Bobby Jones suggested rolling your own Point
and Vector
wrappers around it, making your code much easier to maintain.
The XYZ
class defines methods that are specific to the concept of both points and vectors. Sometimes, it is difficult to know which concept it is dealing with at any given time.
Another benefit of providing your own point and vector wrappers is that you can limit their interface to only methods that make sense.
The following code compiles, but none of it makes any sense:
void PointAndVectorExample( Line revitLine ) { var o = revitLine.Origin; var whatIstheLengthOfaPoint = o.GetLength(); var howIsaPointaUnitLength = o.IsUnitLength(); var lineDirection = revitLine.Direction; var whatDoesThisRepresent = lineDirection.CrossProduct( o ); var thisDoesntMakeSense = lineDirection.DistanceTo( o ); }
Here are some PARTIAL Point3D
and Vector3D
classes to give you the idea, and some helper extensions to make them easier to use. You can take these and wrap the appropriate XYZ
methods for each as well as define additional methods of your own. And even implement additional interfaces, such as IEquatable
.
public class Point3D { public Point3D( XYZ revitXyz ) { XYZ = revitXyz; } public Point3D() : this( XYZ.Zero ) { } public Point3D( double x, double y, double z ) : this( new XYZ( x, y, z ) ) { } public XYZ XYZ { get; private set; } public double X => XYZ.X; public double Y => XYZ.Y; public double Z => XYZ.Z; public double DistanceTo( Point3D source ) { return XYZ.DistanceTo( source.XYZ ); } public Point3D Add( Vector3D source ) { return new Point3D( XYZ.Add( source.XYZ ) ); } public static Point3D operator +( Point3D point, Vector3D vector ) { return point.Add( vector ); } public override string ToString() { return XYZ.ToString(); } public static Point3D Zero => new Point3D( XYZ.Zero ); } public class Vector3D { public Vector3D( XYZ revitXyz ) { XYZ = revitXyz; } public Vector3D() : this( XYZ.Zero ) { } public Vector3D( double x, double y, double z ) : this( new XYZ( x, y, z ) ) { } public XYZ XYZ { get; private set; } public double X => XYZ.X; public double Y => XYZ.Y; public double Z => XYZ.Z; public Vector3D CrossProduct( Vector3D source ) { return new Vector3D( XYZ.CrossProduct( source.XYZ ) ); } public double GetLength() { return XYZ.GetLength(); } public override string ToString() { return XYZ.ToString(); } public static Vector3D BasisX => new Vector3D( XYZ.BasisX ); public static Vector3D BasisY => new Vector3D( XYZ.BasisY ); public static Vector3D BasisZ => new Vector3D( XYZ.BasisZ ); } public static class XYZExtensions { public static Point3D ToPoint3D( this XYZ revitXyz ) { return new Point3D( revitXyz ); } public static Vector3D ToVector3D( this XYZ revitXyz ) { return new Vector3D( revitXyz ); } } public static class LineExtensions { public static Vector3D Direction( this Line revitLine ) { return new Vector3D( revitLine.Direction ); } public static Point3D Origin( this Line revitLine ) { return new Point3D( revitLine.Origin ); } }
Many thanks to Bobby for sharing this!
Another Revit API discussion forum thread
that Bobby also helped out with deals
with Distinct XYZ
:
Question: I have a list of XYZ
points obtained from MEP connectors.
How can I clean it of duplicates, i.e., eliminate points with identical coordinates?
I am trying to do it like this:
var distinctElementConnectors
= MyMepUtils.GetALLConnectors( elements )
.Where( c => c.IsConnected )
.Distinct( c => c.Origin )
.ToHashSet();
The call to Distinct(c => c.Origin)
doesn't work, because Distinct
doesn't know how to compare XYZs (or does it?).
Answer: You are absolutely correct:
The .NET API does not have any built-in mechanism to compare the Revit API XYZ
objects.
However, it is easy to implement, and I have done so several times in different discussion published by The Building Coder.
Here are the first and last mentions so far:
Furthermore, you can take a look
at The Building Coder samples class
XyzEqualityComparer
, defined there in three different modules.
Another direction to go, again suggested by Bobby, assuming you implemented your own Point3D
and Vector3D
wrapper classes, is to have those classes implement the IEquatable
interface.
Here's a shell of the Point3D
class showing an implementation:
public class Point3D : IEquatable<Point3D> { public Point3D( XYZ revitXyz ) { XYZ = revitXyz; } public XYZ XYZ { get; } public double X => XYZ.X; public double Y => XYZ.Y; public double Z => XYZ.Z; public bool Equals( Point3D other ) { if( ReferenceEquals( other, null ) ) return false; if( ReferenceEquals( this, other ) ) return true; return X.IsAlmostEqualTo( other.X ) && Y.IsAlmostEqualTo( other.Y ) && Z.IsAlmostEqualTo( other.Z ); } public override bool Equals( object obj ) { if( ReferenceEquals( null, obj ) ) return false; if( ReferenceEquals( this, obj ) ) return true; if( obj.GetType() != GetType() ) return false; return Equals( (Point3D) obj ); } public override int GetHashCode() { return Tuple.Create( Math.Round( X, 10 ), Math.Round( Y, 10 ), Math.Round( Z, 10 ) ). GetHashCode(); } }
Storing Point3D
instances in a hashset will give you your distinct set of points.
var distinctElementConnectors
= MyMepUtils.GetALLConnectors( elements )
.Where( c => c.IsConnected )
.Select( c => c.Origin.ToPoint3D() )
.ToHashSet();
Yet another solution to address this directly is to define a comparer class for native Revit API Connector
objects, such as the ConnectorXyzComparer
one provided in
at The Building Coder samples Util.cs module:
/// <summary> /// Compare Connector objects based on their location point. /// </summary> public class ConnectorXyzComparer : IEqualityComparer<Connector> { public bool Equals( Connector x, Connector y ) { return null != x && null != y && IsEqual( x.Origin, y.Origin ); } public int GetHashCode( Connector x ) { return HashString( x.Origin ).GetHashCode(); } } /// <summary> /// Get distinct connectors from a set of MEP elements. /// </summary> public static HashSet<Connector> GetDistinctConnectors( List<Connector> cons ) { return cons.Distinct( new ConnectorXyzComparer() ) .ToHashSet(); }
I implemented that in The Building Coder samples release 2018.0.134.1.
Here is the diff to the preceding release.
Thanks again to Bobby, and Happy Birthday coding, everybody!