/* * Copyright 2010, Google Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @fileoverview Loads SVG files and creates o3djs.gpu2d.Paths from * them, inserting all of the associated geometric nodes under the * passed Transform. *
* This file depends on the O3D APIs, o3djs.base, o3djs.io, o3djs.math,
* o3djs.gpu2d, and the XML for <SCRIPT> parser available from
* http://xmljs.sourceforge.net/ .
*/
/**
* Constructs a new SVGLoader.
* @constructor
*/
function SVGLoader() {
}
/**
* Helper function to defer the execution of another function.
* @param {function(): void} func function to execute later.
* @private
*/
function runLater_(func) {
setTimeout(func, 0);
}
/**
* Loads an SVG file at the given URL. The file is downloaded in the
* background. Graphical objects are allocated in the given Pack, and
* created materials are registered under the given DrawList,
* typically the zOrderedDrawList from an
* o3djs.rendergraph.ViewInfo. The created Shapes are parented under
* the given Transform. If the optional completion callback is
* specified, it is called once the file has been downloaded and
* processed.
* @param {string} url URL of the SVG file to load.
* @param {boolean} flipY Whether the Y coordinates should be flipped
* to better match the 3D coordinate system.
* @param {!o3d.Pack} pack Pack to manage created objects.
* @param {!o3d.DrawList} drawList DrawList to use for created
* materials.
* @param {!o3d.Transform} transform Transform under which to place
* created Shapes.
* @param {function(string, *): void}
* opt_completionCallback Optional completion callback which is
* called after the file has been processed. The URL of the file
* is passed as the first argument. The second argument indicates
* whether the file was loaded successfully (true) or not (false).
* The third argument is an error detail if the file failed to
* load successfully.
*/
SVGLoader.prototype.load = function(url,
flipY,
pack,
drawList,
transform,
opt_completionCallback) {
var that = this;
o3djs.io.loadTextFile(url, function(text, exception) {
if (exception) {
runLater_(function() {
if (opt_completionCallback)
opt_completionCallback(url,
false,
e);
});
} else {
runLater_(function() {
try {
that.parse_(text,
flipY,
pack,
drawList,
transform);
if (opt_completionCallback)
opt_completionCallback(url, true, null);
} catch (e) {
if (window.console)
window.console.log(e);
if (opt_completionCallback)
opt_completionCallback(url, false, e);
}
});
}
});
};
/**
* Does the parsing of the SVG file.
* @param {string} svgText the text of the SVG file.
* @param {boolean} flipY Whether the Y coordinates should be flipped
* to better match the 3D coordinate system.
* @param {!o3d.Pack} pack Pack to manage created objects.
* @param {!o3d.DrawList} drawList DrawList to use for created
* materials.
* @param {!o3d.Transform} transform Transform under which to place
* created Shapes.
* @private
*/
SVGLoader.prototype.parse_ = function(svgText,
flipY,
pack,
drawList,
transform) {
/**
* The pack in which to create shapes, materials, etc.
* @type {!o3d.Pack}
* @private
*/
this.pack_ = pack;
/**
* Stack of matrices. Entering a new graphics context pushes a new
* matrix.
* @type {!Array.}
* @private
*/
this.matrixStack_ = [];
/**
* Stack of transforms. Entering a new graphics context pushes a new
* transform.
* @type {!Array.}
* @private
*/
this.transformStack_ = [];
/**
* The current polygon offset. Each successive path parsed
* increments this.
* @type {number}
* @private
*/
this.polygonOffset_ = 0;
this.matrixStack_.push(o3djs.math.identity(4));
this.transformStack_.push(transform);
var parser = new SAXDriver();
var eventHandler = new SVGSAXHandler_(this,
parser,
flipY,
pack,
drawList);
parser.setDocumentHandler(eventHandler);
parser.parse(svgText);
};
/**
* Returns the current transform.
* @return {!o3d.Transform}
* @private
*/
SVGLoader.prototype.currentTransform_ = function() {
var len = this.transformStack_.length;
return this.transformStack_[len - 1];
};
/**
* Returns the current matrix.
* @return {!o3djs.math.Matrix4}
* @private
*/
SVGLoader.prototype.currentMatrix_ = function() {
var len = this.matrixStack_.length;
return this.matrixStack_[len - 1];
};
/**
* Sets the current matrix.
* @param {!o3djs.math.Matrix4} matrix the new current matrix.
* @private
*/
SVGLoader.prototype.setCurrentMatrix_ = function(matrix) {
var length = this.matrixStack_.length;
this.matrixStack_[length - 1] = matrix;
this.transformStack_[length - 1].localMatrix = matrix;
};
/**
* Pushes a new transform / matrix pair.
* @private
*/
SVGLoader.prototype.pushTransform_ = function() {
this.matrixStack_.push(o3djs.math.identity(4));
var xform = this.pack_.createObject('o3d.Transform');
xform.parent = this.currentTransform_();
this.transformStack_.push(xform);
};
/**
* Pops a transform / matrix pair.
* @private
*/
SVGLoader.prototype.popTransform_ = function() {
this.matrixStack_.pop();
this.transformStack_.pop();
};
/**
* Supports the "matrix" command in the graphics context; not yet
* implemented.
* @param {number} a matrix element
* @param {number} b matrix element
* @param {number} c matrix element
* @param {number} d matrix element
* @param {number} e matrix element
* @param {number} f matrix element
* @private
*/
SVGLoader.prototype.matrix_ = function(a, b, c, d, e, f) {
// TODO(kbr): implement
throw 'matrix command not yet implemented';
};
/**
* Supports the "translate" command in the graphics context.
* @param {number} x x translation
* @param {number} y y translation
* @private
*/
SVGLoader.prototype.translate_ = function(x, y) {
var tmp = o3djs.math.matrix4.translation([x, y, 0]);
this.setCurrentMatrix_(
o3djs.math.mulMatrixMatrix(tmp, this.currentMatrix_()));
};
/**
* Supports the "scale" command in the graphics context; not yet
* implemented.
* @param {number} sx x scale
* @param {number} sy y scale
* @private
*/
SVGLoader.prototype.scale_ = function(sx, sy) {
var tmp = o3djs.math.matrix4.scaling([sx, sy, 1]);
this.setCurrentMatrix_(
o3djs.math.mulMatrixMatrix(tmp, this.currentMatrix_()));
};
/**
* Supports the "rotate" command in the graphics context.
* @param {number} angle angle to rotate, in degrees.
* @param {number} cx x component of rotation center.
* @param {number} cy y component of rotation center.
* @private
*/
SVGLoader.prototype.rotate_ = function(angle, cx, cy) {
var rot = o3djs.math.matrix4.rotationZ(o3djs.math.degToRad(angle));
if (cx || cy) {
var xlate1 = o3djs.math.matrix4.translation([cx, cy, 0]);
var xlate2 = o3djs.math.matrix4.translation([-cx, -cy, 0]);
rot = o3djs.math.mulMatrixMatrix(xlate2, rot);
rot = o3djs.math.mulMatrixMatrix(rot, xlate1);
}
this.setCurrentMatrix_(rot);
};
/**
* Supports the "skewX" command in the graphics context; not yet
* implemented.
* @param {number} angle skew X angle, in degrees.
* @private
*/
SVGLoader.prototype.skewX_ = function(angle) {
// TODO(kbr): implement
throw 'skewX command not yet implemented';
};
/**
* Supports the "skewY" command in the graphics context; not yet
* implemented.
* @param {number} angle skew Y angle, in degrees.
* @private
*/
SVGLoader.prototype.skewY_ = function(angle) {
// TODO(kbr): implement
throw 'skewY command not yet implemented';
};
/**
* Parses the data from an SVG path element, constructing an
* o3djs.gpu2d.Path from it.
* @param {string} pathData the path's data (the "d" attribute).
* @param {number} lineNumber the line number of the current parse,
* for error reporting.
* @param {boolean} flipY Whether the Y coordinates should be flipped
* to better match the 3D coordinate system.
* @param {!o3d.Pack} pack Pack to manage created objects.
* @param {!o3d.DrawList} drawList DrawList to use for created
* materials.
* @private
*/
SVGLoader.prototype.parsePath_ = function(pathData,
lineNumber,
flipY,
pack,
drawList) {
var parser = new PathDataParser_(this,
lineNumber,
flipY,
pack,
drawList);
var path = parser.parse(pathData);
if (this.fill_) {
path.setFill(this.fill_);
}
path.setPolygonOffset(-2 * this.polygonOffset_, -3 * this.polygonOffset_);
++this.polygonOffset_;
this.currentTransform_().addShape(path.shape);
};
/**
* Parses the style from an SVG path element or graphics context,
* preparing to set it on the next created o3djs.gpu2d.Path. If it
* doesn't know how to handle it, the default color of solid black
* will be used.
* @param {string} styleData the string containing the "style"
* attribute.
* @param {number} lineNumber the line number of the current parse,
* for error reporting.
* @param {!o3d.Pack} pack Pack to manage created objects.
* @private
*/
SVGLoader.prototype.parseStyle_ = function(styleData,
lineNumber,
pack) {
this.fill_ = null;
var portions = styleData.split(';');
for (var i = 0; i < portions.length; i++) {
var keyVal = portions[i].split(':');
var key = keyVal[0];
var val = keyVal[1];
if (key == 'stroke') {
// TODO(kbr): support strokes
} else if (key == 'stroke-width') {
// TODO(kbr): support stroke width
} else if (key == 'fill') {
if (val.charAt(0) == '#') {
var r = parseInt(val.substr(1, 2), 16);
var g = parseInt(val.substr(3, 2), 16);
var b = parseInt(val.substr(5, 2), 16);
var fill = o3djs.gpu2d.createColor(pack,
r / 255.0,
g / 255.0,
b / 255.0,
1.0);
this.fill_ = fill;
} else if (val.substr(0, 4) == 'rgb(' &&
val.charAt(val.length - 1) == ')') {
var rgbStrings = val.substr(4, val.length - 5).split(',');
var r = parseInt(rgbStrings[0]);
var g = parseInt(rgbStrings[1]);
var b = parseInt(rgbStrings[2]);
var fill = o3djs.gpu2d.createColor(pack,
r / 255.0,
g / 255.0,
b / 255.0,
1.0);
this.fill_ = fill;
}
}
}
};
/**
* Parses the data from an SVG transform attribute, storing the result
* in the current o3d.Transform.
* @param {string} data the transform's data.
* @param {number} lineNumber the line number of the current parse,
* for error reporting.
* @private
*/
SVGLoader.prototype.parseTransform_ = function(data,
lineNumber) {
var parser = new TransformDataParser_(this,
lineNumber);
parser.parse(data);
};
//----------------------------------------------------------------------
// BaseDataParser -- base class for parsers dealing with SVG data
/**
* Base class for parsers dealing with SVG data.
* @param {SVGLoader} loader The SVG loader.
* @param {number} lineNumber the line number of the current parse,
* for error reporting.
* @constructor
* @private
*/
BaseDataParser_ = function(loader,
lineNumber) {
this.loader_ = loader;
this.lineNumber_ = lineNumber;
this.putBackToken_ = null;
this.singleLetterWords_ = false;
}
/**
* Types of tokens.
* @enum
* @private
*/
BaseDataParser_.TokenTypes = {
WORD: 1,
NUMBER: 2,
LPAREN: 3,
RPAREN: 4,
NONE: 5
};
/**
* Parses the given SVG data.
* @param {string} data the SVG data.
* @private
*/
BaseDataParser_.prototype.parse = function(data) {
var parseState = {
// First index of current token
firstIndex: 0,
// Last index of current token (exclusive)
lastIndex: 0,
// Line number of the path element
lineNumber: this.lineNumber_
};
var done = false;
while (!done) {
var tok = this.nextToken_(parseState, data);
switch (tok.kind) {
case BaseDataParser_.TokenTypes.WORD:
// Allow the parsing to override the specification of the last
// command
this.setLastCommand_(tok.val);
this.parseWord_(parseState, data, tok.val);
break;
case BaseDataParser_.TokenTypes.NUMBER:
// Assume this is a repeat of the last command
this.putBack_(tok);
this.parseWord_(parseState, data, this.lastCommand_);
break;
default:
done = true;
break;
}
}
};
/**
* Sets the last parsed command.
* @param {string} command the last parsed command.
* @private
*/
BaseDataParser_.prototype.setLastCommand_ = function(command) {
this.lastCommand_ = command;
};
/**
* Returns true if the given character is a whitespace or separator.
* @param {string} c the character to test.
* @private
*/
BaseDataParser_.prototype.isWhitespaceOrSeparator_ = function(c) {
return (c == ',' ||
c == ' ' ||
c == '\t' ||
c == '\r' ||
c == '\n');
};
/**
* Puts back a token to be consumed during the next iteration. There
* is only a one-token put back buffer.
* @param {!{kind: BaseDataParser_TokenTypes, val: string}} tok The
* token to put back.
* @private
*/
BaseDataParser_.prototype.putBack_ = function(tok) {
this.putBackToken_ = tok;
};
/**
* Returns the next token.
* @param {!{firstIndex: number, lastIndex: number, lineNumber:
* number}} parseState The parse state.
* @param {string} data The data being parsed.
* @private
*/
BaseDataParser_.prototype.nextToken_ = function(parseState, data) {
if (this.putBackToken_) {
var tmp = this.putBackToken_;
this.putBackToken_ = null;
return tmp;
}
parseState.firstIndex = parseState.lastIndex;
if (parseState.firstIndex < data.length) {
// Eat whitespace and separators
while (true) {
var curChar = data.charAt(parseState.firstIndex);
if (this.isWhitespaceOrSeparator_(curChar)) {
++parseState.firstIndex;
if (parseState.firstIndex >= data.length)
break;
} else {
break;
}
}
}
if (parseState.firstIndex >= data.length)
return { kind: BaseDataParser_.TokenTypes.NONE, val: null };
parseState.lastIndex = parseState.firstIndex;
// Surround the next token
var curChar = data.charAt(parseState.lastIndex++);
if (curChar == '-' ||
curChar == '.' ||
(curChar >= '0' && curChar <= '9')) {
while (true) {
var t = data.charAt(parseState.lastIndex);
if (t == '.' ||
(t >= '0' && t <= '9')) {
++parseState.lastIndex;
} else {
break;
}
}
// See whether an exponential format follows: i.e. 136e-3
if (data.charAt(parseState.lastIndex) == 'e') {
++parseState.lastIndex;
if (data.charAt(parseState.lastIndex) == '-') {
++parseState.lastIndex;
}
while (true) {
var t = data.charAt(parseState.lastIndex);
if (t >= '0' && t <= '9') {
++parseState.lastIndex;
} else {
break;
}
}
}
return { kind: BaseDataParser_.TokenTypes.NUMBER,
val: parseFloat(data.substring(parseState.firstIndex,
parseState.lastIndex)) };
} else if ((curChar >= 'A' && curChar <= 'Z') ||
(curChar >= 'a' && curChar <= 'z')) {
if (!this.singleLetterWords_) {
// Consume all adjacent letters -- this is satisfactory for the
// grammar of the "transform" attribute, but not the "d"
// attribute
while (true) {
var t = data.charAt(parseState.lastIndex);
if ((t >= 'A' && t <= 'Z') ||
(t >= 'a' && t <= 'z')) {
++parseState.lastIndex;
} else {
break;
}
}
}
return { kind: BaseDataParser_.TokenTypes.WORD,
val: data.substring(parseState.firstIndex,
parseState.lastIndex) };
} else if (curChar == '(') {
return { kind: BaseDataParser_.TokenTypes.LPAREN,
val: data.substring(parseState.firstIndex,
parseState.lastIndex) };
} else if (curChar == ')') {
return { kind: BaseDataParser_.TokenTypes.RPAREN,
val: data.substring(parseState.firstIndex,
parseState.lastIndex) };
}
throw 'Expected number or word at line ' + parseState.lineNumber;
};
/**
* Verifies that the next token is of the given kind, throwing an
* exception if not.
* @param {!{firstIndex: number, lastIndex: number, lineNumber:
* number}} parseState The parse state.
* @param {string} data The data being parsed.
* @param {BaseDataParser_TokenTypes} tokenType The expected token
* type.
*/
BaseDataParser_.prototype.expect_ = function(parseState, data, tokenType) {
var tok = this.nextToken_(parseState, data);
if (tok.kind != tokenType) {
throw 'At line number ' + parseState.lineNumber +
': expected token type ' + tokenType +
', got ' + tok.kind;
}
};
/**
* Parses a series of floating-point numbers.
* @param {!{firstIndex: number, lastIndex: number, lineNumber:
* number}} parseState The parse state.
* @param {string} data The data being parsed.
* @param {number} numFloats The number of floating-point numbers to
* parse.
* @return {!Array.