Connectors Hand Out
Connectors Hand Out
1 Introduction
CET Designer can be described as a “Space planning solution for highly configurable
products.” The two critical elements of this statement, “space planning” and “highly configurable”
are both made much easier for the user by means of Connectors. At the most fundamental level,
Connectors are simply a ’handle’ that provide some kind of user interaction with the Snapper they
are owned by. They can be used to change configurations, add or remove components, or provide a
’relationship’ between disparate parts of a larger system. However, certain usages of Connectors
are so commonplace that they have already been integrated into the system. Tasks such as
stretching, reorienting, snapping and connecting are already a part of most Connectors and
Snappers.
There are four ’archetypes’ of Connectors that are commonly used:
Manipulators
· These are typically used to just allow the user to manipulate the Snapper. Stretching,
reorienting, etc.
· Does not allow any connections.
· The base Manipulator class is not a useable general-purpose class. Instead, the
Manipulator class should be extended before use.
ConnectPoints
· These are used when making connections from a single point. Think about the posts on a
Lego® brick.
· Allows a single connection.
· The base ConnectPoint class is not useable as a general-purpose class. However,
TransformSnapPoint, a subclass of ConnectPoint, is useable for this purpose.
ConnectLines
· These are used when making connections along an entire line. These are often used by
worksurface Snappers to allow pedestals to attach anywhere along the edge of the
surface.
· Allows multiple connections.
· The base ConnectLine class is a useable general-purpose class.
ConnectFaces
· These represent a flat area with allowable connections along its entire surface. A wall or
panel may use a ConnectFace to allow free placement across their entire surface.
· Allows multiple connections.
· The base ConnectFace class is not useable as a general-purpose class.
2
/**
* Update Connectors.
*/
public void updateConnectors() {
super();
frontLine.setEndPos( (w, 0, 0) );
rightLine.setPos( (w, 0, 0) );
rightLine.setEndPos( (w, d, 0) );
backLine.setPos( (w, d, 0) );
backLine.setEndPos( (0, d, 0) );
leftLine.setPos( (0, d, 0) );
leftLine.setEndPos( (0, 0, 0) );
heightSnap.setPos( (w/2, d/2, surfaceH) );
monitorStandSnap.setPos( (w/2, d, surfaceH) );
}
In certain situations, it makes sense to alter how Connectors appear. For example, it may be
desirable to have a Connector placed at one location, but appear at another. This would be due to
the fact that the actual location of a Connector has an effect on how Snappers are aligned and
connected to each other, but the visible location of the Connector affects what the user sees.
Worksurfaces often do this; they place their Connectors on the floor, but visually place them on the
surface edges.
To adjust the visible location of a Connector, the visiblePosition() method should be
overridden. The system will pass three arguments to this method: the Connector it is asking the
visible position of, the actual location of the Connector, and the Connector that the first Connector
is connected to. The last argument may by null if the specified Connector is not connected to
anything. This method should return the desired visible location of the Connector. Alternatively, the
visible location of the Connector can be adjusted for 2D or 3D views independantly. To accomplish
this, the visible2DPosition() and visible3DPosition() methods may be overriden (these
methods take the same arguments). An example of visiblePosition() can be seen in the code
sample below:
/**
* Connector visible position.
*/
public point visiblePosition(Connector mySnap, point defaultPos,
Connector attach) {
if (mySnap == leftLine or
mySnap == rightLine or
mySnap == backLine or
mySnap == frontLine)
return defaultPos + (0, 0, surfaceH);
if (mySnap == heightSnap)
return defaultPos + (0, 0, 8inch);
return super(..);
}
The visible orientation of a Connector may be altered as well, but this is less common. To
alter the shown orientation, the visibleRotation() method should be overriden. The method
signature is similar to that of visiblePosition(), with the exception that it passes the Connector
’s actual orientation as the second argument. As is the case with visiblePosition(),
visibleRotation() also has 2D and 3D exclusive varients: visible2DRotation() and
visible3DRotation().
It is also possible to make a Connector invisible to the user. When a Connector is invisible, it
still behaves like any other Connector. However, the user will be unable to click on it to stretch it or
disconnect it from another Connector. This is sometimes done for Connectors whose only purpose
is to recieve connections. To control whether or not a Connector is visible, the isVisible()
method should be overridden. The only argument to this method will be the Connector that the
3
system will query for visibility. Again, isVisible() has 2D and 3D varients: isVisibleIn2D() and
isVisibleIn3D(). An example of isVisible() can be seen in the code sample below:
/**
* Is the connector visible?
*/
public bool isVisible(Connector mySnap) {
if (mySnap == monitorStandSnap)
return false;
return super(..);
}
4 Alignment
The alignment of Snappers and their Connectors is typically initiated via an Animation.
However, before the Animation decides that two Snappers should be aligned, a series of steps are
taken:
1. The ConnectRules of the two applicable Connectors are compared to ensure that they are
compatible.
2. animationTrySnap() is invoked on the ’snap’ Snapper to adjust it (if neccessary) and
potentially block the alignment.
4
3. allowTrySnap() and allowTryAttach() are invoked to allow or disallow the alignment.
4. allowSnap() and allowAttach() are invoked to allow or disallow the alignment.
This is only a simplified overview of the alignment process. Behind the scenes, there are
other mechanisms at work that may be altered for additional customized behavior. However, they
are beyond the scope of this document. The steps above are described in detail in the sections that
follow.
5
This is not neccessary, but is often practical; putting the code in an init block ensures the rules
will be available when referenced, but saves start-up time.
public class DtDemoTableEdgeCType extends ConnectType { }
public class DtDemoTableAccessoryCType extends ConnectType { }
public class DtDemoTableMonitorStandCType extends ConnectType { }
init {
// Example 1: Symmetric
dtDemoMonitorStandRule = makeConnectRule("dtDemoMonitorStandRule",
DtDemoTableMonitorStandCType);
6
/**
* Animation try snap.
*/
public bool animationTrySnap(Connector mySnap, Connector attach,
line mouseLine, bool connect=true, bool undoable=false,
ModifyEnv env=null) {
if (?DtDemoTable table = attach.snapper) {
surfaceH = table.surfaceH;
invalidate();
}
return super(..);
}
During animationTrySnap() (in the call to super(..)), the system makes calls to
allowTrySnap() and allowTryAttach() on the ’snap’ Snapper and the ’attach’ Snapper,
respectively. This is why the call to super(..) is important; without it the system cannot continue
with the alignment. As with animationTrySnap(), these methods may be used to control
alignment. They each will recieve the applicable Connectors as arguments. Here, a call to
super(..) is not required if the Snapper is actively deciding to allow or disallow the alignment.
Returning values of true or false will allow or disallow the alignment.
After allowTrySnap() and allowTryAttach(), the system will then call allowSnap() and
allowAttach(). For the most part, these methods are identical in function to the methods in the
previous step. The difference is that these methods are used in both the alignment and the
connection phases; the previously mentioned methods are only involved in the alignment phase.
If all of these checks pass, the two Connectors and their Snappers will be placed in such a
way that they will be sufficiently aligned for a connection.
Default behavior for Connectors is to only consider themselves aligned to each other when
their positions are flush. However, in some situations it is desirable to have Connectors snap and
connect at an offset. For example, worksurfaces may mount at an offset on their back edge to
allow for cables to run behind them. The obvious answer would be to place the Connector itself at
a slight offset. The only problem with this is that it will cause all connections to that Connector to
be at an offset.
As an alternative, the connectOffset() method may be overriden. This method, when
overriden, may specify an offset to snap / connect at. The method is passed four arguments: the
Connector that the system is querying an offset for, the Connector that the system is attempting to
snap / connect to, and two flags snap and attach. Note that the last two arguments are no longer
implemented, and their values should be ignored. Since the method recieves the ’attach’ Connector,
this method may return situation specific offsets. For example, a worksurface may connect at an
offset from a wall, but flush to another worksurface. Note that the returned offset should be an
offset from the position of the Connector (this could be described as Connector local coordinates).
An example of connectOffset() may be seen in the code sample below.
/**
* Connection offset.
*/
public point connectOffset(Connector mySnap, Connector attach,
bool snapping=false, bool attaching=true) {
if (mySnap == backLine)
return (.5inch, 0, 0);
return super(..);
}
5 Connecting
As stated before, placement Animations will not actually connect two Snappers until the
user has completed the placement/alignment. After the Animation has completed, the system will
7
then check all of the Connectors on the placed Snapper to determine if they should be connected
to nearby Connectors. For any two pairs of Connectors, the following steps are taken to potentially
connect them:
1. Invoke connectableTo() on both Connectors. This method will check that Connectors:
o Are sufficiently aligned, based on position and orientation
o Have compatible ConnectRules
o Meet other miscellaneous criteria, often specific to the specific type of Connectors
involved
2. Invoke allowSnap() and allowAttach() on the owners of the Connectors
3. Invoke connect() on the Connectors to complete the connection
connectableTo() is overriden to behave slightly different for each of the various subclasses
of Connector. However, the steps it takes are typically similar. All Connectors will make
ConnectRule comparisons in this call. This check is carried out the same way as it is done during
the alignment phase; both ConnectRules are checked to see if they are compatible with the other.
All Connectors will check to be sure they are sufficiently aligned before the connection is
allowed. Connectors must have opposite orientations to be aligned. For example, a Connector that
is oriented down the positive X-axis of the drawing may only be paired with a Connector oriented
down the negative X-axis. The image below shows two pairs of Connectors such that the
Connectors with the green arrows are correctly aligned to allow a connection, and the Connectors
with the red arrows are misaligned. Connectors must also be aligned by their positions, but this will
mean different things to each archetype: ConnectPoints only require their positions to be aligned,
but ConnectLines accept positions anywhere along the line that they represent.
Once the Connectors are verified to be sufficiently aligned, the system will then call
allowSnap() and allowAttach() on the ’snap’ Snapper and the ’attach’ Snapper. Note that this is
the same method that is called during the alignment phase. For this reason, these methods serve
as control over both snapping and connecting; returning false from these methods will deny both.
Finally, the connect() methods are invoked on the ’snap’ and the ’attach’ to create the actual
connection. In sub-classes of Connectors it is possible to override these methods to make
additional, final checks before allowing the connection. This is, however, unusual; it is often easier
to inject checks and behavior at other stages of the process.
There are two methods that are invoked on Snappers when connections are made or broken:
connected() and disconnected(). These methods will recieve, as arguments, the two Connectors
involved in the event. These methods allow the Snapper to react to changes in its connections.
8
When overriding connected() and disconnected() there are two things to consider. These
methods cannot influence the connection or disconnection; they can only react to the change. To
influence the connection, steps from the previous sections should be used. There is no easy way to
influence a disconnection. Additionally, both Snappers involved in the event will recieve the
methods called, but the order that they are called is somewhat arbitrary, and no assumptions should
be made about the order they are called.
6 Stretching
Connectors are also used for stretching Snappers. Stretching is commonly used to resize
symbols, but can also be used in most situations where a click-and-drag interaction is desired. For
example, stretching could be used to adjust the elevation of wall-mounted storage.
Before a Connector may be used for stretching, the system must know that it is allowed to
do so. This is accomplished by overriding the stretchable() method. The system will pass to this
method a Connector that it wants to query as being stretchable. Returning values of true or false
will allow or dissalow that Connector to be stretched. Note that a Connector that is used for
creating connections may also be used for stretching. An example of the stretchable() method
can be seen in the code sample below.
/**
* Is the connector stretchable?
*/
public bool stretchable(Connector mySnap) {
if (mySnap == leftLine or
mySnap == rightLine or
mySnap == frontLine or
mySnap == backLine or
mySnap == heightSnap)
return true;
if (mySnap == monitorStandSnap)
return false;
return super(..);
}
With the default implementation, a stretch is carried out by a LineStretchAnimation. Note
that a different Animation may be used by overriding the stretchAnimation() method of Snapper
, however, the rest of this section assumes the default behavior.
The Animation will begin with calling the beginStretch() method on the Snapper that owns
the Connector being stretched. This is an ideal opportunity to append animationProperties. The
details of working with animationProperties are beyond the scope of this document, however,
examples of their usage can be seen in the next two code samples. animationProperties should
be used for almost all stretches; users have an expectation for that extra feedback and control to
be there.
9
/**
* Begin stretch.
*/
public void beginStretch(Connector mySnap) {
super(..);
if (mySnap == leftLine or mySnap == rightLine) {
animation.createAnimationProperties();
animationProperties.append3("w", $width, w.distance,
widthDomain.toDistanceSubSet());
animationProperties.finalize(["w"]);
} else if (mySnap == frontLine or mySnap == backLine) {
animation.createAnimationProperties();
animationProperties.append3("d", $depth, d.distance,
depthDomain.toDistanceSubSet());
animationProperties.finalize(["d"]);
} else if (mySnap == heightSnap) {
animation.createAnimationProperties();
animationProperties.append3("h", $depth, surfaceH.distance,
surfaceHeightDomain.toDistanceSubSet());
animationProperties.finalize(["h"]);
}
}
While the user is dragging their cursor around, the Animation will invoke the stretch()
method on the Snapper. This method will be passed three arguments: the Connector involved in
the stretch, the position of the cursor (in Snapper local coordinates), and an AnimationMouseInfo
that contains additional information about the state of the Animation. Note that the position of the
cursor will be bound to a line that is projected in the direction that the Connector is oriented. For
example, if the Connector is facing down the X-axis, the point being passed to stretch() will
always be a point on that axis that is closest to the user’s cursor.
The stretch() method should be overriden to react to the user’s input during the stretch.
The common case is to change dimensions or reposition the Snapper in this method, but there are
not many limitations on what else can be done. After any changes are made, this method should
return true or false to reflect whether or not a change was made. An example of the stretch()
method may be seen in the code sample below.
10
/**
* Stretch.
*/
public bool stretch(Connector mySnap, point mousePos,
AnimationMouseInfo mouseInfo) {
if (mySnap == leftLine or mySnap == rightLine) {
bool left = (mySnap == leftLine);
double newW = (left ? w - mousePos.x : mousePos.x);
newW = widthDomain.closest(newW).safeDouble;
newW = apLockedDistance("w", newW.distance);
if (newW != w) {
if (left)
move( (w-newW, 0, 0).transformed(rot) );
w = newW;
apSetDistance("w", w.distance);
updateConnectors();
updateAfterStretch();
build2D();
invalidate();
}
return true;
} else if (mySnap == frontLine or mySnap == backLine) {
bool front = (mySnap == frontLine);
double newD = (front ? d - mousePos.y : mousePos.y);
newD = depthDomain.closest(newD).safeDouble;
newD = apLockedDistance("d", newD.distance);
if (newD != d) {
if (front)
move( (0, d-newD, 0).transformed(rot));
d = newD;
apSetDistance("d", d.distance);
updateConnectors();
updateAfterStretch();
build2D();
invalidate();
}
return true;
} else if (mySnap == heightSnap) {
double newH = mousePos.z;
newH = surfaceHeightDomain.closest(newH).safeDouble;
newH = apLockedDistance("h", newH.distance);
if (surfaceH != newH) {
surfaceH = newH;
apSetDistance("h", surfaceH.distance);
updateConnectors();
updateAfterStretch();
build2D();
invalidate();
}
return true;
}
return super(..);
}
When the user has released the cursor, the Snapper will recieve a call to the endStretch()
method. This is an appropriate time to run any wrap-up code that needs to happen after a stretch.
Note that the Animation will automatically detect if Connectors have become aligned after it has
completed. If any Connectors have become aligned, the Animation will attempt to connect them
(while still obeying the normal rules and restrictions for creating connections).
11
A. ConnectLines
ConnectLines are an especially common form of Connector, and they have several traits
unique to them. The most obvious is that they are a line, and not a single point. This allows for
multiple connections to be made, as opposed to the ConnectPoint, which only allows a single
connection. With the default implementation, a ConnectLine will assign a section of its line to be
used by anything that is connected to it. Any unused space on the line is available for more
connections.
An example of a ConnectLine being constructed may be seen in the code sample below.
Note that, unlike a ConnectPoint or Manipulator, an orientation is not specified. This is due to the
fact that the orientation of a ConnectLine can be inferred by the line itself. The orientation will be
right-perpendicular to the direction of the line. The figure below demonstrates this with the green
lines indicating the ConnectLines (pointing from start position to end position), and the blue lines
indicating their direction for accepting connections.
rightLine = ConnectLine(this, (w, 0, 0), (w, d, 0));
rightLine.setRule(dtDemoSideToSideRule);
12
The allowLargerThanLine() method is used to indicate if another Snapper is allowed to
connect to the ConnectLine if the Snapper is larger than the ConnectLine. Note that this will not
come in to effect if the ConnectLine is not edge limited. The system will pass two arguments to
this method: the ConnectLine that will be recieving the connection, and the Connector that is
attempting to connect to it. The method should return true to indicate that the incoming Connector
should not be blocked from connection for being too large.
The connectLineConflicts() method is used to specify whether or not two Connectors (or
their Snappers) are “collidable” with each other on the same ConnectLine. This can be used to
allow Connectors to overlap on the ConnectLine. The example code below demonstrates
connectLineConflicts() being used to keep an under table pedestal and an office chair from
blocking each other. This allows both pedestals and chairs to connect to the table, without the need
for separate ConnectLines.
/**
* Connect line conflicts.
*/
public bool connectLineConflicts(ConnectLine myLine, Connector attach0,
Connector attach1) {
if ((attach0.snapper.DtDemoChair and attach1.snapper.DtDemoPedestal) or
(attach0.snapper.DtDemoPedestal and attach1.snapper.DtDemoChair))
return false;
return super(..);
}
connectLineAligns() is used to help the user place a Snapper at certain positions on the
ConnectLine. This method should return a tuple of three bools, indicating that the system should
help the user place the incoming Snapper at the left, center, or right positions. For example, if this
method returns <false, true, true>, the system will help the user place the Snapper at the
center or right position (depending on which is closer to the mouse cursor). If this method returns a
tuple of all false, the system will not help the user, and the incoming Snapper may be ’free-placed’
along all available positions on the ConnectLine.
connectLineAligns() will be passed three arguments: the ConnectLine that is recieving the
connection, the Connector that is attempting to connect to the ConnectLine, and the position of
the user’s cursor, expressed as a distance from the ConnectLine’s start position. The default
implementation returns a value of <true, true, true>, indicating that the system should help the
user place the Snapper at the left, right, or center position. In the code sample below, the
connectLineAligns() method is overriden to center anything connecting to the left or right side of
the Snapper, but free place anything connecting to the back.
13
/**
* Align a 'snap' to an 'attach' ConnectLine.
*/
public <bool, bool, bool> connectLineAligns(ConnectLine mySnap,
Connector attach, double defaultPos) {
if (mySnap == leftLine or mySnap == rightLine)
return <false, true, false>;
if (mySnap == backLine)
return <false, false, false>;
return super(..);
}
connectLineAlignConnector() serves the same purpose as connectLineAligns(), but
with more freedom. This method is passed the same arguments as connectLineAligns(), but
instead of returning a tuple, this method should return a desired position on the ConnectLine,
expressed as a distance from the start position. Note that this method’s default implementation will
call connectLineAligns(), so typically, overriding both is unnecessary.
Last, there is the snapAllocSize() method, which specifies how much space the Snapper
should consume when being connected to a ConnectLine. With the default implementation, the
Snapper’s size is assumed to be the size of the snapping ConnectLine (if it is a ConnectLine), or a
size based on the systemLocalBound(). When overridden, this method will be passed three
arguments: the Connector that is attempting to snap to the ConnectLine, the Connector this is
being snapped to (the argument is a Connector, but it will always be a ConnectLine), and the
nearest Connector on the ConnectLine (if any). The returned value should be a tuple of two
doubles, representing the size of the Snapper before and after the point of connection.
14