diff --git a/client/lib/strokes.js b/client/lib/strokes.js index 07b8cd96..f08cf721 100644 --- a/client/lib/strokes.js +++ b/client/lib/strokes.js @@ -2,7 +2,7 @@ const getStatusLine = (actual, expected) => { const actual_text = `Extracted ${actual} stroke${actual === 1 ? '' : 's'}`; - if (expected === undefined || expected === null) { + if (expected === undefined) { return {cls: 'error', message: `${actual_text}. True number unknown.`}; } else if (actual !== expected) { return {cls: 'error', message: `${actual_text}, but expected ${expected}.`}; diff --git a/lib/stroke_extractor.js b/lib/stroke_extractor.js index eefdaa2a..ff19a031 100644 --- a/lib/stroke_extractor.js +++ b/lib/stroke_extractor.js @@ -1,128 +1,12 @@ "use strict"; -var MAX_BRIDGE_DISTANCE = 64; -var MIN_CORNER_ANGLE = 0.1*Math.PI; -var MIN_CORNER_TANGENT_DISTANCE = 4; -var REVERSAL_PENALTY = 0.5; - -// 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. -// Each path segment has three keys: start, end, and control. -function split_path(path) { - assert(path.length >= 2); - assert(path[0].type === 'M', 'Path did not start with M!'); - assert(path[path.length - 1].type === 'Z', 'Path did not end with Z!'); - - var result = [[]]; - var start = [path[0].x, path[0].y]; - var current = Point.clone(start); - assert(Point.valid(current)); - - for (var i = 1; i < path.length; i++) { - var command = path[i]; - if (command.type === 'M' || command.type === 'Z') { - assert(start.x === current.x && start.y === current.y, 'Open contour!'); - assert(result[result.length -1].length > 0, 'Empty contour!'); - if (command.type === 'Z') { - assert(i === path.length - 1, 'Path ended early!'); - return result; - } - result.push([]); - var start = [command.x, command.y]; - var current = Point.clone(start); - assert(Point.valid(current)); - continue; - } - assert(command.type === 'Q' || command.type === 'L', - 'Got unexpected TTF command: ' + command.type); - var segment = { - 'start': Point.clone(current), - 'end': [command.x, command.y], - 'control': [command.x1, command.y1], - }; - assert(Point.valid(segment.end)); - if (Point.equal(segment.start, segment.end)) { - continue; - } - if (!Point.valid(segment.control) || - Point.equal(segment.start, segment.control) || - Point.equal(segment.end, segment.control)) { - delete segment.control; - } - result[result.length - 1].push(segment); - current = Point.clone(segment.end); - } -} - -// Takes a list of paths. Returns them oriented the way a TTF glyph should be: -// exterior contours counter-clockwise and interior contours clockwise. -function orient_paths(paths) { - for (var i = 0; i < paths.length; i++) { - var contains = 0; - var path = paths[i]; - for (var j = 0; j < paths.length; j++) { - if (j === i) { - continue; - } - contains += path_contains_point(paths[j], path[0].start) ? 1 : 0; - } - var area = get_2x_area(path); - var should_reverse = (area > 0) === (contains % 2 === 1); - if (should_reverse) { - for (var j = 0; j < path.length; j++) { - var ref = [path[j].start, path[j].end]; - path[j].start = ref[1]; - path[j].end = ref[0]; - } - path.reverse(); - } - } - return paths; -} - -// Returns twice the area contained in the path. The result is positive iff the -// path winds in the counter-clockwise direction. -function get_2x_area(path) { - var area = 0; - for (var i = 0; i < path.length; i++) { - var segment = path[i]; - area += (segment.end[0] + segment.start[0])* - (segment.end[1] - segment.start[1]); - } - return area; -} - -// Returns true if the given point is contained inside the given path. -function path_contains_point(path, point) { - var crossings = 0; - var x = point[0]; - var y = point[1]; - for (var i = 0; i < path.length; i++) { - var segment = path[i]; - if ((segment.start[0] < x && x < segment.end[0]) || - (segment.start[0] > x && x > segment.end[0])) { - var t = (x - segment.end[0])/(segment.start[0] - segment.end[0]); - var cy = t*segment.start[1] + (1 - t)*segment.end[1]; - if (y > cy) { - crossings += 1; - } - } else if (segment.start[0] === x && segment.start[1] <= y) { - if (segment.end[0] > x) { - crossings += 1; - } - var last = path[(i + path.length - 1) % (path.length)]; - if (last.start[0] > x) { - crossings += 1; - } - } - } - return crossings % 2 === 1; -} - -// Code for the actual corners-and-bridges algorithm follows. +const MAX_BRIDGE_DISTANCE = 64; +const MIN_CORNER_ANGLE = 0.1*Math.PI; +const MIN_CORNER_TANGENT_DISTANCE = 4; +const REVERSAL_PENALTY = 0.5; // Errors out if the bridges are invalid in some gross way. -function check_bridge(bridge) { +const checkBridge = (bridge) => { assert(Point.valid(bridge[0]) && Point.valid(bridge[1])); assert(!Point.equal(bridge[0], bridge[1])); } @@ -130,27 +14,27 @@ function check_bridge(bridge) { // Returns the list of bridges on the path with the given endpoints. We strip // nearly all of the metadata out of this list to make it easy to hand-correct. // The list that we return is simply a list of pairs of points. -function get_bridges(endpoints, classifier) { - var result = []; - var corners = endpoints.filter(function(x) { return x.corner; }); - var matching = match_corners(corners, classifier); - for (var i = 0; i < corners.length; i++) { - var j = matching[i]; +const getBridges = (endpoints, classifier) => { + const result = []; + const corners = endpoints.filter((x) => x.corner); + const matching = matchCorners(corners, classifier); + for (let i = 0; i < corners.length; i++) { + const j = matching[i]; if (j <= i && matching[j] === i) { continue; } result.push([Point.clone(corners[i].point), Point.clone(corners[j].point)]); } - result.map(check_bridge); + result.map(checkBridge); return result; } // Returns a list of angle and distance features between two corners. -function get_features(ins, out) { - var diff = Point.subtract(out.point, ins.point); - var trivial = Point.equal(diff, [0, 0]); - var angle = Math.atan2(diff[1], diff[0]); - var distance = Math.sqrt(Point.distance2(out.point, ins.point)); +const getFeatures = (ins, out) => { + const diff = Point.subtract(out.point, ins.point); + const trivial = Point.equal(diff, [0, 0]); + const angle = Math.atan2(diff[1], diff[0]); + const distance = Math.sqrt(Point.distance2(out.point, ins.point)); return [ Angle.subtract(angle, ins.angles[0]), Angle.subtract(out.angles[1], angle), @@ -165,12 +49,12 @@ function get_features(ins, out) { // A hand-tuned classifier that uses the features above to return a score for // connecting two corners by a bridge. This classifier throws out most data. -function hand_tuned_classifier(features) { +const handTunedClassifier = (features) => { if (features[6] > 0) { return -Angle.penalty(features[4]); } - var angle_penalty = Angle.penalty(features[0]) + Angle.penalty(features[1]); - var distance_penalty = features[7]; + let angle_penalty = Angle.penalty(features[0]) + Angle.penalty(features[1]); + const distance_penalty = features[7]; if (features[0] > 0 && features[1] > 0 && features[2] + features[3] < -0.5*Math.PI) { angle_penalty = angle_penalty/16; @@ -181,17 +65,17 @@ function hand_tuned_classifier(features) { // Takes a list of corners and returns a bipartite matching between them. // If matching[i] === j, then corners[i] is matched with corners[j] - that is, // we should construct a bridge from corners[i].point to corners[j].point. -function match_corners(corners, classifier) { - var matrix = []; - for (var i = 0; i < corners.length; i++) { +const matchCorners = (corners, classifier) => { + const matrix = []; + for (let i = 0; i < corners.length; i++) { matrix.push([]); - for (var j = 0; j < corners.length; j++) { - matrix[i].push(score_corners(corners[i], corners[j], classifier)); + for (let j = 0; j < corners.length; j++) { + matrix[i].push(scoreCorners(corners[i], corners[j], classifier)); } } - for (var i = 0; i < corners.length; i++) { - for (var j = 0; j < corners.length; j++) { - var reversed_score = matrix[j][i] - REVERSAL_PENALTY; + for (let i = 0; i < corners.length; i++) { + for (let j = 0; j < corners.length; j++) { + const reversed_score = matrix[j][i] - REVERSAL_PENALTY; if (reversed_score > matrix[i][j]) { matrix[i][j] = reversed_score; } @@ -203,8 +87,8 @@ function match_corners(corners, classifier) { // Takes two corners and returns the score assigned to constructing a bridge // from one corner to the other. The score is directed: the bridge from ins to // out may be weighted higher than from out to ins. -function score_corners(ins, out, classifier) { - return classifier(get_features(ins, out)); +const scoreCorners = (ins, out, classifier) => { + return classifier(getFeatures(ins, out)); } // Stores angle and distance metadata around an SVG path segment's start point. @@ -212,8 +96,8 @@ function score_corners(ins, out, classifier) { // the negative (clockwise) direction at that point. function Endpoint(paths, index) { this.index = index; - var path = paths[index[0]]; - var n = path.length; + const path = paths[index[0]]; + const n = path.length; this.indices = [[index[0], (index[1] + n - 1) % n], index]; this.segments = [path[(index[1] + n - 1) % n], path[index[1]]]; this.point = this.segments[0].end; @@ -223,7 +107,7 @@ function Endpoint(paths, index) { Point.subtract(this.segments[0].end, this.segments[0].start), Point.subtract(this.segments[1].end, this.segments[1].start), ]; - var threshold = Math.pow(MIN_CORNER_TANGENT_DISTANCE, 2); + const threshold = Math.pow(MIN_CORNER_TANGENT_DISTANCE, 2); if (this.segments[0].control !== undefined && Point.distance2(this.point, this.segments[0].control) > threshold) { this.tangents[0] = Point.subtract(this.point, this.segments[0].control); @@ -233,14 +117,14 @@ function Endpoint(paths, index) { this.tangents[1] = Point.subtract(this.segments[1].control, this.point); } this.angles = this.tangents.map(Point.angle); - var diff = Angle.subtract(this.angles[1], this.angles[0]); + const diff = Angle.subtract(this.angles[1], this.angles[0]); this.corner = diff < -MIN_CORNER_ANGLE; return this; } // Code for the stroke extraction step follows. -function add_edge_to_adjacency(edge, adjacency) { +const addEdgeToAdjacency = (edge, adjacency) => { assert(edge.length === 2); adjacency[edge[0]] = adjacency[edge[0]] || []; if (adjacency[edge[0]].indexOf(edge[1]) < 0) { @@ -248,71 +132,49 @@ function add_edge_to_adjacency(edge, adjacency) { } } -function get_svg_path(stroke) { - assert(stroke.length > 0); - var terms = ['M', stroke[0].start[0], stroke[0].start[1]]; - for (var i = 0; i < stroke.length; i++) { - var segment = stroke[i]; - 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(' '); -} - -function extract_stroke(paths, endpoint_map, bridge_adjacency, log, - extracted_indices, start, attempt_one) { - var current = start; - var result = []; - var visited = {}; +const extractStroke = (paths, endpoint_map, bridge_adjacency, log, + extracted_indices, start, attempt_one) => { + const result = []; + const visited = {}; + let current = start; // A list of line segments that were added to the path but that were not // part of the original stroke data. None of these should intersect. - var line_segments = []; - var self_intersecting = false; + const line_segments = []; + let self_intersecting = false; - function advance(index) { - return [index[0], (index[1] + 1) % paths[index[0]].length]; - } + const advance = (index) => + [index[0], (index[1] + 1) % paths[index[0]].length]; - function angle(index1, index2) { - var diff = Point.subtract(endpoint_map[Point.key(index2)].point, - endpoint_map[Point.key(index1)].point); + const angle = (index1, index2) => { + const diff = Point.subtract(endpoint_map[Point.key(index2)].point, + endpoint_map[Point.key(index1)].point); assert(diff[0] !== 0 || diff[1] !== 0); - var angle = Math.atan2(diff[1], diff[0]); - return Angle.subtract(angle, endpoint.angles[0]); + const angle = Math.atan2(diff[1], diff[0]); + return Angle.subtract(angle, endpoint.angles[0]); } - function get_intersection(segment1, segment2) { - var diff1 = Point.subtract(segment1[1], segment1[0]); - var diff2 = Point.subtract(segment2[1], segment2[0]); - var cross = diff1[0]*diff2[1] - diff1[1]*diff2[0]; + const getIntersection = (segment1, segment2) => { + const diff1 = Point.subtract(segment1[1], segment1[0]); + const diff2 = Point.subtract(segment2[1], segment2[0]); + const cross = diff1[0]*diff2[1] - diff1[1]*diff2[0]; if (cross === 0) { return undefined; } - var v = Point.subtract(segment1[0], segment2[0]); - var s = (diff1[0]*v[1] - diff1[1]*v[0])/cross; - var t = (diff2[0]*v[1] - diff2[1]*v[0])/cross; + const v = Point.subtract(segment1[0], segment2[0]); + const s = (diff1[0]*v[1] - diff1[1]*v[0])/cross; + const t = (diff2[0]*v[1] - diff2[1]*v[0])/cross; if (0 < s && s < 1 && 0 < t && t < 1) { return [segment1[0][0] + t*diff1[0], segment1[0][1] + t*diff1[1]]; } return undefined; } - function index_to_point(index) { - return endpoint_map[Point.key(index)].point; - } + const indexToPoint = (index) => endpoint_map[Point.key(index)].point; - function push_line_segments(points) { - var old_lines = line_segments.length; - for (var i = 0; i < points.length - 1; i++) { + const pushLineSegments = (points) => { + const old_lines = line_segments.length; + for (let i = 0; i < points.length - 1; i++) { line_segments.push([points[i], points[i + 1]]); result.push({ start: Point.clone(points[i]), @@ -322,9 +184,9 @@ function extract_stroke(paths, endpoint_map, bridge_adjacency, log, } // Log an error if this stroke is self-intersecting. if (!self_intersecting) { - for (var i = 0; i < old_lines; i++) { - for (var j = old_lines; j < line_segments.length; j++) { - if (get_intersection(line_segments[i], line_segments[j])) { + for (let i = 0; i < old_lines; i++) { + for (let j = old_lines; j < line_segments.length; j++) { + if (getIntersection(line_segments[i], line_segments[j])) { self_intersecting = true; return; } @@ -335,41 +197,41 @@ function extract_stroke(paths, endpoint_map, bridge_adjacency, log, // Here there be dragons! // TODO(skishore): Document the point of the geometry in this function. - function select_bridge(endpoint, options) { + const selectBridge = (endpoint, options) => { if (options.length === 1 && extracted_indices[Point.key(options[0])]) { // Handle star-shaped strokes where one stroke ends at the intersection // of the bridges used by two other strokes. - var indices1 = [endpoint.index, options[0]]; - var segment1 = indices1.map(index_to_point); - for (var key in bridge_adjacency) { + const indices1 = [endpoint.index, options[0]]; + const segment1 = indices1.map(indexToPoint); + for (let key in bridge_adjacency) { if (Point.equal(endpoint_map[key].index, indices1[0])) { continue; } - for (var i = 0; i < bridge_adjacency[key].length; i++) { + for (let i = 0; i < bridge_adjacency[key].length; i++) { if (Point.equal(bridge_adjacency[key][i], segment1[0])) { continue; } // Compute the other bridge segment and check if it intersects. - var indices2 = [endpoint_map[key].index, bridge_adjacency[key][i]]; - var segment2 = indices2.map(index_to_point); + const indices2 = [endpoint_map[key].index, bridge_adjacency[key][i]]; + const segment2 = indices2.map(indexToPoint); if (Point.equal(indices2[0], indices1[1]) && !extracted_indices[Point.key(indices2[1])]) { - push_line_segments([segment1[0], segment1[1], segment2[1]]); + pushLineSegments([segment1[0], segment1[1], segment2[1]]); return indices2[1]; } else if (Point.equal(indices2[1], indices1[1]) && !extracted_indices[Point.key(indices2[0])]) { - push_line_segments([segment1[0], segment1[1], segment2[0]]); + pushLineSegments([segment1[0], segment1[1], segment2[0]]); return indices2[0]; } - var intersection = get_intersection(segment1, segment2); + const intersection = getIntersection(segment1, segment2); if (intersection !== undefined) { - var angle1 = angle(indices1[0], indices1[1]); - var angle2 = angle(indices2[0], indices2[1]); + const angle1 = angle(indices1[0], indices1[1]); + const angle2 = angle(indices2[0], indices2[1]); if (Angle.subtract(angle2, angle1) < 0) { indices2.reverse(); segment2.reverse(); } - push_line_segments([segment1[0], intersection, segment2[1]]); + pushLineSegments([segment1[0], intersection, segment2[1]]); return indices2[1]; } } @@ -377,8 +239,8 @@ function extract_stroke(paths, endpoint_map, bridge_adjacency, log, } else { // Handle segments where the correct path is to follow a dead-end bridge, // even if there is another bridge that is more aligned with the stroke. - for (var i = 0; i < options.length; i++) { - var key = Point.key(options[i]); + for (let i = 0; i < options.length; i++) { + const key = Point.key(options[i]); if (!extracted_indices[key]) { return options[i]; } @@ -395,82 +257,80 @@ function extract_stroke(paths, endpoint_map, bridge_adjacency, log, // If there are bridges at the start of the next path segment, follow the // one that makes the largest angle with the current path. The ordering // criterion enforce that we try to cross aligned bridges. - var key = Point.key(current); + const key = Point.key(current); if (bridge_adjacency.hasOwnProperty(key)) { var endpoint = endpoint_map[key]; - var options = bridge_adjacency[key].sort(function(a, b) { - return angle(endpoint.index, a) - angle(endpoint.index, b); - }); - // HACK(skishore): The call to select_bridge may update the result. + const options = bridge_adjacency[key].sort( + (a, b) => angle(endpoint.index, a) - angle(endpoint.index, b)); + // HACK(skishore): The call to selectBridge may update the result. // When a stroke is formed by computing a bridge intersection, then the - // two bridge fragments are added in select_bridge. - var result_length = result.length; - var next = (attempt_one ? options[0] : select_bridge(endpoint, options)); + // two bridge fragments are added in selectBridge. + const result_length = result.length; + const next = (attempt_one ? options[0] : selectBridge(endpoint, options)); if (result.length === result_length) { - push_line_segments( - [endpoint.point, endpoint_map[Point.key(next)].point]); + pushLineSegments([endpoint.point, endpoint_map[Point.key(next)].point]); } current = next; } // Check if we have either closed the loop or hit an extracted segment. - var key = Point.key(current); + const new_key = Point.key(current); if (Point.equal(current, start)) { if (self_intersecting) { - log.push(['error', 'Extracted a self-intersecting stroke.']); + log.push({cls: 'error', + message: 'Extracted a self-intersecting stroke.'}); } - var num_segments_on_path = 0; - for (var index in visited) { + let num_segments_on_path = 0; + for (let index in visited) { extracted_indices[index] = true; num_segments_on_path += 1; } // Single-segment strokes may be due to graphical artifacts in the font. // We drop them to remove these artifacts. if (num_segments_on_path === 1) { - log.push(['success', 'Dropping single-segment stroke.']); + log.push({message: 'Dropping single-segment stroke.'}); return undefined; } return result; - } else if (extracted_indices[key] || visited[key]) { + } else if (extracted_indices[new_key] || visited[new_key]) { return undefined; } } } -function extract_strokes(paths, endpoints, bridges, log) { +const extractStrokes = (paths, endpoints, bridges, log) => { // Build up the necessary hash tables and adjacency lists needed to run the // stroke extraction loop. - var endpoint_map = {}; - var endpoint_position_map = {}; - for (var i = 0; i < endpoints.length; i++) { - var endpoint = endpoints[i]; + const endpoint_map = {}; + const endpoint_position_map = {}; + for (let endpoint of endpoints) { endpoint_map[Point.key(endpoint.index)] = endpoint; endpoint_position_map[Point.key(endpoint.point)] = endpoint; } - bridges.map(check_bridge); - var bridge_adjacency = {}; - for (var i = 0; i < bridges.length; i++) { - var keys = bridges[i].map(Point.key); + bridges.map(checkBridge); + const bridge_adjacency = {}; + for (let bridge of bridges) { + const keys = bridge.map(Point.key); assert(endpoint_position_map.hasOwnProperty(keys[0])); assert(endpoint_position_map.hasOwnProperty(keys[1])); - var xs = keys.map(function(x) { return endpoint_position_map[x].index; }); - add_edge_to_adjacency([Point.key(xs[0]), xs[1]], bridge_adjacency); - add_edge_to_adjacency([Point.key(xs[1]), xs[0]], bridge_adjacency); + const xs = keys.map((x) => endpoint_position_map[x].index); + addEdgeToAdjacency([Point.key(xs[0]), xs[1]], bridge_adjacency); + addEdgeToAdjacency([Point.key(xs[1]), xs[0]], bridge_adjacency); } // Actually extract strokes. Any given path segment index should appear on // exactly one stroke; if it is not on a stroke, we log a warning. - var extracted_indices = {}; - var strokes = []; - for (var attempt = 0; attempt < 3; attempt++) { - var missed = false; + const extracted_indices = {}; + const strokes = []; + for (let attempt = 0; attempt < 3; attempt++) { + let missed = false; for (var i = 0; i < paths.length; i++) { for (var j = 0; j < paths[i].length; j++) { - var index = [i, j]; + const index = [i, j]; if (extracted_indices[Point.key(index)]) { continue; } - var attempt_one = attempt === 0; - var stroke = extract_stroke(paths, endpoint_map, bridge_adjacency, log, - extracted_indices, index, attempt_one); + const attempt_one = attempt === 0; + const stroke = extractStroke(paths, endpoint_map, bridge_adjacency, log, + extracted_indices, index, attempt_one); if (stroke === undefined) { missed = true; continue; @@ -482,105 +342,43 @@ function extract_strokes(paths, endpoints, bridges, log) { return strokes; } } - log.push(['error', 'Stroke extraction missed some path segments.']); + log.push({cls: 'error', + message: 'Stroke extraction missed some path segments.'}); return strokes; } // Exports go below this fold. -this.get_glyph_render_data = function(glyph, manual_bridges, classifier) { - var paths = orient_paths(split_path(glyph.path)); - var endpoints = []; - for (var i = 0; i < paths.length; i++) { - for (var j = 0; j < paths[i].length; j++) { +if (this.stroke_extractor !== undefined) { + throw new Error('Redefining stroke_extractor global!'); +} +this.stroke_extractor = {}; + +this.stroke_extractor.getBridges = (glyph, classifier) => { + assert(glyph.stages.path) + const paths = svg.convertSVGPathToPaths(glyph.stages.path); + const endpoints = []; + for (let i = 0; i < paths.length; i++) { + for (let j = 0; j < paths[i].length; j++) { endpoints.push(new Endpoint(paths, [i, j])); } } - var log = []; - var bridges = get_bridges(endpoints, classifier || hand_tuned_classifier); - var strokes = extract_strokes( - paths, endpoints, manual_bridges || bridges, log); - var character = String.fromCodePoint(parseInt(glyph.name.substr(3), 16)); - var expected = cjklib.getCharacterData(character).strokes; - log.push(['success', 'For character: ' + - String.fromCodePoint(parseInt(glyph.name.substr(3), 16))]); - if (expected === undefined) { - log.push(['success', 'Extracted ' + strokes.length + ' stroke' + - (strokes.length > 1 ? 's' : '') + ' (true value unknown).']); - } else if (strokes.length === expected) { - log.push(['success', 'Extracted ' + strokes.length + ' stroke' + - (strokes.length > 1 ? 's' : '') + '.']); - } else { - log.push(['error', 'Extracted ' + strokes.length + ' stroke' + - (strokes.length > 1 ? 's' : '') + ', but expected ' + - expected + '.']); - } - return { - bridges: bridges, - d: Glyphs.get_svg_path(glyph), - endpoints: endpoints, - strokes: strokes.map(get_svg_path), - log: log, - }; + return getBridges(endpoints, classifier || handTunedClassifier); } -this.check_classifier_on_glyph = function(glyph, classifier) { - assert(glyph.manual.verified); - var render = get_glyph_render_data(glyph, undefined, classifier); - function canonicalize(bridges) { - var result = []; - for (var i = 0; i < bridges.length; i++) { - result.push(Point.key(bridges[i].map(Point.key).sort())); +this.stroke_extractor.getStrokes = (glyph) => { + assert(glyph.stages.path) + assert(glyph.stages.bridges) + const paths = svg.convertSVGPathToPaths(glyph.stages.path); + const endpoints = []; + for (let i = 0; i < paths.length; i++) { + for (let j = 0; j < paths[i].length; j++) { + endpoints.push(new Endpoint(paths, [i, j])); } - return result.sort(); } - return _.isEqual(canonicalize(render.bridges), - canonicalize(glyph.manual.bridges)); -} - -this.get_glyph_training_data = function(glyph) { - assert(glyph.manual.verified); - var paths = orient_paths(split_path(glyph.path)); - var corners = []; - var corner_indices = {}; - var included_in_bridge = []; - for (var i = 0; i < paths.length; i++) { - for (var j = 0; j < paths[i].length; j++) { - var endpoint = new Endpoint(paths, [i, j]); - if (endpoint.corner) { - corner_indices[Point.key(endpoint.point)] = corners.length; - corners.push(endpoint); - included_in_bridge.push(false); - } - } - } - var result = []; - for (var i = 0; i < corners.length; i++) { - for (var j = 0; j < corners.length; j++) { - result.push([get_features(corners[i], corners[j]), 0]); - } - } - for (var i = 0; i < glyph.manual.bridges.length; i++) { - var keys = glyph.manual.bridges[i].map(Point.key); - if (!(corner_indices.hasOwnProperty(keys[0]) && - corner_indices.hasOwnProperty(keys[1]))) { - // One of the bridge endpoints was not even flagged as a corner. - // Continue as if this bridge does not exist. - continue; - } - var indices = [corner_indices[keys[0]], corner_indices[keys[1]]]; - assert(indices[0] !== undefined && indices[1] !== undefined); - result[indices[0] + corners.length*indices[1]][1] = 1; - result[indices[1] + corners.length*indices[0]][1] = 1; - included_in_bridge[indices[0]] = true; - included_in_bridge[indices[1]] = true; - } - // For any corner that was not included in a bridge, assign a high score - // to matching that corner with itself. - for (var i = 0; i < corners.length; i++) { - if (!included_in_bridge[i]) { - result[i + corners.length*i][1] = 1; - } - } - return result; + const log = []; + const stroke_paths = extractStrokes( + paths, endpoints, glyph.stages.bridges, log); + const strokes = stroke_paths.map((x) => svg.convertPathsToSVGPath([x])); + return {log: log, strokes: strokes}; } diff --git a/lib/svg.js b/lib/svg.js index b660399b..e8d34f6b 100644 --- a/lib/svg.js +++ b/lib/svg.js @@ -145,6 +145,9 @@ const splitPath = (path) => { const end = [parseFloat(terms[i + 1], 10), parseFloat(terms[i + 2], 10)]; assert(Point.valid(end)); i += 2; + if (Point.equal(current, end)) { + continue; + } if (control !== undefined && (Point.equal(control, current) || Point.equal(control, end))) { control = undefined; diff --git a/server/migration.js b/server/migration.js index a5b0734f..0f21d57e 100644 --- a/server/migration.js +++ b/server/migration.js @@ -1,5 +1,12 @@ "use strict"; +const checkStrokeExtractorStability = (glyph) => { + const strokes = stroke_extractor.getStrokes(glyph); + if (!_.isEqual(strokes.strokes.sort(), glyph.stages.strokes.sort())) { + console.log(`Different strokes for ${glyph.character}`); + } +} + const completionCallback = undefined; const perGlyphCallback = undefined;