Skip to content

Commit fc1685c

Browse files
committed
feat: add 5 stage
1 parent e364252 commit fc1685c

11 files changed

Lines changed: 706 additions & 128 deletions

File tree

src/App.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { A, Navigate, Route, HashRouter, useLocation } from '@solidjs/router';
1+
import { A, Route, HashRouter, useLocation } from '@solidjs/router';
22
import type { Component, ParentComponent } from 'solid-js';
33
import { createSignal } from 'solid-js';
4+
import Welcome from './stages/Welcome';
45
import Stage1a from './stages/Stage1a';
56
import Stage1b from './stages/Stage1b';
67
import Stage2 from './stages/Stage2';
@@ -17,6 +18,8 @@ import Stage5e from './stages/Stage5e';
1718
import Stage5f from './stages/Stage5f';
1819
import Stage5g from './stages/Stage5g';
1920
import Stage5h from './stages/Stage5h';
21+
import Stage5i from './stages/Stage5i';
22+
import PlaceholderStage from './components/PlaceholderStage';
2023

2124
export type MenuLink = {
2225
href: string;
@@ -74,6 +77,13 @@ export const menuGroups: MenuEntry[] = [
7477
{ href: '/stage-5f', label: 'Порталы 2D: прямоугольник' },
7578
{ href: '/stage-5g', label: 'Высота камеры' },
7679
{ href: '/stage-5h', label: 'Порталы 2D: трапеция' },
80+
{ href: '/stage-5i', label: 'Порталы 2D: многоугольник' },
81+
],
82+
},
83+
{
84+
title: 'Stage 6: bsp',
85+
links: [
86+
{ href: '/stage-6a', label: 'Разбиваем сектора на подсектора' },
7787
],
7888
},
7989
];
@@ -233,7 +243,7 @@ const Layout: ParentComponent = (props) => {
233243

234244
const App: Component = () => (
235245
<HashRouter root={Layout}>
236-
<Route path="/" component={() => <Navigate href="/stage-1a" />} />
246+
<Route path="/" component={() => <Welcome />} />
237247
<Route path="/stage-1a" component={Stage1a} />
238248
<Route path="/stage-1b" component={Stage1b} />
239249
<Route path="/stage-2" component={Stage2} />
@@ -250,6 +260,8 @@ const App: Component = () => (
250260
<Route path="/stage-5f" component={Stage5f} />
251261
<Route path="/stage-5g" component={Stage5g} />
252262
<Route path="/stage-5h" component={Stage5h} />
263+
<Route path="/stage-5i" component={Stage5i} />
264+
<Route path="/stage-6a" component={() => <PlaceholderStage title={"TODO"} />} />
253265
<Route path="*" component={() => 'Not found'} />
254266
</HashRouter>
255267
);

src/stages/Stage5h/debug.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/stages/Stage5h/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const Stage5: Component = () => {
1212

1313
useCameraControls<Settings>({ settings, setSettings });
1414

15-
1615
return (
1716
<section class="flex flex-col gap-4">
1817

@@ -27,7 +26,7 @@ const Stage5: Component = () => {
2726

2827
<div class="grid grid-cols-2 gap-4">
2928
<div class="grid gap-4">
30-
<h4 class="text">Сектора с высотой пола и потолка</h4>
29+
<h4 class="text">Трапецевидный клиппинг не совершен, необходимо обрезать стены по снизу и сверху</h4>
3130
<Canvas
3231
settings={settings}
3332
width={settings().camera.screen.width}

src/stages/Stage5h/render25d.ts

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -227,65 +227,19 @@ function renderSectorWithPortal(
227227
endBottomY = startBottomY + t * (endBottomY - startBottomY);
228228
endX = clip.rightX;
229229
}
230-
231-
// Вертикальная обрезка по topY
232-
const clipTopAtStart = getTopYAtX(clip, startX);
233-
const clipTopAtEnd = getTopYAtX(clip, endX);
234-
235-
if (startTopY < clipTopAtStart) {
236-
const t = (clipTopAtStart - startTopY) / (endTopY - startTopY);
237-
if (isFinite(t) && t > 0 && t < 1) {
238-
startX = startX + t * (endX - startX);
239-
startBottomY = startBottomY + t * (endBottomY - startBottomY);
240-
startTopY = clipTopAtStart;
241-
}
242-
}
243-
244-
if (endTopY < clipTopAtEnd) {
245-
const t = (clipTopAtEnd - startTopY) / (endTopY - startTopY);
246-
if (isFinite(t) && t > 0 && t < 1) {
247-
endX = startX + t * (endX - startX);
248-
endBottomY = startBottomY + t * (endBottomY - startBottomY);
249-
endTopY = clipTopAtEnd;
250-
}
251-
}
252-
253-
// Вертикальная обрезка по bottomY
254-
const clipBottomAtStart = getBottomYAtX(clip, startX);
255-
const clipBottomAtEnd = getBottomYAtX(clip, endX);
256-
257-
if (startBottomY > clipBottomAtStart) {
258-
const t = (clipBottomAtStart - startTopY) / (endTopY - startTopY);
259-
if (isFinite(t) && t > 0 && t < 1) {
260-
startX = startX + t * (endX - startX);
261-
startTopY = startTopY + t * (endTopY - startTopY);
262-
startBottomY = clipBottomAtStart;
263-
}
264-
}
265-
266-
if (endBottomY > clipBottomAtEnd) {
267-
const t = (clipBottomAtEnd - startTopY) / (endTopY - startTopY);
268-
if (isFinite(t) && t > 0 && t < 1) {
269-
endX = startX + t * (endX - startX);
270-
endTopY = startTopY + t * (endTopY - startTopY);
271-
endBottomY = clipBottomAtEnd;
272-
}
273-
}
274230
}
275231

276232
const linedefMiddle = toMiddleVertex(seg.start, seg.end);
277233
const distance = toDistance(camera, linedefMiddle);
278234

279235
const clippedProjection: SegProjection = {
280236
...projection,
281-
start: {
282-
...projection.start,
237+
start: {
283238
screenX: startX,
284239
topY: startTopY,
285240
bottomY: startBottomY
286241
},
287-
end: {
288-
...projection.end,
242+
end: {
289243
screenX: endX,
290244
topY: endTopY,
291245
bottomY: endBottomY
@@ -321,7 +275,6 @@ function renderSectorWithPortal(
321275
floorPolygons.sort((a, b) => b.distance - a.distance);
322276
ceilPolygons.sort((a, b) => b.distance - a.distance);
323277

324-
// Рисуем в порядке: потолок -> стены -> пол
325278
for (const poly of ceilPolygons) {
326279
drawPolygon(ctx, poly.points, poly.color);
327280
}
@@ -345,7 +298,7 @@ function renderSectorWithPortal(
345298
camera,
346299
portal.backSector,
347300
new Set(visitedSectors),
348-
portal.clip // Передаем трапециевидный клип
301+
portal.clip
349302
);
350303
}
351304
}

src/stages/Stage5h/settings.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { Angle } from "@app/lib/Angle";
22
import { createRectangleLines } from "@app/lib/level";
33

44
const camera: Camera = {
5-
x: 300,
5+
x: 70,
66
y: 70,
77
z: 5_000,
88
fov: new Angle(45),
9-
angle: new Angle(-20),
9+
angle: new Angle(0),
1010
screen: {
1111
width: 400,
1212
height: 320,
@@ -28,7 +28,7 @@ const room2: Sector = {
2828
id: 2,
2929
floorHeight: 0,
3030
floorColor: "#666",
31-
ceilHeight: 10_000,
31+
ceilHeight: 20_000,
3232
ceilColor: "#87CEEB",
3333
segs: []
3434
};

src/stages/Stage5i/clipping.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import type { SegProjection } from "./projection";
2+
3+
export function createFloorPolygon(
4+
projection: SegProjection,
5+
screenHeight: number,
6+
): Vertex[] {
7+
const { start, end } = projection;
8+
9+
return [
10+
{ x: start.screenX, y: start.bottomY },
11+
{ x: end.screenX, y: end.bottomY },
12+
{ x: end.screenX, y: screenHeight },
13+
{ x: start.screenX, y: screenHeight },
14+
];
15+
}
16+
17+
export function createCeilPolygon(
18+
projection: SegProjection,
19+
): Vertex[] {
20+
const { start, end } = projection;
21+
22+
return [
23+
{ x: start.screenX, y: 0 },
24+
{ x: end.screenX, y: 0 },
25+
{ x: end.screenX, y: end.topY },
26+
{ x: start.screenX, y: start.topY },
27+
];
28+
}
29+
30+
export function createWallPolygon(projection: SegProjection) {
31+
const { start, end } = projection;
32+
33+
return [
34+
{ x: start.screenX, y: start.bottomY },
35+
{ x: end.screenX, y: end.bottomY },
36+
{ x: end.screenX, y: end.topY },
37+
{ x: start.screenX, y: start.topY },
38+
];
39+
}
40+
41+
// Sutherland-Hodgman algorithm, URL: https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm
42+
// Python implementation, URL https://github.com/mhdadk/sutherland-hodgman
43+
44+
export function intersectPolygons(subjectPolygon: Vertex[], clipPolygon: Vertex[]): Vertex[] {
45+
let outputList = subjectPolygon;
46+
47+
let cp1 = clipPolygon[clipPolygon.length - 1];
48+
49+
for (const cp2 of clipPolygon) {
50+
const inputList = outputList;
51+
outputList = [];
52+
53+
if (inputList.length === 0) break;
54+
55+
let s = inputList[inputList.length - 1];
56+
57+
for (const e of inputList) {
58+
const insideE = isInside(cp1, cp2, e);
59+
const insideS = isInside(cp1, cp2, s);
60+
61+
if (insideE) {
62+
if (!insideS) {
63+
outputList.push(getIntersection(cp1, cp2, s, e));
64+
}
65+
outputList.push(e);
66+
} else if (insideS) {
67+
outputList.push(getIntersection(cp1, cp2, s, e));
68+
}
69+
s = e;
70+
}
71+
cp1 = cp2;
72+
}
73+
74+
return ensureCounterClockwise(outputList)
75+
}
76+
77+
function isInside(cp1: Vertex, cp2: Vertex, p: Vertex): boolean {
78+
const cross = (cp2.x - cp1.x) * (p.y - cp1.y) - (cp2.y - cp1.y) * (p.x - cp1.x);
79+
return cross >= -1e-10;
80+
}
81+
82+
function getIntersection(cp1: Vertex, cp2: Vertex, s: Vertex, e: Vertex): Vertex {
83+
const A1 = cp2.y - cp1.y;
84+
const B1 = cp1.x - cp2.x;
85+
const C1 = A1 * cp1.x + B1 * cp1.y;
86+
87+
const A2 = e.y - s.y;
88+
const B2 = s.x - e.x;
89+
const C2 = A2 * s.x + B2 * s.y;
90+
91+
const det = A1 * B2 - A2 * B1;
92+
93+
if (Math.abs(det) < 1e-10) {
94+
return {
95+
x: (s.x + e.x) / 2,
96+
y: (s.y + e.y) / 2
97+
};
98+
}
99+
100+
const x = (B2 * C1 - B1 * C2) / det;
101+
const y = (A1 * C2 - A2 * C1) / det;
102+
103+
return { x, y };
104+
}
105+
106+
107+
function isCounterClockwise(polygon: Vertex[]): boolean {
108+
let sum = 0;
109+
for (let i = 0; i < polygon.length; i++) {
110+
const p1 = polygon[i];
111+
const p2 = polygon[(i + 1) % polygon.length];
112+
sum += (p2.x - p1.x) * (p2.y + p1.y);
113+
}
114+
return sum < 0;
115+
}
116+
117+
function ensureCounterClockwise(polygon: Vertex[]): Vertex[] {
118+
if (!isCounterClockwise(polygon)) {
119+
return [...polygon].reverse();
120+
}
121+
return [...polygon];
122+
}
123+
124+
export function sortPointsClockwise(points: Vertex[]): Vertex[] {
125+
if (points.length < 3) {
126+
return points;
127+
}
128+
129+
const center = points.reduce(
130+
(acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }),
131+
{ x: 0, y: 0 }
132+
);
133+
134+
center.x /= points.length;
135+
center.y /= points.length;
136+
137+
return points.sort((a, b) => {
138+
const angleA = Math.atan2(a.y - center.y, a.x - center.x);
139+
const angleB = Math.atan2(b.y - center.y, b.x - center.x);
140+
return angleA - angleB; // По часовой стрелке
141+
});
142+
}

0 commit comments

Comments
 (0)