Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
game-maker / gate (push) Successful in 1m52s
game-maker / suite (push) Successful in 6m41s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-18 22:56:25 +02:00
1 parent 1b5e386b60
commit 32057a96fc
25 files changed
+2341 -122

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
/**
* Editing an outline by its corners (#141, #151), the same in the Scene view (a polygon collider)
* and in the Texture tab (a frame's collision outline): drag a corner, press on an edge to put a
* corner there, double-click a corner to take it away. Pure, so the rules are testable without a view.
*/
import type { Vec2 } from '../core/model/Properties';
/** What lies under the pointer: a corner, or the edge from corner `index` to the next one. */
export type PolygonGrip = { kind: 'corner'; index: number } | { kind: 'edge'; index: number; at: Vec2 };
/** Fewer corners than this are no polygon; the last three cannot be taken away. */
export const MIN_CORNERS = 3;
function closestOnSegment(p: Vec2, a: Vec2, b: Vec2): Vec2 {
const ab = { x: b.x - a.x, y: b.y - a.y };
const length = ab.x * ab.x + ab.y * ab.y;
const t = length > 0 ? Math.min(1, Math.max(0, ((p.x - a.x) * ab.x + (p.y - a.y) * ab.y) / length)) : 0;
return { x: a.x + ab.x * t, y: a.y + ab.y * t };
}
/**
* The corner or edge within `tolerance` of `p`, the nearest one; a corner wins over an edge, since
* every corner lies on two edges. An outline of one or two corners has no edge to press on yet.
*/
export function gripAt(points: readonly Vec2[], p: Vec2, tolerance: number): PolygonGrip | null {
let best: PolygonGrip | null = null;
let nearest = tolerance;
for (const [index, corner] of points.entries()) {
const distance = Math.hypot(corner.x - p.x, corner.y - p.y);
if (distance > nearest) continue;
best = { kind: 'corner', index };
nearest = distance;
}
if (best || points.length < MIN_CORNERS) return best;
for (const [index, a] of points.entries()) {
const at = closestOnSegment(p, a, points[(index + 1) % points.length]!);
const distance = Math.hypot(at.x - p.x, at.y - p.y);
if (distance > nearest) continue;
best = { kind: 'edge', index, at };
nearest = distance;
}
return best;
}
/** A corner put at `at` on the edge after corner `index`; it becomes corner `index + 1`. */
export function withCornerAfter(points: readonly Vec2[], index: number, at: Vec2): Vec2[] {
return [...points.slice(0, index + 1), { ...at }, ...points.slice(index + 1)];
}
export function withCornerMoved(points: readonly Vec2[], index: number, to: Vec2): Vec2[] {
return points.map((p, i) => (i === index ? { ...to } : { ...p }));
}
/** The outline without corner `index`; null when that would leave it fewer than `min` corners. */
export function withoutCorner(points: readonly Vec2[], index: number, min = MIN_CORNERS): Vec2[] | null {
if (points.length - 1 < min) return null;
return points.filter((_, i) => i !== index).map((p) => ({ ...p }));
}