mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 01:15:21 +00:00
creates utility for calculating arc radius and uses it in both factory and sam radiuslayers
This commit is contained in:
@@ -4,8 +4,11 @@ import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import type { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
import {
|
||||
computeUncoveredArcIntervals,
|
||||
Interval,
|
||||
} from "./utils/circleUnion";
|
||||
|
||||
type Interval = [number, number];
|
||||
interface FactoryRadius {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -243,7 +246,7 @@ export class FactoryRadiusLayer implements Layer {
|
||||
private computeCircleUnions() {
|
||||
this.factoryRanges = this.getMyFactoryRanges();
|
||||
for (const circle of this.factoryRanges) {
|
||||
this.computeUncoveredArcIntervals(circle, this.factoryRanges);
|
||||
computeUncoveredArcIntervals(circle, this.factoryRanges);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,11 @@ import type {
|
||||
import { ToggleStructureEvent } from "../../InputHandler";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
import {
|
||||
computeUncoveredArcIntervals,
|
||||
Interval,
|
||||
} from "./utils/circleUnion";
|
||||
|
||||
type Interval = [number, number];
|
||||
interface SAMRadius {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -159,111 +162,6 @@ export class SAMRadiusLayer implements Layer {
|
||||
return radiuses;
|
||||
}
|
||||
|
||||
private computeUncoveredArcIntervals(a: SAMRadius, circles: SAMRadius[]) {
|
||||
a.arcs = [];
|
||||
const TWO_PI = Math.PI * 2;
|
||||
const EPS = 1e-9;
|
||||
// helper functions
|
||||
const normalize = (a: number) => {
|
||||
while (a < 0) a += TWO_PI;
|
||||
while (a >= TWO_PI) a -= TWO_PI;
|
||||
return a;
|
||||
};
|
||||
// merge a list of intervals [s,e] (both between 0..2pi), taking wraparound into account
|
||||
const mergeIntervals = (
|
||||
intervals: Array<[number, number]>,
|
||||
): Array<[number, number]> => {
|
||||
if (intervals.length === 0) return [];
|
||||
// normalize to non-wrap intervals
|
||||
const flat: Array<[number, number]> = [];
|
||||
for (const [s, e] of intervals) {
|
||||
const ns = normalize(s);
|
||||
const ne = normalize(e);
|
||||
if (ne < ns) {
|
||||
// wraps, split
|
||||
flat.push([ns, TWO_PI]);
|
||||
flat.push([0, ne]);
|
||||
} else {
|
||||
flat.push([ns, ne]);
|
||||
}
|
||||
}
|
||||
flat.sort((a, b) => a[0] - b[0]);
|
||||
const merged: Array<[number, number]> = [];
|
||||
let cur = flat[0].slice() as [number, number];
|
||||
for (let i = 1; i < flat.length; i++) {
|
||||
const it = flat[i];
|
||||
if (it[0] <= cur[1] + EPS) {
|
||||
cur[1] = Math.max(cur[1], it[1]);
|
||||
} else {
|
||||
merged.push([cur[0], cur[1]]);
|
||||
cur = it.slice() as [number, number];
|
||||
}
|
||||
}
|
||||
merged.push([cur[0], cur[1]]);
|
||||
return merged;
|
||||
};
|
||||
const covered: Interval[] = [];
|
||||
let fullyCovered = false;
|
||||
|
||||
for (const b of circles) {
|
||||
if (a === b) continue;
|
||||
|
||||
// Only same-owner coverage
|
||||
if (a.owner.smallID() !== b.owner.smallID()) continue;
|
||||
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
|
||||
// a fully inside b
|
||||
if (d + a.r <= b.r + EPS) {
|
||||
fullyCovered = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// no overlap
|
||||
if (d >= a.r + b.r - EPS) continue;
|
||||
|
||||
// coincident centers
|
||||
if (d <= EPS) {
|
||||
if (b.r >= a.r) {
|
||||
fullyCovered = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// angular span on a covered by b
|
||||
const theta = Math.atan2(dy, dx);
|
||||
const cosPhi = (a.r * a.r + d * d - b.r * b.r) / (2 * a.r * d);
|
||||
const phi = Math.acos(Math.max(-1, Math.min(1, cosPhi)));
|
||||
|
||||
covered.push([theta - phi, theta + phi]);
|
||||
}
|
||||
|
||||
if (fullyCovered) return;
|
||||
|
||||
const merged = mergeIntervals(covered);
|
||||
|
||||
// subtract from [0, 2π)
|
||||
const uncovered: Interval[] = [];
|
||||
if (merged.length === 0) {
|
||||
uncovered.push([0, TWO_PI]);
|
||||
} else {
|
||||
let cursor = 0;
|
||||
for (const [s, e] of merged) {
|
||||
if (s > cursor + EPS) {
|
||||
uncovered.push([cursor, s]);
|
||||
}
|
||||
cursor = Math.max(cursor, e);
|
||||
}
|
||||
if (cursor < TWO_PI - EPS) {
|
||||
uncovered.push([cursor, TWO_PI]);
|
||||
}
|
||||
}
|
||||
a.arcs = uncovered;
|
||||
}
|
||||
|
||||
private drawArcSegments(ctx: CanvasRenderingContext2D, a: SAMRadius) {
|
||||
const outlineColor = "rgba(0, 0, 0, 1)";
|
||||
const lineColorSelf = "rgba(0, 255, 0, 1)";
|
||||
@@ -314,7 +212,11 @@ export class SAMRadiusLayer implements Layer {
|
||||
this.samRanges = this.getAllSamRanges();
|
||||
for (let i = 0; i < this.samRanges.length; i++) {
|
||||
const a = this.samRanges[i];
|
||||
this.computeUncoveredArcIntervals(a, this.samRanges);
|
||||
computeUncoveredArcIntervals(
|
||||
a,
|
||||
this.samRanges,
|
||||
(circle, other) => circle.owner.smallID() === other.owner.smallID(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
export type Interval = [number, number];
|
||||
|
||||
export interface CircleLike {
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
arcs: Interval[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute for a given circle which angular segments are NOT covered by other circles.
|
||||
* Mutates `circle.arcs` in place.
|
||||
*
|
||||
* `shouldConsider` allows skipping circles that shouldn't cover `circle`
|
||||
* (e.g., different owners). If it returns false, that other circle is ignored.
|
||||
*/
|
||||
export function computeUncoveredArcIntervals<T extends CircleLike>(
|
||||
circle: T,
|
||||
circles: T[],
|
||||
shouldConsider?: (circle: T, other: T) => boolean,
|
||||
) {
|
||||
circle.arcs = [];
|
||||
const TWO_PI = Math.PI * 2;
|
||||
const EPS = 1e-9;
|
||||
|
||||
const normalize = (a: number) => {
|
||||
while (a < 0) a += TWO_PI;
|
||||
while (a >= TWO_PI) a -= TWO_PI;
|
||||
return a;
|
||||
};
|
||||
|
||||
const mergeIntervals = (
|
||||
intervals: Array<[number, number]>,
|
||||
): Array<[number, number]> => {
|
||||
if (intervals.length === 0) return [];
|
||||
const flat: Array<[number, number]> = [];
|
||||
for (const [s, e] of intervals) {
|
||||
const ns = normalize(s);
|
||||
const ne = normalize(e);
|
||||
if (ne < ns) {
|
||||
flat.push([ns, TWO_PI]);
|
||||
flat.push([0, ne]);
|
||||
} else {
|
||||
flat.push([ns, ne]);
|
||||
}
|
||||
}
|
||||
flat.sort((a, b) => a[0] - b[0]);
|
||||
const merged: Array<[number, number]> = [];
|
||||
let cur = flat[0].slice() as [number, number];
|
||||
for (let i = 1; i < flat.length; i++) {
|
||||
const it = flat[i];
|
||||
if (it[0] <= cur[1] + EPS) {
|
||||
cur[1] = Math.max(cur[1], it[1]);
|
||||
} else {
|
||||
merged.push([cur[0], cur[1]]);
|
||||
cur = it.slice() as [number, number];
|
||||
}
|
||||
}
|
||||
merged.push([cur[0], cur[1]]);
|
||||
return merged;
|
||||
};
|
||||
|
||||
const covered: Interval[] = [];
|
||||
let fullyCovered = false;
|
||||
|
||||
for (const other of circles) {
|
||||
if (circle === other) continue;
|
||||
if (shouldConsider && !shouldConsider(circle, other)) continue;
|
||||
|
||||
const dx = other.x - circle.x;
|
||||
const dy = other.y - circle.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
|
||||
// circle fully inside other
|
||||
if (d + circle.r <= other.r + EPS) {
|
||||
fullyCovered = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// no overlap
|
||||
if (d >= circle.r + other.r - EPS) continue;
|
||||
|
||||
// coincident centers
|
||||
if (d <= EPS) {
|
||||
if (other.r >= circle.r) {
|
||||
fullyCovered = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// angular span on circle covered by other
|
||||
const theta = Math.atan2(dy, dx);
|
||||
const cosPhi =
|
||||
(circle.r * circle.r + d * d - other.r * other.r) / (2 * circle.r * d);
|
||||
const phi = Math.acos(Math.max(-1, Math.min(1, cosPhi)));
|
||||
|
||||
covered.push([theta - phi, theta + phi]);
|
||||
}
|
||||
|
||||
if (fullyCovered) return;
|
||||
|
||||
const merged = mergeIntervals(covered);
|
||||
|
||||
// subtract from [0, 2π)
|
||||
const uncovered: Interval[] = [];
|
||||
if (merged.length === 0) {
|
||||
uncovered.push([0, TWO_PI]);
|
||||
} else {
|
||||
let cursor = 0;
|
||||
for (const [s, e] of merged) {
|
||||
if (s > cursor + EPS) {
|
||||
uncovered.push([cursor, s]);
|
||||
}
|
||||
cursor = Math.max(cursor, e);
|
||||
}
|
||||
if (cursor < TWO_PI - EPS) {
|
||||
uncovered.push([cursor, TWO_PI]);
|
||||
}
|
||||
}
|
||||
circle.arcs = uncovered;
|
||||
}
|
||||
Reference in New Issue
Block a user