практика_отсечение-многоугольников / Наволоцкий_1302
.pdf
Рисунок 9 – построенная структура из открытого файла
21
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
1.Vue.js Введение [Электронный ресурс] // Vue.js. –
URL: https://ru.vuejs.org/guide/introduction (дата обращения: 15.12.2025
г.).
2.PixiJS API Documentation [Электронный ресурс] // PixiJS. –
URL: https://pixijs.download/release/docs/index.html (дата обращения:
15.12.2025 г.).
3.TypeScript Documentation [Электронный ресурс]. –
URL: https://www.typescriptlang.org/docs/ (дата обращения: 15.12.2025
г.).
4.Hidden Surface Removal Using Polygon Area Sorting (Weiler-Atherton Algorithm) [Электронный ресурс] // ACM Digital Library. –
URL: https://dl.acm.org/doi/10.1145/965141.563879 (дата обращения: 15.12.2025 г.).
5.Matrix transformations [Электронный ресурс] // MDN Web Docs. – URL: https://developer.mozilla.org/enUS/docs/Web/API/WebGL_API/Matrix_math_for_the_web (дата обращения: 15.12.2025 г.).
6.Point in polygon (Ray casting algorithm) [Электронный ресурс] // Wikipedia. – URL: https://en.wikipedia.org/wiki/Point_in_polygon (дата обращения: 15.12.2025 г.).
7.File System Access API [Электронный ресурс] // MDN Web Docs. – URL: https://developer.mozilla.org/enUS/docs/Web/API/File_System_Access_API (дата обращения: 15.12.2025
г.).
22
Заключение
В ходе выполнения практической работы был спроектирован и разработан программный комплекс для обработки и визуализации 2D-
геометрии. Основным результатом стала реализация математического ядра,
использующего методы линейной алгебры и аналитической геометрии. В
частности, внедрена система однородных координат и матричных преобразований для выполнения операций переноса и вращения, а для поиска пересечений использован метод Крамера с применением общего уравнения прямой.
Ключевым достижением является программная реализация сложного алгоритма отсечения на основе графов. Данное решение позволяет корректно обрабатывать не только выпуклые, но и невыпуклые многоугольники,
включая сложные топологические случаи распада фигуры на несколько независимых областей при пересечении секущей прямой.
Архитектура приложения построена на базе современного стека технологий с соблюдением принципов MVC, что обеспечило строгое разделение структур хранения данных и логики отображения. Разработанное решение полностью удовлетворяет требованиям технического задания,
обеспечивая создание, редактирование, сохранение сцены в файл и визуализацию результатов геометрических операций в реальном времени.
23
Приложение А
Реализация математического ядра
src/core/math/Geometry.ts
import { Line, Polygon, type Point } from "../types";
export enum PolygonPosition { POSITIVE_SIDE = "POSITIVE", NEGATIVE_SIDE = "NEGATIVE", INTERSECTED = "INTERSECTED",
}
// Тип для отрезка пути (часть полигона или часть прямой) interface PathSegment {
start: Point; end: Point; points: Point[];
type: "chain" | "bridge"; used: boolean;
}
export class Geometry {
static getLineCoefficients(line: Line) { const A = line.end.y - line.start.y; const B = line.start.x - line.end.x;
const C = -A * line.start.x - B * line.start.y; return { A, B, C };
}
static evaluatePoint(point: Point, A: number, B: number, C: number): number
{
const val = A * point.x + B * point.y + C; if (Math.abs(val) < 1e-4) return 0;
return val;
}
static getIntersection(
24
cutLine: { A: number; B: number; C: number }, p1: Point,
p2: Point,
): Point | null {
const A1 = cutLine.A, B1 = cutLine.B,
C1 = cutLine.C;
const A2 = p1.y - p2.y, B2 = p2.x - p1.x,
C2 = -A2 * p1.x - B2 * p1.y; const det = A1 * B2 - A2 * B1;
if (Math.abs(det) < 1e-9) return null; return {
x:(B1 * C2 - B2 * C1) / det,
y:(C1 * A2 - C2 * A1) / det,
};
}
static isPointInPolygon(point: Point, vs: Point[]): boolean { let inside = false;
for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { const xi = vs[i].x,
yi = vs[i].y; const xj = vs[j].x,
yj = vs[j].y; const intersect =
yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
static arePointsEqual(p1: Point, p2: Point): boolean {
return Math.abs(p1.x - p2.x) < 0.01 && Math.abs(p1.y - p2.y) < 0.01;
}
25
static cutPolygon(poly: Polygon, line: Line): { positive: Point[][]; negative: Point[][] } {
const { A, B, C } = this.getLineCoefficients(line); const vertices = poly.vertices;
const intersections: Point[] = [];
const enrichedRing: Point[] = [];
for (let i = 0; i < vertices.length; i++) { const curr = vertices[i];
const next = vertices[(i + 1) % vertices.length]; enrichedRing.push(curr);
const val1 = this.evaluatePoint(curr, A, B, C); const val2 = this.evaluatePoint(next, A, B, C);
if ((val1 > 0 && val2 < 0) || (val1 < 0 && val2 > 0)) {
const inter = this.getIntersection({ A, B, C }, curr, next); if (inter) {
enrichedRing.push(inter);
intersections.push(inter);
}
} else if (val2 === 0) {
}
}
if (intersections.length < 2) {
const isPos = this.evaluatePoint(vertices[0], A, B, C) >= 0; return isPos
? { positive: [vertices], negative: [] } : { positive: [], negative: [vertices] };
}
const buildSide = (keepPositive: boolean): Point[][] => { const segments: PathSegment[] = [];
26
let currentChain: Point[] = []; let isCollecting = false;
let startOffset = 0;
let hasBadPoint = false;
for (let i = 0; i < enrichedRing.length; i++) {
const val = this.evaluatePoint(enrichedRing[i], A, B, C); const isGood = keepPositive ? val >= -1e-4 : val <= 1e-4; if (!isGood) {
startOffset = i; hasBadPoint = true; break;
}
}
if (!hasBadPoint) return [vertices];
for (let i = 0; i < enrichedRing.length; i++) {
const idx = (startOffset + i) % enrichedRing.length;
const p = enrichedRing[idx];
const val = this.evaluatePoint(p, A, B, C);
const isGood = keepPositive ? val >= -1e-4 : val <= 1e-4;
if (isGood) {
if (!isCollecting) { isCollecting = true; currentChain = [p];
} else { currentChain.push(p);
}
}else {
if (isCollecting) {
if (currentChain.length > 0) { segments.push({
start: currentChain[0],
27
end: currentChain[currentChain.length - 1], points: [...currentChain],
type: "chain", used: false,
});
}
currentChain = []; isCollecting = false;
}
}
}
if (isCollecting && currentChain.length > 0) { segments.push({
start: currentChain[0],
end: currentChain[currentChain.length - 1], points: [...currentChain],
type: "chain", used: false,
});
}
const dirX = B; const dirY = -A;
const sortedInters = [...intersections].sort((a, b) => { return a.x * dirX + a.y * dirY - (b.x * dirX + b.y * dirY);
});
const finalPolys: Point[][] = [];
while (true) {
const startSeg = segments.find((s) => !s.used && s.type === "chain"); if (!startSeg) break;
const polyPath: Point[] = []; let currentSeg = startSeg;
28
while (currentSeg) { currentSeg.used = true;
if (
polyPath.length > 0 &&
this.arePointsEqual(polyPath[polyPath.length - 1], currentSeg.points[0])
) { polyPath.push(...currentSeg.points.slice(1));
} else { polyPath.push(...currentSeg.points);
}
const currEnd = currentSeg.end;
const candidates = segments.filter(
(s) => s.type === "chain" && (!s.used || s === startSeg),
);
let bestNextSeg: PathSegment | null = null;
for (const cand of candidates) {
if (this.arePointsEqual(currEnd, cand.start)) continue;
const mid = { x: (currEnd.x + cand.start.x) / 2, y: (currEnd.y + cand.start.y) / 2 };
if (this.isPointInPolygon(mid, vertices)) {
const idx1 = sortedInters.findIndex((p) => this.arePointsEqual(p, currEnd));
const idx2 = sortedInters.findIndex((p) => this.arePointsEqual(p, cand.start));
if (idx1 !== -1 && idx2 !== -1 && Math.abs(idx1 - idx2) === 1)
{
bestNextSeg = cand; break;
}
29
}
}
if (bestNextSeg) {
if (bestNextSeg === startSeg) { finalPolys.push(polyPath); break;
} else {
currentSeg = bestNextSeg;
}
} else { break;
}
}
}
return finalPolys;
};
return {
positive: buildSide(true), negative: buildSide(false),
};
}
// --- Helpers ---
static checkPolygonPosition(poly: Polygon, line: Line): PolygonPosition { const { A, B, C } = this.getLineCoefficients(line);
let hasPos = false, hasNeg = false;
for (const p of poly.vertices) {
const val = this.evaluatePoint(p, A, B, C); if (val > 0) hasPos = true;
if (val < 0) hasNeg = true;
if (hasPos && hasNeg) return PolygonPosition.INTERSECTED;
}
30
