Clip the children of an object to a clip area (#260)
game-maker / gate (push) Successful in 3m43s
game-maker / suite (push) Failing after 11m7s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-29 07:08:00 +02:00
1 parent 89aa92d2e3
commit 0c41cb97b1
32 files changed
+759 -27

No files matched your search

+17 -1
View File
@@ -85,6 +85,7 @@ export class World {
down: false,
pressed: false,
released: false,
wheel: 0,
};
/** Objects the editor is dragging (ids): the physics puts them where the drag does, at rest. */
held: ReadonlySet<string> = new Set();
@@ -373,6 +374,7 @@ export class World {
dropPointerEdges(): void {
this.pointer.pressed = false;
this.pointer.released = false;
this.pointer.wheel = 0;
}
/** Game view: where the mouse is (world units) and whether it is over the view at all. */
@@ -382,6 +384,11 @@ export class World {
this.pointer.inside = inside;
}
/** Game view: the wheel turned by `notches`; what turns within one tick adds up. */
turnWheel(notches: number): void {
this.pointer.wheel += notches;
}
/** Game view: the mouse button changed. Sets the edge flag the current tick reports. */
setPointerDown(down: boolean): void {
if (down === this.pointer.down) return;
@@ -404,7 +411,9 @@ export class World {
*/
hitTest(entity: Entity, x: number, y: number): boolean {
const point = { x, y };
const parts = [entity, ...this.descendantsOf(entity)].filter((e) => isHandleEnabled(e));
const parts = [entity, ...this.descendantsOf(entity)].filter(
(e) => isHandleEnabled(e) && this.insideClipAreas(e, point),
);
const colliders = parts.filter((e) => e.type === 'collider');
if (colliders.length > 0)
return colliders.some((c) =>
@@ -417,6 +426,13 @@ export class World {
);
}
/** Whether the point is inside every clip area above the entity (#260): what is cut off is not hit. */
private insideClipAreas(entity: Entity, point: Vec2): boolean {
for (let at = entity.parent; at; at = at.parent)
if (at.type === 'clip' && !footprintContains('clip', at.props, worldMatrixOf(at), point)) return false;
return true;
}
childrenOf(entity: Entity): Entity[] {
return this.entities.filter((e) => e.parent === entity);
}
+3
View File
@@ -473,6 +473,9 @@ export class BehaviorSystem implements System {
if (pointer.pressed && (owner ? world.hitTest(owner, pointer.x, pointer.y) : pointer.inside)) {
i.fire('OnClick', ctx, { x: pointer.x, y: pointer.y });
}
if (pointer.wheel !== 0 && (owner ? world.hitTest(owner, pointer.x, pointer.y) : pointer.inside)) {
i.fire('OnMouseWheel', ctx, { delta: pointer.wheel, x: pointer.x, y: pointer.y });
}
i.fire('OnUpdate', ctx, { dt });
},
dt,