{"record":{"id":"0937553b0b44bc78","repo":"stablyai/orca","slug":"spritesheet-dims-width-dims-height-is-not-a","errorCode":null,"errorMessage":"Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.","messagePattern":"Spritesheet (.+?)×(.+?) is not a clean multiple of frame (.+?)×(.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/pet.ts","lineNumber":341,"sourceCode":"        `Spritesheet is too large (${(sheetStat.size / (1024 * 1024)).toFixed(1)} MB).`\n      )\n    }\n\n    let sprite: NonNullable<CustomPet['sprite']> | undefined\n    if (manifest.frame) {\n      // Why: only decode when a frame layout needs validating — nativeImage can fail on some WebP variants in headless contexts.\n      const sheetBuf = await readFile(sheetSrc)\n      // Why: defend against TOCTOU — file may have grown between stat and read.\n      if (sheetBuf.byteLength > MAX_BYTES) {\n        throw new Error('Spritesheet exceeded the size limit.')\n      }\n      const dims = await readSheetDimensions(sheetBuf)\n      if (!dims) {\n        throw new Error('Could not decode the spritesheet image.')\n      }\n      const { width: fw, height: fh } = manifest.frame\n      if (dims.width % fw !== 0 || dims.height % fh !== 0) {\n        throw new Error(\n          `Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.`\n        )\n      }\n      const columns = dims.width / fw\n      const rows = dims.height / fh\n      if (manifest.animations) {\n        for (const [name, anim] of Object.entries(manifest.animations)) {\n          if (anim.row >= rows) {\n            throw new Error(`Animation \"${name}\" references row ${anim.row} but sheet has ${rows}.`)\n          }\n          if (anim.frames > columns) {\n            throw new Error(\n              `Animation \"${name}\" has ${anim.frames} frames but sheet only has ${columns} columns.`\n            )\n          }\n          if (anim.frameDurationsMs && anim.frameDurationsMs.length !== anim.frames) {\n            throw new Error(\n              `Animation \"${name}\" declares ${anim.frameDurationsMs.length} frame durations but ${anim.frames} frames.`","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/pet.ts#L323-L359","documentation":"Thrown at pet.ts:340-343 when the decoded spritesheet's pixel width or height is not evenly divisible by the manifest's frame.width / frame.height. The sprite engine slices the sheet into a grid of frame cells, so the sheet dimensions must be exact integer multiples of the frame size. columns and rows (computed at lines 345-346) would otherwise be fractional.","triggerScenarios":"pet.json declares frame {width:32,height:32} but the image is 100x100 (100/32 is not integer), or the image dimensions were changed after the manifest was written, or the frame size was picked wrong.","commonSituations":"Author eyeballed the frame size; the sheet was re-exported at a different resolution without updating frame; off-by-one (e.g. 33px frames instead of 32).","solutions":["Set frame.width and frame.height in pet.json to divisors of the actual image dimensions (the message tells you both numbers).","Or re-export the image so its dimensions are exact multiples of the intended frame size.","Double-check the frame size matches the art (e.g. a 256x256 sheet with 32x32 frames gives 8x8 grid)."],"exampleFix":"// before — 100x100 sheet, 32x32 frame (100%32 != 0)\n{ \"frame\": { \"width\": 32, \"height\": 32 } }\n// after — re-export sheet at 96x96 (96/32=3) and keep frame 32x32\n{ \"frame\": { \"width\": 32, \"height\": 32 } }","handlingStrategy":"validation","validationCode":"async function assertCleanFrameMultiple(imagePath: string, frameW: number, frameH: number) {\n  const img = nativeImage.createFromBuffer(await readFile(imagePath))\n  const { width, height } = img.getSize()\n  if (!width || !height) throw new Error('cannot read dimensions')\n  if (width % frameW !== 0 || height % frameH !== 0) throw new Error(`sheet ${width}x${height} not a multiple of frame ${frameW}x${frameH}`)\n}","typeGuard":null,"tryCatchPattern":"try { await importPetBundle(p) }\ncatch (e) { if (e instanceof Error && e.message.includes('not a clean multiple of frame')) { /* fix frame size or re-export sheet */ } else throw e }","preventionTips":["Choose frame dimensions that evenly divide the sheet before authoring art.","Re-export the sheet at a resolution that is an integer multiple of the frame size."],"tags":["pet-bundle","sprite-sheet","validation","frame-layout"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}