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

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] };

}

}

Реализация матричных преобразований

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