gdharness 0.5.11 → 0.6.0

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.
package/build/cli.js CHANGED
@@ -15863,7 +15863,7 @@ var init_tool_definitions = __esm(() => {
15863
15863
  requires: [],
15864
15864
  operations: {
15865
15865
  click: {
15866
- summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed",
15866
+ summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved",
15867
15867
  requires: ["nodePath"]
15868
15868
  },
15869
15869
  action: { summary: "press or release an action", requires: ["action"] },
@@ -12,6 +12,10 @@ const TO_SMALL: int = 32
12
12
  const NEWLINE: int = 10
13
13
  const TAB: int = 9
14
14
 
15
+ ## What a game started without a window gets whatever the project settings say, and the usual
16
+ ## reason a control is out of reach. Only worth telling somebody when it is what they have.
17
+ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
18
+
15
19
  var _host: Node
16
20
  var _values: Values
17
21
 
@@ -216,10 +220,33 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
216
220
  }
217
221
 
218
222
 
223
+ ## Scrolls whatever is holding [param control] until it is on screen, and answers whether
224
+ ## anything moved.
225
+ ##
226
+ ## Innermost container first and outwards, because ensuring visibility inside an inner one moves
227
+ ## the control within the outer one, so the outer has to be asked after the inner has finished
228
+ ## moving it. A control with no ScrollContainer over it moves nothing and answers false, which is
229
+ ## what keeps the refusal below saying the right thing about a control that is simply off screen.
230
+ static func _scroll_into_view(control: Control) -> bool:
231
+ var moved: bool = false
232
+ var walking: Node = control.get_parent()
233
+ while walking != null:
234
+ var holder: ScrollContainer = walking as ScrollContainer
235
+ if holder != null:
236
+ holder.ensure_control_visible(control)
237
+ moved = true
238
+ walking = walking.get_parent()
239
+ return moved
240
+
241
+
219
242
  ## A whole click on a Control: the pointer moves onto it, the button goes down, a frame passes,
220
243
  ## the button comes up. BaseButton fires on the release, which is why a single injected press
221
244
  ## never pressed anything. The position is the control's centre carried into window pixels, so
222
245
  ## the caller never has to do that arithmetic.
246
+ ##
247
+ ## A control out of sight inside a ScrollContainer is scrolled to first; the answer says so under
248
+ ## `scrolled_into_view`, because the view having moved is a thing that happened to the screen and
249
+ ## the caller is the only one who can tell whether that matters.
223
250
  func click(params: Dictionary) -> Dictionary:
224
251
  var node_path: String = str(params.get("path", ""))
225
252
  if node_path.is_empty():
@@ -236,6 +263,19 @@ func click(params: Dictionary) -> Dictionary:
236
263
 
237
264
  var viewport: Viewport = control.get_viewport()
238
265
  var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
266
+
267
+ # A control below the fold of a ScrollContainer is not out of reach, it is one scroll away,
268
+ # which is what a person does without thinking about it before they click. Refusing it
269
+ # instead sent callers to emit the button's own signal, which presses nothing, runs none of
270
+ # the input path and reports success.
271
+ var scrolled: bool = false
272
+ if not viewport.get_visible_rect().has_point(centre):
273
+ scrolled = _scroll_into_view(control)
274
+ if scrolled:
275
+ # A container moves its child on the next layout pass rather than inside the call.
276
+ await _host.get_tree().process_frame
277
+ centre = control.get_global_transform_with_canvas() * (control.size * 0.5)
278
+
239
279
  var position: Vector2 = viewport.get_final_transform() * centre
240
280
  # The GUI only delivers to what is inside the viewport, so a centre outside it would be a
241
281
  # click that silently reached nothing. A game with no window has a 64 by 64 viewport
@@ -243,11 +283,17 @@ func click(params: Dictionary) -> Dictionary:
243
283
  # something the caller can read off the rect on its own.
244
284
  if not viewport.get_visible_rect().has_point(centre):
245
285
  var why: String = ""
246
- if not _host.get_tree().root.can_draw():
247
- why = (
248
- ". This game has no window, and a game with no window has a 64 by 64 viewport "
249
- + "whatever the project settings say: run it with a window to reach this control"
250
- )
286
+ if scrolled:
287
+ why = ". It was scrolled as far as what holds it goes and is still out there"
288
+ elif not _host.get_tree().root.can_draw():
289
+ why = ". This game has no window: run it with a window to reach this control"
290
+ # Only where it is true. The rect is printed just above, so claiming 64 by 64 over a
291
+ # viewport somebody has resized says two different things in one sentence.
292
+ if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
293
+ why = (
294
+ ". This game has no window, and a game with no window has a 64 by 64 viewport "
295
+ + "whatever the project settings say: run it with a window to reach this control"
296
+ )
251
297
  return {
252
298
  "type": "error",
253
299
  "message":
@@ -298,6 +344,7 @@ func click(params: Dictionary) -> Dictionary:
298
344
  "hovered": hovered_path,
299
345
  "landed": landed,
300
346
  "control_afterwards": afterwards,
347
+ "scrolled_into_view": scrolled,
301
348
  }
302
349
 
303
350
 
package/build/index.js CHANGED
@@ -28377,7 +28377,7 @@ var TOOL_SPECS = [
28377
28377
  requires: [],
28378
28378
  operations: {
28379
28379
  click: {
28380
- summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed",
28380
+ summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved",
28381
28381
  requires: ["nodePath"]
28382
28382
  },
28383
28383
  action: { summary: "press or release an action", requires: ["action"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.5.11",
3
+ "version": "0.6.0",
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",