Scene view symbols come from SVG files: the behavior, container, rigidbody, camera and particles icons live in src/viewport/icons/ and are changed by editing the file and running npm run build; they are bundled, parsed once and drawn into a Graphics by the new module symbolIcons.ts, currentColor becomes the theme color of the symbol, the origin sits in the middle with a centered viewBox and the same units as before, so the symbols are pixel for pixel unchanged; what depends on the object (camera frame, collider outline, one-way cone) stays in code; unit tests keep the files to what Pixi's SVG parser and a browser draw alike (no transform, no polygon, fill always given), e2e 5c reads the five symbols in the theme color (#175)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25ec7879a0
commit
1c212b1ccc
12 files changed
+195
-72
No files matched your search
@@ -38,7 +38,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, 1092 checks (GM_STEPS=20 for one step)
|
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1093 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
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ import {
|
|||||||
import { gripAt, withCornerAfter, withCornerMoved, withoutCorner } from '../polygonEdit';
|
import { gripAt, withCornerAfter, withCornerMoved, withoutCorner } from '../polygonEdit';
|
||||||
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
||||||
import type { LayerFocus } from '../../viewport/tilemap';
|
import type { LayerFocus } from '../../viewport/tilemap';
|
||||||
import { CONE_LENGTH, dashedLine, drawSourceIcon, markBlockingSide, placeNode } from '../../viewport/symbols';
|
import { CONE_LENGTH, dashedLine, markBlockingSide, placeNode } from '../../viewport/symbols';
|
||||||
|
import { drawSymbolIcon } from '../../viewport/symbolIcons';
|
||||||
import { screenToWorld, type View, worldToScreen } from '../../viewport/view';
|
import { screenToWorld, type View, worldToScreen } from '../../viewport/view';
|
||||||
import { type CastRay, RAY_SHOWN, type World } from '../../runtime/World';
|
import { type CastRay, RAY_SHOWN, type World } from '../../runtime/World';
|
||||||
import { FreeViewport } from './FreeViewport';
|
import { FreeViewport } from './FreeViewport';
|
||||||
@@ -922,7 +923,7 @@ export class SceneViewport extends FreeViewport {
|
|||||||
if (!icon) {
|
if (!icon) {
|
||||||
icon = new Graphics();
|
icon = new Graphics();
|
||||||
icon.eventMode = 'none';
|
icon.eventMode = 'none';
|
||||||
drawSourceIcon(icon);
|
drawSymbolIcon(icon, 'particles');
|
||||||
this.icons.set(item.id, icon);
|
this.icons.set(item.id, icon);
|
||||||
this.overlay.addChild(icon);
|
this.overlay.addChild(icon);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-14 -14 28 28" width="28" height="28">
|
||||||
|
<path d="M0 -14 L14 0 L0 14 L-14 0 Z" fill="currentColor" />
|
||||||
|
<path d="M-6 -3 H-1 V3 H6" fill="none" stroke="#ffffff" stroke-width="2" />
|
||||||
|
<circle cx="-6" cy="-3" r="2" fill="#ffffff" />
|
||||||
|
<circle cx="6" cy="3" r="2" fill="#ffffff" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -10 40 20" width="40" height="20">
|
||||||
|
<rect x="-12" y="-8" width="24" height="16" fill="currentColor" />
|
||||||
|
<path d="M12 0 L20 -6 L20 6 Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 220 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-13 -13 26 26" width="26" height="26">
|
||||||
|
<rect x="-12" y="-12" width="24" height="24" rx="3" fill="currentColor" fill-opacity="0.25" stroke="currentColor" stroke-width="2" />
|
||||||
|
<rect x="-7" y="-7" width="6" height="6" fill="currentColor" />
|
||||||
|
<rect x="1" y="-7" width="6" height="6" fill="currentColor" />
|
||||||
|
<rect x="-7" y="1" width="6" height="6" fill="currentColor" />
|
||||||
|
<rect x="1" y="1" width="6" height="6" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 491 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 -15 30 30" width="30" height="30">
|
||||||
|
<!-- A source draws itself, so the ring stays hollow and the sparks sit outside it: its effect shows through the middle. -->
|
||||||
|
<circle cx="0" cy="0" r="10" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M8.485 8.485 L11.314 11.314 M-8.485 8.485 L-11.314 11.314 M8.485 -8.485 L11.314 -11.314 M-8.485 -8.485 L-11.314 -11.314" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<circle cx="12.728" cy="12.728" r="2" fill="currentColor" />
|
||||||
|
<circle cx="-12.728" cy="12.728" r="2" fill="currentColor" />
|
||||||
|
<circle cx="12.728" cy="-12.728" r="2" fill="currentColor" />
|
||||||
|
<circle cx="-12.728" cy="-12.728" r="2" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-13 -13 26 26" width="26" height="26">
|
||||||
|
<!-- A weight: a ring with an arrow pointing the way gravity pulls. -->
|
||||||
|
<circle cx="0" cy="0" r="12" fill="currentColor" fill-opacity="0.25" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M0 -6 V4" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M-5 1 L0 7 L5 1 Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* The fixed pictures of the scene view's symbols, kept as SVG files in `icons/` (#175). A file
|
||||||
|
* draws in the symbol's own units around its origin, and `currentColor` stands for the colour the
|
||||||
|
* theme gives the symbol – so the file still shows in any SVG viewer, and a theme change reaches it.
|
||||||
|
*/
|
||||||
|
import { type Graphics, SVGParser } from 'pixi.js';
|
||||||
|
import { Theme } from '../ui';
|
||||||
|
import behavior from './icons/behavior.svg?raw';
|
||||||
|
import camera from './icons/camera.svg?raw';
|
||||||
|
import container from './icons/container.svg?raw';
|
||||||
|
import particles from './icons/particles.svg?raw';
|
||||||
|
import rigidbody from './icons/rigidbody.svg?raw';
|
||||||
|
|
||||||
|
export type SymbolIcon = 'behavior' | 'camera' | 'container' | 'particles' | 'rigidbody';
|
||||||
|
|
||||||
|
export const SYMBOL_SVG: Record<SymbolIcon, string> = { behavior, camera, container, particles, rigidbody };
|
||||||
|
|
||||||
|
const SYMBOL_COLOR: Record<SymbolIcon, number> = {
|
||||||
|
behavior: Theme.colors.behavior,
|
||||||
|
camera: Theme.colors.textMuted,
|
||||||
|
container: Theme.colors.container,
|
||||||
|
particles: Theme.colors.particles,
|
||||||
|
rigidbody: Theme.colors.collider,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function tintSvg(svg: string, color: number): string {
|
||||||
|
return svg.replaceAll('currentColor', `#${color.toString(16).padStart(6, '0')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parsed once: a symbol is redrawn with every frame, and the text parse is what costs. */
|
||||||
|
const parsed = new Map<SymbolIcon, SVGElement>();
|
||||||
|
|
||||||
|
/** Adds the icon to what `node` already draws. */
|
||||||
|
export function drawSymbolIcon(node: Graphics, icon: SymbolIcon): void {
|
||||||
|
let svg = parsed.get(icon);
|
||||||
|
if (!svg) {
|
||||||
|
const text = tintSvg(SYMBOL_SVG[icon], SYMBOL_COLOR[icon]);
|
||||||
|
svg = new DOMParser().parseFromString(text, 'image/svg+xml').documentElement as unknown as SVGElement;
|
||||||
|
parsed.set(icon, svg);
|
||||||
|
}
|
||||||
|
SVGParser(svg, node.context);
|
||||||
|
}
|
||||||
+7
-69
@@ -1,14 +1,7 @@
|
|||||||
/** 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 {
|
import { placementOf, textAnchor } from '../core/properties/bounds';
|
||||||
BEHAVIOR_SYMBOL,
|
|
||||||
CONTAINER_SYMBOL,
|
|
||||||
PARTICLES_SYMBOL,
|
|
||||||
placementOf,
|
|
||||||
RIGIDBODY_SYMBOL,
|
|
||||||
textAnchor,
|
|
||||||
} from '../core/properties/bounds';
|
|
||||||
import {
|
import {
|
||||||
type ColliderShape,
|
type ColliderShape,
|
||||||
colliderShape,
|
colliderShape,
|
||||||
@@ -23,6 +16,7 @@ import { Theme } from '../ui';
|
|||||||
import type { DrawItem } from './items';
|
import type { DrawItem } from './items';
|
||||||
import { sliceGeometry, sliceKey } from './nineSlice';
|
import { sliceGeometry, sliceKey } from './nineSlice';
|
||||||
import { disposeShader, type ShaderSource, syncShader, wornShader } from './shaders';
|
import { disposeShader, type ShaderSource, syncShader, wornShader } from './shaders';
|
||||||
|
import { drawSymbolIcon } from './symbolIcons';
|
||||||
import type { TextureLibrary } from './TextureLibrary';
|
import type { TextureLibrary } from './TextureLibrary';
|
||||||
|
|
||||||
/** Screen-pixel half-width of the grab band along a collider's outline. */
|
/** Screen-pixel half-width of the grab band along a collider's outline. */
|
||||||
@@ -137,46 +131,13 @@ export function drawItem(
|
|||||||
node
|
node
|
||||||
.rect(-size.w / 2, -size.h / 2, size.w, size.h)
|
.rect(-size.w / 2, -size.h / 2, size.w, size.h)
|
||||||
.stroke({ width: 2 / scale, color: Theme.colors.textMuted });
|
.stroke({ width: 2 / scale, color: Theme.colors.textMuted });
|
||||||
node.rect(-12, -8, 24, 16).fill(Theme.colors.textMuted);
|
drawSymbolIcon(node, 'camera');
|
||||||
node.moveTo(12, 0).lineTo(20, -6).lineTo(20, 6).closePath().fill(Theme.colors.textMuted);
|
|
||||||
return;
|
return;
|
||||||
case 'behavior': {
|
case 'behavior':
|
||||||
const r = BEHAVIOR_SYMBOL / 2;
|
case 'container':
|
||||||
node.moveTo(0, -r).lineTo(r, 0).lineTo(0, r).lineTo(-r, 0).closePath().fill(Theme.colors.behavior);
|
case 'rigidbody':
|
||||||
node
|
drawSymbolIcon(node, item.type);
|
||||||
.moveTo(-6, -3)
|
|
||||||
.lineTo(-1, -3)
|
|
||||||
.lineTo(-1, 3)
|
|
||||||
.lineTo(6, 3)
|
|
||||||
.stroke({ width: 2, color: Theme.colors.accentText });
|
|
||||||
node.circle(-6, -3, 2).fill(Theme.colors.accentText);
|
|
||||||
node.circle(6, 3, 2).fill(Theme.colors.accentText);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
case 'container': {
|
|
||||||
const r = CONTAINER_SYMBOL / 2;
|
|
||||||
node
|
|
||||||
.roundRect(-r, -r, CONTAINER_SYMBOL, CONTAINER_SYMBOL, 3)
|
|
||||||
.fill({ color: Theme.colors.container, alpha: 0.25 })
|
|
||||||
.stroke({ width: 2, color: Theme.colors.container });
|
|
||||||
for (const [bx, by] of [
|
|
||||||
[-7, -7],
|
|
||||||
[1, -7],
|
|
||||||
[-7, 1],
|
|
||||||
[1, 1],
|
|
||||||
] as const)
|
|
||||||
node.rect(bx, by, 6, 6).fill(Theme.colors.container);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// A weight: a ring with an arrow pointing the way gravity pulls.
|
|
||||||
case 'rigidbody': {
|
|
||||||
const r = RIGIDBODY_SYMBOL / 2;
|
|
||||||
const color = Theme.colors.collider;
|
|
||||||
node.circle(0, 0, r).fill({ color, alpha: 0.25 }).stroke({ width: 2, color });
|
|
||||||
node.moveTo(0, -6).lineTo(0, 4).stroke({ width: 2, color });
|
|
||||||
node.moveTo(-5, 1).lineTo(0, 7).lineTo(5, 1).closePath().fill(color);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case 'text':
|
case 'text':
|
||||||
node.hitArea = new Rectangle(
|
node.hitArea = new Rectangle(
|
||||||
-size.w / 2 - (textAnchor(item.props) - 0.5) * size.w,
|
-size.w / 2 - (textAnchor(item.props) - 0.5) * size.w,
|
||||||
@@ -212,32 +173,9 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** How far the landing cone of a one-way side reaches, in screen pixels (#159). */
|
/** How far the landing cone of a one-way side reaches, in screen pixels (#159). */
|
||||||
export const CONE_LENGTH = 24;
|
export const CONE_LENGTH = 24;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
interface Bounds {
|
interface Bounds {
|
||||||
left: number;
|
left: number;
|
||||||
top: number;
|
top: number;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { SYMBOL_SVG, tintSvg } from '../../src/viewport/symbolIcons';
|
||||||
|
|
||||||
|
/** Opening tags as name + attributes; the files are flat, so no real XML parser is needed. */
|
||||||
|
function tags(svg: string): { name: string; attrs: Record<string, string> }[] {
|
||||||
|
return [...svg.matchAll(/<([a-z]+)([^>]*?)\/?>/g)].map(([, name, rest]) => ({
|
||||||
|
name: name!,
|
||||||
|
attrs: Object.fromEntries([...rest!.matchAll(/([a-zA-Z-]+)="([^"]*)"/g)].map(([, k, v]) => [k!, v!])),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('scene view symbol icons (#175)', () => {
|
||||||
|
const icons = Object.entries(SYMBOL_SVG);
|
||||||
|
|
||||||
|
it('has a file for every symbol', () => {
|
||||||
|
expect(Object.keys(SYMBOL_SVG).sort()).toEqual([
|
||||||
|
'behavior',
|
||||||
|
'camera',
|
||||||
|
'container',
|
||||||
|
'particles',
|
||||||
|
'rigidbody',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pixi draws the file's coordinates as they are and ignores the viewBox, so the viewBox only
|
||||||
|
// shows the file right elsewhere – centred on the origin, it matches what the scene view draws.
|
||||||
|
it.each(icons)('%s is centred on its origin', (_, svg) => {
|
||||||
|
const root = tags(svg)[0]!;
|
||||||
|
expect(root.name).toBe('svg');
|
||||||
|
const [x, y, w, h] = root.attrs.viewBox!.split(' ').map(Number);
|
||||||
|
expect([x, y]).toEqual([-w! / 2, -h! / 2]);
|
||||||
|
expect([Number(root.attrs.width), Number(root.attrs.height)]).toEqual([w, h]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(icons)('%s uses only what the Pixi parser draws like a browser', (_, svg) => {
|
||||||
|
for (const { name, attrs } of tags(svg).slice(1)) {
|
||||||
|
// polygon and polyline read their points as whole numbers; groups would carry transforms,
|
||||||
|
// which the parser ignores.
|
||||||
|
expect(['path', 'circle', 'rect', 'ellipse', 'line']).toContain(name);
|
||||||
|
expect(attrs.transform).toBeUndefined();
|
||||||
|
// Without a fill a browser fills black, Pixi does not fill at all once there is a stroke.
|
||||||
|
expect(attrs.fill, `${name} states its fill`).toBeDefined();
|
||||||
|
for (const paint of [attrs.fill, attrs.stroke].filter((p) => p !== undefined))
|
||||||
|
expect(paint).toMatch(/^(none|currentColor|#[0-9a-f]{6})$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts the theme colour where the file says currentColor', () => {
|
||||||
|
expect(tintSvg('<path fill="currentColor" stroke="currentColor"/>', 0x0000ff)).toBe(
|
||||||
|
'<path fill="#0000ff" stroke="#0000ff"/>',
|
||||||
|
);
|
||||||
|
for (const [, svg] of icons) expect(tintSvg(svg, 0xb066ff)).not.toContain('currentColor');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||||
|
|
||||||
/** Draw order: symbols always above sprites, `z` above hierarchy order. */
|
/** Draw order: symbols always above sprites, `z` above hierarchy order. */
|
||||||
export async function drawing(t) {
|
export async function drawing(t) {
|
||||||
const { check, json, shot, click, locate, typeText, press, undo, mutate, select, object } = t;
|
const { check, json, shot, click, locate, typeText, press, undo, mutate, select, object } = t;
|
||||||
@@ -48,4 +50,57 @@ export async function drawing(t) {
|
|||||||
await shot('14-z-order');
|
await shot('14-z-order');
|
||||||
await undo();
|
await undo();
|
||||||
check('undo restores z', (await object('o1')).position.z === 0, `z=${(await object('o1')).position.z}`);
|
check('undo restores z', (await object('o1')).position.z === 0, `z=${(await object('o1')).position.z}`);
|
||||||
|
|
||||||
|
// 5c. The symbols are SVG files (#175): one of each in a lane of its own, read where each has a
|
||||||
|
// solid colour of the theme. Offsets are in world units from the symbol's centre.
|
||||||
|
const PLACED = { 's-beh': 'mover', 's-box': 's-box-a', 's-rb': 's-rb-a', 's-cam': 'cam', 's-pt': 's-pt-a' };
|
||||||
|
const SPOTS = [
|
||||||
|
['s-beh', 8, 0, '#b066ff'],
|
||||||
|
['s-beh', -6, -3, '#ffffff'],
|
||||||
|
['s-box', -4, -4, '#3fb8c9'],
|
||||||
|
['s-box', 4, 4, '#3fb8c9'],
|
||||||
|
['s-rb', 0, 3, '#4fd18b'],
|
||||||
|
['s-rb', 0, -4, '#4fd18b'],
|
||||||
|
['s-cam', 0, 0, '#8a90a0'],
|
||||||
|
['s-cam', 17, 0, '#8a90a0'],
|
||||||
|
['s-pt', 12.728, 12.728, '#c26a1f'],
|
||||||
|
['s-pt', -12.728, -12.728, '#c26a1f'],
|
||||||
|
];
|
||||||
|
const ids = Object.keys(PLACED);
|
||||||
|
await mutate(`
|
||||||
|
const p = __gm.state.project;
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
${MAKE_OBJECT}
|
||||||
|
p.assets.push(
|
||||||
|
{ id: 's-box-a', type: 'container', name: 's-box-a', props: { enabled: true }, objects: [] },
|
||||||
|
{ id: 's-rb-a', type: 'rigidbody', name: 's-rb-a', props: { bodyType: 'static' } },
|
||||||
|
{ id: 's-pt-a', type: 'particles', name: 's-pt-a', props: {}, graph: { nodes: [], edges: [] } },
|
||||||
|
);
|
||||||
|
Object.entries(${JSON.stringify(PLACED)}).forEach(([id, assetId], i) => {
|
||||||
|
const o = make(id, assetId, '', ${LANES.symbols}, id);
|
||||||
|
o.position.x = (i - 2) * 120;
|
||||||
|
scene.objects.push(o);
|
||||||
|
});
|
||||||
|
__gm.state.select(null);
|
||||||
|
__gm.lookAt(0, ${LANES.symbols});`);
|
||||||
|
const wrong = await json(`(() => {
|
||||||
|
const zoom = __gm.objectRect('s-beh').w / 28;
|
||||||
|
return ${JSON.stringify(SPOTS)}.flatMap(([id, dx, dy, want]) => {
|
||||||
|
const r = __gm.objectRect(id);
|
||||||
|
const got = __gm.pixelAt(Math.floor(r.x + dx * zoom), Math.floor(r.y + dy * zoom));
|
||||||
|
return got === want ? [] : [id + ' ' + dx + ',' + dy + ': ' + got + ' != ' + want];
|
||||||
|
});
|
||||||
|
})()`);
|
||||||
|
check(
|
||||||
|
'every symbol draws its SVG in the theme colour',
|
||||||
|
wrong.length === 0,
|
||||||
|
wrong.join('; ') || `${SPOTS.length} spots`,
|
||||||
|
);
|
||||||
|
await shot('15-symbols');
|
||||||
|
await mutate(`
|
||||||
|
const p = __gm.state.project;
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
scene.objects = scene.objects.filter((o) => !${JSON.stringify(ids)}.includes(o.id));
|
||||||
|
p.assets = p.assets.filter((a) => !['s-box-a', 's-rb-a', 's-pt-a'].includes(a.id));
|
||||||
|
__gm.lookAt(0, 0);`);
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ export const LANES = {
|
|||||||
physics: -3500,
|
physics: -3500,
|
||||||
shapes: -3900,
|
shapes: -3900,
|
||||||
queries: -4300,
|
queries: -4300,
|
||||||
|
symbols: -4700,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user