/** * [INPUT]: 无外部依赖(纯函数);入参 map{nodes:[{t,d,ref:[[c,l]]}],pts:[[x,y]]}、cats(课程系列,解析 ref→课节)、doneSet(完成集)、courseIdx * [OUTPUT]: buildStarMap(...) → {stars,edges,nodes,stations,mapProg,curIdx,allDone};逐字移植 DC renderVals 几何(源行 2869-2914) * [POS]: domain/v4 的星图几何派生,被 StarMapSvg / CourseMap 消费;数据源 data/v4/courses 或 get_learning_maps * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md */ const ACCENT = 'var(--zzm-accent,#A6402F)' const rr = (i, n) => { const x = Math.sin((i + 1) * n) * 43758.5; return x - Math.floor(x) } const clip = (s) => (s && s.length > 10 ? s.slice(0, 10) + '…' : s || '') // 22 颗装饰背景星(源行 2894) function buildStars() { return Array.from({ length: 22 }, (_, i) => ({ x: +(20 + 700 * rr(i, 12.9)).toFixed(1), y: +(14 + 372 * rr(i, 78.2)).toFixed(1), r: +(0.8 + 1.3 * rr(i, 45.6)).toFixed(2), o: +(0.1 + 0.2 * rr(i, 91.3)).toFixed(2), })) } // 节点前 3 节课预览(源行 2904-2908):ref→cats[c].ls[l] 解析真实标题 + 完成态 function nodeLessons(ref, cats, isDone, y) { const L = ref.slice(0, 3).map((rf, j) => { const a2 = cats[rf[0]] && cats[rf[0]].ls[rf[1]] const dd = isDone(rf[0], rf[1]) return { y: y + 48 + j * 14, t: (dd ? '✓ ' : '') + (a2 ? clip(a2[0]) : '(已删除)'), f: dd ? ACCENT : '#8E8E88', w: dd ? 600 : 400 } }) if (ref.length > 3) L.push({ y: y + 48 + 42, t: '… 共 ' + ref.length + ' 节', f: '#B0B0AA', w: 400 }) return L } // 主入口:真实 map(nodes+pts) + cats + 完成集 → 星图几何 export function buildStarMap(map, cats = [], doneSet = new Set(), courseIdx = 0) { const NDs = map?.nodes ?? [] const pts = map?.pts ?? [] const isDone = (c, l) => doneSet.has(courseIdx + '-' + c + '-' + l) const done = NDs.map((nd) => nd.ref.length > 0 && nd.ref.every(([c, l]) => isDone(c, l))) let curIdx = done.findIndex((d) => !d) const allDone = curIdx < 0 if (allDone) curIdx = 0 const edges = pts.slice(1).map((p, i) => { const a = pts[i], dnn = i < curIdx || allDone return { x1: a[0], y1: a[1], x2: p[0], y2: p[1], c: dnn ? '#1D1D1B' : 'rgba(0,0,0,.17)', w: dnn ? 1.6 : 1.2, d: dnn ? '0' : '3 5' } }) const nodes = NDs.map((nd, i) => { const x = pts[i][0], y = pts[i][1], d = done[i], cu = i === curIdx && !allDone && !d return { x, y, r: d || cu ? 12 : 11, fill: d ? '#1D1D1B' : cu ? ACCENT : '#FFFFFF', stroke: d ? '#1D1D1B' : cu ? ACCENT : '#C6C6C0', mark: d ? '✓' : String(i + 1), mf: d || cu ? '#FFFFFF' : '#71716C', haloR: cu ? 19 : 0, sy: y - 26, stn: '第 ' + (i + 1) + ' 站', label: nd.t, lf: cu ? '#1D1D1B' : '#6E6E6A', lw: cu ? 600 : 500, my: y + 3.5, ly: y + 31, lessons: nodeLessons(nd.ref, cats, isDone, y), } }) const stations = NDs.map((nd, i) => { const d = done[i], cu = i === curIdx && !allDone && !d return { mark: d ? '✓' : String(i + 1), bg: d ? '#1D1D1B' : cu ? ACCENT : '#FFFFFF', c: d || cu ? '#FFFFFF' : '#71716C', bd: d ? '#1D1D1B' : cu ? ACCENT : '#C6C6C0', w: cu ? 650 : 500, tc: cu ? '#161616' : '#3F3F3B', rowBg: cu ? 'rgba(166,64,47,.06)' : 'transparent', title: nd.t, desc: nd.d || '', meta: nd.ref.length + ' 节', index: i, } }) return { stars: buildStars(), edges, nodes, stations, mapProg: done.filter(Boolean).length + ' / ' + NDs.length + ' 站已完成', curIdx, allDone, } }