Скачиваний:
0
Добавлен:
27.12.2025
Размер:
216.59 Кб
Скачать

Список использованных источников

  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/en-US/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/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] };

}

}