Compare commits

...

3 Commits

Author SHA1 Message Date
k88936 cfbc6c36a5 共享 CanvasWorld 统一超采样
Project CI / Frontend tests (pull_request) Failing after 2m16s
Project CI / Repository checks (pull_request) Successful in 2m45s
Project CI / Native shell tests (pull_request) Failing after 4m47s
Project CI / Backend tests (pull_request) Successful in 5m58s
默认以 2 倍内容分辨率渲染并保持 viewport.scale 的最终视觉比例

为 world 内 SVG 几何元素启用非缩放描边

补充超采样 transform 与 inverse-scale 契约测试
2026-09-05 11:14:56 +08:00
k88936 4e7b4c3e2c 补充共享画布超采样决策文档
记录 CanvasWorld 默认 2 倍超采样与 viewport.scale 视觉比例契约

记录资源页继续使用 12000 正方形 world 且不新增矩形尺寸 API

记录 SVG 非缩放描边与资源页迁移验收边界
2026-09-05 11:14:55 +08:00
k88936 6c1a77b346 资源页复用共享 CanvasWorld
移除项目开发页自有 world 变换与动态矩形尺寸

通过共享 CanvasWorld 提供资源卡控件反向缩放变量
2026-09-05 11:14:55 +08:00
7 changed files with 78 additions and 17 deletions
+10
View File
@@ -5900,6 +5900,16 @@ iframe.preview-frame {
text-overflow: ellipsis;
white-space: nowrap;
box-shadow: 0 8px 18px rgb(96 62 47 / 20%);
transform: scale(var(--genarrative-image-canvas-inverse-scale, 1));
transform-origin: top right;
}
.game-resource-card-media-control,
.game-resource-card-placeholder > svg,
.game-resource-card-version-visual > svg,
.game-resource-card-audio-visual > svg {
transform: scale(var(--genarrative-image-canvas-inverse-scale, 1));
transform-origin: center;
}
.game-resource-card-open {
@@ -3,6 +3,7 @@ import {
type CanvasViewport,
resolveViewportFromWheel,
} from '@genarrative/image-canvas-core';
import { CanvasWorld } from '@genarrative/image-canvas-react';
import {
Code2,
FileText,
@@ -4906,17 +4907,12 @@ export default function ProjectDevelopmentView({
>
<RotateCcw size={15} aria-hidden="true" />
</button>
<div
<CanvasWorld
className="game-resource-page-world"
viewport={activeResourceCanvasViewport}
data-resource-boundary={`${resourceCanvasNavigationBounds.x},${resourceCanvasNavigationBounds.y},${resourceCanvasNavigationBounds.width},${resourceCanvasNavigationBounds.height}`}
data-resource-fit-boundary={`${resourceCanvasFitBounds.x},${resourceCanvasFitBounds.y},${resourceCanvasFitBounds.width},${resourceCanvasFitBounds.height}`}
data-resource-viewport={`${activeResourceCanvasViewport.x},${activeResourceCanvasViewport.y},${activeResourceCanvasViewport.scale}`}
style={{
width: `${resourceCanvasNavigationBounds.width}px`,
height: `${resourceCanvasNavigationBounds.height}px`,
transform: `translate(${activeResourceCanvasViewport.x}px, ${activeResourceCanvasViewport.y}px) scale(${activeResourceCanvasViewport.scale})`,
transformOrigin: '0 0',
}}
>
{sortMode === 'dependency' ? (
<ResourceDependencyOverlay
@@ -5016,7 +5012,7 @@ export default function ProjectDevelopmentView({
/>
);
})}
</div>
</CanvasWorld>
{(
visibleResourcesByCategory.get(
activePageCategory,
@@ -7949,6 +7949,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 新建/恢复 ephemeral Codex thread 时,replay 使用 `contextWindowTokens``autoCompactTokenLimit`、本次 `maxOutputTokens` 与 4096 安全余量计算预算,从最新记录向前选择连续完整的 `user` / `assistant` / `tool` 行;超预算旧前缀被省略,单条记录不截断,当前 user request 始终保留。
- 发生省略时在 prompt 开头加入普通 `system: Earlier conversation history was omitted due to context budget.` 提示;当前请求本身超过硬上下文预算则直接失败。该策略是 Direct 专用滑动窗口,不复用 Runtime Agent 的摘要、tail 或 session compaction 生命周期。
## 2026-09-05 共享 CanvasWorld 统一超采样与资源页迁移
- 背景:资源总览页曾在业务层重复实现 world 的 transform、动态矩形尺寸和 2× `zoom`,与网站 / Tauri 共用的 `@genarrative/image-canvas-react` 形成第二套缩放逻辑;超采样还会让 SVG 卡片图标的线条产生视觉加粗。
- 决策:`CanvasWorld` 默认统一采用 2× 超采样,内容按 `zoom: 2` 排版,外层使用 `viewport.scale / 2`,最终视觉比例仍由 `viewport.scale` 定义。所有调用方继续使用共享默认正方形 `CANVAS_WORLD_SIZE = 12000`,不引入 `worldWidth/worldHeight` 等矩形 API。资源页 `navigationBounds` 只保留布局、fit、依赖线 geometry 和 data 属性用途,不再驱动 world DOM 尺寸。共享样式对 world 内 SVG 几何元素设置 `vector-effect: non-scaling-stroke`;位图维持浏览器默认插值,不全局启用 `pixelated`
- 影响范围:资源总览迁移为直接渲染共享 `CanvasWorld`,卡片控件通过共享 inverse-scale CSS 变量保持屏幕级尺寸;Asset Canvas、UI Editor 和依赖线继续消费相同 viewport 坐标与事件换算,不改变业务状态或持久化合同。
- 验证方式:共享包与 AGC 定向类型 / 测试、资源页集成测试、`npm run check:encoding``git diff --check`;重点检查 `scale=1/2` 的最终 transform、SVG 描边、拖拽坐标、关系线端点和默认 `12000 × 12000` world。
## 2026-08-31 AGC 错误报告与诊断上传
- AGC 采用 IDEA 风格的当前进程错误池:按 fingerprint 合并 React / window / Promise / Tauri / Agent 错误,重启后不恢复,不使用 run 或 run_id。
@@ -549,6 +549,7 @@ game-project/
- 中间主视窗提供 `资源管理 / 运行` 切换。`code-prototype` 任务完成前运行入口保持视觉不可用,但仍可点击查看“当前无可运行版本”,不能使用会阻断说明交互的原生 `disabled``aria-disabled`;完成后才允许进入运行表现层。切回资源管理只修改前端展示态,不伪造后端预览暂停结果。
- 资源管理从当前 `GameCreationAppManifest`(包含可选 `versions`)、合法 Agent 文本回执和已导入附件派生资源,固定按文档、项目版本、美术资源、音乐音效资源分区;未知任务产物不再兜底为版本,任务声明中的未登记音频也不冒充正式音频。`按依赖 / 按类型` 使用各自前端排列,dependency 模式额外绘制当前 manifest 与资源投影可证明的依赖关系。排列与图层都不写回 manifest,不能推断或伪造缺失依赖。
- 资源卡支持点击聚焦、搜索和类型筛选。2026-07-28 起完成两套二维坐标与本地 CAS sidecar2026-07-31 起 dependency 模式增加不持久化的原生 SVG 关系图层。2026-08-03 mentor 决定暂缓资源卡拖动,当前卡片不挂载 Pointer Down / Move / Up / Cancel 拖动入口,只允许自动布局和点击聚焦。聚焦态替换中央主视窗内容,保留左侧导航、右侧对话和底部 Agent 状态栏,退出后恢复搜索、布局模式、滚动位置与选中资源;不提供工具栏、工具侧边栏或可拖动标题栏。2026-08-10 起聚焦态以资源元数据、Rust 权威深度、同类型上下游 / 任务流和版本信息为首屏;美术图片与视频只保留卡内本体,不在详情重复放大。安全文档正文与按意图读取的音频控制位于元数据之后;美术编辑、音频编辑 / 替换、版本替换或运行模块仍不在本阶段。
- 2026-09-05 起所有 `@genarrative/image-canvas-react``CanvasWorld` 默认使用统一 2× 超采样:内容层按 `zoom: 2` 排版,外层 transform 使用 `viewport.scale / 2`,因此 `viewport.scale` 仍表示最终视觉比例(`1` 为原始大小,`2` 为放大两倍)。world 继续使用共享默认正方形 `CANVAS_WORLD_SIZE = 12000`,不新增矩形尺寸 API;资源页的 `navigationBounds` 只用于布局、fit、关系线 geometry 和数据属性,不能再设置 world DOM 宽高。共享 world 为 SVG 几何元素启用 `vector-effect: non-scaling-stroke`,避免超采样或缩放使卡片图标和关系线描边变粗;普通位图不强制 `pixelated` 插值。依赖线、卡片拖拽、平移和滚轮锚点继续使用同一逻辑 viewport 坐标。
#### 资源管理串行改造:本体卡、分区缩放与依赖聚类
@@ -2,7 +2,9 @@ import {
CANVAS_WORLD_SIZE,
type CanvasViewport,
} from '@genarrative/image-canvas-core';
import type { HTMLAttributes, ReactNode } from 'react';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
const CANVAS_RENDER_SUPERSAMPLE = 2;
export function CanvasWorld({
children,
@@ -16,18 +18,34 @@ export function CanvasWorld({
viewport: CanvasViewport;
worldSize?: number;
}) {
const viewportScale =
Number.isFinite(viewport.scale) && viewport.scale > 0 ? viewport.scale : 1;
return (
<div
{...props}
className={`genarrative-image-canvas__world ${className}`.trim()}
style={{
...style,
width: worldSize,
height: worldSize,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.scale})`,
}}
style={
{
...style,
width: worldSize,
height: worldSize,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewportScale / CANVAS_RENDER_SUPERSAMPLE})`,
'--genarrative-image-canvas-inverse-scale': String(1 / viewportScale),
} as CSSProperties
}
>
{children}
<div
className="genarrative-image-canvas__world-content"
style={{
position: 'relative',
width: worldSize,
height: worldSize,
zoom: CANVAS_RENDER_SUPERSAMPLE,
}}
>
{children}
</div>
</div>
);
}
@@ -1,10 +1,11 @@
/* @vitest-environment jsdom */
import type { CanvasLayer } from '@genarrative/image-canvas-core';
import { act, renderHook } from '@testing-library/react';
import { act, render, renderHook, screen } from '@testing-library/react';
import { createRef } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { CanvasWorld } from './CanvasWorld';
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
function layer(id: string, x: number): CanvasLayer {
@@ -52,6 +53,24 @@ afterEach(() => {
});
describe('shared canvas React lifecycle', () => {
it('renders a supersampled world while preserving the public viewport scale', () => {
render(
<CanvasWorld viewport={{ x: 12, y: 24, scale: 2 }}>
<span>content</span>
</CanvasWorld>,
);
const world = screen.getByText('content').parentElement?.parentElement;
const content = screen.getByText('content').parentElement;
expect(world?.style.width).toBe('12000px');
expect(world?.style.height).toBe('12000px');
expect(world?.style.transform).toBe('translate(12px, 24px) scale(1)');
expect(
world?.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'),
).toBe('0.5');
expect(content?.style.zoom).toBe('2');
});
it('removes every native listener across repeated mounts', () => {
const viewport = createViewport();
const addListener = vi.spyOn(viewport, 'addEventListener');
@@ -29,6 +29,16 @@
transform-origin: 0 0;
}
.genarrative-image-canvas__world svg path,
.genarrative-image-canvas__world svg line,
.genarrative-image-canvas__world svg polyline,
.genarrative-image-canvas__world svg polygon,
.genarrative-image-canvas__world svg circle,
.genarrative-image-canvas__world svg rect,
.genarrative-image-canvas__world svg ellipse {
vector-effect: non-scaling-stroke;
}
.genarrative-image-canvas__layer {
position: absolute;
display: block;