probe-contour.mjs 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. // 一次性探针:采样白母版的逐行轮廓(左右缘 x),用于拟合自绘 SVG 水滴路径。
  2. // 用法:node probe-contour.mjs <png>
  3. import { readFileSync } from 'node:fs'
  4. import { inflateSync } from 'node:zlib'
  5. function decodePng(path) {
  6. const buf = readFileSync(path)
  7. let pos = 8
  8. const idat = []
  9. let width = 0, height = 0, bitDepth = 0, colorType = 0, palette = null, trns = null
  10. while (pos < buf.length) {
  11. const len = buf.readUInt32BE(pos)
  12. const type = buf.toString('ascii', pos + 4, pos + 8)
  13. const data = buf.subarray(pos + 8, pos + 8 + len)
  14. if (type === 'IHDR') {
  15. width = data.readUInt32BE(0); height = data.readUInt32BE(4)
  16. bitDepth = data[8]; colorType = data[9]
  17. } else if (type === 'PLTE') palette = Buffer.from(data)
  18. else if (type === 'tRNS') trns = Buffer.from(data)
  19. else if (type === 'IDAT') idat.push(data)
  20. else if (type === 'IEND') break
  21. pos += 12 + len
  22. }
  23. const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 1
  24. const stride = width * channels
  25. const raw = inflateSync(Buffer.concat(idat))
  26. const img = Buffer.alloc(height * stride)
  27. for (let y = 0; y < height; y++) {
  28. const filter = raw[y * (stride + 1)]
  29. const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * stride)
  30. const prev = y > 0 ? img.subarray((y - 1) * stride, y * stride) : null
  31. const out = img.subarray(y * stride, (y + 1) * stride)
  32. for (let x = 0; x < stride; x++) {
  33. const a = x >= channels ? out[x - channels] : 0
  34. const b = prev ? prev[x] : 0
  35. const c = prev && x >= channels ? prev[x - channels] : 0
  36. let v = row[x]
  37. if (filter === 1) v += a
  38. else if (filter === 2) v += b
  39. else if (filter === 3) v += (a + b) >> 1
  40. else if (filter === 4) {
  41. const p = a + b - c
  42. const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c)
  43. v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c
  44. }
  45. out[x] = v & 0xff
  46. }
  47. }
  48. const alphaAt = (x, y) => {
  49. if (colorType === 6) return img[y * stride + x * 4 + 3]
  50. if (colorType === 2) return 255
  51. const idx = img[y * stride + x]
  52. if (trns && idx < trns.length) return trns[idx]
  53. return 255
  54. }
  55. return { width, height, alphaAt }
  56. }
  57. const file = process.argv[2] || 'public/cockpit/assets/icons/T_MarkPoint_WhiteFull.png'
  58. const { width, height, alphaAt } = decodePng(file)
  59. const MINX = 67, MAXX = 444, MINY = 19, MAXY = 493
  60. const BW = MAXX - MINX + 1, BH = MAXY - MINY + 1
  61. console.log(`== ${file} 逐行轮廓(bbox 相对坐标,宽 = 左右缘跨度 / bbox 宽)==`)
  62. for (let i = 0; i <= 20; i++) {
  63. const t = i / 20 // 0=顶 1=底
  64. const y = Math.round(MINY + t * (MAXY - MINY))
  65. let lo = -1, hi = -1
  66. for (let x = MINX; x <= MAXX; x++) {
  67. if (alphaAt(x, y) > 8) { if (lo < 0) lo = x; hi = x }
  68. }
  69. if (lo < 0) { console.log(`t=${t.toFixed(2)} y=${y} (空)`); continue }
  70. const lx = ((lo - MINX) / BW).toFixed(3)
  71. const rx = ((hi - MINX) / BW).toFixed(3)
  72. const w = ((hi - lo + 1) / BW).toFixed(3)
  73. console.log(`t=${t.toFixed(2)} y=${y} 左${lx} 右${rx} 宽${w}`)
  74. }
  75. // 圆帽拟合验证:找最宽行与其 y,算帽心/半径(bbox 相对)
  76. let widest = 0, widestY = MINY
  77. for (let y = MINY; y <= MAXY; y++) {
  78. let lo = -1, hi = -1
  79. for (let x = MINX; x <= MAXX; x++) if (alphaAt(x, y) > 8) { if (lo < 0) lo = x; hi = x }
  80. if (lo >= 0 && hi - lo + 1 > widest) { widest = hi - lo + 1; widestY = y }
  81. }
  82. console.log(`\n最宽行 y=${widestY}(相对 ${(widestY - MINY) / BH})宽 ${widest} = bbox 宽 ${(widest / BW).toFixed(3)}`)