mirror of
https://github.com/skishore/makemeahanzi.git
synced 2025-10-29 09:57:38 +08:00
268 lines
9.0 KiB
JavaScript
268 lines
9.0 KiB
JavaScript
import {getAnimationData} from '/lib/animation';
|
||
import {assert, getPWD, Point} from '/lib/base';
|
||
import {cjklib} from '/lib/cjklib';
|
||
import {Glyphs} from '/lib/glyphs';
|
||
import {fixStrokes} from '/lib/stroke_caps/fixStrokes';
|
||
import {stroke_extractor} from '/lib/stroke_extractor';
|
||
import {svg} from '/lib/svg';
|
||
|
||
const addFrequencyField = (glyph) => {
|
||
const data = cjklib.getCharacterData(glyph.character);
|
||
glyph.metadata.frequency = data.frequency;
|
||
Glyphs.save(glyph);
|
||
}
|
||
|
||
const addSimplifiedAndTraditionalFields = (glyph) => {
|
||
const data = cjklib.getCharacterData(glyph.character);
|
||
glyph.simplified = data.simplified;
|
||
glyph.traditional = data.traditional;
|
||
Glyphs.save(glyph);
|
||
}
|
||
|
||
const addStrokeCaps = (glyph) => {
|
||
const raw = glyph.stages.strokes;
|
||
if (raw.raw || raw.corrected) return;
|
||
glyph.stages.strokes = {corrected: fixStrokes(raw), raw};
|
||
Glyphs.save(glyph);
|
||
}
|
||
|
||
const checkStrokeExtractorStability = (glyph) => {
|
||
const strokes = stroke_extractor.getStrokes(
|
||
glyph.stages.path, glyph.stages.bridges);
|
||
if (!_.isEqual(strokes.strokes.sort(), glyph.stages.strokes.sort())) {
|
||
console.log(`Different strokes for ${glyph.character}`);
|
||
}
|
||
}
|
||
|
||
const convertOldPathSchemaToSVGPath = (path) => {
|
||
const terms = [];
|
||
for (let segment of path) {
|
||
assert('LMQZ'.indexOf(segment.type) >= 0, segment.type);
|
||
terms.push(segment.type);
|
||
if (segment.x1 !== undefined) {
|
||
terms.push(segment.x1);
|
||
terms.push(segment.y1);
|
||
}
|
||
if (segment.x !== undefined) {
|
||
terms.push(segment.x);
|
||
terms.push(segment.y);
|
||
}
|
||
}
|
||
return terms.join(' ');
|
||
}
|
||
|
||
const dumpGlyph = (dictionary, graphics) => (glyph) => {
|
||
if (!glyph.stages.verified) {
|
||
return;
|
||
}
|
||
const analysis = glyph.stages.analysis;
|
||
const order = glyph.stages.order;
|
||
const data = cjklib.getCharacterData(glyph.character);
|
||
const pinyin = (glyph.metadata.pinyin || data.pinyin || '')
|
||
.split(',').map((x) => x.trim()).filter((x) => x);
|
||
const strokes = order.map((x) => glyph.stages.strokes.corrected[x.stroke]);
|
||
const medians = order.map((x) => x.median);
|
||
strokes.map((x) => assert(x));
|
||
medians.map((x) => assert(x));
|
||
const has_etymology =
|
||
analysis.etymology.hint || (analysis.etymology.type === 'pictophonetic');
|
||
|
||
dictionary.write(JSON.stringify({
|
||
character: glyph.character,
|
||
definition: glyph.metadata.definition || data.definition,
|
||
pinyin: pinyin,
|
||
decomposition: analysis.decomposition || '?',
|
||
etymology: has_etymology ? analysis.etymology : undefined,
|
||
radical: analysis.radical,
|
||
matches: order.map((x) => x.match),
|
||
}) + '\n');
|
||
graphics.write(JSON.stringify({
|
||
character: glyph.character,
|
||
strokes: strokes,
|
||
medians: medians,
|
||
}) + '\n');
|
||
}
|
||
|
||
const fixBrokenMedians = (glyph, threshold) => {
|
||
threshold = threshold || 16;
|
||
for (let stroke of glyph.stages.order) {
|
||
const distance = Math.sqrt(Point.distance2(
|
||
stroke.median[0], stroke.median[stroke.median.length - 1]));
|
||
if (distance < threshold) {
|
||
console.log(`Found broken median in ${glyph.character}`);
|
||
const paths = svg.convertSVGPathToPaths(
|
||
glyph.stages.strokes[stroke.stroke]);
|
||
assert(paths.length === 1);
|
||
const polygon = svg.getPolygonApproximation(paths[0], threshold);
|
||
let best_point = null;
|
||
let best_value = -Infinity;
|
||
for (let point of polygon) {
|
||
const value = Point.distance2(point, stroke.median[0])
|
||
if (value > best_value) {
|
||
best_point = point;
|
||
best_value = value;
|
||
}
|
||
}
|
||
assert(best_point !== null);
|
||
stroke.median = [best_point, stroke.median[0]];
|
||
Glyphs.save(glyph);
|
||
}
|
||
}
|
||
}
|
||
|
||
const migrateOldGlyphSchemaToNew = (glyph) => {
|
||
const codepoint = parseInt(glyph.name.substr(3), 16);
|
||
const character = String.fromCodePoint(codepoint);
|
||
const data = cjklib.getCharacterData(character);
|
||
assert(glyph.manual && glyph.manual.verified !== undefined,
|
||
`Glyph ${character} was not verified.`);
|
||
// Pull definition and pinyin from simplified character, if available.
|
||
let definition = undefined;
|
||
let pinyin = undefined;
|
||
if (data.simplified) {
|
||
const simplified = Glyphs.get(data.simplified);
|
||
const metadata = (simplified || {metadata: {}}).metadata;
|
||
const base = cjklib.getCharacterData(data.simplified);
|
||
definition = metadata.definition || base.definition;
|
||
pinyin = metadata.pinyin || base.pinyin;
|
||
}
|
||
const result = {
|
||
character: character,
|
||
codepoint: codepoint,
|
||
metadata: {
|
||
definition: definition,
|
||
frequency: data.frequency,
|
||
kangxi_index: data.kangxi_index,
|
||
pinyin: pinyin,
|
||
strokes: undefined,
|
||
},
|
||
stages: {
|
||
path: convertOldPathSchemaToSVGPath(glyph.path),
|
||
bridges: glyph.manual.bridges,
|
||
strokes: glyph.derived.strokes,
|
||
analysis: undefined,
|
||
order: undefined,
|
||
verified: undefined,
|
||
},
|
||
simplified: data.simplified,
|
||
traditional: data.traditional,
|
||
};
|
||
assert(result.stages.path !== undefined);
|
||
assert(result.stages.bridges !== undefined);
|
||
assert(result.stages.strokes !== undefined);
|
||
return result;
|
||
}
|
||
|
||
// Meteor methods that make use of the migration system follow.
|
||
|
||
const dumpToNewSchemaJSON = () => {
|
||
const fs = Npm.require('fs');
|
||
const path = Npm.require('path');
|
||
const pwd = getPWD();
|
||
const dictionary = fs.createWriteStream(path.join(pwd, 'dictionary.txt'));
|
||
const graphics = fs.createWriteStream(path.join(pwd, 'graphics.txt'));
|
||
runMigration(dumpGlyph(dictionary, graphics), (() => {
|
||
dictionary.end();
|
||
graphics.end();
|
||
}));
|
||
}
|
||
|
||
const exportSVGs = () => {
|
||
const fs = Npm.require('fs');
|
||
const path = Npm.require('path');
|
||
const pwd = getPWD();
|
||
const directory = path.join(pwd, '.svgs');
|
||
fs.mkdirSync(directory);
|
||
runMigration((glyph) => {
|
||
const codepoint = glyph.character.codePointAt(0);
|
||
const medians = glyph.stages.order.map((x) => x.median);
|
||
const strokes = glyph.stages.order.map(
|
||
(x) => glyph.stages.strokes.corrected[x.stroke]);
|
||
const raw = SSR.render('animation', getAnimationData(strokes, medians));
|
||
const svg = raw.replace(/\n /g, '\n').split('\n').slice(1, -2).join('\n');
|
||
fs.writeFileSync(path.join(directory, `${codepoint}.svg`), svg);
|
||
}, () => {});
|
||
}
|
||
|
||
const loadFromOldSchemaJSON = (filename) => {
|
||
const fs = Npm.require('fs');
|
||
const path = Npm.require('path');
|
||
const filepath = path.join(getPWD(), 'public', filename);
|
||
fs.readFile(filepath, 'utf8', Meteor.bindEnvironment((error, data) => {
|
||
if (error) throw error;
|
||
const lines = data.split('\n').filter((x) => x.length > 0);
|
||
console.log(`Loaded ${lines.length} old-schema glyphs.`);
|
||
let migrated = 0;
|
||
let definition = 0;
|
||
let pinyin = 0;
|
||
for (var line of lines) {
|
||
try {
|
||
const old_glyph = JSON.parse(line);
|
||
const new_glyph = migrateOldGlyphSchemaToNew(old_glyph);
|
||
const glyph = Glyphs.get(new_glyph.character);
|
||
if (glyph && glyph.stages.verified) {
|
||
console.log(`Glyph already verified: ${glyph.character}`);
|
||
continue;
|
||
}
|
||
Glyphs.save(new_glyph);
|
||
migrated += 1;
|
||
definition += new_glyph.metadata.definition ? 1 : 0;
|
||
pinyin += new_glyph.metadata.pinyin ? 1 : 0;
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
console.log(`Successfully migrated ${migrated} glyphs.`);
|
||
console.log(`Pulled definitions for ${definition} glyphs.`);
|
||
console.log(`Pulled pinyin for ${pinyin} glyphs.`);
|
||
}));
|
||
}
|
||
|
||
// Runs the given per-glyph callback for each glyph in the database.
|
||
// When all the glyphs are migrated, runs the completion callback.
|
||
const runMigration = (per_glyph_callback, completion_callback) => {
|
||
console.log('Running migration...');
|
||
if (per_glyph_callback) {
|
||
const codepoints =
|
||
Glyphs.find({}, {fields: {codepoint: 1}, sort: {codepoint: 1}}).fetch();
|
||
for (let i = 0; i < codepoints.length; i++) {
|
||
const glyph = Glyphs.findOne({codepoint: codepoints[i].codepoint});
|
||
assert(glyph, 'Glyphs changed during migration!');
|
||
per_glyph_callback(glyph);
|
||
if ((i + 1) % 1000 === 0) {
|
||
console.log(`Migrated ${i + 1} glyphs.`);
|
||
}
|
||
}
|
||
}
|
||
if (completion_callback) {
|
||
completion_callback();
|
||
}
|
||
console.log('Migration complete.');
|
||
}
|
||
|
||
Meteor.methods({
|
||
'export': () => {
|
||
cjklib.promise.then(Meteor.bindEnvironment(dumpToNewSchemaJSON))
|
||
.catch(console.error.bind(console));
|
||
},
|
||
'exportSVGs': exportSVGs,
|
||
'loadFromOldSchemaJSON': (filename) => {
|
||
cjklib.promise.then(
|
||
Meteor.bindEnvironment(() => loadFromOldSchemaJSON(filename)))
|
||
.catch(console.error.bind(console));
|
||
},
|
||
});
|
||
|
||
Meteor.startup(() => {
|
||
SSR.compileTemplate('animation', Assets.getText('animation.html'));
|
||
const completion_callback = undefined;
|
||
const per_glyph_callback = undefined;
|
||
if (!per_glyph_callback && !completion_callback) {
|
||
return;
|
||
}
|
||
console.log('Preparing for migration...');
|
||
const migration = () => runMigration(per_glyph_callback, completion_callback);
|
||
cjklib.promise.then(Meteor.bindEnvironment(migration))
|
||
.catch(console.error.bind(console));
|
||
});
|