mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
267 lines
7.9 KiB
TypeScript
267 lines
7.9 KiB
TypeScript
export interface Point {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export type Polygon = Point[];
|
|
export type Region = Polygon[];
|
|
export type Quad = [Point, Point, Point, Point];
|
|
|
|
export interface Size {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface ViewportRect {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface ProjectiveEdge {
|
|
sourceViewport: Size;
|
|
destinationQuad: Quad;
|
|
destinationClips?: Polygon[];
|
|
}
|
|
|
|
export interface GeometryProjection {
|
|
sourceClips: Polygon[];
|
|
edges: ProjectiveEdge[];
|
|
topViewport: Size;
|
|
}
|
|
|
|
export function parseCdpQuad(raw: number[] | undefined): Quad | null {
|
|
if (!raw || raw.length !== 8 || !raw.every(Number.isFinite)) return null;
|
|
return [
|
|
{ x: raw[0], y: raw[1] },
|
|
{ x: raw[2], y: raw[3] },
|
|
{ x: raw[4], y: raw[5] },
|
|
{ x: raw[6], y: raw[7] },
|
|
];
|
|
}
|
|
|
|
export function rectPolygon(rect: { x: number; y: number; w: number; h: number }): Quad {
|
|
return [
|
|
{ x: rect.x, y: rect.y },
|
|
{ x: rect.x + rect.w, y: rect.y },
|
|
{ x: rect.x + rect.w, y: rect.y + rect.h },
|
|
{ x: rect.x, y: rect.y + rect.h },
|
|
];
|
|
}
|
|
|
|
export function viewportPolygon(viewport: Size): Quad {
|
|
return rectPolygon({ x: 0, y: 0, w: viewport.width, h: viewport.height });
|
|
}
|
|
|
|
/** Project a normalized unit-square point into an arbitrary content quad. */
|
|
export function projectUnitPoint(point: Point, quad: Quad): Point {
|
|
const [p0, p1, p2, p3] = quad;
|
|
const dx1 = p1.x - p2.x;
|
|
const dx2 = p3.x - p2.x;
|
|
const dx3 = p0.x - p1.x + p2.x - p3.x;
|
|
const dy1 = p1.y - p2.y;
|
|
const dy2 = p3.y - p2.y;
|
|
const dy3 = p0.y - p1.y + p2.y - p3.y;
|
|
|
|
let a: number;
|
|
let b: number;
|
|
let c: number;
|
|
let d: number;
|
|
let e: number;
|
|
let f: number;
|
|
let g = 0;
|
|
let h = 0;
|
|
|
|
if (Math.abs(dx3) < 1e-9 && Math.abs(dy3) < 1e-9) {
|
|
a = p1.x - p0.x;
|
|
b = p3.x - p0.x;
|
|
c = p0.x;
|
|
d = p1.y - p0.y;
|
|
e = p3.y - p0.y;
|
|
f = p0.y;
|
|
} else {
|
|
const denominator = dx1 * dy2 - dx2 * dy1;
|
|
if (Math.abs(denominator) < 1e-9) {
|
|
return {
|
|
x: p0.x + (p1.x - p0.x) * point.x + (p3.x - p0.x) * point.y,
|
|
y: p0.y + (p1.y - p0.y) * point.x + (p3.y - p0.y) * point.y,
|
|
};
|
|
}
|
|
g = (dx3 * dy2 - dx2 * dy3) / denominator;
|
|
h = (dx1 * dy3 - dx3 * dy1) / denominator;
|
|
a = p1.x - p0.x + g * p1.x;
|
|
b = p3.x - p0.x + h * p3.x;
|
|
c = p0.x;
|
|
d = p1.y - p0.y + g * p1.y;
|
|
e = p3.y - p0.y + h * p3.y;
|
|
f = p0.y;
|
|
}
|
|
|
|
const scale = g * point.x + h * point.y + 1;
|
|
return {
|
|
x: (a * point.x + b * point.y + c) / scale,
|
|
y: (d * point.x + e * point.y + f) / scale,
|
|
};
|
|
}
|
|
|
|
export function projectPolygon(polygon: Polygon, edge: ProjectiveEdge): Polygon {
|
|
const { width, height } = edge.sourceViewport;
|
|
if (width <= 0 || height <= 0) return [];
|
|
return polygon.map((point) =>
|
|
projectUnitPoint({ x: point.x / width, y: point.y / height }, edge.destinationQuad),
|
|
);
|
|
}
|
|
|
|
function polygonSignedArea(points: Polygon): number {
|
|
let area = 0;
|
|
for (let index = 0; index < points.length; index += 1) {
|
|
const current = points[index];
|
|
const next = points[(index + 1) % points.length];
|
|
area += current.x * next.y - next.x * current.y;
|
|
}
|
|
return area / 2;
|
|
}
|
|
|
|
function edgeIntersection(start: Point, end: Point, clipStart: Point, clipEnd: Point): Point {
|
|
const subjectX = end.x - start.x;
|
|
const subjectY = end.y - start.y;
|
|
const clipX = clipEnd.x - clipStart.x;
|
|
const clipY = clipEnd.y - clipStart.y;
|
|
const denominator = subjectX * clipY - subjectY * clipX;
|
|
if (Math.abs(denominator) < 1e-9) return end;
|
|
const offsetX = clipStart.x - start.x;
|
|
const offsetY = clipStart.y - start.y;
|
|
const scale = (offsetX * clipY - offsetY * clipX) / denominator;
|
|
return { x: start.x + scale * subjectX, y: start.y + scale * subjectY };
|
|
}
|
|
|
|
/** Clip a polygon to a convex polygon, preserving every resulting vertex. */
|
|
export function clipPolygon(subject: Polygon, clip: Polygon): Polygon {
|
|
if (subject.length < 3 || clip.length < 3) return [];
|
|
const orientation = polygonSignedArea(clip) >= 0 ? 1 : -1;
|
|
let output = [...subject];
|
|
for (let edge = 0; edge < clip.length && output.length > 0; edge += 1) {
|
|
const clipStart = clip[edge];
|
|
const clipEnd = clip[(edge + 1) % clip.length];
|
|
const input = output;
|
|
output = [];
|
|
const inside = (point: Point) =>
|
|
orientation *
|
|
((clipEnd.x - clipStart.x) * (point.y - clipStart.y) -
|
|
(clipEnd.y - clipStart.y) * (point.x - clipStart.x)) >=
|
|
-1e-9;
|
|
let start = input[input.length - 1];
|
|
for (const end of input) {
|
|
const startInside = inside(start);
|
|
const endInside = inside(end);
|
|
if (endInside) {
|
|
if (!startInside) output.push(edgeIntersection(start, end, clipStart, clipEnd));
|
|
output.push(end);
|
|
} else if (startInside) {
|
|
output.push(edgeIntersection(start, end, clipStart, clipEnd));
|
|
}
|
|
start = end;
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
export function polygonBounds(points: Polygon): ViewportRect | null {
|
|
if (points.length < 3) return null;
|
|
const xs = points.map((point) => point.x);
|
|
const ys = points.map((point) => point.y);
|
|
const x = Math.min(...xs);
|
|
const y = Math.min(...ys);
|
|
const right = Math.max(...xs);
|
|
const bottom = Math.max(...ys);
|
|
if (right <= x || bottom <= y) return null;
|
|
return { x, y, width: right - x, height: bottom - y };
|
|
}
|
|
|
|
export function regionBounds(region: Region): ViewportRect | null {
|
|
return polygonBounds(region.flat());
|
|
}
|
|
|
|
export function polygonArea(points: Polygon): number {
|
|
return Math.abs(polygonSignedArea(points));
|
|
}
|
|
|
|
export function polygonCentroid(points: Polygon): Point | null {
|
|
if (points.length < 3) return null;
|
|
let twiceArea = 0;
|
|
let x = 0;
|
|
let y = 0;
|
|
for (let index = 0; index < points.length; index += 1) {
|
|
const current = points[index];
|
|
const next = points[(index + 1) % points.length];
|
|
const cross = current.x * next.y - next.x * current.y;
|
|
twiceArea += cross;
|
|
x += (current.x + next.x) * cross;
|
|
y += (current.y + next.y) * cross;
|
|
}
|
|
if (Math.abs(twiceArea) < 1e-9) return null;
|
|
return { x: x / (3 * twiceArea), y: y / (3 * twiceArea) };
|
|
}
|
|
|
|
export function projectAndClipRegion(
|
|
region: Region,
|
|
edges: ProjectiveEdge[],
|
|
topViewport: Size,
|
|
): Region {
|
|
let projected = region;
|
|
for (const edge of edges) {
|
|
const sourceClip = viewportPolygon(edge.sourceViewport);
|
|
projected = projected
|
|
.map((polygon) => clipPolygon(polygon, sourceClip))
|
|
.filter((polygon) => polygon.length >= 3)
|
|
.map((polygon) =>
|
|
[edge.destinationQuad, ...(edge.destinationClips ?? [])].reduce(
|
|
(current, clip) => clipPolygon(current, clip),
|
|
projectPolygon(polygon, edge),
|
|
),
|
|
)
|
|
.filter((polygon) => polygon.length >= 3);
|
|
}
|
|
const topClip = viewportPolygon(topViewport);
|
|
return projected
|
|
.map((polygon) => clipPolygon(polygon, topClip))
|
|
.filter((polygon) => polygon.length >= 3);
|
|
}
|
|
|
|
export function projectRegionToViewport(region: Region, projection: GeometryProjection): Region {
|
|
const clipped = region
|
|
.map((polygon) =>
|
|
projection.sourceClips.reduce((current, clip) => clipPolygon(current, clip), polygon),
|
|
)
|
|
.filter((polygon) => polygon.length >= 3);
|
|
return projectAndClipRegion(clipped, projection.edges, projection.topViewport);
|
|
}
|
|
|
|
export function projectRectToViewport(
|
|
rect: { x: number; y: number; w: number; h: number } | null,
|
|
projection: GeometryProjection,
|
|
): ViewportRect | null {
|
|
if (!rect) return null;
|
|
return regionBounds(projectRegionToViewport([rectPolygon(rect)], projection));
|
|
}
|
|
|
|
export function childFrameProjection(
|
|
parent: GeometryProjection,
|
|
ownerRectInParent: { x: number; y: number; w: number; h: number },
|
|
): GeometryProjection {
|
|
const destinationQuad = rectPolygon(ownerRectInParent);
|
|
return {
|
|
sourceClips: [],
|
|
edges: [
|
|
{
|
|
sourceViewport: { width: ownerRectInParent.w, height: ownerRectInParent.h },
|
|
destinationQuad,
|
|
destinationClips: parent.sourceClips,
|
|
},
|
|
...parent.edges,
|
|
],
|
|
topViewport: parent.topViewport,
|
|
};
|
|
}
|