Files
feuerwerk/src/core/model/sceneVisibility.ts
T
adminandClaude Opus 5.5 0c41cb97b1
game-maker / gate (push) Successful in 3m43s
game-maker / suite (push) Failing after 11m7s
Clip the children of an object to a clip area (#260)
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>
2026-09-29 07:08:00 +02:00

73 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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];
}