- •1. Постановка задачи
- •Математический аппарат:
- •2. Математическое моделирование
- •2.1. Математические модели геометрических объектов
- •2.7. Определение вхождения точки в многоугольник (Ray Casting)
- •2.8. Сортировка точек на прямой (Скалярное произведение)
- •2.9. Расчет угла поворота (Арктангенс)
- •2.10. Вычисление центроида группы
- •3. Структура и Архитектура приложения
- •3.1. Слой Математического Ядра
- •3.2. Взаимодействие приложения с математикой (Interaction Pipeline)
- •3.3. Абстракция Системы Координат
- •4. Организация данных
- •5. Руководство пользователя
- •Список использованных источников
- •Заключение
- •Приложение а Реализация математического ядра
- •Реализация матричных преобразований
- •Парсинг и сохранение данных
- •Логика отрисовки и взаимодействия
Список использованных источников
Vue.js Введение [Электронный ресурс] // Vue.js. – URL: https://ru.vuejs.org/guide/introduction (дата обращения: 15.12.2025 г.).
PixiJS API Documentation [Электронный ресурс] // PixiJS. – URL: https://pixijs.download/release/docs/index.html (дата обращения: 15.12.2025 г.).
TypeScript Documentation [Электронный ресурс]. – URL: https://www.typescriptlang.org/docs/ (дата обращения: 15.12.2025 г.).
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 г.).
Matrix transformations [Электронный ресурс] // MDN Web Docs. – URL: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web (дата обращения: 15.12.2025 г.).
Point in polygon (Ray casting algorithm) [Электронный ресурс] // Wikipedia. – URL: https://en.wikipedia.org/wiki/Point_in_polygon (дата обращения: 15.12.2025 г.).
File System Access API [Электронный ресурс] // MDN Web Docs. – URL: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API (дата обращения: 15.12.2025 г.).
Заключение
В ходе выполнения практической работы был спроектирован и разработан программный комплекс для обработки и визуализации 2D-геометрии. Основным результатом стала реализация математического ядра, использующего методы линейной алгебры и аналитической геометрии. В частности, внедрена система однородных координат и матричных преобразований для выполнения операций переноса и вращения, а для поиска пересечений использован метод Крамера с применением общего уравнения прямой.
Ключевым достижением является программная реализация сложного алгоритма отсечения на основе графов. Данное решение позволяет корректно обрабатывать не только выпуклые, но и невыпуклые многоугольники, включая сложные топологические случаи распада фигуры на несколько независимых областей при пересечении секущей прямой.
Архитектура приложения построена на базе современного стека технологий с соблюдением принципов MVC, что обеспечило строгое разделение структур хранения данных и логики отображения. Разработанное решение полностью удовлетворяет требованиям технического задания, обеспечивая создание, редактирование, сохранение сцены в файл и визуализацию результатов геометрических операций в реальном времени.
Приложение а Реализация математического ядра
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(
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;
}
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[] = [];
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],
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;
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;
}
}
}
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;
}
return hasPos ? PolygonPosition.POSITIVE_SIDE : PolygonPosition.NEGATIVE_SIDE;
}
static getScreenIntersectionPoints(line: Line, w: number, h: number) {
const { A, B, C } = this.getLineCoefficients(line);
const points: Point[] = [];
if (Math.abs(A) < 1e-5 && Math.abs(B) < 1e-5) return null;
if (Math.abs(B) > 1e-5) {
const y1 = -C / B;
if (y1 >= 0 && y1 <= h) points.push({ x: 0, y: y1 });
const y2 = (-C - A * w) / B;
if (y2 >= 0 && y2 <= h) points.push({ x: w, y: y2 });
}
if (Math.abs(A) > 1e-5) {
const x1 = -C / A;
if (x1 >= 0 && x1 <= w) points.push({ x: x1, y: 0 });
const x2 = (-C - B * h) / A;
if (x2 >= 0 && x2 <= w) points.push({ x: x2, y: h });
}
if (points.length < 2) return null;
const unique = points.filter(
(p, i, s) => i === s.findIndex((t) => Math.abs(t.x - p.x) < 0.1 && Math.abs(t.y - p.y) < 0.1),
);
if (unique.length < 2) return null;
return { start: unique[0], end: unique[1] };
}
}
