gdharness 1.1.40 → 1.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -288,6 +288,10 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
288
288
  ## the click scrolls it into view first: what is drawn over its place now is not what it will land
289
289
  ## under. Judged not covered at all instead, a button below the fold of a roster that a full-screen
290
290
  ## page covered was picked over the ones in view, scrolled up under the page and pressed there.
291
+ ## Whatever else the scroll holds moves with it and is judged where it goes. Judged where it sat, the
292
+ ## card at the top of a tray scrolled down was drawn over a button above the view, because a later
293
+ ## card is drawn over an earlier one, and a button below the view was passed by the same card only
294
+ ## because the cards beside the bottom edge come before it.
291
295
  ##
292
296
  ## [param areas] holds the box around each of [param drawn], so everything whose box misses the
293
297
  ## point is passed over before anything costlier is asked of it. A window's box is in the space of
@@ -295,7 +299,8 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
295
299
  ## so a window is judged by [method _window_over] instead.
296
300
  static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2]) -> Node:
297
301
  var viewport: Viewport = target.get_viewport()
298
- var point: Vector2 = _where_it_is_clicked(target)
302
+ var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
303
+ var point: Vector2 = _centre_of(target) + _moved_by(target, moves)
299
304
  var layer: int = _layer_of(target)
300
305
  for at: int in drawn.size():
301
306
  var other: Node = drawn[at]
@@ -304,9 +309,11 @@ static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2])
304
309
  if _window_over(target, point, window):
305
310
  return window
306
311
  continue
307
- if not areas[at].has_point(point):
308
- continue
309
312
  var control: Control = other
313
+ # The point where this control is now, which is where the scroll will carry it to the point.
314
+ var before: Vector2 = point - _moved_by(control, moves)
315
+ if not areas[at].has_point(before):
316
+ continue
310
317
  if control == target or control.is_ancestor_of(target) or target.is_ancestor_of(control):
311
318
  continue
312
319
  if control.get_viewport() != viewport:
@@ -314,18 +321,34 @@ static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2])
314
321
  var its_layer: int = _layer_of(control)
315
322
  if its_layer < layer or (its_layer == layer and not control.is_greater_than(target)):
316
323
  continue
317
- if _holds(control, point) and not _clipped_away(control, point):
324
+ if _holds(control, before) and not _clipped_away(control, point, moves):
318
325
  return control
319
326
  return null
320
327
 
321
328
 
322
- ## Where a click at [param target] lands in its viewport: its centre, or where scrolling will bring
323
- ## the centre when a ScrollContainer holding it clips it away, as the click scrolls it first.
324
- static func _where_it_is_clicked(target: Control) -> Vector2:
325
- var point: Vector2 = target.get_global_transform_with_canvas() * (target.size * 0.5)
326
- if _clipped_away(target, point):
327
- point = _where_it_lands(target, point)
328
- return point
329
+ ## [param target]'s centre in its viewport, where it is now.
330
+ static func _centre_of(target: Control) -> Vector2:
331
+ return target.get_global_transform_with_canvas() * (target.size * 0.5)
332
+
333
+
334
+ ## How far the click moves what each ScrollContainer holding [param target] holds, to bring it into
335
+ ## view: empty when no container clips it away, as nothing is scrolled then.
336
+ static func _scrolls_for(target: Control) -> Dictionary[ScrollContainer, Vector2]:
337
+ var none: Dictionary[ScrollContainer, Vector2] = {}
338
+ var centre: Vector2 = _centre_of(target)
339
+ if not _clipped_away(target, centre, none):
340
+ return none
341
+ return _where_it_lands(target, centre)
342
+
343
+
344
+ ## How far the scrolls in [param moves] carry [param node]: the sum over the containers holding it,
345
+ ## and nothing for a container itself, which stays where it is while what it holds moves.
346
+ static func _moved_by(node: Node, moves: Dictionary[ScrollContainer, Vector2]) -> Vector2:
347
+ var moved: Vector2 = Vector2.ZERO
348
+ for holder: ScrollContainer in moves:
349
+ if holder.is_ancestor_of(node):
350
+ moved += moves[holder]
351
+ return moved
329
352
 
330
353
 
331
354
  ## Whether the embedded [param window] is drawn over [param point], [param target]'s centre in its
@@ -366,14 +389,15 @@ static func _holds(control: Control, point: Vector2) -> bool:
366
389
  return Rect2(Vector2.ZERO, control.size).has_point(placed.affine_inverse() * point)
367
390
 
368
391
 
369
- ## Where [param target]'s centre, now at [param point], will be once the click has scrolled it into
370
- ## view: moved as little as puts the whole of it inside each ScrollContainer holding it, innermost
371
- ## first, which is how one brings a control into view. A target larger than a container is centred
372
- ## in it. Only a ScrollContainer, because that is all a click scrolls: another container clipping
373
- ## what it holds, a collapsed section for one, leaves the target where it is.
374
- static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
392
+ ## How far each ScrollContainer holding [param target] moves what it holds once the click has
393
+ ## scrolled [param target], centred at [param point], into view: as little as puts the whole of it
394
+ ## inside each, innermost first, which is how one brings a control into view. A target larger than a
395
+ ## container is centred in it. Only a ScrollContainer, because that is all a click scrolls: another
396
+ ## container clipping what it holds, a collapsed section for one, leaves the target where it is.
397
+ static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[ScrollContainer, Vector2]:
375
398
  var drawn: Transform2D = target.get_global_transform_with_canvas()
376
399
  var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
400
+ var moves: Dictionary[ScrollContainer, Vector2] = {}
377
401
  var landed: Vector2 = point
378
402
  var walk: Node = target.get_parent()
379
403
  while walk != null and not (walk is Viewport):
@@ -382,12 +406,15 @@ static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
382
406
  var shown: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
383
407
  var low: Vector2 = shown.position + half
384
408
  var high: Vector2 = shown.end - half
385
- landed = Vector2(
409
+ var next: Vector2 = Vector2(
386
410
  shown.get_center().x if low.x > high.x else clampf(landed.x, low.x, high.x),
387
411
  shown.get_center().y if low.y > high.y else clampf(landed.y, low.y, high.y)
388
412
  )
413
+ if next != landed:
414
+ moves[holder] = next - landed
415
+ landed = next
389
416
  walk = walk.get_parent()
390
- return landed
417
+ return moves
391
418
 
392
419
 
393
420
  ## Whether a click could not reach [param target] however it scrolled: its centre, where the click
@@ -395,8 +422,9 @@ static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
395
422
  ## container the click does not scroll. A drawer parked off the side of the screen, or anything
396
423
  ## outside the 64 by 64 viewport of a game with no window, was listed among the controls on screen.
397
424
  static func _off_screen(target: Control) -> bool:
398
- var point: Vector2 = _where_it_is_clicked(target)
399
- if _clipped_away(target, point) or not target.get_viewport().get_visible_rect().has_point(point):
425
+ var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
426
+ var point: Vector2 = _centre_of(target) + _moved_by(target, moves)
427
+ if _clipped_away(target, point, moves) or not target.get_viewport().get_visible_rect().has_point(point):
400
428
  return true
401
429
  var reached: Dictionary = Screen.reach(target.get_viewport(), point)
402
430
  var viewport: Viewport = reached["viewport"]
@@ -404,16 +432,19 @@ static func _off_screen(target: Control) -> bool:
404
432
  return not viewport.get_visible_rect().has_point(there)
405
433
 
406
434
 
407
- ## Whether a container clipping what it holds cuts [param point] off from [param control].
435
+ ## Whether a container clipping what it holds cuts [param point] off from [param control], once the
436
+ ## scrolls in [param moves] have carried each container wherever they carry it.
408
437
  ##
409
438
  ## Only up to the viewport [param control] is in. A dialog is a window, and the containers the game
410
439
  ## added it under are in another space: judged against a clipping one of those, every button of a
411
440
  ## confirmation was refused as off the screen while it sat in plain view.
412
- static func _clipped_away(control: Control, point: Vector2) -> bool:
441
+ static func _clipped_away(
442
+ control: Control, point: Vector2, moves: Dictionary[ScrollContainer, Vector2]
443
+ ) -> bool:
413
444
  var walk: Node = control.get_parent()
414
445
  while walk != null and not (walk is Viewport):
415
446
  var holder: Control = walk as Control
416
- if holder != null and holder.clip_contents and not _holds(holder, point):
447
+ if holder != null and holder.clip_contents and not _holds(holder, point - _moved_by(holder, moves)):
417
448
  return true
418
449
  walk = walk.get_parent()
419
450
  return false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.40",
3
+ "version": "1.1.41",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",