diff --git a/server/api b/server/api index 778aa1cb..2c657a10 100755 Binary files a/server/api and b/server/api differ diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index d477a950..c7b976c9 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -906,10 +906,43 @@ func (s *ApiService) doDragMouse(ctx context.Context, body oapi.DragMouseRequest } } - // Phase 2: move along path (excluding first point) using fixed-count relative steps - // Insert a small delay between each relative move to smooth the drag - args2 := []string{} - // Determine per-segment steps and per-step delay from request (with defaults) + // Phase 2: move along path + useSmooth := body.Smooth == nil || *body.Smooth + if useSmooth { + if err := s.doDragMouseSmooth(ctx, log, body, btn, screenWidth, screenHeight); err != nil { + argsCleanup := []string{"mouseup", btn} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + argsCleanup = append(argsCleanup, "keyup", key) + } + } + _, _ = defaultXdoTool.Run(context.Background(), argsCleanup...) + return err + } + } else { + if err := s.doDragMouseLinear(ctx, log, body, btn); err != nil { + return err + } + } + + // Phase 3: mouseup and release modifiers + args3 := []string{"mouseup", btn} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args3 = append(args3, "keyup", key) + } + } + log.Info("executing xdotool (drag end)", "args", args3) + if output, err := defaultXdoTool.Run(ctx, args3...); err != nil { + log.Error("xdotool drag end failed", "err", err, "output", string(output)) + return &executionError{msg: fmt.Sprintf("failed to finish drag: %s", string(output))} + } + + return nil +} + +func (s *ApiService) doDragMouseLinear(ctx context.Context, log *slog.Logger, body oapi.DragMouseRequest, btn string) error { + start := body.Path[0] stepsPerSegment := 10 if body.StepsPerSegment != nil && *body.StepsPerSegment >= 1 { stepsPerSegment = *body.StepsPerSegment @@ -920,7 +953,6 @@ func (s *ApiService) doDragMouse(ctx context.Context, body oapi.DragMouseRequest } stepDelaySeconds := fmt.Sprintf("%.3f", float64(stepDelayMs)/1000.0) - // Precompute total number of relative steps so we can avoid a trailing sleep totalSteps := 0 prev := start for _, pt := range body.Path[1:] { @@ -930,6 +962,7 @@ func (s *ApiService) doDragMouse(ctx context.Context, body oapi.DragMouseRequest prev = pt } + args2 := []string{} prev = start stepIndex := 0 for _, pt := range body.Path[1:] { @@ -943,7 +976,6 @@ func (s *ApiService) doDragMouse(ctx context.Context, body oapi.DragMouseRequest } else { args2 = append(args2, "mousemove_relative", xStr, yStr) } - // add a tiny delay between moves, but not after the last step if stepIndex < totalSteps-1 && stepDelayMs > 0 { args2 = append(args2, "sleep", stepDelaySeconds) } @@ -955,7 +987,6 @@ func (s *ApiService) doDragMouse(ctx context.Context, body oapi.DragMouseRequest log.Info("executing xdotool (drag move)", "args", args2) if output, err := defaultXdoTool.Run(ctx, args2...); err != nil { log.Error("xdotool drag move failed", "err", err, "output", string(output)) - // Try to release button and modifiers argsCleanup := []string{"mouseup", btn} if body.HoldKeys != nil { for _, key := range *body.HoldKeys { @@ -966,23 +997,78 @@ func (s *ApiService) doDragMouse(ctx context.Context, body oapi.DragMouseRequest return &executionError{msg: fmt.Sprintf("failed during drag movement: %s", string(output))} } } + return nil +} - // Phase 3: mouseup and release modifiers - args3 := []string{"mouseup", btn} - if body.HoldKeys != nil { - for _, key := range *body.HoldKeys { - args3 = append(args3, "keyup", key) +func (s *ApiService) doDragMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.DragMouseRequest, btn string, screenWidth, screenHeight int) error { + if body.DurationMs != nil && (*body.DurationMs < 50 || *body.DurationMs > 10000) { + return &validationError{msg: "duration_ms must be between 50 and 10000"} + } + + waypoints := make([][2]int, len(body.Path)) + for i, pt := range body.Path { + waypoints[i] = [2]int{pt[0], pt[1]} + } + + result := mousetrajectory.GenerateMultiSegmentTrajectory(waypoints, screenWidth, screenHeight, body.DurationMs) + points := result.Points + baseDelayMs := result.StepDelayMs + + if len(points) < 2 { + return nil + } + + numSteps := len(points) - 1 + + // Build a single xdotool arg slice with inline sleep directives. + // Use smoothstep easing: slow at start (pickup) and end (placement), + // fast in the middle, matching natural human drag behavior. + args := []string{} + for i := 1; i <= numSteps; i++ { + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + if dx == 0 && dy == 0 { + continue + } + args = append(args, "mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy)) + + if i < numSteps { + delay := smoothStepDelay(i, numSteps, baseDelayMs*2, baseDelayMs/2) + jitter := delay + rand.Intn(5) - 2 + if jitter < 3 { + jitter = 3 + } + args = append(args, "sleep", fmt.Sprintf("%.3f", float64(jitter)/1000.0)) } } - log.Info("executing xdotool (drag end)", "args", args3) - if output, err := defaultXdoTool.Run(ctx, args3...); err != nil { - log.Error("xdotool drag end failed", "err", err, "output", string(output)) - return &executionError{msg: fmt.Sprintf("failed to finish drag: %s", string(output))} + + if len(args) > 0 { + log.Info("executing xdotool (smooth drag move)", "steps", numSteps, "segments", len(body.Path)-1) + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool smooth drag move failed", "err", err, "output", string(output)) + return &executionError{msg: "failed during smooth drag movement"} + } } + log.Info("executed smooth drag movement", "points", len(points), "segments", len(body.Path)-1) return nil } +// smoothStepDelay maps position i/n through a smoothstep curve to produce +// a delay in [fastMs, slowMs]. Slow at start and end, fast in the middle. +// smoothstep(t) = 3t² - 2t³ +func smoothStepDelay(i, n, slowMs, fastMs int) int { + if n <= 1 { + return slowMs + } + t := float64(i) / float64(n) + // Remap t so that 0 and 1 map to 1 (slow) and 0.5 maps to 0 (fast). + // Use distance from center: d = |2t - 1|, then smoothstep on d. + d := math.Abs(2*t - 1) + s := d * d * (3 - 2*d) // smoothstep + return fastMs + int(float64(slowMs-fastMs)*s) +} + func (s *ApiService) DragMouse(ctx context.Context, request oapi.DragMouseRequestObject) (oapi.DragMouseResponseObject, error) { s.inputMu.Lock() defer s.inputMu.Unlock() diff --git a/server/lib/mousetrajectory/mousetrajectory.go b/server/lib/mousetrajectory/mousetrajectory.go index 5bcc8a02..75adeb56 100644 --- a/server/lib/mousetrajectory/mousetrajectory.go +++ b/server/lib/mousetrajectory/mousetrajectory.go @@ -55,6 +55,113 @@ func (t *HumanizeMouseTrajectory) GetPointsInt() [][2]int { return out } +// MultiSegmentResult holds the generated trajectory and the per-step delay. +type MultiSegmentResult struct { + Points [][2]int + StepDelayMs int +} + +const defaultStepDelayMs = 10 + +// GenerateMultiSegmentTrajectory creates a human-like Bezier trajectory through +// a sequence of waypoints. Each consecutive pair gets its own Bezier curve, with +// point counts distributed proportionally to segment distance. The resulting +// points are clamped to [0, screenW-1] x [0, screenH-1]. +func GenerateMultiSegmentTrajectory(waypoints [][2]int, screenW, screenH int, totalDurationMs *int) MultiSegmentResult { + if len(waypoints) < 2 { + return MultiSegmentResult{Points: waypoints, StepDelayMs: defaultStepDelayMs} + } + + segDistances := make([]float64, len(waypoints)-1) + var totalDist float64 + for i := 1; i < len(waypoints); i++ { + dx := float64(waypoints[i][0] - waypoints[i-1][0]) + dy := float64(waypoints[i][1] - waypoints[i-1][1]) + d := math.Sqrt(dx*dx + dy*dy) + segDistances[i-1] = d + totalDist += d + } + + // Determine total number of points across all segments. + var totalPoints int + if totalDurationMs != nil && *totalDurationMs > 0 { + totalPoints = *totalDurationMs / defaultStepDelayMs + if totalPoints < MinPoints { + totalPoints = MinPoints + } + } else { + totalPoints = int(math.Min( + float64(defaultMaxPoints)*float64(len(waypoints)-1), + math.Max(float64(MinPoints), math.Pow(totalDist, 0.25)*pathLengthScale*float64(len(waypoints)-1)))) + } + + var allPoints [][2]int + + for i := 0; i < len(waypoints)-1; i++ { + // Distribute points proportionally to segment distance. + var segPoints int + if totalDist > 0 { + segPoints = int(math.Round(float64(totalPoints) * segDistances[i] / totalDist)) + } else { + segPoints = totalPoints / (len(waypoints) - 1) + } + if segPoints < MinPoints { + segPoints = MinPoints + } + if segPoints > MaxPoints { + segPoints = MaxPoints + } + + opts := &Options{MaxPoints: segPoints} + traj := NewHumanizeMouseTrajectoryWithOptions( + float64(waypoints[i][0]), float64(waypoints[i][1]), + float64(waypoints[i+1][0]), float64(waypoints[i+1][1]), + opts, + ) + segPts := traj.GetPointsInt() + + if i == 0 { + allPoints = append(allPoints, segPts...) + } else { + // Skip first point of subsequent segments to avoid duplicates at junctions. + if len(segPts) > 1 { + allPoints = append(allPoints, segPts[1:]...) + } + } + } + + // Clamp to screen bounds. + clampPoints(allPoints, screenW, screenH) + + stepDelay := defaultStepDelayMs + if totalDurationMs != nil && len(allPoints) > 1 { + stepDelay = *totalDurationMs / (len(allPoints) - 1) + if stepDelay < 3 { + stepDelay = 3 + } + } + + return MultiSegmentResult{Points: allPoints, StepDelayMs: stepDelay} +} + +// clampPoints constrains each point to [0, screenW-1] x [0, screenH-1]. +func clampPoints(points [][2]int, screenW, screenH int) { + maxX := screenW - 1 + maxY := screenH - 1 + for i := range points { + if points[i][0] < 0 { + points[i][0] = 0 + } else if points[i][0] > maxX { + points[i][0] = maxX + } + if points[i][1] < 0 { + points[i][1] = 0 + } else if points[i][1] > maxY { + points[i][1] = maxY + } + } +} + const ( // Bounds padding for Bezier control point region (pixels beyond start/end). boundsPadding = 80 diff --git a/server/lib/mousetrajectory/mousetrajectory_test.go b/server/lib/mousetrajectory/mousetrajectory_test.go index 7c8b7514..bef1c02e 100644 --- a/server/lib/mousetrajectory/mousetrajectory_test.go +++ b/server/lib/mousetrajectory/mousetrajectory_test.go @@ -1,6 +1,7 @@ package mousetrajectory import ( + "math" "testing" "github.com/stretchr/testify/assert" @@ -106,3 +107,70 @@ func TestHumanizeMouseTrajectory_CurvedPath(t *testing.T) { } assert.False(t, allOnLine, "path should be curved, not a straight line") } + +func TestGenerateMultiSegmentTrajectory_ThreeWaypoints(t *testing.T) { + waypoints := [][2]int{{100, 100}, {500, 300}, {900, 100}} + result := GenerateMultiSegmentTrajectory(waypoints, 1920, 1080, nil) + + require.GreaterOrEqual(t, len(result.Points), MinPoints*2, "multi-segment should produce enough points") + assert.Equal(t, 100, result.Points[0][0], "first point X should match first waypoint") + assert.Equal(t, 100, result.Points[0][1], "first point Y should match first waypoint") + assert.Equal(t, 900, result.Points[len(result.Points)-1][0], "last point X should match last waypoint") + assert.Equal(t, 100, result.Points[len(result.Points)-1][1], "last point Y should match last waypoint") +} + +func TestGenerateMultiSegmentTrajectory_TwoWaypoints(t *testing.T) { + waypoints := [][2]int{{0, 0}, {200, 200}} + result := GenerateMultiSegmentTrajectory(waypoints, 1920, 1080, nil) + + require.GreaterOrEqual(t, len(result.Points), MinPoints) + assert.Equal(t, 0, result.Points[0][0]) + assert.Equal(t, 0, result.Points[0][1]) + assert.Equal(t, 200, result.Points[len(result.Points)-1][0]) + assert.Equal(t, 200, result.Points[len(result.Points)-1][1]) +} + +func TestGenerateMultiSegmentTrajectory_WithDurationMs(t *testing.T) { + waypoints := [][2]int{{100, 100}, {500, 300}, {900, 100}} + dur := 2000 + result := GenerateMultiSegmentTrajectory(waypoints, 1920, 1080, &dur) + + require.GreaterOrEqual(t, len(result.Points), MinPoints) + assert.Greater(t, result.StepDelayMs, 0) + + totalMs := result.StepDelayMs * (len(result.Points) - 1) + assert.InDelta(t, 2000, totalMs, 500, "total duration should be approximately 2000ms") +} + +func TestGenerateMultiSegmentTrajectory_PointsClampedToScreen(t *testing.T) { + waypoints := [][2]int{{5, 5}, {50, 50}, {95, 95}} + result := GenerateMultiSegmentTrajectory(waypoints, 100, 100, nil) + + for i, p := range result.Points { + assert.GreaterOrEqual(t, p[0], 0, "point %d X should be >= 0", i) + assert.GreaterOrEqual(t, p[1], 0, "point %d Y should be >= 0", i) + assert.LessOrEqual(t, p[0], 99, "point %d X should be <= screenW-1", i) + assert.LessOrEqual(t, p[1], 99, "point %d Y should be <= screenH-1", i) + } +} + +func TestGenerateMultiSegmentTrajectory_SinglePoint(t *testing.T) { + waypoints := [][2]int{{100, 100}} + result := GenerateMultiSegmentTrajectory(waypoints, 1920, 1080, nil) + + assert.Len(t, result.Points, 1) + assert.Equal(t, 100, result.Points[0][0]) + assert.Equal(t, 100, result.Points[0][1]) +} + +func TestGenerateMultiSegmentTrajectory_ContinuousPath(t *testing.T) { + waypoints := [][2]int{{100, 100}, {500, 500}, {900, 100}, {1300, 500}} + result := GenerateMultiSegmentTrajectory(waypoints, 1920, 1080, nil) + + for i := 1; i < len(result.Points); i++ { + dx := result.Points[i][0] - result.Points[i-1][0] + dy := result.Points[i][1] - result.Points[i-1][1] + dist := math.Sqrt(float64(dx*dx + dy*dy)) + assert.Less(t, dist, 200.0, "consecutive points %d-%d should not jump too far (dist=%.1f)", i-1, i, dist) + } +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index b7750e9b..c6985cd0 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -217,16 +217,22 @@ type DragMouseRequest struct { // Delay Delay in milliseconds between button down and starting to move along the path. Delay *int `json:"delay,omitempty"` + // DurationMs Target total duration in milliseconds for the entire drag movement when smooth=true. Omit for automatic timing based on total path length. + DurationMs *int `json:"duration_ms,omitempty"` + // HoldKeys Modifier keys to hold during the drag HoldKeys *[]string `json:"hold_keys,omitempty"` // Path Ordered list of [x, y] coordinate pairs to move through while dragging. Must contain at least 2 points. Path [][]int `json:"path"` - // StepDelayMs Delay in milliseconds between relative steps while dragging (not the initial delay). + // Smooth Use human-like Bezier curves between path waypoints instead of linear interpolation. When true, steps_per_segment and step_delay_ms are ignored. + Smooth *bool `json:"smooth,omitempty"` + + // StepDelayMs Delay in milliseconds between relative steps while dragging. Ignored when smooth=true. StepDelayMs *int `json:"step_delay_ms,omitempty"` - // StepsPerSegment Number of relative move steps per segment in the path. Minimum 1. + // StepsPerSegment Number of relative move steps per segment in the path. Ignored when smooth=true. Minimum 1. StepsPerSegment *int `json:"steps_per_segment,omitempty"` } @@ -12748,148 +12754,149 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9e3MbN/LgV0HNbZWtW77kR/birftDseVElzhWWcplN6GPC840Sfw0A0wADCXa5f3s", - "V2hg3hgOSUm2ld9WpWKKxKOBfqC70d34GIQiSQUHrlXw4mMgQaWCK8A/vqPRO/gjA6VPpRTSfBUKroFr", - "85GmacxCqpng4/9SgpvvVLiChJpPf5GwCF4E/2Ncjj+2v6qxHe3Tp0+DIAIVSpaaQYIXZkLiZgw+DYKX", - "gi9iFn6u2fPpzNRnXIPkNP5MU+fTkQuQa5DENRwEPwv9WmQ8+kxw/Cw0wfkC85trbklBh6uXIkkzDfIk", - "NM1zRBlIooiZr2h8LkUKUjNDQAsaK2jOcELmZigiFiR0wxGK4ymiBYEbCDMNRJnBuWY0jjejYBCklXE/", - "Bq6D+Vgf/a2MQEJEYqa0maI98oic4gcmOFFapIoITvQKyIJJpQmYnTETMg2J6tvH+oYYfCWMn9mex4NA", - "b1IIXgRUSrrBDZXwR8YkRMGL34s1vC/aifl/gaW+lzELr96ITMGum1zfn3mmtaWH+vbgkMT+avaEGbKj", - "oSbXTK+CQQA8SwxsMSx0MAgkW67MvwmLohiCQTCn4VUwCBZCXlMZVUBXWjK+NKCHBvSZ/bo5/eUmBUS8", - "aeNwU5k1EtfmzywN3DDeCVYijmZXsFG+5UVswUAS87NZn2lLosx0RRzbUSvIbY1eR9kg4Fkyw15uugXN", - "Yo3IbTBOlsxBmsVplgBOLiEFqmvzutHNti8B+fumvYp/kFAIGTFONe5WMQBJhWJuz9ojbdoj/fOQkRpk", - "ehOYoTuINJ0LKqOXFZG0O41quNFtkF9mUgLXBkw7ODHtSC71WvTQgBYH9QJb59R9ZZZifBlDU2JVBRZV", - "JKXSCh0r4kbkcgXkXwaUf5EFgzgiCmIItSLXKxauprwcJQW5EDIZEMojiyYh7VEcGdq1vc0mUGak2Qpy", - "CFIqaQIapBpN+ekNDXW8IYIXv9ueiYEnZwIDEEkypckcSCrFmkUQjaa8JWUtKydGZvQKwpbAMkeLpMvd", - "ur+SdNnsnYg17Nb7jVhDs3cqQSkjJvo6n5uGP8Km0leFUsRxX8cLbFXtBnoWZlLZc3prV9AvsWG1dwyQ", - "9nY0jcrDpkPK5jguzr8KhY0q8raK39p+25FnyEzVrSy2pobb2srzhfgkdzlozzLNOXEJN7rYniaXm5G9", - "XC6BanjFJIRayM1hh2ciIs+uvk1tdxLloxPTkDwWoaYxsascEBgtR+Rvz58fjcgre1jgWfC3589Ri6Ha", - "6HnBi+D//T4Z/u39x6eDZ5/+Enj2KqV61QbiZK5EbKRNCYRpaGYIcemNScaj/9krMnEm32a+ghg0nFO9", - "Omwfe5aQAx7hNHcP+DsI8exbHgY9i9qwn0VGJUUNw52mMp+kshJyEqcryrMEJAuJkGS1SVfAm/inww8n", - "w98mw2+H7//6F+9i2wtjKo3pxtgpbLnnelaAylzngRvZsYltRxgnKbuBWHl1DQkLCWo1k1RD/5CuNTGt", - "zcA/fCCPE7oxxw/P4piwBeFCkwg0hJrOYzjyTnrNIh9BNWfDZlvh925t8wS6H4XbiM0OZbtQsq3W7ROg", - "EcR0U9NDJ01V5ZVpYlafsDhmCkLBI0XmoK8BeA6IUbRR01CaSu2o18h/QmPhtATDXSMEi7PEADrx4eQ2", - "yrjZi710cb9AaZp8v98MyOZ9VfNNKZOqWKJeSZEtV0YHiy0QS8aXI/LGaEROxSJUkxio0uQJSQXjWtVM", - "wibIlQ1J6I2z/55UjcEn7dVs/VFpSGeI7llStzye74lyCTHVbA3EDKkaqyaPDeMZZDDOjLFNcM6jfsTj", - "aLMU5EzBMnHaf2keTbrtowIgxIaFKgVJ3DhmIQX9kTcWCHJcg+i412rpPBsKP0rjzAel6BI8ZNgYOG/o", - "HduaAucx3VwjEx/mIXG9qsZFOSQJjXbS0tS9KotRoy7w7/H/oWtqP+IANX/IJZobEZAVVYSGIShklkcp", - "XcKjAXmEtteNfmSNk0dzKa4VyEdkTSUz0tpZHkkawwsyDeg1ZZqYzqOl0OLxo5XWqXoxHoNtMwpF8ujo", - "70SCziQnleaa6RgeH/19Gky5TycylrXI9ExBWKO2b1rU9obeINlA4eoxfQ1pOfYotDPCFPlmgtRl+wQv", - "nk4me9Eabv6O9KAQ4D3JwXQynNOggnJ1LXqAnMrrQyHxE0fC5tgt92dBWQyRb9dlAXTbzFjTOAOHSYjI", - "fONsV6MXswWhfHNkhUUE0gPPhaY8MjY+wksWUiQ4QHVhLXiUjkSmtwwmMp1metfRMiT49nC/rkCvQJYL", - "cvwSEddlkcXxphxyLkQMlLeoI5/ARyCvWQxnfCHa8oipWcTkdqhQgWaK0NIaGHngMZZdNDP03x7uJ3PE", - "JXhQWz8y8snIuvgSqoMXQUQ1DLG3Z/f8ppJZljWO5kwr8tjYRAMyDSJ5fSOH5r9pYPTiaTCU10M5NP9N", - "g6ORbwZOfXB/RxUQ81Ouhy/MlEJ6d2JnoypXedpEwj7AbL7R4KGTC/YBBQv+PCITsqiAwUCN+l1suEYH", - "XW2yQU4HFRy6Te8ip4uN0pCcrosTuYkYhQ1IuKJ8CQRMw7Z/fRfyo4sFhIYfdqbDQ3FZTHUoUvejEr9X", - "BbcU/SpVF8rLd6cnl6fBIPj13Rn+++r0p1P88O7055M3px413ufLGHQrLD8xpRFvnjUabdGsrb1jjFsG", - "NiwNXOeEuNOlRiGVPCr4T2LZQVsnJBZLnGtTit7KDVWbyCo6V0MqiWVxSBnNY9SlDChNk9RzMpmz3kxf", - "QnRNFUmliLLQUtEu4q1D86tO7UMYmnznzr/+zl2ntiX8ro7/3K12uMO/a4SdHf0t/+p+tnGUSaSAwpKp", - "4YrKJRhdVxvjw7VsGTNGppq1oMMRDQc0Fa5XwIlKhNCr/61lBiPyNmEaW9NMC0P/oTnRjKUzpwoiYoxf", - "pjTlIdR0vueTmtL3/M6NXQPzXsauXVZNzzVLbGqHvyggqyyhfBizKyDfwQcDRZjJda4gcKWBRoYazEfK", - "dWMX/bK6jzxLazqnMKLFQWS660h7kevhXssIlJ71eV9BaQO8vYCxSkOf83IQKBn2DaxEJkPYecymqplP", - "MKiswrdDb6+qcmkPW+R74OjUfPsjyQNF2nJdXPVS7RmPzLEAKlemR/2KtLjyruWc6nDlHKOHYbzLM/qq", - "2yNaCIonzyb7+0dfdfpFR+RsQUTCtIZoQDIF9q5vxZYrUJrQNWWxMbltl1wqSkDycYesU02+mQyeTgZP", - "ng+OJ+/9IOLWzlgUQz++FgS/NiAb2YG320ZRtSI4ZmsgawbXRgkpXOJjCbhMoxqGmq3BL2kkoBdyFq6k", - "SJiB/WP37NiUvHRNCV1okJX152qtFgS4yiQQpgmNaGpvYThcEwN1zfpHmsC9XAGNFlk8wNmKb+IO8ux0", - "SL/qdEQXZPP0yWQ3t3TzdvKwk7fHZZyfuvmxZWgKzzH0EzfO4iqJGnRPBrYtlUA0TVOrXx3sNS6u2ZK+", - "E/UKNgSvJl2skD3Rdz9g/fP/5LzIZnS1SeYixslxohE5peGKmCmIWoksjsgcCK20JSpLUyG19YXcREIL", - "EU/5YwVA/nF8jGvZJCSCBfpbBVdHI+J8Z4owHsZZBGQavEOPyjQwVvPFii20/fhSy9h+OondV6+fT4PR", - "1HqSreuUKesKDxFAGithoAxFMndHlnK3lHa8v+rcGMe/cLa/XtI5DrvHhjakNe6uV15LYQT+6Q2Ed+Ye", - "pWZ5CV5obLiRI1xkyhs3Jpd1b/rv79tBgHYkKpeZUY/UflRF1UwKUfeG+5eROT+33Q+8FCKmK0klW7MY", - "ltAhdqiaZQo81nlzSKosOZjWZiiexXh65DK+Hbtl1+4xfnGj8eQRkqgVxHGx5eYsyLjXRguvPWP9KuSV", - "4eHSWH1Mq8b6kRvRed7sJIz7FtCvcwFfd5PXR98Nm8PZx1Zo5ClfMyk4Gh6F69vAqkAXR7Hb+spulJTf", - "cl/v57HuRmC3Y9qis5cNb+WVplWmKxBWrKPNhFvtwTI4s8sYHHmtDLhheua/BnFLJaYJunL9I1gn9Wz+", - "zTO/j+qbZ0PgpntEbFMyzxYLy1kdTupdBxOZ7h7sUzf2fmRlANJ+6LtgS3PIIvVaHm5Qbx1lCpvXhFpw", - "efruTbB93KqnzDX/8eynn4JBcPbzZTAIfvjlvN9B5ubeQsTvUBU99DRBNZaS88t/Duc0vIKoextCEXtI", - "9me4JhpkwszKQxFnCVd915WDQIrrvrFMkz3vPXHUgQV0y45dpPS6Fr8dx28XwYvf+0LlWkf3p0HTr0Xj", - "WBjTbqb1pv8UPHGtCSWpgiwSw2L1j88v/3nUFKxWs8eDKI9dxrttcyJ1HJd+pJ25++4m4qxBU12EsRHQ", - "uXQgSlszmWaHT9MWB+9beD1Anp9VHMZ0bgQSJcqMto0fUl+Q1NuLAllnr/yi1v0+83W3CRBDqgzfQ0RY", - "GXPlOWQLP26WscgviKlRx2dU+/3E6Me12KiSmeu2h6u4k9U01ZnaExt5TJPCzvaU7ZZKaTZLQ8/6TpVm", - "CTXGyMvzX0iG/vQUZAhc02X1FOQYnNFzjJ7mxydhi9perag9W+129ekogyCBpOsyrYRYgkLMkwQSoyNa", - "6It7to4T3OtuOS9xqmuXNzLj3KDPLhsi/1nUjdiIHZgD84pqaiTZtWTWAdogPXuPzXiaee7mIqrpTopF", - "VJ1l1Os9LMZ937vmW+mLBhwXcqbMcO0VmhYaeBeRlKFE2IC45qNgV5eKW4oEWl6U7qM7XZySlG5iQQ2Z", - "phKUkVB8WWDQBSAISWK2gHATxu6iVd0Wm8XFWkksZhVeFRT893Q/1UFq3WgaVvAGH+4kGgpBagdnikyx", - "4zToYlkDv+cUsI5w+3N+k4VbEK4yflUF2MWDFFEmuzGxjQ4G6Q+/WDDO1Gq3Y6MMAc57dR0avfa3PQ/b", - "X6silrnye0XF2eOQK6F1nQ4EtiE88PCtwukTIhehBOBqJfQ7WO6ShbObn/4H658vIrKXzmjcEr/c4bn9", - "FT22+wy04y2uHeuRUV/TYQwLwy2Sw63udfcY03t1lu/CIN/YPpQd4oGWBaJ7UmnqhOFl2XrCzb63erGm", - "s5vtjvAfhGQfBMd0DpyL0ERkXI+Ivc43hgZ+rwhG4Q0IhyWtfW/w4Jd0FoKe6O3/ayAOd5g/EtfcM32W", - "+ie/zc11kfKzuxO0jyuothlwlbyk+lT7M8XeQ+58ndxK1tpTarEoAt4TX2ivvcs7Bdep907UtesA+zWL", - "4dxYnUoxwdVh8C+lyFK/owJ/cqFbknxfs/b2jRH0ZFF98+zZ0X5JU+Ka+/ziBlb8CT3hOby/dMC7SzzZ", - "9UootKXyvbXXX/amBa8go0MTmrbE91Wz//ZTWc9ppqAa7Ssk2vcQGt6PCl/rns7a6s0hpv35fLXVuOpa", - "kM2klymrk3s3xKgwr9WvVId3mqNWJBCi+YS5vP7IaMO4bA39fq6C2914pOgbb3aIfeiM5MAduGWm20LS", - "BPyRCu9K3TZvZFC8SA3HrkFKFoEiytascDtwVMX5k0mf08zrQsovgT3On4oCC8h7d5Rvh0DnBH3GLywB", - "d1/UlHBULyrygLXtu7N1QxJ6g4G87AOc8TffdUOAUZ/KhR+/+W5HjBw3uPB4x0iECy3S2xKakCGYcfr5", - "5SxJIGJUQ7zBKh14PSoyTZaShrDIYqJWmTZa0IhcrpgiCcbToI+BcbwQljJLNURkzSIQuFl+//A+iZ6W", - "gw1A95jl2cx+3lvTvV2OoNEDtRRXoHrjOPwlHAzs6NTCHHTrDlgJjEiw1RMOr+Lwq2QairoTh23QdqBr", - "Lrk8Vj6f8FDATTPmHA2Y5BS8CH4EySEmZwldgiIn52fBIFiDVBacyeh4NEGVJgVOUxa8CJ6OJqOnLlIc", - "FzLOI6bGi5gu8+Ms9Jxnb0AuAaOfsKWNNYAbptBtJTioAclSY/2TxqCemKs1o0RlKcg1U0JGgymnPCKY", - "xZVxzWLcuaL1K1hfChErMg1ipjRwxpfTACOzY8aBMEXEHMWVUXwXQubpRCjhXXAgBqIYHFrhHKFGo8NV", - "PstrXL9FBSj9nYg2e5VKaoipfDcbPvl8SXYPtSAJbqtLb/l9GgyHV0yoKxuYMxxGTNF5DMNlmk2D90eH", - "x9JYgPxkVbbTMgMbTlcW8HoymXhUb4Tf4jvCnL5iaQ7ZzSSnT4PgmR3JZ8UXM46b9cI+DYLnu/SrF9vC", - "ylNZklC5CV4Ev1i6LECMacbDlUOCAd7BjN1K6s3SWNBoCDcaOGroQ8qjYd7W4Fwojxj4BbthARUhSWLI", - "sRiCfGApoTJcsbVhGLjRWDpJryAhGTdnw3glEhhfIWePy6nH02wyeRoawwM/wWDKFWgiDb8k1Rnsqhg/", - "gA1JzoVT/hnZ0O7XabHUEx69c3u8jR2TLNYspVKPjaE+jKim2ziy3MrugL2yjWFNi37cE7wiNtpthf/q", - "w/vzkl6L2OAUzUUtSBrTEFw+YY6u/bDe0AxOhr/R4YfJ8NvRbPj+4/HgyfPnfqv2A0tnRn1pg/hbSZB5", - "5rrBFzWQpTaWoaCAEurHWPsnDzZMKGcLUHpkxOJR1Rs8Z9ywYN+ZV4DnErx85/ZW8VbB7mEy7th3I1FQ", - "gyUFiAYeMWe5pmAOpogEGn1pgdcSQQU2K0T+mCojkNRRVQgWS3TS0Clc43muF/il3mkeR8mJaFRTaNXo", - "Q+3alcM6OT8jIY3jETlxv1IJufsNIiPlyip+rvTASsSRI1K4CePM2MAkFuHVgChBuCACDWW8/CSFsFEk", - "pNyGfMRA14Ap531l/IpiWvnGE1bkHVhnYV4kC5OfR1OOpoSNmDQ2hrE5w5XjqghsBIfRmsIi5hgv521C", - "jZntCja2apnbrinPDZeUbswoHPS1kFdEioxHQy1ZSmKqgYcbnA0wwJhHbM2ijMZuGJ/k9RRkvIUGtM07", - "v6X046EqCA7ZkVH9JXmvYIQtRSqrNN1gs0bBtJzZ6ogrS6XdE748tdgORJOtXpNXmsvZ+oti6IIlWWwD", - "xizXVWtJ+q3NFo6sMTc2or4bTe+ARi8rhp9vt+4KXfUyir7KtEU1RDclnlMtvrn17ppF24KLRaRBywbu", - "2k60nLv3s2663xPp+/0Dh5I/+gRcdAmWWCuw8NUIrF+tuyL3uOyAr6JAoR9NxW3XPWGoXfpwZ+TcyfyV", - "1Ecfn9mLuDVTbM5ipjeFmfzVYPwHFrkkDHFdze+uo7leetOv9WFuGWoteOWbC1RbI2xAhHPCG82N5lnV", - "ZlqpbZWogZmeN+uGLdkabFasU0xjoApQt6qWLOmp1uXTeIoSbfdEmu0ipAfKDTPQV3JcIihl5rxFE0U8", - "NChmCdoSzKyoDdwpJL4HXatycJ/Ho7+cgp93MWbWrrRYxF3s4vegc1arTOFu7fOZdlE+6jVt/ZtbVFu4", - "JzJvV8u9lXbodsGs7MuS+pu8iEANO/mpWFx1l5JG7YKxWh3hLXLUZWqX82A4DcpMXojS8p6d/Gh+LgM+", - "KummU+5LIh2R1yh/DWASVsCt3dzOVh0QBTDlBhh/ximhmuQl2cIl06OFBIhAXWmRjoRcjm/M/1IptBjf", - "HB/bD2lMGR/bwSJYjFZWnrt7xpXgQqrqddIwhjWU6zUWtbtFDt1WYLyAci40iwUReT39LgX6ntihVf/5", - "QG5AhCK1fE3agj3jq74kpMsdCF8VMXndouqSXkEZu3dfGmMrBPGTw9HWE4cldAnj1IbMljP1ezdbB0sJ", - "AMFBvyhCX9JUZ9KYvyWC8jvqHnS6muZ+IWaDK8naBSDGG6O9jYXh7Two0nynKzpeRZLWtcWan6+Wx+/U", - "wFp0o3UaMk5iscTYR83CK2Urk9rIW+virFAQmcOKrpkhabohayo3fyc6Qy+dqyucM/Boyn81Supc6FVl", - "KThgvlaCoZnOd+nK4Q+sNLfiDWe2Aj6puX/I42IMVIXLCY7srSh6kdDbCBC7HAAnCv/lBLtzYAyH7rmI", - "n8lwiOo1mRB7g2AVcnuH8C+fhLzIYxzvif2qZe4PlI6OvL4SH5IFptQVLHqoNprxHtpcXiiuQzi6MI57", - "wku7Rv4tnBxmJV/RqYXvxKBToxsLrtx3LerBEyLgirHcl/LgKT70mR0a9ZrwnuPrF+fByOujh9gyrwxz", - "CzQ/m3zb36/+hNcdBgR0LMeQxkKN7WsIs6LGBJJJ5vPG11+MuC+XvP9dikNvN8v4VLvOr4h17UoJxWij", - "cvtzvNgnEnbAi33D4b7x0n7i4mCfT4ESu8Todpz1rL9f/WW4O3EWIeTVOq5NvOVhCFtQ9tqGAnzd2MLs", - "gz8BohAfBY7ENY8FjQx3zT4wjLJdgvZFdetMckUo+e3s3IYRV6JHbNkdRJfKLYtKpkC1dG4D/27+V0z+", - "xlKMdsnfpsLSEjs/ZZOHtBgNOl8UVmEy/f7IAMWBDdrJcybqNDCoRhL15WC83+twdvt6K4PS7Hq+xiK8", - "GAmrusEPkS4dsqoihNCc0NySO+hV6WgHgtVUjj4oTR5rKiuhT0nueMHIVjPW0Va6nvIthE1+UzoiYrEA", - "qYhiS47V0bmON2RBlQZZTIjFMng05RFUvzKfqQQsq/OBpc4gpuGKwRpL0YJujoJs5L/1qHCV2aOHwlaD", - "j+3CasVy0Ts4Ij+w5Qqk/auoz0xUQuMYCvQqMs800fQKSCz4EuRoyocWE0q/IP822LZDkOMBcXkOBrEQ", - "kcf/fjqZDJ9PJuTNd2N1ZDq6sPh6x6cDMqcx5aFRpUzPMWKAPP738fNKX4u4ete/DXJ85l2eT4b/q9ap", - "BebxAL8tejyZDJ8VPTowUqGWGQ4TVNFRlmXKP5UJ8m6rgkHlNwsyflC+dP99paLj3luJxUvH2//NRKOu", - "L7sQj0Z+zfKsAScW66KhKNS+q0zorYX/NZyw++mEZbH6NkGhllephP8AyeZ70LVa/nlpphb2CrKJmdKo", - "p6tOuimfFDjsMHmYlFKu2kMqpfkW26yYB0grGAmPmLdBum3awCL0XeZbXjb9Hq+d78J0w2ve0t3xAPGE", - "K8BC2ZhbsI2ZJdCoMLq9vPwOaORM7t1YGSfLVUIz/tfCzSLUoIdlQaBb6RIo+r0xkg+MWDAiszBlTMeC", - "OBRYQT+r1CHo5O52OYj7C/DrqDtxKMdXhsrD8R4gIi9Ae97pqaBujCUq1IqlBYZt6kr3pe1JHIvrPMMF", - "M7VsXoaQxGZYxeAOBBcGIyERTgbYONFRR0ZXrh7cWQpXoZF05GAd8uxGJV/XKbS7PcSRC9R9M51cltP2", - "tzW2Z3LiLtxZlhNiqUhweuiizpP4tHD6WpUdctfm1gROio4X5DdbjdrmajKtSt9mKzTM96yLjzmsd/PO", - "WGNf0o+q1UkqWaiF4azFbnxQTSy8RdbfNn44kLB/Y2lJ1hUE/mmInFaTiRsk2qJ351zpIfh9XaNdfDHl", - "/YzR7yKteUSnvOES7U4ldj7OO2Ou3Kvifb604XopjpBeZhh8OaY1n9JZSXfbS32UtVJjsCoCHpxld1vP", - "RLI0L/nmYMNEYXxSzJDTcIhthmW/3ieiG/Iix8O9iIsTt4d/cpHRJNcOsXHdTPZtWAKVoln3ZQN46nLt", - "jtsDy3bgsr0lxX/h7I8MfMWkSq68dtvRW5+nbWviMsldF874QsRmF1N1UrskaL6saGK4W+OP+ZZ/coWH", - "wCYANulNpCW5NZwU6HhwngbndyjwuM330O9q8FRUzhEl0vThI+oCq2KZFWE2vcd51ETS2MafdrqSbEXs", - "1+rUNvuMuGq6hTTcaAut1x/Udx9QfQPZF899cVopLF3awi4+Fwvi0ghX/TH4x/Di4nToUnOHl96ngd9A", - "xKgrd7UgZnisVO3CfR83hdhR7eYuv6VriTrPpdynh0imuNGtXXbphFbsFhRrjPntQUaY8LqLw/NVRfmi", - "LefnZ7z3LooZLoqSp53VTvN3CVEt++bZsy4wsURoB1hba6Ra5tvlxL+lO/ZAb0aRbv3Qj1F0S5mTM4+H", - "LEO1YrFU43Jj/Vd0YuleKOiQww2CsA/HbqXcXNDkz8wXtaO8FfP90yxEHItrf+RBrUx8pZBpE82Cx5si", - "P4OwRf7oLVPEgbaFMbtPlX3mqazdP1vZYOZeWgi+2IlWPLnee5QZwvqqTy/fyWCAJmIN0kxtGSSN6eYa", - "K6yPXYmYHUoXyTnTksoNOS96u9dquOE+fGi3LICMqLnRhC4p48pa4nMprhVI4p6FmXLBSSxCGq+E0i++", - "ffLkyYhcYhBZBPjoDQ3zJ6kepXQJjwbkkRv3kS0s9cgN+ah8MNBlQMniORSdj1gCh2WodCbx+SNeq2Dk", - "c5y4LSjX/dKeDvdh2bXm+kJZDx448FEaX154ublfY6mhcgmY0nOBkFuK8BCnYxArk5A7ug39ynNt95Y7", - "234Q7vPSQfsZSw8FlJXCpGvzVZSY8r5ZW0cwvsDWi2F89e1+UVx7MPDL4Lj6tp3vKLSP1X1luKVbkPux", - "fAbv0/iK1bNzvYj+kWGaZ79dXnlgb5tK2PN63u7GwkEIrb5e+lVVAXr744OMLzCipHh+NVdbuynOPuDf", - "S3P2gdQ/D9XVH4v9D93dPkCp8wHdLcSnilcxveZv/e3Mz01793yO2UX5jjD3y4OMUq48X2mX1436iO2g", - "02CrP43UqT0W+oX0p8rbnR7i+676luaD9biVJ599XHQ7HYpM9zniys0Tmd7qkftC8ugWniXPS6i9PqbG", - "G6dGx20+cvqfC5R7uECpULXIdMNhVrxFNC4vYf3S1WYOl8903meiduu1oO66TV2vTn2xFO0vVNuiSOxO", - "JawZ2oz5y0PVh4xaWHfJZZ1SLM8+qyJ+6+1ZcWlVvHtURk+MCJZUEok5KuqVkrK8Dp67FSi6d11kodDz", - "X2P1vZzULxpxw8ZJ+uzW6QSVd9Ds1WNNwBW/Dl+7F4CHJ1tf4hWL8qHk9vPBI/J9RiXlGmy83BzIu9cv", - "nz59+u1o+w1IDZQLG49yECT56/cHAmJAeTJ5so2xmZFkLI7xeV0plhKUGpAUa8USLTfW94ml8WV9u9+B", - "lpvhyUL7Xnq8yJZLmyuKJWvxdZXKq2zlyyZyY5mgXMS2R9ke4rlRJJzaMlcKeREwRHMHiRIze3p05g/m", - "72er29Z+LfIBth0otde620H2LX7NH4WRBZR3lmBH47g6bH3bWq8LeULv7vvw9T8J6T17j7exaP4++MOr", - "EIU7UFRILOXaiLzl8QYTDEpZl4IkZ6/weZG5fVRbaXwBBcvBGQkyamNZpNuQXHko8d5w7HmMcX/1yoXC", - "fdlifFqk9eMHF/L/AwAA//9p0GK1mLgAAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 99244a4a..a9e33aed 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1428,14 +1428,23 @@ components: default: 0 steps_per_segment: type: integer - description: Number of relative move steps per segment in the path. Minimum 1. + description: Number of relative move steps per segment in the path. Ignored when smooth=true. Minimum 1. minimum: 1 default: 10 step_delay_ms: type: integer - description: Delay in milliseconds between relative steps while dragging (not the initial delay). + description: Delay in milliseconds between relative steps while dragging. Ignored when smooth=true. minimum: 0 default: 50 + smooth: + type: boolean + description: Use human-like Bezier curves between path waypoints instead of linear interpolation. When true, steps_per_segment and step_delay_ms are ignored. + default: true + duration_ms: + type: integer + description: Target total duration in milliseconds for the entire drag movement when smooth=true. Omit for automatic timing based on total path length. + minimum: 50 + maximum: 10000 hold_keys: type: array description: Modifier keys to hold during the drag