import { describe, expect, it } from 'vitest'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import { AddColliderCommand } from '../../src/core/commands/colliderCommands'; import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands'; import { footprintContains, footprintOf, setTextMeasure, textSize } from '../../src/core/properties/bounds'; import { IDENTITY } from '../../src/core/model/matrix'; import { makeSpriteScene, place, startWorld } from '../helpers'; describe('text objects', () => { it('estimates a text footprint from its content and font size where nothing measures', () => { expect(textSize({ text: 'ab', fontSize: 100 }).w).toBeCloseTo(110, 5); expect(textSize({ text: 'ab', fontSize: 100 }).h).toBeCloseTo(125, 5); expect(footprintOf('text', { text: 'abcd', fontSize: 100 }).w).toBeCloseTo(220, 5); }); it('widens or narrows the footprint by the letter spacing between its characters', () => { expect(textSize({ text: 'abcd\nab', fontSize: 100, letterSpacing: 10 }).w).toBeCloseTo(250, 5); expect(textSize({ text: 'abcd', fontSize: 100, letterSpacing: -6 }).w).toBeCloseTo(202, 5); expect(textSize({ text: 'a', fontSize: 100, letterSpacing: 10 }).w).toBeCloseTo(55, 5); }); it('takes the measured size, in the text’s own font, once something measures', () => { const asked: string[] = []; setTextMeasure((props, font) => { asked.push(font); return props.text === 'unmeasured' ? null : { w: 300, h: 80 }; }); try { expect(footprintOf('text', { text: 'Ag', fontSize: 100 }, null, 'face.ttf')).toEqual({ w: 300, h: 80 }); expect( footprintContains('text', { text: 'Ag', align: 'center' }, IDENTITY, { x: 140, y: 0 }, 'a.ttf'), ).toBe(true); expect(footprintContains('text', { text: 'Ag', align: 'center' }, IDENTITY, { x: 0, y: 45 })).toBe( false, ); expect(asked).toEqual(['face.ttf', 'a.ttf', '']); expect(textSize({ text: 'unmeasured', fontSize: 100 }).h).toBeCloseTo(125, 5); } finally { setTextMeasure(null); } }); }); describe('the mouse', () => { it('hit-tests an object through its colliders, else through its sprites and texts', () => { const f = makeSpriteScene('Button'); const { stack, sceneId } = f; const button = place(f, f.spriteId, { x: 0, y: 0 }); const label = new CreateAssetCommand('text', 'Label'); stack.execute(label); place(f, label.asset.id, { x: 0, y: 200 }, button); const { world } = startWorld(f); const entity = world.findByObjectId(button)!; expect(world.hitTest(entity, 10, 10)).toBe(true); // inside the 100×100 sprite expect(world.hitTest(entity, 10, 200)).toBe(true); // on the label parented below it expect(world.hitTest(entity, 400, 400)).toBe(false); // A collider takes over the hit test for the whole subtree. const collider = new AddColliderCommand(sceneId, button); stack.execute(collider); stack.execute(new SetOverrideCommand(sceneId, collider.objectId, 'width', 20)); world.syncFromModel(); expect(world.hitTest(entity, 5, 5)).toBe(true); expect(world.hitTest(entity, 40, 0)).toBe(false); // outside the 20-wide collider expect(world.hitTest(entity, 10, 200)).toBe(false); // the label no longer answers // Switched off, nothing of it can be hit. entity.props.enabled = false; expect(world.hitTest(entity, 5, 5)).toBe(false); }); it('reports pointer edges for exactly one tick', () => { const { world } = startWorld(makeSpriteScene()); world.movePointer(12, 34, true); world.setPointerDown(true); expect(world.pointer).toMatchObject({ x: 12, y: 34, inside: true, down: true, pressed: true }); world.tick(0.1); expect(world.pointer).toMatchObject({ down: true, pressed: false, released: false }); world.setPointerDown(false); expect(world.pointer).toMatchObject({ down: false, released: true }); }); });