资源画布交互 1:1 复刻美术画布:点卡选中并浮出工具条,移除资源详情面板与全屏编辑路由
新增 features/resource-canvas:资源卡到美术画布 CanvasLayer 的适配、工具条动作可用性判定、C5 当前版本绑定判定、快速编辑草稿工厂与 AGC 宿主 chrome 样式 资源卡点击改为选中(Shift/Ctrl/Meta 多选),选中后在卡片旁复用 ImageCanvasSelectedLayerToolbarView 浮出工具条 快速编辑复用 ImageCanvasQuickEditPanelView 浮层面板,先调 normalize_local_project_raster_resource 正规化任务产物,再走 derive_local_project_resource(editKind=image-reference) 产出新素材,失败重试复用同一 operationId/idempotencyKey 删除 resourceEditorRoute 全链路(ResourceEditSurface 路由、openResourceEditor、handleResourceCardOpenEditor、cancelResourceEditor、submitResourceEdit)与资源详情面板(focusedResource 状态/引用/浮层 JSX/CSS 断言) 分类与标签入口搬到选中工具条;UI 资源保留「UI 编辑器」入口并从工具条打开 移除 apps/ai-game-creator-shell/scripts/check-config.mjs 中 normalize_local_project_raster_resource 的过渡 allowlist 条目 为共享工具条组件新增 opt-in 的 supportedActions/extraActions(默认 null/undefined,网页端行为不变) AGC vite-env.d.ts 补 wx/ReactNativeWebView/WeixinJSBridge 跨端全局声明;hostBridge fail 回调补显式 unknown 类型 测试:资源卡查询 helper 由资源详情按钮改为选中按钮,清理详情面板断言,画布空白左键改为框选后平移用例改用中键
This commit is contained in:
@@ -111,7 +111,6 @@ const allowedUncalledTauriCommands = [
|
|||||||
'chat_with_game_creator_agent',
|
'chat_with_game_creator_agent',
|
||||||
'check_ui_editor_font_glyph_coverage',
|
'check_ui_editor_font_glyph_coverage',
|
||||||
'create_ui_design_resource',
|
'create_ui_design_resource',
|
||||||
'normalize_local_project_raster_resource',
|
|
||||||
'open_game_creator_launcher_window',
|
'open_game_creator_launcher_window',
|
||||||
'open_game_creator_workspace_window',
|
'open_game_creator_workspace_window',
|
||||||
'read_direct_project_conversation',
|
'read_direct_project_conversation',
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* 共享美术画布组件(ImageCanvasSelectedLayerToolbarView / ImageCanvasQuickEditPanelView)
|
||||||
|
* 的 AGC 宿主 chrome。
|
||||||
|
*
|
||||||
|
* 这些 `image-canvas-editor__*` 类名在网页端由全局样式表 src/index.css 提供;AGC 是独立
|
||||||
|
* 应用、不引入那张整站样式表,因此在这里按实际用到的类补一套宿主样式:定位、浮层层级、
|
||||||
|
* 按钮与面板外观。组件本身与交互语义仍全部复用美术画布的实现。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 主题变量:与网页端 .image-canvas-editor 作用域下的一组变量保持一致。 */
|
||||||
|
.game-resource-canvas-toolbar-host,
|
||||||
|
.image-canvas-editor__portal-theme,
|
||||||
|
.image-canvas-editor__portal-menu {
|
||||||
|
--image-canvas-brand-accent: var(--platform-accent, #c7653d);
|
||||||
|
--image-canvas-brand-accent-strong: #6f2f21;
|
||||||
|
--image-canvas-brand-fill: var(
|
||||||
|
--platform-button-primary-fill,
|
||||||
|
linear-gradient(135deg, #df7f40, #b95d3a)
|
||||||
|
);
|
||||||
|
--image-canvas-brand-text-on-fill: var(
|
||||||
|
--platform-button-primary-text,
|
||||||
|
#fffaf5
|
||||||
|
);
|
||||||
|
--image-canvas-brand-border: rgba(204, 117, 76, 0.34);
|
||||||
|
--image-canvas-brand-border-strong: rgba(199, 101, 61, 0.64);
|
||||||
|
--image-canvas-brand-border-soft: rgba(226, 203, 184, 0.82);
|
||||||
|
--image-canvas-brand-soft: rgba(234, 204, 179, 0.28);
|
||||||
|
--image-canvas-brand-soft-strong: rgba(238, 208, 183, 0.46);
|
||||||
|
--image-canvas-brand-shadow: rgba(182, 98, 63, 0.16);
|
||||||
|
--image-canvas-brand-focus-ring: rgba(204, 117, 76, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中工具条:贴在资源卡正上方浮出,层级高于卡片与画布标题栏。 */
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 60;
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: min(560px, 92vw);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.3rem;
|
||||||
|
border: 1px solid var(--image-canvas-brand-border-soft);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0.28rem;
|
||||||
|
box-shadow: 0 18px 34px rgba(15, 23, 42, 0.18);
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--image-canvas-brand-border-soft);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #334155;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 160ms ease,
|
||||||
|
background-color 160ms ease,
|
||||||
|
border-color 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar
|
||||||
|
button:hover:not([disabled]) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: var(--image-canvas-brand-border-strong);
|
||||||
|
background: var(--image-canvas-brand-soft);
|
||||||
|
color: var(--image-canvas-brand-accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar
|
||||||
|
button[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar
|
||||||
|
button.image-canvas-editor__floating-toolbar-text-button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.34rem 0.62rem;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 850;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar-divider {
|
||||||
|
display: block;
|
||||||
|
width: 1px;
|
||||||
|
height: 1.1rem;
|
||||||
|
background: var(--image-canvas-brand-border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快速编辑浮层面板:锚在资源卡正下方。 */
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__generation-composer {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 70;
|
||||||
|
display: flex;
|
||||||
|
width: min(360px, 92vw);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--image-canvas-brand-border-soft);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 22px 40px rgba(15, 23, 42, 0.22);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__generation-prompt {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4.5rem;
|
||||||
|
resize: none;
|
||||||
|
border: 1px solid var(--image-canvas-brand-border-soft);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
background: #fffdfa;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__reference-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__generation-composer-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__option-cluster {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__option-popover-anchor {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__option-popover {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 80;
|
||||||
|
min-width: 12rem;
|
||||||
|
border: 1px solid var(--image-canvas-brand-border-soft);
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-shadow: 0 18px 32px rgba(15, 23, 42, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__option-popover-sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__option-popover-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__option-popover-choice {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.28rem 0.5rem;
|
||||||
|
border: 1px solid var(--image-canvas-brand-border-soft);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #fffdfa;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__generation-submit {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--image-canvas-brand-fill);
|
||||||
|
color: var(--image-canvas-brand-text-on-fill);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 850;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__generation-submit:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__generate-status {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: #8c6252;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.game-resource-canvas-toolbar-host
|
||||||
|
.image-canvas-editor__floating-toolbar {
|
||||||
|
max-width: 94vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-resource-canvas-toolbar-host .image-canvas-editor__generation-composer {
|
||||||
|
width: 94vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import type { CanvasLayer } from '../../../../../src/components/image-editor/ImageCanvasEditorTypes';
|
||||||
|
import type { QuickEditPanelState } from '../../../../../src/components/image-editor/ImageCanvasEditorTypes';
|
||||||
|
import { createQuickEditPanelDraft } from '../../../../../src/components/image-editor/ImageCanvasGenerationDialogModel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AGC 资源卡快速编辑面板的初始草稿。
|
||||||
|
*
|
||||||
|
* 直接在网页端美术画布的草稿工厂上包一层,保证两个画布的默认模型、比例与尺寸
|
||||||
|
* 取值完全相同;AGC 侧不再自带第二套默认值。
|
||||||
|
*/
|
||||||
|
export function createResourceQuickEditPanelDraft(
|
||||||
|
sourceLayer: CanvasLayer,
|
||||||
|
): QuickEditPanelState {
|
||||||
|
return createQuickEditPanelDraft(sourceLayer, {});
|
||||||
|
}
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
|
import type {
|
||||||
|
CanvasAssetKind,
|
||||||
|
CanvasLayer,
|
||||||
|
CanvasMediaType,
|
||||||
|
} from '../../../../../src/components/image-editor/ImageCanvasEditorTypes';
|
||||||
|
import type { ImageCanvasSelectedToolbarAction } from '../../../../../src/components/image-editor/ImageCanvasSelectedLayerToolbarView';
|
||||||
|
import { canonicalProjectedResourceMediaType } from '../../view/project-development/resourceEditModel';
|
||||||
|
import type { ProjectResource } from '../../view/project-development/resourceProjectionModel';
|
||||||
|
|
||||||
|
/** 快速编辑与改造都走 `derive_local_project_resource`,只接受这三种栅格图片格式。 */
|
||||||
|
const RESOURCE_RASTER_MEDIA_TYPES = new Set([
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/webp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const CANVAS_ASSET_KINDS = new Set<string>([
|
||||||
|
'audio',
|
||||||
|
'spec',
|
||||||
|
'character',
|
||||||
|
'character-animation',
|
||||||
|
'icon',
|
||||||
|
'icon-spritesheet',
|
||||||
|
'icon-spec',
|
||||||
|
'publication-material',
|
||||||
|
'ui-design',
|
||||||
|
'video',
|
||||||
|
'sound-effect',
|
||||||
|
'background-music',
|
||||||
|
'scene',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function resourceCanvasMediaType(
|
||||||
|
resource: Pick<ProjectResource, 'mediaType' | 'category' | 'path'>,
|
||||||
|
): CanvasMediaType | undefined {
|
||||||
|
const mediaType = canonicalProjectedResourceMediaType({
|
||||||
|
mediaType: resource.mediaType,
|
||||||
|
path: resource.path,
|
||||||
|
} as ProjectResource);
|
||||||
|
if (mediaType.startsWith('image/')) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
if (mediaType.startsWith('video/')) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
if (mediaType.startsWith('audio/') || resource.category === 'audio') {
|
||||||
|
return 'audio';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resourceCanvasAssetKind(
|
||||||
|
resource: Pick<ProjectResource, 'subtype'>,
|
||||||
|
): CanvasAssetKind | null {
|
||||||
|
return CANVAS_ASSET_KINDS.has(resource.subtype)
|
||||||
|
? (resource.subtype as CanvasAssetKind)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceRasterImage(
|
||||||
|
resource: Pick<ProjectResource, 'mediaType' | 'category' | 'path'>,
|
||||||
|
) {
|
||||||
|
return RESOURCE_RASTER_MEDIA_TYPES.has(
|
||||||
|
canonicalProjectedResourceMediaType({
|
||||||
|
mediaType: resource.mediaType,
|
||||||
|
path: resource.path,
|
||||||
|
} as ProjectResource),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务产物类型的资源只有先被正规化成正式 manifest 素材,才能作为派生源。
|
||||||
|
*
|
||||||
|
* 与 Rust `normalize_local_project_raster_resource_at` 的门禁对齐:必须是已完成
|
||||||
|
* 任务登记在 artifacts 里的栅格图片产物。
|
||||||
|
*/
|
||||||
|
export function canNormalizeResourceIntoManifestAsset(
|
||||||
|
manifest: GameCreationAppManifest,
|
||||||
|
resource: Pick<
|
||||||
|
ProjectResource,
|
||||||
|
'manifestAssetId' | 'producerTaskId' | 'path' | 'mediaType' | 'category'
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
if (resource.manifestAssetId !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const producerTaskId = resource.producerTaskId?.trim();
|
||||||
|
if (!producerTaskId || !isResourceRasterImage(resource)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return manifest.tasks.some(
|
||||||
|
(task) =>
|
||||||
|
task.id === producerTaskId &&
|
||||||
|
task.status === 'completed' &&
|
||||||
|
task.artifacts.includes(resource.path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveResourceCanvasToolbarActions(
|
||||||
|
manifest: GameCreationAppManifest,
|
||||||
|
resource: ProjectResource,
|
||||||
|
): ReadonlySet<ImageCanvasSelectedToolbarAction> {
|
||||||
|
const supported = new Set<ImageCanvasSelectedToolbarAction>();
|
||||||
|
const mediaType = resourceCanvasMediaType(resource);
|
||||||
|
const canDeriveFromResource =
|
||||||
|
resource.manifestAssetId !== null ||
|
||||||
|
canNormalizeResourceIntoManifestAsset(manifest, resource);
|
||||||
|
if (mediaType === 'image') {
|
||||||
|
if (isResourceRasterImage(resource) && canDeriveFromResource) {
|
||||||
|
supported.add('quick-edit');
|
||||||
|
}
|
||||||
|
if (canDeriveFromResource) {
|
||||||
|
supported.add('redraw');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
resourceCanvasAssetKind(resource) === 'character' &&
|
||||||
|
canDeriveFromResource
|
||||||
|
) {
|
||||||
|
supported.add('character-animation');
|
||||||
|
}
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(mediaType === 'video' || mediaType === 'audio') &&
|
||||||
|
canDeriveFromResource
|
||||||
|
) {
|
||||||
|
supported.add('redraw');
|
||||||
|
}
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把资源卡投影成美术画布 `CanvasLayer`,让工具条、命中测试与几何判定共用同一套语义。
|
||||||
|
*/
|
||||||
|
export function projectResourceToCanvasLayer({
|
||||||
|
resource,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
src = '',
|
||||||
|
}: {
|
||||||
|
resource: ProjectResource;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
src?: string;
|
||||||
|
}): CanvasLayer {
|
||||||
|
return {
|
||||||
|
id: resource.id,
|
||||||
|
resourceId: resource.id,
|
||||||
|
title: resource.label,
|
||||||
|
src,
|
||||||
|
mediaType: resourceCanvasMediaType(resource),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
originalWidth: width,
|
||||||
|
originalHeight: height,
|
||||||
|
zIndex: 0,
|
||||||
|
sourceType: 'uploaded',
|
||||||
|
assetKind: resourceCanvasAssetKind(resource),
|
||||||
|
};
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
|
import type { ProjectResource } from '../../view/project-development/resourceProjectionModel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* C5 卡片边框:一个资源是否正在被「当前版本」使用。
|
||||||
|
*
|
||||||
|
* 当前版本 = manifest `versions[]` 的最后一个版本。版本绑定是资源记录的权威口径,
|
||||||
|
* `slotId` 恒等映射为 `asset:{assetId}`,`resourceId` 指向 manifest 资产 ID。
|
||||||
|
* 版本切换状态接入后,只需把 `manifest` 换成「当前选中的版本」即可复用同一判定。
|
||||||
|
*/
|
||||||
|
export function currentVersionResourceBindingIds(
|
||||||
|
manifest: Pick<GameCreationAppManifest, 'versions'>,
|
||||||
|
): Set<string> {
|
||||||
|
const currentVersion = (manifest.versions ?? []).at(-1);
|
||||||
|
if (!currentVersion) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
return new Set(
|
||||||
|
currentVersion.resourceBindings.flatMap((binding) =>
|
||||||
|
binding.slotId === `asset:${binding.resourceId}`
|
||||||
|
? [binding.resourceId]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceUsedByCurrentVersion(
|
||||||
|
resource: Pick<ProjectResource, 'manifestAssetId'>,
|
||||||
|
currentVersionBindingIds: ReadonlySet<string>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
resource.manifestAssetId !== null &&
|
||||||
|
currentVersionBindingIds.has(resource.manifestAssetId)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import '@genarrative/image-canvas-react/styles.css';
|
import '@genarrative/image-canvas-react/styles.css';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
import './features/resource-canvas/resourceCanvasChrome.css';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|||||||
@@ -6738,7 +6738,7 @@ iframe.preview-frame {
|
|||||||
min-height: var(--resource-card-height);
|
min-height: var(--resource-card-height);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
border: 1px solid #eadbd4;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #4e382f;
|
color: #4e382f;
|
||||||
@@ -6768,6 +6768,14 @@ iframe.preview-frame {
|
|||||||
box-shadow: 0 8px 22px rgb(195 105 62 / 15%);
|
box-shadow: 0 8px 22px rgb(195 105 62 / 15%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* C5 卡片边框:只有被当前版本绑定使用的素材带边框,其余资源卡无边框。 */
|
||||||
|
.game-resource-card.is-current-version {
|
||||||
|
border: 1px solid #d87342;
|
||||||
|
box-shadow:
|
||||||
|
0 6px 18px rgb(96 62 47 / 10%),
|
||||||
|
0 0 0 2px rgb(216 115 66 / 14%);
|
||||||
|
}
|
||||||
|
|
||||||
.game-resource-card.is-relation-version-binding {
|
.game-resource-card.is-relation-version-binding {
|
||||||
border-color: #d87342;
|
border-color: #d87342;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -6851,7 +6859,7 @@ iframe.preview-frame {
|
|||||||
transform-origin: bottom right;
|
transform-origin: bottom right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-resource-card-open {
|
.game-resource-card-select {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -6865,7 +6873,7 @@ iframe.preview-frame {
|
|||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-resource-card-open:focus-visible {
|
.game-resource-card-select:focus-visible {
|
||||||
outline: 3px solid rgb(213 123 81 / 55%);
|
outline: 3px solid rgb(213 123 81 / 55%);
|
||||||
outline-offset: -4px;
|
outline-offset: -4px;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+19
@@ -15,4 +15,23 @@ interface Window {
|
|||||||
) => Promise<() => void>;
|
) => Promise<() => void>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// 资源画布复用网页端美术画布的组件,这条依赖链是明确的跨端耦合:
|
||||||
|
// ImageCanvasSelectedLayerToolbarView → ImageCanvasGenerationModel →(值导入
|
||||||
|
// ApiClientError)→ 主仓 src/services/apiClient.ts → src/services/host-bridge/hostBridge.ts。
|
||||||
|
// hostBridge 读取的全局只在主仓 `src/vite-env.d.ts` 里声明过,AGC 侧必须补同名字段
|
||||||
|
// 才能过 strict 检查。以后往 AGC 引 `src/components/image-editor/` 的组件,都要检查这条链。
|
||||||
|
ReactNativeWebView?: {
|
||||||
|
postMessage?: (message: string) => void;
|
||||||
|
};
|
||||||
|
wx?: {
|
||||||
|
miniProgram?: {
|
||||||
|
navigateTo?: (options: {
|
||||||
|
url: string;
|
||||||
|
success?: (result?: unknown) => void;
|
||||||
|
fail?: (error: { errMsg?: string }) => void;
|
||||||
|
}) => void;
|
||||||
|
postMessage?: (message: unknown) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
WeixinJSBridge?: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,35 +332,35 @@ async function submitChat(value: string) {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同一张资源卡上有两个按钮:打开详情与 V3 新增的 @ 引用按钮,二者的可访问名都包含
|
// 同一张资源卡上有两个按钮:选中资源与 V3 新增的 @ 引用按钮,二者的可访问名都包含
|
||||||
// 资源文件名,因此按文件名正则查询会同时命中两个元素。资源详情按钮的可访问名模板固定为
|
// 资源文件名,因此按文件名正则查询会同时命中两个元素。选中按钮的可访问名模板固定为
|
||||||
// `打开资源详情:<分类标签> <资源文件名>`(分类标签内不含空格),这里按模板做精确匹配。
|
// `选中资源:<分类标签> <资源文件名>`(分类标签内不含空格),这里按模板做精确匹配。
|
||||||
function escapeRegExpLiteral(value: string) {
|
function escapeRegExpLiteral(value: string) {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
function resourceDetailButtonName(label: string) {
|
function resourceSelectButtonName(label: string) {
|
||||||
return new RegExp(`^打开资源详情:\\S+ ${escapeRegExpLiteral(label)}$`);
|
return new RegExp(`^选中资源:\\S+ ${escapeRegExpLiteral(label)}$`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResourceDetailButton(label: string) {
|
function getResourceSelectButton(label: string) {
|
||||||
return screen.getByRole('button', { name: resourceDetailButtonName(label) });
|
return screen.getByRole('button', { name: resourceSelectButtonName(label) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function findResourceDetailButton(
|
function findResourceSelectButton(
|
||||||
label: string,
|
label: string,
|
||||||
options?: { timeout?: number; interval?: number },
|
options?: { timeout?: number; interval?: number },
|
||||||
) {
|
) {
|
||||||
return screen.findByRole(
|
return screen.findByRole(
|
||||||
'button',
|
'button',
|
||||||
{ name: resourceDetailButtonName(label) },
|
{ name: resourceSelectButtonName(label) },
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryResourceDetailButton(label: string) {
|
function queryResourceSelectButton(label: string) {
|
||||||
return screen.queryByRole('button', {
|
return screen.queryByRole('button', {
|
||||||
name: resourceDetailButtonName(label),
|
name: resourceSelectButtonName(label),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,10 +1293,10 @@ export {
|
|||||||
describe,
|
describe,
|
||||||
emptyProjectPolicy,
|
emptyProjectPolicy,
|
||||||
expect,
|
expect,
|
||||||
findResourceDetailButton,
|
findResourceSelectButton,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||||||
getResourceDetailButton,
|
getResourceSelectButton,
|
||||||
it,
|
it,
|
||||||
mockRoleAgentReply,
|
mockRoleAgentReply,
|
||||||
nativeClipboardMock,
|
nativeClipboardMock,
|
||||||
@@ -1304,7 +1304,7 @@ export {
|
|||||||
pickProjectFromLauncher,
|
pickProjectFromLauncher,
|
||||||
ProjectDevelopmentView,
|
ProjectDevelopmentView,
|
||||||
projectSupervisorResponseStream,
|
projectSupervisorResponseStream,
|
||||||
queryResourceDetailButton,
|
queryResourceSelectButton,
|
||||||
React,
|
React,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
render,
|
render,
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
createGameCreationAppManifest,
|
createGameCreationAppManifest,
|
||||||
createProjectSupervisorRuntimeHarness,
|
createProjectSupervisorRuntimeHarness,
|
||||||
expect,
|
expect,
|
||||||
findResourceDetailButton,
|
findResourceSelectButton,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
getResourceDetailButton,
|
getResourceSelectButton,
|
||||||
it,
|
it,
|
||||||
nativeClipboardMock,
|
nativeClipboardMock,
|
||||||
pickProjectFromLauncher,
|
pickProjectFromLauncher,
|
||||||
queryResourceDetailButton,
|
queryResourceSelectButton,
|
||||||
React,
|
React,
|
||||||
render,
|
render,
|
||||||
renderAppAt,
|
renderAppAt,
|
||||||
@@ -316,12 +316,12 @@ export function registerClientHomeTests() {
|
|||||||
pickProjectFromLauncher(projectPath);
|
pickProjectFromLauncher(projectPath);
|
||||||
const runButton = await screen.findByRole('tab', { name: '运行' });
|
const runButton = await screen.findByRole('tab', { name: '运行' });
|
||||||
expect(runButton.getAttribute('data-unavailable')).toBe('true');
|
expect(runButton.getAttribute('data-unavailable')).toBe('true');
|
||||||
expect(queryResourceDetailButton('live-hero.png')).toBeNull();
|
expect(queryResourceSelectButton('live-hero.png')).toBeNull();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '同步最新 manifest' }));
|
fireEvent.click(screen.getByRole('button', { name: '同步最新 manifest' }));
|
||||||
|
|
||||||
await openResourceBookCategory('美术资源');
|
await openResourceBookCategory('美术资源');
|
||||||
expect(await findResourceDetailButton('live-hero.png')).not.toBeNull();
|
expect(await findResourceSelectButton('live-hero.png')).not.toBeNull();
|
||||||
await openResourceBookCategory('项目版本');
|
await openResourceBookCategory('项目版本');
|
||||||
expect(
|
expect(
|
||||||
await screen.findByRole('button', { name: /版本 1/ }),
|
await screen.findByRole('button', { name: /版本 1/ }),
|
||||||
@@ -462,7 +462,7 @@ export function registerClientHomeTests() {
|
|||||||
pickProjectFromLauncher(projectPath);
|
pickProjectFromLauncher(projectPath);
|
||||||
const runButton = await screen.findByRole('tab', { name: '运行' });
|
const runButton = await screen.findByRole('tab', { name: '运行' });
|
||||||
expect(runButton.getAttribute('data-unavailable')).toBe('true');
|
expect(runButton.getAttribute('data-unavailable')).toBe('true');
|
||||||
expect(queryResourceDetailButton('runtime-live-hero.png')).toBeNull();
|
expect(queryResourceSelectButton('runtime-live-hero.png')).toBeNull();
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(runtimeHarness.listen).toHaveBeenCalledWith(
|
expect(runtimeHarness.listen).toHaveBeenCalledWith(
|
||||||
'game-creator-manifest-invalidated',
|
'game-creator-manifest-invalidated',
|
||||||
@@ -488,7 +488,7 @@ export function registerClientHomeTests() {
|
|||||||
|
|
||||||
await openResourceBookCategory('美术资源');
|
await openResourceBookCategory('美术资源');
|
||||||
expect(
|
expect(
|
||||||
await findResourceDetailButton('runtime-live-hero.png', {
|
await findResourceSelectButton('runtime-live-hero.png', {
|
||||||
timeout: 5_000,
|
timeout: 5_000,
|
||||||
}),
|
}),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
@@ -663,14 +663,14 @@ export function registerClientHomeTests() {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '项目组' }));
|
fireEvent.click(screen.getByRole('button', { name: '项目组' }));
|
||||||
pickProjectFromLauncher(secondProjectPath);
|
pickProjectFromLauncher(secondProjectPath);
|
||||||
await openResourceBookCategory('美术资源');
|
await openResourceBookCategory('美术资源');
|
||||||
expect(await findResourceDetailButton('second.png')).not.toBeNull();
|
expect(await findResourceSelectButton('second.png')).not.toBeNull();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
resolveStaleRefresh(staleFirstManifest);
|
resolveStaleRefresh(staleFirstManifest);
|
||||||
await staleRefresh;
|
await staleRefresh;
|
||||||
});
|
});
|
||||||
expect(queryResourceDetailButton('stale-first.png')).toBeNull();
|
expect(queryResourceSelectButton('stale-first.png')).toBeNull();
|
||||||
expect(getResourceDetailButton('second.png')).not.toBeNull();
|
expect(getResourceSelectButton('second.png')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts from the client home and opens a project in the same window', async () => {
|
it('starts from the client home and opens a project in the same window', async () => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,13 @@ import ProjectDevelopmentView from '../src/view/project-development';
|
|||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
createGameCreationAppManifest,
|
createGameCreationAppManifest,
|
||||||
findResourceDetailButton,
|
findResourceSelectButton,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
render,
|
render,
|
||||||
screen,
|
screen,
|
||||||
|
setComposerText,
|
||||||
waitFor,
|
waitFor,
|
||||||
|
within,
|
||||||
} from './appSurface/harness';
|
} from './appSurface/harness';
|
||||||
|
|
||||||
const projectPath = '/tmp/live-canvas-integration';
|
const projectPath = '/tmp/live-canvas-integration';
|
||||||
@@ -473,39 +475,36 @@ describe('project resource live canvas integration', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it('derives a document non-destructively and retries with the original operation identity', async () => {
|
it('derives a new art asset non-destructively from the quick edit panel and reuses the original operation identity', async () => {
|
||||||
const { deriveCalls } = installTauri({ failFirstDerive: true });
|
const { deriveCalls } = installTauri({ failFirstDerive: true });
|
||||||
render(<DerivedWorkbench />);
|
render(<DerivedWorkbench />);
|
||||||
|
|
||||||
fireEvent.click(await findResourceDetailButton('rules.md'));
|
fireEvent.click(screen.getByRole('button', { name: '打开美术资源' }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: '编辑资源' }));
|
fireEvent.click(await findResourceSelectButton('source-art.png'));
|
||||||
expect(await screen.findByText('编辑现有资源')).not.toBeNull();
|
const toolbar = await screen.findByRole('toolbar', {
|
||||||
fireEvent.change(screen.getByLabelText('编辑提示词'), {
|
name: '图片工具栏',
|
||||||
target: { value: '把角色头发设定改为红色' },
|
|
||||||
});
|
});
|
||||||
fireEvent.click(screen.getByRole('button', { name: '生成派生资源' }));
|
fireEvent.click(within(toolbar).getByRole('button', { name: '快速编辑' }));
|
||||||
|
const panel = await screen.findByRole('dialog', {
|
||||||
|
name: '快速编辑图片',
|
||||||
|
});
|
||||||
|
await setComposerText(
|
||||||
|
within(panel).getByLabelText('快速编辑提示词'),
|
||||||
|
'把角色头发设定改为红色',
|
||||||
|
);
|
||||||
|
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||||
expect(await screen.findByRole('alert')).not.toBeNull();
|
expect(await screen.findByRole('alert')).not.toBeNull();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '使用原请求重试' }));
|
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||||
|
|
||||||
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
||||||
expect(deriveCalls[0]?.operationId).toBe(deriveCalls[1]?.operationId);
|
expect(deriveCalls[0]?.operationId).toBe(deriveCalls[1]?.operationId);
|
||||||
expect(deriveCalls[0]?.idempotencyKey).toBe(deriveCalls[1]?.idempotencyKey);
|
expect(deriveCalls[0]?.idempotencyKey).toBe(deriveCalls[1]?.idempotencyKey);
|
||||||
|
expect(deriveCalls[0]?.editKind).toBe('image-reference');
|
||||||
|
expect(deriveCalls[0]?.generationMode).toBe('derive');
|
||||||
expect(deriveCalls[0]).not.toHaveProperty('accessToken');
|
expect(deriveCalls[0]).not.toHaveProperty('accessToken');
|
||||||
expect(deriveCalls[0]).not.toHaveProperty('apiKey');
|
expect(deriveCalls[0]).not.toHaveProperty('apiKey');
|
||||||
expect(deriveCalls[1]).not.toHaveProperty('accessToken');
|
expect(deriveCalls[1]).not.toHaveProperty('accessToken');
|
||||||
expect(deriveCalls[1]).not.toHaveProperty('apiKey');
|
expect(deriveCalls[1]).not.toHaveProperty('apiKey');
|
||||||
const operationId = String(deriveCalls[1]?.operationId);
|
|
||||||
expect(
|
|
||||||
await screen.findByRole('dialog', {
|
|
||||||
name: new RegExp(operationId, 'u'),
|
|
||||||
}),
|
|
||||||
).not.toBeNull();
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '收起资源' }));
|
|
||||||
expect(
|
|
||||||
await screen.findByRole('button', {
|
|
||||||
name: '打开资源详情:设计文档 rules.md',
|
|
||||||
}),
|
|
||||||
).not.toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lists an unfinished edit and resumes it using only the private-ledger operation id', async () => {
|
it('lists an unfinished edit and resumes it using only the private-ledger operation id', async () => {
|
||||||
@@ -529,11 +528,14 @@ describe('project resource live canvas integration', () => {
|
|||||||
expect(resumeCalls[0]).not.toHaveProperty('prompt');
|
expect(resumeCalls[0]).not.toHaveProperty('prompt');
|
||||||
expect(resumeCalls[0]).not.toHaveProperty('endpoint');
|
expect(resumeCalls[0]).not.toHaveProperty('endpoint');
|
||||||
expect(resumeCalls[0]).not.toHaveProperty('idempotencyKey');
|
expect(resumeCalls[0]).not.toHaveProperty('idempotencyKey');
|
||||||
|
// 恢复完成后新素材被选中;不再有"资源详情"面板。
|
||||||
expect(
|
expect(
|
||||||
await screen.findByRole('dialog', {
|
(
|
||||||
name: /recovered-rules\.md/,
|
await screen.findByRole('button', {
|
||||||
}),
|
name: /选中资源:\S+ \S*recovered-rules\.md$/,
|
||||||
).not.toBeNull();
|
})
|
||||||
|
).getAttribute('aria-pressed'),
|
||||||
|
).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('显式确认旧 Key-bound 服务后只用原 operation 继续恢复', async () => {
|
it('显式确认旧 Key-bound 服务后只用原 operation 继续恢复', async () => {
|
||||||
@@ -683,8 +685,6 @@ describe('project resource live canvas integration', () => {
|
|||||||
expect(resumeCalls[0]?.operationId).toBe(resumableOperationId);
|
expect(resumeCalls[0]?.operationId).toBe(resumableOperationId);
|
||||||
expect(resumeCalls[0]?.operationId).not.toBe(reconciliationOperationId);
|
expect(resumeCalls[0]?.operationId).not.toBe(reconciliationOperationId);
|
||||||
|
|
||||||
fireEvent.click(await screen.findByRole('button', { name: '收起资源' }));
|
|
||||||
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
await screen.findByRole('button', {
|
await screen.findByRole('button', {
|
||||||
name: '管理未完成编辑 (2)',
|
name: '管理未完成编辑 (2)',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
WandSparkles,
|
WandSparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||||
@@ -19,7 +19,28 @@ import {
|
|||||||
isQuickEditSupportedLayer,
|
isQuickEditSupportedLayer,
|
||||||
} from './ImageCanvasGenerationModel';
|
} from './ImageCanvasGenerationModel';
|
||||||
|
|
||||||
|
/** 选中工具条上的动作标识,供宿主声明自己实际可用的动作集合。 */
|
||||||
|
export type ImageCanvasSelectedToolbarAction =
|
||||||
|
| 'quick-edit'
|
||||||
|
| 'redraw'
|
||||||
|
| 'crop-expand'
|
||||||
|
| 'remove-background'
|
||||||
|
| 'perfect-pixel'
|
||||||
|
| 'split-icon-spritesheet'
|
||||||
|
| 'extract-ui-design'
|
||||||
|
| 'character-animation'
|
||||||
|
| 'download';
|
||||||
|
|
||||||
type ImageCanvasSelectedLayerToolbarViewProps = {
|
type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||||
|
/**
|
||||||
|
* 宿主显式声明的可用动作集合。
|
||||||
|
*
|
||||||
|
* 传 `null`(默认)时完全沿用美术画布内置判定;传入集合时以集合为准,
|
||||||
|
* 让没有对应后端链路的画布宿主不必把「点了没反应」的按钮渲染出来。
|
||||||
|
*/
|
||||||
|
supportedActions?: ReadonlySet<ImageCanvasSelectedToolbarAction> | null;
|
||||||
|
/** 宿主在工具条上追加的动作节点,渲染在下载按钮之前。 */
|
||||||
|
extraActions?: ReactNode;
|
||||||
selectedLayer: CanvasLayer | null;
|
selectedLayer: CanvasLayer | null;
|
||||||
selectedToolbarStyle: CSSProperties | null;
|
selectedToolbarStyle: CSSProperties | null;
|
||||||
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
|
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
|
||||||
@@ -38,6 +59,8 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ImageCanvasSelectedLayerToolbarView({
|
export function ImageCanvasSelectedLayerToolbarView({
|
||||||
|
supportedActions = null,
|
||||||
|
extraActions,
|
||||||
selectedLayer,
|
selectedLayer,
|
||||||
selectedToolbarStyle,
|
selectedToolbarStyle,
|
||||||
onOpenQuickEditPanel,
|
onOpenQuickEditPanel,
|
||||||
@@ -57,7 +80,20 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
if (!selectedLayer || !selectedToolbarStyle) {
|
if (!selectedLayer || !selectedToolbarStyle) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const canRedraw = canOpenRedrawPanel(selectedLayer);
|
const isActionSupported = (
|
||||||
|
action: ImageCanvasSelectedToolbarAction,
|
||||||
|
builtInAvailability: boolean,
|
||||||
|
) => (supportedActions ? supportedActions.has(action) : builtInAvailability);
|
||||||
|
const canRedraw = isActionSupported(
|
||||||
|
'redraw',
|
||||||
|
canOpenRedrawPanel(selectedLayer),
|
||||||
|
);
|
||||||
|
const extraActionDivider = extraActions ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="image-canvas-editor__floating-toolbar-divider"
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
if (selectedLayer.mediaType === 'audio') {
|
if (selectedLayer.mediaType === 'audio') {
|
||||||
return (
|
return (
|
||||||
@@ -79,12 +115,16 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
<span>改造</span>
|
<span>改造</span>
|
||||||
</CanvasChromeButton>
|
</CanvasChromeButton>
|
||||||
) : null}
|
) : null}
|
||||||
<EditorIconButton
|
{extraActionDivider}
|
||||||
label="下载按钮"
|
{extraActions}
|
||||||
title="下载按钮"
|
{isActionSupported('download', true) ? (
|
||||||
icon={Download}
|
<EditorIconButton
|
||||||
onClick={() => onDownloadLayer(selectedLayer)}
|
label="下载按钮"
|
||||||
/>
|
title="下载按钮"
|
||||||
|
icon={Download}
|
||||||
|
onClick={() => onDownloadLayer(selectedLayer)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,6 +133,14 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
selectedLayer.mediaType !== 'video' &&
|
selectedLayer.mediaType !== 'video' &&
|
||||||
selectedLayer.mediaType !== 'image-sequence' &&
|
selectedLayer.mediaType !== 'image-sequence' &&
|
||||||
selectedLayer.assetKind !== 'character-animation';
|
selectedLayer.assetKind !== 'character-animation';
|
||||||
|
const showQuickEdit = isActionSupported(
|
||||||
|
'quick-edit',
|
||||||
|
isQuickEditSupportedLayer(selectedLayer),
|
||||||
|
);
|
||||||
|
const showCharacterAnimation = isActionSupported(
|
||||||
|
'character-animation',
|
||||||
|
selectedLayer.assetKind === 'character',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -102,7 +150,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
aria-label="图片工具栏"
|
aria-label="图片工具栏"
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
{isQuickEditSupportedLayer(selectedLayer) ? (
|
{showQuickEdit ? (
|
||||||
<>
|
<>
|
||||||
<CanvasChromeButton
|
<CanvasChromeButton
|
||||||
className="image-canvas-editor__floating-toolbar-text-button"
|
className="image-canvas-editor__floating-toolbar-text-button"
|
||||||
@@ -119,74 +167,77 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{canRasterEdit ? (
|
{canRasterEdit && isActionSupported('crop-expand', true) ? (
|
||||||
<>
|
<EditorIconButton
|
||||||
<EditorIconButton
|
label="裁扩按钮"
|
||||||
label="裁扩按钮"
|
title="裁扩按钮"
|
||||||
title="裁扩按钮"
|
icon={Crop}
|
||||||
icon={Crop}
|
onClick={() => onOpenCropExpandPanel(selectedLayer)}
|
||||||
onClick={() => onOpenCropExpandPanel(selectedLayer)}
|
/>
|
||||||
/>
|
|
||||||
<EditorIconButton
|
|
||||||
label="去除背景按钮"
|
|
||||||
title="去除背景按钮"
|
|
||||||
icon={ImageOff}
|
|
||||||
onClick={() => onRemoveBackground(selectedLayer)}
|
|
||||||
/>
|
|
||||||
<CanvasChromeButton
|
|
||||||
className="image-canvas-editor__floating-toolbar-text-button"
|
|
||||||
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
|
|
||||||
// 拆分图集按钮同时显示该文案,两个控件会撞同一个无障碍名称。
|
|
||||||
label={
|
|
||||||
isPersistingAssetKind
|
|
||||||
? '完美像素等待素材类型保存'
|
|
||||||
: isPerfectPixelProcessing
|
|
||||||
? '完美像素处理中'
|
|
||||||
: isPerfectPixelPendingConfirmation
|
|
||||||
? '完美像素结果待确认'
|
|
||||||
: '完美像素'
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
isPersistingAssetKind
|
|
||||||
? '完美像素等待素材类型保存'
|
|
||||||
: isPerfectPixelProcessing
|
|
||||||
? '完美像素处理中'
|
|
||||||
: isPerfectPixelPendingConfirmation
|
|
||||||
? '结果待确认,双击原占位继续核对或重试'
|
|
||||||
: '自动识别并规整像素网格'
|
|
||||||
}
|
|
||||||
icon={
|
|
||||||
isPersistingAssetKind || isPerfectPixelProcessing ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Grid2X2 className="h-4 w-4" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
|
|
||||||
// sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
|
|
||||||
// resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
|
|
||||||
// 与相邻的拆分图集按钮保持同一套门禁。
|
|
||||||
disabled={
|
|
||||||
isPersistingAssetKind ||
|
|
||||||
isPerfectPixelProcessing ||
|
|
||||||
isPerfectPixelPendingConfirmation
|
|
||||||
}
|
|
||||||
aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
|
|
||||||
onClick={() => onPerfectPixel(selectedLayer)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{isPersistingAssetKind
|
|
||||||
? '保存中'
|
|
||||||
: isPerfectPixelProcessing
|
|
||||||
? '处理中'
|
|
||||||
: isPerfectPixelPendingConfirmation
|
|
||||||
? '待确认'
|
|
||||||
: '完美像素'}
|
|
||||||
</span>
|
|
||||||
</CanvasChromeButton>
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
{selectedLayer.assetKind === 'icon-spritesheet' ? (
|
{canRasterEdit && isActionSupported('remove-background', true) ? (
|
||||||
|
<EditorIconButton
|
||||||
|
label="去除背景按钮"
|
||||||
|
title="去除背景按钮"
|
||||||
|
icon={ImageOff}
|
||||||
|
onClick={() => onRemoveBackground(selectedLayer)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{canRasterEdit && isActionSupported('perfect-pixel', true) ? (
|
||||||
|
<CanvasChromeButton
|
||||||
|
className="image-canvas-editor__floating-toolbar-text-button"
|
||||||
|
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
|
||||||
|
// 拆分图集按钮同时显示该文案,两个控件会撞同一个无障碍名称。
|
||||||
|
label={
|
||||||
|
isPersistingAssetKind
|
||||||
|
? '完美像素等待素材类型保存'
|
||||||
|
: isPerfectPixelProcessing
|
||||||
|
? '完美像素处理中'
|
||||||
|
: isPerfectPixelPendingConfirmation
|
||||||
|
? '完美像素结果待确认'
|
||||||
|
: '完美像素'
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
isPersistingAssetKind
|
||||||
|
? '完美像素等待素材类型保存'
|
||||||
|
: isPerfectPixelProcessing
|
||||||
|
? '完美像素处理中'
|
||||||
|
: isPerfectPixelPendingConfirmation
|
||||||
|
? '结果待确认,双击原占位继续核对或重试'
|
||||||
|
: '自动识别并规整像素网格'
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
isPersistingAssetKind || isPerfectPixelProcessing ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Grid2X2 className="h-4 w-4" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
|
||||||
|
// sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
|
||||||
|
// resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
|
||||||
|
// 与相邻的拆分图集按钮保持同一套门禁。
|
||||||
|
disabled={
|
||||||
|
isPersistingAssetKind ||
|
||||||
|
isPerfectPixelProcessing ||
|
||||||
|
isPerfectPixelPendingConfirmation
|
||||||
|
}
|
||||||
|
aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
|
||||||
|
onClick={() => onPerfectPixel(selectedLayer)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{isPersistingAssetKind
|
||||||
|
? '保存中'
|
||||||
|
: isPerfectPixelProcessing
|
||||||
|
? '处理中'
|
||||||
|
: isPerfectPixelPendingConfirmation
|
||||||
|
? '待确认'
|
||||||
|
: '完美像素'}
|
||||||
|
</span>
|
||||||
|
</CanvasChromeButton>
|
||||||
|
) : null}
|
||||||
|
{selectedLayer.assetKind === 'icon-spritesheet' &&
|
||||||
|
isActionSupported('split-icon-spritesheet', true) ? (
|
||||||
<CanvasChromeButton
|
<CanvasChromeButton
|
||||||
className="image-canvas-editor__floating-toolbar-text-button"
|
className="image-canvas-editor__floating-toolbar-text-button"
|
||||||
label={
|
label={
|
||||||
@@ -223,7 +274,8 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
</span>
|
</span>
|
||||||
</CanvasChromeButton>
|
</CanvasChromeButton>
|
||||||
) : null}
|
) : null}
|
||||||
{selectedLayer.assetKind === 'ui-design' ? (
|
{selectedLayer.assetKind === 'ui-design' &&
|
||||||
|
isActionSupported('extract-ui-design', true) ? (
|
||||||
<CanvasChromeButton
|
<CanvasChromeButton
|
||||||
className="image-canvas-editor__floating-toolbar-text-button"
|
className="image-canvas-editor__floating-toolbar-text-button"
|
||||||
label="提取素材"
|
label="提取素材"
|
||||||
@@ -234,7 +286,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
<span>提取素材</span>
|
<span>提取素材</span>
|
||||||
</CanvasChromeButton>
|
</CanvasChromeButton>
|
||||||
) : null}
|
) : null}
|
||||||
{selectedLayer.assetKind === 'character' ? (
|
{showCharacterAnimation ? (
|
||||||
<CanvasChromeButton
|
<CanvasChromeButton
|
||||||
className="image-canvas-editor__floating-toolbar-text-button"
|
className="image-canvas-editor__floating-toolbar-text-button"
|
||||||
label="生成动画"
|
label="生成动画"
|
||||||
@@ -262,12 +314,16 @@ export function ImageCanvasSelectedLayerToolbarView({
|
|||||||
</CanvasChromeButton>
|
</CanvasChromeButton>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<EditorIconButton
|
{extraActionDivider}
|
||||||
label="下载按钮"
|
{extraActions}
|
||||||
title="下载按钮"
|
{isActionSupported('download', true) ? (
|
||||||
icon={Download}
|
<EditorIconButton
|
||||||
onClick={() => onDownloadLayer(selectedLayer)}
|
label="下载按钮"
|
||||||
/>
|
title="下载按钮"
|
||||||
|
icon={Download}
|
||||||
|
onClick={() => onDownloadLayer(selectedLayer)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ export async function navigateWechatMiniProgramPage(
|
|||||||
success() {
|
success() {
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
fail(_error) {
|
fail(_error: unknown) {
|
||||||
console.error('[host-bridge] wechat mini program navigation failed');
|
console.error('[host-bridge] wechat mini program navigation failed');
|
||||||
reject(new Error(errorMessage));
|
reject(new Error(errorMessage));
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user