MatchImplB.mesa
Last edited by: David Kurlander - September 6, 1987 8:25:14 pm PDT
Bier, September 4, 1987 11:08:48 pm PDT
DIRECTORY
AtomButtons, Basics, Feedback, GGBasicTypes, GGBoundBox, GGCaret, GGContainer, GGFont, GGInterfaceTypes, GGModelTypes, GGOutline, GGScene, GGSegmentTypes, GGSelect, GGSequence, GGSessionLog, GGSlice, GGUserInput, GGUtility, GGWindow, GList, Imager, ImagerTransformation, IO, List, Match, MatchTurtle, MatchViewer, Real, SlackProcess, Vectors2d, ViewerClasses;
MatchImplB: CEDAR MONITOR
IMPORTS AtomButtons, Basics, Feedback, GGBoundBox, GGCaret, GGOutline, GGScene, GGSelect, GGSequence, GGSessionLog, GGSlice, GGUserInput, GGUtility, GGWindow, GList, ImagerTransformation, IO, List, Match, MatchTurtle, MatchViewer, Real, SlackProcess, Vectors2d
EXPORTS Match = BEGIN
BitVector: TYPE = GGBasicTypes.BitVector;
BoundBox: TYPE = GGModelTypes.BoundBox;
Caret: TYPE = REF CaretObj;
CaretObj: TYPE = GGInterfaceTypes.CaretObj;
ChoiceData: TYPE = MatchViewer.ChoiceData;
FontData: TYPE = GGFont.FontData;
GGDataObj: TYPE = GGInterfaceTypes.GGDataObj;
GGData: TYPE = GGInterfaceTypes.GGData;
ItemMatch: TYPE = Match.ItemMatch;
LooksInfo: TYPE = Match.LooksInfo;
MatchData: TYPE = MatchViewer.MatchData;
MatchDescriptor: TYPE = Match.MatchDescriptor;
MatchLevel: TYPE = MatchViewer.MatchLevel;
Point: TYPE = GGBasicTypes.Point;
SearchInfo: TYPE = Match.SearchInfo;
SearchState: TYPE = Match.SearchState;
Segment: TYPE = GGSegmentTypes.Segment;
SegmentGenerator: TYPE = GGModelTypes.SegmentGenerator;
Sequence: TYPE = GGModelTypes.Sequence;
SequenceOfReal: TYPE = GGModelTypes.SequenceOfReal;
Scene: TYPE = GGModelTypes.Scene;
Slice: TYPE = GGModelTypes.Slice;
SliceDescriptor: TYPE = GGModelTypes.SliceDescriptor;
SliceDescriptorGenerator: TYPE = GGModelTypes.SliceDescriptorGenerator;
SliceDescriptorObj: TYPE = GGModelTypes.SliceDescriptorObj;
SliceGenerator: TYPE = GGModelTypes.SliceGenerator;
SliceParts: TYPE = REF ANY;
StrokeEnd: TYPE = Imager.StrokeEnd; -- TYPE = {square, butt, round}
StrokeJoint: TYPE = Imager.StrokeJoint; -- TYPE = {miter, bevel, round}
Traj: TYPE = GGModelTypes.Traj;
TrajGenerator: TYPE = GGModelTypes.TrajGenerator;
TurtleHeader: TYPE = MatchTurtle.TurtleHeader;
TurtleInfo: TYPE = MatchTurtle.TurtleInfo;
EfficientOrder: PROC [looksList: LIST OF REF ANY] RETURNS [LIST OF REF ANY] = {
Orders the looksList to be in the best (ie, most efficient) order for matching. This means that open curves will be before closed curves which will be before objects with no orientation.
BestMatchOrder: GList.CompareProc = {
[ref1: REF ANY, ref2: REF ANY] RETURNS [Basics.Comparison]
priority1: INT ← SortOrder[ref1];
priority2: INT ← SortOrder[ref2];
RETURN[Basics.CompareInt[priority2, priority1]];
};
SortOrder: PROC [looks: REF ANY] RETURNS [INT] = {
Items with the largest SortOrder are best to match first
simpleLooks: LooksInfo;
WITH looks SELECT FROM
simple: LooksInfo => simpleLooks ← simple;
complex: LIST OF LIST OF LooksInfo => simpleLooks ← complex.first.first;
ENDCASE => ERROR;
IF NOT simpleLooks.shapeDef OR simpleLooks.shape = NIL THEN RETURN[-2]
ELSE IF simpleLooks.shape.closed THEN RETURN[-1]
ELSE RETURN[Real.InlineRound[simpleLooks.shape.length]]; -- match on larger first
};
looksList ← NARROW[GList.Sort[looksList, BestMatchOrder]];
RETURN[looksList];
};
Global Variables to reduce Search Allocations
searchState: SearchState ← NEW[Match.SearchStateObj];
GetSearchState: PUBLIC PROC RETURNS [SearchState] = {
RETURN[searchState];
};
Conjunctive Searching (must match All objects in the From viewer)
SearchConjInit: PUBLIC PROC [them: GGData, event: LIST OF REF ANY] = {
Find any object that matches an object in the From viewer (searchOp = conjunction)
matchData: MatchData ← MatchViewer.GetMatchData[];
searchState: SearchState ← GetSearchState[];
Fix up searchState and matchData for another search.
IF them # matchData.theirData OR GGCaret.GetPoint[matchData.theirData.caret] # matchData.lastCaretPos THEN {
If searching in new viewer, matchData.theirData ← new viewer and update lists.
searchState.ahead ← RemakeConjList[them, event.rest.first, TRUE];
searchState.behind ← NIL;
matchData.theirData ← them;
matchData.lastCaretPos ← GGCaret.GetPoint[them.caret];
} --searching in new viewer
ELSE IF event.rest.first # searchState.lastDirection THEN {
If reversing direction, don't rematch the objects that you just found.
normalGen: SliceDescriptorGenerator ← GGSelect.SelectedSlices[them.scene, normal];
matchSelectD: SliceDescriptor ← NIL;
"match select" all normal-selected objects so they're not included in search
FOR normalD: SliceDescriptor ← GGSelect.NextSliceDescriptor[normalGen], GGSelect.NextSliceDescriptor[normalGen] UNTIL normalD = NIL DO
GGSelect.SelectSlice[normalD, them.scene, match];
ENDLOOP;
IF searchState.ahead # NIL THEN {
matchSelectD ← GGSelect.FindSelectedSlice[searchState.ahead.first.slice, them.scene, match];
IF matchSelectD = NIL THEN matchSelectD ← searchState.ahead.first.slice.class.newParts[searchState.ahead.first.slice, NIL, none];
};
searchState.ahead ← RemakeConjList[them, event.rest.first, TRUE];
searchState.behind ← NIL;
Match.RemoveSelected[searchState.ahead, matchSelectD]; --do bookkeeping so that search will begin (in the other direction) where the last search left off
}; --search direction changed
searchState.lastDirection ← IF event.rest.first = $SearchFromTop THEN $SearchBelow ELSE event.rest.first;
};
RemakeConjList: PUBLIC PROC [theirData: GGData, direction: REF ANY, select: BOOL] RETURNS [ahead: LIST OF SearchInfo ← NIL] = {
ahead will hold SearchInfo's for objects whose boundbox is below the caret if the search is progressing in a downwards direction, and above the caret if the search is progessing upwards.
If select is true, then all slices ahead will be match selected (and those behind will be cleared).
matchData: MatchData ← MatchViewer.GetMatchData[];
matchLevel: MatchLevel ← matchData.matchLevel;
caretPos: Point ← GGCaret.GetPoint[theirData.caret];
sliceGen: SliceGenerator ← GGScene.SlicesInScene[theirData.scene];
slices: LIST OF Slice;
FOR slice: Slice ← GGScene.NextSlice[sliceGen], GGScene.NextSlice[sliceGen] UNTIL slice = NIL DO
IF (direction = $SearchAbove AND slice.class.getBoundBox[slice].hiY >= caretPos.y) OR (direction = $SearchBelow AND slice.class.getBoundBox[slice].hiY <= caretPos.y) OR (direction = $SearchFromTop) THEN slices ← CONS[slice, slices];
ENDLOOP;
Sort the slices by height.
slices ← Match.HeightSort[slices, direction # $SearchAbove];
Build the ahead list.
IF select THEN GGSelect.DeselectAll[theirData.scene, match]; -- speeds subsequent selections
FOR slices ← slices, slices.rest UNTIL slices = NIL DO
infoList: LIST OF SearchInfo ← ExpandSlice[slices.first, matchLevel, direction];
ahead ← NARROW[GList.Nconc[infoList, ahead]];
IF select THEN GGSelect.SelectEntireSlice[slices.first, theirData.scene, match];
ENDLOOP;
};
SearchConjNext: PUBLIC PROC [forward, backward: LIST OF SearchInfo, direction: REF ANY, refresh: BOOLTRUE] RETURNS [newForward, newBackward: LIST OF SearchInfo, found: BOOL] = {
Find and process the next conjunctive match.
matchData: MatchData ← MatchViewer.GetMatchData[];
fromData: GGData ← MatchViewer.GetFromData[];
fromLooks: LIST OF REF ANYEfficientOrder[Match.GetFromLooks[]]; -- ordered for fast search
matchD: MatchDescriptor;
[matchD, forward, backward] ← MatchLeadingObject[fromLooks, forward, backward, direction];
IF matchD.isMatch AND matchD.matchedSlices # NIL THEN { -- we've matched something!
If found, select match, move caret, restore screen, update SearchInfos.
GGSelect.DeselectAll[matchData.theirData.scene, normal];
IF NOT MatchViewer.GetContextSensitive[matchData] THEN SelectMapped[matchData.theirData.scene, matchD.mapping]
ELSE SelectMapped[matchData.theirData.scene, SelectionFilter[fromData.scene, matchD.mapping]];
matchData.lastCaretPos ← [GGCaret.GetPoint[matchData.theirData.caret].x, matchD.matchedSlices.first.slice.boundBox.hiY];
GGCaret.SetAttractor[matchData.theirData.caret, matchData.lastCaretPos, [0, -1], NIL];
IF refresh THEN GGWindow.RestoreScreenAndInvariants[paintAction: $SelectionChanged, ggData: matchData.theirData, remake: none, backgndOK: TRUE, edited: FALSE, okToClearFeedback: TRUE];
forward ← DeleteMatchesFromList[forward, matchD.matchedSlices.rest];
backward ← DeleteMatchesFromList[backward, matchD.matchedSlices.rest];
SetLastMatchDescriptor[matchD];
RETURN[forward, backward, TRUE];
}
ELSE { -- match failed
Otherwise, nothing is selected.
GGSelect.DeselectAll[matchData.theirData.scene, normal];
IF refresh THEN GGWindow.RestoreScreenAndInvariants[paintAction: $SelectionChanged, ggData: matchData.theirData, remake: none, backgndOK: TRUE, edited: FALSE, okToClearFeedback: TRUE];
Match.SetLastMatchDescriptor[NIL];
RETURN[forward, backward, FALSE];
};
};
MatchLeadingObject: PROC [fromLooks: LIST OF REF ANY, forward, backward: LIST OF SearchInfo, direction: REF ANY] RETURNS [matchD: MatchDescriptor, newForward, newBackward: LIST OF SearchInfo] = {
matchData: MatchViewer.MatchData ← MatchViewer.GetMatchData[];
domain: LIST OF SearchInfo ← NARROW[GList.Append[forward, backward]];
domainVec: BitVector ← NewBitVector[GList.Length[domain], FALSE];
vecIndex: NAT ← 0;
thisInfo, nextInfo: SearchInfo;
matchD ← CreateMatchDescriptor[];
IF fromLooks = NIL OR forward = NIL THEN RETURN[matchD, forward, backward]; -- nothing to match on or nothing to match
thisInfo ← forward.first;
DO -- iterate through forward SearchInfos
thisD: SliceDescriptor;
ResetDefaults[matchD];
IF NOT Match.IsEmptySearchInfo[thisInfo] THEN {
leadObj: REF ANY ← fromLooks.first;
[thisD, nextInfo] ← Match.FindMatchInSlice[thisInfo, LIST[leadObj], direction, matchD]; -- those parts of thisInfo that match leadObj, going in direction, using transform in matchD (if any).
IF thisD#NIL THEN { -- we've matched leadObj, now try rest
domainVec[vecIndex] ← TRUE; -- mark that thisInfo slice is taken
matchD ← MatchOtherObjects[fromLooks.rest, domain, domainVec, direction, matchD];
IF matchD.isMatch THEN {
ConjClearMatchBits[thisInfo, nextInfo, matchData.theirData.scene];
matchD.matchedSlices ← CONS[thisD, matchD.matchedSlices];
forward ← CONS[nextInfo, forward.rest];
RETURN[matchD, forward, backward];
};
IF matchD.posD.valid THEN { -- must try all possible orientations of this first element
nextPosD: MatchTurtle.PositionDescriptor;
WHILE (nextPosD ← MatchTurtle.NextMatch[matchD.posD.others]) # NIL DO
matchD.posD ← nextPosD;
[thisD, nextInfo] ← Match.FindMatchInSlice[thisInfo, LIST[leadObj], direction, matchD]; -- sets matchD.posD and matchD.mapping, etc.
IF thisD = NIL THEN LOOP;
matchD ← MatchOtherObjects[fromLooks.rest, domain, domainVec, direction, matchD]; -- sets matchD.mapping, etc.
IF matchD.isMatch THEN {
ConjClearMatchBits[thisInfo, nextInfo, matchData.theirData.scene];
matchD.matchedSlices ← CONS[thisD, matchD.matchedSlices];
forward ← CONS[nextInfo, forward.rest];
RETURN[matchD, forward, backward];
};
matchD.mapping ← NIL;
ENDLOOP;
};
domainVec[vecIndex] ← FALSE; -- no match: free up this element
};
ConjClearMatchBits[thisInfo, nextInfo, matchData.theirData.scene];
thisInfo ← nextInfo;
}
ELSE { -- we've run out of matches on thisInfo
backward ← CONS[forward.first, backward];
forward ← forward.rest;
IF forward = NIL THEN {
matchD.isMatch ← FALSE;
RETURN[matchD, forward, backward]; --nothing to search
};
thisInfo ← forward.first;
vecIndex ← vecIndex + 1;
};
ENDLOOP;
};
MatchOtherObjects: PROC [looksList: LIST OF REF ANY, domain: LIST OF SearchInfo, domainVec: BitVector, direction: REF ANY, matchD: MatchDescriptor] RETURNS [MatchDescriptor] = {
bbox: BoundBox;
simpleLooks: LooksInfo;
vecIndex: INT ← -1;
restrictMatch: BOOL;
saveDObj: Match.MatchDObj ← matchD^; -- copy so state can be restored if no match
IF looksList = NIL THEN {matchD.isMatch ← TRUE; RETURN[matchD];}; -- base case = success
WITH looksList.first SELECT FROM
simple: LooksInfo => simpleLooks ← simple;
complex: LIST OF LIST OF LooksInfo => simpleLooks ← complex.first.first;
ENDCASE => ERROR;
restrictMatch ← simpleLooks.shapeDef AND simpleLooks.shape # NIL AND matchD.posD.valid;
IF restrictMatch THEN {
averageLength: REAL = 300.0; -- average length of a trajectory (exactness unimportant)
maxError: REAL ← MatchViewer.GetMatchTolerance[] * averageLength;
pt: Point ← ImagerTransformation.Transform[matchD.posD.tform1, simpleLooks.shape.originalStart]; -- any point in the pre-normalized shape will do
pt ← ImagerTransformation.InverseTransform[matchD.posD.tform2, pt];
bbox ← GGBoundBox.CreateBoundBox[pt.x - maxError, pt.y - maxError, pt.x + maxError, pt.y + maxError];
};
FOR nextItem: LIST OF SearchInfo ← domain, nextItem.rest UNTIL nextItem = NIL DO
slice: Slice ← nextItem.first.slice;
match: SliceDescriptor;
vecIndex ← vecIndex + 1;
Match is restricted. We can speed up the search by looking only in the right places
IF domainVec[vecIndex] THEN LOOP; -- This puppy is taken
IF restrictMatch THEN IF NOT BoundBoxIntersect[bbox, slice.class.getBoundBox[slice]] THEN LOOP; -- A quick rejection of slice
[match: match] ← Match.FindMatchInSlice[nextItem.first, LIST[looksList.first], direction, matchD];
IF match # NIL THEN { -- we've got a match on this level. recurse!
domainVec[vecIndex] ← TRUE;
matchD ← MatchOtherObjects[looksList.rest, domain, domainVec, direction, matchD];
IF matchD.isMatch THEN {
matchD.matchedSlices ← CONS[match, matchD.matchedSlices];
RETURN[matchD];
};
domainVec[vecIndex] ← FALSE;
matchD^ ← saveDObj; -- restore original descriptor
};
ENDLOOP;
matchD.isMatch ← FALSE; -- found no match
matchD.matchedSlices ← NIL; -- clear it out
RETURN[matchD];
};
Conjunctive Search Utilities
DescriptorInSearchInfo: PROC [sliceD: SliceDescriptor, searchInfo: SearchInfo, matchLevel: MatchLevel] RETURNS [BOOL] = {
IF sliceD.slice.class.type # searchInfo.slice.class.type THEN RETURN[FALSE]
ELSE IF searchInfo.slice.class.type # $Outline OR matchLevel = sliceLevel THEN RETURN [searchInfo.slice = sliceD.slice]
ELSE -- match if the traj is in the descriptor
IF searchInfo.trajs = NIL THEN RETURN[FALSE] -- searchInfo is empty
ELSE RETURN[GGOutline.FindTrajInDescriptor[sliceD, searchInfo.trajs.first] # NIL];
};
DeleteMatchesFromList: PROC [searchList: LIST OF SearchInfo, sliceDList: LIST OF SliceDescriptor] RETURNS [newList: LIST OF SearchInfo ← NIL] = {
matchLevel: MatchLevel ← MatchViewer.GetMatchData[].matchLevel;
FOR l1: LIST OF SearchInfo ← searchList, l1.rest UNTIL l1 = NIL DO
delete: BOOLFALSE;
FOR l2: LIST OF SliceDescriptor ← sliceDList, l2.rest UNTIL l2 = NIL DO
IF DescriptorInSearchInfo[l2.first, l1.first, matchLevel] THEN {delete ← TRUE; EXIT;};
ENDLOOP;
IF NOT delete THEN newList ← CONS[l1.first, newList];
ENDLOOP;
newList ← NARROW[GList.DReverse[newList], LIST OF SearchInfo];
};
ExpandSlice: PROC [slice: Slice, matchLevel: MatchLevel, direction: REF ANY] RETURNS [infoList: LIST OF SearchInfo ← NIL] = {
IF slice.class.type = $Outline AND (matchLevel = trajLevel OR matchLevel = anywhere) THEN {
trajGen: TrajGenerator ← GGOutline.TrajsInOutline[slice];
FOR traj: Traj ← GGScene.NextTraj[trajGen], GGScene.NextTraj[trajGen] UNTIL traj=NIL DO
newInfo: SearchInfo ← SearchInfoFromTraj[traj, matchLevel, direction];
infoList ← CONS[newInfo, infoList];
ENDLOOP;
}
ELSE infoList ← LIST[Match.CreateSearchInfo[slice, direction]];
};
SearchInfoFromTraj: PROC [traj: Traj, matchLevel: MatchLevel, direction: REF ANY] RETURNS [searchInfo: SearchInfo] = {
searchInfo ← NEW[Match.SearchInfoObj ← [slice: traj.parent, reverse: direction = $SearchAbove]];
searchInfo.trajs ← LIST[traj];
IF matchLevel = anywhere THEN Match.AddSegs[searchInfo];
};
BoundBoxIntersect: PROC [b1, b2: BoundBox] RETURNS [BOOL] = {
IF b1.infinite OR b2.infinite THEN RETURN[TRUE];
IF b1.null OR b2.null THEN RETURN[FALSE];
IF b1.loX > b2.hiX OR b2.loX > b1.hiX OR b1.loY > b2.hiY OR b2.loY > b1.hiY THEN RETURN[FALSE]
ELSE RETURN[TRUE];
};
CreateMatchDescriptor: PUBLIC PROC [] RETURNS [matchD: MatchDescriptor] = {
matchD ← NEW[Match.MatchDObj];
matchD.posD ← NEW[MatchTurtle.PositionDObj ← [valid: FALSE, settable: TRUE]];
};
ResetDefaults: PROC [matchD: MatchDescriptor] = {
matchD.posD.valid ← FALSE;
matchD.posD.settable ← TRUE;
matchD.posD.others ← NIL;
matchD.isMatch ← FALSE;
matchD.matchedSlices ← NIL;
matchD.mapping ← NIL;
};
NewBitVector: PROC [length: NAT, initialValue: BOOLFALSE] RETURNS [bitVec: BitVector] = {
bitVec ← NEW[GGBasicTypes.BitVectorObj[length]];
FOR i: NAT IN [0..bitVec.len) DO
bitVec[i] ← initialValue;
ENDLOOP;
};
SelectMapped: PUBLIC PROC [scene: GGModelTypes.Scene, matches: LIST OF ItemMatch] = {
TrueForSeg: GGModelTypes.WalkProc = {
IF seg = thisSeg THEN RETURN[TRUE] ELSE RETURN[FALSE];
};
thisSeg: Segment;
FOR itemList: LIST OF ItemMatch ← matches, itemList.rest UNTIL itemList = NIL DO
sliceD: SliceDescriptor;
slice: Slice ← itemList.first.matchee.slice;
SELECT slice.class.type FROM
$Circle, $Text => sliceD ← slice.class.newParts[slice, NIL, slice];
$Box => {
thisSeg ← itemList.first.matchee.seg;
IF thisSeg = NIL THEN sliceD ← slice.class.newParts[slice, NIL, slice] -- whole box
ELSE sliceD ← GGSlice.WalkSegments[slice, TrueForSeg];
};
$Outline => {
traj: Traj ← itemList.first.matchee.traj;
thisSeg ← itemList.first.matchee.seg;
IF traj = NIL AND thisSeg = NIL THEN sliceD ← slice.class.newParts[slice, NIL, slice]
ELSE IF thisSeg = NIL THEN {
seq: Sequence ← GGSequence.CreateComplete[traj];
sliceD ← GGOutline.DescriptorFromSequence[traj.parent, seq];
}
ELSE sliceD ← GGSlice.WalkSegments[slice, TrueForSeg];
};
ENDCASE;
GGSelect.SelectSlice[sliceD, scene, normal];
ENDLOOP;
};
For Context-Sensitive Searches
SelectionFilter: PUBLIC PROC [scene: GGModelTypes.Scene, matches: LIST OF ItemMatch] RETURNS [selectList: LIST OF ItemMatch ← NIL] = {
Returns a list of those elements of matches whose matcher field points to an item which is selected. Got that?
FOR itemList: LIST OF ItemMatch ← matches, itemList.rest UNTIL itemList = NIL DO
slice: Slice ← itemList.first.matcher.slice;
selected: BOOLFALSE;
SELECT slice.class.type FROM
$Circle, $Text => IF GGSelect.IsSelectedInFull[slice, scene, normal] THEN selected ← TRUE;
$Box => {
seg: Segment ← itemList.first.matcher.seg;
IF seg = NIL THEN selected ← GGSelect.IsSelectedInFull[slice, scene, normal]
ELSE {
sliceD: SliceDescriptor ← GGSelect.FindSelectedSlice[slice, scene, normal];
IF sliceD # NIL THEN {
segGen: SegmentGenerator ← GGSlice.SegmentsInDescriptor[sliceD];
FOR nextSeg: Segment ← GGSlice.NextSegment[segGen], GGSlice.NextSegment[segGen] UNTIL nextSeg = NIL DO
IF seg = nextSeg THEN {selected ← TRUE; EXIT;};
ENDLOOP;
};
};
};
$Outline => {
traj: Traj ← itemList.first.matcher.traj;
seg: Segment ← itemList.first.matcher.seg;
IF traj = NIL AND seg = NIL THEN selected ← GGSelect.IsSelectedInFull[slice, scene, normal]
ELSE IF seg = NIL THEN selected ← GGSelect.IsSelectedInPart[traj, scene, normal]
ELSE {
sliceD: SliceDescriptor ← GGSelect.FindSelectedSlice[slice, scene, normal];
IF sliceD # NIL THEN {
segGen: SegmentGenerator ← GGSlice.SegmentsInDescriptor[sliceD];
FOR nextSeg: Segment ← GGSlice.NextSegment[segGen], GGSlice.NextSegment[segGen] UNTIL nextSeg = NIL DO
IF seg = nextSeg THEN {selected ← TRUE; EXIT;};
ENDLOOP;
};
};
};
ENDCASE;
IF selected THEN selectList ← CONS [itemList.first, selectList];
ENDLOOP;
};
Graphical Replace Routines
YesEvent: PUBLIC GGUserInput.UserInputProc = {
success: BOOL ← ReplaceOperation[];
IF success THEN Match.SearchEvent[NIL, event];
};
ReplaceOperation: PROC [] RETURNS [success: BOOL] = {
A dispatch procedure: establishes that state is OK and calls the proper replace routine.
matchData: MatchData ← MatchViewer.GetMatchData[];
IF matchData.theirData = NIL OR GetLastMatchDescriptor[] = NIL THEN {
MatchViewer.ErrorFeedback["Must search before replacing."];
RETURN[FALSE];
};
IF matchData.replaceOp = doReplace THEN {
IF AtomButtons.GetBinaryState[matchData.to.shape] THEN success ← ReplaceConverse[]
ELSE success ← ReplaceParameters[];
}
ELSE IF matchData.replaceOp = doOperations THEN success ← DoOps[]
ELSE ERROR; -- unknown replaceOp
};
ReplaceParameters: PROC RETURNS [success: BOOLTRUE] = {
1) find out if there's any characteristics ambiguity in To viewer
matchData: MatchData ← MatchViewer.GetMatchData[];
toLooks: LIST OF REF ANY ← Match.GetToLooks[];
singleLook: LooksInfo ← VerifyLooksEquivalence[toLooks];
IF singleLook = NIL THEN {
MatchViewer.ErrorFeedback["Ambiguous characteristics in To viewer."];
RETURN[FALSE];
};
2) we're in good shape, so now we need only copy looks to selection
CopyLooksToSelection[matchData.theirData, singleLook];
};
ReplaceConverse: PROC RETURNS [success: BOOLTRUE] = {
matchData: MatchData ← MatchViewer.GetMatchData[];
scene: Scene ← matchData.theirData.scene;
toScene: Scene ← MatchViewer.GetToData[].scene;
selected: LIST OF SliceDescriptor ← GGScene.GetSelections[scene, normal];
additions: LIST OF Slice ← NIL;
minPriority: INTIF selected = NIL THEN -1 ELSE GGScene.GetPriority[scene, selected.first.slice];
matchD: MatchDescriptor ← GetLastMatchDescriptor[];
sliceGen: SliceGenerator ← GGScene.SlicesInScene[toScene];
mapTform: ImagerTransformation.Transformation ← GetMatchTransform[matchData, matchD];
Get Relevant Looks from Match (that we aren't supposed to change)
matchLooks: LIST OF REF ANYGetLooksOfMatch[matchD, matchData.to, FALSE];
singleLook: LooksInfo ← VerifyLooksEquivalence[matchLooks];
IF singleLook = NIL THEN {
MatchViewer.ErrorFeedback["Ambiguous characteristics in Matched objects."];
RETURN[FALSE];
};
Find minimum priority of objects to replace
FOR sList: LIST OF SliceDescriptor ← selected.rest, sList.rest UNTIL sList = NIL DO
minPriority ← MIN[GGScene.GetPriority[scene, sList.first.slice], minPriority];
ENDLOOP;
Copy originals from To viewer
FOR original: Slice ← GGScene.NextSlice[sliceGen], GGScene.NextSlice[sliceGen] UNTIL original = NIL DO
entireD: SliceDescriptor;
newSlice: Slice ← original.class.copy[original];
newSlice.priority ← GGScene.GetPriority[toScene, original];
entireD ← newSlice.class.newParts[newSlice, NIL, slice];
CopyLooksToSlice[entireD, singleLook];
newSlice.class.transform[entireD, mapTform];
additions ← CONS[newSlice, additions];
ENDLOOP;
Delete selected slices in active search viewer
matchData.theirData.refresh.startBoundBox^ ← DeleteSelected[matchData.theirData]^;
Add slices to scene (at a reasonable priority)
additions ← InverseSortByPriority[additions];
FOR addList: LIST OF Slice ← additions, addList.rest UNTIL addList = NIL DO
GGScene.AddSlice[scene, addList.first, minPriority];
GGBoundBox.EnlargeByBox[matchData.theirData.refresh.startBoundBox, addList.first.class.getBoundBox[addList.first]];
ENDLOOP;
GGWindow.RestoreScreenAndInvariants[paintAction: $ObjectChangedBoundBoxProvided, ggData: matchData.theirData, remake: triggerBag, backgndOK: FALSE, edited: TRUE, okToClearFeedback: TRUE];
FixStateAfterSubstitute[];
};
GetMatchTransform: PROC [matchData: MatchData, matchD: MatchDescriptor] RETURNS [tform: ImagerTransformation.Transformation] = {
IF matchD.posD.valid THEN {
tform ← ImagerTransformation.Concat[matchD.posD.tform1, ImagerTransformation.Invert[matchD.posD.tform2]];
}
ELSE { -- we don't have an explicit match transform
Try to do something reasonable until we add a set of user options
Let's center the replacement on that which is being replaced (if nothing is being replaced, as in a context sensitive search with no selection, then we'll center the replacement on the caret
box1: BoundBox ← GGScene.BoundBoxOfScene[MatchViewer.GetToData[].scene];
box2: BoundBox ← GGScene.BoundBoxOfSelected[matchData.theirData.scene];
source: Point ← [(box1.loX + box1.hiX) / 2.0, (box1.loY + box1.hiY) / 2.0];
destination: Point ← IF NOT box2.null AND NOT box2.infinite THEN [(box2.loX + box2.hiX) / 2.0, (box2.loY + box2.hiY) / 2.0] ELSE GGCaret.GetPoint[matchData.theirData.caret];
tform ← ImagerTransformation.Translate[Vectors2d.Sub[destination, source]];
MatchViewer.ErrorFeedback["Warning: using centering transform."];
};
};
FixStateAfterSubstitute: PROC = {
This routine will remake the forward list to contain only those elements whose boundbox has the proper relationship to the caret, AND which have their match selection bit on.
matchData: MatchViewer.MatchData ← MatchViewer.GetMatchData[];
searchState: SearchState ← Match.GetSearchState[];
IF matchData.searchOp = disjunction THEN searchState.ahead ← Match.RemakeDisjList[matchData.theirData, searchState.lastDirection, FALSE]
ELSE IF matchData.searchOp = conjunction THEN searchState.ahead ← RemakeConjList[matchData.theirData, searchState.lastDirection, FALSE]
ELSE ERROR; -- unknown SearchOp
IF searchState.ahead # NIL THEN { -- find leading elements that are not match selected
RemoveAllUnselected[searchState.ahead, match, matchData.theirData.scene];
};
};
ComplementDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [SliceDescriptor] = {
entire: SliceDescriptor ← sliceD.slice.class.newParts[sliceD.slice, NIL, slice];
RETURN[sliceD.slice.class.differenceParts[entire, sliceD]];
};
RemoveAllUnselected: PROC [searchList: LIST OF SearchInfo, selectClass: GGSegmentTypes.SelectionClass, scene: Scene] = {
FOR info: LIST OF SearchInfo ← searchList, info.rest UNTIL info = NIL DO
selectD: SliceDescriptor ← GGSelect.FindSelectedSlice[info.first.slice, scene, selectClass];
IF selectD # NIL THEN selectD ← ComplementDescriptor[selectD]
ELSE selectD ← info.first.slice.class.newParts[info.first.slice, NIL, slice];
Match.RemoveDescriptorFromInfo[selectD, info.first];
ENDLOOP;
};
InverseSortByPriority: PROC [entityList: LIST OF Slice] RETURNS [sorted: LIST OF Slice] = {
Sort back to front (in decreasing order).
CompareProc: GGUtility.SliceCompareProc = {
[ref1: Slice, ref2: Slice] RETURNS [Basics.Comparison]
priority1, priority2: INT;
priority1 ← ref1.priority;
priority2 ← ref2.priority;
RETURN[Basics.CompareInt[priority2, priority1]];
};
sorted ← GGUtility.SortSliceList[entityList, CompareProc];
};
CopyLooksToSelection: PROC [ggData: GGData, toLooks: LooksInfo] = {
oldBBox: BoundBox ← GGScene.BoundBoxOfSelected[ggData.scene, normal];
newBBox: BoundBox;
sliceDescGen: SliceDescriptorGenerator ← GGSelect.SelectedSlices[ggData.scene, normal];
FOR sliceD: SliceDescriptor ← GGSelect.NextSliceDescriptor[sliceDescGen], GGSelect.NextSliceDescriptor[sliceDescGen] UNTIL sliceD=NIL DO
CopyLooksToSlice[sliceD, toLooks];
ENDLOOP;
newBBox ← GGScene.BoundBoxOfSelected[ggData.scene, normal];
ggData.refresh.startBoundBox^ ← GGBoundBox.BoundBoxOfBoxes[LIST[oldBBox, newBBox]]^; -- following traditional gargoyle form, copying structures and not pointers
GGWindow.RestoreScreenAndInvariants[paintAction: $ObjectChangedBoundBoxProvided, ggData: ggData, remake: none, backgndOK: FALSE, edited: TRUE, okToClearFeedback: FALSE];
};
CopyLooksToSlice: PROC [sliceD: SliceDescriptor, looks: LooksInfo] = {
slice: Slice ← sliceD.slice;
parts: SliceParts ← sliceD.parts;
IF looks.colorDef THEN slice.class.setStrokeColor[slice, parts, looks.color];
IF looks.fillColorDef THEN slice.class.setFillColor[slice, looks.fillColor];
IF looks.stringDef AND slice.class.type = $Text THEN GGSlice.SetText[slice, looks.string];
IF looks.fontDef AND slice.class.type = $Text THEN {
looks.font.transform ← GGSlice.GetFontData[slice].transform;
[] ← GGSlice.SetTextFont[slice, looks.font, MatchViewer.GetToData[].feedback];
};
IF looks.fontTformDef AND slice.class.type = $Text THEN [] ← GGSlice.SetTextFont[slice, looks.fontTform, MatchViewer.GetToData[].feedback]; -- SetTextFont will set the transform too!
IF looks.dashesDef THEN slice.class.setDashed[slice, parts, looks.dashes.dashed, looks.dashes.pattern, looks.dashes.offset, looks.dashes.length];
IF looks.jointsDef THEN slice.class.setStrokeJoint[slice, parts, looks.joints];
IF looks.endsDef THEN slice.class.setStrokeEnd[slice, parts, looks.ends];
IF looks.widthDef THEN [] ← slice.class.setStrokeWidth[slice, parts, looks.width];
};
GetLooksOfMatch: PROC [matchD: MatchDescriptor, choice: MatchViewer.ChoiceData, value: BOOL] RETURNS [looksList: LIST OF REF ANYNIL] = {
Creates a Looks List that describes all the objects that were matched.
matchData: MatchData ← MatchViewer.GetMatchData[];
itemList: LIST OF ItemMatch ← IF MatchViewer.GetContextSensitive[matchData] THEN SelectionFilter[matchData.theirData.scene, matchD.mapping] ELSE matchD.mapping;
FOR itemList ← itemList, itemList.rest UNTIL itemList = NIL DO
item: Match.ItemID ← itemList.first.matchee;
IF item.traj = NIL AND item.seg = NIL THEN {
must be that entire slice was matched, so add its looks to the looksList
looksList ← CONS[Match.GetLooksOfSlice[item.slice, choice, FALSE], looksList];
}
ELSE IF item.seg = NIL THEN {
must be that entire traj was matched, so add its looks to the looksList
looksList ← CONS[Match.GetLooksOfTraj[item.traj, choice, FALSE], looksList];
}
ELSE {
must be that we have a seg match.
we're ignoring shape info, which is OK for current application of this routine. May need to be revised later
looksList ← CONS[Match.GetLooksOfSegment[item.slice, item.traj, item.seg, choice, FALSE, NIL], looksList];
}
ENDLOOP;
};
VerifyLooksEquivalence: PROC [sceneLooks: LIST OF REF ANY] RETURNS [singleLook: LooksInfo ← NIL] = {
FlattenLooks: PROC [looks: LIST OF REF ANY] RETURNS [flatList: LIST OF LooksInfo ← NIL] = {
FOR looks ← looks, looks.rest UNTIL looks = NIL DO
WITH looks.first SELECT FROM
single: LooksInfo => flatList ← CONS[single, flatList];
multiple: LIST OF LIST OF LooksInfo => {
FOR multiple ← multiple, multiple.rest UNTIL multiple = NIL DO
flatList ← NARROW[GList.Append[multiple.first, flatList]];
ENDLOOP;
};
ENDCASE => ERROR;
ENDLOOP;
};
flatLooks: LIST OF LooksInfo ← FlattenLooks[sceneLooks];
IF flatLooks = NIL THEN RETURN[NIL]; -- no single Looks
FOR lptr: LIST OF LooksInfo ← flatLooks, lptr.rest UNTIL lptr = NIL OR lptr.rest=NIL DO
IF NOT Match.LooksEqual[lptr.first, lptr.rest.first] THEN RETURN[NIL]; -- different Looks
ENDLOOP;
RETURN[flatLooks.first];
};
DeleteSelected: PROC [ggData: GGData] RETURNS [bbox: BoundBox] = {
bbox ← GGScene.DeleteAllSelected[ggData.scene];
GGCaret.NoAttractor[ggData.caret];
GGCaret.SitOn[caret: ggData.caret, chair: NIL];
};
lastMatchD: MatchDescriptor; -- This global holds the MatchDescriptor from the most recent match
SetLastMatchDescriptor: PUBLIC PROC [matchD: MatchDescriptor] = {
lastMatchD ← matchD;
};
GetLastMatchDescriptor: PROC RETURNS [MatchDescriptor] = {
RETURN[lastMatchD];
};
ConjClearMatchBits: PROC [first, next: SearchInfo, scene: Scene] = {
This is similar to MatchImplA's AdjustBitsOfDifference, except AdjustBitsOfDifference makes the simplifying assumption that given a slice in the scene there is only a single SearchInfo for that slice. Thus, it need only deselect the entire slice, and select next. This routine will first deselect first, and then select next. It runs a bit slower, but is more general.
firstInfo: SliceDescriptor ← SearchInfoToDescriptor[first];
nextInfo: SliceDescriptor ← IF next = NIL THEN NIL ELSE SearchInfoToDescriptor[next];
GGSelect.DeselectSlice[firstInfo.slice, firstInfo.parts, scene, match];
IF nextInfo # NIL THEN GGSelect.SelectSlice[nextInfo, scene, match];
};
SearchInfoToDescriptor: PROC [info: SearchInfo] RETURNS [sliceD: SliceDescriptor] = {
ContainsSegs: GGModelTypes.WalkProc = { RETURN[GList.Member[seg, info.segs]];};
matchLevel: MatchLevel ← MatchViewer.GetMatchData[].matchLevel;
IF info = NIL THEN RETURN[NIL]; -- can't make an empty descriptor: we don't know the slice
IF Match.IsEmptySearchInfo[info] THEN RETURN[info.slice.class.newParts[info.slice, NIL, none]];
SELECT info.slice.class.type FROM
$Box => {
IF matchLevel # anywhere THEN RETURN[info.slice.class.newParts[info.slice, NIL, slice]]
ELSE RETURN[GGSlice.WalkSegments[info.slice, ContainsSegs]];
};
$Outline => {
trajGen: TrajGenerator ← GGOutline.TrajsInOutline[info.slice];
sliceD ← info.slice.class.newParts[info.slice, NIL, slice];
FOR traj: Traj ← GGScene.NextTraj[trajGen], GGScene.NextTraj[trajGen] UNTIL traj = NIL DO
IF NOT GList.Member[traj, info.trajs] THEN sliceD ← GGOutline.RemoveTraj[sliceD, traj];
ENDLOOP;
IF matchLevel = anywhere THEN { -- only include segs in info.segs
segDescriptor: SliceDescriptor ← GGSlice.WalkSegments[info.slice, ContainsSegs];
sliceD ← GGOutline.RemoveTraj[sliceD, info.trajs.first];
sliceD ← info.slice.class.unionParts[sliceD, segDescriptor];
};
};
ENDCASE => RETURN[info.slice.class.newParts[info.slice, NIL, slice]];
};
ChangeAll: PUBLIC GGUserInput.UserInputProc = {
matchData: MatchData ← MatchViewer.GetMatchData[];
searchState: SearchState ← GetSearchState[];
toSearch: GGData ← MatchViewer.GetGGInputFocus[];
subCount: NAT ← 0;
IF toSearch = NIL THEN RETURN; -- input focus wasn't in a Gargoyle viewer
SELECT matchData.searchOp FROM
disjunction => {
Match.SearchDisjInit[toSearch, event];
DO -- now do the substitute
found, success: BOOL;
[searchState.ahead, found] ← Match.SearchDisjNext[searchState.ahead, event.rest.first];
IF NOT found THEN EXIT;
success ← ReplaceOperation[];
IF NOT success THEN EXIT;
subCount ← subCount + 1;
ENDLOOP;
};
conjunction => {
SearchConjInit[toSearch, event];
DO -- now do the substitute
found, success: BOOL;
[searchState.ahead, searchState.behind, found] ← SearchConjNext[searchState.ahead, searchState.behind, event.rest.first];
IF NOT found THEN EXIT;
success ← ReplaceOperation[];
IF NOT success THEN EXIT;
subCount ← subCount + 1;
ENDLOOP;
};
ENDCASE => ERROR; -- Somebody must have implemented a new SearchOp
Feedback.PutFHerald[MatchViewer.GetToData[].feedback, oneLiner, "%g substitutions.", IO.card[subCount]];
};
Do Operations Routines
StartOps: PUBLIC GGUserInput.UserInputProc = {
searchState: Match.SearchState ← Match.GetSearchState[];
searchState.macroOps ← NIL;
searchState.macroOn ← TRUE;
[] ← SlackProcess.RegisterLogger[ggData.slackHandle, SaveOps];
[] ← SlackProcess.EnableSessionLogging[ggData.slackHandle];
};
SaveOps: SlackProcess.LoggingProc = {
This LoggingProc is initiated by StartOps, and terminated by EndOps. Save the event at the front of searchState.macroOps.
toData: GGData ← MatchViewer.GetToData[];
IF toData = clientData AND event.first # $MatchEndOps THEN { -- ie, event occurred in the To viewer, and it wasn't the $MatchEndOps atom
searchState.macroOps ← CONS[event, searchState.macroOps];
};
};
EndOps: PUBLIC GGUserInput.UserInputProc = {
searchState: Match.SearchState ← Match.GetSearchState[];
IF NOT searchState.macroOn THEN {
MatchViewer.ErrorFeedback["Must do StartOps before EndOps!"];
RETURN;
};
searchState.macroOn ← FALSE;
searchState.macroOps ← NARROW[GList.DReverse[searchState.macroOps]]; --order ops temporally
[] ← SlackProcess.DisableSessionLogging[ggData.slackHandle];
[] ← SlackProcess.RegisterLogger[ggData.slackHandle, GGSessionLog.EnterAction]; -- re-install old logging function
};
DoOps: PROC RETURNS [success: BOOLTRUE] = {
matchData: MatchData ← MatchViewer.GetMatchData[];
matchD: MatchDescriptor ← GetLastMatchDescriptor[];
searchState: Match.SearchState ← Match.GetSearchState[];
IF searchState.macroOn THEN {
MatchViewer.ErrorFeedback["First press EndOps button."];
RETURN[FALSE];
};
IF searchState.macroOps = NIL THEN {
MatchViewer.ErrorFeedback["No operations have been fetched from To viewer."];
RETURN[FALSE];
};
FOR opList: LIST OF LIST OF REF ANY ← searchState.macroOps, opList.rest UNTIL opList = NIL DO
newOp: LIST OF REF ANYNIL;
For each event, replace each REF Point with the proper transformed Point
FOR op: LIST OF REF ANY ← opList.first, op.rest UNTIL op = NIL DO
IF ISTYPE[op.first, REF Point] THEN {
newPoint: REF Point ← NEW[Point];
newPoint^ ← ImagerTransformation.Transform[GetMatchTransform[matchData, matchD], NARROW[op.first, REF Point]^];
newOp ← CONS[newPoint, newOp];
}
ELSE newOp ← CONS[op.first, newOp];
ENDLOOP;
newOp ← List.DReverse[newOp];
GGUserInput.PlayAction[matchData.theirData, newOp];
ENDLOOP;
Now, we must move the caret back to the last position, so the search proceeds harmoniously. This is complicated by the fact that we must set the caret back AFTER the queue has emptied. For this reason we've created a semaphore which is released when the $MatchReleaseSemaphore atom is reached by the slack process dispatcher. Grumble.
GGUserInput.PlayAction[matchData.theirData, LIST[$MatchReleaseSemaphore]];
P[];
GGCaret.SetAttractor[matchData.theirData.caret, matchData.lastCaretPos, [0, -1], NIL];
GGWindow.RestoreScreenAndInvariants[paintAction: $CaretMoved, ggData: matchData.theirData, remake: none, backgndOK: TRUE, edited: FALSE, okToClearFeedback: FALSE];
};
Synchronization Primitives (prevents doing another macro operation before the first is done).
MatchReleaseSemaphore: GGUserInput.UserInputProc = {
V[];
};
wakeUp: CONDITION;
resourceAllocated: BOOLTRUE;
P: ENTRY PROCEDURE [] = {
WHILE resourceAllocated DO
WAIT wakeUp;
ENDLOOP;
resourceAllocated ← TRUE;
};
V: ENTRY PROCEDURE [] = {
resourceAllocated ← FALSE;
NOTIFY wakeUp;
};
GGUserInput.RegisterAction[atom: $MatchReleaseSemaphore, eventProc: MatchReleaseSemaphore, argType: none, ensureUnique: FALSE]; -- An ACK, used to avoid monitor deadlock in the doOps mechanism
END.