An inventory grid could not scroll, because nothing could cut a child off at an edge. A game also had no way to hear the mouse wheel. Both are needed for a scrolling list, a dialog text or a window with more content than room. - A new object type Clip area has width and height and turns, scales and mirrors with its parent. The size gizmo drags width and height. - Everything below a clip area, on every level, is drawn only inside its rectangle, in the Scene view as in the game. Nested areas cut to their intersection. The rule works on a convex polygon, so a turned area cuts right as well. - On click, Is hovered and On mouse wheel do not hit a cut-off part. The Scene view does not pick it either. - In the Scene view the area is a dashed frame that is grabbed only at its edge, so a click inside lands on the content. The eye under Shape hides the frame. In the game the area is invisible. - While an object below an area or one of its parents is selected, the Scene view shows the cut-off part at 30 % opacity and it can be grabbed there. The rule lives in core/properties/clip.ts. - The Properties node knows the type Clip area. - The new event node On mouse wheel fires by the rule of On click. Its delta counts in notches, positive is down, and a frame's turns arrive as one value. It also gives x and y. Limits: - Only sprites and texts show the faded rest. Tilemaps and particles are cut and can be grabbed when open, but are not shown faded. - Masks by texture shape are out, as noted in the issue. Tests: - ./check.sh: 2773 tests, CHECK EXIT 0. - Full e2e run (local): ALL PASSED, 1549 checks, measured. - tests/core/clip-areas.test.ts: 12 tests for the cut rule, scene and game, save and load, hits, the wheel with and without owner, the notch conversion and the open rule. - e2e 58, 5 checks with real mouse and wheel: the scene cuts (74 of 74 red inside, 0 outside), a click on the cut part picks nothing, the selected rest is faded and grabbable, the game cuts, one notch logs 1. - e2e 04 and 46 count the object types and the eye rows. They now include Clip area. - Counter-checks: 6 in unit tests and 3 in the browser, all fail as expected. The originals were restored and checked with cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
/**
|
||
* What the scene view shows of each placeable type: its symbol, its name, and for a collider its
|
||
* outline. It is saved with the project, so whoever opens it sees the same scene, and a project
|
||
* written before there was such a field reads as the defaults.
|
||
*/
|
||
import { hasSceneIcon, type PlaceableAssetType } from './AssetTypes';
|
||
|
||
export type VisibilityPart = 'symbol' | 'label' | 'shape';
|
||
|
||
export type TypeVisibility = Record<VisibilityPart, boolean>;
|
||
|
||
export type SceneVisibility = Record<PlaceableAssetType, TypeVisibility>;
|
||
|
||
/** Every placeable type, in the order its name sorts – Behavior to Tilemap. */
|
||
export const VISIBILITY_TYPES: readonly PlaceableAssetType[] = [
|
||
'behavior',
|
||
'camera',
|
||
'clip',
|
||
'collider',
|
||
'container',
|
||
'particles',
|
||
'rigidbody',
|
||
'sprite',
|
||
'text',
|
||
'tilemap',
|
||
];
|
||
|
||
/**
|
||
* The switches a type has. Only a type with a symbol can hide it, every type can show its name,
|
||
* and only what collides has an outline to hide: a collider its shape, a tilemap the outline of
|
||
* its solid cells – the map's art stays. A sprite without its picture would be nothing at all.
|
||
*/
|
||
export function visibilityParts(type: PlaceableAssetType): VisibilityPart[] {
|
||
const parts: VisibilityPart[] = hasSceneIcon(type) ? ['symbol', 'label'] : ['label'];
|
||
if (type === 'collider' || type === 'tilemap' || type === 'clip') parts.push('shape');
|
||
return parts;
|
||
}
|
||
|
||
/** What a type shows until it is switched: all of it, except a name nothing showed before. */
|
||
export function defaultVisibility(type: PlaceableAssetType): TypeVisibility {
|
||
return { symbol: true, label: hasSceneIcon(type), shape: true };
|
||
}
|
||
|
||
export function defaultSceneVisibility(): SceneVisibility {
|
||
return Object.fromEntries(
|
||
VISIBILITY_TYPES.map((type) => [type, defaultVisibility(type)]),
|
||
) as SceneVisibility;
|
||
}
|
||
|
||
/** Read off untrusted JSON: every type and part that is missing or not a boolean is the default. */
|
||
export function sceneVisibilityOf(raw: unknown): SceneVisibility {
|
||
const stored = (raw as { sceneVisibility?: unknown } | null)?.sceneVisibility;
|
||
const out = defaultSceneVisibility();
|
||
if (!stored || typeof stored !== 'object') return out;
|
||
for (const type of VISIBILITY_TYPES) {
|
||
const entry = (stored as Record<string, unknown>)[type];
|
||
if (!entry || typeof entry !== 'object') continue;
|
||
for (const part of Object.keys(out[type]) as VisibilityPart[]) {
|
||
const value = (entry as Record<string, unknown>)[part];
|
||
if (typeof value === 'boolean') out[type][part] = value;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** Whether the scene view draws `part` of an object of `type`; a type without that part never does. */
|
||
export function shows(visibility: SceneVisibility, type: string, part: VisibilityPart): boolean {
|
||
const entry = (visibility as Partial<Record<string, TypeVisibility>>)[type];
|
||
if (!entry) return part !== 'label';
|
||
if (!visibilityParts(type as PlaceableAssetType).includes(part)) return part === 'shape';
|
||
return entry[part];
|
||
}
|