<> <> <> <<>> <> 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 ] ]; <> CreatePathViewer: PUBLIC PROCEDURE [name: Rope.ROPE, menuLabels: LIST OF MenuLabel, buttonProc: ButtonHitProc _ NIL, redrawProc: DrawProc _ NIL] RETURNS [pathData: PathData] = { <> 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 = { <> handle: Handle _ NARROW[clientData]; Redraw[pathData: clientData, erase: TRUE]; }; Quit: QuitProc = { <> handle: Handle _ NARROW[clientData]; DestroyRefreshProcess[handle.refreshData]; <> FreeTrajectoryList[handle.trajectoryList]; }; ButtonHit: ButtonProc = { <> handle: Handle _ NARROW[clientData]; IF handle.buttonProc # NIL THEN handle.buttonProc[event: event, x: x, y: y]; }; DrawInPathViewer: PUBLIC PROCEDURE [pathData: PathData, drawProc: DrawProc] = { <> handle: Handle _ NARROW[pathData]; DrawInViewer[handle.pathViewer, drawProc]; }; Redraw: PUBLIC PROCEDURE [pathData: PathData, erase: BOOLEAN _ FALSE] = { <> handle: Handle _ NARROW[pathData]; RequestRefresh[handle.refreshData, erase]; }; FlushImage: PUBLIC PROCEDURE [pathData: PathData] = { <> 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]; }; <> <<>> NewTrajectory: PUBLIC PROCEDURE [pathData: PathData] = { <> handle: Handle _ NARROW[pathData]; handle.newTrajectory _ TRUE; DoSetActiveTrajectory[handle, NIL]; }; SetActiveTrajectory: PUBLIC PROCEDURE [pathData: PathData, where: Point] RETURNS [activeTrajectorySet: BOOLEAN, hitPoint: 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] = { <> 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] = { <> handle: Handle _ NARROW[pathData]; RETURN [handle.activeTrajectory = NIL OR handle.activeTrajectory.first.segments = NIL]; }; DeleteActiveTrajectory: PUBLIC PROCEDURE [pathData: PathData] = { <> 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]; }; GetTrajectoryFile: PUBLIC PROCEDURE [pathData: PathData, fileName: Rope.ROPE] = { <> 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] = { <> handle: Handle _ NARROW[pathData]; WriteTrajectoryFile[fileName, handle.trajectoryList]; }; EnumerateTrajectories: PUBLIC PROCEDURE [pathData: PathData, moveToProc: MoveToProc, lineToProc: LineToProc, curveToProc: CurveToProc] = { <> 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; }; <> <<>> AddKnotToFront: PUBLIC PROCEDURE [pathData: PathData, position: Point] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> handle: Handle _ NARROW[pathData]; activeVertexSet _ handle.activeVertex # NIL; IF activeVertexSet THEN vertex _ handle.activeVertex.first.point; }; MoveActiveVertex: PUBLIC PROCEDURE [pathData: PathData, newPosition: Point] = { <> 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] = { <> 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]; }; }; SetTrajectoryContinuity: PUBLIC PROCEDURE [pathData: PathData, continuity: BooleanSpecification] = { <> MoveTo: MoveToProc = {}; LineTo: LineToProc = {SetContinuity[pathData, p1, continuity]}; CurveTo: CurveToProc = {SetContinuity[pathData, p3, continuity]}; EnumerateActiveTrajectory[pathData: pathData, moveToProc: MoveTo, lineToProc: LineTo, curveToProc: CurveTo]; }; FixControlPoint: PUBLIC PROCEDURE [pathData: PathData, controlPoint: Point, fixed: BooleanSpecification] = { <> 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] = { <> 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] = { <> 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] = { <> 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] = { <> handle.vertexHit.hitCached _ FALSE; handle.curveHit.hitCached _ FALSE; }; DoRequestRefresh: PROCEDURE [handle: Handle] = { <> NewRefreshData[handle.refreshData, handle.trajectoryList, handle.activeTrajectory]; RequestRefresh[handle.refreshData]; }; END. <<>>