A source wears an icon without becoming decoration (#89)
game-maker / suite (push) Successful in 9m49s
game-maker / suite (push) Successful in 9m49s
Behavior and Container draw an icon because isSymbolType() is true for them, but that predicate does not mean "draws an icon" — it means "is editor decoration". Four places read it that way: collectWorldItems, GameViewport. collectFrame and syncDrawOrder all drop symbols out of the running world and the game picture, and placementOf() gives them an upright unscaled box. Listing 'particles' there would have handed the source an icon and taken away its particles, in the scene view as well as in the game. So the two questions are two functions now. hasSceneIcon() says a type draws an icon; isSymbolType() keeps meaning decoration. The icon itself is a SceneViewport decoration, which is editor-only for free because GameViewport never syncs decorations. A ring (r 10, #c26a1f) with four sparks, upright and unscaled like the other symbols, its middle left open — unlike Behavior and Container a source draws something of its own, and the field is densest exactly there. The name sits beside it and a dashed line runs to the parent, same colour. Found while writing the tests: the source's grab box scaled with its transform while the icon did not, so the two came apart. placementOf() returns only the translation for 'particles', making the box the upright 24 x 24 that covers its icon. The source's own transform still feeds every particle untouched. 824 checks (821 + 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8ae20d044f
commit
78baf0c719
9 files changed
+177
-13
No files matched your search
@@ -37,7 +37,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:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 821 checks (GM_STEPS=20 for one step)
|
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 824 checks (GM_STEPS=20 for one step)
|
||||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ export function isSymbolType(type: AssetType): boolean {
|
|||||||
return type === 'camera' || type === 'behavior' || type === 'collider' || type === 'container';
|
return type === 'camera' || type === 'behavior' || type === 'collider' || type === 'container';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types the scene view marks with an icon of their own (#89). A source is *not* a symbol: its
|
||||||
|
* particles are scene content that has to survive into the running game, and `isSymbolType` is what
|
||||||
|
* throws a thing out of the game and out of the draw order. Only the icon is editor decoration.
|
||||||
|
*/
|
||||||
|
export function hasSceneIcon(type: AssetType): boolean {
|
||||||
|
return isSymbolType(type) || type === 'particles';
|
||||||
|
}
|
||||||
|
|
||||||
export function hasColorSwatch(type: AssetType): boolean {
|
export function hasColorSwatch(type: AssetType): boolean {
|
||||||
return type === 'sprite' || type === 'text';
|
return type === 'sprite' || type === 'text';
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* "Add collider", so all three agree on how big a thing is – and, through the object's transform, on
|
* "Add collider", so all three agree on how big a thing is – and, through the object's transform, on
|
||||||
* where its corners are.
|
* where its corners are.
|
||||||
*/
|
*/
|
||||||
import { type AssetType, isSymbolType } from '../model/AssetTypes';
|
import { type AssetType, hasSceneIcon } from '../model/AssetTypes';
|
||||||
import {
|
import {
|
||||||
apply,
|
apply,
|
||||||
boxCorners,
|
boxCorners,
|
||||||
@@ -71,11 +71,13 @@ export function footprintOf(type: AssetType, props: PropertyBag): { w: number; h
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* How an item of this type is drawn: with its whole transform, or – for the icons of behaviors,
|
* How an item of this type is drawn: with its whole transform, or – for the icons of behaviors,
|
||||||
* containers and the camera frame – upright and unscaled at its position, since an icon that turns
|
* containers, particle sources and the camera frame – upright and unscaled at its position, since an
|
||||||
* with its parent would only be harder to read. Colliders are symbols too but follow their body.
|
* icon that turns with its parent would only be harder to read. A source's own frame still goes into
|
||||||
|
* every particle; what stays upright is the square that can be grabbed, so it covers its icon.
|
||||||
|
* Colliders are symbols too but follow their body.
|
||||||
*/
|
*/
|
||||||
export function placementOf(type: AssetType, m: Mat): Mat {
|
export function placementOf(type: AssetType, m: Mat): Mat {
|
||||||
return isSymbolType(type) && type !== 'collider' ? translation(m.tx, m.ty) : m;
|
return hasSceneIcon(type) && type !== 'collider' ? translation(m.tx, m.ty) : m;
|
||||||
}
|
}
|
||||||
|
|
||||||
function anchorOffset(type: AssetType, props: PropertyBag, w: number): number {
|
function anchorOffset(type: AssetType, props: PropertyBag, w: number): number {
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
|||||||
/** Selection outlines in the Scene tab: a switched-off object is invisible but still framed. */
|
/** Selection outlines in the Scene tab: a switched-off object is invisible but still framed. */
|
||||||
gizmos: () => viewport.gizmoInfo(),
|
gizmos: () => viewport.gizmoInfo(),
|
||||||
layerOf: (id: string) => viewport.layerOf(id),
|
layerOf: (id: string) => viewport.layerOf(id),
|
||||||
|
/** The icon a particle source wears in the Scene tab, with its name and its owner link. */
|
||||||
|
sceneIcon: (id: string) => viewport.iconInfo(id),
|
||||||
drawOrder: () => viewport.drawOrder(),
|
drawOrder: () => viewport.drawOrder(),
|
||||||
objectScreen: (id: string) => viewport.objectScreen(id),
|
objectScreen: (id: string) => viewport.objectScreen(id),
|
||||||
objectRect: (id: string) => viewport.objectRect(id),
|
objectRect: (id: string) => viewport.objectRect(id),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { type FederatedPointerEvent, type FederatedWheelEvent, Graphics, Text } from 'pixi.js';
|
import { type FederatedPointerEvent, type FederatedWheelEvent, Graphics, Text } from 'pixi.js';
|
||||||
import { oneStep } from '../../core/commands/CompoundCommand';
|
import { oneStep } from '../../core/commands/CompoundCommand';
|
||||||
import { MoveSceneObjectCommand, PlaceAssetCommand } from '../../core/commands/sceneObjectCommands';
|
import { MoveSceneObjectCommand, PlaceAssetCommand } from '../../core/commands/sceneObjectCommands';
|
||||||
import { isSymbolType } from '../../core/model/AssetTypes';
|
import { hasSceneIcon } from '../../core/model/AssetTypes';
|
||||||
import { findObject, subtreeRoots } from '../../core/model/hierarchy';
|
import { findObject, subtreeRoots } from '../../core/model/hierarchy';
|
||||||
import { objectRef, resolvedHolder } from '../../core/model/instance';
|
import { objectRef, resolvedHolder } from '../../core/model/instance';
|
||||||
import { applyVector, type Extent, invert, linearOf, type Mat } from '../../core/model/matrix';
|
import { applyVector, type Extent, invert, linearOf, type Mat } from '../../core/model/matrix';
|
||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
zoomFactor,
|
zoomFactor,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
||||||
import { dashedLine } from '../../viewport/symbols';
|
import { dashedLine, drawSourceIcon } from '../../viewport/symbols';
|
||||||
import { freeView, screenToWorld, type View, worldToScreen } from '../../viewport/view';
|
import { freeView, screenToWorld, type View, worldToScreen } from '../../viewport/view';
|
||||||
import { type Frame, WorldViewport } from './WorldViewport';
|
import { type Frame, WorldViewport } from './WorldViewport';
|
||||||
|
|
||||||
@@ -75,11 +75,18 @@ export interface GizmoInfo {
|
|||||||
|
|
||||||
const OFF_GIZMO_ALPHA = 0.45;
|
const OFF_GIZMO_ALPHA = 0.45;
|
||||||
|
|
||||||
|
/** Owner links in the colour of the icon they leave; anything else in the muted default. */
|
||||||
|
const LINK_COLORS: Partial<Record<DrawItem['type'], number>> = {
|
||||||
|
behavior: Theme.colors.behavior,
|
||||||
|
particles: Theme.colors.particles,
|
||||||
|
};
|
||||||
|
|
||||||
export class SceneViewport extends WorldViewport {
|
export class SceneViewport extends WorldViewport {
|
||||||
private readonly grid = new Graphics();
|
private readonly grid = new Graphics();
|
||||||
private readonly links = new Graphics();
|
private readonly links = new Graphics();
|
||||||
private readonly gizmos = new Graphics();
|
private readonly gizmos = new Graphics();
|
||||||
private readonly labels = new Map<string, Text>();
|
private readonly labels = new Map<string, Text>();
|
||||||
|
private readonly icons = new Map<string, Graphics>();
|
||||||
/** World point at the viewport centre, and zoom. */
|
/** World point at the viewport centre, and zoom. */
|
||||||
private view = { x: 0, y: 0, zoom: 1 };
|
private view = { x: 0, y: 0, zoom: 1 };
|
||||||
private panning: { startX: number; startY: number; viewX: number; viewY: number } | null = null;
|
private panning: { startX: number; startY: number; viewX: number; viewY: number } | null = null;
|
||||||
@@ -87,6 +94,8 @@ export class SceneViewport extends WorldViewport {
|
|||||||
private marquee: Marquee | null = null;
|
private marquee: Marquee | null = null;
|
||||||
private dropPreview: DropPreview | null = null;
|
private dropPreview: DropPreview | null = null;
|
||||||
private lastGizmos: GizmoInfo[] = [];
|
private lastGizmos: GizmoInfo[] = [];
|
||||||
|
/** Ids that got an owner link this frame (test hook). */
|
||||||
|
private lastLinks: string[] = [];
|
||||||
|
|
||||||
constructor(state: EditorState) {
|
constructor(state: EditorState) {
|
||||||
super(state);
|
super(state);
|
||||||
@@ -382,12 +391,50 @@ export class SceneViewport extends WorldViewport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override syncDecorations(item: DrawItem, onScreen: boolean, scale: number, extent: Extent): void {
|
protected override syncDecorations(item: DrawItem, onScreen: boolean, scale: number, extent: Extent): void {
|
||||||
|
this.syncIcon(item, onScreen);
|
||||||
this.syncLabel(item, onScreen, scale, extent);
|
this.syncLabel(item, onScreen, scale, extent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override disposeDecorations(id: string): void {
|
protected override disposeDecorations(id: string): void {
|
||||||
this.labels.get(id)?.destroy();
|
this.labels.get(id)?.destroy();
|
||||||
this.labels.delete(id);
|
this.labels.delete(id);
|
||||||
|
this.icons.get(id)?.destroy();
|
||||||
|
this.icons.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The icon of a particle source, the name beside it and whether it hangs off its owner (test hook). */
|
||||||
|
iconInfo(id: string): { layer: string; visible: boolean; label: string; link: boolean } | null {
|
||||||
|
const icon = this.icons.get(id);
|
||||||
|
if (!icon) return null;
|
||||||
|
const label = this.labels.get(id);
|
||||||
|
return {
|
||||||
|
layer: icon.parent === this.overlay ? 'overlay' : 'objects',
|
||||||
|
visible: icon.visible,
|
||||||
|
label: label?.visible ? label.text : '',
|
||||||
|
link: this.lastLinks.includes(id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A source draws only its particles, so without a mark of its own an empty one is invisible and
|
||||||
|
* cannot be found (#89). The mark is a decoration rather than a symbol: `GameViewport` never syncs
|
||||||
|
* decorations, so it is editor-only without the source leaving the running game.
|
||||||
|
*/
|
||||||
|
private syncIcon(item: DrawItem, show: boolean): void {
|
||||||
|
let icon = this.icons.get(item.id);
|
||||||
|
if (!show || item.type !== 'particles') {
|
||||||
|
if (icon) icon.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!icon) {
|
||||||
|
icon = new Graphics();
|
||||||
|
icon.eventMode = 'none';
|
||||||
|
drawSourceIcon(icon);
|
||||||
|
this.icons.set(item.id, icon);
|
||||||
|
this.overlay.addChild(icon);
|
||||||
|
}
|
||||||
|
icon.visible = true;
|
||||||
|
icon.position.set(item.x, item.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncGrid(): void {
|
private syncGrid(): void {
|
||||||
@@ -412,12 +459,14 @@ export class SceneViewport extends WorldViewport {
|
|||||||
*/
|
*/
|
||||||
private drawParentLinks(items: DrawItem[], scale: number): void {
|
private drawParentLinks(items: DrawItem[], scale: number): void {
|
||||||
this.links.clear();
|
this.links.clear();
|
||||||
|
this.lastLinks = [];
|
||||||
const byId = new Map(items.map((i) => [i.id, i]));
|
const byId = new Map(items.map((i) => [i.id, i]));
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (!isSymbolType(item.type) || !item.parentId || item.type === 'collider' || !item.enabled) continue;
|
if (!hasSceneIcon(item.type) || !item.parentId || item.type === 'collider' || !item.enabled) continue;
|
||||||
const parent = byId.get(item.parentId);
|
const parent = byId.get(item.parentId);
|
||||||
if (!parent || !parent.enabled) continue;
|
if (!parent || !parent.enabled) continue;
|
||||||
const color = item.type === 'behavior' ? Theme.colors.behavior : Theme.colors.textMuted;
|
this.lastLinks.push(item.id);
|
||||||
|
const color = LINK_COLORS[item.type] ?? Theme.colors.textMuted;
|
||||||
dashedLine(this.links, item.x, item.y, parent.x, parent.y, 8 / scale, 6 / scale);
|
dashedLine(this.links, item.x, item.y, parent.x, parent.y, 8 / scale, 6 / scale);
|
||||||
this.links.stroke({ width: 1.5 / scale, color, alpha: 0.8 });
|
this.links.stroke({ width: 1.5 / scale, color, alpha: 0.8 });
|
||||||
this.links.circle(parent.x, parent.y, 3 / scale).fill(color);
|
this.links.circle(parent.x, parent.y, 3 / scale).fill(color);
|
||||||
@@ -426,7 +475,7 @@ export class SceneViewport extends WorldViewport {
|
|||||||
|
|
||||||
private syncLabel(item: DrawItem, show: boolean, scale: number, extent: Extent): void {
|
private syncLabel(item: DrawItem, show: boolean, scale: number, extent: Extent): void {
|
||||||
let label = this.labels.get(item.id);
|
let label = this.labels.get(item.id);
|
||||||
if (!show || !isSymbolType(item.type)) {
|
if (!show || !hasSceneIcon(item.type)) {
|
||||||
if (label) label.visible = false;
|
if (label) label.visible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const Theme = {
|
|||||||
collider: 0x4fd18b,
|
collider: 0x4fd18b,
|
||||||
trigger: 0xf2c84b,
|
trigger: 0xf2c84b,
|
||||||
container: 0x3fb8c9,
|
container: 0x3fb8c9,
|
||||||
|
/** Particle source icon and owner links; the same orange the graph gives the category. */
|
||||||
|
particles: 0xc26a1f,
|
||||||
marquee: 0x4c8dff,
|
marquee: 0x4c8dff,
|
||||||
/** A value deviating from what it follows: an override on a placed object, a non-default on an asset. */
|
/** A value deviating from what it follows: an override on a placed object, a non-default on an asset. */
|
||||||
override: 0xf2c84b,
|
override: 0xf2c84b,
|
||||||
|
|||||||
+30
-1
@@ -1,7 +1,13 @@
|
|||||||
/** Everything here draws in the item's own units at its origin; `placeNode` puts it into the world. */
|
/** Everything here draws in the item's own units at its origin; `placeNode` puts it into the world. */
|
||||||
import { type Container, type Graphics, Mesh, MeshGeometry, Rectangle, Texture } from 'pixi.js';
|
import { type Container, type Graphics, Mesh, MeshGeometry, Rectangle, Texture } from 'pixi.js';
|
||||||
import { determinant, IDENTITY, type Mat } from '../core/model/matrix';
|
import { determinant, IDENTITY, type Mat } from '../core/model/matrix';
|
||||||
import { BEHAVIOR_SYMBOL, CONTAINER_SYMBOL, placementOf, textAnchor } from '../core/properties/bounds';
|
import {
|
||||||
|
BEHAVIOR_SYMBOL,
|
||||||
|
CONTAINER_SYMBOL,
|
||||||
|
PARTICLES_SYMBOL,
|
||||||
|
placementOf,
|
||||||
|
textAnchor,
|
||||||
|
} from '../core/properties/bounds';
|
||||||
import { type ColliderShape, colliderShape, isTrigger } from '../core/properties/collider';
|
import { type ColliderShape, colliderShape, isTrigger } from '../core/properties/collider';
|
||||||
import { SHEET_EDGE } from '../core/shader/nodes';
|
import { SHEET_EDGE } from '../core/shader/nodes';
|
||||||
import { Theme } from '../ui';
|
import { Theme } from '../ui';
|
||||||
@@ -149,6 +155,29 @@ export function drawItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The icon of a particle source (#89), drawn at its position in world units like the other symbols.
|
||||||
|
* Unlike them it marks something that draws itself, so the ring stays hollow and the sparks sit
|
||||||
|
* outside it: the effect it belongs to shows through its middle.
|
||||||
|
*/
|
||||||
|
export function drawSourceIcon(node: Graphics): void {
|
||||||
|
const r = PARTICLES_SYMBOL / 2;
|
||||||
|
const color = Theme.colors.particles;
|
||||||
|
node.circle(0, 0, r - 2).stroke({ width: 2, color });
|
||||||
|
for (const [ux, uy] of DIAGONALS) {
|
||||||
|
node.moveTo(ux * r, uy * r).lineTo(ux * (r + 4), uy * (r + 4));
|
||||||
|
node.stroke({ width: 2, color });
|
||||||
|
node.circle(ux * (r + 6), uy * (r + 6), 2).fill(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIAGONALS = [
|
||||||
|
[Math.SQRT1_2, Math.SQRT1_2],
|
||||||
|
[-Math.SQRT1_2, Math.SQRT1_2],
|
||||||
|
[Math.SQRT1_2, -Math.SQRT1_2],
|
||||||
|
[-Math.SQRT1_2, -Math.SQRT1_2],
|
||||||
|
] as const;
|
||||||
|
|
||||||
/** Hit area covering only the outline band, in local coordinates and world units. */
|
/** Hit area covering only the outline band, in local coordinates and world units. */
|
||||||
function colliderOutlineHitArea(
|
function colliderOutlineHitArea(
|
||||||
shape: ColliderShape,
|
shape: ColliderShape,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '../../src/core/commands/sceneObjectCommands';
|
} from '../../src/core/commands/sceneObjectCommands';
|
||||||
import { worldPositionOf, worldTransformOf } from '../../src/core/graph';
|
import { worldPositionOf, worldTransformOf } from '../../src/core/graph';
|
||||||
import type { ContainerAsset } from '../../src/core/model/Asset';
|
import type { ContainerAsset } from '../../src/core/model/Asset';
|
||||||
|
import { hasSceneIcon, isSymbolType } from '../../src/core/model/AssetTypes';
|
||||||
import {
|
import {
|
||||||
apply,
|
apply,
|
||||||
decompose,
|
decompose,
|
||||||
@@ -180,5 +181,23 @@ describe('footprint under a transform', () => {
|
|||||||
// A behavior's diamond keeps its size and stays upright: the frame does not scale by 2.
|
// A behavior's diamond keeps its size and stays upright: the frame does not scale by 2.
|
||||||
const symbol = footprintExtent('behavior', {}, m);
|
const symbol = footprintExtent('behavior', {}, m);
|
||||||
expect(symbol.right - symbol.left).toBe(28);
|
expect(symbol.right - symbol.left).toBe(28);
|
||||||
|
// Since a source wears an icon of its own (#89), the square that can be grabbed covers it
|
||||||
|
// instead of growing with the source's scale.
|
||||||
|
const source = footprintExtent('particles', {}, m);
|
||||||
|
expect(source.right - source.left).toBe(24);
|
||||||
|
expect(footprintContains('particles', {}, m, { x: 111, y: 100 })).toBe(true);
|
||||||
|
expect(footprintContains('particles', {}, m, { x: 121, y: 100 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a source wears an icon without becoming a symbol', () => {
|
||||||
|
expect(hasSceneIcon('particles')).toBe(true);
|
||||||
|
// The decisive one: `isSymbolType` does not mean "has an icon", it means "editor decoration".
|
||||||
|
// Were a source one, `collectWorldItems` and the Game tab would both drop it and the effect
|
||||||
|
// would never be seen outside the Scene tab.
|
||||||
|
expect(isSymbolType('particles')).toBe(false);
|
||||||
|
for (const type of ['camera', 'behavior', 'collider', 'container'] as const)
|
||||||
|
expect(hasSceneIcon(type)).toBe(true);
|
||||||
|
expect(hasSceneIcon('sprite')).toBe(false);
|
||||||
|
expect(hasSceneIcon('text')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { colourDistance } from '../lib/harness.mjs';
|
||||||
import { LANES } from './lib/scaffold.mjs';
|
import { LANES } from './lib/scaffold.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,7 +518,58 @@ export async function particles(t) {
|
|||||||
);
|
);
|
||||||
await shot('31-particles-scene');
|
await shot('31-particles-scene');
|
||||||
|
|
||||||
// 31m. The graph tab draws no Pixi complaint of its own.
|
// 31m. (#89) A source draws only its particles, so an empty one would be invisible: it wears an
|
||||||
|
// icon of its own, in the overlay above the objects, with its name beside it.
|
||||||
|
const icon = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
||||||
|
check(
|
||||||
|
'the source wears an icon in the overlay, named beside it',
|
||||||
|
icon?.layer === 'overlay' && icon.visible === true && icon.label === 'Fountain',
|
||||||
|
JSON.stringify(icon),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 31n. (#89) The ring is hollow on purpose – the mark may not hide the effect it marks. Scanned
|
||||||
|
// outward from the source's own point, because a 2-unit stroke at an exact radius would be a
|
||||||
|
// test of rounding rather than of drawing.
|
||||||
|
const ICON_COLOUR = '#c26a1f';
|
||||||
|
const unit = (await json(`__gm.objectRect(${JSON.stringify(SOURCE)})`)).w / 24;
|
||||||
|
const row = [];
|
||||||
|
for (let dx = 0; dx <= Math.ceil(19 * unit); dx++)
|
||||||
|
row.push(await json(`__gm.pixelAt(${Math.round(spotOn.x + dx)}, ${Math.round(spotOn.y)})`));
|
||||||
|
const ringAt = row.findIndex((c, dx) => dx > 4 * unit && colourDistance(c, ICON_COLOUR) < 12);
|
||||||
|
check(
|
||||||
|
'the icon rings the source and leaves its middle to the particles',
|
||||||
|
ringAt > 0 && colourDistance(row[0], ICON_COLOUR) > 60 && row[0] !== '#22252c',
|
||||||
|
JSON.stringify({ centre: row[0], unit, ringAt, ring: row[ringAt] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 31o. (#89) Parented, it hangs off its owner by a dashed line like a behavior does – otherwise a
|
||||||
|
// source that moves with something else is a mark in mid-air.
|
||||||
|
await mutate(`
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
const anchor = structuredClone(scene.objects[0]);
|
||||||
|
Object.assign(anchor, { id: 'pt-anchor', name: 'Anchor', parentId: '',
|
||||||
|
position: { x: 0, y: ${LANE}, z: 0 }, overrides: {}, instanceEdits: {} });
|
||||||
|
scene.objects.push(anchor);
|
||||||
|
const source = scene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
|
source.parentId = 'pt-anchor';
|
||||||
|
source.position = { x: 200, y: 0, z: 0 };`);
|
||||||
|
await sleep(300);
|
||||||
|
const linked = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
||||||
|
await mutate(`
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
const source = scene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
|
source.parentId = '';
|
||||||
|
source.position = { x: 0, y: ${LANE}, z: 0 };
|
||||||
|
scene.objects = scene.objects.filter((o) => o.id !== 'pt-anchor');`);
|
||||||
|
await sleep(300);
|
||||||
|
const unlinked = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
||||||
|
check(
|
||||||
|
'a parented source hangs off its owner by a line, a free one by none',
|
||||||
|
linked?.link === true && unlinked?.link === false,
|
||||||
|
JSON.stringify({ linked, unlinked }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 31p. The graph tab draws no Pixi complaint of its own.
|
||||||
const logged = await json('__gm.console.lines()');
|
const logged = await json('__gm.console.lines()');
|
||||||
check(
|
check(
|
||||||
'nothing about the particle tab draws a complaint from Pixi',
|
'nothing about the particle tab draws a complaint from Pixi',
|
||||||
@@ -525,7 +577,7 @@ export async function particles(t) {
|
|||||||
JSON.stringify(logged.slice(0, 3)),
|
JSON.stringify(logged.slice(0, 3)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 31n. The step takes back everything it made, so the steps after it see the fixture again.
|
// 31q. The step takes back everything it made, so the steps after it see the fixture again.
|
||||||
await evaluate('__gm.particleTime(null)');
|
await evaluate('__gm.particleTime(null)');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await mutate(`
|
await mutate(`
|
||||||
|
|||||||
Reference in new issue
Block a user