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
@@ -61,7 +61,8 @@ import {
|
||||
import { gripAt, withCornerAfter, withCornerMoved, withoutCorner } from '../polygonEdit';
|
||||
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
||||
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 { type CastRay, RAY_SHOWN, type World } from '../../runtime/World';
|
||||
import { FreeViewport } from './FreeViewport';
|
||||
@@ -922,7 +923,7 @@ export class SceneViewport extends FreeViewport {
|
||||
if (!icon) {
|
||||
icon = new Graphics();
|
||||
icon.eventMode = 'none';
|
||||
drawSourceIcon(icon);
|
||||
drawSymbolIcon(icon, 'particles');
|
||||
this.icons.set(item.id, 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. */
|
||||
import { type Container, type Graphics, Mesh, MeshGeometry, Rectangle, Texture } from 'pixi.js';
|
||||
import { determinant, IDENTITY, type Mat } from '../core/model/matrix';
|
||||
import {
|
||||
BEHAVIOR_SYMBOL,
|
||||
CONTAINER_SYMBOL,
|
||||
PARTICLES_SYMBOL,
|
||||
placementOf,
|
||||
RIGIDBODY_SYMBOL,
|
||||
textAnchor,
|
||||
} from '../core/properties/bounds';
|
||||
import { placementOf, textAnchor } from '../core/properties/bounds';
|
||||
import {
|
||||
type ColliderShape,
|
||||
colliderShape,
|
||||
@@ -23,6 +16,7 @@ import { Theme } from '../ui';
|
||||
import type { DrawItem } from './items';
|
||||
import { sliceGeometry, sliceKey } from './nineSlice';
|
||||
import { disposeShader, type ShaderSource, syncShader, wornShader } from './shaders';
|
||||
import { drawSymbolIcon } from './symbolIcons';
|
||||
import type { TextureLibrary } from './TextureLibrary';
|
||||
|
||||
/** Screen-pixel half-width of the grab band along a collider's outline. */
|
||||
@@ -137,46 +131,13 @@ export function drawItem(
|
||||
node
|
||||
.rect(-size.w / 2, -size.h / 2, size.w, size.h)
|
||||
.stroke({ width: 2 / scale, color: Theme.colors.textMuted });
|
||||
node.rect(-12, -8, 24, 16).fill(Theme.colors.textMuted);
|
||||
node.moveTo(12, 0).lineTo(20, -6).lineTo(20, 6).closePath().fill(Theme.colors.textMuted);
|
||||
drawSymbolIcon(node, 'camera');
|
||||
return;
|
||||
case 'behavior': {
|
||||
const r = BEHAVIOR_SYMBOL / 2;
|
||||
node.moveTo(0, -r).lineTo(r, 0).lineTo(0, r).lineTo(-r, 0).closePath().fill(Theme.colors.behavior);
|
||||
node
|
||||
.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);
|
||||
case 'behavior':
|
||||
case 'container':
|
||||
case 'rigidbody':
|
||||
drawSymbolIcon(node, item.type);
|
||||
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':
|
||||
node.hitArea = new Rectangle(
|
||||
-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). */
|
||||
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 {
|
||||
left: number;
|
||||
top: number;
|
||||
|
||||
Reference in new issue
Block a user