The scene view outlines a tilemap's collision only along its outer edges: where two pieces meet, their shared stretch runs once in each direction and is not drawn, so an autotile lake is one outline without cross strokes at the quarters, merged rectangles of plain tiles and one-way tiles follow the same rule, the bridges from splitting concave outlines disappear and a piece doubled on two layers is outlined once; fill and collision stay as they were, and the result is cached per tilemap and rebuilt only when its collision data changes (#173)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a31f5e0ad
commit
d4ea04d4d6
7 files changed
+365
-10
No files matched your search
@@ -41,7 +41,8 @@ import {
|
||||
tilemapRows,
|
||||
tilemapTileSize,
|
||||
} from '../../core/model/tilemap';
|
||||
import type { CellPolygon, CellRect } from '../../core/model/tilemapCollision';
|
||||
import type { BoundaryLine } from '../../core/model/polygon';
|
||||
import { type CellPolygon, type CellRect, solidBoundary } from '../../core/model/tilemapCollision';
|
||||
import { footprintCorners } from '../../core/properties/bounds';
|
||||
import { colliderPoints, COLLIDER_POINTS_KEY } from '../../core/properties/collider';
|
||||
import { localMatrixOf, parentTransform, transformOf } from '../../core/properties/transform';
|
||||
@@ -154,6 +155,28 @@ const LINK_COLORS: Partial<Record<DrawItem['type'], number>> = {
|
||||
rigidbody: Theme.colors.collider,
|
||||
};
|
||||
|
||||
/** Where a resolved tilemap blocks: what its outline in the scene view is worked out from. */
|
||||
type TilemapSolids = Pick<NonNullable<DrawItem['tilemap']>, 'solids' | 'outlines' | 'oneWay'>;
|
||||
|
||||
function sameSolids(a: TilemapSolids, b: TilemapSolids): boolean {
|
||||
const sameRects = (p: readonly CellRect[], q: readonly CellRect[]) =>
|
||||
p.length === q.length &&
|
||||
p.every((r, i) => r.x === q[i]!.x && r.y === q[i]!.y && r.w === q[i]!.w && r.h === q[i]!.h);
|
||||
const samePolygons = (p: readonly CellPolygon[], q: readonly CellPolygon[]) =>
|
||||
p.length === q.length &&
|
||||
p.every(
|
||||
(polygon, i) =>
|
||||
polygon.outline.length === q[i]!.outline.length &&
|
||||
polygon.outline.every((c, j) => c.x === q[i]!.outline[j]!.x && c.y === q[i]!.outline[j]!.y),
|
||||
);
|
||||
return (
|
||||
sameRects(a.solids, b.solids) &&
|
||||
samePolygons(a.outlines, b.outlines) &&
|
||||
sameRects(a.oneWay.solids, b.oneWay.solids) &&
|
||||
samePolygons(a.oneWay.outlines, b.oneWay.outlines)
|
||||
);
|
||||
}
|
||||
|
||||
export class SceneViewport extends FreeViewport {
|
||||
private readonly grid = new Graphics();
|
||||
private readonly links = new Graphics();
|
||||
@@ -162,6 +185,14 @@ export class SceneViewport extends FreeViewport {
|
||||
private readonly icons = new Map<string, Graphics>();
|
||||
/** Where each placed tilemap blocks, outlined like a collider (#132). */
|
||||
private readonly solids = new Map<string, Graphics>();
|
||||
/**
|
||||
* The outer edges of each tilemap's solids and what they were worked out from (#173). A map is
|
||||
* resolved afresh on every draw, so they are kept by what blocks rather than by the resolved map.
|
||||
*/
|
||||
private readonly boundaries = new Map<
|
||||
string,
|
||||
{ from: TilemapSolids; solid: BoundaryLine[]; oneWay: BoundaryLine[] }
|
||||
>();
|
||||
private editorWorld: World | null = null;
|
||||
private lastRays: CastRay[] = [];
|
||||
private dragging: ObjectDrag | null = null;
|
||||
@@ -752,15 +783,17 @@ export class SceneViewport extends FreeViewport {
|
||||
this.icons.delete(id);
|
||||
this.solids.get(id)?.destroy();
|
||||
this.solids.delete(id);
|
||||
this.boundaries.delete(id);
|
||||
}
|
||||
|
||||
/** The blocking rectangles and outlined cells of a placed tilemap as outlined, in cells (test hook). */
|
||||
/** The blocking rectangles and outlined cells of a placed tilemap, and the outer edges drawn round them, in cells (test hook). */
|
||||
solidInfo(id: string): {
|
||||
visible: boolean;
|
||||
rects: CellRect[];
|
||||
outlines: Vec2[][];
|
||||
oneWay: CellRect[];
|
||||
oneWayOutlines: Vec2[][];
|
||||
boundary: BoundaryLine[];
|
||||
} | null {
|
||||
const node = this.solids.get(id);
|
||||
const item = this.lastItems.find((i) => i.id === id);
|
||||
@@ -771,9 +804,27 @@ export class SceneViewport extends FreeViewport {
|
||||
outlines: item.tilemap.outlines.map((polygon) => polygon.outline.map((p) => ({ ...p }))),
|
||||
oneWay: item.tilemap.oneWay.solids.map((rect) => ({ ...rect })),
|
||||
oneWayOutlines: item.tilemap.oneWay.outlines.map((polygon) => polygon.outline.map((p) => ({ ...p }))),
|
||||
boundary: this.boundaryOf(item).solid.map((line) => ({
|
||||
points: line.points.map((p) => ({ ...p })),
|
||||
closed: line.closed,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private boundaryOf(item: DrawItem): { solid: BoundaryLine[]; oneWay: BoundaryLine[] } {
|
||||
const from: TilemapSolids = item.tilemap!;
|
||||
let boundary = this.boundaries.get(item.id);
|
||||
if (!boundary || !sameSolids(boundary.from, from)) {
|
||||
boundary = {
|
||||
from,
|
||||
solid: solidBoundary(from.solids, from.outlines),
|
||||
oneWay: solidBoundary(from.oneWay.solids, from.oneWay.outlines),
|
||||
};
|
||||
this.boundaries.set(item.id, boundary);
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
|
||||
/**
|
||||
* The solid cells of a tilemap, drawn the way a collider is: tinted and outlined, above the
|
||||
* content so a painted wall cannot hide them. Editor-only for the same reason as the icon: the
|
||||
@@ -800,7 +851,10 @@ export class SceneViewport extends FreeViewport {
|
||||
const width = 2 / (scale * scaleOf(item.m));
|
||||
node.clear();
|
||||
const color = Theme.colors.collider;
|
||||
const trace = (rects: readonly CellRect[], outlines: readonly CellPolygon[]) => {
|
||||
const boundary = this.boundaryOf(item);
|
||||
// Filled piece by piece, but stroked only along the outer edges, so no seam shows (#173).
|
||||
const fill = (rects: readonly CellRect[], outlines: readonly CellPolygon[], alpha: number) => {
|
||||
if (rects.length + outlines.length === 0) return;
|
||||
for (const rect of rects) {
|
||||
const corner = cellOrigin(item.props, rect.x, rect.y);
|
||||
node.rect(corner.x, corner.y, rect.w * tile, rect.h * tile);
|
||||
@@ -810,11 +864,21 @@ export class SceneViewport extends FreeViewport {
|
||||
polygon.outline.map((p) => cellOrigin(item.props, p.x, p.y)),
|
||||
true,
|
||||
);
|
||||
node.fill({ color, alpha });
|
||||
};
|
||||
trace(rects, outlines);
|
||||
node.fill({ color, alpha: 0.12 }).stroke({ width, color });
|
||||
trace(oneWay.solids, oneWay.outlines);
|
||||
node.fill({ color, alpha: 0.06 }).stroke({ width: width / 2, color });
|
||||
const stroke = (lines: readonly BoundaryLine[], lineWidth: number) => {
|
||||
if (lines.length === 0) return;
|
||||
for (const line of lines)
|
||||
node.poly(
|
||||
line.points.map((p) => cellOrigin(item.props, p.x, p.y)),
|
||||
line.closed,
|
||||
);
|
||||
node.stroke({ width: lineWidth, color });
|
||||
};
|
||||
fill(rects, outlines, 0.12);
|
||||
stroke(boundary.solid, width);
|
||||
fill(oneWay.solids, oneWay.outlines, 0.06);
|
||||
stroke(boundary.oneWay, width / 2);
|
||||
// A platform's top is what blocks, so that is what stands out.
|
||||
for (const rect of oneWay.solids) {
|
||||
const a = cellOrigin(item.props, rect.x, rect.y);
|
||||
|
||||
Reference in new issue
Block a user