mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
531 lines (471 loc) • 11 kB
JavaScript
/**
* Used to read and write structured text formats.
*/
export default class TokenStream {
/**
* @param {?string} buffer
*/
constructor(buffer) {
this.buffer = buffer || '';
this.index = 0;
this.ident = 0; // Used for writing blocks nicely.
this.fractionDigits = 6; // The number of fraction digits when writing floats.
}
/**
* Reads the next token in the stream.
* Whitespaces are ignored outside of strings in the form of "".
* Comments in the form of // are ignored.
* Commas and colons are ignored as well.
* Curly braces are used as separators, generally to denote text blocks.
*
* For example, given the following string:
*
* Header "A String" {
* Name Value, // A Comment
* }
*
* Read will return the values in order:
*
* Header
* "A String"
* {
* Name
* Value
* }
*
* There are wrappers around read, below, that help to read structured code, check them out!
*
* @return {?string}
*/
read() {
let buffer = this.buffer;
let length = buffer.length;
let inComment = false;
let inString = false;
let token = '';
while (this.index < length) {
let c = buffer[this.index++];
if (inComment) {
if (c === '\n') {
inComment = false;
}
} else if (inString) {
if (c === '\\') {
token += c + buffer[this.index++];
} else if (c === '\n') {
token += '\\n';
} else if (c === '\r') {
token += '\\r';
} else if (c === '"') {
return token;
} else {
token += c;
}
} else if (c === ' ' || c === ',' || c === '\t' || c === '\n' || c === ':' || c === '\r') {
if (token.length) {
return token;
}
} else if (c === '{' || c === '}') {
if (token.length) {
this.index--;
return token;
} else {
return c;
}
} else if (c === '/' && buffer[this.index] === '/') {
if (token.length) {
this.index--;
return token;
} else {
inComment = true;
}
} else if (c === '"') {
if (token.length) {
this.index--;
return token;
} else {
inString = true;
}
} else {
token += c;
}
}
}
/**
* Reads the next token without advancing the stream.
*
* @return {string}
*/
peek() {
let index = this.index;
let value = this.read();
this.index = index;
return value;
}
/**
* Reads the next token, and parses it as an integer.
*
* @return {number}
*/
readInt() {
return parseInt(this.read());
}
/**
* Reads the next token, and parses it as a float.
*
* @return {number}
*/
readFloat() {
return parseFloat(this.read());
}
/**
* Read an MDL keyframe value.
* If the value is a scalar, it us just the number.
* If the value is a vector, it is enclosed with curly braces.
* @param {Float32Array|Uint32Array} value
*/
readKeyframe(value) {
if (value.length === 1) {
if (value instanceof Float32Array) {
value[0] = this.readFloat();
} else {
value[0] = this.readInt();
}
} else {
this.readTypedArray(value);
}
}
/**
* Reads an array of integers in the form:
* { Value1, Value2, ..., ValueN }
*
* @param {ArrayBufferView} view
* @return {ArrayBufferView}
*/
readIntArray(view) {
this.read(); // {
for (let i = 0, l = view.length; i < l; i++) {
view[i] = this.readInt();
}
this.read(); // }
return view;
}
/**
* Reads an array of floats in the form:
* { Value1, Value2, ..., ValueN }
*
* @param {ArrayBufferView} view
* @return {ArrayBufferView}
*/
readFloatArray(view) {
this.read(); // {
for (let i = 0, l = view.length; i < l; i++) {
view[i] = this.readFloat();
}
this.read(); // }
return view;
}
/**
* Reads into a uint or float typed array.
*
* @param {Uint32Array|Float32Array} view
*/
readTypedArray(view) {
if (view instanceof Float32Array) {
this.readFloatArray(view);
} else {
this.readIntArray(view);
}
}
/**
* Reads a color in the form:
*
* { R, G, B }
*
* The color is sizzled to BGR.
*
* @param {Float32Array} view
*/
readColor(view) {
this.read(); // {
view[2] = this.readFloat();
view[1] = this.readFloat();
view[0] = this.readFloat();
this.read(); // }
}
/**
* {
* { Value1, Value2, ..., ValueSize },
* { Value1, Value2, ..., ValueSize },
* ...
* }
*
* @param {ArrayBufferView} view
* @param {number} size
* @return {ArrayBufferView}
*/
readVectorArray(view, size) {
this.read(); // {
for (let i = 0, l = view.length / size; i < l; i++) {
this.read(); // {
for (let j = 0; j < size; j++) {
view[i * size + j] = this.readFloat();
}
this.read(); // }
}
this.read(); // }
return view;
}
/**
* Helper generator for block reading.
* Let's say we have a block like so:
* {
* Key1 Value1
* Key2 Value2
* ...
* KeyN ValueN
* }
* The generator yields the keys one by one, and the caller needs to read the values based on the keys.
* It is used for most MDL blocks.
*/
* readBlock() {
this.read(); // {
let token = this.read();
while (token !== '}') {
yield token;
token = this.read();
}
}
/**
* Writes a color in the form:
*
* { B, G, R }
*
* The color is sizzled to RGB.
*
* @param {string} name 'Color' or 'static Color'.
* @param {Float32Array} view
*/
writeColor(name, view) {
this.writeLine(`${name} { ${view[2]}, ${view[1]}, ${view[0]} },`);
}
/**
* Flag,
*
* @param {string} flag
*/
writeFlag(flag) {
this.writeLine(`${flag},`);
}
/**
* Name Value,
*
* @param {string} name
* @param {number|string} value
*/
writeAttrib(name, value) {
this.writeLine(`${name} ${value},`);
}
/**
* Same as writeAttrib, but formats the given number.
*
* @param {string} name
* @param {number} value
*/
writeFloatAttrib(name, value) {
this.writeLine(`${name} ${this.formatFloat(value)},`);
}
/**
* Name "Value",
*
* @param {string} name
* @param {string} value
*/
writeStringAttrib(name, value) {
this.writeLine(`${name} "${value}",`);
}
/**
* Name { Value0, Value1, ..., ValueN },
*
* @param {string} name
* @param {TypedArray} value
*/
writeArrayAttrib(name, value) {
this.writeLine(`${name} { ${value.join(', ')} },`);
}
/**
* Name { Value0, Value1, ..., ValueN },
*
* @param {string} name
* @param {Float32Array} value
*/
writeFloatArrayAttrib(name, value) {
this.writeLine(`${name} { ${this.formatFloatArray(value)} },`);
}
/**
* @param {string} name
* @param {Uint32Array|Float32Array} value
*/
writeTypedArrayAttrib(name, value) {
if (value instanceof Float32Array) {
this.writeFloatArrayAttrib(name, value);
} else {
this.writeArrayAttrib(name, value);
}
}
/**
* Write an MDL keyframe.
*
* @param {string} start
* @param {Float32Array|Uint32Array} value
*/
writeKeyframe(start, value) {
if (value.length === 1) {
if (value instanceof Float32Array) {
this.writeFloatAttrib(start, value[0]);
} else {
this.writeAttrib(start, value[0]);
}
} else {
this.writeTypedArrayAttrib(start, value);
}
}
/**
* { Value0, Value1, ..., ValueN },
*
* @param {TypedArray} value
*/
writeArray(value) {
this.writeLine(`{ ${value.join(', ')} },`);
}
/**
* { Value0, Value1, ..., ValueN },
*
* @param {Float32Array} value
*/
writeFloatArray(value) {
this.writeLine(`{ ${this.formatFloatArray(value)} },`);
}
/**
* Name Entries {
* { Value1, Value2, ..., valueSize },
* { Value1, Value2, ..., valueSize },
* ...
* }
*
* @param {string} name
* @param {TypedArray} view
* @param {number} size
*/
writeVectorArray(name, view, size) {
this.startBlock(name, view.length / size);
for (let i = 0, l = view.length; i < l; i += size) {
this.writeFloatArray(view.subarray(i, i + size));
}
this.endBlock();
}
/**
* Adds the given string to the buffer.
*
* @param {string} s
*/
write(s) {
this.buffer += s;
}
/**
* Adds the given string to the buffer.
* The current indentation level is prepended, and the stream goes to the next line after the write.
*
* @param {string} line
*/
writeLine(line) {
this.buffer += `${'\t'.repeat(this.ident)}${line}\n`;
}
/**
* Starts a new block in the form:
*
* Header1 Header2 ... HeaderN {
* ...
* }
*
* @param {...string} headers
*/
startBlock(...headers) {
if (headers.length) {
this.writeLine(`${headers.join(' ')} {`);
} else {
this.writeLine('{');
}
this.ident += 1;
}
/**
* Starts a new block in the form:
*
* Header "Name" {
* ...
* }
*
* @param {string} header
* @param {string} name
*/
startObjectBlock(header, name) {
// Turns out you can have quotation marks in object names.
this.writeLine(`${header} "${name.replace(/"/g, '\\"')}" {`);
this.ident += 1;
}
/**
* Ends a previously started block, and handles the indentation.
*/
endBlock() {
this.ident -= 1;
this.writeLine('}');
}
/**
* Ends a previously started block, and handles the indentation.
* Adds a comma after the block end.
*/
endBlockComma() {
this.ident -= 1;
this.writeLine('},');
}
/**
* Increases the indentation level for following line writes.
*/
indent() {
this.ident += 1;
}
/**
* Decreases the indentation level for following line writes.
*/
unindent() {
this.ident -= 1;
}
/**
* Formats a given float to the shorter of either its string representation, or its fixed point representation with the stream's fraction digits.
*
* @param {number} value
* @return {string}
*/
formatFloat(value) {
let s = value.toString();
let f = value.toFixed(this.fractionDigits);
if (s.length > f.length) {
return f;
} else {
return s;
}
}
/**
* Uses formatFloat to format a whole array, and returns it as a comma separated string.
*
* @param {Float32Array} value
* @return {string}
*/
formatFloatArray(value) {
let result = [];
for (let i = 0, l = value.length; i < l; i++) {
result[i] = this.formatFloat(value[i]);
}
return result.join(', ');
}
}