feat: prevent overlapping of blips
This commit is contained in:
committed by
Mathias Schopmans
parent
13591b9672
commit
535c9e8a8f
@@ -6,9 +6,16 @@ import { markedHighlight } from "marked-highlight";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import config from "../next.config.mjs";
|
import config from "../next.config.mjs";
|
||||||
|
import Positioner from "./positioner";
|
||||||
|
|
||||||
|
import { getChartConfig, getQuadrants, getRings } from "@/lib/data";
|
||||||
import { Flag, Item } from "@/lib/types";
|
import { Flag, Item } from "@/lib/types";
|
||||||
|
|
||||||
|
const rings = getRings();
|
||||||
|
const quadrants = getQuadrants();
|
||||||
|
const { size } = getChartConfig();
|
||||||
|
const positioner = new Positioner(size, quadrants, rings);
|
||||||
|
|
||||||
const marked = new Marked(
|
const marked = new Marked(
|
||||||
markedHighlight({
|
markedHighlight({
|
||||||
langPrefix: "hljs language-",
|
langPrefix: "hljs language-",
|
||||||
@@ -72,6 +79,7 @@ async function parseDirectory(dirPath: string): Promise<Item[]> {
|
|||||||
flag: Flag.Default,
|
flag: Flag.Default,
|
||||||
tags: data.tags || [],
|
tags: data.tags || [],
|
||||||
revisions: [],
|
revisions: [],
|
||||||
|
position: [0, 0],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
items[id].release = releaseDate;
|
items[id].release = releaseDate;
|
||||||
@@ -146,8 +154,7 @@ function postProcessItems(items: Item[]): {
|
|||||||
|
|
||||||
const processedItems = items.map((item) => ({
|
const processedItems = items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
// @todo: Maybe we should use a better random number generator to avoid overlapping of blips
|
position: positioner.getNextPosition(item.quadrant, item.ring),
|
||||||
random: [Math.sqrt(Math.random()), Math.random()] as [number, number],
|
|
||||||
flag: getFlag(item, latestRelease),
|
flag: getFlag(item, latestRelease),
|
||||||
// only keep revision which ring or body is different
|
// only keep revision which ring or body is different
|
||||||
revisions: item.revisions
|
revisions: item.revisions
|
||||||
|
|||||||
95
scripts/positioner.ts
Normal file
95
scripts/positioner.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { Quadrant, Ring } from "@/lib/types";
|
||||||
|
|
||||||
|
type Position = [x: number, y: number];
|
||||||
|
type RingDimension = [innerRadius: number, outerRadius: number];
|
||||||
|
|
||||||
|
// Corresponding to positions 1, 2, 3, and 4 respectively
|
||||||
|
const startAngles = [270, 0, 180, 90];
|
||||||
|
|
||||||
|
export default class Positioner {
|
||||||
|
private readonly centerRadius: number;
|
||||||
|
private readonly minDistance: number = 20;
|
||||||
|
private readonly paddingRing: number = 15;
|
||||||
|
private readonly paddingAngle: number = 10;
|
||||||
|
private positions: Record<string, Position[]> = {};
|
||||||
|
private ringDimensions: Record<string, RingDimension> = {};
|
||||||
|
private quadrantAngles: Record<string, number> = {};
|
||||||
|
|
||||||
|
constructor(size: number, quadrants: Quadrant[], rings: Ring[]) {
|
||||||
|
this.centerRadius = size / 2;
|
||||||
|
|
||||||
|
quadrants.forEach((quadrant) => {
|
||||||
|
this.quadrantAngles[quadrant.id] = startAngles[quadrant.position - 1];
|
||||||
|
});
|
||||||
|
|
||||||
|
rings.forEach((ring, index) => {
|
||||||
|
const innerRadius =
|
||||||
|
(rings[index - 1]?.radius ?? 0) * this.centerRadius + this.paddingRing;
|
||||||
|
const outerRadius =
|
||||||
|
(ring.radius ?? 1) * this.centerRadius - this.paddingRing;
|
||||||
|
this.ringDimensions[ring.id] = [innerRadius, outerRadius];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDistance(a: Position, b: Position): number {
|
||||||
|
const [x1, y1] = a;
|
||||||
|
const [x2, y2] = b;
|
||||||
|
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isOverlapping(position: Position, positions: Position[]): boolean {
|
||||||
|
return positions.some(
|
||||||
|
(p) => Positioner.getDistance(position, p) < this.minDistance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getXYPosition(
|
||||||
|
quadrantId: string,
|
||||||
|
ringId: string,
|
||||||
|
radiusFraction: number,
|
||||||
|
angleFraction: number,
|
||||||
|
): Position {
|
||||||
|
const [innerRadius, outerRadius] = this.ringDimensions[ringId];
|
||||||
|
const ringWidth = outerRadius - innerRadius;
|
||||||
|
const absoluteRadius = innerRadius + radiusFraction * ringWidth;
|
||||||
|
|
||||||
|
const startAngle = this.quadrantAngles[quadrantId] + this.paddingAngle;
|
||||||
|
const endAngle = startAngle + 90 - 2 * this.paddingAngle;
|
||||||
|
const absoluteAngle = startAngle + (endAngle - startAngle) * angleFraction;
|
||||||
|
const angleInRadians = ((absoluteAngle - 90) * Math.PI) / 180;
|
||||||
|
|
||||||
|
return [
|
||||||
|
Math.round(this.centerRadius + absoluteRadius * Math.cos(angleInRadians)),
|
||||||
|
Math.round(this.centerRadius + absoluteRadius * Math.sin(angleInRadians)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNextPosition(quadrantId: string, ringId: string): Position {
|
||||||
|
this.positions[quadrantId] ??= [];
|
||||||
|
|
||||||
|
let tries = 0;
|
||||||
|
let position: Position;
|
||||||
|
|
||||||
|
do {
|
||||||
|
position = this.getXYPosition(
|
||||||
|
quadrantId,
|
||||||
|
ringId,
|
||||||
|
Math.sqrt(Math.random()),
|
||||||
|
Math.random(),
|
||||||
|
);
|
||||||
|
tries++;
|
||||||
|
} while (
|
||||||
|
this.isOverlapping(position, this.positions[quadrantId]) &&
|
||||||
|
tries < 150
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tries >= 150) {
|
||||||
|
console.warn(
|
||||||
|
`Could not find a non-overlapping position for ${quadrantId} in ring ${ringId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.positions[quadrantId].push(position);
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,29 +86,8 @@ const _Chart: FC<ChartProps> = ({
|
|||||||
const ring = rings.find((r) => r.id === item.ring);
|
const ring = rings.find((r) => r.id === item.ring);
|
||||||
const quadrant = quadrants.find((q) => q.id === item.quadrant);
|
const quadrant = quadrants.find((q) => q.id === item.quadrant);
|
||||||
if (!ring || !quadrant) return null; // If no ring or quadrant, don't render item
|
if (!ring || !quadrant) return null; // If no ring or quadrant, don't render item
|
||||||
|
const [x, y] = item.position;
|
||||||
|
|
||||||
const padding = 15; // Padding in pixels
|
|
||||||
const paddingAngle = 10; // Padding in degrees
|
|
||||||
|
|
||||||
// Random factors to determine position within the ring
|
|
||||||
const [randomRadius, randomAngleFactor] = item.random || [
|
|
||||||
Math.sqrt(Math.random()),
|
|
||||||
Math.random(),
|
|
||||||
];
|
|
||||||
const innerRadius =
|
|
||||||
(rings[rings.indexOf(ring) - 1]?.radius || 0) + padding / center; // Add inner padding
|
|
||||||
const outerRadius = (ring.radius || 1) - padding / center; // Subtract outer padding
|
|
||||||
const ringWidth = (outerRadius - innerRadius) * center; // Width of the ring in the SVG
|
|
||||||
|
|
||||||
// Calculate the position within the ring
|
|
||||||
const itemRadius = innerRadius * center + randomRadius * ringWidth;
|
|
||||||
// Calculate the angle with padding offset, avoiding the exact edges
|
|
||||||
const startAngle = startAngles[quadrant.position - 1] + paddingAngle;
|
|
||||||
const endAngle = startAngle + 90 - 2 * paddingAngle; // Subtract padding from both sides
|
|
||||||
const itemAngle = startAngle + (endAngle - startAngle) * randomAngleFactor;
|
|
||||||
|
|
||||||
// Convert polar coordinates to cartesian for the item's position
|
|
||||||
const { x, y } = polarToCartesian(itemRadius, itemAngle);
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface Item {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
release: Release;
|
release: Release;
|
||||||
revisions?: Revision[];
|
revisions?: Revision[];
|
||||||
random?: [radius: number, angle: number];
|
position: [x: number, y: number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ring {
|
export interface Ring {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Radar } from "@/components/Radar/Radar";
|
|||||||
import { Tags } from "@/components/Tags/Tags";
|
import { Tags } from "@/components/Tags/Tags";
|
||||||
import {
|
import {
|
||||||
getAppName,
|
getAppName,
|
||||||
|
getChartConfig,
|
||||||
getItems,
|
getItems,
|
||||||
getQuadrants,
|
getQuadrants,
|
||||||
getReleases,
|
getReleases,
|
||||||
@@ -17,6 +18,7 @@ const Home: CustomPage = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const tag = router.query.tag as string | undefined;
|
const tag = router.query.tag as string | undefined;
|
||||||
const appName = getAppName();
|
const appName = getAppName();
|
||||||
|
const chartConfig = getChartConfig();
|
||||||
const version = getReleases().length;
|
const version = getReleases().length;
|
||||||
const rings = getRings();
|
const rings = getRings();
|
||||||
const quadrants = getQuadrants();
|
const quadrants = getQuadrants();
|
||||||
@@ -33,7 +35,12 @@ const Home: CustomPage = () => {
|
|||||||
Version #{version}
|
Version #{version}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Radar quadrants={quadrants} rings={rings} items={items} />
|
<Radar
|
||||||
|
size={chartConfig.size}
|
||||||
|
quadrants={quadrants}
|
||||||
|
rings={rings}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
<Tags tags={tags} activeTag={tag} />
|
<Tags tags={tags} activeTag={tag} />
|
||||||
<QuadrantList items={items} />
|
<QuadrantList items={items} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user