{"record":{"id":"df53e63b722355b6","repo":"moeru-ai/airi","slug":"the-crop-range-is-outside-the-motion-timeline","errorCode":null,"errorMessage":"The crop range is outside the motion timeline.","messagePattern":"The crop range is outside the motion timeline\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/features/devtools/motion/live2d/composables/keyframes.ts","lineNumber":242,"sourceCode":"    durationMs,\n    samples: [\n      { atMs: 0, ...neutralLive2DMotionControlPose },\n      { atMs: durationMs, ...neutralLive2DMotionControlPose },\n    ],\n  })\n}\n\n/**\n * Crops a project to a timeline range and moves the retained range to time zero.\n * The function interpolates source and overlay values at both crop boundaries.\n */\nexport function cropLive2DMotionProject(\n  project: Live2DMotionProject,\n  startMs: number,\n  endMs: number,\n): Live2DMotionProject {\n  if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs < 0 || endMs > project.durationMs || startMs >= endMs)\n    throw new Error('The crop range is outside the motion timeline.')\n\n  const durationMs = endMs - startMs\n  const sourceTimes = [...new Set([\n    startMs,\n    ...project.source.samples\n      .map(sample => sample.atMs)\n      .filter(atMs => atMs > startMs && atMs < endMs),\n    endMs,\n  ])]\n  const source: Live2DMotionRecording = {\n    format: 'airi-live2d-motion/v6',\n    durationMs,\n    samples: sourceTimes.map(atMs => ({\n      atMs: atMs - startMs,\n      ...evaluateLive2DMotionRecording(project.source, atMs),\n    })),\n  }\n","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/moeru-ai/airi/blob/9c213115f8bd0fff9e6eabab02b077ac32da21be/packages/stage-ui/src/features/devtools/motion/live2d/composables/keyframes.ts#L224-L260","documentation":"cropLive2DMotionProject validates the requested [startMs, endMs) window against the motion project's timeline before cropping. It throws when the range is not a valid finite sub-range of the source motion: non-finite values, negative start, end beyond project.durationMs, or start >= end. This guards downstream slicing logic from empty or out-of-bounds sample windows.","triggerScenarios":"Calling cropLive2DMotionProject(project, startMs, endMs) where startMs or endMs is NaN/Infinity, startMs < 0, endMs > project.durationMs, or startMs >= endMs (e.g. zero-length or reversed range).","commonSituations":"Computing crop bounds from UI sliders or parsed user input that yields NaN; assuming durationMs is in seconds instead of milliseconds; passing (end, start) swapped; cropping to the full duration with start === end expecting a no-op.","solutions":["Clamp and order the range before calling: const s = Math.max(0, Math.min(startMs, project.durationMs)); const e = Math.max(s + 1, Math.min(endMs, project.durationMs)).","Verify startMs/endMs are finite numbers with Number.isFinite before invoking.","Ensure endMs is strictly greater than startMs; if you need a zero-width selection, special-case it instead of calling crop.","Confirm the project's durationMs (milliseconds) matches the units of your range values."],"exampleFix":"// before\ncropLive2DMotionProject(project, startInput, endInput) // startInput may be NaN or > duration\n// after\nconst start = Math.max(0, Number.isFinite(startInput) ? startInput : 0)\nconst end = Math.min(project.durationMs, Math.max(start + 1, endInput))\nif (end > start) cropLive2DMotionProject(project, start, end)","handlingStrategy":"validation","validationCode":"function canCrop(project, startMs, endMs) {\n  return Number.isFinite(startMs)\n    && Number.isFinite(endMs)\n    && startMs >= 0\n    && endMs <= project.durationMs\n    && startMs < endMs\n}","typeGuard":null,"tryCatchPattern":"let cropped\ntry {\n  cropped = cropLive2DMotionProject(project, start, end)\n}\ncatch (error) {\n  if (error.message === 'The crop range is outside the motion timeline.') {\n    cropped = project // or surface a user-facing range error\n  }\n  else throw error\n}","preventionTips":["Always clamp the range with Math.max/Math.min against 0 and project.durationMs before cropping.","Enforce start < end in the UI (e.g. dual-slider with min-gap).","Treat all durations as milliseconds consistently across your code.","Derive crop bounds from project.durationMs rather than hardcoded values."],"tags":["validation","live2d","motion","range-check"],"backgroundTag":"invalid-argument-range","analyzedSha":"9c213115f8bd0fff9e6eabab02b077ac32da21be","analyzedAt":"2026-09-02T04:27:24.639Z","contentChangedAt":"2026-09-02T04:27:24.639Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}