практика_отсечение-многоугольников / Наволоцкий_1302
.pdfreturn 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] };
}
}
Реализация матричных преобразований
src/core/math/Matrix.ts
import type { Point } from "../types";
/**
*Класс для работы с матрицами преобразований 3x3.
*Используется для аффинных преобразований в 2D (сдвиг, поворот).
31
* Хранит данные в плоском массиве: [m00, m01, m02, m10, m11, m12, m20, m21, m22]
*/
export class Matrix { values: number[];
constructor(values?: number[]) { if (values) {
if (values.length !== 9) {
throw new Error("Matrix must have exactly 9 values!");
}
this.values = values;
}else {
this.values = [1, 0, 0, 0, 1, 0, 0, 0, 1];
}
}
/**
*Умножение матриц: A * B
*Результат - новая матрица, объединяющая эффекты (сначала B, потом A)
*/
multiply(other: Matrix): Matrix { const a = this.values;
const b = other.values; const result: number[] = [];
// Строка 0
result[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6]; result[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7]; result[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8];
// Строка 1
result[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6]; result[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7]; result[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8];
// Строка 2
32
result[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6]; result[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7]; result[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8];
return new Matrix(result);
}
transformPoint(p: Point): Point { const m = this.values;
const x = m[0] * p.x + m[1] * p.y + m[2]; const y = m[3] * p.x + m[4] * p.y + m[5];
return { x, y };
}
/**
* Матрица переноса (Translation) */
static translation(dx: number, dy: number): Matrix { return new Matrix([1, 0, dx, 0, 1, dy, 0, 0, 1]);
}
/**
*Матрица поворота (Rotation) вокруг начала координат (0,0)
*@param angleDegrees угол в градусах
*/
static rotation(angleDegrees: number): Matrix { const rad = (angleDegrees * Math.PI) / 180; const c = Math.cos(rad);
const s = Math.sin(rad);
return new Matrix([c, -s, 0, s, c, 0, 0, 0, 1]);
}
/** * Единичная матрица
33
*/
static identity(): Matrix { return new Matrix();
}
}
Парсинг и сохранение данных
src/core/parsers/FileParser.ts
import { Polygon, Line, type Point, type SceneData } from "../types"; import { COLORS } from "../theme";
export class FileParser { /**
* Превращает данные текстового формата в данные сцены
*/
static parse(text: string): SceneData { const polygons: Polygon[] = [];
let cuttingLine: Line | null = null;
const lines = text
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0);
let currentPolygonPoints: Point[] = []; let isReadingPolygon = false;
let isReadingLine = false; let linePoints: Point[] = [];
for (const line of lines) { if (line === "POLYGON") {
isReadingPolygon = true; currentPolygonPoints = []; continue;
}
34
if (line === "END" && isReadingPolygon) { if (currentPolygonPoints.length >= 3) {
polygons.push(new Polygon(currentPolygonPoints, COLORS.POLYGON.DEFAULT));
}
isReadingPolygon = false; continue;
}
if (line === "LINE") { isReadingLine = true; linePoints = []; continue;
}
const parts = line.split(/\s+/); if (parts.length >= 2) {
const x = parseFloat(parts[0]); const y = parseFloat(parts[1]);
if (!isNaN(x) && !isNaN(y)) { const point: Point = { x, y };
if (isReadingPolygon) { currentPolygonPoints.push(point);
} else if (isReadingLine) { linePoints.push(point);
}
}
}
}
if (linePoints.length >= 2) {
cuttingLine = new Line(linePoints[0], linePoints[1]);
}
35
return { polygons, cuttingLine };
}
/**
* Превращает данные сцены обратно в текстовый формат
*/
static save(data: SceneData): string { const lines: string[] = [];
data.polygons.forEach((poly) => { lines.push("POLYGON"); poly.vertices.forEach((v) => {
lines.push(`${v.x.toFixed(2)} ${v.y.toFixed(2)}`); });
lines.push("END");
lines.push("");
});
if (data.cuttingLine) { lines.push("LINE");
const start = data.cuttingLine.start; const end = data.cuttingLine.end;
lines.push(`${start.x.toFixed(2)} ${start.y.toFixed(2)}`); lines.push(`${end.x.toFixed(2)} ${end.y.toFixed(2)}`);
}
return lines.join("\n");
}
}
Логика отрисовки и взаимодействия
drawScene из файла src/components/PixiCanvas.vue const drawScene = () => {
if (!app || !polygonsLayer || !lineLayer || !uiGraphics) return;
polygonsLayer.removeChildren();
36
lineLayer.removeChildren();
uiGraphics.clear();
props.data.polygons.forEach((polyData) => { let status = PolygonPosition.POSITIVE_SIDE; if (props.data.cuttingLine) {
status = Geometry.checkPolygonPosition(polyData, props.data.cuttingLine);
}
if (status === PolygonPosition.INTERSECTED && props.data.cuttingLine) {
const { positive, negative } = Geometry.cutPolygon(polyData, props.data.cuttingLine);
positive.forEach((polyPoints) => { if (polyPoints.length > 2) {
const gPos = new Graphics(); drawPolyShape(gPos, polyPoints); gPos.fill(COLORS.POLYGON.POSITIVE);
gPos.stroke({ width: 1, color: "red", alpha: 0.5 }); setupInteractive(gPos, polyData); polygonsLayer!.addChild(gPos);
}
});
negative.forEach((polyPoints) => { if (polyPoints.length > 2) {
const gNeg = new Graphics(); drawPolyShape(gNeg, polyPoints); gNeg.fill(COLORS.POLYGON.NEGATIVE);
gNeg.stroke({ width: 1, color: "red", alpha: 0.5 }); setupInteractive(gNeg, polyData); polygonsLayer!.addChild(gNeg);
}
});
if (polyData.isSelected) {
37
const gSel = new Graphics(); drawPolyShape(gSel, polyData.vertices);
gSel.stroke({ width: 3, color: COLORS.POLYGON.SELECTED }); polygonsLayer!.addChild(gSel);
}
}else {
const g = new Graphics();
if (polyData.vertices.length > 2) { drawPolyShape(g, polyData.vertices); let fillColor = COLORS.POLYGON.DEFAULT;
if (status === PolygonPosition.POSITIVE_SIDE && props.data.cuttingLine)
fillColor = COLORS.POLYGON.POSITIVE;
else if (status === PolygonPosition.NEGATIVE_SIDE && props.data.cuttingLine)
fillColor = COLORS.POLYGON.NEGATIVE;
g.fill(fillColor);
if (polyData.isSelected) g.stroke({ width: 3, color: COLORS.POLYGON.SELECTED });
else g.stroke({ width: 1, color: "red" });
}
setupInteractive(g, polyData); polygonsLayer!.addChild(g);
}
});
if (props.data.cuttingLine) {
const line = props.data.cuttingLine; const gLine = new Graphics(); gLine.eventMode = "static"; gLine.cursor = "pointer";
const dx = line.end.x - line.start.x; const dy = line.end.y - line.start.y;
const infStart = { x: line.start.x - dx * 100, y: line.start.y - dy * 100
};
38
const infEnd = { x: line.end.x + dx * 100, y: line.end.y + dy * 100 };
const sStart = CoordinateSystem.toScreen(infStart); const sEnd = CoordinateSystem.toScreen(infEnd);
gLine.moveTo(sStart.x, sStart.y); gLine.lineTo(sEnd.x, sEnd.y);
gLine.stroke({ width: 2, color: COLORS.LINE.CUTTING, alpha: 0.3 });
gLine.stroke({ width: 20, alpha: 0.001, color: 0xffffff });
const p1 = CoordinateSystem.toScreen(line.start); const p2 = CoordinateSystem.toScreen(line.end);
const color = line.isSelected ? COLORS.POLYGON.SELECTED : COLORS.LINE.CUTTING;
gLine.moveTo(p1.x, p1.y); gLine.lineTo(p2.x, p2.y);
gLine.stroke({ width: 4, color: color });
gLine.circle(p1.x, p1.y, 4); gLine.circle(p2.x, p2.y, 4); gLine.fill(color);
gLine.on("pointerdown", (e) => onObjectDown(e, line)); lineLayer.addChild(gLine);
}
if (creationPoints.length > 0) { creationPoints.forEach((pWorld, index) => {
const pScreen = CoordinateSystem.toScreen(pWorld); uiGraphics.circle(pScreen.x, pScreen.y, 4);
if (props.activeTool === "create-poly" && index === 0) { uiGraphics.fill(isHoveringStart ? "#00ff00" : "#ffffff");
if (isHoveringStart) uiGraphics.stroke({ width: 2, color: "#00ff00"
});
39
}else { uiGraphics.fill("#ffffff");
}
});
if (props.activeTool === "create-poly" && creationPoints.length > 1) { const start = CoordinateSystem.toScreen(creationPoints[0]); uiGraphics.moveTo(start.x, start.y);
for (let i = 1; i < creationPoints.length; i++) {
const p = CoordinateSystem.toScreen(creationPoints[i]); uiGraphics.lineTo(p.x, p.y);
}
uiGraphics.stroke({ width: 2, color: "#00ffff" });
}
const lastWorld = creationPoints[creationPoints.length - 1]; const lastScreen = CoordinateSystem.toScreen(lastWorld); const mouseScreen = CoordinateSystem.toScreen(tempMousePos);
uiGraphics.moveTo(lastScreen.x, lastScreen.y); uiGraphics.lineTo(mouseScreen.x, mouseScreen.y);
const rubberColor = props.activeTool === "create-line" ? "#ff00ff" : "#00ffff";
uiGraphics.stroke({ width: 1, color: rubberColor, alpha: 0.7 });
}
const selected = getAllSelected();
if (props.activeTool === "rotate" && selected.length > 0) { const centerWorld = getGroupCentroid(selected);
const centerScreen = CoordinateSystem.toScreen(centerWorld); uiGraphics.circle(centerScreen.x, centerScreen.y, 6); uiGraphics.fill("white");
uiGraphics.stroke({ width: 1, color: "black" });
}
};
40
