diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cb6e266 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change log + +## (unreleased) + * Feature: Add the option to merge short edges into long ones to save drawing resources [#8](https://github.com/xenharmonic-devs/ji-lattice/issues/8) + +## 0.0.1 + * Feature: Collect overlapping steps in an equally tempered grid into a list of indices [#6](https://github.com/xenharmonic-devs/ji-lattice/issues/6) + * Feature: Connect vertices separated by Manhattan distance of 2 through an auxiliary vertex + * Bug fix: Support custom edges with auxiliary vertices [#7](https://github.com/xenharmonic-devs/ji-lattice/issues/7) diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 73b732f..181e61b 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -155,6 +155,7 @@ describe("Scott Dakota's PR24 lattice", () => { const options = scottDakota24(); options.edgeMonzos = [[0, -2, -1]]; options.maxDistance = 2; + options.mergeEdges = true; const {vertices, edges} = spanLattice(monzos, options); expect(vertices).toEqual([ {index: 0, x: 0, y: 0}, @@ -167,18 +168,15 @@ describe("Scott Dakota's PR24 lattice", () => { {index: undefined, x: 119, y: 13}, ]); expect(edges).toEqual([ - {x1: 0, y1: 0, x2: 31, y2: 9, type: 'primary'}, - {x1: 0, y1: 0, x2: 26, y2: -14, type: 'primary'}, - {x1: 31, y1: 9, x2: 62, y2: 18, type: 'primary'}, - {x1: 31, y1: 9, x2: 57, y2: -5, type: 'primary'}, - {x1: 62, y1: 18, x2: 93, y2: 27, type: 'primary'}, - {x1: 62, y1: 18, x2: 88, y2: 4, type: 'primary'}, - {x1: 93, y1: 27, x2: 119, y2: 13, type: 'auxiliary'}, - {x1: 26, y1: -14, x2: 57, y2: -5, type: 'primary'}, - {x1: 57, y1: -5, x2: 88, y2: 4, type: 'primary'}, - {x1: 88, y1: 4, x2: 119, y2: 13, type: 'auxiliary'}, - {x1: 0, y1: 0, x2: 88, y2: 4, type: 'custom'}, - {x1: 31, y1: 9, x2: 119, y2: 13, type: 'auxiliary'}, + {x1: 0, x2: 93, y1: 0, y2: 27, type: 'primary'}, + {x1: 0, x2: 26, y1: 0, y2: -14, type: 'primary'}, + {x1: 0, x2: 88, y1: 0, y2: 4, type: 'custom'}, + {x1: 26, x2: 88, y1: -14, y2: 4, type: 'primary'}, + {x1: 31, x2: 57, y1: 9, y2: -5, type: 'primary'}, + {x1: 31, x2: 119, y1: 9, y2: 13, type: 'auxiliary'}, + {x1: 62, x2: 88, y1: 18, y2: 4, type: 'primary'}, + {x1: 88, x2: 119, y1: 4, y2: 13, type: 'auxiliary'}, + {x1: 93, x2: 119, y1: 27, y2: 13, type: 'auxiliary'}, ]); }); @@ -266,6 +264,7 @@ describe('Grid spanner', () => { [0, -1], ], gridLines: {delta1: true, delta2: true}, + mergeEdges: true, }); expect(vertices).toEqual([ {x: -2, y: 2, indices: [1]}, // [x] @@ -278,14 +277,12 @@ describe('Grid spanner', () => { {x: 1, y: 0, indices: [3]}, // Fifth {x: 2, y: 0, indices: [1]}, // Second ]); + expect(edges).toEqual([ - {x1: -2, y1: 2, x2: -1, y2: 2, type: 'custom'}, - {x1: -2, y1: -1, x2: -1, y2: -1, type: 'custom'}, - {x1: -1, y1: 2, x2: 0, y2: 2, type: 'custom'}, - {x1: -1, y1: -1, x2: 0, y2: -1, type: 'custom'}, + {x1: -2, y1: 2, x2: 0, y2: 2, type: 'custom'}, + {x1: -2, y1: -1, x2: 0, y2: -1, type: 'custom'}, + {x1: 0, y1: 0, x2: 2, y2: 0, type: 'custom'}, {x1: 0, y1: 0, x2: 0, y2: -1, type: 'custom'}, - {x1: 0, y1: 0, x2: 1, y2: 0, type: 'custom'}, - {x1: 1, y1: 0, x2: 2, y2: 0, type: 'custom'}, {x1: -3, y1: 2, x2: 3, y2: 2, type: 'gridline'}, {x1: -3, y1: 1, x2: 3, y2: 1, type: 'gridline'}, {x1: -3, y1: -0, x2: 3, y2: 0, type: 'gridline'}, @@ -417,7 +414,7 @@ describe('Grid spanner', () => { const {vertices, edges} = spanGrid(steps, options); expect(vertices.length).toBeLessThanOrEqual(1000); - expect(edges.length).toBeLessThanOrEqual(1000); + expect(edges.length).toBeLessThanOrEqual(2000); }); it('has sane behavior with insane coordinates', () => { diff --git a/src/index.ts b/src/index.ts index 0277e06..186b742 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,8 @@ export type LatticeOptions = { maxDistance?: number; /** Prime-count vectors of connections in addition the the primes. */ edgeMonzos?: number[][]; + /** Flag to merge short edges into a long ones wherever possible. */ + mergeEdges?: boolean; }; /** @@ -122,6 +124,9 @@ export type GridOptions = { /** Options for calculating gridlines. */ gridLines?: GridLineOptions; + /** Flag to merge short edges into a long ones wherever possible. */ + mergeEdges?: boolean; + /** Search range for discovering vertices and edges in view. */ range?: number; /** Maximum number of vertices to return. */ @@ -147,6 +152,55 @@ const SCOTT_DAKOTA_SINE = [ -17, -16, -14, -12, -9, -5 ]; +/** + * Combine edges that share an endpoint and slope into longer ones. + * @param edges Large number of short edges to merge. + * @returns Smaller number of long edges. + */ +export function mergeEdges(edges: Edge[]) { + // Choose a canonical orientation. + const oriented: Edge[] = []; + for (const edge of edges) { + if (edge.x2 < edge.x1 || (edge.x2 === edge.x1 && edge.y2 < edge.y1)) { + oriented.push({ + x1: edge.x2, + y1: edge.y2, + x2: edge.x1, + y2: edge.y1, + type: edge.type, + }); + } else { + oriented.push(edge); + } + } + oriented.sort((a, b) => a.x1 - b.x1 || a.y1 - b.y1); + const result: Edge[] = []; + const spent = new Set(); + for (let i = 0; i < oriented.length; ++i) { + if (spent.has(i)) { + continue; + } + // eslint-disable-next-line prefer-const + let {x1, y1, x2, y2, type} = oriented[i]; + const dx = x2 - x1; + const dy = y2 - y1; + for (let j = i + 1; j < oriented.length; ++j) { + const e = oriented[j]; + if (e.x1 === x2 && e.y1 === y2 && e.type === type) { + const dex = e.x2 - e.x1; + const dey = e.y2 - e.y1; + if (dex * dy === dx * dey) { + x2 = e.x2; + y2 = e.y2; + spent.add(j); + } + } + } + result.push({x1, x2, y1, y2, type}); + } + return result; +} + /** * Calculate the taxicab norm / Manhattan distance between two vectors. * @param a Prime exponents of a musical interval. @@ -317,7 +371,7 @@ export function spanLattice(monzos: number[][], options: LatticeOptions) { const unprojected = unproject(connectingMonzos, options); const vertices: Vertex[] = []; - const edges: Edge[] = []; + let edges: Edge[] = []; for (let index = 0; index < monzos.length; ++index) { vertices.push({ @@ -369,6 +423,10 @@ export function spanLattice(monzos: number[][], options: LatticeOptions) { } } + if (options.mergeEdges) { + edges = mergeEdges(edges); + } + return { vertices, edges, @@ -560,7 +618,7 @@ export function spanGrid(steps: number[], options: GridOptions) { } = options; const range = options.range ?? 100; const maxVertices = options.maxVertices ?? 1000; - const maxEdges = options.maxEdges ?? 1000; + const maxEdges = options.maxEdges ?? 2000; steps = steps.map(s => mmod(s, modulus)); @@ -601,23 +659,56 @@ export function spanGrid(steps: number[], options: GridOptions) { const edges: Edge[] = []; if (edgeVectors) { - let evs = [...edgeVectors]; - evs = evs.concat(evs.map(ev => ev.map(e => -e))); - search: for (let i = 0; i < vertices.length; ++i) { - for (let j = i + 1; j < vertices.length; ++j) { - const dx = vertices[i].x - vertices[j].x; - const dy = vertices[i].y - vertices[j].y; - for (const [vx, vy] of evs) { - if (dx === vx && dy === vy) { - edges.push({ - x1: vertices[i].x, - y1: vertices[i].y, - x2: vertices[j].x, - y2: vertices[j].y, - type: 'custom', - }); - if (edges.length >= maxEdges) { - break search; + if (options.mergeEdges) { + search: for (const [vx, vy] of edgeVectors) { + const spent = new Set(); + for (let i = 0; i < vertices.length; ++i) { + if (spent.has(i)) { + continue; + } + let x1 = vertices[i].x; + let y1 = vertices[i].y; + let x2 = vertices[i].x; + let y2 = vertices[i].y; + for (let j = i + 1; j < vertices.length; ++j) { + if (vertices[j].x - x2 === vx && vertices[j].y - y2 === vy) { + x2 = vertices[j].x; + y2 = vertices[j].y; + spent.add(j); + } + if (x1 - vertices[j].x === vx && y1 - vertices[j].y === vy) { + x1 = vertices[j].x; + y1 = vertices[j].y; + spent.add(j); + } + } + if (x1 !== x2 || y1 !== y2) { + edges.push({x1, y1, x2, y2, type: 'custom'}); + } + if (edges.length >= maxEdges) { + break search; + } + } + } + } else { + let evs = [...edgeVectors]; + evs = evs.concat(evs.map(ev => ev.map(e => -e))); + search: for (let i = 0; i < vertices.length; ++i) { + for (let j = i + 1; j < vertices.length; ++j) { + const dx = vertices[i].x - vertices[j].x; + const dy = vertices[i].y - vertices[j].y; + for (const [vx, vy] of evs) { + if (dx === vx && dy === vy) { + edges.push({ + x1: vertices[i].x, + y1: vertices[i].y, + x2: vertices[j].x, + y2: vertices[j].y, + type: 'custom', + }); + if (edges.length >= maxEdges) { + break search; + } } } }