extends SceneTree ## Does dragging an anchor marker in the rig lab move that anchor where the ## mouse went? ## ## The maths behind a viewport drag has four frames in it — screen, world, ## skeleton, gun — and every one is a chance to transpose an inverse or lose a ## handedness. All of those mistakes still MOVE the marker, so "the number ## changed" proves nothing. What is asserted here is that the number changed by ## the RIGHT AMOUNT, in the frame that knob is written in, derived independently ## from the camera. ## ## Deliberately NOT asserted: that the marker lands exactly under the mouse. It ## does not, and the reason is a real property of the hold rather than a bug in ## the drag — see THE FEEDBACK below. The screen-space check kept here is a ## direction-and-order-of-magnitude one, which is the band the transform ## mistakes above actually live in: a swapped axis or a lost handedness sends ## the marker the wrong way entirely. ## ## THE FEEDBACK. Every anchor hangs off the shoulder — `stock_pos = shoulder + ## pocket`, and the grip and fore anchors are measured out from there. The ## shoulder is driven by the arm, and the arm is chasing the anchor. So moving ## an anchor moves the shoulder, which moves the anchor again. Measured from a ## clean slate it settles at 0.77x-1.13x of the drag depending on which anchor, ## which is small enough to be invisible interactively — you stop dragging when ## it looks right. ## ## It is worth the paragraph because of how it first showed up. Without the ## `_reset` between cases below, each drag started on top of the last one still ## working its way through the arm, and the ratio read 1.5x-1.8x; waiting LONGER ## for the pose to settle made it worse rather than better, which is the ## opposite of how a settling error behaves and is what gave the compounding ## away. ## ## godot --path . -s res://debug/anchor_drag_check.gd const LAB := "res://debug/rig_lab.tscn" const DRAG := Vector2(60, 0) ## Metres. What the knob is checked to — this part is exact maths, so it can be. const KNOB_TOLERANCE := 0.0005 ## The screen-space check is direction and order of magnitude only. See above. const MIN_TRAVEL := 0.5 const MAX_TRAVEL := 2.2 var _fails := 0 func _init() -> void: await process_frame var lab: Node = load(LAB).instantiate() root.add_child(lab) # The pose layer chases its targets exponentially at a rate times DELTA, and # a headless run is uncapped, so each frame advances the blend by almost # nothing and the hold takes hundreds of frames to stop moving on its own. for _i in 300: await process_frame if lab._model == null or not lab._model.loaded or lab._model._pose_mod == null: _expect(false, "the lab built a character with a pose layer") _done() return _expect(true, "the lab built a character with a pose layer") # Frame the hands, so a pixel is a small distance in the world — a drag # measured at arm's length is mostly noise. lab._pivot = lab._model.skeleton.global_transform * lab._model._pose_mod.dbg_grip lab._dist = 0.6 lab._update_camera() for _i in 20: await process_frame for case in [[0, "trigger hand", "grip_shift"], [1, "support hand", "fore_shift"], [2, "buttstock", "pocket_hip"]]: await _drag_case(lab, case[0], case[1], case[2]) _done() func _drag_case(lab: Node, marker: int, label: String, key: String) -> void: # From a clean slate each time, or the second case measures the first's # shift still working its way through the arm. lab._reset("hold") for _i in 120: await process_frame var m: Node3D = lab._markers[marker] if not m.visible: _expect(false, "the %s marker is visible" % label) return var before: Vector2 = lab._cam.unproject_position(m.global_position) _expect(lab._marker_under(before) == marker, "the %s marker is grabbable where it is drawn" % label) # What the drag SHOULD write, worked out from the camera here rather than # from the lab's own code, so the two have to agree independently. var want: Vector3 = _expected(lab, marker, m.global_position, before, before + DRAG) var was: Vector3 = _knob(lab, key) lab._begin_drag(marker, before) # In steps, as a real drag arrives — a single jump would hide an error that # accumulates per motion event. for step in 6: lab._drag_to(before + DRAG * (float(step + 1) / 6.0)) await process_frame lab._drag_marker = -1 var wrote: Vector3 = _knob(lab, key) - was var err: float = (wrote - want).length() _expect(err <= KNOB_TOLERANCE, "the %s drag wrote %s into '%s' (wanted %s, off by %.2f mm)" % [label, _mm(wrote), key, _mm(want), err * 1000.0]) # ...and the marker really did go that way on screen. for _i in 150: await process_frame var now: Vector2 = lab._cam.unproject_position(m.global_position) var moved: Vector2 = now - before var along: float = moved.dot(DRAG.normalized()) / DRAG.length() _expect(along >= MIN_TRAVEL and along <= MAX_TRAVEL, "the %s marker followed the drag (%.2fx of it; the shoulder feedback puts this over 1)" % [label, along]) ## The knob delta a drag from `a` to `b` ought to produce, in that knob's frame. func _expected(lab: Node, marker: int, at: Vector3, a: Vector2, b: Vector2) -> Vector3: var world := _plane(lab._cam, at, b) - _plane(lab._cam, at, a) var v: Vector3 = lab._model.skeleton.global_transform.basis.inverse() * world if lab.MARKER_KNOB[marker][1] == "gun": v = lab._model._pose_mod.dbg_gun_basis.inverse() * v return v func _plane(cam: Camera3D, at: Vector3, mouse: Vector2) -> Vector3: var origin := cam.project_ray_origin(mouse) var dir := cam.project_ray_normal(mouse) var n := -cam.global_transform.basis.z return origin + dir * (((at - origin).dot(n)) / dir.dot(n)) ## An absent knob reads as its DEFAULT, not as zero. ## ## Those are the same thing for `grip_shift` and `fore_shift` and not for ## `pocket_hip`, whose default is (30, -70, 60) mm. Reading it as zero made a ## perfectly correct 60 px drag look like a 97 mm error — the difference was ## exactly the default. The lab has a note about this trap in `_reset`; it is ## just as easy to walk into from a test. func _knob(lab: Node, key: String) -> Vector3: var v = lab._knobs["hold"].get(key, WeaponHoldTuning.default_for(key)) return v if v is Vector3 else Vector3.ZERO func _mm(v: Vector3) -> String: return "(%.0f, %.0f, %.0f) mm" % [v.x * 1000.0, v.y * 1000.0, v.z * 1000.0] func _expect(ok: bool, what: String) -> void: if ok: print(" OK: %s" % what) else: print(" FAIL: %s" % what) _fails += 1 func _done() -> void: print("\n=== ANCHOR DRAG ===\nFailures: %d" % _fails) quit(1 if _fails > 0 else 0)