Главная »Статьи »AutoCAD и Delphi » |
Данная статья продолжает изложение, начатое в статье SHX-файлы: Чтение, расширяя созданные там классы. Для отрисовки используются описания кодов форм, описанные в справке AutoCAD в разделе Create Shape Definition Files (Создание определений формы). В класс TShxShape вводим поля: текущих координат, состояние пера и масштаба, а также координат охватывающего прямоугольника (необязательно) и флага обработки вертикального текста (отрисовка вертикального текста не реализована): FX, FY: Double; FPenDown: Boolean; FGlyphScale: Double; FXmin, FXmax, FYmin, FYmax: Double; FVert: Boolean; Все примитивы глифа преобразовываем в отрезки, тогда метод непосредственно отрисовки выгдялит просто: procedure TShxShape.DrawLine(ACanvas: TCanvas); var iX, iY: Integer; begin // Отрегулировать границы охватывающего прямоугольника AdjustBounds(FX, FY); // Отрисовать if ACanvas = nil then Exit; iX:= Round(FX); iY:= Round(FY); if FPenDown then begin ACanvas.LineTo(iX, iY); ACanvas.Pixels[iX, iY]:= ACanvas.Pen.Color; end else ACanvas.MoveTo(iX, iY); end; Метод отрисовки глифа: procedure TShxShape.DrawToCanvas(ACanvas: TCanvas; var AX, AY: Double; AScale: Double); var i: Word; begin FVert:= False; FGlyphScale:= AScale; FXmin:= MaxDouble; FXmax:= -MaxDouble; FYmin:= MaxDouble; FYmax:= -MaxDouble; FX:= AX; FY:= AY; // Вначале переместить курсор в точку (FX, FY) FPenDown:= False; DrawLine(ACanvas); FPenDown:= True; // Отрисовать глиф FGlyphHighIndex:= High(FGlyph); i:= 0; while i < FGlyphHighIndex do begin SpecialCodeHandler(ACanvas, i); end; if (i > FGlyphHighIndex) or ((i = FGlyphHighIndex) and (FGlyph[FGlyphHighIndex] <> GLYPH_SHAPE_END)) then raise Exception.CreateFmt(rsUnexpectedGlyphEnd, [i, FGlyphHighIndex]); // Если символ = LF, то обнулить коорд. X if FNumber = LF then FX:= 0.0 - FY; // Вернуть координаты по завершении глифа AX:= FX; AY:= FY; // Если высота охват. прямоугольника = 0, то установить ее в соотв. с высотой шрифта if IsZero(FYmax - FYmin) then FYmin:= FFont.Above * FGlyphScale; // Отрегулировать границы охватывающего прямоугольника FFont.AdjustBounds(FXmin, FYmin); FFont.AdjustBounds(FXmax, FYmax); FFont.GetBounds(FXmin, FXmax, FYmin, FYmax); end; Метод принимает координаты, в которых глиф должен быть отрисован и возвращает в этих же переменных текущие координаты после отрисовки. Также метод принимает значение масштаба. Это позволяет отрисовывать глифы, ссылающиеся на другие глифы (спецкод 0x07). Все команды и данные для отрисовки выполняются методом SpecialCodeHandler: procedure TShxShape.SpecialCodeHandler(ACanvas: TCanvas; var Index: Word); var SubShapeNum: Word; OldPenDown: Boolean; begin case FGlyph[Index] of $0: Inc(Index); // конец описания формы $1: begin // включение режима отрисовки (опускание пера) FPenDown:= True; Inc(Index); end; $2: begin // отключение режима отрисовки (поднятие пера) FPenDown:= False; Inc(Index); end; $3: begin // деление длин векторов на следующий байт if (Index + 1) < FGlyphHighIndex then begin if not FVert then FGlyphScale:= FGlyphScale / FGlyph[Index + 1]; end else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 2); if FVert then FVert:= False; end; $4: begin // умножение длин векторов на следующий байт if (Index + 1) < FGlyphHighIndex then begin if not FVert then FGlyphScale:= FGlyphScale * FGlyph[Index + 1]; end else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 2); if FVert then FVert:= False; end; $5: begin // занесение текущей позиции в стек if not FVert then PosStack.Push(TPosition.Create(FX, FY)); Inc(Index); if FVert then FVert:= False; end; $6: begin // восстановление текущей позиции из стека // Проверяем пустоту стека (символ 13 шрифта "bold.shx" содержит // всего одну команду - $6, а символ 1 одну команду - $5) if not FVert and (PosStack.Count > 0) then begin FX:= PosStack.Peek.X; FY:= PosStack.Peek.Y; PosStack.Pop; OldPenDown:= FPenDown; FPenDown:= False; DrawLine(ACanvas); FPenDown:= OldPenDown; end; Inc(Index); if FVert then FVert:= False; end; $7: begin // отрисовка субформы, номер которой определяется следующим байтом (для UNIFONT - двумя) if FFont.FFontKind = fkUniFont then begin if (Index + 2) < FGlyphHighIndex then SubShapeNum:= (FGlyph[Index + 1] shl 8) + FGlyph[Index + 2] else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 3); end else begin if (Index + 1) < FGlyphHighIndex then SubShapeNum:= FGlyph[Index + 1] else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 2); end; if not FVert then begin FFont[FFont.GetShapeIndex(SubShapeNum)].DrawToCanvas(ACanvas, FX, FY, FGlyphScale); FPenDown:= False; // сброс поднятия пера после отрисовки субформы end; if FVert then FVert:= False; end; $8: begin // смещение по осям X-Y, заданное следующими двумя байтами if (Index + 2) < FGlyphHighIndex then begin if not FVert then begin FX:= FX + Shortint(FGlyph[Index + 1]) * FGlyphScale; FY:= FY + Shortint(FGlyph[Index + 2]) * FGlyphScale; DrawLine(ACanvas); end; end else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 3); if FVert then FVert:= False; end; $9: begin // ряд из нескольких смещений по осям X-Y, оканчивающийся на (0,0) Inc(Index, 2); // сейчас указывает на dY while True do begin if (Index >= FGlyphHighIndex) then raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index - 2, FGlyph[Index - 2], FGlyphHighIndex]); if (FGlyph[Index - 1] = 0) and (FGlyph[Index] = 0) then // [(dX,dY) = (0,0)]? begin Inc(Index); Break; end; if not FVert then begin FX:= FX + Shortint(FGlyph[Index - 1]) * FGlyphScale; FY:= FY + Shortint(FGlyph[Index]) * FGlyphScale; DrawLine(ACanvas); end; Inc(Index, 2); end; if FVert then FVert:= False; end; $A: begin // октантная дуга, заданная следующими двумя байтами (R, DSC) if (Index + 2) < FGlyphHighIndex then begin if not FVert then // радиус, [направление, начальный октант, кол. октантов] DrawOctantArc(ACanvas, FGlyph[Index + 1], FGlyph[Index + 2]); end else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 3); if FVert then FVert:= False; end; $B: begin // дробная дуга, заданная следующими пятью байтами if (Index + 5) < FGlyphHighIndex then begin if not FVert then // StartOffset, EndOffset, HighRadius DrawFractionalArc(ACanvas, FGlyph[Index + 1], FGlyph[Index + 2], FGlyph[Index + 3], FGlyph[Index + 4], FGlyph[Index + 5]); // LowRadius, DSC end else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 6); if FVert then FVert:= False; end; $C: begin // дуга, заданная смещением X-Y и прогибом (dX,dY,Bulge) if (Index + 3) < FGlyphHighIndex then begin if not FVert then // dX, dY, Bulge DrawBulgeArc(ACanvas, FGlyph[Index + 1], FGlyph[Index + 2], FGlyph[Index + 3]); end else raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index, FGlyph[Index], FGlyphHighIndex]); Inc(Index, 4); if FVert then FVert:= False; end; $D: begin // несколько дуг, заданных прогибом (dX,dY,Bulge) Inc(Index, 3); // сейчас указывает на Bulge while True do begin if (Index > FGlyphHighIndex) then raise Exception.CreateFmt(rsUnexpectedSpecialCodeFmt, [Index - 3, FGlyph[Index - 3], FGlyphHighIndex]); if (FGlyph[Index - 2] = 0) and (FGlyph[Index - 1] = 0) then // [(dX,dY) = (0,0)]? Break; if not FVert then // dX, dY, Bulge) DrawBulgeArc(ACanvas, FGlyph[Index - 2], FGlyph[Index - 1], FGlyph[Index]); Inc(Index, 3); end; if FVert then FVert:= False; end; $E: begin // обработка следующей команды только для вертикального текста FVert:= True; Inc(Index); end; else begin // длина вектора и код направления if not FVert then DrawLenDir(ACanvas, (FGlyph[Index] and $F0) shr 4, FGlyph[Index] and $0F); Inc(Index); if FVert then FVert:= False; end; end; end; Заметки "на полях":
В методе обновляются текущие координаты и вызываются методы отрисовки. Класс стека координат (от TObjectStack) тривиальный, см. в прилагаемых исходниках. Для отрисовки дуг создан универсальный метод DrawArc; остальные методы (DrawOctantArc, DrawFractionalArc, DrawBulgeArc) находят для него параметры и вызывают DrawArc. // StartAngle, SweepAngle - в градусах. // StartAngle д.б. задан в соответствии с направлением отсчета - CW: // если CW=False - против часовой; // если CW=True - по часовой. procedure TShxShape.DrawArc(ACanvas: TCanvas; CenterX, CenterY, Radius: Double; CW: Boolean; StartAngle, SweepAngle: Double); var OldPenDown: Boolean; EndX, EndY: Double; StepCnt, i: Integer; BeginAngle, FlareAngle, ArcLength, ArcSegmentSize, Sin, Cos: Double; begin BeginAngle:= DegToRad(StartAngle); // начальный угол, рад. FlareAngle:= DegToRad(SweepAngle); // угол раскрыва дуги, рад. ArcLength:= Radius * FlareAngle; // длина дуги // Если заданная дуга по часовой стрелке: запомнить точку начала дуги против часовой // стрелки, после построения восстановить ее - это будет точка конца заданной дуги if CW then begin BeginAngle:= 2 * Pi - BeginAngle - FlareAngle; SinCos(BeginAngle, Sin, Cos); EndX:= CenterX + Radius * Cos; EndY:= CenterY + Radius * Sin; // Встали в точку конца заданной дуги (т.е. точку начала дуги против часовой стрелки) OldPenDown:= FPenDown; FPenDown:= False; FX:= EndX; FY:= EndY; DrawLine(ACanvas); FPenDown:= OldPenDown; end; // Длина сегмента (мин. кол. сегментов = 3) ArcSegmentSize:= ArcLength / Max((FFont.ArcSmoothness * SweepAngle) / 360.0, 3.0); StepCnt:= Trunc(Ceil(ArcLength / ArcSegmentSize)); for i:= 1 to StepCnt do begin SinCos(BeginAngle + FlareAngle * i / StepCnt, Sin, Cos); FX:= CenterX + Radius * Cos; FY:= CenterY + Radius * Sin; // Добавить точку DrawLine(ACanvas); end; // Восстановить точку конца заданной дуги if CW then begin FPenDown:= False; FX:= EndX; FY:= EndY; DrawLine(ACanvas); FPenDown:= OldPenDown; end; end; Свойство шрифта ArcSmoothness задает количество сегментов, на которые разбивается дуга в полную окружность. Методы DrawOctantArc, DrawFractionalArc, DrawBulgeArc и DecodeOctant: // Если знак "+", то байт DSC = (S * 16 + C), иначе DSC = (S * 16 + C) or $80 procedure DecodeOctant(DSC: Byte; var S, C: Byte; var CW: Boolean); var tmp: Shortint; b: Byte; begin tmp:= Shortint(DSC); CW:= ((tmp and $80) <> 0); b:= Byte(tmp); S:= (b and $70) shr 4; C:= b and $07; end; procedure TShxShape.DrawOctantArc(ACanvas: TCanvas; R, DSC: Byte); var S, C: Byte; // начальный октант, количество охватываемых октантов CW: Boolean; // True - по часовой стрелке; False - против часовой стрелки CenterX, CenterY, Radius: Double; StartAngle, SweepAngle: Integer; // в градусах begin DecodeOctant(DSC, S, C, CW); Radius:= R * FGlyphScale; GetOctantArcParams(FX, FY, Radius, S, C, CW, CenterX, CenterY, StartAngle, SweepAngle); DrawArc(ACanvas, CenterX, CenterY, Radius, CW, StartAngle, SweepAngle); end; procedure TShxShape.DrawFractionalArc(ACanvas: TCanvas; StartOffset, EndOffset, HighRadius, LowRadius, DSC: Byte); var S, C: Byte; // начальный октант, конечный октант CW: Boolean; // True - по часовой стрелке; False - против часовой стрелки CenterX, CenterY, Radius: Double; StartAngle, SweepAngle: Double; // в градусах begin DecodeOctant(DSC, S, C, CW); Radius:= ((HighRadius shl 8) + LowRadius) * FGlyphScale; GetFractionalArcParams(FX, FY, Radius, S, C, StartOffset, EndOffset, CW, CenterX, CenterY, StartAngle, SweepAngle); DrawArc(ACanvas, CenterX, CenterY, Radius, CW, StartAngle, SweepAngle); end; procedure TShxShape.DrawBulgeArc(ACanvas: TCanvas; dX, dY, Bulge: Byte); var OfsX, OfsY: Double; b: Shortint; CenterX, CenterY, Radius: Double; CW: Boolean; StartAngle, SweepAngle: Double; begin OfsX:= Shortint(dX) * FGlyphScale; OfsY:= Shortint(dY) * FGlyphScale; b:= Shortint(Bulge); if b <> 0 then // если это дуга - рисовать ее begin GetBulgeArcParams(FX, FY, OfsX, OfsY, b, 127.0, CenterX, CenterY, Radius, CW, StartAngle, SweepAngle); DrawArc(ACanvas, CenterX, CenterY, Radius, CW, StartAngle, SweepAngle); end else // если это прямой сегмент - рисовать прямую begin FX:= FX + OfsX; FY:= FY + OfsY; DrawLine(ACanvas); end; end; Методы получения параметров различных видов дуг// Углы в градусах. // StartOctant, OctantCount = [0..7]. OctantCount = 0 означает 8 октантов. procedure GetOctantArcParams(BeginX, BeginY, Radius: Double; StartOctant, OctantCount: Byte; Clockwise: Boolean; var CenterX, CenterY: Double; var StartAngle, SweepAngle: Integer); var Sin, Cos: Double; begin StartAngle:= StartOctant * 45; SinCos(DegToRad(StartAngle), Sin, Cos); CenterX:= BeginX - Radius * Cos; CenterY:= BeginY - Radius * Sin; if OctantCount = 0 then SweepAngle:= 360 else SweepAngle:= 45 * OctantCount; if Clockwise then StartAngle:= 360 - StartAngle; end; // Углы в градусах. procedure GetFractionalArcParams(BeginX, BeginY, Radius: Double; StartOctant, OctantCount, StartOffset, EndOffset: Byte; Clockwise: Boolean; var CenterX, CenterY, StartAngle, SweepAngle: Double); var S: Byte; StartAngleInRad, StartBeta, EndBeta, EndAngle: Double; Sin, Cos: Extended; begin StartBeta:= StartOffset * 45.0 / 256.0; EndBeta:= EndOffset * 45.0 / 256.0; if not Clockwise then // против часовой begin S:= StartOctant; StartAngle:= S * 45.0 + StartBeta; if OctantCount <> 0 then begin if EndOffset = 0 then EndAngle:= (StartOctant + OctantCount) * 45.0 + EndBeta else EndAngle:= (StartOctant + OctantCount - 1) * 45.0 + EndBeta; SweepAngle:= EndAngle - StartAngle; end else SweepAngle:= 7.0 * 45.0 + EndBeta - StartBeta; end else begin // по часовой S:= ((8 - StartOctant) mod 8); StartAngle:= S * 45.0 + StartBeta; if OctantCount = 0 then begin EndAngle:= S * 45.0 + (OctantCount - 1) * 45.0 + EndBeta end else begin if EndOffset = 0 then EndAngle:= S * 45.0 + OctantCount * 45.0 + EndBeta else EndAngle:= S * 45.0 + (OctantCount - 1) * 45.0 + EndBeta; end; if EndAngle > 360.0 then EndAngle:= EndAngle - 360.0; if EndAngle < StartAngle then SweepAngle:= 360.0 + EndAngle - StartAngle else SweepAngle:= EndAngle - StartAngle; end; if SweepAngle > 360.0 then SweepAngle:= 360.0; // Для расчета центра пересчитаем в угол против часовой if Clockwise then StartAngleInRad:= DegToRad(360.0 - StartAngle) else StartAngleInRad:= DegToRad(StartAngle); SinCos(StartAngleInRad, Sin, Cos); CenterX:= BeginX - Radius * Cos; CenterY:= BeginY - Radius * Sin; end; // Углы в градусах. // Для шрифта Factor = 127, т.к. Bulge = (2 * H / D) * 127, в остальных случаях Factor = 1. procedure GetBulgeArcParams(BeginX, BeginY, dX, dY, Bulge, Factor: Double; var CenterX, CenterY, Radius: Double; var Clockwise: Boolean; var StartAngle, SweepAngle: Double); var EndX, EndY, MidX, MidY: Double; Chord, HalfChord, H, S: Double; begin Clockwise:= (Bulge < 0.0); // Точка конца дуги EndX:= BeginX + dx; EndY:= BeginY + dy; // Длина хорды Chord:= Sqrt((EndX - BeginX) * (EndX - BeginX) + (EndY - BeginY) * (EndY - BeginY)); // Половина длины хорды HalfChord:= Chord / 2.0; // Стрелка (sagitta) H:= Abs(Bulge) * Chord / 2.0 / Factor; // Радиус Radius:= (HalfChord * HalfChord + H * H) / H / 2.0; // Координаты точки по-середине между концами дуги MidX:= (BeginX + EndX) / 2.0; MidY:= (BeginY + EndY) / 2.0; // Знак в уравнениях центра SweepAngle:= ArcTan(Abs(Bulge / Factor)) * 4.0; // внутренний угол дуги if not Clockwise then // CCW - против часовой стрелки begin if SweepAngle >= Pi then S:= -1.0 else S:= +1.0; end else // CW - по часовой стрелке begin if SweepAngle >= Pi then S:= +1.0 else S:= -1.0; end; // Координаты центра CenterX:= MidX + S * Sqrt(Radius * Radius - HalfChord * HalfChord) * (BeginY - EndY) / Chord; CenterY:= MidY + S * Sqrt(Radius * Radius - HalfChord * HalfChord) * (EndX - BeginX) / Chord; // Углы StartAngle:= ArcTan2(BeginY - CenterY, BeginX - CenterX); if (StartAngle < 0.0) then begin if Clockwise then StartAngle:= Abs(StartAngle) else StartAngle:= TWO_PI + StartAngle; end else begin if Clockwise then StartAngle:= TWO_PI - StartAngle; end; StartAngle:= RadToDeg(StartAngle); SweepAngle:= RadToDeg(SweepAngle); end; Документация AutoCAD не всегда полна и точна, в частности при описании Fractional Arc (дробной дуги). Эксперименты показали следующее: В заключение таблица, в которой показаны отличительные особенности шрифтов, использованных для тестирования:
* — для шрифтов это настоящее количество символов, т.е. без учета символа 0. В прилагаемом примере построено приложение, реализующее описанный функционал: К статье прилагаются файлы: Внимание! Запрещается воспроизведение
данной статьи или ее части без согласования с автором. Если вы желаете разместить
эту статью на своем сайте или издать в печатном виде, свяжитесь с автором. |