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, 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, 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, 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, fontPrefix: Rope.ROPE, -- e.g. "xerox/xc1-2-2/" or "xerox/pressfonts/" fontFamily: Rope.ROPE, -- e.g. "Helvetica" or "Tioga" or "Gacha" fontFace: Rope.ROPE, -- e.g. "-bi" fontName: Rope.ROPE, -- exact stiring to give to ImagerFont.Find scale: REAL _ 0.0, -- preferred size in points for screen fonts preferredSize: REAL _ 0.0, -- preferred size in points for screen fonts trueFont: ImagerFont.Font _ NIL, screenFont: ImagerFont.Font _ NIL, amplifySpace: REAL, -- because the Imager allows space correction (see Imager.mesa) 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, color: Imager.Color _ Imager.black, amplifySpace: REAL, dropShadowsOn: BOOL _ FALSE, dropShadowOffset: Vector _ [0.0, 0.0], shadowColor: Imager.Color _ Imager.black] RETURNS [slice: Slice] = { textData: TextData _ NEW[TextDataObj _ [ box: GGBoundBox.NullBoundBox[], -- filled in by call to TextSetBoundBox feedbackBox: GGBoundBox.NullBoundBox[], -- filled in by call to TextSetBoundBox inverseScale: [-1.0, -1.0], -- dummy vector amplifySpace: amplifySpace, points: NEW[TextPointsData], color: color, rope: text, 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 ]]; }; 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]; }; TFontParamsFromFontName: PROC [fontName: Rope.ROPE] RETURNS [fontFamily: Rope.ROPE, fontFace: Rope.ROPE, 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 }; }; face: Rope.ROPE; fontStream: IO.STREAM _ IO.RIS[fontName]; [fontFamily, ----] _ 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 _ NIL; CONTINUE; };]; IF face#NIL THEN fontFace _ Rope.Concat["-", face]; }; SetTextFont: PUBLIC PROC [slice: Slice, prefix: Rope.ROPE, family: Rope.ROPE, face: Rope.ROPE, fontName: Rope.ROPE, scale: REAL _ 1.0, fontScale: REAL _ 1.0, transform: ImagerTransformation.Transformation, feedback: ViewerClasses.Viewer] RETURNS [ success: BOOL _ TRUE] = { textData: TextData _ NARROW[slice.data]; IF slice.class.type#$Text THEN RETURN; IF prefix=NIL OR family=NIL OR scale<=0.0 OR fontScale<=0.0 THEN GOTO Abort; IF transform=NIL THEN transform _ ImagerTransformation.PreScale[m: ImagerTransformation.Translate[ImagerTransformation.Factor[m: textData.transform].t], s: scale]; textData.trueFont _ ImagerFont.Scale[ImagerFont.Find[fontName ! Imager.Error => GOTO Abort;], 1.0/fontScale ]; textData.screenFont _ textData.trueFont; -- for now textData.fontPrefix _ prefix; textData.fontFamily _ family; textData.fontFace _ face; textData.fontName _ fontName; textData.scale _ scale; textData.preferredSize _ fontScale; textData.transform _ transform; textData.inverse _ ImagerTransformation.Invert[transform]; textData.inverseScale _ ImagerTransformation.Factor[textData.inverse].s; TextSetBoundBox[slice]; EXITS Abort => { scratch: IO.STREAM _ IO.ROS[]; IF transform#NIL THEN GGParseOut.WriteFactoredTransformation[scratch, transform]; GGError.PutF[feedback, oneLiner, "No Such Font: %g%g%g scale: %g transform: %g", [rope[prefix]], [rope[family]], [rope[face]], [real[scale]], [rope[IO.RopeFromROS[scratch]]] ]; success _ FALSE; }; }; GetFontValues: PUBLIC PROC [slice: Slice] RETURNS [prefix, family, face: Rope.ROPE, transform: ImagerTransformation.Transformation, preferredSize: REAL] = { problem: Rope.ROPE; textData: TextData; IF slice.class.type#$Text THEN ERROR; textData _ NARROW[slice.data]; [prefix, family, face, ----, preferredSize, problem] _ GGUtility.UserDataFromFontData[textData.fontName, textData.scale, textData.preferredSize]; RETURN[prefix, family, face, textData.transform, preferredSize]; }; GetFontValuesRope: PUBLIC PROC [slice: Slice] RETURNS [values: Rope.ROPE] = { prefix, family, face, problem: Rope.ROPE; preferredSize: REAL _ -1.0; textData: TextData; scratch: IO.STREAM _ IO.ROS[]; IF slice.class.type#$Text THEN RETURN[NIL]; textData _ NARROW[slice.data]; [prefix, family, face, ----, preferredSize, problem] _ GGUtility.UserDataFromFontData[textData.fontName, textData.scale, textData.preferredSize]; IO.PutChar[self: scratch, char: ' ]; GGParseOut.WriteFactoredTransformation[scratch, textData.transform]; IO.PutChar[self: scratch, char: ' ]; RETURN[IO.PutFR["%g %g%g %g %g", [rope[prefix]], [rope[family]], [rope[face]], [rope[IO.RopeFromROS[scratch]]], [real[preferredSize]] ] ]; }; 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]; textData.box _ GGBoundBox.BoundBoxOfBoundBox[textData.box, textData.transform]; GGBoundBox.EnlargeByOffset[textData.box, halfCP]; IF textData.dropShadowOn THEN GGBoundBox.EnlargeByVector[textData.box, textData.dropShadowOffset]; slice.boundBox _ GGBoundBox.CopyBoundBox[textData.box]; left _ 0.0; right _ RopeEscapement[textData.trueFont, textData.rope, textData.amplifySpace].x; middle _ (left + right)/2.0; fontExtents _ ImagerFont.RopeBoundingBox[textData.trueFont, "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.color, textData.amplifySpace, textData.dropShadowOn, textData.dropShadowOffset, textData.shadowColor]; IF NOT SetTextFont[copy, textData.fontPrefix, textData.fontFamily, textData.fontFace, textData.fontName, textData.scale, textData.preferredSize, ImagerTransformation.Copy[textData.transform], NIL] THEN ERROR; }; 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.trueFont]; 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.trueFont]; 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.SetAmplifySpace[dc, textData.amplifySpace]; -- adjust spacing (for right justified text) 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%g ", [rope[textData.rope]], [rope[textData.fontPrefix]], [rope[textData.fontFamily]], [rope[textData.fontFace]] ]; GGParseOut.WriteTransformation[f, textData.transform]; f.PutF[" %g ", [real[textData.preferredSize]] ]; 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.PutF[" %g", [real[textData.amplifySpace]]]; }; TextFilein: PROC [f: IO.STREAM, version: REAL, feedback: Viewer] RETURNS [slice: Slice] = { rope, fontName: Rope.ROPE; prefix: Rope.ROPE _ "xerox/pressfonts/"; family: Rope.ROPE _ "Helvetica"; face: Rope.ROPE _ ""; size: REAL _ 1.0; preferredSize: REAL _ 1.0; transform: ImagerTransformation.Transformation; shadowColor: Imager.Color _ Imager.black; textColor: Imager.Color _ Imager.black; dropShadowOn, good: BOOL _ FALSE; dropShadowOffset: Vector _ [0.0, 0.0]; amplifySpace: REAL _ 1.0; IF version > 8601.22 THEN { rope _ f.GetRopeLiteral[]; } ELSE { GGParseIn.ReadBlankAndRope[f, "("]; rope _ GGParseIn.ReadBlankAndWord[f]; GGParseIn.ReadBlankAndRope[f, ")"]; }; IF version >= 8701.30 THEN { prefix _ GGParseIn.ReadBlankAndWord[f]; }; IF version <= 8601.06 THEN { }-- use default family and face ELSE IF version <= 8601.27 THEN { -- a simple name like "Helvetica" or "Gacha" family _ GGParseIn.ReadBlankAndWord[f]; } ELSE IF version <= 8605.12 THEN { -- a complex name like "xerox/pressfonts/Helvetica-BIR" fontName _ GGParseIn.ReadBlankAndWord[f]; size _ GGParseIn.ReadBlankAndReal[f]; [] _ GGParseIn.ReadBlankAndWord[f]; fontName _ UnpackComplexFontName[fontName, size]; -- like Helvetica12BI [family, face, ----] _ TFontParamsFromFontName[fontName]; } ELSE IF version <= 8605.28 THEN { -- a mixed mode name like Helvetica7BI or TimesRoman12 fontName _ GGParseIn.ReadBlankAndWord[f]; size _ GGParseIn.ReadBlankAndReal[f]; [] _ GGParseIn.ReadBlankAndWord[f]; [family, face, ----] _ TFontParamsFromFontName[fontName]; } ELSE IF version < 8701.30 THEN { -- a mixed mode name like Helvetica7BI or TimesRoman12. No colorName, real text color fontName _ GGParseIn.ReadBlankAndWord[f]; size _ GGParseIn.ReadBlankAndReal[f]; [family, face, ----] _ TFontParamsFromFontName[fontName]; } ELSE { fail: BOOL _ FALSE; [fail, ----, family, face] _ GGUtility.ParseFontData[f, FALSE, TRUE, TRUE]; IF fail THEN ERROR; transform _ GGParseIn.ReadTransformation[f]; preferredSize _ GGParseIn.ReadBlankAndReal[f]; }; IF version > 8605.28 THEN { textColor _ GGParseIn.ReadColor[f, version]; }; IF version >= 8609.26 THEN { [dropShadowOn, good] _ GGParseIn.ReadBOOL[f, version]; IF NOT good THEN ERROR; IF dropShadowOn THEN { dropShadowOffset _ GGParseIn.ReadPoint[f]; shadowColor _ GGParseIn.ReadColor[f, version]; }; } ELSE dropShadowOn _ FALSE; IF version <= 8607.22 THEN { -- a point rather than a transform point: Point _ GGParseIn.ReadPoint[f]; transform _ ImagerTransformation.Translate[point]; transform _ ImagerTransformation.PreScale[transform, size]; size _ 1.0; } ELSE IF version < 8701.30 THEN { transform _ GGParseIn.ReadTransformation[f]; transform _ ImagerTransformation.PreScale[transform, size]; size _ 1.0; }; -- transform already read in for later versions IF version >= 8701.13 THEN { amplifySpace _ GGParseIn.ReadBlankAndReal[f]; } ELSE amplifySpace _ 1.0; slice _ MakeTextSlice[text: rope, color: textColor, amplifySpace: amplifySpace, dropShadowsOn: dropShadowOn, dropShadowOffset: dropShadowOffset, shadowColor: shadowColor]; IF NOT SetTextFont[slice: slice, prefix: prefix, family: family, face: face, fontName: GGUtility.FontDataFromUserData[prefix, family, face, size, preferredSize].fontName, scale: size, fontScale: preferredSize, transform: transform, feedback: feedback] THEN ERROR; }; 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: PROC [fullName: Rope.ROPE, feedback: Viewer, transform: ImagerTransformation.Transformation _ NIL, localBox: BoundBox, 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, localBox, includeByValue]; }; MakeIPSliceFromMaster: PUBLIC PROC [ipMaster: Interpress.Master, pixelsPerUnit: REAL _ 2834.646, fullName: Rope.ROPE _ NIL, feedback: Viewer, transform: ImagerTransformation.Transformation _ NIL, localBox: BoundBox _ NIL, includeByValue: BOOL] RETURNS [slice: Slice] = { memContext: Imager.Context; 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]; }; IF localBox = NIL THEN { 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.PutChar[IO.SP]; GGParseOut.WriteBox[f, ipData.tightBox]; 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; localBox: BoundBox; fullName _ f.GetRopeLiteral[]; [fullName] _ FS.FileInfo[fullName]; -- add the version number transform _ GGParseIn.ReadTransformation[f]; IF version >= 8701.28 THEN { localBox _ GGParseIn.ReadBox[f]; } ELSE localBox _ NIL; IF version >= 8612.04 THEN { goodValue: BOOL; GGParseIn.ReadBlankAndRope[f, "includeByValue:"]; [includeByValue, goodValue] _ GGParseIn.ReadBOOL[f, version]; 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 _ MakeIPSliceFromMaster[ipMaster, 1.0, fullName, feedback, transform, localBox, TRUE]; } ELSE { slice _ MakeIPSliceFromFile[fullName, feedback, transform, localBox, FALSE]; }; } ELSE { slice _ MakeIPSliceFromFile[fullName, feedback, transform, localBox, 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 February 3, 1987 12:59:53 pm PST Last edited by Bier on January 30, 1987 6:17:40 pm 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 must be filled in by client call to 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! The text slice's font will be set to the desired prefix, family, and face (face may be NIL to denote the "regular" face). If transform=NIL, the existing slice transformation will be factored and the translation component will be retained; i.e. the SetTextFont will have the effect of changing the font and font size but retaining the origin of the text. Previous rotation/scaling will be lost. The new text transformation will use the old translation component and the new value of scale. The font will be itself scaled by the value fontScale. If transform#NIL, the old text transformation is replaced by the new transform, scale is ignored, and fontScale is simply stored in the textData. 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. Get the text Get the prefix Get the family and face and size get color 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 ÊN¿˜codešœ™Kšœ<™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˜—š œžœžœ žœ4žœžœžœSžœ˜íšœžœ˜(Kšœ Ÿ'˜GKšœ(Ÿ'˜OKšœŸ˜,Kšœ˜Kšœžœ˜Kšœ ˜ Kšœ ˜ Kšœ˜Kšœ#˜#Kšœ˜Kšœ˜Kš I™I—šœžœ ˜K˜&Kšœ˜Kšœžœ˜ Kšœžœžœžœ˜&Kšœ.Ÿ˜EKšœ ž˜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–[pName: ROPE]š œ žœžœžœžœ ˜)Kš œ Ÿœžœ:žœžœ˜yšœ"žœžœ˜JKš  Ðbk =™J—Kš œŸœžœ-žœ žœžœžœ˜zKšžœžœžœ#˜3K˜K˜—š œžœžœžœžœ žœžœ žœžœXžœ žœžœ˜‘KšœWžœ.žœ§žœ™¶K™Kšœžœ ˜(Kšžœžœžœ˜&Kšžœžœžœžœžœ žœžœžœ˜LKšžœ žœžœŽ˜£Kš œ  œ  œžœ˜nK–p[prefix: ATOM, family: ATOM, face: NodeStyle.FontFace, size: REAL, alphabets: NodeStyle.FontAlphabets]š )Ðbc ˜3Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ#˜#Kšœ˜Kšœ:˜:KšœH˜HKšœ˜šž˜šœ ˜ K–“[fileName: ROPE, accessOptions: FS.AccessOptions _ read, streamOptions: FS.StreamOptions _ (5)[TRUE, TRUE, TRUE, TRUE, TRUE], keep: CARDINAL _ 1B (1), createByteCount: FS.ByteCount _ 2560, streamBufferParms: FS.StreamBufferParms _ [vmPagesPerBuffer: 8, nBuffers: 2], extendFileProc: FS.ExtendFileProc, wantedCreatedTime: GMT _ nullGMT, remoteCheck: BOOL _ TRUE, wDir: ROPE _ NIL]š œ žœžœžœ¡œ˜Kšžœ žœžœ<˜QKšœ”žœ œ˜°Kšœ žœ˜K˜——K˜K˜—š œžœžœžœžœAžœ˜œKšœžœ˜Kšœ˜Kšžœžœžœ˜%Kšœ žœ ˜K–[format: ROPE _ NIL, v1: IO.Value _ [null[]], v2: IO.Value _ [null[]], v3: IO.Value _ [null[]], v4: IO.Value _ [null[]], v5: IO.Value _ [null[]]]šœAœ<˜‘Kšžœ:˜@K˜K˜—š œžœžœžœžœ˜MKšœ$žœ˜)Kšœžœ˜Kšœ˜K–ldStream: STREAM _ NIL]š œ žœžœžœžœ˜Kšžœžœžœžœ˜+Kšœ žœ ˜K–[format: ROPE _ NIL, v1: IO.Value _ [null[]], v2: IO.Value _ [null[]], v3: IO.Value _ [null[]], v4: IO.Value _ [null[]], v5: IO.Value _ [null[]]]šœAœ<˜‘K–[self: STREAM, char: CHAR]˜$KšœD˜DK–[self: STREAM, char: CHAR]˜$K–[format: ROPE _ NIL, v1: IO.Value _ [null[]], v2: IO.Value _ [null[]], v3: IO.Value _ [null[]], v4: IO.Value _ [null[]], v5: IO.Value _ [null[]]]šžœ„˜Š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šœžœžœžœ˜bKš œžœžœžœžœ˜QKšœŸ)˜GKšœ3Ÿ,˜_Kšœ#˜#K˜K˜—šœžœA˜UK™Cšœžœ˜Kšœ1˜1Kšœžœžœžœ˜bKš œžœžœžœžœ˜]KšœŸ)˜GKšœ3Ÿ,˜_Kšœ#˜#K˜—Kšœ'˜'K˜K˜—šÐbn œžœ˜¦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šœ‡˜‡Kšœ6˜6Kšœ0˜0K˜)Kšœ žœžœ˜šžœžœ˜Kšœžœ˜Kšœ žœžœ˜Kšœ4˜4Kšœ žœžœ˜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˜Kš ™—šžœžœ˜Kšœ'˜'K˜Kš  ™ —KšžœžœŸ˜<šžœžœžœŸ,˜NKšœ'˜'K˜—šžœžœžœŸ7˜YK˜)K˜%K˜#Kšœ2Ÿ˜GKšœŸœ&˜9K˜—šžœžœžœŸ6˜XK˜)K˜%K˜#KšœŸœ&˜9Kšœ˜—šžœžœžœŸU˜vK˜)K˜%KšœŸœ&˜9Kšœ˜—šžœ˜Kšœžœžœ˜Kš œŸœ-žœžœžœ˜KKšžœžœžœ˜Kšœ,˜,K˜.K˜Kš  ™ —šžœžœ˜Kšœ,˜,K˜K– [f: STREAM]š ™—šžœžœ˜Kšœ6˜6Kšžœžœžœžœ˜šžœžœ˜Kšœ*˜*Kšœ.˜.K˜—K˜—šžœžœ˜K˜—šžœžœŸ"˜?Kšœ&˜&Kšœ2˜2Kšœ;˜;K˜ K˜—šžœžœžœ˜ Kšœ,˜,Kšœ;˜;K˜ KšœŸ/˜2—K˜šžœž˜Kšœ-˜-K˜—Kšžœ˜K˜Kšœ«˜«K–ß[slice: GGModelTypes.Slice, prefix: ROPE, family: ROPE, face: ROPE, faceRope: ROPE, scale: REAL _ 1.0, fontScale: REAL _ 1.0, transform: ImagerTransformation.Transformation, feedback: ViewerClasses.Viewer]šžœžœöžœžœ˜‡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šœM˜MKšžœžœ žœžœ˜Kšœk˜k˜K˜——K™šœžœžœ.žœžœžœEžœžœžœžœ˜ŽKš H™HKšœ˜˜Kš ,™,—Kšœ:˜:Jšœ)˜)JšœGžœ˜LK˜šžœŸ˜!š œžœ˜-Kšœ)˜)Kšœ˜—šžœ žœžœ˜šœQ˜QKšœ,˜,Kšžœ˜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š !¡ ™&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šœ ˜ K˜—Kšžœ žœ˜šžœžœ˜Kšœ žœ˜Kšœ1˜1Kšœ=˜=Kšžœžœ žœžœ˜K˜šžœžœ˜Kš -™-K˜*K˜K˜1Kšœ žœ(˜7Kšžœžœžœ˜&Kšœžœžœ ˜"K˜Kšœ/žœ˜4˜Kš "™"—KšœVžœ˜\K˜—šžœ˜KšœEžœ˜LK˜—K˜—šžœ˜KšœEžœ˜LK˜—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