DIRECTORY Atom, Convert, FileNames, FS, GGBasicTypes, GGBoundBox, GGError, GGFromImager, GGInterfaceTypes, GGLines, GGModelTypes, GGObjects, GGParseOut, GGParseIn, GGSlice, GGShapes, GGTransform, GGUtility, GGVector, Imager, ImagerFont, ImagerInterpress, ImagerMemory, ImagerTransformation, Interpress, IO, IPMaster, NodeStyle, NodeStyleFont, RealFns, RefText, Rope, ViewerClasses; GGSliceImplB: CEDAR PROGRAM IMPORTS Atom, Convert, FileNames, FS, GGBoundBox, GGError, GGFromImager, GGLines, GGObjects, GGParseIn, GGParseOut, GGSlice, GGShapes, GGTransform, GGUtility, GGVector, Imager, ImagerFont, ImagerInterpress, ImagerMemory, ImagerTransformation, Interpress, IO, NodeStyleFont, RealFns, RefText, Rope EXPORTS GGSlice = BEGIN BoundBox: TYPE = GGModelTypes.BoundBox; CameraData: TYPE = GGInterfaceTypes.CameraData; Color: TYPE = Imager.Color; DisplayStyle: TYPE = GGSlice.DisplayStyle; ExtendMode: TYPE = GGModelTypes.ExtendMode; GargoyleData: TYPE = GGInterfaceTypes.GargoyleData; GargoyleDataObj: TYPE = GGInterfaceTypes.GargoyleDataObj; Point: TYPE = GGBasicTypes.Point; PointGenerator: TYPE = GGModelTypes.PointGenerator; PointGeneratorObj: TYPE = GGModelTypes.PointGeneratorObj; PointPairGenerator: TYPE = GGModelTypes.PointPairGenerator; PointPairGeneratorObj: TYPE = GGModelTypes.PointPairGeneratorObj; Scene: TYPE = GGModelTypes.Scene; SelectedObjectData: TYPE = GGModelTypes.SelectedObjectData; SelectionClass: TYPE = GGInterfaceTypes.SelectionClass; SelectMode: TYPE = GGModelTypes.SelectMode; Slice: TYPE = GGModelTypes.Slice; SliceClass: TYPE = GGModelTypes.SliceClass; SliceClassObj: TYPE = GGModelTypes.SliceClassObj; SliceDescriptor: TYPE = GGModelTypes.SliceDescriptor; SliceDescriptorObj: TYPE = GGModelTypes.SliceDescriptorObj; SliceObj: TYPE = GGModelTypes.SliceObj; SliceParts: TYPE = GGModelTypes.SliceParts; Vector: TYPE = GGBasicTypes.Vector; Viewer: TYPE = ViewerClasses.Viewer; TextEdgeArray: TYPE = PACKED ARRAY [0..textMaxEdges) OF BOOL; -- left, top, right, bottom, baseline, centerline. TextPointArray: TYPE = PACKED ARRAY [0..textMaxPoints) OF BOOL; -- see picture. Used in text parts data. TextPoints: TYPE = REF TextPointsData; TextPointsData: TYPE = ARRAY [0..textMaxPoints) OF Point; -- see picture textMaxPoints: INTEGER = 12; textMaxEdges: INTEGER = 7; TextData: TYPE = REF TextDataObj; TextDataObj: TYPE = RECORD [ box: BoundBox _ NIL, -- not a bounding box but part of the text representation feedbackBox: BoundBox _ NIL, -- both a bounding box and part of the text representation transform: ImagerTransformation.Transformation, amplifySpace: REAL, -- because the Imager allows space correction (see Imager.mesa) inverse: ImagerTransformation.Transformation, -- inverse of transform inverseScale: Vector, -- cached value of ImagerTransformation.Factor[inverse].s points: TextPoints, -- cache for "joints" color: Imager.Color _ NIL, rope: Rope.ROPE _ NIL, fontName: Rope.ROPE _ NIL, -- font description: e.g. Helvetica11BI fontFamily: ATOM, fontFace: NodeStyle.FontFace _ Regular, fontSize: REAL _ 10.0, printFont: ImagerFont.Font _ NIL, screenFont: ImagerFont.Font _ NIL, dropShadowOn: BOOL _ FALSE, dropShadowOffset: Vector, shadowColor: Imager.Color _ NIL ]; TextParts: TYPE = REF TextPartsObj; TextPartsObj: TYPE = RECORD [ points: TextPointArray, -- which corners of text box are selected. edges: TextEdgeArray, -- which edges of text box are selected. includeText: BOOL -- FALSE => corners and edges only; no text ]; TextHitData: TYPE = REF TextHitDataObj; TextHitDataObj: TYPE = RECORD [ point: [-1..textMaxPoints), -- which point of box is hit. edge: [-1..textMaxEdges), -- which edge of box is hit. hitPoint: Point ]; FontNameError: PUBLIC SIGNAL = CODE; BuildTextSliceClass: PUBLIC PROC [] RETURNS [class: SliceClass] = { OPEN GGSlice; class _ NEW[SliceClassObj _ [ type: $Text, getBoundBox: TextBoundBox, getTightBox: TextTightBox, copy: TextCopy, drawParts: TextDrawParts, drawTransform: TextDrawTransform, drawSelectionFeedback: TextDrawSelectionFeedback, drawAttractorFeedback: NoOpDrawAttractorFeedback, transform: TextTransform, describe: TextDescribe, fileout: TextFileout, filein: TextFilein, emptyParts: TextEmptyParts, newParts: TextNewParts, unionParts: TextUnionParts, differenceParts: TextDiffParts, movingParts: TextMovingParts, fixedParts: TextFixedParts, augmentParts: TextAugmentParts, pointsInDescriptor: TextPointsInDescriptor, pointPairsInDescriptor: TextPointPairsInDescriptor, nextPoint: TextNextPoint, nextPointPair: TextNextPointPair, closestPoint: TextClosestPoint, closestPointAndTangent: NIL, closestSegment: TextClosestSegment, lineIntersection: NoOpLineIntersection, circleIntersection: NoOpCircleIntersection, hitDataAsSimpleCurve: NoOpHitDataAsSimpleCurve, setStrokeWidth: TextSetStrokeWidth, getStrokeWidth: TextGetStrokeWidth, setStrokeColor: TextSetStrokeColor, getStrokeColor: TextGetStrokeColor, setFillColor: TextSetFillColor, getFillColor: TextGetFillColor ]]; }; MakeTextSlice: PUBLIC PROC [text: Rope.ROPE, fontName: Rope.ROPE, color: Imager.Color _ Imager.black, transform: ImagerTransformation.Transformation, amplifySpace: REAL, dropShadowsOn: BOOL _ FALSE, dropShadowOffset: Vector _ [0.0, 0.0], shadowColor: Imager.Color _ Imager.black, feedback: Viewer] RETURNS [slice: Slice] = { inverse: ImagerTransformation.Transformation _ ImagerTransformation.Invert[transform]; textData: TextData _ NEW[TextDataObj _ [ box: GGBoundBox.NullBoundBox[], -- filled in by call to TextSetBoundBox feedbackBox: GGBoundBox.NullBoundBox[], -- filled in by call to TextSetBoundBox transform: transform, amplifySpace: amplifySpace, inverse: inverse, inverseScale: ImagerTransformation.Factor[inverse].s, points: NEW[TextPointsData], -- filled in by call to TextSetBoundBox color: color, rope: text, fontName: NIL, -- font description: e.g. Helvetica11BI dropShadowOn: dropShadowsOn, dropShadowOffset: dropShadowOffset, shadowColor: shadowColor ]]; slice _ NEW[SliceObj _ [ class: GGSlice.FetchSliceClass[$Text], data: textData, parent: NIL, selectedInFull: [FALSE, FALSE, FALSE], boundBox: GGBoundBox.CreateBoundBox[0,0,0,0], -- gets filled in later onOverlay: FALSE ]]; SetTextFont[slice, fontName, feedback]; -- updates the boundBox }; GetText: PUBLIC PROC [slice: Slice] RETURNS [text: Rope.ROPE] = { RETURN[IF slice.class.type#$Text THEN NIL ELSE NARROW[slice.data, TextData].rope]; }; AppendText: PUBLIC PROC [slice: Slice, text: Rope.ROPE] = { textData: TextData; IF slice.class.type#$Text THEN RETURN; textData _ NARROW[slice.data]; textData.rope _ Rope.Concat[textData.rope, text]; -- update text TextSetBoundBox[slice]; }; BackspaceText: PUBLIC PROC [slice: Slice] = { textData: TextData; IF slice.class.type#$Text THEN RETURN; textData _ NARROW[slice.data]; textData.rope _ Rope.Substr[base: textData.rope, len: Rope.Length[textData.rope]-1]; -- update text TextSetBoundBox[slice]; }; DigitBreak: IO.BreakProc = { RETURN[IF char IN ['0..'9] THEN break ELSE other]; }; FontNameFromFontParams: PROC [fontFamily: ATOM, fontFace: NodeStyle.FontFace, fontSize: REAL] RETURNS [fontName: Rope.ROPE _ NIL] = { sizeRope: Rope.ROPE _ Convert.RopeFromReal[fontSize]; faceRope: Rope.ROPE _ SELECT fontFace FROM BoldItalic => "BI", Bold => "B", Italic => "I", ENDCASE => ""; fontName _ Rope.Cat[Atom.GetPName[fontFamily], sizeRope, faceRope]; }; FontParamsFromFontName: PUBLIC PROC [fontName: Rope.ROPE] RETURNS [fontFamily: ATOM, fontFace: NodeStyle.FontFace, fontSize: REAL] = { GetNumber: PROC [s: IO.STREAM] RETURNS [x: REAL _ 0.0] = { frac, fracCount: REAL _ 0.0; c: CHAR; UNTIL (c _ IO.GetChar[s ! IO.EndOfStream => {c _ '\000; CONTINUE;}; ]) NOT IN ['0..'9] DO x _ x*10+(c-'0); ENDLOOP; IF c#'\000 AND c#'. THEN s.Backup[c]; -- restore the terminating character except for decimal point IF c='. THEN { -- fractional part UNTIL (c _ IO.GetChar[s ! IO.EndOfStream => {c _ '\000; CONTINUE;}; ]) NOT IN ['0..'9] DO fracCount _ fracCount+1; frac _ frac*10.0+(c-'0); ENDLOOP; x _ x+frac*RealFns.Power[10.0, -1*fracCount]; IF c#'\000 THEN s.Backup[c]; -- restore the terminating character }; }; familyName, face: Rope.ROPE; bold, italic: BOOL _ FALSE; fontStream: IO.STREAM _ IO.RIS[fontName]; [familyName, ----] _ IO.GetTokenRope[stream: fontStream, breakProc: DigitBreak ! IO.EndOfStream => SIGNAL FontNameError]; fontSize _ GetNumber[fontStream ! IO.EndOfStream => SIGNAL FontNameError]; [face, ----] _ IO.GetTokenRope[stream: fontStream, breakProc: IO.TokenProc ! IO.EndOfStream => {face _ ""; CONTINUE; };]; fontFamily _ Atom.MakeAtom[familyName]; SELECT Rope.Length[face] FROM 0 => NULL; 1 => { char: CHAR _ Rope.Fetch[base: face, index: 0]; bold _ char='B OR char='b; italic _ char='I OR char='i; }; 2 => { char0: CHAR _ Rope.Fetch[base: face, index: 0]; char1: CHAR _ Rope.Fetch[base: face, index: 1]; bold _ char0='B OR char0='b OR char1='B OR char1='b; italic _ char0='I OR char0='i OR char1='I OR char1='i; }; ENDCASE => NULL; fontFace _ SELECT TRUE FROM bold AND italic => BoldItalic, bold => Bold, italic => Italic, ENDCASE => Regular; }; SetTextFont: PUBLIC PROC [slice: Slice, fontName: Rope.ROPE, feedback: Viewer] = { textData: TextData; fontFamily: ATOM; fontFace: NodeStyle.FontFace; fontSize: REAL; IF slice.class.type#$Text OR fontName=NIL THEN RETURN; textData _ NARROW[slice.data]; [fontFamily, fontFace, fontSize] _ FontParamsFromFontName[fontName ! FontNameError => GOTO Abort; ]; IF fontSize=0.0 THEN { scale: REAL _ ImagerTransformation.Factor[textData.transform].s.x; GGError.Append[feedback, Rope.Cat["Font size 0.0 ignored for ", fontName], oneLiner]; GGError.Blink[feedback] ; fontSize _ 10.0*(IF scale>1.0 THEN scale ELSE 1.0/scale); -- oh, well }; textData.printFont _ NodeStyleFont.FontFromStyleParams[prefix: printPrefix, family: fontFamily, face: fontFace, size: fontSize, alphabets: CapsAndLower]; IF Rope.Find[s1: textData.printFont.name, s2: "xerox/xc2-2-2/modern", case: FALSE]#-1 THEN { oldName: Rope.ROPE; fontFamily _ $TimesRoman; fontFace _ Regular; oldName _ fontName; fontName _ FontNameFromFontParams[fontFamily, fontFace, fontSize]; textData.printFont _ NodeStyleFont.FontFromStyleParams[prefix: printPrefix, family: fontFamily, face: fontFace, size: fontSize, alphabets: CapsAndLower]; GGError.Append[feedback, Rope.Cat["Font ", textData.printFont.name, " used for ", oldName], oneLiner]; GGError.Blink[feedback]; }; textData.screenFont _ NodeStyleFont.FontFromStyleParams[prefix: screenPrefix, family: fontFamily, face: fontFace, size: fontSize, alphabets: CapsAndLower]; IF Rope.Find[s1: textData.screenFont.name, s2: "Tioga10", case: FALSE]#-1 THEN textData.screenFont _ textData.printFont; -- undesired font substitution textData.fontName _ fontName; textData.fontFamily _ fontFamily; textData.fontFace _ fontFace; textData.fontSize _ fontSize; TextSetBoundBox[slice]; EXITS Abort => { GGError.Append[feedback, Rope.Concat["FontNameError: ", fontName], oneLiner]; GGError.Blink[feedback] ; }; }; GetTextFont: PUBLIC PROC [slice: Slice] RETURNS [font: ImagerFont.Font, fontName: Rope.ROPE] = { textData: TextData; IF slice.class.type#$Text THEN RETURN[NIL, NIL]; textData _ NARROW[slice.data]; RETURN[textData.printFont, textData.fontName]; }; SetTextAmplifySpace: PUBLIC PROC [slice: Slice, amplifySpace: REAL, feedback: ViewerClasses.Viewer] = { textData: TextData; textData _ NARROW[slice.data]; textData.amplifySpace _ amplifySpace; TextSetBoundBox[slice]; }; GetTextAmplifySpace: PUBLIC PROC [slice: Slice] RETURNS [amplifySpace: REAL] = { textData: TextData; textData _ NARROW[slice.data]; RETURN[textData.amplifySpace]; }; DropShadowOn: PUBLIC PROC [slice: Slice, offset: Vector] = { textData: TextData _ NARROW[slice.data]; textData.dropShadowOn _ TRUE; textData.dropShadowOffset _ offset; }; DropShadowOff: PUBLIC PROC [slice: Slice] = { textData: TextData _ NARROW[slice.data]; textData.dropShadowOn _ FALSE }; UnpackComplexFontName: PROC [fontName: Rope.ROPE, fontSize: REAL] RETURNS [new: Rope.ROPE] = { face: Rope.ROPE; new _ FileNames.GetShortName[fontName]; -- throw away "xerox/pressfonts/" face _ FileNames.Tail[new, '-]; -- get face component (MIR, BRR, ...) new _ Rope.Substr[base: new, start: 0, len: Rope.SkipTo[s: new, pos: 0, skip: "-"]]; -- throw away "-XXX" new _ Rope.Cat[new, Convert.RopeFromReal[fontSize], SELECT TRUE FROM Rope.Equal[face, "MRR", FALSE] => "", Rope.Equal[face, "BRR", FALSE] => "B", Rope.Equal[face, "MIR", FALSE] => "I", Rope.Equal[face, "BIR", FALSE] => "BI", ENDCASE => ERROR ]; }; TextBoundBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox]= { RETURN[slice.boundBox]; }; TextTightBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox] = { textData: TextData _ NARROW[slice.data]; RETURN[textData.feedbackBox]; }; StringEscapement: PROC [font: ImagerFont.Font, string: ImagerFont.XStringProc, amplifySpace: REAL] RETURNS [ImagerFont.VEC] = { sum: ImagerFont.VEC _ [0, 0]; charAction: ImagerFont.XCharProc ~ { width: ImagerFont.VEC; width _ ImagerFont.Width[font, char]; IF ImagerFont.Amplified[font, char] THEN { width.x _ width.x*amplifySpace; width.y _ width.y*amplifySpace; }; sum.x _ sum.x+width.x; sum.y _ sum.y+width.y; }; string[charAction]; RETURN[sum]; }; RopeEscapement: PROC [font: ImagerFont.Font, rope: Rope.ROPE, amplifySpace: REAL, start: INT _ 0, len: INT _ INT.LAST] RETURNS [ImagerFont.VEC] ~ { string: ImagerFont.XStringProc ~ { ImagerFont.MapRope[rope, start, len, charAction] }; RETURN[StringEscapement[font, string, amplifySpace]]; }; TextSetBoundBox: PROC [slice: Slice] = { textData: TextData _ NARROW[slice.data]; fontExtents: ImagerFont.Extents; halfCP: REAL = GGModelTypes.halfJointSize + 1; left, right, middle, top, base, bottom, center: REAL; left _ 0.0; right _ RopeEscapement[textData.screenFont, textData.rope, textData.amplifySpace].x; middle _ (left + right)/2.0; fontExtents _ ImagerFont.RopeBoundingBox[textData.screenFont, "acbdefghijklmnopqrstuvwxyzACBDEFGHIJKLMNOPQRSTUVWXYZ"]; top _ fontExtents.ascent; base _ 0.0; bottom _ -fontExtents.descent; center _ (top + bottom)/2.0; GGBoundBox.UpdateBoundBox[textData.box, left, bottom, right, top]; GGBoundBox.EnlargeByOffset[textData.box, halfCP]; textData.box _ GGBoundBox.BoundBoxOfBoundBox[textData.box, textData.transform]; GGBoundBox.AllowForJoints[textData.box]; IF textData.dropShadowOn THEN GGBoundBox.EnlargeByVector[textData.box, textData.dropShadowOffset]; slice.boundBox _ GGBoundBox.CopyBoundBox[textData.box]; left _ 0.0; right _ RopeEscapement[textData.printFont, textData.rope, textData.amplifySpace].x; middle _ (left + right)/2.0; fontExtents _ ImagerFont.RopeBoundingBox[textData.printFont, "acbdefghijklmnopqrstuvwxyzACBDEFGHIJKLMNOPQRSTUVWXYZ"]; top _ fontExtents.ascent; base _ 0.0; bottom _ -fontExtents.descent; center _ (top + bottom)/2.0; GGBoundBox.UpdateBoundBox[textData.feedbackBox, left, bottom, right, top]; textData.feedbackBox _ GGBoundBox.BoundBoxOfBoundBox[textData.feedbackBox, textData.transform]; -- take into account the font transformation textData.points[0] _ [left, bottom]; textData.points[1] _ [left, base]; textData.points[2] _ [left, center]; textData.points[3] _ [left, top]; textData.points[4] _ [middle, bottom]; textData.points[5] _ [middle, base]; textData.points[6] _ [middle, center]; textData.points[7] _ [middle, top]; textData.points[8] _ [right, bottom]; textData.points[9] _ [right, base]; textData.points[10] _ [right, center]; textData.points[11] _ [right, top]; }; TextCopy: PROC [slice: Slice] RETURNS [copy: Slice] = { textData: TextData _ NARROW[slice.data]; copy _ MakeTextSlice[textData.rope, textData.fontName, textData.color, ImagerTransformation.Copy[textData.transform], textData.amplifySpace, textData.dropShadowOn, textData.dropShadowOffset, textData.shadowColor, NIL]; }; TextDrawParts: PROC [slice: Slice, parts: SliceParts, dc: Imager.Context, camera: GGInterfaceTypes.CameraData, quick: BOOL] = { DoDrawText: PROC = { Imager.ConcatT[dc, textData.transform]; IF textData.dropShadowOn THEN DrawDropShadow[dc, textData, camera]; DrawText[dc, textData, camera]; }; textData: TextData _ NARROW[slice.data]; textParts: TextParts _ NARROW[parts]; IF textParts = NIL OR textParts.includeText THEN Imager.DoSaveAll[dc, DoDrawText]; }; TextDrawSelectionFeedback: PROC [slice: Slice, selectedParts: SliceParts, hotParts: SliceParts, dc: Imager.Context, camera: CameraData, dragInProgress, caretIsMoving, hideHot, quick: BOOL] = { DoDrawFeedback: PROC = { feedbackBox: BoundBox _ textData.feedbackBox; thisCPisHot, thisCPisSelected: BOOL; pts: ARRAY [0..11] OF Point; t: ImagerTransformation.Transformation _ textData.transform; FOR i: NAT IN [0..11] DO pts[i] _ ImagerTransformation.Transform[t, textData.points[i]]; ENDLOOP; Imager.SetColor[dc, Imager.black]; FOR point: INTEGER IN [0..textMaxPoints) DO thisCPisHot _ hotTextParts#NIL AND hotTextParts.points[point]; thisCPisSelected _ normalTextParts#NIL AND normalTextParts.points[point]; IF thisCPisHot THEN GGShapes.DrawSelectedJoint[dc, pts[point], hot]; IF thisCPisSelected THEN GGShapes.DrawSelectedJoint[dc, pts[point], normal]; IF NOT thisCPisHot AND NOT thisCPisSelected THEN GGShapes.DrawCP[dc, pts[point]]; ENDLOOP; IF normalTextParts#NIL THEN { Imager.SetStrokeJoint[dc, round]; Imager.SetStrokeWidth[dc, 1.0]; FOR edge: INTEGER IN [0..textMaxEdges) DO IF normalTextParts.edges[edge] THEN SELECT edge FROM 0 => Imager.MaskVector[dc, pts[0], pts[3] ]; 1 => Imager.MaskVector[dc, pts[3], pts[11] ]; 2 => Imager.MaskVector[dc, pts[11], pts[8] ]; 3 => Imager.MaskVector[dc, pts[8], pts[0] ]; 4 => Imager.MaskVector[dc, pts[1], pts[9] ]; 5 => Imager.MaskVector[dc, pts[2], pts[10] ]; 6 => Imager.MaskVector[dc, pts[4], pts[7] ]; ENDCASE => ERROR; ENDLOOP; }; }; normalTextParts, hotTextParts: TextParts; textData: TextData _ NARROW[slice.data]; IF caretIsMoving OR dragInProgress THEN RETURN; IF selectedParts = NIL AND hotParts = NIL THEN RETURN; normalTextParts _ NARROW[selectedParts]; hotTextParts _ NARROW[hotParts]; IF camera.quality#quality THEN Imager.DoSaveAll[dc, DoDrawFeedback]; }; DrawText: PROC [dc: Imager.Context, textData: TextData, camera: CameraData] = { Imager.SetFont[dc, IF camera.displayStyle=screen THEN textData.screenFont ELSE textData.printFont]; Imager.SetColor[dc, IF textData.color=NIL THEN Imager.black ELSE textData.color]; Imager.SetXY[dc, [0.0, 0.0]]; -- dc must be transformed completely here Imager.SetAmplifySpace[dc, textData.amplifySpace]; -- adjust spacing (for right justified text) Imager.ShowRope[dc, textData.rope]; }; DrawDropShadow: PROC [dc: Imager.Context, textData: TextData, camera: CameraData] = { DoDrawDropShadow: PROC = { Imager.TranslateT[dc, textData.dropShadowOffset]; Imager.SetFont[dc, IF camera.displayStyle=screen THEN textData.screenFont ELSE textData.printFont]; Imager.SetColor[dc, IF textData.shadowColor=NIL THEN Imager.black ELSE textData.shadowColor]; Imager.SetXY[dc, [0.0, 0.0]]; -- dc must be transformed completely here Imager.ShowRope[dc, textData.rope]; }; Imager.DoSaveAll[dc, DoDrawDropShadow]; }; TextDrawTransform: PROC [slice: Slice, parts: SliceParts, dc: Imager.Context, camera: GGInterfaceTypes.CameraData, transform: ImagerTransformation.Transformation] = { DoTextDrawTransform: PROC = { Imager.ConcatT[dc, ImagerTransformation.Concat[textData.transform, transform]]; IF textData.dropShadowOn THEN DrawDropShadow[dc, textData, camera]; DrawText[dc, textData, camera]; }; textData: TextData _ NARROW[slice.data]; Imager.DoSaveAll[dc, DoTextDrawTransform]; }; TextTransform: PROC [slice: Slice, parts: SliceParts, transform: ImagerTransformation.Transformation] = { textData: TextData _ NARROW[slice.data]; textData.transform _ ImagerTransformation.Concat[textData.transform, transform]; textData.inverse _ ImagerTransformation.Invert[textData.transform]; textData.inverseScale _ ImagerTransformation.Factor[textData.inverse].s; TextSetBoundBox[slice]; }; TextDescribe: PROC [slice: Slice, parts: SliceParts] RETURNS [rope: Rope.ROPE] = { prefix: Rope.ROPE; textParts: TextParts _ NARROW[parts]; eCount, pCount: NAT _ 0; FOR i: INTEGER IN [0..textMaxPoints) DO IF textParts.points[i] THEN { pCount _ pCount + 1; prefix _ textPointRopes[i]; }; ENDLOOP; FOR i: INTEGER IN [0..textMaxEdges) DO IF textParts.edges[i] THEN { eCount _ eCount + 1; prefix _ textEdgeRopes[i]; }; ENDLOOP; IF pCount+eCount>1 THEN RETURN["multiple parts of a Text slice"]; IF eCount=0 AND pCount=0 THEN RETURN["NO parts of a Text slice"]; rope _ Rope.Concat[prefix, " of a Text slice"]; }; TextFileout: PROC [slice: Slice, f: IO.STREAM] = { textData: TextData _ NARROW[slice.data]; f.PutF["\"%q\" %g %g", [rope[textData.rope]], [rope[textData.fontName]], [real[textData.fontSize]] ]; f.PutChar[IO.SP]; GGParseOut.WriteColor[f, textData.color]; f.PutChar[IO.SP]; IF textData.dropShadowOn THEN { GGParseOut.WriteBOOL[f, TRUE]; f.PutChar[IO.SP]; GGParseOut.WritePoint[f, textData.dropShadowOffset]; f.PutChar[IO.SP]; GGParseOut.WriteColor[f, textData.shadowColor]; } ELSE GGParseOut.WriteBOOL[f, FALSE]; f.PutChar[IO.SP]; GGParseOut.WriteTransformation[f, textData.transform]; f.PutChar[IO.SP]; f.PutF["%g", [real[textData.amplifySpace]]]; }; TextFilein: PROC [f: IO.STREAM, version: REAL, feedback: Viewer] RETURNS [slice: Slice] = { rope, fontName: Rope.ROPE; fontSize: REAL; point: Point; shadowColor: Imager.Color _ Imager.black; textColor: Imager.Color _ Imager.black; dropShadowOn, good: BOOL; dropShadowOffset: Vector _ [0.0, 0.0]; amplifySpace: REAL; transform: ImagerTransformation.Transformation; IF version > 8601.22 THEN { rope _ f.GetRopeLiteral[]; } ELSE { GGParseIn.ReadBlankAndRope[f, "("]; rope _ GGParseIn.ReadBlankAndWord[f]; GGParseIn.ReadBlankAndRope[f, ")"]; }; IF version <= 8601.06 THEN { -- no font name at all fontName _ "Helvetica10"; fontSize _ 10.0; } ELSE IF version <= 8601.27 THEN { -- a simple name like "Helvetica" or "Gacha" fontName _ GGParseIn.ReadBlankAndWord[f]; fontName _ Rope.Concat[fontName, "10"]; fontSize _ 10.0; } ELSE IF version <= 8605.12 THEN { -- a complex name like "xerox/pressfonts/Helvetica-BIR" fontName _ GGParseIn.ReadBlankAndWord[f]; fontSize _ GGParseIn.ReadBlankAndReal[f]; [] _ GGParseIn.ReadBlankAndWord[f]; fontName _ UnpackComplexFontName[fontName, fontSize]; } ELSE IF version <= 8605.28 THEN { -- a mixed mode name like Helvetica7BI or TimesRoman12 fontName _ GGParseIn.ReadBlankAndWord[f]; fontSize _ GGParseIn.ReadBlankAndReal[f]; [] _ GGParseIn.ReadBlankAndWord[f]; } ELSE { -- a mixed mode name like Helvetica7BI or TimesRoman12. No colorName, real text color fontName _ GGParseIn.ReadBlankAndWord[f]; fontSize _ GGParseIn.ReadBlankAndReal[f]; GGParseIn.ReadBlank[f]; textColor _ GGParseIn.ReadColor[f]; }; GGParseIn.ReadBlank[f]; IF version >= 8609.26 THEN { [dropShadowOn, good] _ GGParseIn.ReadBOOL[f]; IF NOT good THEN ERROR; IF dropShadowOn THEN { GGParseIn.ReadBlank[f]; dropShadowOffset _ GGParseIn.ReadPoint[f]; GGParseIn.ReadBlank[f]; shadowColor _ GGParseIn.ReadColor[f]; }; } ELSE dropShadowOn _ FALSE; GGParseIn.ReadBlank[f]; IF version <= 8607.22 THEN { -- a point rather than a transform point _ GGParseIn.ReadPoint[f]; transform _ ImagerTransformation.Translate[point]; } ELSE transform _ GGParseIn.ReadTransformation[f]; IF version >= 8701.13 THEN { amplifySpace _ GGParseIn.ReadBlankAndReal[f]; } ELSE amplifySpace _ 1.0; slice _ MakeTextSlice[rope, fontName, textColor, transform, amplifySpace, dropShadowOn, dropShadowOffset, shadowColor, feedback]; }; MakeComplete: PROC [parts: SliceParts] = { WITH parts SELECT FROM textParts: TextParts => { textParts.points _ ALL[TRUE]; textParts.edges _ ALL[TRUE]; textParts.includeText _ TRUE; }; ipParts: IPParts => { ipParts.points _ ALL[TRUE]; ipParts.edges _ ALL[TRUE]; ipParts.innards _ TRUE; }; ENDCASE => ERROR; }; IsComplete: PROC [parts: SliceParts] RETURNS [BOOL] = { WITH parts SELECT FROM textParts: TextParts => RETURN[ textParts.points=ALL[TRUE] AND textParts.edges=ALL[TRUE] AND textParts.includeText=TRUE]; ipParts: IPParts => RETURN[ ipParts.points=ALL[TRUE] AND ipParts.edges=ALL[TRUE] AND ipParts.innards=TRUE]; ENDCASE => ERROR; }; IsEmpty: PROC [parts: SliceParts] RETURNS [BOOL] = { WITH parts SELECT FROM textParts: TextParts => RETURN[ textParts.points=ALL[FALSE] AND textParts.edges=ALL[FALSE] AND textParts.includeText = FALSE]; ipParts: IPParts => RETURN[ ipParts.points=ALL[FALSE] AND ipParts.edges=ALL[FALSE] AND ipParts.innards = FALSE]; ENDCASE => ERROR; }; TextEmptyParts: PROC [slice: Slice, parts: SliceParts] RETURNS [BOOL] = { RETURN[IsEmpty[parts]]; }; TextNewParts: PROC [slice: Slice, hitData: REF ANY, mode: SelectMode] RETURNS [parts: SliceParts] = { textHitData: TextHitData _ NARROW[hitData]; textParts: TextParts _ NEW[TextPartsObj _ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ]; SELECT mode FROM joint => { IF textHitData.point#-1 THEN { textParts.points[textHitData.point] _ TRUE; } ELSE { hitData: REF ANY; pointHitData: TextHitData; success: BOOL; wholeParts: SliceParts; wholeD: SliceDescriptor; wholeParts _ TextNewParts[slice, NIL, topLevel]; wholeD _ NEW[SliceDescriptorObj _ [slice, wholeParts]]; [----, ----, hitData, success] _ slice.class.closestPoint[wholeD, textHitData.hitPoint, GGUtility.plusInfinity]; IF NOT success THEN ERROR; pointHitData _ NARROW[hitData]; IF pointHitData.point = -1 THEN ERROR; textParts.points[pointHitData.point] _ TRUE; }; textParts.includeText _ TRUE; }; segment => IF textHitData.edge#-1 THEN { textParts.edges[textHitData.edge] _ TRUE; textParts.includeText _ TRUE; }; controlPoint => { textParts.points _ ALL[TRUE]; textParts.edges _ ALL[TRUE]; }; traj, slice, topLevel => MakeComplete[textParts]; none => { -- prefer corner to edge if both are true IF textHitData.point#-1 THEN textParts.points[textHitData.point] _ TRUE ELSE IF textHitData.edge#-1 THEN textParts.edges[textHitData.edge] _ TRUE; }; ENDCASE => ERROR; parts _ textParts; -- RETURN[textParts] }; TextUnionParts: PROC [slice: Slice, partsA: SliceParts, partsB: SliceParts] RETURNS [aPlusB: SliceParts] = { textPartsA: TextParts _ NARROW[partsA]; textPartsB: TextParts _ NARROW[partsB]; newParts: TextParts _ NEW[TextPartsObj _ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ]; FOR i: INTEGER IN [0..textMaxPoints) DO newParts.points[i] _ textPartsA.points[i] OR textPartsB.points[i]; ENDLOOP; FOR i: INTEGER IN [0..textMaxEdges) DO newParts.edges[i] _ textPartsA.edges[i] OR textPartsB.edges[i]; ENDLOOP; newParts.includeText _ textPartsA.includeText OR textPartsB.includeText; aPlusB _ newParts; -- RETURN[newParts] }; TextDiffParts: PROC [slice: Slice, partsA: SliceParts, partsB: SliceParts] RETURNS [aMinusB: SliceParts] = { textPartsA: TextParts _ NARROW[partsA]; textPartsB: TextParts _ NARROW[partsB]; newParts: TextParts _ NEW[TextPartsObj _ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ]; IF partsB=NIL THEN {newParts^ _ textPartsA^; aMinusB _ newParts; RETURN;}; FOR i: INTEGER IN [0..textMaxPoints) DO newParts.points[i] _ IF textPartsB.points[i] THEN FALSE ELSE textPartsA.points[i]; ENDLOOP; FOR i: INTEGER IN [0..textMaxEdges) DO newParts.edges[i] _ IF textPartsB.edges[i] THEN FALSE ELSE textPartsA.edges[i]; ENDLOOP; newParts.includeText _ IF textPartsB.includeText THEN FALSE ELSE textPartsA.includeText; aMinusB _ newParts; -- RETURN[newParts] }; TextMovingParts: PROC [slice: Slice, parts: SliceParts] RETURNS [moving: SliceParts] = { newParts: TextParts _ NEW[TextPartsObj _ [points: ALL[TRUE], edges: ALL[TRUE], includeText: TRUE ] ]; IF IsEmpty[parts] THEN { -- nothing at all is moving newParts.edges _ ALL[FALSE]; newParts.points _ ALL[FALSE]; newParts.includeText _ FALSE; }; moving _ newParts; -- RETURN[newParts] }; TextFixedParts: PROC [slice: Slice, parts: SliceParts, selectedList: LIST OF REF ANY] RETURNS [fixed: SliceParts] = { moving: SliceParts _ TextMovingParts[slice, parts]; allParts: TextParts _ NEW[TextPartsObj]; MakeComplete[allParts]; fixed _ TextDiffParts[slice, allParts, moving]; }; TextAugmentParts: PROC [slice: Slice, parts: SliceParts, selectClass: SelectionClass] RETURNS [more: SliceParts] = { textParts: TextParts _ NARROW[parts]; newParts: TextParts _ NEW[TextPartsObj _ [points: textParts.points, edges: textParts.edges, includeText: textParts.includeText] ]; FOR i: INTEGER IN [0..textMaxEdges) DO IF textParts.edges[i] THEN SELECT i FROM 0 => {newParts.points[0] _ TRUE; newParts.points[1] _ TRUE; newParts.points[2] _ TRUE; newParts.points[3] _ TRUE;}; 1 => {newParts.points[3] _ TRUE; newParts.points[7] _ TRUE; newParts.points[11] _ TRUE;}; 2 => {newParts.points[8] _ TRUE; newParts.points[9] _ TRUE; newParts.points[10] _ TRUE; newParts.points[11] _ TRUE;}; 3 => {newParts.points[0] _ TRUE; newParts.points[4] _ TRUE; newParts.points[8] _ TRUE;}; 4 => {newParts.points[1] _ TRUE; newParts.points[5] _ TRUE; newParts.points[9] _ TRUE;}; 5 => {newParts.points[2] _ TRUE; newParts.points[6] _ TRUE; newParts.points[10] _ TRUE;}; 6 => {newParts.points[4] _ TRUE; newParts.points[5] _ TRUE; newParts.points[6] _ TRUE; newParts.points[7] _ TRUE;}; ENDCASE => ERROR; ENDLOOP; more _ newParts; }; TextPointsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [pointGen: PointGenerator] = { parts: TextParts _ NARROW[sliceD.parts]; pointGen _ NEW[PointGeneratorObj _ [sliceD, 0, 0, NIL] ]; FOR point: INTEGER IN [0..textMaxPoints) DO IF parts.points[point] THEN pointGen.toGo _ pointGen.toGo + 1; ENDLOOP; }; TextPointPairsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [pointPairGen: PointPairGenerator] = { parts: TextParts _ NARROW[sliceD.parts]; pointPairGen _ NEW[PointPairGeneratorObj _ [sliceD, 0, 0, NIL] ]; -- toGo and index now used FOR edge: INTEGER IN [0..textMaxEdges) DO IF parts.edges[edge] THEN pointPairGen.toGo _ pointPairGen.toGo + 1; ENDLOOP; }; TextNextPoint: PROC [pointGen: PointGenerator] RETURNS [pointAndDone: GGModelTypes.PointAndDone] = { IF pointGen=NIL OR pointGen.toGo = 0 THEN { pointAndDone.done _ TRUE; RETURN; } ELSE { sliceD: SliceDescriptor _ pointGen.sliceD; textData: TextData _ NARROW[sliceD.slice.data]; textParts: TextParts _ NARROW[sliceD.parts]; index: INTEGER _ -1; pointAndDone.done _ FALSE; FOR index _ pointGen.index, index+1 UNTIL index >=textMaxPoints DO IF textParts.points[index] THEN EXIT; -- index will point to next available point ENDLOOP; pointAndDone.point _ ImagerTransformation.Transform[textData.transform, textData.points[index] ]; pointGen.toGo _ pointGen.toGo-1; pointGen.index _ IF pointGen.toGo=0 THEN 99 ELSE index+1; -- bump to the next available point in the generator }; }; TextNextPointPair: PROC [pointPairGen: PointPairGenerator] RETURNS [pointPairAndDone: GGModelTypes.PointPairAndDone] = { IF pointPairGen=NIL OR pointPairGen.toGo = 0 THEN { pointPairAndDone.done _ TRUE; RETURN; } ELSE { sliceD: SliceDescriptor _ pointPairGen.sliceD; textData: TextData _ NARROW[sliceD.slice.data]; textParts: TextParts _ NARROW[sliceD.parts]; t: ImagerTransformation.Transformation _ textData.transform; index: INTEGER _ -1; pointPairAndDone.done _ FALSE; FOR index _ pointPairGen.index, index+1 UNTIL index >=textMaxEdges DO IF textParts.edges[index] THEN EXIT; -- index will point to next availabe edge ENDLOOP; SELECT index FROM -- index in [0..textMaxEdges) of next available edge 0 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[0] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[3] ]; }; 1 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[3] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[11] ]; }; 2 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[11] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[8] ]; }; 3 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[8] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[0] ]; }; 4 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[1] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[9] ]; }; 5 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[2] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[10] ]; }; 6 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, textData.points[4] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, textData.points[7] ]; }; ENDCASE => SIGNAL GGError.Problem[msg: "Broken Invariant"]; pointPairGen.toGo _ pointPairGen.toGo - 1; pointPairGen.index _ IF pointPairGen.toGo=0 THEN 99 ELSE index+1; -- bump to the next available pair in the generator }; }; 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) ]; }; TextClosestPoint: PROC [sliceD: SliceDescriptor, testPoint: Point, tolerance: REAL] RETURNS [bestPoint: Point, bestDist: REAL, hitData: REF ANY, success: BOOL _ FALSE] = { index: NAT _ 9999; textData: TextData _ NARROW[sliceD.slice.data]; bigBox: GGBasicTypes.BoundBoxObj _ [textData.feedbackBox.loX-tolerance, textData.feedbackBox.loY-tolerance, textData.feedbackBox.hiX+tolerance, textData.feedbackBox.hiY+tolerance, FALSE, FALSE]; -- box in GG coordinates IF PointIsInBox[testPoint, bigBox] THEN { textParts: TextParts _ NARROW[sliceD.parts]; localTestpoint: Point _ GGTransform.Transform[textData.inverse, testPoint]; localTolerance: REAL _ ABS[tolerance*MAX[textData.inverseScale.x, textData.inverseScale.y]]; -- MAX function is not correct when text is skewed. [bestDist, index, bestPoint, success] _ TextNearestPoint[textData.points, localTestpoint, localTolerance, textParts.points]; IF success THEN { bestPoint _ GGTransform.Transform[textData.transform, bestPoint]; bestDist _ GGVector.Distance[testPoint, bestPoint]; hitData _ NEW[TextHitDataObj _ [point: index, edge: -1, hitPoint: bestPoint] ]; }; }; }; TextClosestSegment: PROC [sliceD: SliceDescriptor, testPoint: Point, tolerance: REAL] RETURNS [bestPoint: Point, bestDist: REAL, hitData: REF ANY, success: BOOL _ FALSE] = { textData: TextData _ NARROW[sliceD.slice.data]; bigBox: GGBasicTypes.BoundBoxObj _ [textData.feedbackBox.loX-tolerance, textData.feedbackBox.loY-tolerance, textData.feedbackBox.hiX+tolerance, textData.feedbackBox.hiY+tolerance, FALSE, FALSE]; IF PointIsInBox[testPoint, bigBox] THEN { seg: NAT _ 9999; textHitData: TextHitData; textParts: TextParts _ NARROW[sliceD.parts]; localTestpoint: Point _ GGTransform.Transform[textData.inverse, testPoint]; localTolerance: REAL _ ABS[tolerance*MAX[textData.inverseScale.x, textData.inverseScale.y]]; -- MAX function is not correct when text is skewed. [bestDist, seg, bestPoint, success] _ TextNearestEdge[textData.points, localTestpoint, localTolerance, textParts.edges]; IF success THEN { bestPoint _ GGTransform.Transform[textData.transform, bestPoint]; bestDist _ GGVector.Distance[testPoint, bestPoint]; hitData _ textHitData _ NEW[TextHitDataObj _ [point: -1, edge: seg, hitPoint: bestPoint] ]; }; }; }; TextNearestPoint: PROC [points: TextPoints, testPoint: Point, tolerance: REAL _ 1E6, mask: TextPointArray ] RETURNS [bestDist: REAL, bestPoint: NAT, bestXY: Point, success: BOOL _ FALSE] = { thisPoint: Point; thisDist2, bestDist2: REAL; tolerance2: REAL _ tolerance*tolerance; index: NAT _ 0; bestXY _ [-1.0, -1.0]; bestDist2 _ 1E12; -- better be big enough bestPoint _ 9999; FOR index IN [0..textMaxPoints) DO IF mask[index] THEN { thisPoint _ points[index]; -- use feedbackBox relative coordinates thisDist2 _ GGVector.DistanceSquared[thisPoint, testPoint]; IF thisDist2 < bestDist2 THEN { bestDist2 _ thisDist2; bestPoint _ index; bestXY _ thisPoint; }; }; ENDLOOP; bestDist _ RealFns.SqRt[bestDist2]; IF bestDist < tolerance THEN success _ TRUE; }; scratchEdge: GGBasicTypes.Edge _ GGLines.CreateEmptyEdge[]; TextNearestEdge: PUBLIC PROC [points: TextPoints, testPoint: Point, tolerance: REAL _ 1E6, mask: TextEdgeArray _ ALL[TRUE] ] RETURNS [bestDist: REAL, bestSeg: NAT, bestPoint: Point, success: BOOL _ FALSE] = { thisPoint: Point; thisDist2, bestDist2: REAL; tolerance2: REAL _ tolerance*tolerance; index: NAT _ 0; bestPoint _ [-1.0, -1.0]; bestDist2 _ 1E12; -- better be big enough bestSeg _ 9999; FOR index IN [0..textMaxEdges) DO IF mask[index] THEN { SELECT index FROM 0 => GGLines.FillEdge[points[0], points[3], scratchEdge ]; 1 => GGLines.FillEdge[points[3], points[11], scratchEdge ]; 2 => GGLines.FillEdge[points[11], points[8], scratchEdge ]; 3 => GGLines.FillEdge[points[8], points[0], scratchEdge ]; 4 => GGLines.FillEdge[points[1], points[9], scratchEdge ]; 5 => GGLines.FillEdge[points[2], points[10], scratchEdge ]; 6 => GGLines.FillEdge[points[4], points[7], scratchEdge ]; ENDCASE => ERROR; thisPoint _ GGLines.NearestPointOnEdge[testPoint, scratchEdge]; thisDist2 _ GGVector.DistanceSquared[thisPoint, testPoint]; IF thisDist2 < bestDist2 THEN { bestDist2 _ thisDist2; bestSeg _ index; bestPoint _ thisPoint; }; }; ENDLOOP; bestDist _ RealFns.SqRt[bestDist2]; IF bestDist < tolerance THEN success _ TRUE; }; textPointRopes: ARRAY [0..textMaxPoints) OF Rope.ROPE = [ "lower left point", "base left point", "center left point", "upper left point", "lower mid point", "base mid point", "center mid point", "upper mid point", "lower right point", "base right point", "center right point", "upper right point" ]; textEdgeRopes: ARRAY [0..textMaxEdges) OF Rope.ROPE = [ "left edge", "top edge", "right edge", "bottom edge", "baseline", "horizontal center line", "vertical center line" ]; TextSetStrokeWidth: PROC [slice: Slice, parts: SliceParts, strokeWidth: REAL] = {}; TextGetStrokeWidth: PROC [slice: Slice, parts: SliceParts] RETURNS [strokeWidth: REAL] = { RETURN[0.0]; }; TextSetStrokeColor: PROC [slice: Slice, parts: SliceParts, color: Imager.Color] = { textData: TextData _ NARROW[slice.data]; textData.shadowColor _ color; }; TextGetStrokeColor: PROC [slice: Slice, parts: SliceParts] RETURNS [color: Imager.Color] = { textData: TextData _ NARROW[slice.data]; color _ textData.shadowColor; }; TextSetFillColor: PROC [slice: Slice, color: Imager.Color] = { textData: TextData _ NARROW[slice.data]; textData.color _ color; }; TextGetFillColor: PROC [slice: Slice] RETURNS [color: Imager.Color] = { textData: TextData _ NARROW[slice.data]; RETURN[textData.color]; }; ipMaxPoints: INTEGER = 4; ipMaxEdges: INTEGER = 4; IPPointArray: TYPE = --PACKED-- ARRAY [0..ipMaxPoints) OF BOOL; -- ll, ul, ur, lr IPEdgeArray: TYPE = --PACKED-- ARRAY [0..ipMaxEdges) OF BOOL; -- left, top, right, bottom IPData: TYPE = REF IPDataObj; IPDataObj: TYPE = RECORD [ localBox: BoundBox, -- a bounding box in local coordinates. Set once when the slice is created. tightBox: BoundBox, -- a tight-fitting box in scene coordinates. includeByValue: BOOL, -- store the actual interpress in the Gargoyle master? file: Rope.ROPE, -- fileName of Interpress Master mem: Imager.Context, -- imager memory context transform: ImagerTransformation.Transformation, -- includes default position inverse: ImagerTransformation.Transformation, inverseScale: Vector -- cached value of ImagerTransformation.Factor[inverse].s ]; IPParts: TYPE = REF IPPartsObj; IPPartsObj: TYPE = RECORD [ points: IPPointArray, -- which corners of box are selected. edges: IPEdgeArray, -- which edges of box are selected. innards: BOOL _ FALSE -- FALSE => corners and edges only; no innards ]; IPHitData: TYPE = REF IPHitDataObj; IPHitDataObj: TYPE = RECORD [ point: [-1..ipMaxPoints), -- which point of box is hit. edge: INTEGER, -- which edge of box is hit. hitPoint: Point ]; BuildIPSliceClass: PUBLIC PROC [] RETURNS [class: SliceClass] = { OPEN GGSlice; class _ NEW[SliceClassObj _ [ type: $IP, getBoundBox: IPBoundBox, getTightBox: IPTightBox, copy: IPCopy, drawParts: IPDrawParts, drawTransform: IPDrawTransform, drawSelectionFeedback: IPDrawSelectionFeedback, drawAttractorFeedback: NoOpDrawAttractorFeedback, transform: IPTransform, describe: IPDescribe, fileout: IPFileout, filein: IPFilein, emptyParts: IPEmptyParts, newParts: IPNewParts, unionParts: IPUnionParts, differenceParts: IPDiffParts, movingParts: IPMovingParts, fixedParts: IPFixedParts, augmentParts: IPAugmentParts, pointsInDescriptor: IPPointsInDescriptor, pointPairsInDescriptor: IPPointPairsInDescriptor, nextPoint: IPNextPoint, nextPointPair: IPNextPointPair, closestPoint: IPClosestPoint, closestPointAndTangent: NIL, closestSegment: IPClosestSegment, lineIntersection: NoOpLineIntersection, circleIntersection: NoOpCircleIntersection, hitDataAsSimpleCurve: NoOpHitDataAsSimpleCurve, setStrokeWidth: IPSetStrokeWidth, getStrokeWidth: IPGetStrokeWidth, setStrokeColor: IPSetStrokeColor, getStrokeColor: IPGetStrokeColor, setFillColor: IPSetFillColor, getFillColor: IPGetFillColor ]]; }; MakeIPSliceFromFile: PUBLIC PROC [fullName: Rope.ROPE, feedback: Viewer, transform: ImagerTransformation.Transformation _ NIL, includeByValue: BOOL] RETURNS [slice: Slice] = { ipMaster: Interpress.Master; success: BOOL _ FALSE; [ipMaster, success] _ GGUtility.OpenInterpressOrComplain[feedback, fullName]; IF NOT success THEN RETURN; slice _ MakeIPSliceFromMaster[ipMaster, 2834.646, fullName, feedback, transform, includeByValue]; }; MakeIPSliceFromMaster: PUBLIC PROC [ipMaster: Interpress.Master, pixelsPerUnit: REAL _ 2834.646, fullName: Rope.ROPE _ NIL, feedback: Viewer, transform: ImagerTransformation.Transformation _ NIL, includeByValue: BOOL] RETURNS [slice: Slice] = { memContext: Imager.Context; localBox: BoundBox; memContext _ ImagerMemory.NewMemoryContext[]; -- in pixels Imager.ScaleT[memContext, pixelsPerUnit]; Interpress.DoPage[master: ipMaster, page: 1, context: memContext, log: NIL]; BEGIN -- compute the bounding box MakeScene: PROC [context: Imager.Context] = { ImagerMemory.Replay[memContext, context]; }; scene: Scene; scene _ GGFromImager.Capture[action: MakeScene ! GGFromImager.Warning => { GGError.Append[feedback, message, oneLiner]; RESUME; }]; localBox _ GGObjects.TightBoxOfScene[scene]; END; slice _ MakeIPSliceAux[fullName, memContext, feedback, transform, includeByValue, localBox]; }; MakeIPSliceFromMaskPixel: PUBLIC PROC [pa: Imager.PixelArray, color: Color, feedback: Viewer, transform: ImagerTransformation.Transformation _ NIL] RETURNS [slice: Slice] = { memContext: Imager.Context; ipData: IPData; inverse: ImagerTransformation.Transformation; inverseScale: Imager.VEC; localBox: BoundBox; memContext _ ImagerMemory.NewMemoryContext[]; Imager.SetColor[memContext, color]; Imager.MaskPixel[memContext, pa]; transform _ IF transform=NIL THEN GGTransform.Identity[] ELSE transform; inverse _ ImagerTransformation.Invert[transform]; inverseScale _ ImagerTransformation.Factor[inverse].s; localBox _ GGBoundBox.BoundBoxOfPixelArray[pa]; ipData _ NEW[IPDataObj _ [localBox: localBox, includeByValue: TRUE, file: NIL, mem: memContext, transform: transform, inverse: inverse, inverseScale: inverseScale ]]; slice _ NEW[SliceObj _ [ class: GGSlice.FetchSliceClass[$IP], data: ipData, parent: NIL, selectedInFull: [FALSE, FALSE, FALSE], boundBox: GGBoundBox.CreateBoundBox[0,0,0,0] -- gets set next line ]]; IPUpdateBoundBox[slice]; }; MakeIPSliceAux: PROC [fileName: Rope.ROPE, memContext: Imager.Context, feedback: Viewer, transform: ImagerTransformation.Transformation, includeByValue: BOOL, localBox: BoundBox] RETURNS [slice: Slice] = { ipData: IPData; inverse: ImagerTransformation.Transformation; inverseScale: Imager.VEC; transform _ IF transform=NIL THEN GGTransform.Identity[] ELSE transform; inverse _ ImagerTransformation.Invert[transform]; inverseScale _ ImagerTransformation.Factor[inverse].s; ipData _ NEW[IPDataObj _ [localBox: localBox, includeByValue: includeByValue, file: fileName, mem: memContext, transform: transform, inverse: inverse, inverseScale: inverseScale]]; slice _ NEW[SliceObj _ [ class: GGSlice.FetchSliceClass[$IP], data: ipData, parent: NIL, selectedInFull: [FALSE, FALSE, FALSE], boundBox: GGBoundBox.CreateBoundBox[0,0,0,0] -- gets set next line ]]; IPUpdateBoundBox[slice]; }; SetIncludeByValue: PUBLIC PROC [slice: Slice, includeByValue: BOOL] = { ipData: IPData _ NARROW[slice.data]; ipData.includeByValue _ includeByValue; }; GetIncludeByValue: PUBLIC PROC [slice: Slice] RETURNS [includeByValue: BOOL] = { ipData: IPData _ NARROW[slice.data]; includeByValue _ ipData.includeByValue; }; IPUpdateBoundBox: PROC [slice: Slice] = { ipData: IPData _ NARROW[slice.data]; ipData.tightBox _ GGBoundBox.BoundBoxOfBoundBox[ipData.localBox, ipData.transform]; slice.boundBox _ GGBoundBox.CopyBoundBox[ipData.tightBox]; GGBoundBox.AllowForJoints[slice.boundBox]; }; IPBoundBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox]= { RETURN[slice.boundBox]; }; IPTightBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox]= { ipData: IPData _ NARROW[slice.data]; RETURN[ipData.tightBox]; }; IPCopy: PROC [slice: Slice] RETURNS [copy: Slice _ NIL] = { ipData: IPData _ NARROW[slice.data]; transform: Imager.Transformation _ ImagerTransformation.Copy[ipData.transform]; copy _ MakeIPSliceAux[ipData.file, ipData.mem, NIL, transform, ipData.includeByValue, ipData.localBox]; }; IPDrawParts: PROC [slice: Slice, parts: SliceParts, dc: Imager.Context, camera: GGInterfaceTypes.CameraData, quick: BOOL] = { DoDrawInnards: PROC = { Imager.ConcatT[dc, ipData.transform]; Imager.SetColor[dc, Imager.black]; -- otherwise the replay could end up in a random color. ImagerMemory.Replay[ipData.mem, dc]; }; ipData: IPData _ NARROW[slice.data]; ipParts: IPParts _ NARROW[parts]; IF ipParts = NIL OR ipParts.innards THEN Imager.DoSaveAll[dc, DoDrawInnards]; }; IPDrawSelectionFeedback: PROC [slice: Slice, selectedParts: SliceParts, hotParts: SliceParts, dc: Imager.Context, camera: CameraData, dragInProgress, caretIsMoving, hideHot, quick: BOOL] = { DoDrawFeedback: PROC = { t: ImagerTransformation.Transformation _ ipData.transform; box: BoundBox _ ipData.localBox; thisCPisHot, thisCPisSelected: BOOL; pts: ARRAY [0..3] OF Point; pts[0] _ ImagerTransformation.Transform[t, [box.loX, box.loY]]; pts[1] _ ImagerTransformation.Transform[t, [box.loX, box.hiY]]; pts[2] _ ImagerTransformation.Transform[t, [box.hiX, box.hiY]]; pts[3] _ ImagerTransformation.Transform[t, [box.hiX, box.loY]]; FOR point: INTEGER IN [0..ipMaxPoints) DO thisCPisHot _ hotIPParts#NIL AND hotIPParts.points[point]; thisCPisSelected _ normalIPParts#NIL AND normalIPParts.points[point]; IF thisCPisHot THEN GGShapes.DrawSelectedJoint[dc, pts[point], hot]; IF thisCPisSelected THEN GGShapes.DrawSelectedJoint[dc, pts[point], normal]; IF NOT thisCPisHot AND NOT thisCPisSelected THEN GGShapes.DrawCP[dc, pts[point]]; ENDLOOP; Imager.SetStrokeJoint[dc, round]; Imager.SetStrokeWidth[dc, 1.0]; Imager.SetColor[dc, Imager.black]; IF normalIPParts # NIL THEN { FOR edge: INTEGER IN [0..ipMaxEdges) DO IF normalIPParts.edges[edge] THEN SELECT edge FROM 0 => Imager.MaskVector[dc, pts[0], pts[1]]; 1 => Imager.MaskVector[dc, pts[1], pts[2]]; 2 => Imager.MaskVector[dc, pts[2], pts[3]]; 3 => Imager.MaskVector[dc, pts[3], pts[0]]; ENDCASE => ERROR; ENDLOOP; }; }; normalIPParts, hotIPParts: IPParts; ipData: IPData _ NARROW[slice.data]; IF caretIsMoving OR dragInProgress THEN RETURN; IF selectedParts = NIL AND hotParts = NIL THEN RETURN; normalIPParts _ NARROW[selectedParts]; hotIPParts _ NARROW[hotParts]; IF camera.quality#quality THEN Imager.DoSaveAll[dc, DoDrawFeedback]; }; IPDrawTransform: PROC [slice: Slice, parts: SliceParts, dc: Imager.Context, camera: GGInterfaceTypes.CameraData, transform: ImagerTransformation.Transformation] = { DoIPDrawTransform: PROC = { Imager.ConcatT[dc, ImagerTransformation.Concat[ipData.transform, transform]]; Imager.SetColor[dc, Imager.black]; -- otherwise the replay could end up in a random color. ImagerMemory.Replay[ipData.mem, dc]; }; ipData: IPData _ NARROW[slice.data]; Imager.DoSaveAll[dc, DoIPDrawTransform]; }; IPTransform: PROC [slice: Slice, parts: SliceParts, transform: ImagerTransformation.Transformation] = { ipData: IPData _ NARROW[slice.data]; ipData.transform _ ImagerTransformation.Concat[ipData.transform, transform]; ipData.inverse _ ImagerTransformation.Invert[ipData.transform]; ipData.inverseScale _ ImagerTransformation.Factor[ipData.inverse].s; IPUpdateBoundBox[slice]; }; IPDescribe: PROC [slice: Slice, parts: SliceParts] RETURNS [rope: Rope.ROPE] = { prefix: Rope.ROPE; ipParts: IPParts _ NARROW[parts]; eCount, pCount: NAT _ 0; FOR i: INTEGER IN [0..ipMaxPoints) DO IF ipParts.points[i] THEN { pCount _ pCount + 1; prefix _ ipCornerRopes[i]; }; ENDLOOP; FOR i: INTEGER IN [0..ipMaxEdges) DO IF ipParts.edges[i] THEN { eCount _eCount + 1; prefix _ ipEdgeRopes[i]; }; ENDLOOP; IF pCount+eCount>1 THEN RETURN["multiple parts of an IP slice"]; IF eCount=0 AND pCount=0 THEN RETURN["NO parts of an IP slice"]; rope _ Rope.Concat[prefix, " of an IP slice"]; }; IPFileout: PROC [slice: Slice, f: IO.STREAM] = { FromMemory: PROC [dc: Imager.Context] = { Imager.SetColor[dc, Imager.black]; -- otherwise the replay could end up in a random color. ImagerMemory.Replay[ipData.mem, dc]; }; ipData: IPData _ NARROW[slice.data]; ipRef: ImagerInterpress.Ref; masterStream: IO.STREAM; masterSize: NAT; masterRope: Rope.ROPE; f.PutF["\"%q\" ", [rope[ipData.file]]]; GGParseOut.WriteTransformation[f, ipData.transform]; f.PutRope[" includeByValue: "]; GGParseOut.WriteBOOL[f, ipData.includeByValue]; f.PutChar[IO.CR]; IF ipData.includeByValue THEN { masterStream _ IO.ROS[]; ipRef _ ImagerInterpress.CreateFromStream[masterStream, "Interpress/Xerox/3.0 "]; ImagerInterpress.DoPage[ipRef, FromMemory]; ImagerInterpress.Finish[ipRef]; masterRope _ masterStream.RopeFromROS[]; masterSize _ Rope.Length[masterRope]; f.PutF["%g\n", [integer[masterSize]]]; f.PutRope[masterRope]; }; }; IPFilein: PROC [f: IO.STREAM, version: REAL, feedback: Viewer] RETURNS [slice: Slice _ NIL] = { ipMaster: Interpress.Master _ NIL; fullName: Rope.ROPE; success: BOOL _ FALSE; transform: ImagerTransformation.Transformation; masterText: REF TEXT; masterSize, nBytesRead: NAT; masterStream: IO.STREAM; includeByValue: BOOL; fullName _ f.GetRopeLiteral[]; GGParseIn.ReadBlank[f]; transform _ GGParseIn.ReadTransformation[f]; IF version >= 8612.04 THEN { goodValue: BOOL; GGParseIn.ReadBlankAndRope[f, "includeByValue:"]; GGParseIn.ReadBlank[f]; [includeByValue, goodValue] _ GGParseIn.ReadBOOL[f]; IF NOT goodValue THEN ERROR; IF includeByValue THEN { masterSize _ GGParseIn.ReadBlankAndNAT[f]; GGParseIn.ReadBlank[f]; masterText _ RefText.ObtainScratch[masterSize+2]; nBytesRead _ IO.GetBlock[f, masterText, 0, masterSize]; IF nBytesRead # masterSize THEN ERROR; masterStream _ IO.TIS[masterText]; ipMaster _ Interpress.FromStream[masterStream, NIL]; slice _ GGSlice.MakeIPSliceFromMaster[ipMaster, 1.0, fullName, feedback, transform, TRUE]; } ELSE { slice _ MakeIPSliceFromFile[fullName, feedback, transform, FALSE]; }; } ELSE { slice _ MakeIPSliceFromFile[fullName, feedback, transform, FALSE]; }; }; GetInterpressFileName: PROC [ipName: Rope.ROPE, feedback: Viewer] RETURNS [fullName: Rope.ROPE _ NIL, success: BOOL _ TRUE] = { cp: FS.ComponentPositions; IF Rope.Length[ipName]=0 OR Rope.Equal[ipName, ""] THEN RETURN[NIL, FALSE]; [fullName, cp, ] _ FS.ExpandName[ipName ! FS.Error => { GGError.Append[feedback, "Gargoyle: FS Error during name expansion", oneLiner]; GGError.Blink[feedback]; success _ FALSE; CONTINUE; }; ]; IF success AND cp.ext.length=0 THEN fullName _ Rope.Concat[fullName, ".IP"]; -- add IP extension }; IPEmptyParts: PROC [slice: Slice, parts: SliceParts] RETURNS [BOOL] = { RETURN[IsEmpty[parts]]; }; IPNewParts: PROC [slice: Slice, hitData: REF ANY, mode: SelectMode] RETURNS [parts: SliceParts] = { ipHitData: IPHitData _ NARROW[hitData]; ipParts: IPParts _ NEW[IPPartsObj _ [points: ALL[FALSE], edges: ALL[FALSE] ] ]; SELECT mode FROM joint => { IF ipHitData.point#-1 THEN { ipParts.points[ipHitData.point] _ TRUE; } ELSE { hitData: REF ANY; pointHitData: IPHitData; success: BOOL; wholeD: SliceDescriptor; wholeParts: SliceParts; wholeParts _ IPNewParts[slice, NIL, slice]; wholeD _ NEW[SliceDescriptorObj _ [slice, wholeParts]]; [----, ----, hitData, success] _ slice.class.closestPoint[wholeD, ipHitData.hitPoint, GGUtility.plusInfinity]; IF NOT success THEN ERROR; pointHitData _ NARROW[hitData]; IF pointHitData.point = -1 THEN ERROR; ipParts.points[pointHitData.point] _ TRUE; }; ipParts.innards _ TRUE; }; segment => IF ipHitData.edge#-1 THEN{ ipParts.edges[ipHitData.edge] _ TRUE; ipParts.innards _ TRUE; }; controlPoint => { ipParts.points _ ALL[TRUE]; ipParts.edges _ ALL[TRUE]; }; traj, slice, topLevel => MakeComplete[ipParts]; none => { -- prefer corner to edge if both are true IF ipHitData.point#-1 THEN ipParts.points[ipHitData.point] _ TRUE ELSE IF ipHitData.edge#-1 THEN ipParts.edges[ipHitData.edge] _ TRUE; }; ENDCASE => ERROR; parts _ ipParts; -- RETURN[ipParts] }; IPUnionParts: PROC [slice: Slice, partsA: SliceParts, partsB: SliceParts] RETURNS [aPlusB: SliceParts] = { ipPartsA: IPParts _ NARROW[partsA]; ipPartsB: IPParts _ NARROW[partsB]; newParts: IPParts _ NEW[IPPartsObj _ [points: ALL[FALSE], edges: ALL[FALSE] ] ]; FOR i: INTEGER IN [0..ipMaxPoints) DO newParts.points[i] _ ipPartsA.points[i] OR ipPartsB.points[i]; ENDLOOP; FOR i: INTEGER IN [0..ipMaxEdges) DO newParts.edges[i] _ ipPartsA.edges[i] OR ipPartsB.edges[i]; ENDLOOP; newParts.innards _ ipPartsA.innards OR ipPartsB.innards; aPlusB _ newParts; -- RETURN[newParts] }; IPDiffParts: PROC [slice: Slice, partsA: SliceParts, partsB: SliceParts] RETURNS [aMinusB: SliceParts] = { ipPartsA: IPParts _ NARROW[partsA]; ipPartsB: IPParts _ NARROW[partsB]; newParts: IPParts _ NEW[IPPartsObj _ [points: ALL[FALSE], edges: ALL[FALSE] ] ]; IF partsB=NIL THEN {newParts^ _ ipPartsA^; aMinusB _ newParts; RETURN;}; FOR i: INTEGER IN [0..ipMaxPoints) DO newParts.points[i] _ IF ipPartsB.points[i] THEN FALSE ELSE ipPartsA.points[i]; ENDLOOP; FOR i: INTEGER IN [0..ipMaxEdges) DO newParts.edges[i] _ IF ipPartsB.edges[i] THEN FALSE ELSE ipPartsA.edges[i]; ENDLOOP; newParts.innards _ IF ipPartsB.innards THEN FALSE ELSE ipPartsA.innards; aMinusB _ newParts; -- RETURN[newParts] }; IPMovingParts: PROC [slice: Slice, parts: SliceParts] RETURNS [moving: SliceParts] = { newParts: IPParts _ NEW[IPPartsObj _ [points: ALL[TRUE], edges: ALL[TRUE], innards: TRUE ] ]; IF IsEmpty[parts] THEN { -- nothing at all is moving newParts.edges _ ALL[FALSE]; newParts.points _ ALL[FALSE]; newParts.innards _ FALSE; }; moving _ newParts; -- RETURN[newParts] }; IPFixedParts: PROC [slice: Slice, parts: SliceParts, selectedList: LIST OF REF ANY] RETURNS [fixed: SliceParts] = { moving: SliceParts _ IPMovingParts[slice, parts]; allParts: IPParts _ NEW[IPPartsObj]; MakeComplete[allParts]; fixed _ IPDiffParts[slice, allParts, moving]; }; IPAugmentParts: PROC [slice: Slice, parts: SliceParts, selectClass: SelectionClass] RETURNS [more: SliceParts] = { ipParts: IPParts _ NARROW[parts]; newParts: IPParts _ NEW[IPPartsObj _ [points: ipParts.points, edges: ipParts.edges] ]; FOR i: INTEGER IN [0..ipMaxEdges) DO IF ipParts.edges[i] THEN SELECT i FROM 0 => {newParts.points[0] _ TRUE; newParts.points[1] _ TRUE;}; 1 => {newParts.points[1] _ TRUE; newParts.points[2] _ TRUE;}; 2 => {newParts.points[2] _ TRUE; newParts.points[3] _ TRUE;}; 3 => {newParts.points[3] _ TRUE; newParts.points[0] _ TRUE;}; ENDCASE => ERROR; ENDLOOP; more _ newParts; }; IPPointsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [pointGen: PointGenerator] = { parts: IPParts _ NARROW[sliceD.parts]; pointGen _ NEW[PointGeneratorObj _ [sliceD, 0, 0, NIL] ]; FOR point: INTEGER IN [0..ipMaxPoints) DO IF parts.points[point] THEN pointGen.toGo _ pointGen.toGo + 1; ENDLOOP; }; IPPointPairsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [pointPairGen: PointPairGenerator] = { parts: IPParts _ NARROW[sliceD.parts]; pointPairGen _ NEW[PointPairGeneratorObj _ [sliceD, 0, 0, NIL] ]; -- toGo and index not used FOR edge: INTEGER IN [0..ipMaxEdges) DO IF parts.edges[edge] THEN pointPairGen.toGo _ pointPairGen.toGo + 1; ENDLOOP; }; IPNextPoint: PROC [pointGen: PointGenerator] RETURNS [pointAndDone: GGModelTypes.PointAndDone] = { IF pointGen=NIL OR pointGen.toGo = 0 THEN { pointAndDone.done _ TRUE; RETURN; } ELSE { sliceD: SliceDescriptor _ pointGen.sliceD; ipData: IPData _ NARROW[sliceD.slice.data]; ipParts: IPParts _ NARROW[sliceD.parts]; index: INTEGER _ -1; box: BoundBox _ ipData.localBox; pointAndDone.done _ FALSE; FOR index _ pointGen.index, index+1 UNTIL index >=ipMaxPoints DO IF ipParts.points[index] THEN EXIT; -- index will point to next available point ENDLOOP; pointAndDone.point _ ImagerTransformation.Transform[ipData.transform, SELECT index FROM 0 => [box.loX, box.loY], 1 => [box.loX, box.hiY], 2 => [box.hiX, box.hiY], 3 => [box.hiX, box.loY], ENDCASE => ERROR ]; pointGen.toGo _ pointGen.toGo-1; pointGen.index _ IF pointGen.toGo=0 THEN 99 ELSE index+1; -- bump to the next available point in the generator }; }; IPNextPointPair: PROC [pointPairGen: PointPairGenerator] RETURNS [pointPairAndDone: GGModelTypes.PointPairAndDone] = { IF pointPairGen=NIL OR pointPairGen.toGo = 0 THEN { pointPairAndDone.done _ TRUE; RETURN; } ELSE { sliceD: SliceDescriptor _ pointPairGen.sliceD; ipData: IPData _ NARROW[sliceD.slice.data]; ipParts: IPParts _ NARROW[sliceD.parts]; box: BoundBox _ ipData.localBox; t: ImagerTransformation.Transformation _ ipData.transform; index: INTEGER _ -1; pointPairAndDone.done _ FALSE; FOR index _ pointPairGen.index, index+1 UNTIL index >=ipMaxEdges DO IF ipParts.edges[index] THEN EXIT; -- index will point to next availabe edge ENDLOOP; SELECT index FROM -- index in [0..ipMaxEdges) of next available edge 0 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, [box.loX, box.loY] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, [box.loX, box.hiY] ]; }; 1 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, [box.loX, box.hiY] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, [box.hiX, box.hiY] ]; }; 2 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, [box.hiX, box.hiY] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, [box.hiX, box.loY] ]; }; 3 => { pointPairAndDone.lo _ ImagerTransformation.Transform[t, [box.hiX, box.loY] ]; pointPairAndDone.hi _ ImagerTransformation.Transform[t, [box.loX, box.loY] ]; }; ENDCASE => SIGNAL GGError.Problem[msg: "Broken Invariant"]; pointPairGen.toGo _ pointPairGen.toGo - 1; pointPairGen.index _ IF pointPairGen.toGo=0 THEN 99 ELSE index+1; -- bump to the next available pair in the generator }; }; IPClosestPoint: PROC [sliceD: SliceDescriptor, testPoint: Point, tolerance: REAL] RETURNS [bestPoint: Point, bestDist: REAL, hitData: REF ANY, success: BOOL _ FALSE] = { index: NAT _ 9999; slice: Slice _ sliceD.slice; ipData: IPData _ NARROW[slice.data]; toleranceBox: GGBasicTypes.BoundBoxObj _ [ipData.tightBox.loX-tolerance, ipData.tightBox.loY-tolerance, ipData.tightBox.hiX+tolerance, ipData.tightBox.hiY+tolerance, FALSE, FALSE]; -- box in GG coordinates IF PointIsInBox[testPoint, toleranceBox] THEN { ipHitData: IPHitData; ipParts: IPParts _ NARROW[sliceD.parts]; localTestpoint: Point _ GGTransform.Transform[ipData.inverse, testPoint]; localTolerance: REAL _ ABS[tolerance*MAX[ipData.inverseScale.x, ipData.inverseScale.y]]; -- MAX function is not correct when text is skewed. [bestDist, index, bestPoint, success] _ GGBoundBox.NearestPoint[ipData.localBox, localTestpoint, localTolerance, ipParts.points]; IF success THEN { bestPoint _ GGTransform.Transform[ipData.transform, bestPoint]; bestDist _ GGVector.Distance[testPoint, bestPoint]; hitData _ ipHitData _ NEW[IPHitDataObj _ [point: index, edge: -1, hitPoint: bestPoint] ]; }; }; }; IPClosestSegment: PROC [sliceD: SliceDescriptor, testPoint: Point, tolerance: REAL] RETURNS [bestPoint: Point, bestDist: REAL, hitData: REF ANY, success: BOOL _ FALSE] = { slice: Slice _ sliceD.slice; ipData: IPData _ NARROW[slice.data]; toleranceBox: GGBasicTypes.BoundBoxObj _ [ipData.tightBox.loX-tolerance, ipData.tightBox.loY-tolerance, ipData.tightBox.hiX+tolerance, ipData.tightBox.hiY+tolerance, FALSE, FALSE]; -- box in GG coordinates IF PointIsInBox[testPoint, toleranceBox] THEN { ipHitData: IPHitData; index: NAT _ 9999; ipData: IPData _ NARROW[slice.data]; ipParts: IPParts _ NARROW[sliceD.parts]; localTestpoint: Point _ GGTransform.Transform[ipData.inverse, testPoint]; -- transfrom point to "IP space" localTolerance: REAL _ ABS[tolerance*MAX[ipData.inverseScale.x, ipData.inverseScale.y]]; -- MAX function is not correct when text is skewed. [bestDist, index, bestPoint, success] _ GGBoundBox.NearestSegment[ipData.localBox, localTestpoint, localTolerance, ipParts.points]; IF success THEN { bestPoint _ GGTransform.Transform[ipData.transform, bestPoint]; bestDist _ GGVector.Distance[testPoint, bestPoint]; hitData _ ipHitData _ NEW[IPHitDataObj _ [point: -1, edge: index, hitPoint: bestPoint] ]; }; }; }; ipCornerRopes: ARRAY [0..ipMaxPoints) OF Rope.ROPE = [ "lower left corner", "upper left corner", "upper right corner", "lower right corner"]; ipEdgeRopes: ARRAY [0..ipMaxEdges) OF Rope.ROPE = [ "left edge", "top edge", "right edge", "bottom edge"]; IPSetStrokeColor: PROC [slice: Slice, parts: SliceParts, color: Imager.Color] = {}; IPGetStrokeColor: PROC [slice: Slice, parts: SliceParts] RETURNS [color: Imager.Color _ NIL] = {}; IPSetFillColor: PROC [slice: Slice, color: Imager.Color] = {}; IPGetFillColor: PROC [slice: Slice] RETURNS [color: Imager.Color _ NIL] = {}; IPSetStrokeWidth: PROC [slice: Slice, parts: SliceParts, strokeWidth: REAL] = {}; IPGetStrokeWidth: PROC [slice: Slice, parts: SliceParts] RETURNS [strokeWidth: REAL _ 0.0] = {}; printPrefix: ATOM; screenPrefix: ATOM; Init: PRIVATE PROC [] = { printPrefix _ Atom.MakeAtom["xerox/pressfonts/"]; screenPrefix _Atom.MakeAtom ["xerox/tiogafonts/"]; }; Init[]; END. ¾GGSliceImplB.mesa Copyright c 1986 by Xerox Corporation. All rights reserved. Last edited by Pier on November 19, 1986 6:29:39 pm PST Last edited by Bier on January 15, 1987 1:23:35 am PST Contents: Implements various slice classes in Gargoyle. [Artwork node; type 'ArtworkInterpress on' to command tool] Text Points as shown Text Edges: 0 => left, 1=> top, 2=> right, 3=> bottom, 4 => baseline, 5 => horizontal centerline, 6 => vertical centerline Filled in by BoxClosestPoint, BoxClosestSegment, in same order as TextPointArray and TextEdgeArray. -1 means not hit Procs peculiar to text Fundamentals Drawing Transforming Textual Description Parts Hit Testing Style other font related values to be filled in by SetTextFont A fontName is a font designation with family, size, and face, like Helvetica23BI or TimesRoman6.95 (real font sizes are OK) can't user IO.GetInt because a string like Helvetica9BI causes it to fail! A fontName is a font designation with family, size, and face, like Helvetica23BI or TimesRoman6 IF fontSize=0.0 THEN ERROR; Now, what do you want to do?? Answer: see some text in your picture. How?? Answer: manufacture a font size that will be big enough to see. FontFromStyleParams has failed to find the requested font and substituted "xerox/xc2-2-2/modern". We don't want a Xerox font for a print font, so we will substitute Xerox/PressFonts/TimesRoman instead Takes a complex name like "xerox/pressfonts/Helvetica-MIR" and a font size and returns a simple name like Helvetica10I Text slice class procs GGModelTypes.SliceBoundBoxProc GGModelTypes.SliceTightBoxProc The following 7 values are of interest: left: = 0.0. right: = Width[string]. middle: (left + right) / 2. top: base: 0.0 bottom: center: (top + bottom) / 2 ASSERT: extents all positivesubtract left and descent, add right and ascent Bounding Boxes are calculated from screen fonts (which are larger). feedbackBoxes are used for hit testing and are calculated from the print font. GGModelTypes.SliceCopyProc Drawing GGModelTypes.SliceDrawPartsProc GGModelTypes.SliceDrawSelectionFeedbackProc IF NOT quick AND normalTextParts#NIL THEN { called with the dc transformation already set to object coordinates called with the dc transformation already set to object coordinates GGModelTypes.SliceDrawTransformProc Transforming GGModelTypes.SliceTransformProc Textual Description GGModelTypes.SliceDescribeProc GGModelTypes.SliceFileoutProc Write a description of yourself onto stream f. GGModelTypes.SliceFileinProc Read a description of yourself from stream f. Strings now surronded by quotes. Formerly surrounded by parens. We keep evoloving font names Handle Drop Shadows Hit Testing GGModelTypes.SliceEmptyPartsProc GGModelTypes.SliceNewPartsProc GGModelTypes.SliceUnionPartsProc GGModelTypes.SliceMovingPartsProc If anything is moving, everything is moving GGModelTypes.SliceFixedPartsProc Start with every part and remove the ones that are moving GGModelTypes.SliceAugmentPartsProc For every edge, add its corresponding points GGModelTypes.SliceClosestPointProc GGModelTypes.SliceClosestSegmentProc hitData _ NIL; -- automatically done by compiler Finds the point which is in mask and nearest to testPoint (and its distance from testPoint). Number the points as shown in the picture above Finds the edge which is in mask and nearest to testPoint (and its distance from testPoint). Style Interpress slice data and class procs slice.boundBox allows for joints and stroke-width IPHitDataObj: TYPE = RECORD [ point: [-1..ipMaxPoints), -- which point of box is hit. edge: [-1..ipMaxEdges) -- which edge of box is hit. THIS DEF CAUSES A FATAL COMPILER ERROR IN PASS FIVE !! ]; Routines peculiar to IPSlices Fundamentals Drawing Transforming Textual Description Parts Part Generators Hit Testing Style The interpress master is assumed to be in units of pixelsPerUnit pixels. The memory context has 1 unit = 1/72.0 inch. The transformations should be set up so that MaskPixel[pa] will look correct in a context where the units are pixels if transform is concatenated before pa is drawn. In the common case, transform will be a pure translation. The memory context has 1 unit = 1/72.0 inch. IP Class Routines Fundamentals GGModelTypes.SliceBoundBoxProc GGModelTypes.SliceBoundBoxProc GGModelTypes.SliceCopyProc Drawing GGModelTypes.SliceDrawPartsProc GGModelTypes.SliceDrawSelectionFeedbackProc The feedback rectangle can be rotated. It is the tight bounding box of the rectangular interpress master. It is derived from the local box. IF NOT quick AND normalIPParts # NIL THEN { GGModelTypes.SliceDrawTransformProc Transforming GGModelTypes.SliceTransformProc Textual Description GGModelTypes.SliceDescribeProc GGModelTypes.SliceFileoutProc Write a description of yourself onto stream f. Write the interpress into a Rope.ROPE. Send the rope to stream f. GGModelTypes.SliceFileinProc Read a description of yourself from stream f. next make an IPSlice and return it Hit Testing GGModelTypes.SliceEmptyPartsProc GGModelTypes.SliceNewPartsProc GGModelTypes.SliceUnionPartsProc GGModelTypes.SliceMovingPartsProc If anything is moving, everything is moving GGModelTypes.SliceFixedPartsProc Start with every part and remove the ones that are moving GGModelTypes.SliceAugmentPartsProc For every edge, add its corresponding points ENDCASE => [0.0, 0.0] -- should not happen GGModelTypes.SliceClosestPointProc hitData _ NIL; -- automatically done by compiler GGModelTypes.SliceClosestSegmentProc hitData _ NIL; -- automatically done by compiler Style ÊJê˜codešœ™Kšœ Ïmœ1™Kšœ Ÿœ¡,˜>K˜—Kšœ ŸœŸœ˜'šœŸœŸœ˜Kšœt™tKšœ¡˜9Kšœ¡˜6K˜K˜—K˜Kšž œŸœŸœŸœ˜$K™K™šžœŸœŸœŸœ˜CKšŸœ ˜ šœŸœ˜˜ KšÏb ™ —Kšœ˜K˜šœ˜Kš¢™—Kšœ˜K˜!K˜1˜1Kš¢ ™ —šœ˜Kš¢™—K˜K˜˜Kš¢™—K˜K˜K˜K˜K˜K˜˜Kš¢ ™ —K˜+K˜3K˜K˜!Kšœ˜KšœŸœ˜Kšœ#˜#K˜'K˜+˜/Kš¢™—K˜#K˜#K˜#K˜#K˜K˜K˜—K˜K˜—šž œŸœŸœ ŸœŸœdŸœŸœŸœeŸœ˜ÄKšœV˜VšœŸœ˜(Kšœ ¡'˜GKšœ(¡'˜OKšœ˜Kšœ˜Kšœ˜Kšœ5˜5KšœŸœ¡'˜DKšœ ˜ Kšœ ˜ Kšœ Ÿœ¡(˜7Kšœ˜Kšœ#˜#Kšœ˜Kšœ˜Kš¢8™8—šœŸœ ˜K˜&Kšœ˜KšœŸœ˜ KšœŸœŸœŸœ˜&Kšœ.¡˜EKšœ Ÿ˜Kšœ˜—Kš¢ œ¡˜?K˜K˜—š žœŸœŸœŸœ Ÿœ˜AKš ŸœŸœŸœŸœŸœŸœ˜RK˜K˜—šž œŸœŸœŸœ˜;Kšœ˜KšŸœŸœŸœ˜&Kšœ Ÿœ ˜Kšœ2¡˜@Kšœ˜K˜K˜—šž œŸœŸœ˜-Kšœ˜KšŸœŸœŸœ˜&Kšœ Ÿœ ˜KšœU¡˜cKšœ˜K˜K˜—šž œŸœ˜Kš ŸœŸœŸœ ŸœŸœ˜3K˜K˜—šžœŸœŸœ*ŸœŸœŸœŸœ˜†K•StartOfExpansionL[from: REAL, precision: Convert.RealPrecision _ 7, useE: BOOL _ FALSE]šœŸœ"˜5šœŸœŸœ Ÿ˜*Kšœ˜K˜ K˜KšŸœ˜—–[atom: ATOM]šœC˜CK˜—K˜K˜—šžœŸœŸœŸœŸœŸœ*Ÿœ˜‡KšœxŸœ™{š ž œŸœŸœŸœŸœŸœ ˜:KšœŸœ˜KšœŸœ˜š ŸœŸœ ŸœŸœŸœŸœ Ÿ˜YK˜KšŸœ˜—KšŸœ ŸœŸœ¡=˜cšŸœŸœ¡˜!š ŸœŸœ ŸœŸœŸœŸœ Ÿ˜YKšœ˜K˜KšŸœ˜—K˜-KšŸœ Ÿœ¡$˜AK˜—K˜—K˜KšœŸœ˜KšœŸœŸœ˜K–[pName: ROPE]š œ ŸœŸœŸœŸœ ˜)Kš œ ¡œŸœ:ŸœŸœ˜yšœ"ŸœŸœ˜JKš¢ Ðbk¢=™J—Kš œ¡œŸœ-Ÿœ ŸœŸœ˜yK˜K– [base: ROPE, index: INT _ 0]šœ'˜'šŸœŸ˜KšœŸœ˜ šœ˜KšœŸœ$˜.KšœŸœ ˜KšœŸœ ˜Kšœ˜—šœ˜KšœŸœ$˜/KšœŸœ$˜/KšœŸœ Ÿœ Ÿœ ˜4KšœŸœ Ÿœ Ÿœ ˜6Kšœ˜—KšŸœŸœ˜—šœ ŸœŸœŸ˜KšœŸœ˜K˜ K˜KšŸœ ˜—K˜K˜—šž œŸœŸœŸœ˜RKšœ_™_Kšœ˜Kšœ Ÿœ˜Kšœ˜Kšœ Ÿœ˜Kš ŸœŸœ ŸœŸœŸœ˜6Kšœ Ÿœ ˜KšœVŸœ ˜dKšŸœŸœŸœ™–p[prefix: ATOM, family: ATOM, face: NodeStyle.FontFace, size: REAL, alphabets: NodeStyle.FontAlphabets]šŸœŸœ˜KšœŸœ7˜BKšœU˜UKšœ˜K™ŠKšœŸœ ŸœŸœ ¡ ˜EK˜—Kš¢œ‡˜™–>[s1: ROPE, s2: ROPE, pos1: INT _ 0, case: BOOL _ TRUE]šŸœ¢ œ)ŸœŸœ˜\KšœÉ™ÉKšœŸœ˜Kšœ˜Kšœ˜Kšœ˜KšœB˜BKš¢œ‡˜™Kšœf˜fKšœ˜K˜—K–p[prefix: ATOM, family: ATOM, face: NodeStyle.FontFace, size: REAL, alphabets: NodeStyle.FontAlphabets]š¢œˆ˜›K–>[s1: ROPE, s2: ROPE, pos1: INT _ 0, case: BOOL _ TRUE]š Ÿœ¢ œŸœŸœ¢œ¢œ¡˜—Kšœ˜Kšœ!˜!Kšœ˜Kšœ˜Kšœ˜šŸ˜šœ ˜ KšœM˜MKšœ˜K˜——K˜K˜—š ž œŸœŸœŸœ(Ÿœ˜`Kšœ˜Kš ŸœŸœŸœŸœŸœ˜0Kšœ Ÿœ ˜KšŸœ(˜.K˜K˜—šžœŸœŸœŸœ%˜gKšœ˜Kšœ Ÿœ ˜Kšœ%˜%Kšœ˜K˜K˜—š žœŸœŸœŸœŸœ˜PKšœ˜Kšœ Ÿœ ˜KšŸœ˜K˜K˜—šž œŸœŸœ#˜Kšœ#ŸœŸœ˜IKšŸœ Ÿœ1˜DKšŸœŸœ4˜LKš ŸœŸœ ŸœŸœŸœ!˜QKšŸœ˜—Kš ŸœŸœŸœŸœŸœ™+šŸœŸœŸœ˜K–>[context: Imager.Context, strokeJoint: Imager.StrokeJoint]šœ!˜!K˜šŸœŸœŸœŸ˜)KšŸœŸ˜#šŸœŸ˜Kšœ-˜-Kšœ.˜.Kšœ.˜.Kšœ-˜-Kšœ-˜-Kšœ.˜.Kšœ-˜-KšŸœŸœ˜—KšŸœ˜—K˜—K˜—K˜)KšœŸœ ˜(KšŸœŸœŸœŸœ˜/Kš ŸœŸœŸœ ŸœŸœŸœ˜6KšœŸœ˜(KšœŸœ ˜ KšŸœŸœ&˜DK˜K˜—šžœŸœA˜OK™CKšœŸœŸœŸœ˜cKš œŸœŸœŸœŸœ˜QKšœ¡)˜GKšœ3¡,˜_Kšœ#˜#K˜K˜—šžœŸœA˜UK™CšžœŸœ˜Kšœ1˜1KšœŸœŸœŸœ˜cKš œŸœŸœŸœŸœ˜]Kšœ¡)˜GKšœ#˜#K˜—Kšœ'˜'K˜K˜—šž  œŸœ˜¦Kšœ#™#šž  œŸœ˜KšœO˜OKšŸœŸœ&˜CKšœ˜K˜—KšœŸœ ˜(Kšœ¢ œ˜*K˜K˜—Kšœ ™ šž œŸœV˜iK™KšœŸœ ˜(KšœP˜PKšœC˜CKšœH˜HKšœ˜K˜K˜—Kšœ™šž œŸœ#Ÿœ Ÿœ˜RK™Kšœ Ÿœ˜KšœŸœ˜%KšœŸœ˜šŸœŸœŸœŸ˜'šŸœŸœ˜Kšœ˜K˜K˜—KšŸœ˜—šŸœŸœŸœŸ˜&šŸœŸœ˜Kšœ˜K˜K˜—KšŸœ˜—KšŸœŸœŸœ#˜AKšŸœ Ÿœ ŸœŸœ˜AKšœ/˜/K˜K˜—šž œŸœŸœŸœ˜2K™K™.K˜KšœŸœ ˜(Kšœe˜eKšœ ŸœŸœ˜K˜)Kšœ ŸœŸœ˜šŸœŸœ˜KšœŸœ˜Kšœ ŸœŸœ˜Kšœ4˜4Kšœ ŸœŸœ˜K˜/K˜—KšŸœŸœ˜$Kšœ ŸœŸœ˜Kšœ6˜6Kšœ ŸœŸœ˜Kšœ,˜,K˜K˜—š ž œŸœŸœŸœ ŸœŸœ˜[K™K™-KšœŸœ˜Kšœ Ÿœ˜K˜ Kšœ)˜)Kšœ'˜'KšœŸœ˜Kšœ&˜&KšœŸœ˜K˜/K™?šŸœŸœ˜Kšœ˜K˜—šŸœ˜Kšœ#˜#K˜%K˜#K˜—K™šŸœŸœ¡˜3Kšœ˜K˜K˜—šŸœŸœŸœ¡,˜NK˜)Kšœ'˜'K˜K˜—šŸœŸœŸœ¡7˜YK˜)K˜)K˜#K˜5K˜—šŸœŸœŸœ¡6˜XK˜)K˜)K˜#Kšœ˜—šŸœ¡U˜\K˜)K˜)K– [f: STREAM]˜K˜#Kšœ˜K– [f: STREAM]š¢™—K˜šŸœŸœ˜Kšœ-˜-KšŸœŸœŸœŸœ˜šŸœŸœ˜K– [f: STREAM]˜Kšœ*˜*K– [f: STREAM]˜Kšœ%˜%K˜—K˜—šŸœŸœ˜K˜—K˜šŸœŸœ¡"˜?Kšœ˜Kšœ2˜2K˜—KšŸœ-˜1K˜šŸœŸ˜Kšœ-˜-K˜—KšŸœ˜Kšœ˜K˜K˜—Kšœ ™ šž œŸœ˜+šŸœŸœŸ˜šœ˜KšœŸœŸœ˜KšœŸœŸœ˜KšœŸœ˜K˜—šœ˜KšœŸœŸœ˜KšœŸœŸœ˜KšœŸœ˜K˜—KšŸœŸœ˜—K˜K˜—šž œŸœŸœŸœ˜7šŸœŸœŸ˜KšœŸœŸœŸœŸœŸœŸœŸœŸœ˜yKšœŸœŸœŸœŸœŸœŸœŸœŸœ˜kKšŸœŸœ˜—K˜K˜—šžœŸœŸœŸœ˜4šŸœŸœŸ˜KšœŸœŸœŸœŸœŸœŸœŸœŸœ˜~KšœŸœŸœŸœŸœŸœŸœŸœŸœ˜pKšŸœŸœ˜—K˜K˜—šžœŸœ#ŸœŸœ˜IKšœ ™ KšŸœ˜K˜—K˜š ž œŸœŸœŸœŸœ˜eKšœ™KšœŸœ ˜+Kš œŸœŸœŸœ ŸœŸœŸœ˜išŸœŸ˜šœ ˜ šŸœŸœ˜Kšœ&Ÿœ˜+K˜—šŸœ˜Kšœ ŸœŸœ˜Kšœ˜Kšœ Ÿœ˜K˜K˜Kšœ!Ÿœ ˜0Kšœ Ÿœ+˜7Kšœ¡œe˜pKšŸœŸœ ŸœŸœ˜KšœŸœ ˜KšŸœŸœŸœ˜&Kšœ'Ÿœ˜,K˜—KšœŸœ˜K˜—šœ ŸœŸœ˜(Kšœ$Ÿœ˜)KšœŸœ˜K˜—šœ˜KšœŸœŸœ˜KšœŸœŸœ˜K˜—Kšœ1˜1šœ ¡)˜3KšŸœŸœ'Ÿ˜GKšŸœŸœŸœ%Ÿœ˜JK˜—KšŸœŸœ˜—Kšœ¡˜'K˜K˜—šžœŸœ8Ÿœ˜lKšœ ™ KšœŸœ ˜'KšœŸœ ˜'Kš œŸœŸœŸœ ŸœŸœŸœ˜hšŸœŸœŸœŸ˜'Kšœ*Ÿœ˜BKšŸœ˜—šŸœŸœŸœŸ˜&Kšœ(Ÿœ˜?KšŸœ˜—Kšœ.Ÿœ˜HKšœ¡˜&K˜K˜—šž œŸœ8Ÿœ˜lKšœŸœ ˜'KšœŸœ ˜'Kš œŸœŸœŸœ ŸœŸœŸœ˜hKšŸœŸœŸœ/Ÿœ˜JšŸœŸœŸœŸ˜'Kš œŸœŸœŸœŸœ˜RKšŸœ˜—šŸœŸœŸœŸ˜&Kš œŸœŸœŸœŸœ˜OKšŸœ˜—Kš œŸœŸœŸœŸœ˜XKšœ¡˜'K˜K˜—šžœŸœ#Ÿœ˜XKšœ!™!K™+Kš œŸœŸœŸœ ŸœŸœŸœ˜ešŸœŸœ¡˜4KšœŸœŸœ˜KšœŸœŸœ˜KšœŸœ˜Kšœ˜—Kšœ¡˜&Kšœ˜K˜—šžœŸœ1ŸœŸœŸœŸœŸœ˜uKšœ ™ K™9Kšœ3˜3KšœŸœ˜(Kšœ˜Kšœ/˜/Kšœ˜K˜—šžœŸœ@Ÿœ˜tKšœ"™"KšœŸœ˜%KšœŸœi˜‚K™,šŸœŸœŸœŸ˜&šŸœŸœŸœŸ˜(Kš œŸœŸœŸœŸœ˜sKšœŸœŸœŸœ˜YKš œŸœŸœŸœŸœ˜uKšœŸœŸœŸœ˜XKšœŸœŸœŸœ˜XKšœŸœŸœŸœ˜YKš œŸœŸœŸœŸœ˜s—KšŸœŸœ˜KšŸœ˜—Kšœ˜K˜—K˜šžœŸœŸœ˜]KšœŸœ˜(Kšœ Ÿœ$Ÿœ˜9šŸœŸœŸœŸ˜+KšŸœŸœ#˜>KšŸœ˜—K˜K˜—šžœŸœŸœ'˜iKšœŸœ˜(KšœŸœ(Ÿœ¡˜\šŸœŸœŸœŸ˜)KšŸœŸœ+˜DKšŸœ˜—K˜K˜—šž œŸœŸœ.˜dšŸœ ŸœŸœŸœ˜+KšœŸœ˜KšŸœ˜K˜—šŸœ˜Kšœ*˜*KšœŸœ˜/KšœŸœ˜,KšœŸœ˜KšœŸœ˜šŸœ!ŸœŸ˜BKšŸœŸœŸœ¡+˜QKšŸœ˜—Kšœa˜aKšœ ˜ KšœŸœŸœŸœ ¡4˜nK˜—K˜K˜—šžœŸœ$Ÿœ6˜xšŸœŸœŸœŸœ˜3KšœŸœ˜KšŸœ˜K˜—šŸœ˜Kšœ.˜.KšœŸœ˜/KšœŸœ˜,Kšœ=˜=KšœŸœ˜KšœŸœ˜šŸœ%ŸœŸ˜EKšŸœŸœŸœ¡)˜NKšŸœ˜—šŸœŸœ¡4˜Fšœ˜KšœM˜MKšœM˜MK˜—šœ˜KšœM˜MKšœN˜NK˜—šœ˜KšœN˜NKšœM˜MK˜—šœ˜KšœM˜MKšœM˜MK˜—šœ˜KšœM˜MKšœM˜MK˜—šœ˜KšœM˜MKšœN˜NK˜—šœ˜KšœM˜MKšœM˜MK˜—KšŸœŸœ*˜;—Kšœ*˜*KšœŸœŸœŸœ ¡3˜uK˜—K˜K˜—šž œŸœ.ŸœŸœ˜RKš ŸœŸœŸœŸœŸœ˜]K˜K˜—šžœŸœ8ŸœŸœŸœ ŸœŸœ ŸœŸœ˜«K™"KšœŸœ˜KšœŸœ˜/Kšœ´ŸœŸœ¡˜ÛšŸœ!Ÿœ˜)KšœŸœ˜,KšœK˜KKšœŸœŸœ Ÿœ5¡3˜Kšœ|˜|šŸœ Ÿœ˜KšœA˜AKšœ3˜3Kšœ ŸœB˜OK˜—K˜—K˜K˜—šžœŸœ8ŸœŸœŸœ ŸœŸœ ŸœŸœ˜­Kšœ$™$Kšœ Ÿœ¡!™1KšœŸœ˜/Kšœ´ŸœŸœ˜ÂšŸœ!Ÿœ˜)KšœŸœ˜Kšœ˜KšœŸœ˜,KšœK˜KKšœŸœŸœ Ÿœ5¡3˜Kšœx˜xšŸœ Ÿœ˜KšœA˜AKšœ3˜3KšœŸœ@˜[K˜—K˜—K˜K˜—šžœŸœ3ŸœŸœ Ÿœ ŸœŸœŸœ˜¾Kšœ™Kšœ˜KšœŸœ˜Kšœ Ÿœ˜'KšœŸœ˜Kšœ˜Kšœ¡˜)Kšœ˜šŸœŸœŸ˜"šŸœ Ÿœ˜Kšœ¡'˜BKšœ;˜;šŸœŸœ˜Kšœ˜Kšœ˜Kšœ˜K˜—K˜—KšŸœ˜—Kšœ#˜#KšŸœŸœ Ÿœ˜,K˜K˜—Kšœ;˜;šžœŸœŸœ3ŸœŸœŸœŸœ Ÿœ ŸœŸœŸœ˜ÐKšœ]™]Kšœ˜KšœŸœ˜Kšœ Ÿœ˜'KšœŸœ˜Kšœ˜Kšœ¡˜)Kšœ˜šŸœŸœŸ˜!šŸœ Ÿœ˜šŸœŸ˜Kšœ:˜:Kšœ;˜;Kšœ;˜;Kšœ:˜:Kšœ:˜:Kšœ;˜;Kšœ:˜:KšŸœŸœ˜—Kšœ?˜?Kšœ;˜;šŸœŸœ˜Kšœ˜Kšœ˜Kšœ˜K˜—K˜—KšŸœ˜—Kšœ#˜#KšŸœŸœ Ÿœ˜,K˜K˜—šœŸœŸœŸœ˜9K˜OK˜KK˜RK˜—šœŸœŸœŸœ˜7K˜5K˜KšœŸœ ˜(Kšœ˜Kšœ˜K˜—šžœŸœŸœ˜GKšœŸœ ˜(KšŸœ˜Kšœ˜—K˜K™%Kšœ Ÿœ˜Kšœ Ÿœ˜Kš œŸœ¡ œŸœŸœŸœ¡˜RKš œ Ÿœ¡ œŸœŸœŸœ¡˜YKšœŸœŸœ ˜šœ ŸœŸœ˜Kšœ¡M˜`šœ4¡ œ˜@Kš¢1™1—KšœŸœ¡6˜LKšœ Ÿœ¡ ˜1Kšœ¡˜-Kšœ0¡˜LKšœ-˜-Kšœ¡9˜NK˜—Kšœ ŸœŸœ ˜šœ ŸœŸœ˜Kšœ¡%˜;Kšœ¡#˜7Kšœ ŸœŸœ¡/˜EK˜—Kšœ ŸœŸœ˜#šœŸœŸœ™Kšœ¡™7Kšœ¡S™jK™—šœŸœŸœ˜Kšœ¡˜7KšœŸœ¡˜+K˜K˜—K˜K™šžœŸœŸœŸœ˜AKšŸœ ˜ šœŸœ˜˜ Kš¢ ™ —Kšœ˜K˜šœ ˜ Kš¢™—Kšœ˜K˜K˜/˜1Kš¢ ™ —šœ˜Kš¢™—K˜K˜˜Kš¢™—Kšœ˜K˜K˜K˜K˜K˜˜Kš¢™—K˜)K˜1K˜˜Kš¢ ™ —Kšœ˜KšœŸœ˜Kšœ!˜!K˜'K˜+˜/Kš¢™—K˜!K˜!K˜!K˜!K˜K˜K˜—K˜K˜—šžœŸœŸœŸœEŸœŸœŸœ˜¯Kšœ˜Kšœ ŸœŸœ˜K˜KšœM˜MKšŸœŸœ ŸœŸœ˜K˜Kšœa˜a˜K˜——K™šžœŸœŸœ.ŸœŸœŸœEŸœŸœŸœ˜ôKš¢H™HKšœ˜Kšœ˜˜Kš¢,™,—Kšœ:˜:Jšœ)˜)JšœGŸœ˜LK˜šŸœ¡˜!šž œŸœ˜-Kšœ)˜)Kšœ˜—Kšœ ˜ šœJ˜JKšœ,˜,KšŸœ˜Kšœ˜—Kšœ,˜,—KšŸœ˜Kšœ\˜\K˜K˜—š žœŸœŸœjŸœŸœ˜®Kš¢à™àKšœ˜Kšœ˜Kšœ-˜-KšœŸœ˜Kšœ˜˜Kš¢,™,—Kšœ-˜-Kšœ#˜#Kšœ!˜!K˜Kš œ Ÿœ ŸœŸœŸœ ˜HKšœ1˜1K–*[m: ImagerTransformation.Transformation]šœ6˜6Kšœ/˜/K˜Kšœ Ÿœ2ŸœŸœY˜¦K˜šœŸœ ˜K˜$Kšœ ˜ KšœŸœ˜ KšœŸœŸœŸœ˜&Kšœ-¡˜BKšœ˜—Kšœ˜K˜—K˜š žœŸœŸœpŸœŸœ˜ÍKšœ˜Kšœ-˜-KšœŸœ˜K˜Kš œ Ÿœ ŸœŸœŸœ ˜HKšœ1˜1K–*[m: ImagerTransformation.Transformation]šœ6˜6K˜Kšœ Ÿœ¨˜´šœŸœ ˜K˜$Kšœ ˜ KšœŸœ˜ KšœŸœŸœŸœ˜&Kšœ-¡˜BKšœ˜—Kšœ˜K˜K˜—K™šžœŸ œ Ÿœ˜GKšœŸœ ˜$Kšœ'˜'K˜K˜—šžœŸ œŸœŸœ˜PKšœŸœ ˜$Kšœ'˜'K˜—K™K™K™Kšœ ™ šžœŸœ˜)KšœŸœ ˜$KšœS˜SKšœ:˜:Kšœ*˜*K˜K˜—šž œŸœ#Ÿœ˜MK™KšŸœ˜K˜K˜—šž œŸœ#Ÿœ˜MK™KšœŸœ ˜$KšŸœ˜K˜K™—šžœŸœŸœŸœ˜;Kšœ™KšœŸœ ˜$KšœO˜OKšœ/Ÿœ5˜gK˜K˜—Kšœ™šž œŸœcŸœ˜}Kšœ™šž œŸœ˜Kšœ%˜%Kšœ#¡7˜ZKšœ$˜$Kšœ˜—KšœŸœ ˜$KšœŸœ˜!KšŸœ ŸœŸœŸœ%˜MK˜K˜—šžœŸœ˜Ÿœ˜¾K™+K™šžœŸœ˜Kšœ:˜:Kšœ ˜ KšœŸœ˜$KšœŸœŸœ˜Kšœ?˜?Kšœ?˜?Kšœ?˜?Kšœ?˜?šŸœŸœŸœŸ˜)KšœŸœŸœ˜:Kšœ!ŸœŸœ˜EKšŸœ Ÿœ1˜DKšŸœŸœ4˜LKš ŸœŸœ ŸœŸœŸœ!˜QKšŸœ˜—K–>[context: Imager.Context, strokeJoint: Imager.StrokeJoint]šœ!˜!K˜Kšœ"˜"Kš ŸœŸœŸœŸœŸœ™+šŸœŸœŸœ˜šŸœŸœŸœŸ˜'šŸœŸœŸœŸ˜2Kšœ,˜,Kšœ,˜,Kšœ,˜,Kšœ,˜,KšŸœŸœ˜——KšŸœ˜K˜—K˜—K˜#KšœŸœ ˜$KšŸœŸœŸœŸœ˜/Kš ŸœŸœŸœ ŸœŸœŸœ˜6KšœŸœ˜&Kšœ Ÿœ ˜KšŸœŸœ&˜DK˜K˜—šž  œŸœ˜¤Kšœ#™#šž  œŸœ˜KšœM˜MKšœ#¡7˜ZKšœ$˜$K˜—KšœŸœ ˜$Kšœ¢ œ˜(K˜K˜—K™Kšœ ™ K™šž œŸœV˜gK™KšœŸœ ˜$KšœL˜LKšœ?˜?KšœD˜DKšœ˜K˜K˜—Kšœ™K™šž œŸœ#Ÿœ Ÿœ˜PK™Kšœ Ÿœ˜KšœŸœ˜!KšœŸœ˜šŸœŸœŸœŸ˜%šŸœŸœ˜Kšœ˜Kšœ˜K˜—KšŸœ˜—šŸœŸœŸœŸ˜$šŸœŸœ˜Kšœ˜Kšœ˜K˜—KšŸœ˜—KšŸœŸœŸœ"˜@KšŸœ Ÿœ ŸœŸœ˜@Kšœ.˜.K˜K˜—šž œŸœŸœŸœ˜0K™K™.šž œŸœ˜)Kšœ#¡7˜ZKšœ$˜$K˜—KšœŸœ ˜$Kšœ˜KšœŸœŸœ˜Kšœ Ÿœ˜KšœŸœ˜K˜Kšœ'˜'Kšœ4˜4Kšœ˜Kšœ/˜/Kšœ ŸœŸœ˜K˜šŸœŸœ˜Kš¢!£¢™&KšœŸœŸœ˜KšœQ˜QKšœ+˜+Kšœ˜Kšœ(˜(K–[base: ROPE]šœ%˜%˜Kš¢™—Kšœ&˜&Kšœ˜K˜—K˜K˜—šžœŸœŸœŸœ ŸœŸœŸœ˜_K™KšœŸœ˜"KšœŸœ˜Kšœ ŸœŸœ˜K˜/Kšœ ŸœŸœ˜KšœŸœ˜KšœŸœŸœ˜KšœŸœ˜K˜Kšœ˜K˜Kšœ,˜,šœ˜Kšœ Ÿœ˜Kšœ1˜1K˜Kšœ4˜4KšŸœŸœ ŸœŸœ˜K˜šŸœŸœ˜Kš¢-™-K˜*K˜K˜1Kšœ Ÿœ(˜7KšŸœŸœŸœ˜&KšœŸœŸœ ˜"K˜Kšœ/Ÿœ˜4˜Kš¢"™"—KšœTŸœ˜ZK˜—šŸœ˜Kšœ;Ÿœ˜BK˜—K˜—šŸœ˜Kšœ;Ÿœ˜BK˜—K˜K™—šžœŸœŸœŸœŸœŸœ ŸœŸœ˜KšœŸœ˜Kš ŸœŸœŸœŸœŸœŸœ˜KšœŸœŸœ ˜7KšœP˜PKšœ˜Kšœ Ÿœ˜KšŸœ˜ Kšœ˜—KšŸœ ŸœŸœ*¡˜`K˜K˜—K™Kšœ ™ K™šž œŸœ#ŸœŸœ˜GKšœ ™ KšŸœ˜K˜—K˜š ž œŸœŸœŸœŸœ˜cKšœ™KšœŸœ ˜'Kš œŸœŸœŸœ ŸœŸœ˜OšŸœŸ˜šœ Ÿ˜ šŸœŸœ˜Kšœ"Ÿœ˜'K˜—šŸœ˜Kšœ ŸœŸœ˜Kšœ˜Kšœ Ÿœ˜K˜K˜KšœŸœ ˜+Kšœ Ÿœ+˜7Kšœ¡œc˜nKšŸœŸœ ŸœŸœ˜KšœŸœ ˜KšŸœŸœŸœ˜&Kšœ%Ÿœ˜*K˜—KšœŸœ˜K˜—šœ ŸœŸœ˜%Kšœ Ÿœ˜%KšœŸœ˜K˜—šœ˜KšœŸœŸœ˜KšœŸœŸœ˜K˜—Kšœ/˜/šœ ¡)˜3KšŸœŸœ#Ÿ˜AKšŸœŸœŸœ!Ÿœ˜DK˜—KšŸœŸœ˜—Kšœ¡˜#K˜K˜—šž œŸœ8Ÿœ˜jKšœ ™ KšœŸœ ˜#KšœŸœ ˜#Kš œŸœŸœŸœ ŸœŸœ˜PšŸœŸœŸœŸ˜%Kšœ(Ÿœ˜>KšŸœ˜—šŸœŸœŸœŸ˜$Kšœ&Ÿœ˜;KšŸœ˜—Kšœ$Ÿœ˜8Kšœ¡˜&K˜K˜—šž œŸœ8Ÿœ˜jKšœŸœ ˜#KšœŸœ ˜#Kš œŸœŸœŸœ ŸœŸœ˜PKšŸœŸœŸœ-Ÿœ˜HšŸœŸœŸœŸ˜%Kš œŸœŸœŸœŸœ˜NKšŸœ˜—šŸœŸœŸœŸ˜$Kš œŸœŸœŸœŸœ˜KKšŸœ˜—Kš œŸœŸœŸœŸœ˜HKšœ¡˜'K˜K˜—šž œŸœ#Ÿœ˜VKšœ!™!K™+Kš œŸœŸœŸœ ŸœŸœ Ÿœ˜]šŸœŸœ¡˜5KšœŸœŸœ˜KšœŸœŸœ˜KšœŸœ˜Kšœ˜—Kšœ¡˜&Kšœ˜K˜—šž œŸœ1ŸœŸœŸœŸœŸœ˜sKšœ ™ K™9Kšœ1˜1KšœŸœ ˜$Kšœ˜Kšœ-˜-Kšœ˜K˜—šžœŸœ@Ÿœ˜rKšœ"™"KšœŸœ˜!KšœŸœ?˜VK™,šŸœŸœŸœŸ˜$šŸœŸœŸœŸ˜&KšœŸœŸœ˜=KšœŸœŸœ˜=KšœŸœŸœ˜=KšœŸœŸœ˜=—KšŸœŸœ˜KšŸœ˜—Kšœ˜K˜—K˜šžœŸœŸœ˜[KšœŸœ˜&Kšœ Ÿœ$Ÿœ˜9šŸœŸœŸœŸ˜)KšŸœŸœ#˜>KšŸœ˜—K˜K˜—šžœŸœŸœ'˜gKšœŸœ˜&KšœŸœ(Ÿœ¡˜\šŸœŸœŸœŸ˜'KšŸœŸœ+˜DKšŸœ˜—K˜K˜—šž œŸœŸœ.˜bšŸœ ŸœŸœŸœ˜+KšœŸœ˜KšŸœ˜K˜—šŸœ˜Kšœ*˜*KšœŸœ˜+KšœŸœ˜(KšœŸœ˜K˜ KšœŸœ˜šŸœ!ŸœŸ˜@KšŸœŸœŸœ¡+˜OKšŸœ˜—šœFŸœŸ˜WK˜K˜K˜K˜KšŸœ¡™*KšŸœŸ˜Kšœ˜—Kšœ ˜ KšœŸœŸœŸœ ¡4˜nK˜—K˜K˜—šžœŸœ$Ÿœ6˜všŸœŸœŸœŸœ˜3KšœŸœ˜KšŸœ˜K˜—šŸœ˜Kšœ.˜.KšœŸœ˜+KšœŸœ˜(K˜ Kšœ;˜;KšœŸœ˜KšœŸœ˜šŸœ%ŸœŸ˜CKšŸœŸœŸœ¡)˜LKšŸœ˜—šŸœŸœ¡2˜Dšœ˜KšœM˜MKšœM˜MK˜—šœ˜KšœM˜MKšœM˜MK˜—šœ˜KšœM˜MKšœM˜MK˜—šœ˜KšœM˜MKšœM˜MK˜—KšŸœŸœ*˜;—Kšœ*˜*KšœŸœŸœŸœ ¡3˜uK˜—K˜K˜—šžœŸœ8ŸœŸœŸœ ŸœŸœ ŸœŸœ˜©K™"Kšœ Ÿœ¡!™1KšœŸœ˜Kšœ˜KšœŸœ ˜$Kšœ¦ŸœŸœ¡˜ÍšŸœ'Ÿœ˜/Kšœ˜KšœŸœ˜(KšœI˜IKšœŸœŸœ Ÿœ1¡3˜ŒKšœ˜šŸœ Ÿœ˜Kšœ?˜?Kšœ3˜3KšœŸœ@˜YK˜—K˜—K˜K˜—šžœŸœ8ŸœŸœŸœ ŸœŸœ ŸœŸœ˜«Kšœ$™$Kšœ Ÿœ¡!™1Kšœ˜KšœŸœ ˜$Kšœ¦ŸœŸœ¡˜ÍšŸœ'Ÿœ˜/Kšœ˜KšœŸœ˜KšœŸœ ˜$KšœŸœ˜(KšœJ¡ ˜jKšœŸœŸœ Ÿœ1¡3˜ŒKšœƒ˜ƒšŸœ Ÿœ˜Kšœ?˜?Kšœ3˜3KšœŸœ@˜YK˜—K˜—K˜K˜—šœŸœŸœŸœ˜6K˜V—šœ ŸœŸœŸœ˜3K˜6—K˜Kšœ™K™KšžœŸœ=˜SKšžœŸœ#ŸœŸœ˜bKšžœŸœ*˜>KšžœŸœŸœŸœ˜MKšžœŸœ0Ÿœ˜QšžœŸœ#ŸœŸœ ˜`K˜—Kšœ Ÿœ˜KšœŸœ˜K˜šžœŸœŸœ˜Kšœ1˜1Kšœ2˜2K˜—K˜K˜K˜KšŸœ˜—…—ò*SÒ