PathEditorImpl.mesa
Written by Darlene Plebon on August 31, 1983 11:24 am
Routines providing a client interface to the Path Editor.
DIRECTORY
PathEditor,
PEAlgebra USING [ClosestPointOnLineSegment, DistanceSquared],
PEBezier USING [Bezier, BezierToSegment, SegmentToBezier, SplitBezier],
PEConstraints USING [Constrain, ContinuousAt],
PEDisplay USING [DrawControlPoints, DrawSegment, DrawSegmentAndVertices, DrawVertex, DrawVertices],
PEFileOps USING [ReadTrajectoryFile, WriteTrajectoryFile],
PEHitTest USING [SegmentHitTest, TrajectoryHitTest, VertexHitTest],
PERefresh USING [CreateRefreshProcess, DestroyRefreshProcess, DisableSegmentRefresh, DisableTrajectoryRefresh, EnableSegmentRefresh, NewRefreshData, RefreshData, RequestRefresh],
PETrajectoryOps USING [FollowingVertex, ForAllSegments, ForAllTrajectories, ForAllVertices, FreeSegmentList, FreeTrajectoryList, InsertSegment, InsertTrajectory, InsertVertex, IsAKnot, IsAControlPoint, IsFirstSegment, IsLastSegment, LastSegmentNode, LastTrajectoryNode, LastVertexNode, PrecedingVertex, RemoveSegment, RemoveTrajectory, RemoveVertex, SegmentProc, SwapSegments, TrajectoryProc, VertexListAppend, VertexListLength, VertexListNthElement, VertexProc],
PETypes,
PEViewer USING [BuildViewer, ButtonProc, DrawInViewer, DrawProc, QuitProc, RedrawProc],
Rope USING [ROPE],
ViewerClasses USING [Viewer];
PathEditorImpl:
CEDAR
PROGRAM
IMPORTS PEAlgebra, PEBezier, PEConstraints, PEDisplay, PEFileOps, PEHitTest, PERefresh, PETrajectoryOps, PEViewer
EXPORTS PathEditor =
BEGIN OPEN PathEditor, PEAlgebra, PEBezier, PEConstraints, PEDisplay, PEFileOps, PEHitTest, PERefresh, PETrajectoryOps, PETypes, PEViewer;
Handle: TYPE = REF PathEditorRec;
PathEditorRec:
TYPE =
RECORD [
refreshData: RefreshData ← NIL, -- data for refresh process
buttonProc: ButtonHitProc ← NIL, -- client's button procedure
redrawProc: DrawProc ← NIL, -- client's redraw procedure
pathViewer: ViewerClasses.Viewer, -- path editor viewer
trajectoryList: TrajectoryList ← NIL, -- list of segments in the path
newTrajectory: BOOLEAN ← TRUE, -- create a new trajectory?
activeTrajectory: TrajectoryNode ← NIL, -- trajectory currently being manipulated
activeVertex: VertexNode ← NIL, -- vertex currently being manipulated
activeSegment: SegmentNode ← NIL, -- segment containing activeVertex
splitSegment: SegmentNode ← NIL, -- segment being split by a vertex addition
splitReplacements: SegmentNode ← NIL, -- replacement segments for segment being split
vertexHit:
RECORD [
-- cache for hits in vertex hit testing
hitCached: BOOLEAN ← FALSE,
where: Point,
closeToVertex: BOOLEAN,
vertexType: VertexType,
vertex: Point
],
curveHit:
RECORD [
-- cache for hits in curve hit testing
hitCached: BOOLEAN ← FALSE,
where: Point,
closeToCurve: BOOLEAN,
curveType: CurveType,
p0, p1, p2, p3: Point,
hitPoint: Point
]
];
Routines which operate on the entire image.
CreatePathViewer:
PUBLIC
PROCEDURE [name: Rope.
ROPE, menuLabels:
LIST
OF MenuLabel, buttonProc: ButtonHitProc ←
NIL, redrawProc: DrawProc ←
NIL]
RETURNS [pathData: PathData] = {
This routine creates a viewer for the Path Editor. The viewer is created with the supplied name and menu labels. Client supplied routines are called to process button hits and when the entire viewer must be redrawn for some reason. This routine must be called before any others. It returns a pointer to Path Editor data which must be passed in all subsequent calls from the client.
handle: Handle← NEW[PathEditorRec];
handle.buttonProc ← buttonProc;
handle.redrawProc ← redrawProc;
handle.pathViewer ← BuildViewer[
name: name,
menuLabels: menuLabels,
clientData: handle,
redrawProc: DoRedraw,
quitProc: Quit,
buttonProc: ButtonHit ];
handle.refreshData ← CreateRefreshProcess[handle.pathViewer, handle.redrawProc];
pathData ← handle;
};
DoRedraw: RedrawProc = {
This routine handles re-draw requests from the PEViewer package.
handle: Handle ← NARROW[clientData];
Redraw[pathData: clientData, erase: TRUE];
};
Quit: QuitProc = {
This routine cleans up upon program termination.
handle: Handle ← NARROW[clientData];
DestroyRefreshProcess[handle.refreshData];
Break all the back links.
FreeTrajectoryList[handle.trajectoryList];
};
ButtonHit: ButtonProc = {
This routine handles menu and mouse button hits in the Path Editor viewer.
handle: Handle ← NARROW[clientData];
IF handle.buttonProc # NIL THEN handle.buttonProc[event: event, x: x, y: y];
};
DrawInPathViewer:
PUBLIC
PROCEDURE [pathData: PathData, drawProc: DrawProc] = {
This routine calls the client supplied routine with the Path Editor viewer's context so it can draw in the Path Editor's viewer.
handle: Handle ← NARROW[pathData];
DrawInViewer[handle.pathViewer, drawProc];
};
Redraw:
PUBLIC
PROCEDURE [pathData: PathData, erase:
BOOLEAN ← FALSE] = {
This routine redraws the entire image. The viewer may be erased first, if desired.
handle: Handle ← NARROW[pathData];
RequestRefresh[handle.refreshData, erase];
};
FlushImage:
PUBLIC PROCEDURE [pathData: PathData] = {
This routine flushes the contents of the image, so the client can start over.
handle: Handle ← NARROW[pathData];
DoDrawTrajectory: TrajectoryProc = {
DoDrawSegment: SegmentProc = {
DrawSegment[pathViewer: handle.pathViewer, segment: s.first, undo: TRUE];
IF t = handle.activeTrajectory THEN DrawVertices[pathViewer: handle.pathViewer, segment: s.first, undo: TRUE];
};
ForAllSegments[t.first.segments, DoDrawSegment];
};
ForAllTrajectories[handle.trajectoryList, DoDrawTrajectory];
FreeTrajectoryList[handle.trajectoryList];
handle.trajectoryList ← NIL;
NewTrajectory[pathData];
InvalidateHitCache[handle];
};
Routines which operate on entire trajectories.
NewTrajectory:
PUBLIC PROCEDURE [pathData: PathData] = {
This routine creates a new trajectory and makes it the active trajectory. The trajectory is not actually created until an operation is performed which gives it some contents (e.g. AddKnotToFront). That is this routine indicates the intent to start a new trajectory.
handle: Handle ← NARROW[pathData];
handle.newTrajectory ← TRUE;
DoSetActiveTrajectory[handle, NIL];
};
SetActiveTrajectory:
PUBLIC PROCEDURE [pathData: PathData, where: Point]
RETURNS [activeTrajectorySet:
BOOLEAN, hitPoint: Point] = {
This routine makes the trajectory which is closest to the specified point the active trajectory. A trajectory must be made active before its contents can be edited. When a trajectory is first set active, the active vertex or active segment are unset (i.e. NIL). The specified position must be "close to" (i.e. whithin a certain minimum distance of) a trajectory in order for the operation to be carried out. This routine returns an indication of whether the active trajectory was in fact set and the position of the point on the active trajectory which is closest to the specified point.
handle: Handle ← NARROW[pathData];
hitTrajectory: TrajectoryNode;
[hitTrajectory, hitPoint] ← TrajectoryHitTest[handle.trajectoryList, where];
DoSetActiveTrajectory[handle, hitTrajectory];
activeTrajectorySet ← hitTrajectory # NIL;
};
DoSetActiveTrajectory:
PROCEDURE [handle: Handle, activeTrajectory: TrajectoryNode] = {
This routine sets the active trajectory to be the specified trajectory node. This routine also unsets the active vertex and active segment (i.e. sets them to NIL).
DrawAsBackground: SegmentProc = {
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: s.first, undo: TRUE];
DrawSegment[pathViewer: handle.pathViewer, segment: s.first, background: TRUE];
};
DrawAsForeground: SegmentProc = {
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: s.first, background: FALSE];
};
DoSetActiveVertex[handle, NIL, NIL];
IF handle.activeTrajectory # activeTrajectory
THEN {
NewRefreshData[handle.refreshData, handle.trajectoryList, activeTrajectory];
IF handle.activeTrajectory #
NIL
THEN
ForAllSegments[handle.activeTrajectory.first.segments, DrawAsBackground];
handle.activeTrajectory ← activeTrajectory;
IF handle.activeTrajectory #
NIL
THEN
ForAllSegments[handle.activeTrajectory.first.segments, DrawAsForeground];
RequestRefresh[handle.refreshData];
};
};
TrajectoryIsEmpty:
PUBLIC PROCEDURE [pathData: PathData]
RETURNS [empty:
BOOLEAN] = {
This routine determines if the active trajectory currently has any contents.
handle: Handle ← NARROW[pathData];
RETURN [handle.activeTrajectory = NIL OR handle.activeTrajectory.first.segments = NIL];
};
DeleteActiveTrajectory:
PUBLIC PROCEDURE [pathData: PathData] = {
This routine deletes the active trajectory.
handle: Handle ← NARROW[pathData];
UnDraw: SegmentProc = {
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: s.first, undo: TRUE];
};
DisableTrajectoryRefresh[handle.activeTrajectory.first];
ForAllSegments[handle.activeTrajectory.first.segments, UnDraw];
[handle.trajectoryList,] ← RemoveTrajectory[handle.trajectoryList, handle.activeTrajectory];
DoSetActiveTrajectory[handle, NIL];
};
EnumerateTrajectories:
PUBLIC PROCEDURE [pathData: PathData, moveToProc: MoveToProc, lineToProc: LineToProc, curveToProc: CurveToProc] = {
This routine enumerates all the segments in the all the trajectories. Client specified routines are called for each line segment, and bezier curve in each trajectory. The moveToProc is called for the first point (FP) of each trajectory.
handle: Handle ← NARROW[pathData];
EnumerateTrajectory: TrajectoryProc = {
handle.activeTrajectory ← t;
EnumerateActiveTrajectory[pathData, moveToProc, lineToProc, curveToProc];
};
saveActiveTrajectory: TrajectoryNode ← handle.activeTrajectory;
ForAllTrajectories[handle.trajectoryList, EnumerateTrajectory];
handle.activeTrajectory ← saveActiveTrajectory;
};
Routines which operate on (the contents of) the active trajectory.
AddKnotToFront:
PUBLIC PROCEDURE [pathData: PathData, position: Point] = {
This routine extends the active trajectory by adding a knot (and thus a segment) to its front end.
handle: Handle ← NARROW[pathData];
knot: Vertex ← NEW[VertexRec ← [point: position]];
newSegment: Segment ← NEW[SegmentRec ← [type: moveTo]];
newTrajectory: Trajectory;
IF handle.activeTrajectory =
NIL
AND handle.newTrajectory
THEN {
newTrajectory ← NEW[TrajectoryRec];
[handle.trajectoryList, handle.activeTrajectory] ← InsertTrajectory[handle.trajectoryList, newTrajectory, NIL];
handle.newTrajectory ← FALSE;
};
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.activeTrajectory.first.segments #
NIL
THEN {
DisableSegmentRefresh[handle.activeTrajectory.first.segments.first];
handle.activeTrajectory.first.segments.first.type ← curveTo;
};
[newSegment.vertices,] ← InsertVertex[newSegment.vertices, knot, NIL];
[handle.activeTrajectory.first.segments,] ← InsertSegment[handle.activeTrajectory.first.segments, newSegment, NIL];
Constrain[pathViewer: handle.pathViewer, vertex: handle.activeTrajectory.first.segments.first.vertices, segment: handle.activeTrajectory.first.segments, newPosition: position];
DoSetActiveVertex[handle, handle.activeTrajectory.first.segments.first.vertices, handle.activeTrajectory.first.segments];
InvalidateHitCache[handle];
};
AddCubicToFront:
PUBLIC PROCEDURE [pathData: PathData, cp1, cp2, knot: Point] = {
This routine adds a cubic curve segment to the front of the active trajectory. The cubic is defined by the bezier vertices: knot, cp2, cp1, FP, where FP is the first knot in the original path.
handle: Handle ← NARROW[pathData];
knotVertex: Vertex ← NEW[VertexRec ← [point: knot]];
cp1Vertex: Vertex ← NEW[VertexRec ← [point: cp1]];
cp2Vertex: Vertex ← NEW[VertexRec ← [point: cp2]];
newSegment: Segment ← NEW[SegmentRec ← [type: moveTo]];
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.activeTrajectory.first.segments #
NIL
THEN {
DisableSegmentRefresh[handle.activeTrajectory.first.segments.first];
handle.activeTrajectory.first.segments.first.type ← curveTo;
[handle.activeTrajectory.first.segments.first.vertices,] ← InsertVertex[handle.activeTrajectory.first.segments.first.vertices, cp1Vertex, NIL];
[handle.activeTrajectory.first.segments.first.vertices,] ← InsertVertex[handle.activeTrajectory.first.segments.first.vertices, cp2Vertex, NIL];
DrawVertices[handle.pathViewer, handle.activeTrajectory.first.segments.first];
[newSegment.vertices,] ← InsertVertex[newSegment.vertices, knotVertex, NIL];
[handle.activeTrajectory.first.segments,] ← InsertSegment[handle.activeTrajectory.first.segments, newSegment, NIL];
Constrain[pathViewer: handle.pathViewer, vertex: handle.activeTrajectory.first.segments.rest.first.vertices.rest, segment: handle.activeTrajectory.first.segments.rest, newPosition: cp1];
EnableSegmentRefresh[handle.activeTrajectory.first.segments.rest.first];
};
DoSetActiveVertex[handle, NIL, NIL];
InvalidateHitCache[handle];
};
AddKnotToRear:
PUBLIC PROCEDURE [pathData: PathData, position: Point] = {
This routine extends the active trajectory by adding a knot (and thus a segment) to its rear end.
handle: Handle ← NARROW[pathData];
knot: Vertex ← NEW[VertexRec ← [point: position]];
newSegment: Segment ← NEW[SegmentRec];
newSegmentNode: SegmentNode;
IF handle.activeTrajectory = NIL OR handle.activeTrajectory.first.segments = NIL THEN AddKnotToFront[pathData, position]
ELSE {
[newSegment.vertices,] ← InsertVertex[newSegment.vertices, knot, NIL];
[handle.activeTrajectory.first.segments, newSegmentNode] ← InsertSegment[handle.activeTrajectory.first.segments, newSegment, LastSegmentNode[handle.activeTrajectory.first.segments]];
Constrain[pathViewer: handle.pathViewer, vertex: newSegment.vertices, segment: newSegmentNode, newPosition: position];
DoSetActiveVertex[handle, newSegment.vertices, newSegmentNode];
InvalidateHitCache[handle];
};
};
AddCubicToRear:
PUBLIC PROCEDURE [pathData: PathData, cp1, cp2, knot: Point] = {
This routine adds a cubic curve segment to the rear (end) of the active trajectory. The cubic is defined by the bezier vertices: LP, cp1, cp2, knot, where LP is the last knot in the original path.
handle: Handle ← NARROW[pathData];
knotVertex: Vertex ← NEW[VertexRec ← [point: knot]];
cp1Vertex: Vertex ← NEW[VertexRec ← [point: cp1]];
cp2Vertex: Vertex ← NEW[VertexRec ← [point: cp2]];
newSegment: Segment ← NEW[SegmentRec ← [type: moveTo]];
newSegmentNode: SegmentNode;
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.activeTrajectory.first.segments #
NIL
THEN {
[newSegment.vertices,] ← InsertVertex[newSegment.vertices, knotVertex, NIL];
[newSegment.vertices,] ← InsertVertex[newSegment.vertices, cp2Vertex, NIL];
[newSegment.vertices,] ← InsertVertex[newSegment.vertices, cp1Vertex, NIL];
DrawVertices[handle.pathViewer, newSegment];
[handle.activeTrajectory.first.segments, newSegmentNode] ← InsertSegment[handle.activeTrajectory.first.segments, newSegment, LastSegmentNode[handle.activeTrajectory.first.segments]];
Constrain[pathViewer: handle.pathViewer, vertex: newSegment.vertices, segment: newSegmentNode, newPosition: cp1];
};
DoSetActiveVertex[handle, NIL, NIL];
InvalidateHitCache[handle];
};
SplitSegment:
PUBLIC PROCEDURE [pathData: PathData, where: Point] = {
This routine splits the segment in the active trajectory which is nearest the specified position at the point which is closest to the specified position. The specified position must be "close to" (i.e. whithin a certain minimum distance of) a segment of the active trajectory in order for the operation to be carried out. This routine also updates the display to reflect the two new segments and saves appropriate information for reversing the action. A call to this routine may be followed by zero or more calls to AdjustSegmentSplit. Finally, ConfirmSegmentSplit must be called to clean up the display.
handle: Handle ← NARROW[pathData];
segment: SegmentNode;
point: Point;
t: REAL;
originalBezier: Bezier;
b1, b2: Bezier;
IF handle.activeTrajectory = NIL THEN RETURN;
[segment, point, t] ← SegmentHitTest[handle.activeTrajectory.first.segments, where];
originalBezier ← SegmentToBezier[segment.first];
[b1, b2] ← SplitBezier[originalBezier, t];
[handle.splitReplacements,] ← InsertSegment[NIL, BezierToSegment[b1], NIL];
handle.splitReplacements.first.fp ← segment.first.fp;
[handle.splitReplacements,] ← InsertSegment[handle.splitReplacements, BezierToSegment[b2], handle.splitReplacements];
handle.splitReplacements.rest.first.fp ← VertexListNthElement[handle.splitReplacements.first.vertices, -1];
handle.splitReplacements.rest.first.fp.fixed ← TRUE;
DisableSegmentRefresh[handle.splitReplacements.first];
DisableSegmentRefresh[handle.splitReplacements.rest.first];
[handle.activeTrajectory.first.segments, handle.splitSegment] ← SwapSegments[handle.activeTrajectory.first.segments, handle.splitReplacements, segment];
DrawControlPoints[pathViewer: handle.pathViewer, segment: segment.first, undo: TRUE];
DrawVertices[pathViewer: handle.pathViewer, segment: handle.splitReplacements.first];
DrawControlPoints[pathViewer: handle.pathViewer, segment: handle.splitReplacements.rest.first];
DoSetActiveVertex[handle, NIL, NIL];
InvalidateHitCache[handle];
};
AdjustSegmentSplit:
PUBLIC PROCEDURE [pathData: PathData, where: Point] = {
This routine moves the knot at which a bezier segment is split into two segments.
handle: Handle ← NARROW[pathData];
hitSegment: SegmentNode;
hitPoint: Point;
hitT: REAL;
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.splitSegment #
NIL
THEN {
[handle.activeTrajectory.first.segments, handle.splitReplacements] ← SwapSegments[handle.activeTrajectory.first.segments, handle.splitSegment, handle.splitReplacements, 2];
DrawVertices[pathViewer: handle.pathViewer, segment: handle.splitReplacements.first, undo: TRUE];
DrawControlPoints[pathViewer: handle.pathViewer, segment: handle.splitReplacements.rest.first, undo: TRUE];
DrawSegment[pathViewer: handle.pathViewer, segment: handle.splitSegment.first];
};
[hitSegment, hitPoint, hitT] ← SegmentHitTest[handle.activeTrajectory.first.segments, where];
IF handle.splitSegment #
NIL
THEN {
IF hitSegment # handle.splitSegment
THEN {
DrawVertices[pathViewer: handle.pathViewer, segment: handle.splitReplacements.first, undo: TRUE];
DrawControlPoints[pathViewer: handle.pathViewer, segment: handle.splitReplacements.rest.first, undo: TRUE];
DrawControlPoints[pathViewer: handle.pathViewer, segment: handle.splitSegment.first];
EnableSegmentRefresh[segment: handle.splitSegment.first];
};
FreeSegmentList[handle.splitReplacements];
handle.splitReplacements ← NIL;
};
IF hitSegment = NIL THEN handle.splitSegment ← NIL
ELSE SplitSegment[pathData: pathData, where: where];
DoRequestRefresh[handle];
DoSetActiveVertex[handle, NIL, NIL];
InvalidateHitCache[handle];
};
ConfirmSegmentSplit:
PUBLIC PROCEDURE[pathData: PathData] = {
This routine confirms a segment split. That is, it makes it permanent. This routine basically cleans up the display a bit.
handle: Handle ← NARROW[pathData];
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.splitSegment #
NIL
THEN {
DrawSegment[pathViewer: handle.pathViewer, segment: handle.splitSegment.first, undo: TRUE];
FreeSegmentList[handle.splitSegment];
handle.splitSegment ← NIL;
DrawSegment[pathViewer: handle.pathViewer, segment: handle.splitReplacements.first];
DrawSegment[pathViewer: handle.pathViewer, segment: handle.splitReplacements.rest.first];
EnableSegmentRefresh[handle.splitReplacements.first];
EnableSegmentRefresh[handle.splitReplacements.rest.first];
};
InvalidateHitCache[handle];
};
AddControlPoint:
PUBLIC PROCEDURE [pathData: PathData, where, position: Point] = {
This routine adds a control point to the specified segment (the segment in the active trajectory which is closest to "where"). Position specifies the position of the new control point. If the segment already contained one control point, the new control point is added before or after the current one depending on the the distance from "where" to the line segments defined by the existing control point and the two knots of the segment. If "where" is closer to the line segment defined by the existing control point and the first knot, the new control point is inserted before the existing one; otherwise it is inserted following the existing control point in the segment. If the segment already contains two control points, no action is performed.
handle: Handle ← NARROW[pathData];
vertex: Vertex ← NEW[VertexRec ← [point: position]];
newVertexNode: VertexNode ← NIL;
hitSegment: SegmentNode;
hitPoint: Point;
hitT: REAL;
numberVertices, maxNumberVertices: CARDINAL;
segmentFull: BOOLEAN;
precedingVertex: VertexNode;
d0, d1: REAL;
IF handle.activeTrajectory = NIL THEN RETURN;
[hitSegment, hitPoint, hitT] ← SegmentHitTest[handle.activeTrajectory.first.segments, where];
IF hitSegment #
NIL
THEN {
numberVertices ← VertexListLength[hitSegment.first.vertices];
maxNumberVertices ←
SELECT hitSegment.first.type
FROM
moveTo => 1,
curveTo => 3,
ENDCASE => 0;
segmentFull ← numberVertices >= maxNumberVertices;
IF ~segmentFull
THEN {
DrawSegment[pathViewer: handle.pathViewer, segment: hitSegment.first, undo: TRUE];
IF numberVertices < 2 THEN precedingVertex ← NIL
ELSE {
d0 ← DistanceSquared[where, ClosestPointOnLineSegment[point: where, p0: hitSegment.first.fp.point, p1: hitSegment.first.vertices.first.point]];
d1 ← DistanceSquared[where, ClosestPointOnLineSegment[point: where, p0: hitSegment.first.vertices.first.point, p1: hitSegment.first.vertices.rest.first.point]];
precedingVertex ← IF d0 < d1 THEN NIL ELSE hitSegment.first.vertices;
};
[hitSegment.first.vertices, newVertexNode] ← InsertVertex[hitSegment.first.vertices, vertex, precedingVertex];
Constrain[pathViewer: handle.pathViewer, vertex: newVertexNode, segment: hitSegment, newPosition: position];
InvalidateHitCache[handle];
};
};
DoSetActiveVertex[handle, newVertexNode, hitSegment];
};
CloseActiveTrajectory:
PUBLIC PROCEDURE [pathData: PathData] = {
This routine makes the active trajectory a closed trajectory.
handle: Handle ← NARROW[pathData];
fp: VertexNode;
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.activeTrajectory.first.segments.first.type = moveTo
THEN {
handle.activeTrajectory.first.segments.first.type ← curveTo;
[fp,] ← PrecedingVertex[handle.activeTrajectory.first.segments.first.vertices, handle.activeTrajectory.first.segments];
handle.activeTrajectory.first.segments.first.fp ← IF fp # NIL THEN fp.first ELSE NIL;
DrawSegment[pathViewer: handle.pathViewer, segment: handle.activeTrajectory.first.segments.first];
};
DoSetActiveVertex[handle, NIL, NIL];
};
OpenActiveTrajectory:
PUBLIC PROCEDURE [pathData: PathData] = {
This routine makes the active trajectory an open trajectory.
handle: Handle ← NARROW[pathData];
lastVertex: VertexNode;
DoDeleteVertex: VertexProc = {
IF v # lastVertex
THEN {
DrawVertex[pathViewer: handle.pathViewer, vertex: v.first, undo: TRUE];
[handle.activeTrajectory.first.segments.first.vertices,] ← RemoveVertex[handle.activeTrajectory.first.segments.first.vertices, v];
};
};
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.activeTrajectory.first.segments.first.type # moveTo
THEN {
DrawSegment[pathViewer: handle.pathViewer, segment: handle.activeTrajectory.first.segments.first, undo: TRUE];
lastVertex ← LastVertexNode[handle.activeTrajectory.first.segments.first.vertices];
ForAllVertices[handle.activeTrajectory.first.segments.first.vertices, DoDeleteVertex];
handle.activeTrajectory.first.segments.first.type ← moveTo;
handle.activeTrajectory.first.segments.first.fp ← NIL;
};
DoSetActiveVertex[handle, NIL, NIL];
};
DeleteVertex:
PUBLIC PROCEDURE [pathData: PathData, vertex: Point] = {
This routine deletes the vertex (which may be a knot or a control point) in the active trajectory which is closest to the specified point. The specified point must be "close to" (i.e. whithin a certain minimum distance of) a vertex in the active trajectory in order for the operation to be carried out.
handle: Handle ← NARROW[pathData];
closestVertex, pVertex, fVertex: VertexNode;
closestSegment, pSegment, fSegment: SegmentNode;
IF handle.activeTrajectory = NIL THEN RETURN;
[closestVertex, closestSegment] ← VertexHitTest[handle.activeTrajectory.first.segments, vertex];
[pVertex, pSegment] ← PrecedingVertex[closestVertex, closestSegment];
[fVertex, fSegment] ← FollowingVertex[closestVertex, closestSegment];
IF closestVertex #
NIL
THEN {
IF IsAControlPoint[closestVertex]
THEN {
DisableSegmentRefresh[segment: closestSegment.first];
DrawVertex[pathViewer: handle.pathViewer, vertex: closestVertex.first, undo: TRUE];
DrawSegment[pathViewer: handle.pathViewer, segment: closestSegment.first, undo: TRUE];
[closestSegment.first.vertices,] ← RemoveVertex[closestSegment.first.vertices, closestVertex];
DrawSegment[pathViewer: handle.pathViewer, segment: closestSegment.first];
EnableSegmentRefresh[segment: closestSegment.first];
}
ELSE {
SELECT
TRUE
FROM
closestSegment.rest = closestSegment => {
DisableSegmentRefresh[segment: closestSegment.first];
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: closestSegment.first, undo: TRUE];
[handle.activeTrajectory.first.segments,] ← RemoveSegment[handle.activeTrajectory.first.segments, closestSegment]
};
IsFirstSegment[closestSegment] => {
DisableSegmentRefresh[segment: closestSegment.first];
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: closestSegment.first, undo: TRUE];
IF IsLastSegment[closestSegment]
THEN
[handle.activeTrajectory.first.segments,] ← RemoveSegment[handle.activeTrajectory.first.segments, closestSegment]
ELSE {
DisableSegmentRefresh[segment: closestSegment.rest.first];
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: closestSegment.rest.first, undo: TRUE];
closestSegment.first.vertices.first^ ← LastVertexNode[closestSegment.rest.first.vertices].first^;
[handle.activeTrajectory.first.segments,] ← RemoveSegment[handle.activeTrajectory.first.segments, closestSegment.rest];
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: closestSegment.first];
EnableSegmentRefresh[segment: closestSegment.first];
};
};
IsLastSegment[closestSegment] => {
DisableSegmentRefresh[segment: closestSegment.first];
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: closestSegment.first, undo: TRUE];
[handle.activeTrajectory.first.segments,] ← RemoveSegment[handle.activeTrajectory.first.segments, closestSegment];
};
ENDCASE => MergeSegments[handle: handle, segmentNode: closestSegment];
};
IF TrajectoryIsEmpty[pathData]
THEN {
DeleteActiveTrajectory[pathData];
NewTrajectory[pathData];
};
IF ContinuousAt[pVertex, pSegment] THEN Constrain[handle.pathViewer, pVertex, pSegment, pVertex.first.point];
IF ContinuousAt[fVertex, fSegment] THEN Constrain[handle.pathViewer, fVertex, fSegment, fVertex.first.point];
DoRequestRefresh[handle];
};
DoSetActiveVertex[handle, NIL, NIL];
InvalidateHitCache[handle];
};
MergeSegments:
PROCEDURE [handle: Handle, segmentNode: SegmentNode] = {
This routine merges the specified segment with the one following it in the segment list, deleting the common knot and the control points surrounding the common knot. The second of the two segments is the one which is actually removed from the segment list. If no segment follows the specified segment in the segment list, then no action is performed by this routine.
DeleteVertex: VertexProc = {
DrawVertex[pathViewer: handle.pathViewer, vertex: v.first, undo: TRUE];
[segmentNode.first.vertices,] ← RemoveVertex[segmentNode.first.vertices, v];
};
vertexNode: VertexNode;
vertexList: VertexList;
IF IsLastSegment[segmentNode] THEN RETURN;
DisableSegmentRefresh[segment: segmentNode.first];
DisableSegmentRefresh[segment: segmentNode.rest.first];
DrawSegment[pathViewer: handle.pathViewer, segment: segmentNode.first, undo: TRUE];
DrawSegment[pathViewer: handle.pathViewer, segment: segmentNode.rest.first, undo: TRUE];
vertexNode ← segmentNode.first.vertices;
IF vertexNode.rest # NIL THEN vertexNode ← vertexNode.rest;
ForAllVertices[vertexNode, DeleteVertex];
vertexList ← segmentNode.rest.first.vertices;
segmentNode.rest.first.vertices ← NIL;
IF VertexListLength[vertexList] > 2
THEN {
DrawVertex[pathViewer: handle.pathViewer, vertex: vertexList.first, undo: TRUE];
[vertexList,] ← RemoveVertex[vertexList, vertexList];
};
segmentNode.first.vertices ← VertexListAppend[segmentNode.first.vertices, vertexList];
[handle.activeTrajectory.first.segments,] ← RemoveSegment[handle.activeTrajectory.first.segments, segmentNode.rest];
DrawSegment[pathViewer: handle.pathViewer, segment: segmentNode.first];
EnableSegmentRefresh[segment: segmentNode.first];
};
SetActiveVertex:
PUBLIC PROCEDURE [pathData: PathData, where: Point]
RETURNS [activeVertexSet:
BOOLEAN, vertex: Point] = {
This routine makes the vertex of the active trajectory which is closest to the specified point the active vertex. A vertex must be made active before it can be moved. The specified position must be "close to" (i.e. whithin a certain minimum distance of) a segment in the active trajectory in order for the operation to be carried out. This routine returns an indication of whether the active vertex was in fact set and the position of the active vertex. The routines for adding knots and control points also set the active vertex. The NewImage, DeleteVertex and AddCubic routines unset the active vertex.
handle: Handle ← NARROW[pathData];
activeVertex: VertexNode;
activeSegment: SegmentNode;
IF handle.activeTrajectory = NIL THEN activeVertex ← NIL
ELSE [activeVertex, activeSegment] ← VertexHitTest[handle.activeTrajectory.first.segments, where];
DoSetActiveVertex[handle, activeVertex, activeSegment];
activeVertexSet ← activeVertex # NIL;
IF activeVertexSet THEN vertex ← activeVertex.first.point;
};
DoSetActiveVertex:
PROCEDURE [handle: Handle, activeVertex: VertexNode, activeSegment: SegmentNode] = {
This routine sets up all of the information necessary for moving a vertex around. activeVertex is the vertex to be moved around. activeSegment is the first segment containing activeVertex.
IF activeVertex =
NIL
THEN {
handle.activeVertex ← NIL;
handle.activeSegment ← NIL;
}
ELSE {
handle.activeVertex ← activeVertex;
handle.activeSegment ← activeSegment;
};
};
GetActiveVertex:
PUBLIC PROCEDURE [pathData: PathData]
RETURNS [activeVertexSet:
BOOLEAN, vertex: Point] = {
This routine returns an indication of whether the active vertex is set and the position of the active vertex.
handle: Handle ← NARROW[pathData];
activeVertexSet ← handle.activeVertex # NIL;
IF activeVertexSet THEN vertex ← handle.activeVertex.first.point;
};
MoveActiveVertex:
PUBLIC PROCEDURE [pathData: PathData, newPosition: Point] = {
This routine moves the active vertex (which may be a knot or a control point) closest to the specified point to the specfied new position.
handle: Handle ← NARROW[pathData];
IF handle.activeVertex #
NIL
THEN {
Constrain[pathViewer: handle.pathViewer, vertex: handle.activeVertex, segment: handle.activeSegment, newPosition: newPosition];
DoRequestRefresh[handle];
};
InvalidateHitCache[handle];
};
SetContinuity:
PUBLIC PROCEDURE [pathData: PathData, knot: Point, continuity: BooleanSpecification] = {
This routine sets the continuity requirements at the vertex in the active trajectory which is closest to the specified location. The specified point (knot) must be "close to" (i.e. within a certain minimum distance of) a vertex in the active trajectory in order for the operation to be carried out. This operation only applies to knots. The client can specify that continuity is required (on), that continuity is not required (off), or that the current continuity requirement setting is to be toggled. When continuity is requested, the positions of some vertices may be altered to satisfy the request.
handle: Handle ← NARROW[pathData];
hitVertex: VertexNode;
hitSegment: SegmentNode;
IF handle.activeTrajectory = NIL THEN RETURN;
[hitVertex, hitSegment] ← VertexHitTest[handle.activeTrajectory.first.segments, knot];
IF IsAKnot[hitVertex]
THEN {
DisableSegmentRefresh[hitSegment.first];
DrawVertex[pathViewer: handle.pathViewer, vertex: hitVertex.first, undo: TRUE];
SELECT continuity
FROM
on => hitVertex.first.fixed ← TRUE;
off => hitVertex.first.fixed ← FALSE;
toggle => hitVertex.first.fixed ← ~hitVertex.first.fixed;
ENDCASE;
DrawVertex[pathViewer: handle.pathViewer, vertex: hitVertex.first];
IF ContinuousAt[hitVertex, hitSegment] THEN Constrain[pathViewer: handle.pathViewer, vertex: hitVertex, segment: hitSegment, newPosition: hitVertex.first.point];
EnableSegmentRefresh[hitSegment.first];
};
};
FixControlPoint:
PUBLIC PROCEDURE [pathData: PathData, controlPoint: Point, fixed: BooleanSpecification] = {
This routine sets the indicator of whether the vertex in the active trajectory which is closest to the specified location is fixed (i.e. may not be moved unless explicitly requested by the client) or not fixed (i.e. may be moved without a client request in order to satisfy continuity requirements at some other vertex). The specified point (knot) must be "close to" (i.e. within a certain minimum distance of) a vertex in the active trajectory in order for the operation to be carried out. This operation only applies to control points. The client can specify that the vertex be fixed (on), that the vertex can be moved (off), of that the current setting is to be toggled.
handle: Handle ← NARROW[pathData];
hitVertex: VertexNode;
hitSegment: SegmentNode;
IF handle.activeTrajectory = NIL THEN RETURN;
[hitVertex, hitSegment] ← VertexHitTest[handle.activeTrajectory.first.segments, controlPoint];
IF hitVertex #
NIL
AND ~IsAKnot[hitVertex]
THEN {
DisableSegmentRefresh[hitSegment.first];
DrawVertex[pathViewer: handle.pathViewer, vertex: hitVertex.first, undo: TRUE];
SELECT fixed
FROM
on => hitVertex.first.fixed ← TRUE;
off => hitVertex.first.fixed ← FALSE;
toggle => hitVertex.first.fixed ← ~hitVertex.first.fixed;
ENDCASE;
DrawVertex[pathViewer: handle.pathViewer, vertex: hitVertex.first];
EnableSegmentRefresh[hitSegment.first];
};
};
EnumerateActiveTrajectory:
PUBLIC PROCEDURE [pathData: PathData, moveToProc: MoveToProc, lineToProc: LineToProc, curveToProc: CurveToProc] = {
This routine enumerates all the segments in the active trajectory. Client specified routines are called for each line segment, and bezier curve in the trajectory. The moveToProc is called for the first point (FP) of the active trajectory.
handle: Handle ← NARROW[pathData];
EnumerateSegment: SegmentProc = {
numberVertices: CARDINAL;
numberVertices ← VertexListLength[s.first.vertices];
SELECT
TRUE
FROM
s.first.type = moveTo => moveToProc[p1: s.first.vertices.first.point];
numberVertices = 1 => lineToProc[p1: s.first.vertices.first.point];
numberVertices = 2 => curveToProc[p1: s.first.vertices.first.point, p2: s.first.vertices.first.point, p3: s.first.vertices.rest.first.point];
numberVertices = 3 => curveToProc[p1: s.first.vertices.first.point, p2: s.first.vertices.rest.first.point, p3: s.first.vertices.rest.rest.first.point];
ENDCASE;
};
IF handle.activeTrajectory = NIL THEN RETURN;
ForAllSegments[handle.activeTrajectory.first.segments, EnumerateSegment];
};
HitTestVertices:
PUBLIC PROCEDURE [pathData: PathData, where: Point]
RETURNS [closeToVertex:
BOOLEAN, vertexType: VertexType, vertex: Point] = {
This routine determines the vertex in the active trajectory which is closest to the specified point. Only those vertices which are in the active trajectory and within a threshold distance of the point are considered. This routine returns an indication of whether the point is close to a vertex, and if so, the position of the closest vertex and its type.
handle: Handle ← NARROW[pathData];
hitVertex: VertexNode;
hitSegment: SegmentNode;
IF handle.vertexHit.hitCached
AND where = handle.vertexHit.where
THEN {
closeToVertex ← handle.vertexHit.closeToVertex;
IF closeToVertex
THEN {
vertexType ← handle.vertexHit.vertexType;
vertex ← handle.vertexHit.vertex;
};
}
ELSE {
IF handle.activeTrajectory = NIL THEN hitVertex ← NIL
ELSE [hitVertex, hitSegment] ← VertexHitTest[handle.activeTrajectory.first.segments, where];
closeToVertex ← hitVertex # NIL;
IF closeToVertex
THEN {
vertex ← hitVertex.first.point;
vertexType ←
SELECT
TRUE
FROM
IsAKnot[hitVertex] AND IsFirstSegment[hitSegment] => frontKnot,
IsAKnot[hitVertex] AND IsLastSegment[hitSegment] => rearKnot,
IsAKnot[hitVertex] => intermediateKnot,
ENDCASE => controlPoint;
};
handle.vertexHit.hitCached ← TRUE;
handle.vertexHit.closeToVertex ← closeToVertex;
IF closeToVertex
THEN {
handle.vertexHit.vertexType ← vertexType;
handle.vertexHit.vertex ← vertex;
};
};
};
HitTestCurves:
PUBLIC PROCEDURE [pathData: PathData, where: Point]
RETURNS [closeToCurve:
BOOLEAN, curveType: CurveType, p0, p1, p2, p3: Point, hitPoint: Point] = {
This routine determines the curve segment in the active trajectory which is closest to the specified point. Only those segments which are in the active trajectory and within a threshold distance of the point are considered. This routine returns an indication of whether the point is close to a curve segment, and if so, the position of the vertices of the segment, its type, and the point on the curve closest to the specified point.
handle: Handle ← NARROW[pathData];
hitT: REAL;
hitSegment: SegmentNode;
numberVertices: CARDINAL;
bezier: Bezier;
IF handle.curveHit.hitCached
AND where = handle.curveHit.where
THEN {
closeToCurve ← handle.curveHit.closeToCurve;
IF closeToCurve
THEN {
curveType ← handle.curveHit.curveType;
p0 ← handle.curveHit.p0;
p1 ← handle.curveHit.p1;
p2 ← handle.curveHit.p2;
p3 ← handle.curveHit.p3;
hitPoint ← handle.curveHit.hitPoint;
};
}
ELSE {
IF handle.activeTrajectory = NIL THEN hitSegment ← NIL
ELSE [hitSegment, hitPoint, hitT] ← SegmentHitTest[handle.activeTrajectory.first.segments, where];
closeToCurve ← hitSegment # NIL;
IF closeToCurve
THEN {
numberVertices ← VertexListLength[hitSegment.first.vertices];
bezier ← SegmentToBezier[hitSegment.first];
p0 ← bezier.b0;
p1 ← bezier.b1;
p2 ← bezier.b2;
p3 ← bezier.b3;
curveType ←
SELECT numberVertices
FROM
0 => point,
1 => line,
ENDCASE => bezier;
IF curveType = line THEN p1 ← bezier.b3;
};
handle.curveHit.hitCached ← TRUE;
handle.curveHit.closeToCurve ← closeToCurve;
IF closeToCurve
THEN {
handle.curveHit.curveType ← curveType;
handle.curveHit.p0 ← p0;
handle.curveHit.p1 ← p1;
handle.curveHit.p2 ← p2;
handle.curveHit.p3 ← p3;
handle.curveHit.hitPoint ← hitPoint;
};
};
};
InvalidateHitCache:
PROCEDURE [handle: Handle] = {
This routine invalidates the contents of the vertex and curve hit caches. This routine is called whenever an operation is performed which may cause the contents to be out of date and inaccurate.
handle.vertexHit.hitCached ← FALSE;
handle.curveHit.hitCached ← FALSE;
};