DIRECTORY AtomButtons, BiScrollers, CodeTimer, FunctionCache, GGBasicTypes, GGCaret, GGCircleCache, GGCircles, GGGravity, GGInterfaceTypes, GGModelTypes, GGSegmentTypes, GGSequence, GGShapes, GGTraj, Imager, ImagerBackdoor, ImagerTransformation, Lines2d, Process, Real, RealFns, Rope, Vectors2d; GGGravityImpl: CEDAR PROGRAM IMPORTS AtomButtons, BiScrollers, CodeTimer, FunctionCache, GGCaret, GGCircleCache, GGCircles, GGSequence, GGShapes, GGTraj, Imager, ImagerBackdoor, Lines2d, Process, RealFns, Vectors2d EXPORTS GGGravity = BEGIN AlignmentCircle: TYPE = REF AlignmentCircleObj; AlignmentCircleObj: TYPE = GGInterfaceTypes.AlignmentCircleObj; AlignmentLine: TYPE = REF AlignmentLineObj; AlignmentLineObj: TYPE = GGInterfaceTypes.AlignmentLineObj; AlignmentPoint: TYPE = REF AlignmentPointObj; AlignmentPointObj: TYPE = GGInterfaceTypes.AlignmentPointObj; Angle: TYPE = GGBasicTypes.Angle; BoundBox: TYPE = GGModelTypes.BoundBox; Caret: TYPE = GGInterfaceTypes.Caret; Circle: TYPE = GGBasicTypes.Circle; Edge: TYPE = GGBasicTypes.Edge; FeatureData: TYPE = REF FeatureDataObj; FeatureDataObj: TYPE = GGModelTypes.FeatureDataObj; GGData: TYPE = GGInterfaceTypes.GGData; JointGenerator: TYPE = GGModelTypes.JointGenerator; Line: TYPE = GGBasicTypes.Line; AlignBag: TYPE = REF AlignBagObj; AlignBagObj: TYPE = GGInterfaceTypes.AlignBagObj; Outline: TYPE = GGModelTypes.Outline; OutlineDescriptor: TYPE = REF OutlineDescriptorObj; OutlineDescriptorObj: TYPE = GGModelTypes.OutlineDescriptorObj; Point: TYPE = GGBasicTypes.Point; Segment: TYPE = GGSegmentTypes.Segment; SegmentGenerator: TYPE = GGModelTypes.SegmentGenerator; Sequence: TYPE = GGModelTypes.Sequence; Slice: TYPE = GGModelTypes.Slice; SliceDescriptor: TYPE = REF SliceDescriptorObj; SliceDescriptorObj: TYPE = GGModelTypes.SliceDescriptorObj; SliceParts: TYPE = GGModelTypes.SliceParts; Traj: TYPE = GGModelTypes.Traj; TriggerBag: TYPE = GGInterfaceTypes.TriggerBag; Vector: TYPE = GGBasicTypes.Vector; Problem: PUBLIC SIGNAL [msg: Rope.ROPE] = CODE; maxDistance: REAL = 99999.0; noPoint: Point = [0.0, 0.0]; noJoint: NAT = 9999; noCP: NAT = 8888; noSeg: NAT = 7777; FeatureFromOutline: PUBLIC PROC [outline: Outline, parts: SliceParts _ NIL] RETURNS [feature: FeatureData] = { sliceParts: SliceParts _ IF parts=NIL THEN outline.class.newParts[outline, NIL, slice].parts ELSE parts; outlineD: OutlineDescriptor _ NEW[OutlineDescriptorObj _ [slice: outline, parts: sliceParts]]; feature _ NEW[FeatureDataObj]; feature.type _ outline; feature.shape _ outlineD; }; FeatureFromSlice: PUBLIC PROC [slice: Slice, parts: SliceParts _ NIL] RETURNS [feature: FeatureData] = { sliceParts: SliceParts _ IF parts=NIL THEN slice.class.newParts[slice, NIL, slice].parts ELSE parts; -- may get a descriptor of the whole slice sliceD: SliceDescriptor _ NEW[SliceDescriptorObj _ [slice: slice, parts: sliceParts]]; feature _ NEW[FeatureDataObj]; feature.type _ slice; feature.shape _ sliceD; }; FeatureFromAnchor: PUBLIC PROC [anchor: Caret] RETURNS [feature: FeatureData] = { feature _ NEW[FeatureDataObj]; feature.type _ anchor; -- that is, anchor => the member of the enumerated type feature.shape _ anchor; -- that is, anchor => the parameter }; JointAddSlopeLine: PUBLIC PROC [degrees: REAL, direction: Vector, point: Point, objectBag: AlignBag] RETURNS [feature: FeatureData] = { line: Line; coincident: FeatureData _ LineThru[point, degrees, objectBag.slopeLines]; IF coincident # NIL THEN { alignmentLine: AlignmentLine _ NARROW[coincident.shape]; alignmentLine.triggerPoints _ CONS[point, alignmentLine.triggerPoints]; feature _ NIL; } ELSE { alignmentLine: AlignmentLine; line _ Lines2d.LineFromPointAndVector[point, direction]; alignmentLine _ NEW[AlignmentLineObj _ [line: line, degrees: degrees, triggerPoints: CONS[point, NIL]]]; feature _ NEW[FeatureDataObj]; feature.type _ slopeLine; feature.shape _ alignmentLine; AddFeature[feature, objectBag]; }; }; JointAddCircle: PUBLIC PROC [radius: REAL, point: Point, objectBag: AlignBag] RETURNS [feature: FeatureData] = { circle: Circle; coincident: FeatureData; coincident _ SameCircle[point, radius, objectBag.radiiCircles]; IF coincident # NIL THEN { alignmentCircle: AlignmentCircle _ NARROW[coincident.shape]; alignmentCircle.triggerPoints _ CONS[point, alignmentCircle.triggerPoints]; feature _ NIL; } ELSE { alignmentCircle: AlignmentCircle; circle _ GGCircles.CircleFromPointAndRadius[point, radius]; alignmentCircle _ NEW[AlignmentCircleObj _ [circle: circle, triggerPoints: CONS[point, NIL]]]; feature _ NEW[FeatureDataObj]; feature.type _ radiiCircle; feature.shape _ alignmentCircle; AddFeature[feature, objectBag]; }; }; SameCircle: PRIVATE PROC [point: Point, radius: REAL, list: LIST OF FeatureData] RETURNS [coincident: FeatureData] = { circle: Circle; FOR l: LIST OF FeatureData _ list, l.rest UNTIL l = NIL DO circle _ NARROW[l.first.shape, AlignmentCircle].circle; IF RealFns.AlmostEqual[circle.origin.x, point.x, -10] AND RealFns.AlmostEqual[circle.origin.y, point.y, -10] AND RealFns.AlmostEqual[circle.radius, radius, -10] THEN RETURN[l.first]; ENDLOOP; RETURN[NIL]; }; LineThru: PRIVATE PROC [point: Point, degrees: REAL, list: LIST OF FeatureData] RETURNS [coincident: FeatureData] = { epsilon: REAL = 1.0e-5; line: Line; FOR l: LIST OF FeatureData _ list, l.rest UNTIL l = NIL DO line _ NARROW[l.first.shape, AlignmentLine].line; IF NARROW[l.first.shape, AlignmentLine].degrees = degrees AND Lines2d.LineDistance[point, line] < epsilon THEN RETURN[l.first]; ENDLOOP; RETURN[NIL]; }; SegmentAddTwoAngleLines: PUBLIC PROC [degrees: REAL, segNum: NAT, lo, hi: Point, objectBag: AlignBag] RETURNS [line1, line2: FeatureData] = { loLine: Line _ Lines2d.LineFromPointAndAngle[pt: lo, degrees: Vectors2d.AngleFromVector[[x: hi.x-lo.x, y: hi.y-lo.y]]+degrees]; hiLine: Line _ Lines2d.LineFromPointAndAngle[pt: hi, degrees: Vectors2d.AngleFromVector[[x: lo.x-hi.x, y: lo.y-hi.y]]+degrees]; line1 _ NEW[FeatureDataObj]; line1.type _ angleLine; line1.shape _ NEW[AlignmentLineObj _ [line: loLine, degrees: degrees, triggerPoints: NIL]]; AddFeature[line1, objectBag]; line2 _ NEW[FeatureDataObj]; line2.type _ angleLine; line2.shape _ NEW[AlignmentLineObj _ [line: hiLine, degrees: degrees, triggerPoints: NIL]]; AddFeature[line2, objectBag]; }; SegmentAddDistanceLines: PUBLIC PROC [distance: REAL, segNum: NAT, lo, hi: Point, objectBag: AlignBag] RETURNS [line1, line2: FeatureData] = { line, leftLine, rightLine: Line; line _ Lines2d.LineFromPoints[lo, hi]; leftLine _ Lines2d.LineLeftOfLine[line, distance]; line1 _ NEW[FeatureDataObj]; line1.type _ distanceLine; line1.shape _ leftLine; AddFeature[line1, objectBag]; IF ABS[distance] > 0.0 THEN { rightLine _ Lines2d.LineRightOfLine[line, distance]; line2 _ NEW[FeatureDataObj]; line2.type _ distanceLine; line2.shape _ rightLine; AddFeature[line2, objectBag]; } ELSE line2 _ NIL; }; SegmentAddMidpoint: PUBLIC PROC [segNum: NAT, lo, hi: Point, objectBag: AlignBag] RETURNS [feature: FeatureData] = { midpoint: Point; alignmentPoint: AlignmentPoint; midpoint _ Vectors2d.Scale[Vectors2d.Add[lo, hi], 0.5]; alignmentPoint _ NEW[AlignmentPointObj _ [point: midpoint, tangent: FALSE]]; feature _ NEW[FeatureDataObj]; feature.type _ midpoint; feature.shape _ alignmentPoint; AddFeature[feature, objectBag]; }; AddFeature: PRIVATE PROC [featureData: FeatureData, objectBag: AlignBag] = { Process.CheckForAbort[]; SELECT featureData.type FROM midpoint => objectBag.midpoints _ CONS[featureData, objectBag.midpoints]; distanceLine => objectBag.distanceLines _ CONS[featureData, objectBag.distanceLines]; slopeLine => objectBag.slopeLines _ CONS[featureData, objectBag.slopeLines]; angleLine => objectBag.angleLines _ CONS[featureData, objectBag.angleLines]; radiiCircle => objectBag.radiiCircles _ CONS[featureData, objectBag.radiiCircles]; intersectionPoint => objectBag.intersectionPoints _ CONS[featureData, objectBag.intersectionPoints]; anchor => objectBag.anchor _ featureData; ENDCASE => SIGNAL Problem[msg: "Unimplemented feature type"]; }; alignmentColor: Imager.Color _ ImagerBackdoor.MakeStipple[145065B]; checkerColor: Imager.Color _ ImagerBackdoor.MakeStipple[122645B]; useCache: BOOL _ TRUE; DrawFeatureList: PUBLIC PROC [dc: Imager.Context, alignObjects: LIST OF FeatureData, ggData: GGData] = { DrawLineAux: PROC [line: Line] = { -- uses FunctionCache to avoid duplication LineCompare: PROC [argument: FunctionCache.Domain] RETURNS [good: BOOL] = { thisLine: Line _ NARROW[argument]; RETURN[thisLine.d = line.d AND thisLine.theta = line.theta]; }; ok: BOOL; [----, ok] _ FunctionCache.Lookup[ggData.refresh.lineCache, LineCompare]; IF ok THEN RETURN ELSE { FunctionCache.Insert[ggData.refresh.lineCache, line, NIL, 2]; }; IF ABS[line.theta] < 1.0 OR ABS[line.theta - 90.0] < 1.0 THEN { Imager.SetColor[dc, checkerColor]; GGShapes.DrawLine[dc, line, rect, 0.0]; -- 0 width lines go fast Imager.SetColor[dc, alignmentColor]; } ELSE GGShapes.DrawLine[dc, line, rect, 0.0]; -- 0 width lines go fast }; DrawCircleAux: PROC [circle: Circle] = { -- uses GGCircleCache cachedCircle: GGCircleCache.CachedCircle; IF (cachedCircle _ GGCircleCache.Lookup[ggData.hitTest.radiusCircleCache, circle.radius])#NIL THEN GGCircleCache.DrawCachedCircle[dc, circle.origin, cachedCircle] -- cache hit ELSE { -- cache miss. Attempt a cache entry and retry GGCircleCache.Insert[ggData.hitTest.radiusCircleCache, circle.radius]; IF (cachedCircle _ GGCircleCache.Lookup[ggData.hitTest.radiusCircleCache, circle.radius])#NIL THEN GGCircleCache.DrawCachedCircle[dc, circle.origin, cachedCircle] ELSE GGShapes.DrawCircle[dc, circle]; }; }; rect: Imager.Rectangle _ BiScrollers.ViewportBox[ggData.biScroller]; Imager.SetStrokeWidth[dc, 1.0]; Imager.SetColor[dc, alignmentColor]; FOR list: LIST OF FeatureData _ alignObjects, list.rest UNTIL list = NIL DO WITH list.first.shape SELECT FROM slopeLine: AlignmentLine => DrawLineAux[slopeLine.line]; angleLine: AlignmentLine => DrawLineAux[angleLine.line]; circle: AlignmentCircle => IF useCache THEN DrawCircleAux[circle.circle] ELSE GGShapes.DrawCircle[dc, circle.circle]; distanceLine: GGBasicTypes.Line => GGShapes.DrawLine[dc, distanceLine, rect]; ENDCASE => ERROR; ENDLOOP; }; DrawAlignBagRegardless: PUBLIC PROC [dc: Imager.Context, objectBag: AlignBag, ggData: GGData] = { IF objectBag = NIL THEN RETURN; DrawFeatureList[dc, objectBag.slopeLines, ggData]; DrawFeatureList[dc, objectBag.angleLines, ggData]; DrawFeatureList[dc, objectBag.radiiCircles, ggData]; DrawFeatureList[dc, objectBag.distanceLines, ggData]; }; GoodCurve: TYPE = REF GoodCurveObj; GoodCurveObj: TYPE = RECORD [ dist: REAL _ maxDistance, -- the distance from point to testPoint segNum: NAT _ noSeg, -- the best segment of this curve (if it is a trajectory) point: Point, -- the best point on this curve featureData: FeatureData _ NIL, -- this curve hitData: REF ANY ]; GoodPointType: TYPE = {joint, controlPoint, slice, intersectionPoint, midpoint, none}; GoodPoint: TYPE = REF GoodPointObj; GoodPointObj: TYPE = RECORD [ type: GoodPointType _ none, dist: REAL _ maxDistance, point: Point _ noPoint, joint: NAT _ noJoint, -- the joint of the trajectory mentioned in FeatureData (if this is a joint) controlPoint: NAT _ noCP, -- the control point number (if this is a control point) segNum: NAT _ noSeg, -- the segment to which the control point belongs featureData: FeatureData _ NIL, -- this point hitData: REF ANY ]; UniMap: PUBLIC PROC [testPoint: Point, criticalR: REAL, currentObjects: AlignBag, activeObjects: TriggerBag, ggData: GGData, intersections: BOOL] RETURNS [resultPoint: Point, feature: FeatureData, hitData: REF ANY] = { ENABLE UNWIND => ggData.gravityPool _ NewGravityPool[]; -- perhaps ABORT happened while pool was in use CodeTimer.StartInt[$UniMap, $Gargoyle]; SELECT AtomButtons.GetButtonState[ggData.hitTest.gravButton] FROM on => SELECT ggData.hitTest.gravityType FROM strictDistance => [resultPoint, feature, hitData] _ StrictDistance[testPoint, criticalR, currentObjects, activeObjects, ggData, intersections]; innerCircle => [resultPoint, feature, hitData] _ InnerCircle[testPoint, criticalR, ggData.hitTest.innerR, currentObjects, activeObjects, ggData, intersections]; ENDCASE => ERROR; off => { resultPoint _ testPoint; feature _ NIL; }; ENDCASE => ERROR; CodeTimer.StopInt[$UniMap, $Gargoyle]; }; StrictDistance: PROC [testPoint: Point, criticalR: REAL, currentObjects: AlignBag, activeObjects: TriggerBag, ggData: GGData, intersections: BOOL] RETURNS [resultPoint: Point, feature: FeatureData, hitData: REF ANY] = { thisCurve, bestCurve: GoodCurve; thisPoint, bestPoint: GoodPoint; IF EmptyBag[currentObjects] AND EmptyTriggers[activeObjects] --AND NOT (intersections AND GGCaret.Exists[ggData.anchor]) -- THEN { resultPoint _ testPoint; feature _ NIL; RETURN; }; [thisPoint, bestPoint, thisCurve, bestCurve] _ BestPointAndCurve[currentObjects, activeObjects, testPoint, criticalR, ggData.anchor, intersections, ggData]; IF bestPoint.type = none AND bestCurve.dist = maxDistance THEN { feature _ NIL; resultPoint _ testPoint; ReturnPointsToPool[thisPoint, bestPoint, ggData]; ReturnCurvesToPool[thisCurve, bestCurve, ggData]; RETURN}; IF (NOT bestPoint.type = none) AND bestPoint.dist < criticalR AND bestPoint.dist <= bestCurve.dist THEN { -- use the best point resultPoint _ bestPoint.point; feature _ bestPoint.featureData; hitData _ bestPoint.hitData; } ELSE IF bestCurve.dist < criticalR THEN { resultPoint _ bestCurve.point; feature _ bestCurve.featureData; hitData _ bestCurve.hitData; } ELSE { feature _ NIL; resultPoint _ testPoint; }; ReturnPointsToPool[thisPoint, bestPoint, ggData]; ReturnCurvesToPool[thisCurve, bestCurve, ggData]; }; -- StrictDistance InnerCircle: PROC [testPoint: Point, criticalR: REAL, innerR: REAL, currentObjects: AlignBag, activeObjects: TriggerBag, ggData: GGData, intersections: BOOL] RETURNS [resultPoint: Point, feature: FeatureData, hitData: REF ANY] = { thisCurve, bestCurve: GoodCurve; thisPoint, bestPoint: GoodPoint; IF EmptyBag[currentObjects] AND EmptyTriggers[activeObjects] --AND NOT (intersections AND GGCaret.Exists[ggData.anchor])-- THEN { resultPoint _ testPoint; feature _ NIL; RETURN; }; [thisPoint, bestPoint, thisCurve, bestCurve] _ BestPointAndCurve[currentObjects, activeObjects, testPoint, criticalR, ggData.anchor, intersections, ggData]; IF bestPoint.type = none AND bestCurve.dist = maxDistance THEN { feature _ NIL; resultPoint _ testPoint; ReturnPointsToPool[thisPoint, bestPoint, ggData]; ReturnCurvesToPool[thisCurve, bestCurve, ggData]; RETURN}; IF (NOT bestPoint.type = none) AND (bestPoint.dist < innerR OR (bestPoint.dist < criticalR AND bestPoint.dist <= bestCurve.dist)) THEN { -- use the best point resultPoint _ bestPoint.point; feature _ bestPoint.featureData; hitData _ bestPoint.hitData; } ELSE IF bestCurve.dist < criticalR THEN { resultPoint _ bestCurve.point; feature _ bestCurve.featureData; hitData _ bestCurve.hitData; } ELSE { feature _ NIL; resultPoint _ testPoint; }; ReturnPointsToPool[thisPoint, bestPoint, ggData]; ReturnCurvesToPool[thisCurve, bestCurve, ggData]; }; -- InnerCircle BestPointAndCurve: PROC [currentObjects: AlignBag, sceneObjects: TriggerBag, testPoint: Point, tolerance: REAL, anchor: Caret, intersections: BOOL, ggData: GGData] RETURNS [thisPoint, bestPoint: GoodPoint, thisCurve, bestCurve: GoodCurve] = { epsilon: REAL = 1.0e-3; line: Line; circle: Circle; sliceD: SliceDescriptor; success: BOOL _ FALSE; featureData: FeatureData; [thisPoint, bestPoint] _ GetPointsFromPool[ggData]; [thisCurve, bestCurve] _ GetCurvesFromPool[ggData]; bestCurve^ _ [maxDistance, noSeg, noPoint, NIL]; -- magic values for debugging bestPoint^ _ [none, maxDistance, noPoint, noJoint, noCP, noSeg, NIL]; -- magic values for debugging FOR slopeLines: LIST OF FeatureData _ currentObjects.slopeLines, slopeLines.rest UNTIL slopeLines = NIL DO featureData _ slopeLines.first; line _ NARROW[featureData.shape, AlignmentLine].line; thisCurve.dist _ Lines2d.LineDistance[testPoint, line]; success _ thisCurve.dist < tolerance; IF success AND thisCurve.dist < bestCurve.dist THEN { thisCurve.point _ Lines2d.DropPerpendicular[testPoint, line]; thisCurve.featureData _ featureData; thisCurve.hitData _ NIL; UpdateBestCurve[thisCurve, bestCurve]; }; ENDLOOP; FOR angleLines: LIST OF FeatureData _ currentObjects.angleLines, angleLines.rest UNTIL angleLines = NIL DO featureData _ angleLines.first; line _ NARROW[featureData.shape, AlignmentLine].line; thisCurve.dist _ Lines2d.LineDistance[testPoint, line]; success _ thisCurve.dist < tolerance; IF success AND thisCurve.dist < bestCurve.dist THEN { thisCurve.point _ Lines2d.DropPerpendicular[testPoint, line]; thisCurve.featureData _ featureData; thisCurve.hitData _ NIL; UpdateBestCurve[thisCurve, bestCurve]; }; ENDLOOP; FOR dLines: LIST OF FeatureData _ currentObjects.distanceLines, dLines.rest UNTIL dLines = NIL DO featureData _ dLines.first; thisCurve.dist _ Lines2d.LineDistance[testPoint, (line _ NARROW[featureData.shape])]; success _ thisCurve.dist < tolerance; IF success AND thisCurve.dist < bestCurve.dist THEN { thisCurve.point _ Lines2d.DropPerpendicular[testPoint, line]; thisCurve.featureData _ featureData; thisCurve.hitData _ NIL; UpdateBestCurve[thisCurve, bestCurve]; }; ENDLOOP; FOR iPoints: LIST OF FeatureData _ currentObjects.intersectionPoints, iPoints.rest UNTIL iPoints = NIL DO featureData _ iPoints.first; thisPoint.type _ intersectionPoint; thisPoint.dist _ Vectors2d.Distance[NARROW[featureData.shape, AlignmentPoint].point, testPoint]; success _ thisPoint.dist < tolerance; IF success AND thisPoint.dist < bestPoint.dist THEN { thisPoint.point _ NARROW[featureData.shape, AlignmentPoint].point; thisPoint.featureData _ featureData; thisPoint.hitData _ NIL; UpdateBestPoint[thisPoint, bestPoint]; }; ENDLOOP; FOR midpoints: LIST OF FeatureData _ currentObjects.midpoints, midpoints.rest UNTIL midpoints = NIL DO featureData _ midpoints.first; thisPoint.type _ midpoint; thisPoint.dist _ Vectors2d.Distance[NARROW[featureData.shape, AlignmentPoint].point, testPoint]; success _ thisPoint.dist < tolerance; IF success AND thisPoint.dist < bestPoint.dist THEN { thisPoint.point _ NARROW[featureData.shape, AlignmentPoint].point; thisPoint.featureData _ featureData; thisPoint.hitData _ NIL; UpdateBestPoint[thisPoint, bestPoint]; }; ENDLOOP; FOR circles: LIST OF FeatureData _ currentObjects.radiiCircles, circles.rest UNTIL circles = NIL DO featureData _ circles.first; circle _ NARROW[featureData.shape, AlignmentCircle].circle; thisCurve.dist _ GGCircles.CircleDistance[testPoint, circle]; success _ thisCurve.dist < tolerance; IF success AND thisCurve.dist < bestCurve.dist THEN { thisCurve.point _ GGCircles.PointProjectedOntoCircle[testPoint, circle]; thisCurve.featureData _ featureData; thisCurve.hitData _ NIL; UpdateBestCurve[thisCurve, bestCurve]; }; ENDLOOP; FOR slices: LIST OF FeatureData _ sceneObjects.slices, slices.rest UNTIL slices = NIL DO featureData _ slices.first; sliceD _ NARROW[featureData.shape, SliceDescriptor]; [thisCurve.point, thisCurve.dist, thisCurve.hitData, success] _ sliceD.slice.class.closestSegment[sliceD, testPoint, tolerance]; IF success AND (thisCurve.dist - epsilon <= bestCurve.dist) THEN { -- so segments are preferred to slopelines bestCurve.dist _ thisCurve.dist; bestCurve.segNum _ thisCurve.segNum; bestCurve.point _ thisCurve.point; bestCurve.hitData _ thisCurve.hitData; bestCurve.featureData _ featureData; }; [thisPoint.point, thisPoint.dist, thisPoint.hitData, success] _ sliceD.slice.class.closestPoint[sliceD, testPoint, tolerance]; IF success AND (thisPoint.dist - epsilon <= bestPoint.dist) THEN { bestPoint.type _ slice; bestPoint.dist _ thisPoint.dist; bestPoint.point _ thisPoint.point; bestPoint.hitData _ thisPoint.hitData; bestPoint.joint _ 999; bestPoint.featureData _ featureData; }; ENDLOOP; featureData _ currentObjects.anchor; IF featureData # NIL THEN { anchor: Caret _ NARROW[featureData.shape]; IF NOT GGCaret.Exists[anchor] THEN ERROR; thisPoint.type _ intersectionPoint; -- REALLY CARETPOINT thisPoint.point _ GGCaret.GetPoint[anchor]; thisPoint.dist _ Vectors2d.Distance[thisPoint.point, testPoint]; thisPoint.hitData _ NIL; IF thisPoint.dist < tolerance AND thisPoint.dist < bestPoint.dist THEN { thisPoint.featureData _ featureData; UpdateBestPoint[thisPoint, bestPoint]; }; }; }; UpdateBestCurve: PROC [thisCurve, bestCurve: GoodCurve] = { bestCurve.dist _ thisCurve.dist; bestCurve.segNum _ noSeg; -- magic number for debugging bestCurve.point _ thisCurve.point; bestCurve.featureData _ thisCurve.featureData; }; UpdateBestPoint: PROC [thisPoint, bestPoint: GoodPoint] = { bestPoint.type _ thisPoint.type; bestPoint.dist _ thisPoint.dist; bestPoint.point _ thisPoint.point; bestPoint.joint _ noSeg; -- magic number for debugging bestPoint.featureData _ thisPoint.featureData; }; EmptyBag: PROC [objectBag: AlignBag] RETURNS [BOOL] = { RETURN[ objectBag.slopeLines = NIL AND objectBag.angleLines = NIL AND objectBag.radiiCircles = NIL AND objectBag.distanceLines = NIL AND objectBag.midpoints = NIL AND objectBag.intersectionPoints = NIL AND objectBag.anchor = NIL]; }; EmptyTriggers: PROC [triggerBag: TriggerBag] RETURNS [BOOL] = { RETURN[ triggerBag.slices = NIL AND triggerBag.intersectionPoints = NIL AND triggerBag.anchor = NIL ]; }; useBBox: BOOL _ TRUE; PointIsInBox: PROC [test: Point, box: GGBasicTypes.BoundBoxObj] RETURNS [BOOL] = { RETURN[ NOT (test.x < box.loX OR test.x > box.hiX OR test.y < box.loY OR test.y > box.hiY) ]; }; NearestSegment: PROC [testPoint: Point, seq: Sequence, tolerance: REAL] RETURNS [bestDist: REAL, bestSeg: NAT, bestPoint: Point, success: BOOL] = { thisDist2, bestDist2: REAL; thisPoint: Point; segGen: SegmentGenerator; thisSuccess: BOOL; next: GGSequence.SegAndIndex; bigBox: GGBasicTypes.BoundBoxObj; success _ FALSE; IF useBBox THEN { bigBox _ [seq.boundBox.loX-tolerance, seq.boundBox.loY-tolerance, seq.boundBox.hiX+tolerance, seq.boundBox.hiY+tolerance, FALSE, FALSE]; IF NOT PointIsInBox[testPoint, bigBox] THEN RETURN; -- success=FALSE }; segGen _ GGSequence.SegmentsInSequence[seq]; next _ GGSequence.NextSegmentAndIndex[segGen]; IF next.seg = NIL THEN { -- The sequence is a single joint bestDist _ maxDistance; -- magic number for debugging bestSeg _ noSeg; -- magic number meaning no segment bestPoint _ [-1.0, -1.0]; RETURN; }; [bestPoint, thisSuccess] _ next.seg.class.closestPoint[next.seg, testPoint, tolerance]; IF NOT thisSuccess THEN { bestDist2 _ maxDistance; bestSeg _ noSeg; bestPoint _ [-1.0, -1.0]; } ELSE { bestDist2 _ Vectors2d.DistanceSquared[bestPoint, testPoint]; bestSeg _ next.index; success _ TRUE; }; FOR next _ GGSequence.NextSegmentAndIndex[segGen], GGSequence.NextSegmentAndIndex[segGen] UNTIL next.seg = NIL DO [thisPoint, thisSuccess] _ next.seg.class.closestPoint[next.seg, testPoint, tolerance]; IF NOT thisSuccess THEN { thisDist2 _ maxDistance; thisPoint _ [-1.0, -1.0]; } ELSE { thisDist2 _ Vectors2d.DistanceSquared[thisPoint, testPoint]; success _ TRUE; }; IF success AND thisDist2 < bestDist2 THEN { bestDist2 _ thisDist2; bestSeg _ next.index; bestPoint _ thisPoint; }; ENDLOOP; bestDist _ RealFns.SqRt[bestDist2]; }; NearestJoint: PROC [testPoint: Point, seq: Sequence, tolerance: REAL] RETURNS [bestDist: REAL, bestJoint: NAT, bestPoint: Point, success: BOOL] = { tolerance2: REAL _ tolerance*tolerance; thisDist2, bestDist2: REAL; thisPoint: Point; jointGen: JointGenerator; index: INT; bigBox: GGBasicTypes.BoundBoxObj; success _ FALSE; IF useBBox THEN { bigBox _ [seq.boundBox.loX-tolerance, seq.boundBox.loY-tolerance, seq.boundBox.hiX+tolerance, seq.boundBox.hiY+tolerance, FALSE, FALSE]; IF NOT PointIsInBox[testPoint, bigBox] THEN RETURN; -- success=FALSE }; jointGen _ GGSequence.JointsInSequence[seq]; index _ GGSequence.NextJoint[jointGen]; IF index = -1 THEN { bestDist _ maxDistance; bestJoint _ noJoint; bestPoint _ [-1.0, -1.0]; RETURN; }; bestPoint _ GGTraj.FetchJointPos[seq.traj, index]; bestDist2 _ Vectors2d.DistanceSquared[bestPoint, testPoint]; bestJoint _ index; IF bestDist2 < tolerance2 THEN success _ TRUE; FOR index _ GGSequence.NextJoint[jointGen], GGSequence.NextJoint[jointGen] UNTIL index = -1 DO thisPoint _ GGTraj.FetchJointPos[seq.traj, index]; thisDist2 _ Vectors2d.DistanceSquared[thisPoint, testPoint]; IF thisDist2 < tolerance2 THEN success _ TRUE; IF success AND thisDist2 < bestDist2 THEN { bestDist2 _ thisDist2; bestJoint _ index; bestPoint _ thisPoint; }; ENDLOOP; bestDist _ RealFns.SqRt[bestDist2]; }; NearestControlPoint: PROC [testPoint: Point, seq: Sequence, tolerance: REAL] RETURNS [bestDist: REAL, bestSeg: NAT, bestControlPoint: NAT, bestPoint: Point, success: BOOL] = { SomeCP: PROC [i: NAT] RETURNS [BOOL] = { cpCount: NAT _ seq.controlPoints[i].len; FOR j: NAT IN [0..cpCount) DO IF seq.controlPoints[i][j] THEN RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; }; tolerance2: REAL _ tolerance*tolerance; thisDist2, bestDist2: REAL; thisControlPoint: NAT; thisPoint: Point; segGen: SegmentGenerator; thisSuccess: BOOL; next: GGSequence.SegAndIndex; bigBox: GGBasicTypes.BoundBoxObj; success _ FALSE; IF useBBox THEN { bigBox _ [seq.boundBox.loX-tolerance, seq.boundBox.loY-tolerance, seq.boundBox.hiX+tolerance, seq.boundBox.hiY+tolerance, FALSE, FALSE]; IF NOT PointIsInBox[testPoint, bigBox] THEN RETURN; -- success=FALSE }; segGen _ GGSequence.SegmentsInSequence[seq]; next _ GGSequence.NextSegmentAndIndex[segGen]; WHILE next.seg#NIL AND NOT SomeCP[next.index] DO next _ GGSequence.NextSegmentAndIndex[segGen]; ENDLOOP; IF next.seg = NIL THEN { -- The sequence has no control points bestDist _ maxDistance; -- magic number meaning no segment bestControlPoint _ noCP; -- magic number meaning no CP bestPoint _ [-1.0, -1.0]; RETURN; }; [bestPoint, thisControlPoint, thisSuccess] _ next.seg.class.closestControlPoint[next.seg, testPoint, tolerance]; IF NOT thisSuccess THEN { bestDist2 _ maxDistance; bestSeg _ noSeg; bestControlPoint _ noCP; bestPoint _ [-1.0, -1.0]; } ELSE { bestDist2 _ Vectors2d.DistanceSquared[bestPoint, testPoint]; bestSeg _ next.index; bestControlPoint _ thisControlPoint; success _ TRUE; }; FOR next _ GGSequence.NextSegmentAndIndex[segGen], GGSequence.NextSegmentAndIndex[segGen] UNTIL next.seg = NIL DO IF SomeCP[next.index] THEN { [thisPoint, thisControlPoint, thisSuccess] _ next.seg.class.closestControlPoint[next.seg, testPoint, tolerance]; IF NOT thisSuccess THEN { thisDist2 _ maxDistance; thisPoint _ [-1.0, -1.0]; } ELSE { thisDist2 _ Vectors2d.DistanceSquared[thisPoint, testPoint]; success _ TRUE; }; IF success AND thisDist2 < bestDist2 THEN { bestDist2 _ thisDist2; bestSeg _ next.index; bestControlPoint _ thisControlPoint; bestPoint _ thisPoint; }; }; ENDLOOP; bestDist _ RealFns.SqRt[bestDist2]; }; maxPoints: NAT = 6; maxCurves: NAT = 6; GravityPool: TYPE = REF GravityPoolObj; GravityPoolObj: TYPE = RECORD [ pointPoolIndex: NAT _ 0, pointPool: ARRAY [0..maxPoints) OF GoodPoint, curvePoolIndex: NAT _ 0, curvePool: ARRAY [0..maxCurves) OF GoodCurve ]; GetPointsFromPool: PROC [ggData: GGData] RETURNS [p1, p2: GoodPoint] = { pool: GravityPool _ NARROW[ggData.gravityPool]; IF pool.pointPoolIndex >= maxPoints THEN ERROR; p1 _ pool.pointPool[pool.pointPoolIndex]; p2 _ pool.pointPool[pool.pointPoolIndex+1]; pool.pointPoolIndex _ pool.pointPoolIndex + 2; }; ReturnPointsToPool: PROC [p1, p2: GoodPoint, ggData: GGData] = { pool: GravityPool _ NARROW[ggData.gravityPool]; IF pool.pointPoolIndex<2 THEN ERROR; pool.pointPoolIndex _ pool.pointPoolIndex - 1; pool.pointPool[pool.pointPoolIndex] _ p1; pool.pointPoolIndex _ pool.pointPoolIndex - 1; pool.pointPool[pool.pointPoolIndex] _ p2; }; GetCurvesFromPool: PROC [ggData: GGData] RETURNS [c1, c2: GoodCurve] = { pool: GravityPool _ NARROW[ggData.gravityPool]; IF pool.curvePoolIndex >= maxCurves THEN ERROR; c1 _ pool.curvePool[pool.curvePoolIndex]; c2 _ pool.curvePool[pool.curvePoolIndex+1]; pool.curvePoolIndex _ pool.curvePoolIndex + 2; }; ReturnCurvesToPool: PROC [c1, c2: GoodCurve, ggData: GGData] = { pool: GravityPool _ NARROW[ggData.gravityPool]; IF pool.curvePoolIndex<2 THEN ERROR; pool.curvePoolIndex _ pool.curvePoolIndex - 1; pool.curvePool[pool.curvePoolIndex] _ c1; pool.curvePoolIndex _ pool.curvePoolIndex - 1; pool.curvePool[pool.curvePoolIndex] _ c2; }; NewGravityPool: PUBLIC PROC [] RETURNS [REF] = { -- reuseable storage for BestPointAndCurve proc to avoid NEWs pool: GravityPool _ NEW[GravityPoolObj]; -- indices are automatically made =0 FOR i: NAT IN [0..maxPoints) DO pool.pointPool[i] _ NEW[GoodPointObj]; ENDLOOP; FOR i: NAT IN [0..maxCurves) DO pool.curvePool[i] _ NEW[GoodCurveObj]; ENDLOOP; RETURN[pool]; }; InitStats: PROC [] = { interval: CodeTimer.Interval; interval _ CodeTimer.CreateInterval[$UniMap]; CodeTimer.AddInt[interval, $Gargoyle]; }; InitStats[]; END. #€GGGravityImpl.mesa Copyright c 1985 by Xerox Corporation. All rights reserved. Last edited by Bier on April 7, 1987 0:41:17 am PDT Contents: Procedures which take a test point and map it to a point on a nearby set of objects. Pier, May 13, 1987 2:35:53 pm PDT Magic Numbers for Debugging Building the Trigger Bag Building the Object Bag The angle (degrees) and direction vector are the global information. lineList is used to avoid redundant lines. For now, each slope line only remembers one of the joints which it passes thru. radius is global. [point, jointNum, traj] describes the joint. Each circle remembers all of the joints at its center. IF circle.origin = point AND circle.radius = radius THEN RETURN[l.first]; line1.hitPart _ NEW[SequencePartObj _ [segNum, -1, -1]]; line2.hitPart _ NEW[SequencePartObj _ [segNum, -1, -1]]; No global information is needed. [segNum, traj] describes the segment. line1.hitPart _ NEW[SequencePartObj _ [segNum, -1, -1]]; line2.hitPart _ NEW[SequencePartObj _ [segNum, -1, -1]]; No global information is needed. [segNum, traj] describes the segment. feature.hitPart _ NEW[GGInterfaceTypes.SequencePartObj _ [segNum, -1, -1]]; AddLineIntersections: PROC [featureData: FeatureData, objectBag: AlignBag] = { iPoint: Point; parallel: BOOL; points: ARRAY[1..2] OF Point; hitCount: NAT; line: Line; WITH featureData.shape SELECT FROM l: Line => line _ l; aL: AlignmentLine => line _ aL.line; ENDCASE => ERROR Problem[msg: "Broken Invariant"]; FOR lineList: LIST OF FeatureData _ objectBag.slopeLines, lineList.rest UNTIL lineList = NIL DO [iPoint, parallel] _ Lines2d.LineMeetsLine[NARROW[lineList.first.shape, AlignmentLine].line, line]; IF NOT parallel THEN { AddIntersectionPointFeature[iPoint, featureData, lineList.first, objectBag]; }; ENDLOOP; FOR lineList: LIST OF FeatureData _ objectBag.angleLines, lineList.rest UNTIL lineList = NIL DO [iPoint, parallel] _ Lines2d.LineMeetsLine[NARROW[lineList.first.shape, AlignmentLine].line, line]; IF NOT parallel THEN { AddIntersectionPointFeature[iPoint, featureData, lineList.first, objectBag]; }; ENDLOOP; FOR dlineList: LIST OF FeatureData _ objectBag.distanceLines, dlineList.rest UNTIL dlineList = NIL DO [iPoint, parallel] _ Lines2d.LineMeetsLine[NARROW[dlineList.first.shape], line]; IF NOT parallel THEN { AddIntersectionPointFeature[iPoint, featureData, dlineList.first, objectBag]; }; ENDLOOP; FOR circleList: LIST OF FeatureData _ objectBag.radiiCircles, circleList.rest UNTIL circleList = NIL DO [points, hitCount] _ GGCircles.LineMeetsCircle[line, NARROW[circleList.first.shape, AlignmentCircle].circle]; FOR i: NAT IN [1..hitCount] DO AddIntersectionPointFeature[points[i], featureData, circleList.first, objectBag]; ENDLOOP; ENDLOOP; }; AddCircleIntersections: PROC [featureData: FeatureData, objectBag: AlignBag] = { points: ARRAY[1..2] OF Point; hitCount: NAT; circle: Circle _ NARROW[featureData.shape, AlignmentCircle].circle; FOR lineList: LIST OF FeatureData _ objectBag.slopeLines, lineList.rest UNTIL lineList = NIL DO [points, hitCount] _ GGCircles.LineMeetsCircle[NARROW[lineList.first.shape, AlignmentLine].line, circle]; FOR i: NAT IN [1..hitCount] DO AddIntersectionPointFeature[points[i], lineList.first, featureData, objectBag]; ENDLOOP; ENDLOOP; FOR lineList: LIST OF FeatureData _ objectBag.angleLines, lineList.rest UNTIL lineList = NIL DO [points, hitCount] _ GGCircles.LineMeetsCircle[NARROW[lineList.first.shape, AlignmentLine].line, circle]; FOR i: NAT IN [1..hitCount] DO AddIntersectionPointFeature[points[i], lineList.first, featureData, objectBag]; ENDLOOP; ENDLOOP; FOR dlineList: LIST OF FeatureData _ objectBag.distanceLines, dlineList.rest UNTIL dlineList = NIL DO [points, hitCount] _ GGCircles.LineMeetsCircle[NARROW[dlineList.first.shape], circle]; FOR i: NAT IN [1..hitCount] DO AddIntersectionPointFeature[points[i], dlineList.first, featureData, objectBag]; ENDLOOP; ENDLOOP; FOR circleList: LIST OF FeatureData _ objectBag.radiiCircles, circleList.rest UNTIL circleList = NIL DO [points, hitCount] _ GGCircles.CircleMeetsCircle[NARROW[circleList.first.shape, AlignmentCircle].circle, circle]; FOR i: NAT IN [1..hitCount] DO AddIntersectionPointFeature[points[i], circleList.first, featureData, objectBag]; ENDLOOP; ENDLOOP; }; AddIntersectionPointFeature: PRIVATE PROC [point: Point, feature1, feature2: FeatureData, objectBag: AlignBag] = { featureData: FeatureData; alignmentPoint: AlignmentPoint; featureData _ NEW[FeatureDataObj]; alignmentPoint _ NEW[AlignmentPointObj _ [point: point, curve1: feature1, curve2: feature2]]; featureData.type _ intersectionPoint; featureData.shape _ alignmentPoint; AddFeature[featureData, objectBag]; }; Drawing Alignment Objects THE FOLLOWING STIPPLEs ARE VERY PRECISELY DETERMINED. DO NOT MESS WITH THEM !! alignmentColor: Imager.Color _ Imager.black; -- doing this buys less than 10% in performance checkerColor: Imager.Color _ Imager.black; Draws all objects in the object bag regardless of how they have been marked. Hit Testing Calls one of the procedures below, depending on the current gravity type. Map: PUBLIC PROC [testPoint: Point, criticalR: REAL, currentObjects: AlignBag, activeObjects: TriggerBag, ggData: GGData, intersections: BOOL] RETURNS [resultPoint: Point, feature: FeatureData] = { ENABLE UNWIND => ggData.gravityPool _ NewGravityPool[]; -- in case an ABORT happened while pool was in use [resultPoint, feature] _ GGMultiGravity.Map[testPoint, criticalR, currentObjects, activeObjects, ggData, intersections]; }; Here we find both the nearest point and the nearest line. The winning object is simply the closest. Pick the closer of the curve and the point. Here we find both the nearest point and the nearest line. Within innerR, points get preference. Look at the closest point first. If it is within innerR, use it. Otherwise, pick the closer of the curve and the point. Auxiliary Hit Testing Routines The storage borrowed from the Pool is returned to the caller. The caller has the responsibility to return that storage to the Pool when it is no longer needed. outlineD: OutlineDescriptor; Alignment Lines. Active Scene Objects. FOR outlines: LIST OF FeatureData _ sceneObjects.outlines, outlines.rest UNTIL outlines = NIL DO featureData _ outlines.first; outlineD _ NARROW[featureData.shape, OutlineDescriptor]; [thisCurve.point, thisCurve.dist, thisCurve.hitData, success] _ outlineD.slice.class.closestSegment[outlineD, testPoint, tolerance]; IF success AND (thisCurve.dist - epsilon <= bestCurve.dist) THEN { -- so segments are preferred to slopelines bestCurve.dist _ thisCurve.dist; bestCurve.segNum _ thisCurve.segNum; bestCurve.point _ thisCurve.point; bestCurve.hitData _ thisCurve.hitData; bestCurve.featureData _ featureData; }; [thisPoint.point, thisPoint.dist, thisPoint.hitData, success] _ outlineD.slice.class.closestPoint[outlineD, testPoint, tolerance]; IF success AND (thisPoint.dist - epsilon <= bestPoint.dist) THEN { bestPoint.type _ joint; bestPoint.dist _ thisPoint.dist; bestPoint.point _ thisPoint.point; bestPoint.hitData _ thisPoint.hitData; bestPoint.joint _ 999; bestPoint.featureData _ featureData; }; ENDLOOP; Handle the anchor. IF intersections THEN { }; ExtractResultFromCurve: PROC [curve: GoodCurve, feature: FeatureData] = { SELECT feature.type FROM outline => feature.hitPart _ curve.hitData; slice => feature.hitPart _ curve.hitData; slopeLine, angleLine, radiiCircle, distanceLine => {}; ENDCASE => SIGNAL Problem[msg: "Unimplemented type."]; }; ExtractResultFromPoint: PROC [goodPoint: GoodPoint, feature: FeatureData] = { SELECT goodPoint.type FROM joint, controlPoint => feature.hitPart _ goodPoint.hitData; slice => feature.hitPart _ goodPoint.hitData; intersectionPoint => feature.hitPart _ NIL; midpoint => { midPoint features contain a reasonable segNum in their hitPart }; ENDCASE => SIGNAL Problem [msg: "Unimplemented result type."]; }; triggerBag.outlines = NIL AND Hit Testing For Different Kinds of Objects. Finds the nearest joint of traj to testPoint (and its distance from testPoint). Finds the nearest control point of seq (within tolerance) to testPoint (and its distance from testPoint). Storage Pools To be used to reduce GoodPoint allocations. To be used to reduce GoodCurve allocations. Pier, December 5, 1985 10:06:40 am PST changes to: removed all references to camera. Bier, January 23, 1986 5:46:09 pm PST An initial policy decision was that any object which was gravity active should be drawn on the alignment lines plane. This meant that all trajectories were drawn when they were gravity active, even though they are already visible (modulo overlap). This had the nice side effect that all trajectories become visible during selection. It also helped with debugging (remember the "ghosts"?) However, I am changing this policy now for performance reasons. I may change my mind later. changes to: DrawAlignBag, DrawAlignBagRegardless, DIRECTORY, GGGravityImpl Pier, June 11, 1986 3:15:44 pm PDT renamed all references to activeObjects to be currentObjects renamed all references to sensitiveObjects to be activeObjects Κ$»˜code™Kšœ Οmœ1™Kšœ)˜)KšžœXžœžœA‘ ˜―šžœ‘.˜5KšœF˜FKšžœXžœžœ@˜’K–5[dc: Imager.Context, circle: GGBasicTypes.Circle]šžœ!˜%K˜—K˜—KšœD˜DK˜Kšœ$˜$š žœžœžœ'žœžœž˜Kšžœžœž˜!Kšœ8˜8Kšœ8˜8Kšœžœ žœžœ(˜uKšœM˜MKšžœžœ˜—Kšžœ˜K˜—K˜—šŸœžœžœ>˜aKšœL™LKšžœ žœžœžœ˜Kšœ2˜2Kšœ2˜2Kšœ4˜4Kšœ5˜5Kšœ˜—K˜K™ K™Kšœ žœžœ˜#šœžœžœ˜Kšœžœ‘'˜AKšœžœ ‘9˜NKšœ‘˜-Kšœžœ‘ ˜-Kšœ žœž˜K˜K˜—KšœžœC˜VKšœ žœžœ˜#šœžœžœ˜Kšœ˜Kšœžœ˜K˜Kšœžœ ‘L˜bKšœžœ ‘8˜RKšœžœ ‘1˜FKšœžœ‘ ˜-Kšœ žœž˜K˜—K™šŸœžœžœžœVžœžœ5žœžœ˜ΪKšžœžœ+‘/˜gK™IKšœ'˜'šžœ7ž˜Ašœžœž˜,šœ˜Kšœ}˜}—šœ˜Kšœ‘˜‘—Kšžœžœ˜—šœ˜Kšœ˜Kšœ žœ˜K˜—Kšžœžœ˜—Kšœ&˜&K˜K˜—š ŸœžœžœžœVžœžœ/™ΕKšžœžœ,‘2™kKšœx™xK™K™—šŸœžœžœVžœžœ5žœžœ˜ΫKšœd™dKšœ ˜ Kšœ ˜ š žœžœ žœžœžœ"žœ˜‚Kšœ˜Kšœ žœ˜Kšžœ˜K˜—Kšœœ˜œKšœ+™+šžœžœžœ˜@Kšœ žœ˜Kšœ˜Kšœ1˜1Kšœ1˜1Kšžœ˜—š žœžœžœžœ"žœ‘˜Kšœ˜Kšœ ˜ Kšœ˜K˜—šžœžœžœ˜)Kšœ˜Kšœ ˜ Kšœ˜K˜—šžœ˜Kšœ žœ˜Kšœ˜K˜—Kšœ1˜1Kšœ1˜1Kšœ‘˜K˜—šΠbn  žœžœ žœVžœžœ5žœžœ˜ζKšœ`™`Kšœ ˜ Kšœ ˜ š žœžœ žœžœžœ"žœ˜Kšœ˜Kšœ žœ˜Kšžœ˜K˜—šœœ˜œKšœy™y—šžœžœžœ˜@Kšœ žœ˜Kšœ˜Kšœ1˜1Kšœ1˜1Kšžœ˜—š žœžœžœžœžœ$žœ‘˜žKšœ˜Kšœ ˜ Kšœ˜K˜—šžœžœžœ˜)Kšœ˜Kšœ ˜ Kšœ˜K˜—šžœ˜Kšœ žœ˜Kšœ˜K˜—Kšœ1˜1Kšœ1˜1Kšœ‘˜—K™K™K™š ŸœžœSžœ žœžœG˜ςK™ŸKšœ žœ ˜Kšœ ˜ Kšœ˜K˜K™Kšœ žœžœ˜Kšœ˜Kšœ3˜3Kšœ3˜3Kšœ+žœ‘˜Nšœ@žœ‘˜eKš ™—š žœ  œžœžœ:žœžœž˜jKšœ˜Kšœžœ(˜5Kšœ7˜7Kšœ%˜%šžœ žœ!žœ˜5Kšœ=˜=Kšœ$˜$Kšœžœ˜Kšœ&˜&K˜—Kšžœ˜—š žœ  œžœžœ:žœžœž˜jKšœ˜Kšœžœ(˜5Kšœ7˜7Kšœ%˜%šžœ žœ!žœ˜5Kšœ=˜=Kšœ$˜$Kšœžœ˜Kšœ&˜&K˜—Kšžœ˜—š žœ œžœžœ9žœ žœž˜aKšœ˜Kšœ9žœ˜UKšœ%˜%šžœ žœ!žœ˜5Kšœ=˜=Kšœ$˜$Kšœžœ˜Kšœ&˜&K˜—Kšžœ˜—š žœ œžœžœ?žœ žœž˜iKšœ˜Kšœ#˜#Kšœ$žœ6˜`Kšœ%˜%šžœ žœ!žœ˜5Kšœžœ*˜BKšœ$˜$Kšœžœ˜Kšœ&˜&K˜—Kšžœ˜—š žœ  œžœžœ8žœ žœž˜fKšœ˜Kšœ˜Kšœ$žœ6˜`Kšœ%˜%šžœ žœ!žœ˜5Kšœžœ*˜BKšœ$˜$Kšœžœ˜Kšœ&˜&K˜—Kšžœ˜—š žœ œžœžœ9žœ žœž˜cKšœ˜Kšœ žœ,˜;Kšœ=˜=Kšœ%˜%šžœ žœ!žœ˜5KšœH˜HKšœ$˜$Kšœžœ˜Kšœ&˜&K˜—Kšžœ˜Kš ™—š žœ œžœžœ0žœ žœž˜XKšœ˜Kšœ žœ%˜4Kšœ€˜€šžœ žœ.žœ‘*˜mKšœ ˜ Kšœ$˜$Kšœ"˜"Kšœ&˜&Kšœ$˜$K˜—Kšœ~˜~šžœ žœ.žœ˜BKšœ˜Kšœ ˜ Kšœ"˜"Kšœ&˜&Kšœ˜Kšœ$˜$Kšœ˜—Kšžœ˜—š žœ œžœžœ4žœ žœž™`Kšœ™Kšœ žœ'™8Kšœ„™„šžœ žœ.žœ‘*™mKšœ ™ Kšœ$™$Kšœ"™"Kšœ&™&Kšœ$™$K™—Kšœ‚™‚šžœ žœ.žœ™BKšœ™Kšœ ™ Kšœ"™"Kšœ&™&Kšœ™Kšœ$™$Kšœ™—Kšžœ™Kš ™—Kšžœžœ™Kšœ$˜$šžœžœžœ˜Kšœžœ˜*Kšžœžœžœžœ˜)Kšœ$‘˜8Kšœ+˜+Kš œ2˜@Kšœžœ˜šžœžœ!žœ˜HKšœ$˜$Kšœ&˜&K˜—K˜—K™K˜K˜—šŸœžœ&˜;Kšœ ˜ Kšœ‘˜7Kšœ"˜"Kšœ.˜.K˜K˜—šŸœžœ&˜;Kšœ ˜ Kšœ ˜ Kšœ"˜"Kšœ‘˜6Kšœ.˜.Kšœ˜K˜—šŸœžœ-™Išžœž™Kšœ+™+Kšœ)™)Kšœ6™6Kšžœžœ%™6—K™K™—šŸœžœ1™Mšžœž™Kšœ;™;Kšœ-™-Kšœ'žœ™+™ K™>K™—Kšžœžœ-™>—K™K™—šŸœžœžœžœ˜7šžœ˜Kšœžœž˜Kšœžœž˜Kšœžœž˜ Kšœžœž˜!Kšœžœž˜Kšœžœž˜&Kšœžœ˜—K˜K˜—šŸ œžœžœžœ˜?šžœ˜Kšœžœž™Kšœžœž˜Kšœ žœž˜'Kšœž˜K˜—K˜—K˜K™+K™Kšœ žœžœ˜K˜šŸ œžœ.žœžœ˜RKš žœžœžœžœžœ˜]K˜K˜—šŸœžœ.žœžœ žœ žœžœ˜“KšœΟuœ £œžœ˜Kšœ˜K˜Kšœ žœ˜Kšœ˜Kšœ!˜!K–[bBox: GGBasicTypes.BoundBox]šœ žœ˜šžœ žœ˜Kšœzžœžœ˜ˆKš žœžœ!žœžœ‘˜DKšœ˜—Kšœ,˜,Kšœ.˜.šžœ žœžœ‘!˜:Kšœ‘˜6Kšœ‘"˜4Kšœ˜Kšžœ˜K˜—KšœW˜Wšžœžœ žœ˜Kšœ£œ˜Kšœ˜Kšœ˜K˜—šžœ˜Kšœ£œ3˜Kšœ‘"˜;Kšœ‘˜7Kšœ˜Kšžœ˜K˜—Kšœp˜pšžœžœ žœ˜Kšœ£œ˜Kšœ˜Kšœ˜Kšœ˜K˜—šžœ˜Kšœ£œ3˜™J—™"K™—K™—…—pͺΉ