diff --git a/README.md b/README.md index 354ea921..fbe23c53 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1375 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1377 checks (GM_STEPS=20 for one step) # every tool signs in first – see the Accounts page node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs diff --git a/src/editor/texture/TextureCanvas.ts b/src/editor/texture/TextureCanvas.ts index a22e9f6b..292e8636 100644 --- a/src/editor/texture/TextureCanvas.ts +++ b/src/editor/texture/TextureCanvas.ts @@ -120,6 +120,9 @@ export class TextureCanvas extends SheetCanvas { private sheetKey: unknown[] = []; private sheetBuilds = 0; private sheetOutlines = 0; + /** Outlines drawn tinted, on the sheet and on the overlay – only the selected frame's should be. */ + private sheetFilled = 0; + private overlayFilled = 0; private readonly labels = new Map(); private readonly labelLayer = new Container(); private readonly plates = new Graphics(); @@ -180,6 +183,7 @@ export class TextureCanvas extends SheetCanvas { return { sheetBuilds: this.sheetBuilds, sheetOutlines: this.sheetOutlines, + filled: { sheet: this.sheetFilled, overlay: this.overlayFilled }, origin: { x: origin.x, y: origin.y }, labels: [...this.labels.values()] .filter((label) => label.visible) @@ -604,15 +608,18 @@ export class TextureCanvas extends SheetCanvas { this.sheet.clear(); const others = frames.filter((f) => f.id !== selectedId); for (const frame of others) this.drawFrame(this.sheet, view, frame, false); - for (const frame of others) this.drawOutlines(this.sheet, view, frame, false, false); + this.sheetFilled = 0; + for (const frame of others) + this.sheetFilled += this.drawOutlines(this.sheet, view, frame, false, false); this.sheetBuilds += 1; this.sheetOutlines = others.reduce((n, f) => n + (f.collision?.length ?? 0), 0); } this.overlay.clear(); + this.overlayFilled = 0; const selected = frames.find((f) => f.id === selectedId); if (selected) { this.drawFrame(this.overlay, view, selected, true); - this.drawOutlines(this.overlay, view, selected, this.collision, true); + this.overlayFilled = this.drawOutlines(this.overlay, view, selected, this.collision, true); if (!this.collision) { this.drawBorders(view, selected); this.drawHandles(view, selected); @@ -656,7 +663,7 @@ export class TextureCanvas extends SheetCanvas { * A frame's collision outlines (#141), always shown so one sees which frames block with them; on * the edited frame the active outline is drawn heavier and every corner of every outline gets a * handle. An outline that is no polygon is drawn in red. Only the selected frame's outlines are - * `filled`, so the tint does not lie over the art of every other frame. + * `filled`, so the tint does not lie over the art of every other frame. Returns how many were tinted. */ private drawOutlines( g: Graphics, @@ -664,16 +671,20 @@ export class TextureCanvas extends SheetCanvas { frame: TextureFrame, edited: boolean, filled: boolean, - ): void { + ): number { const outlines = frame.collision ?? []; const active = edited ? this.activeOutline(frame, outlines) : -1; + let tinted = 0; for (const [index, points] of outlines.entries()) { const screen = points.map((p) => imageToLocal(view, frame.x + p.x, frame.y + p.y)); const color = isPolygon(points) ? Theme.colors.collider : Theme.colors.danger; const width = index === active ? 2 : 1; if (screen.length >= 3) { g.poly(screen, true); - if (filled) g.fill({ color, alpha: COLLIDER_FILL_ALPHA }); + if (filled) { + g.fill({ color, alpha: COLLIDER_FILL_ALPHA }); + tinted += 1; + } g.stroke({ width, color }); } else if (screen.length === 2) g.moveTo(screen[0]!.x, screen[0]!.y).lineTo(screen[1]!.x, screen[1]!.y).stroke({ width: 2, color }); @@ -684,6 +695,7 @@ export class TextureCanvas extends SheetCanvas { .fill(index === active ? Theme.colors.accentText : Theme.colors.viewport) .stroke({ width: 1, color }); } + return tinted; } private drawHandles(view: SheetView, frame: TextureFrame): void { diff --git a/src/editor/viewport/tilemapSolids.ts b/src/editor/viewport/tilemapSolids.ts index d726070b..b86fa9e4 100644 --- a/src/editor/viewport/tilemapSolids.ts +++ b/src/editor/viewport/tilemapSolids.ts @@ -41,6 +41,8 @@ export class TilemapSolidOutlines { string, { from: TilemapSolids; solid: BoundaryLine[]; oneWay: BoundaryLine[] } >(); + /** What each map's last draw tinted, or nothing: the painted layer's part, while it is selected. */ + private readonly tinted = new Map(); constructor(private readonly layer: Container) {} @@ -48,9 +50,13 @@ export class TilemapSolidOutlines { this.nodes.get(id)?.destroy(); this.nodes.delete(id); this.boundaries.delete(id); + this.tinted.delete(id); } - /** The blocking rectangles and outlined cells of a placed tilemap, and the outer edges drawn round them, in cells (test hook). */ + /** + * The blocking rectangles and outlined cells of a placed tilemap, the outer edges drawn round + * them, and the tinted cells, in cells (test hook). + */ info(item: DrawItem | undefined): { visible: boolean; rects: CellRect[]; @@ -58,10 +64,19 @@ export class TilemapSolidOutlines { oneWay: CellRect[]; oneWayOutlines: Vec2[][]; boundary: BoundaryLine[]; + filled: { rects: CellRect[]; outlines: number; oneWay: CellRect[] } | null; } | null { const node = item && this.nodes.get(item.id); if (!node || !item.tilemap) return null; + const tinted = this.tinted.get(item.id); return { + filled: tinted + ? { + rects: tinted.solids.map((rect) => ({ ...rect })), + outlines: tinted.outlines.length, + oneWay: tinted.oneWay.solids.map((rect) => ({ ...rect })), + } + : null, visible: node.visible, rects: item.tilemap.solids.map((rect) => ({ ...rect })), outlines: item.tilemap.outlines.map((polygon) => polygon.outline.map((p) => ({ ...p }))), @@ -98,6 +113,7 @@ export class TilemapSolidOutlines { const rects = item.tilemap?.solids ?? []; const outlines = item.tilemap?.outlines ?? []; const oneWay = item.tilemap?.oneWay ?? { solids: [], outlines: [] }; + this.tinted.delete(item.id); if (!show || rects.length + outlines.length + oneWay.solids.length + oneWay.outlines.length === 0) { if (node) node.visible = false; return; @@ -139,6 +155,7 @@ export class TilemapSolidOutlines { node.stroke({ width: lineWidth, color }); }; if (filled) { + this.tinted.set(item.id, filled); fill(filled.solids, filled.outlines, COLLIDER_FILL_ALPHA); fill(filled.oneWay.solids, filled.oneWay.outlines, COLLIDER_FILL_ALPHA / 2); } diff --git a/tools/e2e/40-tilemap.mjs b/tools/e2e/40-tilemap.mjs index 004dbaa5..f3cb46fb 100644 --- a/tools/e2e/40-tilemap.mjs +++ b/tools/e2e/40-tilemap.mjs @@ -998,6 +998,55 @@ export async function tilemap(t) { ); await shot('40-tilemap-solid'); + // 40l-iii. The outline shows on every map, the tint only on the map being painted, and there only + // where the layer picked in the panel blocks: the block on the lower, (4, 0) on the upper. + const tinted = async () => + (await json(`__gm.tilemap.solids(${JSON.stringify(MAP_OBJECT)})`))?.filled ?? null; + const tintedByTile = await tinted(); + await select({ kind: 'object', holderId: 'scene1', objectIds: [MAP_OBJECT], objectId: MAP_OBJECT }); + await settle(); + const layerIds = (await json(`__gm.tilemap.layers(${JSON.stringify(made.id)})`)).map((l) => l.id); + const tintOf = async (row) => { + const spot = await inPanel(t, `__gm.tilemap.layerRowAt(${row})`); + if (spot) await click(spot.name.x, spot.y); + await settle(); + // No layer chosen means the topmost is painted on, as the panel has it. + const chosen = (await json('__gm.tilemap.paint()')).layerId; + return { layer: layerIds.includes(chosen) ? chosen : layerIds.at(-1), filled: await tinted() }; + }; + const tintRows = [await tintOf(0), await tintOf(1)]; + const expectedTint = { + [layerIds[0]]: [{ x: 0, y: 0, w: 3, h: 3 }], + [layerIds[1]]: [{ x: 4, y: 0, w: 1, h: 1 }], + }; + const besideMap = await json( + `__gm.state.activeScene.objects.find((o) => o.id !== ${JSON.stringify(MAP_OBJECT)} && !o.parentId).id`, + ); + await select({ + kind: 'object', + holderId: 'scene1', + objectIds: [MAP_OBJECT, besideMap], + objectId: MAP_OBJECT, + }); + await settle(); + const tintedWithTwo = await tinted(); + await select({ kind: 'asset', assetId: 'at-grass' }); + await settle(); + check( + 'only the selected map is tinted, and only where the layer painted on blocks', + tintedByTile === null && + tintedWithTwo === null && + layerIds.length === 2 && + new Set(tintRows.map((r) => r.layer)).size === 2 && + tintRows.every( + (r) => + JSON.stringify(r.filled?.rects) === JSON.stringify(expectedTint[r.layer]) && + r.filled.outlines === 0 && + r.filled.oneWay.length === 0, + ), + JSON.stringify({ tintedByTile, tintRows, tintedWithTwo }), + ); + // In the game a crate of its own walks into the block: the seeded Crate sprite with a 100 × 60 // collider and the Mover (ArrowRight moves its owner, colliding with solids) below it. Its own // rather than the seeded red crate, which an earlier step of a full run may have removed. It diff --git a/tools/e2e/42-shapes.mjs b/tools/e2e/42-shapes.mjs index bde8c7a5..5249172c 100644 --- a/tools/e2e/42-shapes.mjs +++ b/tools/e2e/42-shapes.mjs @@ -327,6 +327,17 @@ export async function shapes(t) { unselected.sheetOutlines === 1 && restored.sheetOutlines === 2 && (await drawInfo()).sheetOutlines === 1, JSON.stringify({ unselected: unselected.sheetOutlines, restored: restored.sheetOutlines }), ); + // Every frame's outlines show, but only the selected frame's are tinted, so the tint does not lie + // over the whole sheet. + check( + "only the selected frame's collision outlines are tinted", + beforeDrag.filled.overlay === 1 && + beforeDrag.filled.sheet === 0 && + unselected.sheetOutlines === 1 && + unselected.filled.sheet === 0 && + unselected.filled.overlay === 0, + JSON.stringify({ selected: beforeDrag.filled, unselected: unselected.filled }), + ); const area = await json('__gm.texture.rect()'); const beforePan = await drawInfo(); const [px, py] = [Math.round(area.x + area.w / 2), Math.round(area.y + area.h / 2)];