mirror of
https://github.com/skishore/makemeahanzi.git
synced 2025-11-03 05:48:23 +08:00
Add helper functions to serialize and deserialize paths
This commit is contained in:
@ -1,9 +1,12 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
const fs = maybeRequire('fs');
|
const fs = maybeRequire('fs');
|
||||||
const path = maybeRequire('path');
|
const path = maybeRequire('path');
|
||||||
|
|
||||||
const CHARACTER_FIELDS = ['character', 'decomposition', 'definition',
|
const CHARACTER_FIELDS = ['character', 'decomposition', 'definition',
|
||||||
'kangxi_index', 'pinyin', 'strokes'];
|
'kangxi_index', 'pinyin', 'strokes'];
|
||||||
|
|
||||||
|
assert(this.cjklib === undefined);
|
||||||
this.cjklib = {
|
this.cjklib = {
|
||||||
characters: {},
|
characters: {},
|
||||||
gb2312: {},
|
gb2312: {},
|
||||||
@ -27,7 +30,7 @@ CHARACTER_FIELDS.map((field) => cjklib.characters[field] = {});
|
|||||||
|
|
||||||
// Input: String contents of a cjklib data file.
|
// Input: String contents of a cjklib data file.
|
||||||
// Output: a list of rows, each of which is a list of String columns.
|
// Output: a list of rows, each of which is a list of String columns.
|
||||||
getCJKLibRows = (data) => {
|
const getCJKLibRows = (data) => {
|
||||||
const lines = data.split('\n');
|
const lines = data.split('\n');
|
||||||
return lines.filter((line) => line.length > 0 && line[0] !== '#')
|
return lines.filter((line) => line.length > 0 && line[0] !== '#')
|
||||||
.map((line) => line.split(',').map(
|
.map((line) => line.split(',').map(
|
||||||
@ -36,7 +39,7 @@ getCJKLibRows = (data) => {
|
|||||||
|
|
||||||
// Input: String contents of a Unihan data file.
|
// Input: String contents of a Unihan data file.
|
||||||
// Output: a list of rows, each of which is a list of String columns.
|
// Output: a list of rows, each of which is a list of String columns.
|
||||||
getUnihanRows = (data) => {
|
const getUnihanRows = (data) => {
|
||||||
const lines = data.split('\n');
|
const lines = data.split('\n');
|
||||||
return lines.filter((line) => line.length > 0 && line[0] !== '#')
|
return lines.filter((line) => line.length > 0 && line[0] !== '#')
|
||||||
.map((line) => line.split('\t'));
|
.map((line) => line.split('\t'));
|
||||||
@ -44,11 +47,12 @@ getUnihanRows = (data) => {
|
|||||||
|
|
||||||
// Input: a String of the form 'U+<hex>' representing a Unicode codepoint.
|
// Input: a String of the form 'U+<hex>' representing a Unicode codepoint.
|
||||||
// Output: the character at that codepoint
|
// Output: the character at that codepoint
|
||||||
parseUnicodeStr = (str) => String.fromCodePoint(parseInt(str.substr(2), 16));
|
const parseUnicodeStr =
|
||||||
|
(str) => String.fromCodePoint(parseInt(str.substr(2), 16));
|
||||||
|
|
||||||
// Input: the path to a Unihan data file, starting from the public directory.
|
// Input: the path to a Unihan data file, starting from the public directory.
|
||||||
// Output: Promise that resolves to the String contents of that file.
|
// Output: Promise that resolves to the String contents of that file.
|
||||||
readFile = (filename) => new Promise((resolve, reject) => {
|
const readFile = (filename) => new Promise((resolve, reject) => {
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
const filepath = path.join(process.env.PWD, 'public', filename);
|
const filepath = path.join(process.env.PWD, 'public', filename);
|
||||||
fs.readFile(filepath, 'utf8', (error, data) => {
|
fs.readFile(filepath, 'utf8', (error, data) => {
|
||||||
@ -68,7 +72,7 @@ readFile = (filename) => new Promise((resolve, reject) => {
|
|||||||
// Output: Promise that fills result with a mapping character -> decomposition.
|
// Output: Promise that fills result with a mapping character -> decomposition.
|
||||||
// The decompositions are formatted using Ideographic Description Sequence
|
// The decompositions are formatted using Ideographic Description Sequence
|
||||||
// symbols - see the Unicode standard for more details.
|
// symbols - see the Unicode standard for more details.
|
||||||
fillDecompositions = (decompositions, glyphs, result) => {
|
const fillDecompositions = (decompositions, glyphs, result) => {
|
||||||
return Promise.all([decompositions, glyphs]).then(([rows, glyphs]) => {
|
return Promise.all([decompositions, glyphs]).then(([rows, glyphs]) => {
|
||||||
rows.filter((row) => parseInt(row[2], 10) === (glyphs[row[0]] || 0))
|
rows.filter((row) => parseInt(row[2], 10) === (glyphs[row[0]] || 0))
|
||||||
.map((row) => result[row[0]] = row[1]);
|
.map((row) => result[row[0]] = row[1]);
|
||||||
@ -76,7 +80,7 @@ fillDecompositions = (decompositions, glyphs, result) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output: Promise that fills result with a mapping character -> Pinyin.
|
// Output: Promise that fills result with a mapping character -> Pinyin.
|
||||||
fillDefinitions = (readings, result) => {
|
const fillDefinitions = (readings, result) => {
|
||||||
return readings.then((rows) => {
|
return readings.then((rows) => {
|
||||||
rows.filter((row) => row[1] === 'kDefinition')
|
rows.filter((row) => row[1] === 'kDefinition')
|
||||||
.map((row) => result[parseUnicodeStr(row[0])] = row[2]);
|
.map((row) => result[parseUnicodeStr(row[0])] = row[2]);
|
||||||
@ -85,7 +89,7 @@ fillDefinitions = (readings, result) => {
|
|||||||
|
|
||||||
// Output: Promise that fills result with a mapping character -> Kangxi radical-
|
// Output: Promise that fills result with a mapping character -> Kangxi radical-
|
||||||
// stroke count, which is a pair of integers [radical, extra_strokes].
|
// stroke count, which is a pair of integers [radical, extra_strokes].
|
||||||
fillKangxiIndex = (readings, result) => {
|
const fillKangxiIndex = (readings, result) => {
|
||||||
return readings.then((rows) => {
|
return readings.then((rows) => {
|
||||||
const getIndex = (adotb) => adotb.split('.').map((x) => parseInt(x, 10));
|
const getIndex = (adotb) => adotb.split('.').map((x) => parseInt(x, 10));
|
||||||
rows.filter((row) => row[1] === 'kRSKangXi')
|
rows.filter((row) => row[1] === 'kRSKangXi')
|
||||||
@ -94,7 +98,7 @@ fillKangxiIndex = (readings, result) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output: Promise that fills result with a mapping character -> Pinyin.
|
// Output: Promise that fills result with a mapping character -> Pinyin.
|
||||||
fillPinyin = (readings, result) => {
|
const fillPinyin = (readings, result) => {
|
||||||
return readings.then((rows) => {
|
return readings.then((rows) => {
|
||||||
rows.filter((row) => row[1] === 'kMandarin')
|
rows.filter((row) => row[1] === 'kMandarin')
|
||||||
.map((row) => result[parseUnicodeStr(row[0])] = row[2]);
|
.map((row) => result[parseUnicodeStr(row[0])] = row[2]);
|
||||||
@ -102,7 +106,7 @@ fillPinyin = (readings, result) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output: Promise that fills result with a mapping character -> stroke count.
|
// Output: Promise that fills result with a mapping character -> stroke count.
|
||||||
fillStrokeCounts = (dictionary_like_data, result) => {
|
const fillStrokeCounts = (dictionary_like_data, result) => {
|
||||||
return dictionary_like_data.then((rows) => {
|
return dictionary_like_data.then((rows) => {
|
||||||
rows.filter((row) => row[1] === 'kTotalStrokes')
|
rows.filter((row) => row[1] === 'kTotalStrokes')
|
||||||
.map((row) => result[parseUnicodeStr(row[0])] = parseInt(row[2], 10));
|
.map((row) => result[parseUnicodeStr(row[0])] = parseInt(row[2], 10));
|
||||||
@ -113,7 +117,7 @@ fillStrokeCounts = (dictionary_like_data, result) => {
|
|||||||
// - index_to_radical_map: Map from index -> list of radicals at that index
|
// - index_to_radical_map: Map from index -> list of radicals at that index
|
||||||
// - radical_to_index_map: Map from radical -> index of that radical
|
// - radical_to_index_map: Map from radical -> index of that radical
|
||||||
// - primary_radical: Map from index -> primary radical at that index
|
// - primary_radical: Map from index -> primary radical at that index
|
||||||
fillRadicalData = (locale, radicals, result) => {
|
const fillRadicalData = (locale, radicals, result) => {
|
||||||
return radicals.then((rows) => {
|
return radicals.then((rows) => {
|
||||||
rows.filter((row) => row[3].indexOf(locale) >= 0).map((row) => {
|
rows.filter((row) => row[3].indexOf(locale) >= 0).map((row) => {
|
||||||
if (!result.index_to_radical_map.hasOwnProperty(row[0])) {
|
if (!result.index_to_radical_map.hasOwnProperty(row[0])) {
|
||||||
@ -131,7 +135,8 @@ fillRadicalData = (locale, radicals, result) => {
|
|||||||
// Output: Promise that fills result with a map from Unicode radical-codeblock
|
// Output: Promise that fills result with a map from Unicode radical-codeblock
|
||||||
// character -> equivalent Unicode CJK-codeblock (hopefully, GB2312) character.
|
// character -> equivalent Unicode CJK-codeblock (hopefully, GB2312) character.
|
||||||
// There may be Unicode radical characters without a CJK equivalent.
|
// There may be Unicode radical characters without a CJK equivalent.
|
||||||
fillRadicalToCharacterMap = (locale, radical_equivalent_characters, result) => {
|
const fillRadicalToCharacterMap =
|
||||||
|
(locale, radical_equivalent_characters, result) => {
|
||||||
return radical_equivalent_characters.then((rows) => {
|
return radical_equivalent_characters.then((rows) => {
|
||||||
rows.filter((row) => row[2].indexOf(locale) >= 0)
|
rows.filter((row) => row[2].indexOf(locale) >= 0)
|
||||||
.map((row) => result[row[0]] = row[1]);
|
.map((row) => result[row[0]] = row[1]);
|
||||||
@ -139,7 +144,7 @@ fillRadicalToCharacterMap = (locale, radical_equivalent_characters, result) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Given the data from the GB2312 data file, fills the GB2312 result map.
|
// Given the data from the GB2312 data file, fills the GB2312 result map.
|
||||||
fillGB2312 = (data, result) => {
|
const fillGB2312 = (data, result) => {
|
||||||
Array.from(data).map((character) => {
|
Array.from(data).map((character) => {
|
||||||
if (character === '\n') return;
|
if (character === '\n') return;
|
||||||
assert(character.length === 1);
|
assert(character.length === 1);
|
||||||
@ -152,7 +157,7 @@ fillGB2312 = (data, result) => {
|
|||||||
|
|
||||||
// Given the rows of the locale-character map from the cjklib data, returns a
|
// Given the rows of the locale-character map from the cjklib data, returns a
|
||||||
// mapping from characters to the appropriate glyph in that locale.
|
// mapping from characters to the appropriate glyph in that locale.
|
||||||
parseLocaleGlyphMap = (locale, rows) => {
|
const parseLocaleGlyphMap = (locale, rows) => {
|
||||||
const result = {};
|
const result = {};
|
||||||
rows.filter((row) => row[2].indexOf(locale) >= 0)
|
rows.filter((row) => row[2].indexOf(locale) >= 0)
|
||||||
.map((row) => result[row[0]] = parseInt(row[1], 10));
|
.map((row) => result[row[0]] = parseInt(row[1], 10));
|
||||||
@ -161,7 +166,7 @@ parseLocaleGlyphMap = (locale, rows) => {
|
|||||||
|
|
||||||
// Methods used for final post-processing of the loaded datasets.
|
// Methods used for final post-processing of the loaded datasets.
|
||||||
|
|
||||||
cleanupCJKLibData = () => {
|
const cleanupCJKLibData = () => {
|
||||||
const characters = cjklib.characters;
|
const characters = cjklib.characters;
|
||||||
const radicals = cjklib.radicals;
|
const radicals = cjklib.radicals;
|
||||||
const convert_astral_characters = (x) => x.length === 1 ? x : '?'
|
const convert_astral_characters = (x) => x.length === 1 ? x : '?'
|
||||||
|
|||||||
@ -12,6 +12,7 @@ var EDIT_STROKES = true;
|
|||||||
function change_glyph(method, glyph) {
|
function change_glyph(method, glyph) {
|
||||||
glyph = glyph || Session.get('glyph.data');
|
glyph = glyph || Session.get('glyph.data');
|
||||||
Meteor.call(method, glyph, function(err, data) {
|
Meteor.call(method, glyph, function(err, data) {
|
||||||
|
Session.set('glyph.test', data);
|
||||||
data = fill_glyph_fields(data);
|
data = fill_glyph_fields(data);
|
||||||
Session.set('glyph.data', data);
|
Session.set('glyph.data', data);
|
||||||
if (method === 'save_glyph') {
|
if (method === 'save_glyph') {
|
||||||
|
|||||||
@ -5,49 +5,6 @@ var MIN_CORNER_ANGLE = 0.1*Math.PI;
|
|||||||
var MIN_CORNER_TANGENT_DISTANCE = 4;
|
var MIN_CORNER_TANGENT_DISTANCE = 4;
|
||||||
var REVERSAL_PENALTY = 0.5;
|
var REVERSAL_PENALTY = 0.5;
|
||||||
|
|
||||||
// Helper methods for use with angles, which are floats in [-pi, pi).
|
|
||||||
var Angle = {
|
|
||||||
subtract: function(angle1, angle2) {
|
|
||||||
var result = angle1 - angle2;
|
|
||||||
if (result < -Math.PI) {
|
|
||||||
result += 2*Math.PI;
|
|
||||||
}
|
|
||||||
if (result >= Math.PI) {
|
|
||||||
result -= 2*Math.PI;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
penalty: function(diff) {
|
|
||||||
return diff*diff;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper methods for use with "points", which are pairs of integers.
|
|
||||||
var Point = {
|
|
||||||
angle: function(point) {
|
|
||||||
return Math.atan2(point[1], point[0]);
|
|
||||||
},
|
|
||||||
clone: function(point) {
|
|
||||||
return [point[0], point[1]];
|
|
||||||
},
|
|
||||||
distance2: function(point1, point2) {
|
|
||||||
var diff = Point.subtract(point1, point2);
|
|
||||||
return Math.pow(diff[0], 2) + Math.pow(diff[1], 2);
|
|
||||||
},
|
|
||||||
equal: function(point1, point2) {
|
|
||||||
return point1[0] === point2[0] && point1[1] === point2[1];
|
|
||||||
},
|
|
||||||
key: function(point) {
|
|
||||||
return point.join(',');
|
|
||||||
},
|
|
||||||
subtract: function(point1, point2) {
|
|
||||||
return [point1[0] - point2[0], point1[1] - point2[1]];
|
|
||||||
},
|
|
||||||
valid: function(point) {
|
|
||||||
return point[0] !== undefined && point[1] !== undefined;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Takes a non-empty list of SVG commands that may contain multiple contours.
|
// Takes a non-empty list of SVG commands that may contain multiple contours.
|
||||||
// Returns a list of lists of path segment objects that each form one contour.
|
// Returns a list of lists of path segment objects that each form one contour.
|
||||||
// Each path segment has three keys: start, end, and control.
|
// Each path segment has three keys: start, end, and control.
|
||||||
|
|||||||
128
lib/svg.js
Normal file
128
lib/svg.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
if (this.svg !== undefined) throw new Error('Redefining svg global!');
|
||||||
|
this.svg = {};
|
||||||
|
|
||||||
|
// A normal-form SVG path is a path data string with the following properties:
|
||||||
|
// - Every command in the path is in ['L', 'M', 'Q', 'Z'].
|
||||||
|
// - Adjacent tokens in the path are separated by exactly one space.
|
||||||
|
// - There is exactly one 'Z', and it is the last command.
|
||||||
|
//
|
||||||
|
// A segment is a section of a path, represented as an object that has a start,
|
||||||
|
// an end, and possibly a control, all of which are valid Points (that is, pairs
|
||||||
|
// of Numbers).
|
||||||
|
|
||||||
|
// Takes a normal-form SVG path and returns a list of lists of segments on it.
|
||||||
|
const splitPath = (path) => {
|
||||||
|
assert(path.length > 0);
|
||||||
|
assert(path[0] === 'M', `Path did not start with M: ${path}`);
|
||||||
|
assert(path[path.length - 1] === 'Z', `Path did not end with Z: ${path}`);
|
||||||
|
const terms = path.split(' ');
|
||||||
|
const result = [];
|
||||||
|
let start = undefined;
|
||||||
|
let current = undefined;
|
||||||
|
for (let i = 0; i < terms.length; i++) {
|
||||||
|
const command = terms[i];
|
||||||
|
assert(command.length > 0, `Path includes empty command: ${path}`);
|
||||||
|
assert('LMQZ'.indexOf(command) >= 0, command);
|
||||||
|
if (command === 'M' || command === 'Z') {
|
||||||
|
if (current !== undefined) {
|
||||||
|
assert(Point.equal(current, start), `Path has open contour: ${path}`);
|
||||||
|
assert(result[result.length - 1].length > 0,
|
||||||
|
`Path has empty contour: ${path}`);
|
||||||
|
if (command === 'Z') {
|
||||||
|
assert(i === terms.length - 1, `Path ended early: ${path}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push([]);
|
||||||
|
assert(i < terms.length - 2, `Missing point on path: ${path}`);
|
||||||
|
start = [parseFloat(terms[i + 1], 10), parseFloat(terms[i + 2], 10)];
|
||||||
|
assert(Point.valid(start));
|
||||||
|
i += 2;
|
||||||
|
current = Point.clone(start);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let control = undefined;
|
||||||
|
if (command === 'Q') {
|
||||||
|
assert(i < terms.length - 2, `Missing point on path: ${path}`);
|
||||||
|
control = [parseFloat(terms[i + 1], 10), parseFloat(terms[i + 2], 10)];
|
||||||
|
assert(Point.valid(control));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
assert(i < terms.length - 2, `Missing point on path: ${path}`);
|
||||||
|
const end = [parseFloat(terms[i + 1], 10), parseFloat(terms[i + 2], 10)];
|
||||||
|
assert(Point.valid(end));
|
||||||
|
i += 2;
|
||||||
|
if (control !== undefined &&
|
||||||
|
(Point.equal(control, current) || Point.equal(control, end))) {
|
||||||
|
control = undefined;
|
||||||
|
}
|
||||||
|
result[result.length - 1].push({
|
||||||
|
start: Point.clone(current),
|
||||||
|
control: control,
|
||||||
|
end: end,
|
||||||
|
});
|
||||||
|
current = Point.clone(end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a TrueType font command list (as provided by opentype.js) and returns
|
||||||
|
// a normal-form SVG path as defined above.
|
||||||
|
svg.convertCommandsToPath = (commands) => {
|
||||||
|
const terms = [];
|
||||||
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const command = commands[i];
|
||||||
|
assert('LMQZ'.indexOf(command.type) >= 0, command.type);
|
||||||
|
if (command.type === 'Z') {
|
||||||
|
assert(i === commands.length - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
terms.push(command.type);
|
||||||
|
assert((command.x1 !== undefined) === (command.type === 'Q'));
|
||||||
|
if (command.x1 !== undefined) {
|
||||||
|
terms.push(command.x1);
|
||||||
|
terms.push(command.y1);
|
||||||
|
}
|
||||||
|
assert(command.x !== undefined);
|
||||||
|
terms.push(command.x);
|
||||||
|
terms.push(command.y);
|
||||||
|
}
|
||||||
|
terms.push('Z');
|
||||||
|
return terms.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a normal-form SVG path to a list of list of segments. Each segment
|
||||||
|
// in the segment list has a start, an end, and possibly a control, all of which
|
||||||
|
// are valid Points (that is, pairs of numbers).
|
||||||
|
//
|
||||||
|
// The segment lists obey an orientation constraint: segments on external paths
|
||||||
|
// are oriented clockwise, while those on internal paths are oriented clockwise.
|
||||||
|
svg.convertPathToSegmentLists = (path) => {
|
||||||
|
return splitPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes the given segment lists and returns a normal-form SVG path that
|
||||||
|
// represents those segments.
|
||||||
|
svg.convertSegmentListsToPath = (paths) => {
|
||||||
|
const terms = [];
|
||||||
|
for (let path of paths) {
|
||||||
|
assert(path.length > 0);
|
||||||
|
terms.push('M');
|
||||||
|
terms.push(path[0].start[0]);
|
||||||
|
terms.push(path[0].start[1]);
|
||||||
|
for (let segment of path) {
|
||||||
|
if (segment.control === undefined) {
|
||||||
|
terms.push('L');
|
||||||
|
} else {
|
||||||
|
terms.push('Q');
|
||||||
|
terms.push(segment.control[0]);
|
||||||
|
terms.push(segment.control[1]);
|
||||||
|
}
|
||||||
|
terms.push(segment.end[0]);
|
||||||
|
terms.push(segment.end[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terms.push('Z');
|
||||||
|
return terms.join(' ');
|
||||||
|
}
|
||||||
36
lib/util.js
36
lib/util.js
@ -1,5 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// Prints the message and throws an error if the conditionis false.
|
||||||
this.assert = (condition, message) => {
|
this.assert = (condition, message) => {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
@ -13,6 +14,8 @@ if (Meteor.isServer) {
|
|||||||
Meteor.npmRequire('es6-shim');
|
Meteor.npmRequire('es6-shim');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a list of the unique values in the given array, ordered by their
|
||||||
|
// first appearance in the array.
|
||||||
Array.prototype.unique = function() {
|
Array.prototype.unique = function() {
|
||||||
const result = [];
|
const result = [];
|
||||||
const seen = {};
|
const seen = {};
|
||||||
@ -24,3 +27,36 @@ Array.prototype.unique = function() {
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper methods for use with angles, which are floats in [-pi, pi).
|
||||||
|
assert(this.Angle === undefined, 'Redefining Angle global!');
|
||||||
|
this.Angle = {
|
||||||
|
subtract: (angle1, angle2) => {
|
||||||
|
var result = angle1 - angle2;
|
||||||
|
if (result < -Math.PI) {
|
||||||
|
result += 2*Math.PI;
|
||||||
|
}
|
||||||
|
if (result >= Math.PI) {
|
||||||
|
result -= 2*Math.PI;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
penalty: (diff) => diff*diff,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNumber = (x) => Number.isFinite(x) && !Number.isNaN(x);
|
||||||
|
|
||||||
|
// Helper methods for use with "points", which are pairs of integers.
|
||||||
|
assert(this.Point === undefined, 'Redefining Point global!');
|
||||||
|
this.Point = {
|
||||||
|
angle: (point) => Math.atan2(point[1], point[0]),
|
||||||
|
clone: (point) => [point[0], point[1]],
|
||||||
|
distance2(point1, point2) {
|
||||||
|
var diff = Point.subtract(point1, point2);
|
||||||
|
return Math.pow(diff[0], 2) + Math.pow(diff[1], 2);
|
||||||
|
},
|
||||||
|
equal: (point1, point2) => point1[0] === point2[0] && point1[1] === point2[1],
|
||||||
|
key: (point) => point.join(','),
|
||||||
|
subtract: (point1, point2) => [point1[0] - point2[0], point1[1] - point2[1]],
|
||||||
|
valid: (point) => isNumber(point[0]) && isNumber(point[1]),
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user