From 495d87847dd07a5f1e6f5a01f932b590a7d5c2c8 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Thu, 24 Sep 2026 12:05:29 +0200 Subject: [PATCH] Tint a collider's area only while the collider itself is selected A collider filled its area with a faint green in the scene view all the time. It lay over every sprite a collider sits on and made the art look washed out. The outline alone shows where a collider is. The area only matters while one works on that collider. - drawItem takes a selected flag. Only a selected collider is filled, every other one is drawn as its outline. - The fill is stronger than before, alpha 0.3 instead of 0.12, so the selected area is easy to see. The value is COLLIDER_FILL_ALPHA in src/viewport/symbols.ts. - The scene view says what is selected through isSelected. The game view keeps the default and never fills. - Only the collider object itself counts. Selecting its body leaves the area unfilled. Tests: - 2 new unit tests in tests/viewport/collider-color.test.ts read the Pixi instructions: outline only when not selected, fill at 0.3 and outline when selected. - Counter check: with the fill drawn regardless of selection the outline-only test fails. The original is back per cmp. - ./check.sh: 158 files, 2334 tests, CHECK EXIT 0. - e2e 1-17,28-30: no FAIL. e2e 39-43: ALL PASSED, 145 checks. A first run left out 39, which 40 builds on, and failed 8 checks in 40 for the missing tile only. Co-Authored-By: Claude Opus 5.5 --- src/editor/viewport/SceneViewport.ts | 5 ++++ src/editor/viewport/WorldViewport.ts | 13 +++++++++- src/viewport/symbols.ts | 10 ++++++-- tests/viewport/collider-color.test.ts | 34 ++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/editor/viewport/SceneViewport.ts b/src/editor/viewport/SceneViewport.ts index 78de58d9..9c43475d 100644 --- a/src/editor/viewport/SceneViewport.ts +++ b/src/editor/viewport/SceneViewport.ts @@ -363,6 +363,11 @@ export class SceneViewport extends FreeViewport { return this.corners.info(); } + protected override isSelected(objectId: string): boolean { + const s = this.state.selection; + return s?.kind === 'object' && s.objectIds.includes(objectId); + } + /** * Pixi only says which hit area was pressed; what is really under the pointer is worked out * here, in the order it is drawn (#203): a tilemap's grid is empty where it has no tile, and a diff --git a/src/editor/viewport/WorldViewport.ts b/src/editor/viewport/WorldViewport.ts index 000e48ed..f7cab9cb 100644 --- a/src/editor/viewport/WorldViewport.ts +++ b/src/editor/viewport/WorldViewport.ts @@ -137,6 +137,11 @@ export abstract class WorldViewport extends Widget { protected onItemPointerDown(_objectId: string, _e: FederatedPointerEvent): void {} + /** Whether the object itself is selected – a selected collider shows its area filled. */ + protected isSelected(_objectId: string): boolean { + return false; + } + /** Screen-space drawing that follows the camera, redrawn after a full redraw and after a pan. */ protected drawOnScreen(_items: DrawItem[], _view: View): void {} @@ -317,7 +322,13 @@ export abstract class WorldViewport extends Widget { this.syncTilemap(item, onScreen); if (!onScreen) continue; this.visibleCount++; - drawItem(node, item, size, view.scale * scaleOf(placementOf(item.type, item.m))); + drawItem( + node, + item, + size, + view.scale * scaleOf(placementOf(item.type, item.m)), + this.isSelected(item.id), + ); } this.afterItems(items, view); this.drawOnScreen(items, view); diff --git a/src/viewport/symbols.ts b/src/viewport/symbols.ts index 1d61f4ea..d22c0595 100644 --- a/src/viewport/symbols.ts +++ b/src/viewport/symbols.ts @@ -23,6 +23,9 @@ import type { TextureLibrary } from './TextureLibrary'; /** Screen-pixel half-width of the grab band along a collider's outline. */ const COLLIDER_GRAB_BAND = 6; +/** How strongly a selected collider tints its area. */ +export const COLLIDER_FILL_ALPHA = 0.3; + /** * Gives a Pixi node the transform `m`, its scale multiplied by `w`×`h` (a unit quad becomes the * sprite's box). A similarity is a rotation and two scales whose signs carry the mirror, which is @@ -107,13 +110,15 @@ export function disposeSpriteMesh(mesh: Mesh): void { /** * Draws in the node's own units after `placeNode` put it where the item is. `scale` is the - * on-screen pixels per unit of the node, so strokes stay constant on screen. + * on-screen pixels per unit of the node, so strokes stay constant on screen. Only a `selected` + * collider fills its area; otherwise the tint would lie over every sprite a collider sits on. */ export function drawItem( node: Graphics, item: DrawItem, size: { w: number; h: number }, scale: number, + selected = false, ): void { placeNode(node, placementOf(item.type, item.m)); node.clear(); @@ -153,7 +158,8 @@ export function drawItem( const shape = colliderShape(item.props, IDENTITY, item.frame); traceShape(node, shape); const color = colliderColor(shape, item.props); - node.fill({ color, alpha: 0.12 }).stroke({ width: 2 / scale, color }); + if (selected) node.fill({ color, alpha: COLLIDER_FILL_ALPHA }); + node.stroke({ width: 2 / scale, color }); const facing = oneWayFacing(item.props, IDENTITY); if (facing) markBlockingSide(node, shapeBounds(shape), facing, 5 / scale, color, { diff --git a/tests/viewport/collider-color.test.ts b/tests/viewport/collider-color.test.ts index 07d4e0e5..71ef3351 100644 --- a/tests/viewport/collider-color.test.ts +++ b/tests/viewport/collider-color.test.ts @@ -3,7 +3,9 @@ import { IDENTITY } from '../../src/core/model/matrix'; import type { PropertyBag } from '../../src/core/model/Properties'; import { colliderShape } from '../../src/core/properties/collider'; import { Theme } from '../../src/ui'; -import { colliderColor } from '../../src/viewport/symbols'; +import { Graphics } from 'pixi.js'; +import type { DrawItem } from '../../src/viewport/items'; +import { COLLIDER_FILL_ALPHA, colliderColor, drawItem } from '../../src/viewport/symbols'; const colorOf = (props: PropertyBag) => colliderColor(colliderShape(props, IDENTITY), props); @@ -21,3 +23,33 @@ describe('the colour of a collider in the scene view, outline and icon alike', ( expect(colorOf({ shape: 'polygon', points: bowtie, isTrigger: true })).toBe(Theme.colors.danger); }); }); + +describe('the area of a collider in the scene view', () => { + const item = { + id: 'c', + type: 'collider', + m: IDENTITY, + props: { shape: 'box', width: 40, height: 20 }, + frame: null, + } as unknown as DrawItem; + const actions = (selected: boolean) => { + const node = new Graphics(); + drawItem(node, item, { w: 40, h: 20 }, 1, selected); + return node.context.instructions.map((i) => ({ + action: i.action, + alpha: (i.data.style as { alpha: number }).alpha, + })); + }; + + it('is only an outline while the collider is not selected', () => { + expect(actions(false)).toEqual([{ action: 'stroke', alpha: 1 }]); + }); + + it('is filled, more strongly than before, once the collider itself is selected', () => { + expect(COLLIDER_FILL_ALPHA).toBeGreaterThan(0.12); + expect(actions(true)).toEqual([ + { action: 'fill', alpha: COLLIDER_FILL_ALPHA }, + { action: 'stroke', alpha: 1 }, + ]); + }); +});