连线锚点:每条边(含扇出)钉住父底中点/子顶中点;规范补锚点定标(真机 14 端点 Δx=0.00)与紧凑卡 182
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
This commit is contained in:
@@ -469,9 +469,10 @@ A 路线里那个必须提前知道的互斥点已经按既有做法绕开:`cr
|
||||
- **卡面不再有动作按钮**:「打开作品 / 开始共创」已从卡面移除(用户口径:看封面决定不了共创,必须点进去看)——**整卡就是入口**(`GameCard` 本身是 `<button>`,点击 → 作品详情;占位节点点击不导航)。
|
||||
- **图谱语义走角标槽**:第 N 代 / 主干 / 当前作品 / 父代-子代 / `▶N`(子树合计)全部通过 `badges` 槽承载;高亮 / 变暗 / 主干线状态仍在外层最薄 wrapper 的 class(wrapper 只负责定位与连线锚点、不改卡内视觉)。选中/高亮改为**悬停或键盘聚焦**驱动(点击已让位给「进详情」)。
|
||||
- **封面**:`.game-card--lineage .game-cover { aspect-ratio: 16/9; min-height: 0 }` + 图片 `100%×100% + object-fit: cover`(统一 16:9 铺满)。
|
||||
- **层高常量按新卡实测重算**:真机实测(212 宽画布卡、标题两行)卡高 **231.5px** → `LINEAGE_CARD_BASE_HEIGHT = 214` / `TITLE_LINE_HEIGHT = 20`(两行估算 234 ≥ 实测);每行字符数按实测换行取 `10`;紧凑卡 `210 / +20`。层间距 = 上一层最高卡 + `rowGap`,实测无重叠。
|
||||
- **层高常量按新卡实测重算**:真机实测(212 宽画布卡、标题两行)卡高 **231.5px** → `LINEAGE_CARD_BASE_HEIGHT = 214` / `TITLE_LINE_HEIGHT = 20`(两行估算 234 ≥ 实测);每行字符数按实测换行取 `10`;紧凑卡 **`182 / +20`**(窄视口 390×844 / 360×800、卡宽 148、两行标题实测 **196px** ⇒ 估算 202 ≥ 实测,留 6px)。层间距 = 上一层最高卡 + `rowGap`,冷启动实测层间空隙 **107 / 107 / 107**(层盒 `0..232 / 318..550 / 656..888 / 994..1226`),层内零重叠。
|
||||
- **连线锚点口径(2026-10-07 真机定标)**:竖版 `from = 父盒下边中点`、`to = 子盒上边中点`;横版 `from = 父盒右边中点`、`to = 子盒左边中点`。**水平方向严格落在卡片中心**:真机 7 条边 × 两端共 14 个端点,用 `path.getPointAtLength(0|len)` + `getScreenCTM()` 变换到视口坐标与 `getBoundingClientRect()` 同坐标系比较,偏差 **全部 0.00px**(节点盒内左右内边距相等 ⇒ 卡片中心 == 盒中心,实测 `cardCenter − nodeCenter = 0`)。**不存在任何横向扇出偏移**:一个父带多个子时,各条边从同一个父中点出发、各自落到不同子的中点(`lineageTreeLayout.test.ts` 已用 `toEqual` 钉住每条边,含扇出边)。纵向锚点取的是**布局盒**(`node.y` / `node.y + 估算层高`)而不是卡片自身的矩形:节点盒实际渲染高度 = 卡片 + 上下内边距(`11.2px×2` + `1px 边框`),比估算层高高,所以父端锚点落在卡片底边**上方约 9.7px**(被卡片盖住,不可见)、子端锚点落在子卡上边**上方约 12.2px**(一段空地);两端留白**只与这组 CSS 度量有关、逐边恒定**,因此视觉上各条连线一致。
|
||||
- **大纲行(移动端列表形态)继续用 `LineageNodeContent`**:它是**列表行**不是卡片(行内保留「打开作品 / 开始共创」,是移动端的既有设计),因此该组件未被删除;若产品要求连大纲也卡化,再单独处理。
|
||||
- **层高按该层最高卡片**:布局新增输入 `cardHeights`(每个节点的卡片高度,由**纯函数** `estimateLineageCardHeight` 估算:基础高 + (标题行数−1)×行高,标题行数按字符宽度估、上限 2 行)。竖版 `第 N 层的 y = 上一层 y + 上一层最高卡片高度 + rowGap`,层内所有卡片取该层最大高度(同层等高,`min-height` 撑开),因此「同层里有一张 2 行标题的高卡」也不会被下一层压住;横版(窄屏图谱)槽位高取全局最大卡片高。**不测量 DOM**(纯函数可单测、离线复算,不会先渲染再重排抖动)。常量由真机校准(**卡片改为广场卡组件后重测**):桌面两行标题实测 231.5px → 取 214 / +20;每行字符数取 10;窄屏紧凑卡 210 / +20(估算 ≥ 实测,只多不少)。
|
||||
- **层高按该层最高卡片**:布局新增输入 `cardHeights`(每个节点的卡片高度,由**纯函数** `estimateLineageCardHeight` 估算:基础高 + (标题行数−1)×行高,标题行数按字符宽度估、上限 2 行)。竖版 `第 N 层的 y = 上一层 y + 上一层最高卡片高度 + rowGap`,层内所有卡片取该层最大高度(同层等高,`min-height` 撑开),因此「同层里有一张 2 行标题的高卡」也不会被下一层压住;横版(窄屏图谱)槽位高取全局最大卡片高。**不测量 DOM**(纯函数可单测、离线复算,不会先渲染再重排抖动)。常量由真机校准(**卡片改为广场卡组件后重测**):桌面两行标题实测 231.5px → 取 214 / +20;每行字符数取 10;窄屏紧凑卡 **182 / +20**(实测两行 196px ⇒ 估算 202,只多不少)。
|
||||
- **代际轴按层高画**:`LineageGenerationAxisEntry` 增加 `height`(该层层高),轴的每一行高度跟着层高走(不再拿「第一个节点的高度」凑)。
|
||||
- **角标组移到封面下方**(不再用深色药丸压在封面图上),并且**上色**——每类语义一色、**全部取自平台既有 `--platform-*` 变量,不新增色值**:代际 = 中性(neutral)|**主干 = 暖色(warm,强调)**|**当前作品 = 主色实心(accent solid)**|父代 = cool|子代 = success|▶游玩数 = 中性。组内仍是 flex + gap + 可换行(**绝不重叠**),并有 1px 边框保证在浅色卡片上有边界。
|
||||
- **卡片底部重做**:作者行独立一行、更小更淡(`--platform-text-muted`);操作区是**两个等权重按钮**(`flex: 1 1 0`、`min-height: 2rem` = 32px 触控目标、同圆角同内边距),`打开作品` = 次要(描边),`**开始共创**` = 主要(填充/着色);窄卡允许换行但不裁切。大纲行同口径(`打开作品` 描边 / `开始共创` 填充)。
|
||||
@@ -1093,6 +1094,8 @@ procedure 另要求**受信服务身份**(归属判定靠 `owner_user_id`,
|
||||
| 族谱画布(`layout="stacked"`)节点**不再显示作者名** | 原因:`LineageNodeContent` 只在 `layout === 'inline'` 渲染作者行,画布传 `stacked` → 作者整列消失(`aria-label` 里仍有,故只有视觉缺失)。修复 `9cf30517d`:仅**窄屏紧凑卡片**省作者,其余两种形态都渲染。复验:本地夹具脚本(族谱/血缘) → **188/188 PASS**(原唯一 FAIL「浏览器视觉:母版节点显示作者名」转绿);1440 桌面截图可见卡片上有「演示作者甲/乙/丙/丁」 | ✅ 已修并复验 |
|
||||
| 移动端切「图谱视图」(横向画布)**不会初始定位到当前作品** | 修复前 390 宽实测:`zoom=100%`、`transform: matrix(1,0,0,1,0,0)`,画布可视区 `x 37–353`,当前作品节点头部 `x 457–633` → 0/8 节点可见。修复 `ec8f27cb2`(横版画布 + 进入时按宽度适配并定位当前作品)。复验(360/390/414 三档):`zoom=35%`、当前作品在画布内、可见节点 **7/8(360、390)/ 8/8(414)** | ✅ 已修并复验 |
|
||||
| (遗留备注)窄屏图谱视图里点卡片会跳详情 | 窄屏紧凑卡片整块是「打开详情」按钮,没有可锁定区域 → 信息面板/面包屑的窄屏验证改由**大纲形态**做(点行弹底部 sheet、面包屑出现并被滚进视口,三档均 PASS)。不是缺陷,但窄屏画布下确实没有「锁定」入口 | 📝 记录 |
|
||||
| **连线锚点是否落在卡片水平中心**(2026-10-07 定标,用户点名要逐条数值) | 方法:`path.getPointAtLength(0)` / `(len)` 取端点 + `path.getScreenCTM()` 变换到视口坐标,与 `card.getBoundingClientRect()` **同一坐标系**比较(不手算 `scale(0.6)`)。演示树 7 条边 × 两端 = 14 个端点,`Δx` **全部 0.00px**(父/子卡中心与端点 x 完全相等);同一批还量到 `cardCenter − nodeCenter = 0.00`(节点盒左右内边距相等 ⇒ 卡片在盒内精确居中),故早期一次「13.8px」是**配对错误**(按最近卡中心匹配)不是错位。纵向:父端锚点在卡底边上方 9.7px(被卡片盖住)、子端在子卡顶边上方 12.2px,逐边恒定(来源 = 节点盒 `11.2px` 上下内边距 + `1px` 边框,见 §3.6.1)。原始数据 `%TEMP%\lineage-cardunify-verify\anchor-table.json`、截图 `05-anchor-table.png` | ✅ 已验证 |
|
||||
| **「expected 304 to be 344」(差 40px)定性** | **结论:我的断言写错,不是布局错位**。elbow 路径末点是写成 `… V 344`(无逗号)的,我用 `/(\d+),(\d+)/g` 抓「最后一个坐标对」时抓到的是圆角 `Q 348,292 348,304` 的**曲线控制点**(`348,304`),它当然不等于端点。证据三重:① 纯函数打印(`lineageTreeLayout.test.ts` 临时 `console.log`)显示 4 条边的 `from/to` 与父底中点 / 子顶中点**逐字段相等**;② 已把断言补成「对**每一条边**(含一父多子扇出)`toEqual` 父底中点 / 子顶中点 + 路径首点 = from + 路径末段以 `to.y` 结尾」并常驻测试(本轮提交);③ 真机 14 端点 Δx 全 0.00。**因此「横向扇出偏移」这条假设被否掉**,连线不存在横向扇出 | ✅ 已定性并落地断言 |
|
||||
|
||||
### 5.7 AGC 客户端共创页 / 「开始共创」确认页与外壳滚动链(2026-10-07)实施证据
|
||||
|
||||
|
||||
@@ -225,12 +225,36 @@ describe('buildLineageTreeLayout', () => {
|
||||
node('root', 0, null),
|
||||
node('a', 1, 'root'),
|
||||
node('b', 1, 'root'),
|
||||
node('a1', 2, 'a'),
|
||||
node('b1', 2, 'b'),
|
||||
]);
|
||||
const elbow = branched.edges.find((item) => item.childGameId === 'a')!;
|
||||
expect(elbow.path).toContain(' V ');
|
||||
expect(elbow.path).toContain(' H ');
|
||||
expect(elbow.path).toContain(' Q ');
|
||||
expect(elbow.path).not.toContain(' C ');
|
||||
|
||||
// 每一条边(含「一个父 + 多个子」的扇出边)都必须钉在父卡下边中点 / 子卡上边中点上,
|
||||
// 不允许任何横向扇出偏移:图谱连线垂直落在卡片水平中心(真机同口径)。
|
||||
const byId = new Map(branched.nodes.map((item) => [item.gameId, item]));
|
||||
expect(branched.edges).toHaveLength(4);
|
||||
for (const item of branched.edges) {
|
||||
const parent = byId.get(item.parentGameId)!;
|
||||
const child = byId.get(item.childGameId)!;
|
||||
expect(item.from).toEqual({
|
||||
x: parent.x + parent.width / 2,
|
||||
y: parent.y + parent.height,
|
||||
});
|
||||
expect(item.to).toEqual({
|
||||
x: child.x + child.width / 2,
|
||||
y: child.y,
|
||||
});
|
||||
// 路径首点 = 父锚点;路径末点写在最后一段 V/H 里(曲线控制点不是端点)。
|
||||
expect(item.path.startsWith(`M ${item.from.x},${item.from.y}`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(item.path.trimEnd().endsWith(` ${item.to.y}`)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('确定性:同一输入两次调用结果逐字段相同', () => {
|
||||
|
||||
Reference in New Issue
Block a user