fit-pin-path.mjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // 一次性探针(票 47 DOM overlay 自绘母版):
  2. // ① 拟合母版下半收尖轮廓(假设=圆弧,检验残差);② 量镂空版主版的孔洞几何。
  3. // 输出直接嵌进 markerOverlay.js 的手绘 SVG path。
  4. import { readFileSync } from 'node:fs'
  5. import { inflateSync } from 'node:zlib'
  6. function decodePng(path) {
  7. const buf = readFileSync(path)
  8. let pos = 8
  9. const idat = []
  10. let width = 0, height = 0, colorType = 0, trns = null
  11. while (pos < buf.length) {
  12. const len = buf.readUInt32BE(pos)
  13. const type = buf.toString('ascii', pos + 4, pos + 8)
  14. const data = buf.subarray(pos + 8, pos + 8 + len)
  15. if (type === 'IHDR') { width = data.readUInt32BE(0); height = data.readUInt32BE(4); colorType = data[9] }
  16. else if (type === 'tRNS') trns = Buffer.from(data)
  17. else if (type === 'IDAT') idat.push(data)
  18. else if (type === 'IEND') break
  19. pos += 12 + len
  20. }
  21. const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 1
  22. const stride = width * channels
  23. const raw = inflateSync(Buffer.concat(idat))
  24. const img = Buffer.alloc(height * stride)
  25. for (let y = 0; y < height; y++) {
  26. const filter = raw[y * (stride + 1)]
  27. const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * stride)
  28. const prev = y > 0 ? img.subarray((y - 1) * stride, y * stride) : null
  29. const out = img.subarray(y * stride, (y + 1) * stride)
  30. for (let x = 0; x < stride; x++) {
  31. const a = x >= channels ? out[x - channels] : 0
  32. const b = prev ? prev[x] : 0
  33. const c = prev && x >= channels ? prev[x - channels] : 0
  34. let v = row[x]
  35. if (filter === 1) v += a
  36. else if (filter === 2) v += b
  37. else if (filter === 3) v += (a + b) >> 1
  38. else if (filter === 4) {
  39. const p = a + b - c
  40. const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c)
  41. v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c
  42. }
  43. out[x] = v & 0xff
  44. }
  45. }
  46. const alphaAt = (x, y) => {
  47. if (colorType === 6) return img[y * stride + x * 4 + 3]
  48. if (colorType === 2) return 255
  49. const idx = img[y * stride + x]
  50. if (trns && idx < trns.length) return trns[idx]
  51. return 255
  52. }
  53. return { width, height, alphaAt }
  54. }
  55. const MINX = 67, MAXX = 444, MINY = 19, MAXY = 493
  56. // 帽圆(实测):圆心 (255.5, 208),半径 189 —— 与 MASTER_GEOMETRY capCenter(0.499,0.397) 一致
  57. const CAP = { cx: 255.5, cy: 208, r: 189 }
  58. const file = process.argv[2] || 'public/cockpit/assets/icons/T_MarkPoint_WhiteFull.png'
  59. const { alphaAt } = decodePng(file)
  60. const edge = (y) => {
  61. for (let x = MINX; x <= MAXX; x++) if (alphaAt(x, y) > 8) return x
  62. return null
  63. }
  64. // ---- ① 下半段左缘采样 + 圆拟合 ----
  65. const pts = []
  66. for (let y = 380; y <= 490; y += 5) {
  67. const x = edge(y)
  68. if (x != null) pts.push([x, y])
  69. }
  70. function fitCircle(points) {
  71. const n = points.length
  72. let suu = 0, suv = 0, su = 0, svv = 0, sv = 0, suw = 0, svw = 0, sw = 0
  73. for (const [u, v] of points) {
  74. const w = u * u + v * v
  75. suu += u * u; suv += u * v; su += u; svv += v * v; sv += v
  76. suw += w * u; svw += w * v; sw += w
  77. }
  78. const A = [[suu, suv, su], [suv, svv, sv], [su, sv, n]]
  79. const b = [suw, svw, sw]
  80. for (let i = 0; i < 3; i++) {
  81. let p = i
  82. for (let k = i + 1; k < 3; k++) if (Math.abs(A[k][i]) > Math.abs(A[p][i])) p = k
  83. ;[A[i], A[p]] = [A[p], A[i]]; [b[i], b[p]] = [b[p], b[i]]
  84. for (let k = i + 1; k < 3; k++) {
  85. const f = A[k][i] / A[i][i]
  86. for (let j = i; j < 3; j++) A[k][j] -= f * A[i][j]
  87. b[k] -= f * b[i]
  88. }
  89. }
  90. const z = [0, 0, 0]
  91. for (let i = 2; i >= 0; i--) {
  92. z[i] = b[i]
  93. for (let j = i + 1; j < 3; j++) z[i] -= A[i][j] * z[j]
  94. z[i] /= A[i][i]
  95. }
  96. const [D, E, F] = z
  97. return { cx: -D / 2, cy: -E / 2, r: Math.sqrt((D * D) / 4 + (E * E) / 4 - F) }
  98. }
  99. const fit = fitCircle(pts)
  100. let maxRes = 0
  101. for (const [x, y] of pts) maxRes = Math.max(maxRes, Math.abs(Math.hypot(x - fit.cx, y - fit.cy) - fit.r))
  102. console.log(`下半段圆弧拟合:圆心 (${fit.cx.toFixed(1)}, ${fit.cy.toFixed(1)}) 半径 ${fit.r.toFixed(1)},最大残差 ${maxRes.toFixed(2)}px`)
  103. const tip = { x: 255.5, y: 493 }
  104. const dTip = Math.hypot(tip.x - fit.cx, tip.y - fit.cy)
  105. console.log(`尖端到拟合圆心距离 ${dTip.toFixed(1)} vs 半径 ${fit.r.toFixed(1)}(差 ${(dTip - fit.r).toFixed(1)}px → ${Math.abs(dTip - fit.r) < 2 ? '尖端在圆上,A 命令直达' : '尖端不在圆上'})`)
  106. // 帽圆 ∩ 拟合圆 交点
  107. const dx = fit.cx - CAP.cx, dy = fit.cy - CAP.cy, d = Math.hypot(dx, dy)
  108. const a = (CAP.r * CAP.r - fit.r * fit.r + d * d) / (2 * d)
  109. const h = Math.sqrt(Math.max(0, CAP.r * CAP.r - a * a))
  110. const mx = CAP.cx + (a * dx) / d, my = CAP.cy + (a * dy) / d
  111. console.log(`帽圆∩拟合圆:(${(mx + (h * dy) / d).toFixed(1)}, ${(my - (h * dx) / d).toFixed(1)}) / (${(mx - (h * dy) / d).toFixed(1)}, ${(my + (h * dx) / d).toFixed(1)})`)
  112. // ---- ② 镂空版孔洞几何 ----
  113. const hollow = decodePng('public/cockpit/assets/icons/T_MarkPoint_White.png')
  114. const rowGaps = (y) => {
  115. const gaps = []
  116. let start = -1
  117. for (let x = MINX; x <= MAXX; x++) {
  118. const al = hollow.alphaAt(x, y)
  119. if (al <= 8 && start < 0) start = x
  120. if (al > 8 && start >= 0) { gaps.push([start, x - 1]); start = -1 }
  121. }
  122. if (start >= 0) gaps.push([start, MAXX])
  123. return gaps
  124. }
  125. const colGaps = () => {
  126. const gaps = []
  127. let start = -1
  128. for (let y = MINY; y <= MAXY; y++) {
  129. const al = hollow.alphaAt(256, y)
  130. if (al <= 8 && start < 0) start = y
  131. if (al > 8 && start >= 0) { gaps.push([start, y - 1]); start = -1 }
  132. }
  133. if (start >= 0) gaps.push([start, MAXY])
  134. return gaps
  135. }
  136. console.log('\n镂空版 y=208 透明段:', JSON.stringify(rowGaps(208)))
  137. console.log('镂空版 y=190 透明段:', JSON.stringify(rowGaps(190)))
  138. console.log('镂空版 x=256 透明段:', JSON.stringify(colGaps()))