gdharness 1.2.31 → 1.2.32

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.
Files changed (27) hide show
  1. package/build/cli.js +7212 -6230
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
  5. package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
  6. package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
  8. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
  9. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -41
  11. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
  12. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +54 -120
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
  18. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
  20. package/build/godot/operations/file_reads.gd +237 -0
  21. package/build/godot/operations/gdscript_authoring.gd +136 -19
  22. package/build/godot/operations/godot_operations.gd +24 -0
  23. package/build/godot/operations/property_values.gd +767 -0
  24. package/build/godot/operations/rename_checks.gd +5 -2
  25. package/build/index.js +5437 -4469
  26. package/build/keeper.js +184 -71
  27. package/package.json +1 -1
@@ -11,8 +11,10 @@ const Values = preload("runtime_values.gd")
11
11
  ## reports are the same place by construction rather than by agreement.
12
12
  const Queries = preload("runtime_queries.gd")
13
13
  const Read = preload("reading.gd")
14
+ const Hover = preload("runtime_hover.gd")
14
15
  const Screen = preload("runtime_screen.gd")
15
16
  const Targets = preload("runtime_targets.gd")
17
+ const Mouse = preload("runtime_mouse.gd")
16
18
 
17
19
  ## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
18
20
  const TO_SMALL: int = 32
@@ -33,8 +35,12 @@ const PROCESS_MODES: Dictionary[int, String] = {
33
35
  ## Whether this run has had the pointer note in full; the game process is the run.
34
36
  static var _pointer_explained: bool = false
35
37
 
38
+ ## Whether this run has had the note on a supplied hover in full.
39
+ static var _hover_explained: bool = false
40
+
36
41
  var _host: Node
37
42
  var _values: Values
43
+ var _hover: Hover = Hover.new()
38
44
 
39
45
  ## Where the last injected pointer event put the pointer, once one has.
40
46
  ##
@@ -87,6 +93,32 @@ static func _pointer_noted(answer: Dictionary) -> Dictionary:
87
93
  return answer
88
94
 
89
95
 
96
+ ## [param answer], saying that the hover it reports was supplied here, when [param supplied]: see
97
+ ## runtime_hover.gd.
98
+ static func _hover_noted(answer: Dictionary, supplied: bool) -> Dictionary:
99
+ if not supplied:
100
+ return answer
101
+ answer["hover_supplied"] = true
102
+ if not _hover_explained:
103
+ _hover_explained = true
104
+ answer["hover_note"] = (
105
+ "This window has had no pointer enter from the operating system, and with subwindow"
106
+ + " embedding off Godot hovers nothing in a native window until it has, so a Button there"
107
+ + " takes no press. The runtime gave the controls under the pointer the hover the engine"
108
+ + " would have: their mouse_entered and mouse_exited, and the hover a Button presses on."
109
+ + " Viewport.gui_get_hovered_control() still answers null in this window. Said once a run;"
110
+ + " hover_supplied marks every answer it applies to."
111
+ )
112
+ return answer
113
+
114
+
115
+ ## Hovers what is under [param position] in the game's window, where the pointer events Input is
116
+ ## given go, when the engine cannot: see runtime_hover.gd.
117
+ func _follow_in_the_window(position: Vector2) -> bool:
118
+ var root: Window = _host.get_tree().root
119
+ return _hover.follow(root, root.get_final_transform().affine_inverse() * position)
120
+
121
+
90
122
  ## The movement a pointer arriving at [param position] carries, which is the distance from where
91
123
  ## the last injected event put it, and none at all for the first. The arrival is recorded, with how
92
124
  ## long it took: at least a frame, since two motions sent in one frame did not cross the screen in
@@ -242,22 +274,6 @@ func _glyph_of(keycode: Key, shifted: bool) -> int:
242
274
  return keycode
243
275
 
244
276
 
245
- ## A point the tool schema sends as two flat numbers, or the older form of one [x, y] value.
246
- ## Answers a Vector2, or the String that says what was wrong with it.
247
- func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: String) -> Variant:
248
- if params.has(x_key) and params.has(y_key):
249
- return Vector2(Read.as_float(params[x_key]), Read.as_float(params[y_key]))
250
- var raw: Variant = params.get(pair_key, Vector2.ZERO)
251
- if raw is Vector2:
252
- return raw
253
- if raw is Array:
254
- var pair: Array = raw
255
- if pair.size() < 2:
256
- return "%s array must contain [x, y]" % pair_key
257
- return Vector2(Read.as_float(pair[0]), Read.as_float(pair[1]))
258
- return "%s must be Vector2 or [x, y]" % pair_key
259
-
260
-
261
277
  ## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
262
278
  ##
263
279
  ## The whole click by default, for the reason [method inject_action] is the whole press, and
@@ -267,20 +283,20 @@ func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: Str
267
283
  ## `pressed` is how a caller asks for one half, the way a drag is made: true holds the button down,
268
284
  ## false lets go of one being held.
269
285
  func inject_mouse_click(params: Dictionary) -> Dictionary:
270
- var point: Variant = _read_point(params, "x", "y", "position")
286
+ var point: Variant = Mouse.read_point(params, "x", "y", "position")
271
287
  if point is String:
272
288
  return {"type": "error", "message": point}
273
289
  var position: Vector2 = point
274
- var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
290
+ var button: int = Mouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
275
291
  if button < 0:
276
- return _no_such_button(params.get("button"))
292
+ return Mouse.no_such_button(params.get("button"))
277
293
  var held: bool = Read.as_bool(params.get("pressed", true), true)
278
294
  var double: bool = Read.as_bool(params.get("doubleClick", false))
279
295
  # A wheel step is a press and a release together, as a mouse sends one: there is no holding a
280
296
  # wheel. A lone wheel press left the viewport's mouse focus on the control that took it, with
281
297
  # the wheel's bit in the focus mask, and every click after it landed on that control rather
282
298
  # than under the pointer, landing true and doing nothing, until a release was sent by hand.
283
- var notch: bool = held and _mask_of(button) == 0
299
+ var notch: bool = held and Mouse.mask_of(button) == 0
284
300
  var whole: bool = notch or not params.has("pressed")
285
301
  var holding: Variant = Values.held_for(params)
286
302
  if holding is String:
@@ -295,6 +311,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
295
311
  # A button pressed somewhere puts the pointer there: the motion after a held press measures
296
312
  # its drag from the press, which is where a real pointer would be.
297
313
  var _moved: Vector2 = _arrive(position)
314
+ var supplied: bool = _follow_in_the_window(position)
298
315
  Input.parse_input_event(_button(position, button, whole or held, double))
299
316
  if notch:
300
317
  Input.parse_input_event(_button(position, button, false, false))
@@ -306,16 +323,19 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
306
323
  await _host.get_tree().process_frame
307
324
 
308
325
  return _pointer_noted(
309
- {
310
- "type": "input_injected",
311
- "input_type": "mouse_click",
312
- "held_frames": Values.held_answer(whole and not notch, frames),
313
- "position": [position.x, position.y],
314
- "button": button,
315
- "pressed": held and not whole,
316
- "whole": whole,
317
- "double": double
318
- }
326
+ _hover_noted(
327
+ {
328
+ "type": "input_injected",
329
+ "input_type": "mouse_click",
330
+ "held_frames": Values.held_answer(whole and not notch, frames),
331
+ "position": [position.x, position.y],
332
+ "button": button,
333
+ "pressed": held and not whole,
334
+ "whole": whole,
335
+ "double": double
336
+ },
337
+ supplied
338
+ )
319
339
  )
320
340
 
321
341
 
@@ -328,7 +348,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
328
348
  ## would have carried. A relative the caller gives is kept, since a game may want to read a motion
329
349
  ## the position does not show.
330
350
  func inject_mouse_motion(params: Dictionary) -> Dictionary:
331
- var point: Variant = _read_point(params, "x", "y", "position")
351
+ var point: Variant = Mouse.read_point(params, "x", "y", "position")
332
352
  if point is String:
333
353
  return {"type": "error", "message": point}
334
354
  var position: Vector2 = point
@@ -338,20 +358,87 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
338
358
  Read.as_float(params.get("relativeX", 0.0)), Read.as_float(params.get("relativeY", 0.0))
339
359
  )
340
360
  elif params.has("relative"):
341
- var movement: Variant = _read_point(params, "relativeX", "relativeY", "relative")
361
+ var movement: Variant = Mouse.read_point(params, "relativeX", "relativeY", "relative")
342
362
  if movement is String:
343
363
  return {"type": "error", "message": movement}
344
364
  relative = movement
345
365
 
366
+ var supplied: bool = _follow_in_the_window(position)
346
367
  Input.parse_input_event(_motion(position, relative))
347
368
 
348
369
  return _pointer_noted(
349
- {
350
- "type": "input_injected",
351
- "input_type": "mouse_motion",
352
- "position": [position.x, position.y],
353
- "relative": [relative.x, relative.y]
370
+ _hover_noted(
371
+ {
372
+ "type": "input_injected",
373
+ "input_type": "mouse_motion",
374
+ "position": [position.x, position.y],
375
+ "relative": [relative.x, relative.y]
376
+ },
377
+ supplied
378
+ )
379
+ )
380
+
381
+
382
+ ## Drags with a mouse button from one point to another, through any points between, as a hand does:
383
+ ## the press, one motion each process frame along the stroke, and the release where the last motion
384
+ ## left the pointer, a frame after it. A game acting on every motion it gets sees as many as a real
385
+ ## stroke over that many frames sends. Made of calls, a stroke cost a round trip a motion, so the
386
+ ## one a caller could afford was three or four motions where a hand sends dozens, and a stir that
387
+ ## moves a few cells a motion moved two for a stroke.
388
+ ##
389
+ ## Each motion goes in a frame of its own because the engine merges the motions it is given within
390
+ ## one frame into a single event, as it does a real mouse's, and a game counting motions counts one.
391
+ func inject_mouse_drag(params: Dictionary) -> Dictionary:
392
+ var stroke: Variant = Mouse.stroke_of(params)
393
+ if stroke is String:
394
+ return {"type": "error", "message": stroke}
395
+ var points: Array[Vector2] = stroke
396
+ var button: int = Mouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
397
+ if button < 0:
398
+ return Mouse.no_such_button(params.get("button"))
399
+ var own: int = Mouse.mask_of(button)
400
+ if own == 0:
401
+ return {
402
+ "type": "error",
403
+ "message": "A wheel step has no held position to drag from: give left, right or middle."
404
+ }
405
+ var frames: int = Read.as_int(params.get("frames", 0), 0)
406
+ if frames < 1 or frames > Values.MOST_HELD:
407
+ return {
408
+ "type": "error", "message": "frames is %d, and it takes 1 to %d." % [frames, Values.MOST_HELD]
354
409
  }
410
+
411
+ var lengths: Array[float] = Mouse.lengths_of(points)
412
+ var start: Vector2 = points[0]
413
+ var end: Vector2 = points[points.size() - 1]
414
+ var _moved: Vector2 = _arrive(start)
415
+ var supplied: bool = _follow_in_the_window(start)
416
+ Input.parse_input_event(_button(start, button, true, false))
417
+ var travelled: Vector2 = Vector2.ZERO
418
+ for step: int in range(1, frames + 1):
419
+ await _host.get_tree().process_frame
420
+ var at: Vector2 = Mouse.along(points, lengths, float(step) / frames)
421
+ var relative: Vector2 = _arrive(at)
422
+ travelled += relative
423
+ supplied = _follow_in_the_window(at) or supplied
424
+ Input.parse_input_event(_motion(at, relative))
425
+ await _host.get_tree().process_frame
426
+ Input.parse_input_event(_button(end, button, false, false))
427
+ await _host.get_tree().process_frame
428
+
429
+ return _pointer_noted(
430
+ _hover_noted(
431
+ {
432
+ "type": "input_injected",
433
+ "input_type": "mouse_drag",
434
+ "from": [start.x, start.y],
435
+ "to": [end.x, end.y],
436
+ "button": button,
437
+ "motions": frames,
438
+ "relative_total": [travelled.x, travelled.y],
439
+ },
440
+ supplied
441
+ )
355
442
  )
356
443
 
357
444
 
@@ -507,15 +594,16 @@ func click(params: Dictionary) -> Dictionary:
507
594
  var out_there: String = _out_of_reach(control, node_path, local, reached, scrolled)
508
595
  if not out_there.is_empty():
509
596
  return {"type": "error", "message": out_there}
510
- var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
597
+ var button: int = Mouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
511
598
  if button < 0:
512
- return _no_such_button(params.get("button"))
599
+ return Mouse.no_such_button(params.get("button"))
513
600
  var double: bool = Read.as_bool(params.get("double", false))
514
601
 
515
602
  # Pushed into the viewport rather than through Input: Input accumulates events and flushes
516
603
  # them at the next frame, so the hovered control read below would be the one from before
517
604
  # the pointer moved. The viewport delivers it to the GUI the same way a real one arrives.
518
605
  viewport.push_input(_motion(position, _arrive(position)))
606
+ var supplied: bool = _hover.follow(viewport, centre)
519
607
  # What the engine itself thinks is under the pointer, which is the answer to "did it land",
520
608
  # read before the press so the caller learns about a control on top rather than a click
521
609
  # that went to it. Read in full here, path included, because nothing about that control
@@ -526,7 +614,7 @@ func click(params: Dictionary) -> Dictionary:
526
614
  # and the window keeps the GUI state. Reading the parent reported every dialog as not landed
527
615
  # while the button it was aimed at pressed perfectly well, which is the worst shape an answer
528
616
  # can have, since the caller believes the miss over what the game just did.
529
- var hovered: Control = control.get_viewport().gui_get_hovered_control()
617
+ var hovered: Control = _hover.hovered_in(control.get_viewport())
530
618
  var hovered_path: Variant = null
531
619
  if hovered != null:
532
620
  hovered_path = str(hovered.get_path())
@@ -584,7 +672,7 @@ func click(params: Dictionary) -> Dictionary:
584
672
  answer["link_pressed"] = not links.is_empty()
585
673
  if not links.is_empty():
586
674
  answer["link_meta"] = _values.serialize(links.back())
587
- return _pointer_noted(answer)
675
+ return _pointer_noted(_hover_noted(answer, supplied))
588
676
 
589
677
 
590
678
  ## Why [param control] would take no click a player gave it, or "" when it would.
@@ -761,12 +849,13 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
761
849
  )
762
850
  }
763
851
  var position: Vector2 = arriving.get_final_transform() * reached_point
764
- var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
852
+ var button: int = Mouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
765
853
  if button < 0:
766
- return _no_such_button(params.get("button"))
854
+ return Mouse.no_such_button(params.get("button"))
767
855
  var double: bool = Read.as_bool(params.get("double", false))
768
856
 
769
857
  arriving.push_input(_motion(position, _arrive(position)))
858
+ var supplied: bool = _hover.follow(arriving, reached_point)
770
859
  # Read before the press, for the reason the Control click reads it: what the caller needs to
771
860
  # know is whether a panel is sitting over the room, and the press is what would change it.
772
861
  var through: Dictionary = _through_the_interface(out)
@@ -778,19 +867,22 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
778
867
  await _host.get_tree().process_frame
779
868
 
780
869
  return _pointer_noted(
781
- {
782
- "type": "clicked",
783
- "path": node_path,
784
- "position": _values.serialize(position),
785
- "button": button,
786
- "double": double,
787
- "hovered": hovered_path,
788
- # The interface did not take it, so it reached the game's own input. As close to "it
789
- # landed" as anything outside the game can get, and said in the same word the Control
790
- # click says it in.
791
- "landed": through["landed"],
792
- "camera": found["camera"],
793
- }
870
+ _hover_noted(
871
+ {
872
+ "type": "clicked",
873
+ "path": node_path,
874
+ "position": _values.serialize(position),
875
+ "button": button,
876
+ "double": double,
877
+ "hovered": hovered_path,
878
+ # The interface did not take it, so it reached the game's own input. As close to "it
879
+ # landed" as anything outside the game can get, and said in the same word the
880
+ # Control click says it in.
881
+ "landed": through["landed"],
882
+ "camera": found["camera"],
883
+ },
884
+ supplied
885
+ )
794
886
  )
795
887
 
796
888
 
@@ -803,12 +895,12 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
803
895
  ## over what shows the one inside: the SubViewportContainer, with nothing drawn over it, or inside
804
896
  ## the embedded window. Read in the first viewport alone, a click beside a container that is smaller
805
897
  ## than what it shows, or under a panel drawn over the container, answered landed.
806
- static func _through_the_interface(out: Array[Dictionary]) -> Dictionary:
898
+ func _through_the_interface(out: Array[Dictionary]) -> Dictionary:
807
899
  var first_under: Variant = null
808
900
  for step: Dictionary in out:
809
901
  var viewport: Viewport = step["viewport"]
810
902
  var through: Variant = step["through"]
811
- var hovered: Control = viewport.gui_get_hovered_control()
903
+ var hovered: Control = _hover.hovered_in(viewport)
812
904
  if through is Window:
813
905
  var window: Window = through
814
906
  var point: Vector2 = step["point"]
@@ -879,53 +971,6 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
879
971
  event.pressed = pressed
880
972
  event.double_click = double
881
973
  var held: int = Input.get_mouse_button_mask()
882
- var own: int = _mask_of(button)
974
+ var own: int = Mouse.mask_of(button)
883
975
  event.button_mask = (held | own) if pressed else (held & ~own)
884
976
  return event
885
-
886
-
887
- ## The mask bit for a button, which is the engine's own mapping: left 1, right 2, middle 4, the
888
- ## two extra buttons 128 and 256. The wheel steps between have none.
889
- func _mask_of(button: int) -> int:
890
- if button >= MOUSE_BUTTON_WHEEL_UP and button <= MOUSE_BUTTON_WHEEL_RIGHT:
891
- return 0
892
- return 1 << (button - 1)
893
-
894
-
895
- ## What to say about a name that is not a mouse button, with the ones that are.
896
- func _no_such_button(named: Variant) -> Dictionary:
897
- return {
898
- "type": "error",
899
- "message":
900
- (
901
- "%s is not a mouse button. It takes: left, right, middle, wheel_up, wheel_down, or the number of one."
902
- % JSON.stringify(named)
903
- )
904
- }
905
-
906
-
907
- ## The button [param raw] names, or -1 for a name that is not one of them.
908
- ##
909
- ## A name nobody recognises used to come back as the left button, so a right click asked for by a
910
- ## spelling this does not know went to the left button and reported the left button. The caller
911
- ## reads the answer and believes it, which is the whole reason nothing here guesses.
912
- func _resolve_mouse_button(raw: Variant) -> int:
913
- if raw is String:
914
- var named: String = raw
915
- match named.to_lower():
916
- "left":
917
- return MOUSE_BUTTON_LEFT
918
- "right":
919
- return MOUSE_BUTTON_RIGHT
920
- "middle":
921
- return MOUSE_BUTTON_MIDDLE
922
- "wheel_up", "wheelup":
923
- return MOUSE_BUTTON_WHEEL_UP
924
- "wheel_down", "wheeldown":
925
- return MOUSE_BUTTON_WHEEL_DOWN
926
- _:
927
- return -1
928
- # The engine's buttons run from 1 to 9: 0 is MOUSE_BUTTON_NONE, which no event carries, and a
929
- # number past the two extra buttons names nothing a mask bit can be made for.
930
- var number: int = Read.as_int(raw, -1)
931
- return number if number >= MOUSE_BUTTON_LEFT and number <= MOUSE_BUTTON_XBUTTON2 else -1
@@ -0,0 +1,112 @@
1
+ extends RefCounted
2
+
3
+ ## What a pointer call names: a point, the stroke of a drag, a mouse button and its mask bit. Read
4
+ ## here and sent from runtime_input.gd, which keeps where the pointer is.
5
+
6
+ const Read = preload("reading.gd")
7
+
8
+
9
+ ## A point the tool schema sends as two flat numbers, or the older form of one [x, y] value.
10
+ ## Answers a Vector2, or the String that says what was wrong with it.
11
+ static func read_point(params: Dictionary, x_key: String, y_key: String, pair_key: String) -> Variant:
12
+ if params.has(x_key) and params.has(y_key):
13
+ return Vector2(Read.as_float(params[x_key]), Read.as_float(params[y_key]))
14
+ var raw: Variant = params.get(pair_key, Vector2.ZERO)
15
+ if raw is Vector2:
16
+ return raw
17
+ if raw is Array:
18
+ var pair: Array = raw
19
+ if pair.size() < 2:
20
+ return "%s array must contain [x, y]" % pair_key
21
+ return Vector2(Read.as_float(pair[0]), Read.as_float(pair[1]))
22
+ return "%s must be Vector2 or [x, y]" % pair_key
23
+
24
+
25
+ ## The points of a drag: x, y, then each of via, then toX, toY. Answers the String saying what is
26
+ ## wrong instead, since a point left out would otherwise read as the window's corner.
27
+ static func stroke_of(params: Dictionary) -> Variant:
28
+ for key: String in ["x", "y", "toX", "toY"]:
29
+ if not params.has(key):
30
+ return "mouse_drag needs x, y, toX and toY: %s is missing." % key
31
+ var points: Array[Vector2] = [Vector2(Read.as_float(params["x"]), Read.as_float(params["y"]))]
32
+ var via: Variant = params.get("via", [])
33
+ if not via is Array:
34
+ return "via must be a list of [x, y] points."
35
+ var between: Array = via
36
+ for point: Variant in between:
37
+ var pair: Array = point if point is Array else []
38
+ if pair.size() < 2:
39
+ return "via must be a list of [x, y] points, and holds %s." % JSON.stringify(point)
40
+ points.append(Vector2(Read.as_float(pair[0]), Read.as_float(pair[1])))
41
+ points.append(Vector2(Read.as_float(params["toX"]), Read.as_float(params["toY"])))
42
+ return points
43
+
44
+
45
+ ## The distance along [param points] to each of them from the first.
46
+ static func lengths_of(points: Array[Vector2]) -> Array[float]:
47
+ var lengths: Array[float] = [0.0]
48
+ for at: int in range(1, points.size()):
49
+ lengths.append(lengths[at - 1] + points[at - 1].distance_to(points[at]))
50
+ return lengths
51
+
52
+
53
+ ## The point [param share] of the way along the stroke through [param points], by distance, with
54
+ ## [param lengths] from [method lengths_of].
55
+ static func along(points: Array[Vector2], lengths: Array[float], share: float) -> Vector2:
56
+ var total: float = lengths[lengths.size() - 1]
57
+ if total == 0.0:
58
+ return points[0]
59
+ var wanted: float = total * share
60
+ for at: int in range(1, points.size()):
61
+ if lengths[at] >= wanted:
62
+ var span: float = lengths[at] - lengths[at - 1]
63
+ var part: float = 0.0 if span == 0.0 else (wanted - lengths[at - 1]) / span
64
+ return points[at - 1].lerp(points[at], part)
65
+ return points[points.size() - 1]
66
+
67
+
68
+ ## The mask bit for a button, which is the engine's own mapping: left 1, right 2, middle 4, the
69
+ ## two extra buttons 128 and 256. The wheel steps between have none.
70
+ static func mask_of(button: int) -> int:
71
+ if button >= MOUSE_BUTTON_WHEEL_UP and button <= MOUSE_BUTTON_WHEEL_RIGHT:
72
+ return 0
73
+ return 1 << (button - 1)
74
+
75
+
76
+ ## What to say about a name that is not a mouse button, with the ones that are.
77
+ static func no_such_button(named: Variant) -> Dictionary:
78
+ return {
79
+ "type": "error",
80
+ "message":
81
+ (
82
+ "%s is not a mouse button. It takes: left, right, middle, wheel_up, wheel_down, or the number of one."
83
+ % JSON.stringify(named)
84
+ )
85
+ }
86
+
87
+
88
+ ## The button [param raw] names, or -1 for a name that is not one of them.
89
+ ##
90
+ ## A name nobody recognises used to come back as the left button, so a right click asked for by a
91
+ ## spelling this does not know went to the left button and reported the left button. The caller
92
+ ## reads the answer and believes it, which is the whole reason nothing here guesses.
93
+ static func button_of(raw: Variant) -> int:
94
+ if raw is String:
95
+ var named: String = raw
96
+ match named.to_lower():
97
+ "left":
98
+ return MOUSE_BUTTON_LEFT
99
+ "right":
100
+ return MOUSE_BUTTON_RIGHT
101
+ "middle":
102
+ return MOUSE_BUTTON_MIDDLE
103
+ "wheel_up", "wheelup":
104
+ return MOUSE_BUTTON_WHEEL_UP
105
+ "wheel_down", "wheeldown":
106
+ return MOUSE_BUTTON_WHEEL_DOWN
107
+ _:
108
+ return -1
109
+ # The engine's buttons run from 1 to 9: 0 is MOUSE_BUTTON_NONE, which no event carries, and a
110
+ # number past the two extra buttons names nothing a mask bit can be made for.
111
+ var number: int = Read.as_int(raw, -1)
112
+ return number if number >= MOUSE_BUTTON_LEFT and number <= MOUSE_BUTTON_XBUTTON2 else -1
@@ -0,0 +1 @@
1
+ uid://ceeejawjqqy2j