PathEditorImpl.mesa
Copyright (C) 1984 by Xerox Corporation. All rights reserved.
Written by Darlene Plebon on August 31, 1983 11:24 am
Last editted by Pauline Ts'o on May 31, 1984 1:03:53 pm PDT
Routines providing a client interface to the Path Editor.
Last Edited by: Tso, August 14, 1984 10:39:39 am PDT
DIRECTORY
Convert USING [RopeFromReal],
PathEditor,
PEAlgebra USING [ClosestPointOnLineSegment, DistanceSquared],
PEBetaSpline USING [BetaSplinePoints],
PEBezier USING [Bezier, BezierToSegment, SegmentToBezier, SplitBezier],
PEConstraints USING [Constrain],
PEConstructSpline USING [LinkBetaSplineSegments, BuildBetaSplineTrajectory],
PEDisplay USING [DrawControlPoints, DrawSegment, DrawSegmentAndVertices, DrawVertex, DrawVertices],
PEFileOps USING [ReadTrajectoryFile, WriteTrajectoryFile],
PEHitTest USING [SegmentHitTest, TrajectoryHitTest, VertexHitTest],
PERefresh USING [CreateRefreshProcess, DestroyRefreshProcess, DisableSegmentRefresh, DisableTrajectoryRefresh, EnableSegmentRefresh, EnableTrajectoryRefresh, NewRefreshData, RefreshData, RequestRefresh],
PESimplify USING [WeightedMidpoint],
PETrajectoryOps,
PETypes,
PEViewer USING [BuildViewer, ButtonProc, DrawInViewer, DrawProc, QuitProc, RedrawProc],
Rope USING [ROPE],
ViewerClasses USING [Viewer],
ViewerTools USING [SetContents];
PathEditorImpl: CEDAR PROGRAM
IMPORTS Convert, PEAlgebra, PEBetaSpline, PEBezier, PEConstraints, PEConstructSpline, PEDisplay, PEFileOps, PEHitTest, PERefresh, PESimplify, PETrajectoryOps, PEViewer, ViewerTools
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
biasViewer: ViewerClasses.Viewer,   -- bias viewer
tensionViewer: ViewerClasses.Viewer,  -- tension viewer
trajectoryList: TrajectoryList ← NIL,   -- list of segments in the path
newTrajectory: BOOLEANTRUE,   -- 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, handle.biasViewer, handle.tensionViewer] ← 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];
};
IF handle.activeTrajectory = NIL THEN RETURN;
DisableTrajectoryRefresh[handle.activeTrajectory.first];
ForAllSegments[handle.activeTrajectory.first.segments, UnDraw];
[handle.trajectoryList,] ← RemoveTrajectory[handle.trajectoryList, handle.activeTrajectory];
DoSetActiveTrajectory[handle, NIL];
};
GetTrajectoryFile: PUBLIC PROCEDURE [pathData: PathData, fileName: Rope.ROPE] = {
This routine reads the specified trajectory file into the path editor. The trajectories read in are appended to those already in the image.
handle: Handle ← NARROW[pathData];
trajectories: TrajectoryList ← ReadTrajectoryFile[fileName];
t: TrajectoryNode ← LastTrajectoryNode[handle.trajectoryList];
IF t = NIL THEN handle.trajectoryList ← trajectories
ELSE {
t.rest ← trajectories;
IF trajectories # NIL THEN trajectories.preceding ← t;
};
DoSetActiveTrajectory[handle, NIL];
DoRequestRefresh[handle];
};
StoreTrajectoryFile: PUBLIC PROCEDURE [pathData: PathData, fileName: Rope.ROPE] = {
This routine writes a text representation of the trajectories in the path editor to a file.
handle: Handle ← NARROW[pathData];
WriteTrajectoryFile[fileName, handle.trajectoryList];
};
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];
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];
};
StoreBetas: PUBLIC PROCEDURE [pathData: PathData, bias: REAL, tension: REAL] = {
This routine updates the Beta-spline with the current values of bias and tension and re-computes the segment of the curve that is affected by the change.
activeSegment: PETypes.SegmentNode;  -- current segment as segment list is walked  
first: BOOLEANTRUE;      -- indicates if segment is first seg. to be processed
firstSegInList: PETypes.Segment;    -- ptr to first segment in active trajectory
handle: Handle ← NARROW[pathData];
handleSegList: PETypes.SegmentList;   -- ptr into active trajectory segment list
newSegmentList: PETypes.SegmentList;  -- list of the spline segments affected by change
splineTraj: PETypes.Trajectory ← NIL;  -- trajectory node containing new spline
tempSegList: PETypes.SegmentList ← NIL; -- temporary segment list of new spline trajectory as it is being built up
IF handle.activeTrajectory = NIL THEN RETURN;
IF handle.activeTrajectory.first.segments.global = TRUE THEN{
-- Recompute the entire spline curve.
activeSegment ← handle.activeTrajectory.first.segments;
firstSegInList ← activeSegment.first;
WHILE first OR activeSegment.first # firstSegInList DO
activeSegment.first.vertices.first.bias ← bias;
activeSegment.first.vertices.first.tension ← tension;
activeSegment ← activeSegment.rest;
IF first THEN first ← FALSE;
ENDLOOP;
newSegmentList ← handle.activeTrajectory.first.segments;
handleSegList ← handle.activeTrajectory.first.segments;
}
ELSE {
-- Change only the stored beta values for the active vertex.
IF handle.activeVertex = NIL THEN RETURN;
handle.activeVertex.first.bias ← bias;
handle.activeVertex.first.tension ← tension;
[handleSegList, newSegmentList] ← BuildBetaSplineSegment[handle, FALSE];
};
-- Compute the new curve segment(s)
tempSegList ← PEBetaSpline.BetaSplinePoints[newSegmentList, handleSegList, FALSE];
splineTraj ← PEConstructSpline.BuildBetaSplineTrajectory[tempSegList];
InsertBetaSplineSegment[handle, splineTraj, handle.activeTrajectory.first.segments.global];
newSegmentList ← NIL; tempSegList ← NIL;
DoRequestRefresh[handle];
};
SetGlobal: PUBLIC PROCEDURE [pathData: PathData] = {
This routine sets the flag for uniformily Beta splines
handle: Handle ← NARROW[pathData];
IF handle.activeTrajectory = NIL THEN RETURN;
handle.activeTrajectory.first.segments.global ← TRUE;
};
SetLocal: PUBLIC PROCEDURE [pathData: PathData] = {
This routine sets the flag for continuously Beta splines
handle: Handle ← NARROW[pathData];
IF handle.activeTrajectory = NIL THEN RETURN;
handle.activeTrajectory.first.segments.global ← FALSE;
};
SetSimplify: PUBLIC PROCEDURE [pathData: PathData, delta: REAL] = {
This routine removes one control point from the active trajectory while trying to maintain the original shape of the spline.
collinearSegNode: PETypes.SegmentNode;
filtered: BOOLEAN;
firstSegment: PETypes.SegmentNode;
handle: Handle ← NARROW[pathData];
newControlPolygon: PETypes.Trajectory;
newSpline: PETypes.Trajectory;
newSplineSegmentList: PETypes.SegmentList;
newTrajectoryList: PETypes.TrajectoryList;
oldSegList: PETypes.SegmentList;
segment: PETypes.SegmentNode;
IF handle.activeTrajectory = NIL THEN RETURN;
[filtered, collinearSegNode, newControlPolygon, newSpline, oldSegList] ← PESimplify.WeightedMidpoint[handle.activeTrajectory, delta];
IF filtered THEN{        -- Control polygon was filtered
[newTrajectoryList, ] ← PETrajectoryOps.InsertTrajectory[handle.trajectoryList, newControlPolygon, NIL];
handle.trajectoryList ← newTrajectoryList;
IF newSpline # NIL THEN{
[handle.trajectoryList, ] ← PETrajectoryOps.InsertTrajectory[newTrajectoryList, newSpline, NIL];
handle.trajectoryList.rest.spline ← handle.trajectoryList.first;
};
}
ELSE IF collinearSegNode # NIL THEN {  -- Collinear points were found and simplified
[handle.trajectoryList] ← DeleteVertex[pathData: pathData, vertex: collinearSegNode.first.vertices.first.point, collinear: TRUE, closestVertex: collinearSegNode.first.vertices, closestSegment: collinearSegNode];
}
ELSE IF oldSegList # NIL THEN{    -- Circular arcs were found and simplified
-- Obsolete control polygon must be erased
firstSegment ← oldSegList;
DrawSegmentAndVertices[pathViewer: handle.pathViewer, segment: firstSegment.first, undo: TRUE];
segment ← firstSegment.rest;
WHILE segment # firstSegment DO
DrawSegmentAndVertices[handle.pathViewer, segment.first, TRUE];
segment ← segment.rest;
ENDLOOP;
-- Obsolete spline curve must be erased and new one calculated
PERefresh.DisableTrajectoryRefresh[handle.activeTrajectory.spline];
firstSegment ← handle.activeTrajectory.spline.segments;
EraseOldSpline[handle, firstSegment];
segment ← firstSegment.rest;
WHILE segment # firstSegment DO
EraseOldSpline[handle, segment];
segment ← segment.rest;
ENDLOOP;
[newSplineSegmentList] ← PEBetaSpline.BetaSplinePoints[handle.activeTrajectory.first.segments, handle.activeTrajectory.first.segments, FALSE];
IF newSplineSegmentList # NIL THEN{
newSplineSegmentList.first.type ← moveTo;
newSplineSegmentList.first.fp ← NIL;
handle.activeTrajectory.spline.segments ← newSplineSegmentList;
};
PERefresh.EnableTrajectoryRefresh[handle.activeTrajectory.spline];
};
DoRequestRefresh[handle];
};
CloseActiveTrajectory: PUBLIC PROCEDURE [pathData: PathData] = {
This routine makes the active trajectory a closed trajectory. A new spline segment must be tacked onto the head of the active trajectory's associated spline.
fp: VertexNode;
handle: Handle ← NARROW[pathData];
handleSegList: PETypes.SegmentList;   -- pointer into old segment list to first affected pt.
newSegmentList: PETypes.SegmentList;  -- list of ctrl pts associated w/new spline segment
segment: PETypes.SegmentNodeRec;   -- holds current segment as segment list is walked
splineTraj: PETypes.Trajectory;    -- trajectory node pointing to new spline curve
splineTrajectory: PETypes.TrajectoryList;  -- pointer to old spline curve
tempSegList: PETypes.SegmentList ← NIL; -- temporary segment list of new spline as it is being built up
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];
handle.activeTrajectory.first.segments ← handle.activeTrajectory.first.segments.preceding;
handle.activeSegment ← handle.activeTrajectory.first.segments;
handle.activeVertex ← handle.activeSegment.first.vertices;
-- Create the new spline segment to tack onto the head of the spline trajectory
newSegmentList ← NEW[PETypes.SegmentNodeRec];
newSegmentList ← NIL;
handleSegList ← handle.activeTrajectory.first.segments.rest.rest.rest;
segment ← handleSegList^;
FOR i: CARDINAL ← 0, i+1 UNTIL i = 4 DO -- use the 4 ctrl pts at head for new spline seg
newSegmentList ← PEConstructSpline.LinkBetaSplineSegments[tempSegList, segment];
tempSegList ← newSegmentList;
segment ← segment.preceding^;
handleSegList ← handleSegList.preceding;
ENDLOOP;
newSegmentList.first.type ← moveTo;
newSegmentList.first.fp ← NIL;
tempSegList ← PEBetaSpline.BetaSplinePoints[newSegmentList, handleSegList, FALSE];
splineTraj ← PEConstructSpline.BuildBetaSplineTrajectory[tempSegList];
newSegmentList ← NIL; tempSegList ← NIL;
-- Find the trajectory that is the spline associated with the active trajectory
splineTrajectory ← handle.trajectoryList;
WHILE splineTrajectory.first # handle.activeTrajectory.spline DO splineTrajectory ← splineTrajectory.rest ENDLOOP;
PERefresh.DisableTrajectoryRefresh[handle.activeTrajectory.spline];
PERefresh.DisableTrajectoryRefresh[splineTrajectory.first];
-- Tack the new spline segment to the head of the old spline
splineTrajectory.first.segments.first.type ← curveTo;
[newSegmentList] ← PETrajectoryOps.LinkSegments[splineTrajectory.first.segments, NIL, splineTraj.segments];
splineTrajectory.first.segments ← newSegmentList;
handle.activeTrajectory.spline ← splineTrajectory.first;
-- Set spline properties for proper refreshing
IF handle.activeTrajectory.spline # NIL THEN{
handle.activeTrajectory.spline.segments.first.type ← moveTo;
handle.activeTrajectory.spline.segments.first.fp ← NIL;
PERefresh.EnableTrajectoryRefresh[handle.activeTrajectory.spline];
};
DoRequestRefresh[handle];
};
DoSetActiveVertex[handle, NIL, NIL];
};
OpenActiveTrajectory: PUBLIC PROCEDURE [pathData: PathData] = {
This routine makes the active trajectory an open trajectory. The leading segment of the spline curve must be deleted.
handle: Handle ← NARROW[pathData];
lastVertex: VertexNode;
newSegmentList: PETypes.SegmentList;  -- new spline segments, after leading seg is deleted
splineTrajectory: PETypes.TrajectoryList;  -- pointer to old spline segments
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 {
-- Find the spline trajectory associated with the active trajectory
handle.activeVertex ← handle.activeTrajectory.first.segments.first.vertices;
splineTrajectory ← handle.trajectoryList;
WHILE splineTrajectory.first # handle.activeTrajectory.spline DO splineTrajectory ← splineTrajectory.rest ENDLOOP;
PERefresh.DisableTrajectoryRefresh[handle.activeTrajectory.spline];
PERefresh.DisableTrajectoryRefresh[splineTrajectory.first];
-- Delete the first segment of the spline trajectory associated with the active trajectory
newSegmentList ← DeleteSplineSegmentHead[handle, splineTrajectory];
splineTrajectory.first.segments ← newSegmentList;
handle.activeTrajectory.spline ← splineTrajectory.first;
IF handle.activeTrajectory.spline # NIL THEN{
handle.activeTrajectory.spline.segments.first.type ← moveTo;
handle.activeTrajectory.spline.segments.first.fp ← NIL;
PERefresh.EnableTrajectoryRefresh[handle.activeTrajectory.spline];
};
handle.activeTrajectory.first.segments ← handle.activeTrajectory.first.segments.rest;
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, collinear: BOOLEAN, closestVertex: VertexNode, closestSegment: SegmentNode] RETURNS [newTrajectoryList: PETypes.TrajectoryList]= {
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.
closed: BOOLEANFALSE;      -- indicates if active trajectory is closed
pVertex, fVertex: VertexNode;
pSegment, fSegment: SegmentNode;
handle: Handle ← NARROW[pathData] ;
handleSegList: PETypes.SegmentList;   -- pointer into old seg list to first affected point
newSegmentList: PETypes.SegmentList;  -- list of ctrl pts associated w/new spline segment
splineTraj: PETypes.Trajectory;    -- trajectory node pointing to new spline curve
tempSegList: PETypes.SegmentList ← NIL; --temporary segment list of new spline as it is being built up
IF handle.activeTrajectory = NIL THEN RETURN;
IF ~collinear THEN -- The point to delete is the closest one next to the mouse cursor
[closestVertex, closestSegment] ← VertexHitTest[handle.activeTrajectory.first.segments, vertex];
[pVertex, pSegment] ← PrecedingVertex[closestVertex, closestSegment];
[fVertex, fSegment] ← FollowingVertex[closestVertex, closestSegment];
IF closestVertex # NIL THEN {
-- Re-compute the part of the spline that is affected by the deletion of the control point
handle.activeSegment ← closestSegment;
handle.activeVertex ← closestVertex;
[handleSegList, newSegmentList] ← BuildBetaSplineSegment[handle, TRUE];
tempSegList ← PEBetaSpline.BetaSplinePoints[newSegmentList, handleSegList, FALSE];
splineTraj ← PEConstructSpline.BuildBetaSplineTrajectory[tempSegList];
InsertBetaSplineSegment[handle, splineTraj, FALSE];
newSegmentList ← NIL; tempSegList ← NIL;
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];
}
-- For some reason, pointers get changed when a vertex is deleted, so perform a god-awful hack to correct for that occurrence, ie. re-compute the entire spline curve all over again.
ELSE {
IF handle.activeTrajectory.first.segments.first.type = curveTo THEN closed ← TRUE;
tempSegList ← PEBetaSpline.BetaSplinePoints[handle.activeTrajectory.first.segments, handle.activeTrajectory.first.segments, closed];
IF tempSegList # NIL THEN{
tempSegList.first.type ← moveTo;
tempSegList.first.fp ← NIL;
};
IF handle.activeTrajectory.spline # NIL THEN {
PERefresh.DisableTrajectoryRefresh[handle.activeTrajectory.spline];
handle.activeTrajectory.spline.segments ← tempSegList;
PERefresh.EnableTrajectoryRefresh[handle.activeTrajectory.spline];
};
tempSegList ← NIL;
};
newTrajectoryList ← handle.trajectoryList;
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;
ViewerTools.SetContents[handle.biasViewer, Convert.RopeFromReal[handle.activeVertex.first.bias]];
ViewerTools.SetContents[handle.tensionViewer, Convert.RopeFromReal[handle.activeVertex.first.tension]];
};
};
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];
handleSegList: PETypes.SegmentList;  -- pointer into old segment list to first affected point
newSegmentList: PETypes.SegmentList; -- list of ctrl pts associated w/new spline segment
splineTraj: PETypes.Trajectory ← NIL; -- trajectory node pointing to new spline curve
tempSegList: PETypes.SegmentList;  -- temporary segment list of new spline as it is being built up
IF handle.activeVertex # NIL THEN {
Constrain[pathViewer: handle.pathViewer, vertex: handle.activeVertex, segment: handle.activeSegment, newPosition: newPosition];
-- Re-compute the spline segments that are changed by the movement of the control point
[handleSegList, newSegmentList] ← BuildBetaSplineSegment[handle, FALSE];
tempSegList ← PEBetaSpline.BetaSplinePoints[newSegmentList, handleSegList, FALSE];
splineTraj ← PEConstructSpline.BuildBetaSplineTrajectory[tempSegList];
InsertBetaSplineSegment[handle, splineTraj, FALSE];
newSegmentList ← NIL; tempSegList ← NIL;
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];
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;
};
};
};
BuildBetaSplineSegment: PRIVATE PROCEDURE [handle: Handle, delete: BOOLEAN] RETURNS [handleSegList: PETypes.SegmentList, newSegmentList: PETypes.SegmentList] = {
This routine is called when spline segments are changing shape due to the movement of, deletion of, or a change in the beta values of the active vertex. This routine purportedly builds and returns a new segment list that contains only those control points which control the changing spline segments (there is a maximum of seven such control points at any given time). It also returns a pointer, handleSegList, to the first affected control point in the old segment list.
closed: BOOLEANFALSE;     -- indicates if active trajectory is closed
count: INT;         -- counter to ensure max # of ctrl. pts. is not exceeded
first: BOOLEAN;        -- indicates if a point is first one to be processed
firstSegInList: PETypes.Segment;   -- pointer to first segment in active trajectory
influencingCPts: CARDINAL ← 3;   -- max of 3 ctrl. pts. to one side of active Vertex can influence changing shape of the active spline curve
numOfInfluencingCPts: CARDINAL ← 6; -- 0 to 6 => 7 ctrl pts max are affected here
segment: PETypes.SegmentNodeRec;  -- holds ptr to current segment as seg list is walked
tempSegList: PETypes.SegmentList;  -- temporarily holds the new segment list as it is being built
-- Build new segment list containing affected control point. Also keep tabs on the corresponding positions within the active segment list.
segment ← handle.activeSegment^;
handleSegList ← handle.activeSegment;
firstSegInList ← handle.activeTrajectory.first.segments.first;
IF firstSegInList.type = curveTo THEN closed ← TRUE;
first ← TRUE;
count ← influencingCPts;
WHILE (segment.first # firstSegInList AND count > 0) OR first DO
-- Walk backward from active vertex to find first affected control point
segment ← segment.rest^;
handleSegList ← handleSegList.rest;
count ← count-1;
IF first THEN first ← FALSE;
ENDLOOP;
IF segment.first = firstSegInList AND ~closed THEN {
-- Exclude the last segment found. We have wrapped around to the head of the active trajectory
segment ← segment.preceding^;
handleSegList ← handleSegList.preceding;
count ← count+1;
};
tempSegList ← NEW[PETypes.SegmentNodeRec];
tempSegList ← NIL;
newSegmentList ← NEW[PETypes.SegmentNodeRec];
first ← TRUE;
-- At this point, we have backed up to the first affected control point. Now walk forward to find the, at most, six other affected control points.
WHILE (segment.first # firstSegInList AND count < numOfInfluencingCPts) OR first DO
-- If the active vertex is to be deleted, then we do not want to include it in the new seg. list
IF ~(delete AND segment.first.vertices = handle.activeVertex) THEN{
newSegmentList ← PEConstructSpline.LinkBetaSplineSegments[tempSegList, segment];
tempSegList ← newSegmentList;
};
segment ← segment.preceding^;
handleSegList ← handleSegList.preceding;
count ← count+1;
IF first THEN first ← FALSE;
ENDLOOP;
IF count <= numOfInfluencingCPts AND ~(delete AND segment.first.vertices = handle.activeVertex)THEN {
-- Include the last seg. found if it is the head of the active traj. & it is not being deleted
newSegmentList ← PEConstructSpline.LinkBetaSplineSegments[tempSegList, segment];
};
newSegmentList.first.type ← moveTo;
newSegmentList.first.fp ← NIL;
};

EraseOldSpline: PRIVATE PROCEDURE [handle: Handle, segment: PETypes.SegmentNode] = {
This routine just "whites out" a given segment.
DrawSegment[pathViewer: handle.pathViewer, segment: segment.first, undo: TRUE];
};
InsertBetaSplineSegment: PRIVATE PROCEDURE [handle: Handle, splineTraj: PETypes.Trajectory, changeAll: BOOLEAN] = {
This routine is called when a part of a Beta spline must be replaced due to movement of, deletion of, or change in the beta values of a control point. This routine excises the outdated spline segment and splices the new spline segment, splineTraj, in its place or possibly just tacks the new spline segment to the head or tail of the old spline trajectory. If the boolean, changeAll, is set to TRUE, then the whole spline is replaced with the new one.
count: INT;
first: BOOLEAN;          -- TRUE iff a pt or seg is first one to be processed
found: BOOLEANTRUE;      -- TRUE iff an old segment needs to be replaced
insertSeg: PETypes.SegmentNode;    -- place in seg list where new seg is to be placed
newSegmentList: PETypes.SegmentList ← NIL; -- list of ctrl pts associated w/new spline seg.
newTrajectoryList: PETypes.TrajectoryList; -- if no spline yet exists, a new one is created
replacedFirstSeg: BOOLEANFALSE;   -- TRUE iff the first seg in the old list is replaced
saveSeg: PETypes.SegmentNode;    -- temp ptr to segments, usu. the first one in a list
splineTrajectory: PETypes.TrajectoryList;  -- pointer to old spline curve
tempSegList: PETypes.SegmentList;   -- temporary segment list of new spline as it is being built up
IF handle.activeTrajectory.spline # NIL AND splineTraj # NIL THEN { -- replace old spline with new
-- Find the 1st point on the spline associated with the active trajectory
splineTrajectory ← handle.trajectoryList;
WHILE splineTrajectory.first # handle.activeTrajectory.spline DO splineTrajectory ← splineTrajectory.rest ENDLOOP;
PERefresh.DisableTrajectoryRefresh[handle.activeTrajectory.spline];
PERefresh.DisableTrajectoryRefresh[splineTrajectory.first];
-- The movement or addition of a control point will change the shape of the spline associated with the active trajectory or control polygon. We must retrieve those spline segments (a maximum of 4) that will be affected. This retrieval can be done because each spline segment has pointers to the 4 control points that determine its shape. If a control point is being added, then the boolean, found, is set to FALSE to indicate that no old segment is needs to be replaced.
insertSeg ← splineTrajectory.first.segments;
saveSeg ← insertSeg;
first ← TRUE;
IF changeAll THEN found ← TRUE
-- Finding the first affected spline segment
ELSE WHILE insertSeg.first.controlPtN # handle.activeVertex AND insertSeg.first.controlPtC # handle.activeVertex AND insertSeg.first.controlPtP # handle.activeVertex AND insertSeg.first.controlPtPP # handle.activeVertex AND (insertSeg # saveSeg OR first) DO
insertSeg ← insertSeg.rest;
IF insertSeg = saveSeg THEN found ← FALSE;
IF first THEN first ← FALSE;
ENDLOOP;
IF found THEN {   -- splice the new segments somewhere into the old spline traj.
saveSeg ← insertSeg;
first ← TRUE;
count ← 0;
IF changeAll THEN{ -- erase the whole old spline
WHILE insertSeg # saveSeg OR first DO
first ← FALSE;
EraseOldSpline[handle, insertSeg];
insertSeg ← insertSeg.rest;
ENDLOOP;
}
ELSE {     -- erase a part of the old spline
WHILE ((insertSeg.first.controlPtN = handle.activeVertex OR insertSeg.first.controlPtC = handle.activeVertex OR insertSeg.first.controlPtP = handle.activeVertex OR insertSeg.first.controlPtPP = handle.activeVertex) AND insertSeg # saveSeg) OR first DO
first ← FALSE;
count ← count+1;
EraseOldSpline[handle, insertSeg];
insertSeg ← insertSeg.rest;
ENDLOOP;
IF saveSeg = splineTrajectory.first.segments THEN replacedFirstSeg ← TRUE;
[newSegmentList,] ← PETrajectoryOps.UnlinkSegments[splineTrajectory.first.segments, saveSeg, count];
};
-- Replace the whole old spline with the new one
IF newSegmentList = NIL OR changeAll THEN splineTrajectory.first ← splineTraj
-- Or replace just a part of the old spline with the new spline piece
ELSE [splineTrajectory.first.segments] ← PETrajectoryOps.LinkSegments[newSegmentList, insertSeg.preceding, splineTraj.segments];
-- If the first segment of the old spline has been replaced, then all pointers to the spline must be updated to point to the head of the new spline segment
IF replacedFirstSeg THEN splineTrajectory.first.segments ← splineTraj.segments;
}
ELSE{ -- tack the new curve segments onto the head or tail of the spline trajectory
IF handle.activeSegment.first.type = moveTo THEN { -- tack it on the head
splineTrajectory.first.segments.first.type ← curveTo;
[newSegmentList] ← PETrajectoryOps.LinkSegments[splineTrajectory.first.segments, NIL, splineTraj.segments];
splineTrajectory.first.segments ← newSegmentList;
}
ELSE {               -- tack it on the tail
[newSegmentList] ← PETrajectoryOps.LinkSegments[splineTrajectory.first.segments, splineTrajectory.first.segments.preceding, splineTraj.segments];
splineTrajectory.first.segments ← newSegmentList;
};
};
handle.activeTrajectory.spline ← splineTrajectory.first;
}
ELSE IF handle.activeTrajectory.spline = NIL AND splineTraj # NIL THEN {
-- Start a new spline trajectory
[newTrajectoryList, ] ← PETrajectoryOps.InsertTrajectory[handle.trajectoryList, splineTraj, NIL];
PERefresh.DisableTrajectoryRefresh[newTrajectoryList.first];
handle.trajectoryList ← newTrajectoryList;
handle.activeTrajectory.spline ← handle.trajectoryList.first;
}
ELSE IF handle.activeTrajectory.spline # NIL AND splineTraj = NIL THEN {
-- Delete either the head or tail of the spline trajectory
-- First, find the spline associated with the active trajectory
splineTrajectory ← handle.trajectoryList;
WHILE splineTrajectory.first # handle.activeTrajectory.spline DO splineTrajectory ← splineTrajectory.rest ENDLOOP;
PERefresh.DisableTrajectoryRefresh[handle.activeTrajectory.spline];
PERefresh.DisableTrajectoryRefresh[splineTrajectory.first];
IF handle.activeSegment.first.type = moveTo THEN {  -- Delete spline curve segment head
newSegmentList ← DeleteSplineSegmentHead[handle, splineTrajectory];
}
ELSE{               -- Delete spline curve segment tail
insertSeg ← splineTrajectory.first.segments.preceding;
newSegmentList ← splineTrajectory.first.segments;
WHILE insertSeg.first.controlPtPP = handle.activeVertex DO
EraseOldSpline[handle, insertSeg];
[tempSegList, ] ← PETrajectoryOps.RemoveSegment[newSegmentList, insertSeg];
newSegmentList ← tempSegList;   
insertSeg ← newSegmentList.preceding; 
ENDLOOP;
};
splineTrajectory.first.segments ← newSegmentList;
handle.activeTrajectory.spline ← splineTrajectory.first;
};
IF handle.activeTrajectory.spline # NIL AND handle.activeTrajectory.spline.segments # NIL THEN{  -- Reset the spline parameters for proper refreshing
handle.activeTrajectory.spline.segments.first.type ← moveTo;
handle.activeTrajectory.spline.segments.first.fp ← NIL;
PERefresh.EnableTrajectoryRefresh[handle.activeTrajectory.spline];
}
ELSE IF handle.activeTrajectory.spline # NIL AND handle.activeTrajectory.spline.segments = NIL THEN{  -- The spline trajectory is empty, so remove the spline from the trajectory list
handle.activeTrajectory.spline ← NIL;
[handle.trajectoryList, ] ← PETrajectoryOps.RemoveTrajectory[handle.trajectoryList, splineTrajectory];
};
};
DeleteSplineSegmentHead: PRIVATE PROCEDURE [handle: Handle, splineTrajectory: PETypes.TrajectoryList] RETURNS [newSegmentList: PETypes.SegmentList] = {
This routine deletes the first curve segment of the given spline.
first: BOOLEAN;       -- TRUE iff a point or segment is first one to be processed
insertSeg: PETypes.SegmentNode; -- place in segment list where new segment is to be placed
tempSegList: PETypes.SegmentList; -- temporary segment list of new spline as it is being built up
-- Delete spline curve segment head
newSegmentList ← NIL;
IF splineTrajectory.first.segments = NIL THEN RETURN;
insertSeg ← splineTrajectory.first.segments;
newSegmentList ← splineTrajectory.first.segments;
first ← TRUE;
WHILE insertSeg.first.controlPtN = handle.activeVertex AND (first OR insertSeg # splineTrajectory.first.segments) DO
IF first THEN first ← FALSE;
EraseOldSpline[handle, insertSeg];
insertSeg ← insertSeg.rest;
ENDLOOP;
-- The following seven lines of code is not my idea of efficiency, but for reasons I cannot fathom, it will not work if I place the segment removal code in the while loop directly above
insertSeg ← splineTrajectory.first.segments;
first ← TRUE;
WHILE insertSeg # NIL AND insertSeg.first.controlPtN = handle.activeVertex AND (first OR insertSeg # splineTrajectory.first.segments) DO
IF first THEN first ← FALSE;
[tempSegList, ] ← PETrajectoryOps.RemoveSegment[newSegmentList, insertSeg];
newSegmentList ← tempSegList;
insertSeg ← newSegmentList;
ENDLOOP;
IF insertSeg = NIL THEN newSegmentList ← NIL;
};
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;
};
DoRequestRefresh: PROCEDURE [handle: Handle] = {
This routine requests that the image be refreshed, after updating the information the refresh process requires about the image.
NewRefreshData[handle.refreshData, handle.trajectoryList, handle.activeTrajectory];
RequestRefresh[handle.refreshData];
};
END.