Форум Flasher.ru

Форум Flasher.ru (http://www.flasher.ru/forum/index.php)
-   Статьи (http://www.flasher.ru/forum/forumdisplay.php?f=101)
-   -   Портируем, рефакторим, оптимизируем (http://www.flasher.ru/forum/showthread.php?t=109223)

Iv 16.03.2008 19:32

Создаем классы конкретных команд
 
Создаем классы команд, все они наследуются от DrawingCommand.
Вот как будут выглядеть конструкторы этих классов:
Код AS3:

public function FillCommand (color:uint, alpha:Number) {...}

Код AS3:

public function StyleCommand (thickness:Number, color:uint, alpha:Number) {...}

Код AS3:

public function MoveCommand (target:Point) {...}

Код AS3:

public function LineCommand (target:Point) {...}

Код AS3:

public function CurveCommand (control:Point, end:Point) {...}

Добавим во все классы инициализацию экземпляров и методы доступа к данным.

И, раз уж мы принялись структурировать проект, в папке svg создадим папку geom и переместим туда классы CubicBezierSVG, LineSVG и QuadraticBezierSVG. Правим ошибки, тестируем.

Iv 16.03.2008 21:57

Замена
 
Вложений: 1
Возвращаемся в класс PathToArray и оцениваем возможность применения новых классов.
Только в этот момент обнаруживаю, что замена строковых данных на константы не везде сработала, поскольку строковые данные использовались не только в двойных кавычках, но и в одинарных.
Производим замену на константы.

Наша задача - заменить в массиве нетипизированные данные на объекты данных. Это нужно сделать как в процедуре наполнения массива, так и в процедуре использования.
Массив наполняется в классе PathToArray, где называется "drawCmds" и используется в классе SVGDisplayInFlash, где называется "dCmds". Заменим эти имена на одно: drawingCommands.

Начнем замену на объекты данных с класса PathToArray. Благодаря тому, что строки заменены на константы, нам трудно ошибиться какого класса команда должна быть использована.
Дублируем старую команду, комментируем верхнюю, а в нижней делаем соответствующую замену. Первые три замены будут выглядеть так:
Код AS3:

// drawingCommands.push([DrawingCommand.FILL, [fill.color, fill.alpha]]);
drawingCommands.push(new FillCommand(fill.color, fill.alpha));
 
// drawingCommands.push([DrawingCommand.STYLE, [stroke.width, stroke.color, stroke.alpha]]);
drawingCommands.push(new StyleCommand(stroke.width, stroke.color, stroke.alpha));
 
// drawingCommands.push([DrawingCommand.MOVE, [firstP.x, firstP.y]]);
drawingCommands.push(new MoveCommand(firstP.clone()));

Обратите внимание на то, что в последнем случае примера в качестве аргумента конструктора мы использовали копию точки, а не ее саму.
Заменяем все случаи добавления в массив и переходим к методу getShapes класса SVGDisplayInFlash.
Здесь для замены мы используем наши новые возможности. Мы можем заменить условную логику на полиморфизм. Суть будет заключаться в том, что в каждом классе команды рисования мы создадим метод draw и будем вызывать его в цикле.
Чтобы реализовать такое поведение можно пойти двумя путями: создать метод draw в классе DrawingCommand, а в подклассах его перекрыть, либо создать интерфейс и в классах рисования имплементировать его.
Мы пойдем вторым путем, поскольку нам это поможет создавать сами методы. К тому-же нам не придется беспокоиться о том, что кому-то в голову придет использовать метод draw у экземпляра класса DrawingCommand.

В папке draw cоздаем интерфейс IDrawable и описываем метод draw:
Код AS3:

package com.itechnica.svg.draw {
        import flash.display.Graphics;       
 
        public interface IDrawable {
                function draw(target:Graphics):void;
        }
}

Затем классам папки draw, кроме DrawingCommand, добавим имплементацию интерфейса, например:
Код AS3:

public class CurveCommand extends DrawingCommand implements IDrawable {...

После этого редактор подсветит ошибку, используя CTRL+1 исправляем ее - добавится метод draw. Каждому классу задаем свою соответствующую логику этого метода, например, класс CurveCommand будет использовать curveTo:
Код AS3:

public function draw(target:Graphics):void {
        target.curveTo(controlPoint.x, controlPoint.y, end.x, end.y);
}

Аналогично поступаем с остальными классами.

После чего вся логика, ранее реализованая в switch заменится на две строки:
Код AS3:

private function getShapes(iPath:Number):void {
        var drawingCommands:Array = new Array();
        conv = new PathToArray(paths[iPath], drawingCommands);
 
        // draw the shapes in clips in holder movieclip
        // holder.createEmptyMovieClip("p"+iPath, iPath+1);
 
        const drawLayer:Sprite = new Sprite();
        holderMc.addChild(drawLayer);
        const drawTarget:Graphics = drawLayer.graphics;
 
        for (var i:Number=0; i<drawingCommands.length; i++) {
                var command:IDrawable = drawingCommands[i] as IDrawable;
                command.draw(drawTarget);
        }
 
        // repeat til all paths have been read
        if (++iPath < nPathNodes) getShapes(iPath);
}

Тестируем, всё работает.

Iv 17.03.2008 14:24

хоку
 
В раздумиях великих
по коду класса я брожу.
PathToArray.

Iv 17.03.2008 15:01

Следующая цель
 
Следующая цель у меня сомнений не вызывает. Мне категорически не нравятся два огромных метода в классе PathToArray. Они огромны, из-за этого их логика сложна для восприятия.
Вспомните, каким был метод getShapes и каким он стал. В итоге хочется чего-то такого.
Плюс к этому, не хочется повторяться: я помню, что статья учебная и главная цель научиться чему-то новому и полезному.

Смотрим, что из себя структурно представляет метод makeDrawCmds. В цикле while имеется switch, в каждом из case-ов которого имеется логика конвертации набора данных в объект данных.
Всё бы ничего, но у нас в case-ах имеются загадочные строковые данные, которые нужно сделать константами. Мы понимаем откуда они вообще взялись: они используются в svg для обозначения того, какие действия следует произвести с данными.
Но это в общем. А вот что именно обозначают эти буквы мы не знаем. Поэтому осмысленные имена дать не можем.
Чтобы разобраться, обратимся к первоисточнику: описанию формата SVG: http://www.w3.org/Graphics/SVG/

Iv 17.03.2008 15:52

Формат данных
 
Смотрим и обнаруживаем раздел SVG in Russian. В словарик заглядывать не надо, чтобы понять что это за линки.
Заглянем для общего развития на википедию: http://ru.wikipedia.org/wiki/SVG
Погуляв по русским линкам я нашел:
http://jre.cplire.ru/koi/oct01/5/text.html
Цитата:

Для рисования линий в SVG используется тег <path>. Здесь атрибут d представляет параметры кривой – буквы обозначают команды, а цифры параметры этой команды. Например, M – это команда MoveTo, а цифры, следующие за ней, обозначают координаты, куда нужно перейти, чтобы начать рисовать кривую. Аналогично, С – это команда CurveTo, которая рисует кривую Безье.
http://www.xml.nsu.ru/extra/svgintro_2.xml
Цитата:

Траектории
Траектории в SVG представляют контуры объектов. Одним из наиболее важных атрибутов элемента <path> является атрибут d, который содержит данные, описывающие траекторию. Атрибут d несет инструкции типа "moveto" (переместить), "line" (провести линию), "curve" (провести кривую Безье второй или третьей степени), "arc" (провести дугу) и "closepath" (закрыть траекторию). Все эти инструкции обозначаются какой-нибудь одной буквой (например, "moveto" обозначается символом M). В нашем примере мы используем инструкции "moveto" и "quadratic bezier curve". Следующий код задает кривую, изображающую телефонный шнур:

<path id="cord" d="M 235,130 q 25,0 25,25 q 0,25 25,25 q 25,0 25,
25 q 0,25 25,25 q 25,0 25,25 q 0,25 25,25 q 25,0 25,25 q 0,
25 25,25 q 25,0 25,25" />

В атрибуте d мы сначала задаем инструкцию M, которая дает указание сделать сдвиг к новой точке, от нее начнется кривая. Заглавная M в данном случае означает, что в описании используются абсолютные координаты, а маленькая m - что используются относительные. После того, как мы с помощью инструкции "moveto" определили начальную точку траектории, мы используем инструкцию q для задания сегмента кривой Безье второго порядка. И снова, маленькая q означает, что описание дается в относительных координатах, а заглавная Q - что в абсолютных. Параметрами команды q является серия координатных пар в виде (x1,y1 x,y). Эти координатные пары задают кривую Безье второго порядка, которая проходит из текущей точки в точку с координатами (x,y) используя точку (x1,y1) в качестве контрольной. После выполнения одной инструкции q, текущая точка переместится в соответствии с координатным параметром. Поскольку мы используем относительные координаты, мы можем переместить изображение телефонного шнура, просто изменив координаты начальной точки.
Эта информация помогает нам разобраться, то для дальнейших действий мы используем всё-таки официальное описание: http://www.w3.org/TR/SVG11/paths.html

Iv 17.03.2008 16:19

class FormatSVG
 
Суммируем наши знания в коде: создадим класс FormatSVG и перенесем в него из описания формата все инструкции атрибута d:
Код AS3:

package com.itechnica.svg {
 
        // http://www.w3.org/TR/SVG11/paths.html
        public class FormatSVG {
 
                // 8.3.2 The "moveto" commands
 
                public static const MOVE_TO_ABSOLUTE : String = "M";
                public static const MOVE_TO_RELATIVE : String = "m";
 
                // 8.3.3 The "closepath" command
                public static const CLOSEPATH_ABSOLUTE : String = "Z";
                public static const CLOSEPATH_RELATIVE : String = "z";
 
                // 8.3.4 The "lineto" commands
                public static const LINE_TO_ABSOLUTE : String = "L";
                public static const LINE_TO_RELATIVE : String = "l";
 
                public static const HORIZONTAL_LINE_TO_ABSOLUTE : String = "H";
                public static const HORIZONTAL_LINE_TO_RELATIVE : String = "h";
 
                public static const VERTICAL_LINE_TO_ABSOLUTE : String = "V";
                public static const VERTICAL_LINE_TO_RELATIVE : String = "v";
 
                // 8.3.6 The cubic Bйzier curve commands
                public static const CUBIC_CURVE_TO_ABSOLUTE : String = "C";
                public static const CUBIC_CURVE_TO_RELATIVE : String = "c";
 
                public static const CUBIC_SMOOTH_CURVE_TO_ABSOLUTE : String = "S";
                public static const CUBIC_SMOOTH_CURVE_TO_RELATIVE : String = "s";
 
                // 8.3.7 The quadratic Bйzier curve commands
                public static const QUADRATIC_CURVE_TO_ABSOLUTE : String = "Q";
                public static const QUADRATIC_CURVE_TO_RELATIVE : String = "q";
 
                public static const QUADRATIC_SMOOTH_CURVE_TO_ABSOLUTE : String = "T";
                public static const QUADRATIC_SMOOTH_CURVE_TO_RELATIVE : String = "t";
 
                // 8.3.8 The elliptical arc curve commands
                public static const ARC_TO_ABSOLUTE : String = "A";
                public static const ARC_TO_RELATIVE : String = "a";
        }
}


Iv 17.03.2008 21:46

тест
 
Перед тем, как начать замену на константы, сделаем один ход, который и впоследствии нам пригодится.
Мы сделаем простой тест.
Задача теста будет проста: сверять результаты работы скрипта с некими образцами.

Для начала создадим методы toString в классах папки draw:

DrawingCommand:
Код AS3:

public function toString() : String {
        throw new Error("com.itechnica.svg.draw.DrawingCommand");
        return "com.itechnica.svg.draw.DrawingCommand";
}

CurveCommand:
Код AS3:

override public function toString() : String {
        return "curveTo(" + controlPoint.x + ", " + controlPoint.y + ", " + end.x + ", " + end.y + ")";
}

FillCommand:
Код AS3:

override public function toString() : String {
        return "beginFill(0x"+colorValue.toString(16)+", "+ alphaValue+")";
}

LineCommand:
Код AS3:

override public function toString() : String {
        return "lineTo(" + targetPoint.x + ", " + targetPoint.y + ")";
}

MoveCommand:
Код AS3:

override public function toString() : String {
        return "moveTo(" + targetPoint.x + ", " + targetPoint.y + ")";
}

StyleCommand:
Код AS3:

override public function toString() : String {
        return "beginFill("+thicknessValue+", 0x"+colorValue.toString(16)+", "+ alphaValue+")";
}

Затем создадим класс Test:
Код AS3:

package {
 
        public class Test {
 
                public static const testArray:Array = [];
 
        }
}

Добавим код, который нам поможет легко получать тестовые строки и тестировать код:

Код AS3:

private function getShapes(iPath:Number):void {
        var drawingCommands:Array = new Array();
        conv = new PathToArray(paths[iPath], drawingCommands);
 
        // TEST as3
        const updateTestArray:Boolean = false;
 
        const testString:String = Test.testArray[iPath];
        const currentString:String = drawingCommands.toString();
 
        if (updateTestArray) {
                trace("[\""+currentString+"\"],");
        } else if (currentString != testString) {
                throw new Error("SVGDisplayInFlash.getShapes test error");
        }
        //  END TEST as3
        ................

Компилируем проект и получаем в окне Output длинную строку. Копируем и вставляем ее в массив testArray класса Test.
После этого заменяем значение updateTestArray на false и компилируем еще раз.
Если ошибок не возникло, значит тест пройден успешно.

Нужно четко отдавать себе отчет в том, что тест не идеален. Он отлавливает только те случаи, когда изменяется результирующая строка. Но далеко не факт, что данный пример svg файла использует все теги svg.
Но даже такой тест лучше чем ничего.

Iv 17.03.2008 21:53

Применение новых констант
 
Вложений: 1
Вернемся к константам класса FormatSVG.
Ранее в классе DrawingCommand мы уже использовали константы, описывающие эти же сущности. Пришел их черед - они отслужили своё. По очереди закомментируем их, а в местах использования заменим на соответствующие из нашего нового класса. Каждый раз тестируем проект.

По ходу дела обнаруживаем, что неверно интерпретировали "S" - думали, что это стиль, а оказалось что это кубическая кривая. Всё-таки работа с первоисточником очень важна и к описанию формата SVG надо было обращаться раньше.

Следующим шагом избавляемся от использования строковых данных в case-ах метода makeDrawCmds.
Это занудная работа, поэтому выложу результат.

Iv 18.03.2008 18:22

Вынос
 
Вложений: 1
Теперь мы готовы к следующему шагу: выделению методов из makeDrawCmds.
Начнем с самого первого case: FormatSVG.MOVE_TO_ABSOLUTE.
- копируем содержимое case до строки break.
- создаем метод createMoveToCommand
- вставляем скопированное в метод.
- задаем аргументы, одноименные с переменными, вызывающими ошибку.
Получаем такой метод:

Код AS3:

private function createMoveToCommand(firstP:Point, lastP:Point, svgCmds:Array, j:int) : void {
        // moveTo point
        firstP = lastP = new Point(Number(svgCmds[j]), Number(svgCmds[j + 1]));
        drawingCommands.push(new FillCommand(fill.color, fill.alpha));
        drawingCommands.push(new StyleCommand(stroke.width, stroke.color, stroke.alpha));
        drawingCommands.push(new MoveCommand(firstP.clone()));
 
        j += 2;
        if (j < svgCmds.length && !isNaN(Number(svgCmds[j]))) { 
                do {
                        // if multiple points listed, add the rest as lineTo points
                        lastP = new Point(Number(svgCmds[j]), Number(svgCmds[j + 1]));
 
                        drawingCommands.push(new LineCommand(lastP.clone()));
 
                        firstP = lastP;
                        j += 2;
                } while (j < svgCmds.length && !isNaN(Number(svgCmds[j])));
        }
}

Тестируем, получаем ошибку, разбираемся в чем дело.
Видим, что в методе изменяется значение переменной j, которая в дальнейшем используется в коде. Добавляем возвращаемое значение:

Код AS3:

private function createMoveToCommand(firstP:Point, lastP:Point, svgCmds:Array, j:int) : int {
        .........
        return j;
}

И в месте вызова добавляем присвоение нового значения j:
Код AS3:

j = createMoveToCommand(firstP, lastP, svgCmds, j);

Тестируем, получаем ошибку.

Вот сейчас пишу и понятия не имею, что это за ошибка и откуда она взялась. Нет, конечно я мог бы вначале добиться результата и затем с видом умника достать рояль из кустов. Но ведь так не бывает в реальной жизни. Я вначале пишу что и как хочу сделать, затем пытаюсь это реализовать и пишу что получилось. Вот сейчас на непонятность нарвался, и буду рассказывать как я в таких случаях поступаю.

Еще раз проверил старый код: закомментировал вызов метода, раскомментировал код, протестировал, всё работает.
Возвращаем комментарии обратно и смотрим, что еще может сносить.
Вполне реально - переприсвоение firstP и lastP. Встречается в четырех строках метода. И, если первую строку мы в состоянии вынести за пределы метода, то остальные две не получится.
Попробуем решить проблему так:
- вынесем объявление точек из метод в класс, сделаем переменные приватными.
Код AS3:

private        var firstP:Point;
private        var lastP:Point;
private        var lastC:Point;

- добавим в начало метода обнуление переменных
Код AS3:

firstP = lastP = lastC = null;

- удалим первые два аргумента в вызове метода и в методе.
Тестируем. Работает.
Становится понятна причина: в аргументах мы объявили точки, и присвоение новых значений переменным, объявленным в аргументах не давало требуемой замены одноименных переменных в методе makeDrawCmds.

Копируем вызов:
Код AS3:

j = createMoveToCommand(svgCmds, j);

переходим на case FormatSVG.LINE_TO_RELATIVE и вставляем. Переименовываем в createLineToRelativeCommand. После чего с помощью CTRL+1 создаем метод.
Копируем и комментируем дальнейший код до break. Переходим на новый метод и вставляем в него скопированный код. Заменяем return null на return j. Тестируем. Работает.
Аналогично двигаемся дальше. После каждого шага тестируем.

Первой остановкой на этом пути стало появление сubicBezier. Поиском по коду смотрим каким образом она применяется. В коде нет случаев, когда сubicBezier присваивается другой переменной. Попробуем объявить ее локальной переменной.
И не забываем изменить возвращаемое значение с null на j. Тестируем, всё ок. Дальше планируем в аналогичных случаях поступать также.
Если по ходу дела нарываемся на бесконечный цикл, значит попросту забыли заменить null на return j. Нарывался два раза.

После того, как закончили и протестировали, сносим закомментированные строки и ненужное более объявление переменной сubicBezier.
В итоге должен получиться вот такой метод:
Код AS3:

private function makeDrawCmds(svgCmds:Array):void {
        var j:Number = 0;
        var cmd:String;
        firstP = lastP = lastC = null;
        do {
                cmd = svgCmds[j++];
                switch (cmd) {
                        case FormatSVG.MOVE_TO_ABSOLUTE :
                                j = createMoveToCommand(svgCmds, j);
                                break;
                        case FormatSVG.LINE_TO_RELATIVE :
                                j = createLineToRelativeCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.LINE_TO_ABSOLUTE :
                                j = createLineToCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.HORIZONTAL_LINE_TO_RELATIVE :
                                j = createHorizontalLineToRelativeCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.HORIZONTAL_LINE_TO_ABSOLUTE :
                                j = createHorizontalLineToCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.VERTICAL_LINE_TO_RELATIVE :
                                j = createVerticalLineToRelativeCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.VERTICAL_LINE_TO_ABSOLUTE :
                                j = createVerticalLineToCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.QUADRATIC_CURVE_TO_RELATIVE :
 
                                j = createQuadraticCurveToRelativeCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.QUADRATIC_CURVE_TO_ABSOLUTE :
                                j = createQuadraticCurveToCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.CUBIC_CURVE_TO_RELATIVE :
                                j = createCubicCurveToRelativeCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.CUBIC_CURVE_TO_ABSOLUTE :
                                j = createCubicCurveToCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.CUBIC_SMOOTH_CURVE_TO_RELATIVE :
                                j = createCubicSmoothCurveToRelativeCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.CUBIC_SMOOTH_CURVE_TO_ABSOLUTE:
                                j = createCubicSmoothCurveToCommand(svgCmds, j);
                                break;
 
                        case FormatSVG.CLOSEPATH_RELATIVE :
                        case FormatSVG.CLOSEPATH_ABSOLUTE :
                                j = createClosePathCommand(svgCmds, j);
                                break;               
                } // end switch
        }  while (j < svgCmds.length);
}

Не фонтан, конечно, но это уже кое-что: мы в состоянии увидеть логику метода.

Iv 18.03.2008 20:35

Пространства имен
 
Раз уж взялись за этот метод, доведем дело до конца.
Этот switch мне очень не нравится. Что мы можем сделать, чтобы избавиться? Есть два пути: первый мы уже использовали в похожей ситуации: заменили условный выбор полиморфизмом в методе getShapes класса SVGDisplayInFlash.
Подойдет ли аналогичный способ для этого случая? Теоретически, мы можем загнать код в такие рамки. Но это неудобно и потребует существенного изменения логики приложения, чего мы избегаем на данном этапе.
Второй путь - применить пространства имен, его и попробуем применить.

Делаем небольшой тест:
- для начала создадим пространство имен
Код AS3:

private namespace MOVE_TO_ABSOLUTE = "M";

затем заменим пространство имен private у метода createMoveToCommand на созданное:
Код AS3:

MOVE_TO_ABSOLUTE function createMoveToCommand(svgCmds:Array, j:int) : int {...

И создадим тестовый код в соответствующем case:
Код AS3:

case FormatSVG.MOVE_TO_ABSOLUTE :
 
        var createMoveToCommand:*;
        var space:Namespace = Namespace(cmd);
        var method:Function = space::createMoveToCommand as Function;
        trace(space+ " >>> "+method);
 
        j = method(svgCmds, j);
        break;

Протестируем, всё работает.

Идея заключается в том, чтобы метод makeDrawCmds принял примерно такой вид:
Код AS3:

private function makeDrawCmds(svgCmds:Array):void {
 
        firstP = lastP = lastC = null;
        var j:Number = 0;
        var createDrawCommand:Function;
        var space:Namespace;
        var method:Function;
 
        do {
                space = Namespace(svgCmds[j++]);
                method = space::createDrawCommand as Function;
                j = method(svgCmds, j);
 
        }  while (j < svgCmds.length);
}

Для этого придется создать создать массу пространств имен и перенести в эти пространства имен методы create...Command и переименовать их все в createDrawCommand.
В итоге мы получим вполне симпатичный метод makeDrawCmds и... несколько неприятностей вдовесок.

Для объявления пространства имен мы не можем использовать константы, объявленные в классе FormatSVG. Мы должны обязательно их объявлять вот так:
Код AS3:

private namespace MOVE_TO_ABSOLUTE = "M";

но не можем применить ни один их этих вариантов:
Код AS3:

private namespace MOVE_TO_ABSOLUTE = new Namespace(FormatSVG.MOVE_TO_ABSOLUTE);
private namespace MOVE_TO_ABSOLUTE = FormatSVG.MOVE_TO_ABSOLUTE;

Это значит, что если вдруг нам понадобится изменить значение константы, то придется не забыть и изменить объявление пространства имен. Мы-то это знаем, но не те, кто будут править код после нас.

Второй неприятный довесок: FDT плохо поддерживает работу с пространствами имен. Например, чтобы избавиться от подсвечивания ошибки, нам пришлось объявлять переменную:
Код AS3:

var createMoveToCommand:Function;

Хотя есть случаи, в которых использование пространств имен себя оправдывает, но это не наш случай.

Iv 18.03.2008 22:02

Третий путь
 
Я решил пойти по третьему пути: создать коллекцию методов и брать методы из нее.
Это быстрый вариант, понятный большинству разработчиков и в нем можно задействовать константы класса FormatSVG. Хотя без некоторых переделок не обойтись.

Опять протестируем на коротком примере.
Объявим статической константой коллекцию методов и добавим ссылку на метод:
Код AS3:

private static const DRAW_METHODS:Object = new Object();
        DRAW_METHODS[FormatSVG.MOVE_TO_ABSOLUTE] = createMoveToCommand;

Тестируем и обнаруживаем, что статическая константа не видит метод экземпляра класса. Ну, в общем это правильно, особенно учитывая то, что каждый метод привязан к экземпляру класса.

Мы можем создать такой экземпляр перед объявлением коллекции:
Код AS3:

private static const INSTANCE:PathToArray = new PathToArray(null, null);

Тестируем, получаем ошибку. Причина ошибки понятна: мы передали невалидные аргументы. Чтобы от нее избавиться, достаточно в конструктор класса добавить проверку:
Код AS3:

if (svgNode == null || dCmds == null) {
        return;
}

Теперь добавляем тестовый вызов:
Код AS3:

case FormatSVG.MOVE_TO_ABSOLUTE :
        var drawMethod:Function = DRAW_METHODS[FormatSVG.MOVE_TO_ABSOLUTE];
        trace("drawMethod: " +drawMethod);
        j = drawMethod.call(this, svgCmds, j);
        // j = createMoveToCommand(svgCmds, j);
        break;

и опять обламываемся. Функция в коллекции найдена, но внутри функции всё равно идет обращение к экземплярам пустого объекта.
Чтобы избежать этого, передадим текущий объект в аргументах и внутри методов будем использовать обращение используя только его. А поскольку пустой экземпляр в таком случае не нужен, удаляем объявление константы INSTANCE и делаем метод createMoveToCommand статическим.
Вот что получается:

Код AS3:

private static const DRAW_METHODS:Object = new Object();
        DRAW_METHODS[FormatSVG.MOVE_TO_ABSOLUTE] = createMoveToCommand;
 
private function makeDrawCmds(svgCmds:Array):void {
        ....
        case FormatSVG.MOVE_TO_ABSOLUTE :
 
                var drawMethod:Function = DRAW_METHODS[FormatSVG.MOVE_TO_ABSOLUTE];
                j = drawMethod(this, svgCmds, j);
                // j = createMoveToCommand(svgCmds, j);
                break;
        ....
}
private static function createMoveToCommand(instance:PathToArray, svgCmds:Array, j:int) : int {
        // moveTo point
        instance.firstP = instance.lastP = new Point(Number(svgCmds[j]), Number(svgCmds[j + 1]));
        instance.drawingCommands.push(new FillCommand(instance.fill.color, instance.fill.alpha));
        instance.drawingCommands.push(new StyleCommand(instance.stroke.width, instance.stroke.color, instance.stroke.alpha));
        instance.drawingCommands.push(new MoveCommand(instance.firstP.clone()));
        j += 2;
        if (j < svgCmds.length && !isNaN(Number(svgCmds[j]))) { 
                do {
                        // if multiple points listed, add the rest as lineTo points
                        instance.lastP = new Point(Number(svgCmds[j]), Number(svgCmds[j + 1]));
                        instance.drawingCommands.push(new LineCommand(instance.lastP.clone()));
                        instance.firstP = instance.lastP;
                        j += 2;
                } while (j < svgCmds.length && !isNaN(Number(svgCmds[j])));
        }
        return j;
}

Если мы аналогично заменим обращения ко всем методам, станет ли наш код от этого лучше? Нет, только не это.

Мы честно пытались сделать makeDrawCmds лучше и достигли определенных успехов: теперь он читабелен. Но структура кода пока очень жесткая и мы столкнулись с тем, что не можем сделать этот участок лучше.
Стоит ли продолжать?
Нужно уметь остановиться и сказать себе, что время этой части кода еще не наступило.
Позднее, когда архитектура проекта станет лучше, мы вернемся к этому вопросу.

Iv 19.03.2008 17:19

Рефакторинг метода extractCommands
 
Переименуем методы: makeDrawCmds в makeDrawCommands и extractCmds в extractCommands.
И перейдем к рефакторингу метода extractCommands.


Даже визуально заметно, что метод можно разделить на две части: логику применения атрибутов и разбиение строки запятыми.
Начнем со второй части. Создаем метод pathDataToArray и копируем в него логику разбиения строки запятыми:

Код AS3:

private function pathDataToArray(node:XMLNode) : Array {
        var dstring:String = "";
        // if commas included, is it Adobe Illustrator (no spaces) or SVG Factory/other?
        if (getAttribute(node, "d").indexOf(",") > -1) { 
                // has commas?
                if (getAttribute(node, "d").indexOf(" ") > -1) { 
                        // yes, has spaces?
                        // change spaces to commas, then deal as for Illustrator
                        dstring = String2.replace(getAttribute(node, "d"), " ", ",");
                }  else {
                        dstring = getAttribute(node, "d");
                }
        } else { 
                // no commas
                // get rid of extra spaces and change rest to commas
                dstring = getAttribute(node, "d");
                dstring = String2.shrinkSequencesOf(dstring, " ");
                dstring = String2.replace(getAttribute(node, "d"), " ", ",");
        }               
        dstring = String2.replace(dstring, "c", ",c,");
        dstring = String2.replace(dstring, "C", ",C,");
        dstring = String2.replace(dstring, "S", ",S,");
        dstring = String2.replace(dstring, "s", ",s,");
        // separate the z from the last element
        dstring = String2.replace(dstring, "z", ",z");
        // change the following if M can be mid-path
        dstring = String2.replace(dstring, "M", "M,");
        dstring = String2.replace(dstring, "L", ",L,");
        dstring = String2.replace(dstring, "l", ",l,");
        dstring = String2.replace(dstring, "H", ",H,");
        dstring = String2.replace(dstring, "h", ",h,");
        dstring = String2.replace(dstring, "V", ",V,");
        dstring = String2.replace(dstring, "v", ",v,");
        dstring = String2.replace(dstring, "Q", ",Q,");
        dstring = String2.replace(dstring, "q", ",q,");
        dstring = String2.replace(dstring, "T", ",T,");
        dstring = String2.replace(dstring, "t", ",t,");
        // Adobe includes no delimiter before negative numbers
        dstring = String2.replace(dstring, "-", ",-");
        // get rid of any dup commas we might have introduced
        dstring = String2.replace(dstring, ",,", ",");
        // get rid of spaces
        // (cr/lf's have to be removed before the xml object can be created,
        //  so that is done in xml.onData method)
        dstring = String2.replace(dstring, " ", "");
        dstring = String2.replace(dstring, "\t", "");
 
        return dstring.split(",");       
}

Перед началом скопированной части добавим вызов в методе extractCommands:
Код AS3:

return pathDataToArray(node);

Тестируем, всё в порядке, удаляем ставший ненужным код в методе extractCommands.

Iv 21.03.2008 14:53

Рефакторим extractCommands
 
Вернемся к методу extractCommands.
Он всё еще великоват по размеру и его логика теряется во множестве условных операторов. К тому же метод содержит логически обособленные блоки, которые проще воспринимать отдельными методами.
Для начала попробуем вынести блок if (hasFill).
В строке, следующей за if (hasFill) добавим:
Код AS3:

fill = getFill();

C помощью CTRL+1 создадим метод и скопируем в него содержимое блока if до else. Закомментируем скопированный участок кода и из буфера обмена вставляем код в созданный метод.
Далее по очереди проходим по подсвеченым ошибкам и решаем что делать.
startColor - объявим локально.
node - объявим аргументом функции и передадим node в вызове.
thisColor - объявим локально.
Далее заменим присвоение переменной fill значения на return. Заодно удалим участки else, поскольку они оказываются ненужными. В итоге получаем такой метод:
Код AS3:

private function getFill(node:XMLNode) : Fill {
        // parse for fill color specification
        // if a hex number is specified, startColor will be > 0
        // if a color name is specified, startColor will be 0
        var startColor:Number = getAttribute(node, "fill").indexOf("#") + 1;
        if (startColor == 0) { 
                // name specified instead of color number
                var thisColor:Number = colors[getAttribute(node, "fill")];
                // if (thisColor == undefined) {
                if (isNaN(thisColor)) {
                        return new Fill(0, 0)// set invisible if undefined
                }
                return new Fill(thisColor, 100);
        }
        return new Fill(parseInt(getAttribute(node, "fill").substr(startColor, 6), 16), 100);
}

Тестируем. Работает.

Сносим закомментированный код и этот участок кода превращается во вполне удобоваримый:
Код AS3:

if (hasFill) {
        fill = getFill(node);
} else {
        fill = new Fill(0xffffff, 100);
}


Iv 21.03.2008 15:01

hasStroke
 
На очереди следующий участок - блок if (hasStroke).
Поступаем аналогично предыдущему случаю и получаем такой метод:
Код AS3:

private function getStroke(node:XMLNode) : Stroke {
        // parse for stroke color specification
        var startColor:Number = getAttribute(node, "stroke").indexOf("#") + 1;               
        if (startColor == 0) { 
                // name specified instead of color number
                var thisColor:Number = colors[getAttribute(node, "stroke")];
                // if (thisColor == undefined) {
                if (isNaN(thisColor)) {
                        return new Stroke(0, 0, 0);
                }
                return new Stroke(colors[getAttribute(node, "stroke")], 0, 100);
 
        }
        return new Stroke(parseInt(getAttribute(node, "stroke").substr(startColor, 6), 16), 0, 100);
}

и вот такой вызов:
Код AS3:

// stroke: color, width, alpha
if (hasStroke) {
        stroke = getStroke(node);
} else { // if stroke is undefined, use invisible stroke
        stroke = new Stroke(0, 0, 0);
}


Iv 21.03.2008 15:28

блок hasTransform
 
Блок кода if (hasTransform) аналогичными действиями превращается в метод:
Код AS3:

private function getRotation(node : XMLNode) : Number {
        // parse for rotation specification
        // hasRotate = getAttribute(node, "transform").indexOf("rotate");
        // if (hasRotate > -1) {
        var hasRotate:Boolean = getAttribute(node, "transform").indexOf("rotate") > -1;
        if (hasRotate) {
                var startRotate:Number = getAttribute(node, "transform").indexOf("(");
                var endRotate:Number = getAttribute(node, "transform").indexOf(")");
                return parseInt(getAttribute(node, "transform").substr(startRotate + 1, endRotate - startRotate));
        }
        return 0;
}

А вызов становится таким:
Код AS3:

if (hasTransform) {
        rotation = getRotation(node);
} else {
        rotation = 0;
}

В этом месте я отметил для себя странность: переменная rotation локальна, мы ее присваиваем, но нигде не используем. Перенесем ее объявление ближе к присвоению и всё вместе закомментируем:
Код AS3:

//        var rotation:Number;
//        if (hasTransform) {
//                rotation = getRotation(node);
//        } else {
//                rotation = 0;
//        }

Мы нашли странный участок кода и закомментировали его, тем самым убрав неиспользуемую логику из приложения. Этот участок кода мы обнаружили в процессе рефакторинга. Заметьте, что в логику метода мы не вдавались, мы лишь вынесли разные логические блоки в отдельные методы.
Рефакторинг в этом смысле очень похож на ситуацию, когда мы берем груду запчастей и начинаем их все раскладывать по полочкам, иногда вытирая пыль. В этом процессе нам достаточно в общем виде представлять зачем запчасть нужна, при этом нас совершенно не заботит как именно она делает свое дело.
Процесс рефакторинга не изменяет логику приложения, но, в итоге, дает нам возможность впоследствии сделать это точечно, сосредоточившись на изменении логики небольших методов.
Просто сравните каким был метод extractCommands и каким он стал сейчас. В данный момент его логика прозрачна и понятна. Логика каждого из вынесенных методов не требует семи пядей во лбу и может быть легко изменена или оптимизирована, если такая потребность возникнет.

Iv 21.03.2008 16:51

Необходимость структурных преобразований
 
Если вы помните, в самом начале мы определяли цели, к которым стремимся в процессе рефакторинга. Мы решили, что целью будет приведение кода в такой вид, при котором он мог бы стать основой для редактора SVG файлов.
Когда я смотрел статьи и спецификацию SVG формата, я обратил внимание на то, что на самом деле наш проект очень далек даже от базовых возможностей формата.
К примеру, если у нас будет SVG файл, содержащий окружность, заданную в соответствии со спецификацией, наш код не сможет ее отрисовать.
В нашу задачу не входит создавать недостающее, но мы должны сделать всё, для того, чтобы новые классы органично вошли в проект.
То что есть сейчас - отрисовка фигуры произвольной формы или пути. И мы знаем, что могут быть другие фигуры. В этой ситуации логично создать специальный пакет, в котором классы этих фигур смогут жить.
Второй немаловажный момент - необходимость перехода от представления пути к представлению редактируемой фигуры.
Тут понадобятся разъяснения: мы уже понимаем, что происходит: мы получаем строковые данные из атрибута d, разбиваем на конкретные команды в формате svg и преобразуем эти команды в удобоваримые для flash. К сожалению, в некоторых случаях это необратимая операция: кривые Безье третьего порядка, будучи разбитыми на последовательность кривых Безье второго порядка обратно уже не восстановить. А это требуется, поскольку для пользователя условия редактирования объекта не должны ухудшаться.

В итоге, систему нужно переводить на другие рельсы:
- данные формата должны сохраняться и конвертироваться только на этапе отрисовки;
- каждый тип XML узла SVG формата должен иметь соответствующий класс;

Iv 21.03.2008 18:34

К разработке
 
Помечаем в мозгу, что рефакторинг остановлен и мы начинаем заниматься разработкой.

Тактика, которую мы будем использовать такова: создаем классы и методы, которые считаем нужными, и с таким интерфейсом, который считаем удобным. Затем используем то, что нам необходимо из старого проекта, но не тупо переносим, а делаем это по аналогии, и с использованием возможностей AS3.

Начнем со структуры.
В папке svg создадим класс DocumentSVG.
В папке geom создадим папку lines и переместим в нее все классы папки geom, исправим ошибки, возникшие вследствие этого.
Вернемся к стандарту SVG, а именно к его части, описывающей остальные фигуры: http://www.w3.org/TR/SVG11/shapes.html#Introduction
и посмотрим, рисование каких фигур поддерживается:
path, rect, circle, ellipse, line, polyline, polygon.

Создадим папку shapes в папке geom. Создадим базовый класс для всех фигур: ShapeSVG.
И в этой же папке объявим интерфейс IShapeSVG:
Код AS3:

package com.itechnica.svg.geom.shapes {
        import flash.display.Graphics;                       
 
        public interface IShapeSVG {
 
                function update(svgNode : XML) : void;
                function toSVG() : XML;
                function toString() : String;
                function draw(target : Graphics) : void;
 
        }
}

В этой же папке создадим классы соответственно списку: PathShape, RectangleShape, CircleShape, EllipseShape, LineShape, PolyLineShape, PolygonShape. Все их унаследуем от
ShapeSVG и имплементируем интерфейс IShapeSVG.
По-началу все они будут выглядеть как близнеца братья, вот так:
Код AS3:

package com.itechnica.svg.geom.shapes {
        import flash.display.Graphics;
 
        public class CircleShape extends ShapeSVG implements IShapeSVG {
 
                public function update(svgNode : XML) : void {
                }
 
                public function toSVG() : XML {
                        return null;
                }
 
                public function toString() : String {
                        return "";
                }
 
                public function draw(target : Graphics) : void {
                }
        }
}

Теперь, когда структура реализована, нам потребуется тестовый SVG файл, в котором присутствуют все виды применяемых линий и свойств. Текущий пример, который мы использовали больше не удовлетворяет нашим требованиям, поскольку от рефакторинга мы перешли к разработке.

Wave 08.10.2008 12:04

театр одного актера! :)

нескромный вопрос из зала:
как возможен рефакторинг без тестирования?

Iv 08.10.2008 13:24

Цитата:

Сообщение от Wave (Сообщение 768740)
как возможен рефакторинг без тестирования?

- статью ты не читал видимо, поскольку в самом начале я просил сюда не писать, а делать это приватом.
- отсутствие формализованных тестов вовсе не означает, что тестирования нет (ты все-таки статью не читал).
- а во-вторых - можно. Представь себе. Особенно в данном случае, когда исходный AS2 код сам не проходит тестов. Мне что, приводить его в порядок и затем портировать?

Не стоит клинить на вызубреных догмах из учебников. Делать нужно так, как лежит душа и как подсказывает интуиция.

И да, с ответами - в приват.


Часовой пояс GMT +4, время: 16:54.

Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2026, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.