Skip to content

Commit

Permalink
Merge pull request #9 from xenharmonic-devs/merge-edges
Browse files Browse the repository at this point in the history
Add the option to merge short edges into long ones
  • Loading branch information
frostburn authored Feb 25, 2024
2 parents d9f5071 + 9b3fc6b commit 618cd88
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 38 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 16 additions & 19 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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'},
]);
});

Expand Down Expand Up @@ -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]
Expand All @@ -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'},
Expand Down Expand Up @@ -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', () => {
Expand Down
129 changes: 110 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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. */
Expand All @@ -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<number>();
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.
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -369,6 +423,10 @@ export function spanLattice(monzos: number[][], options: LatticeOptions) {
}
}

if (options.mergeEdges) {
edges = mergeEdges(edges);
}

return {
vertices,
edges,
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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<number>();
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;
}
}
}
}
Expand Down

0 comments on commit 618cd88

Please sign in to comment.