资源画布交互 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',
|
||||
'check_ui_editor_font_glyph_coverage',
|
||||
'create_ui_design_resource',
|
||||
'normalize_local_project_raster_resource',
|
||||
'open_game_creator_launcher_window',
|
||||
'open_game_creator_workspace_window',
|
||||
'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 './styles.css';
|
||||
import './features/resource-canvas/resourceCanvasChrome.css';
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
@@ -6738,7 +6738,7 @@ iframe.preview-frame {
|
||||
min-height: var(--resource-card-height);
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
border: 1px solid #eadbd4;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #4e382f;
|
||||
@@ -6768,6 +6768,14 @@ iframe.preview-frame {
|
||||
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 {
|
||||
border-color: #d87342;
|
||||
box-shadow:
|
||||
@@ -6851,7 +6859,7 @@ iframe.preview-frame {
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.game-resource-card-open {
|
||||
.game-resource-card-select {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
@@ -6865,7 +6873,7 @@ iframe.preview-frame {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.game-resource-card-open:focus-visible {
|
||||
.game-resource-card-select:focus-visible {
|
||||
outline: 3px solid rgb(213 123 81 / 55%);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+19
@@ -15,4 +15,23 @@ interface Window {
|
||||
) => 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: '发送' }));
|
||||
}
|
||||
|
||||
// 同一张资源卡上有两个按钮:打开详情与 V3 新增的 @ 引用按钮,二者的可访问名都包含
|
||||
// 资源文件名,因此按文件名正则查询会同时命中两个元素。资源详情按钮的可访问名模板固定为
|
||||
// `打开资源详情:<分类标签> <资源文件名>`(分类标签内不含空格),这里按模板做精确匹配。
|
||||
// 同一张资源卡上有两个按钮:选中资源与 V3 新增的 @ 引用按钮,二者的可访问名都包含
|
||||
// 资源文件名,因此按文件名正则查询会同时命中两个元素。选中按钮的可访问名模板固定为
|
||||
// `选中资源:<分类标签> <资源文件名>`(分类标签内不含空格),这里按模板做精确匹配。
|
||||
function escapeRegExpLiteral(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function resourceDetailButtonName(label: string) {
|
||||
return new RegExp(`^打开资源详情:\\S+ ${escapeRegExpLiteral(label)}$`);
|
||||
function resourceSelectButtonName(label: string) {
|
||||
return new RegExp(`^选中资源:\\S+ ${escapeRegExpLiteral(label)}$`);
|
||||
}
|
||||
|
||||
function getResourceDetailButton(label: string) {
|
||||
return screen.getByRole('button', { name: resourceDetailButtonName(label) });
|
||||
function getResourceSelectButton(label: string) {
|
||||
return screen.getByRole('button', { name: resourceSelectButtonName(label) });
|
||||
}
|
||||
|
||||
function findResourceDetailButton(
|
||||
function findResourceSelectButton(
|
||||
label: string,
|
||||
options?: { timeout?: number; interval?: number },
|
||||
) {
|
||||
return screen.findByRole(
|
||||
'button',
|
||||
{ name: resourceDetailButtonName(label) },
|
||||
{ name: resourceSelectButtonName(label) },
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
function queryResourceDetailButton(label: string) {
|
||||
function queryResourceSelectButton(label: string) {
|
||||
return screen.queryByRole('button', {
|
||||
name: resourceDetailButtonName(label),
|
||||
name: resourceSelectButtonName(label),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1293,10 +1293,10 @@ export {
|
||||
describe,
|
||||
emptyProjectPolicy,
|
||||
expect,
|
||||
findResourceDetailButton,
|
||||
findResourceSelectButton,
|
||||
fireEvent,
|
||||
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||||
getResourceDetailButton,
|
||||
getResourceSelectButton,
|
||||
it,
|
||||
mockRoleAgentReply,
|
||||
nativeClipboardMock,
|
||||
@@ -1304,7 +1304,7 @@ export {
|
||||
pickProjectFromLauncher,
|
||||
ProjectDevelopmentView,
|
||||
projectSupervisorResponseStream,
|
||||
queryResourceDetailButton,
|
||||
queryResourceSelectButton,
|
||||
React,
|
||||
readFileSync,
|
||||
render,
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
createGameCreationAppManifest,
|
||||
createProjectSupervisorRuntimeHarness,
|
||||
expect,
|
||||
findResourceDetailButton,
|
||||
findResourceSelectButton,
|
||||
fireEvent,
|
||||
getResourceDetailButton,
|
||||
getResourceSelectButton,
|
||||
it,
|
||||
nativeClipboardMock,
|
||||
pickProjectFromLauncher,
|
||||
queryResourceDetailButton,
|
||||
queryResourceSelectButton,
|
||||
React,
|
||||
render,
|
||||
renderAppAt,
|
||||
@@ -316,12 +316,12 @@ export function registerClientHomeTests() {
|
||||
pickProjectFromLauncher(projectPath);
|
||||
const runButton = await screen.findByRole('tab', { name: '运行' });
|
||||
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' }));
|
||||
|
||||
await openResourceBookCategory('美术资源');
|
||||
expect(await findResourceDetailButton('live-hero.png')).not.toBeNull();
|
||||
expect(await findResourceSelectButton('live-hero.png')).not.toBeNull();
|
||||
await openResourceBookCategory('项目版本');
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /版本 1/ }),
|
||||
@@ -462,7 +462,7 @@ export function registerClientHomeTests() {
|
||||
pickProjectFromLauncher(projectPath);
|
||||
const runButton = await screen.findByRole('tab', { name: '运行' });
|
||||
expect(runButton.getAttribute('data-unavailable')).toBe('true');
|
||||
expect(queryResourceDetailButton('runtime-live-hero.png')).toBeNull();
|
||||
expect(queryResourceSelectButton('runtime-live-hero.png')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(runtimeHarness.listen).toHaveBeenCalledWith(
|
||||
'game-creator-manifest-invalidated',
|
||||
@@ -488,7 +488,7 @@ export function registerClientHomeTests() {
|
||||
|
||||
await openResourceBookCategory('美术资源');
|
||||
expect(
|
||||
await findResourceDetailButton('runtime-live-hero.png', {
|
||||
await findResourceSelectButton('runtime-live-hero.png', {
|
||||
timeout: 5_000,
|
||||
}),
|
||||
).not.toBeNull();
|
||||
@@ -663,14 +663,14 @@ export function registerClientHomeTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '项目组' }));
|
||||
pickProjectFromLauncher(secondProjectPath);
|
||||
await openResourceBookCategory('美术资源');
|
||||
expect(await findResourceDetailButton('second.png')).not.toBeNull();
|
||||
expect(await findResourceSelectButton('second.png')).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
resolveStaleRefresh(staleFirstManifest);
|
||||
await staleRefresh;
|
||||
});
|
||||
expect(queryResourceDetailButton('stale-first.png')).toBeNull();
|
||||
expect(getResourceDetailButton('second.png')).not.toBeNull();
|
||||
expect(queryResourceSelectButton('stale-first.png')).toBeNull();
|
||||
expect(getResourceSelectButton('second.png')).not.toBeNull();
|
||||
});
|
||||
|
||||
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 {
|
||||
act,
|
||||
createGameCreationAppManifest,
|
||||
findResourceDetailButton,
|
||||
findResourceSelectButton,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
setComposerText,
|
||||
waitFor,
|
||||
within,
|
||||
} from './appSurface/harness';
|
||||
|
||||
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 });
|
||||
render(<DerivedWorkbench />);
|
||||
|
||||
fireEvent.click(await findResourceDetailButton('rules.md'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '编辑资源' }));
|
||||
expect(await screen.findByText('编辑现有资源')).not.toBeNull();
|
||||
fireEvent.change(screen.getByLabelText('编辑提示词'), {
|
||||
target: { value: '把角色头发设定改为红色' },
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开美术资源' }));
|
||||
fireEvent.click(await findResourceSelectButton('source-art.png'));
|
||||
const toolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
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();
|
||||
fireEvent.click(screen.getByRole('button', { name: '使用原请求重试' }));
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
||||
expect(deriveCalls[0]?.operationId).toBe(deriveCalls[1]?.operationId);
|
||||
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('apiKey');
|
||||
expect(deriveCalls[1]).not.toHaveProperty('accessToken');
|
||||
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 () => {
|
||||
@@ -529,11 +528,14 @@ describe('project resource live canvas integration', () => {
|
||||
expect(resumeCalls[0]).not.toHaveProperty('prompt');
|
||||
expect(resumeCalls[0]).not.toHaveProperty('endpoint');
|
||||
expect(resumeCalls[0]).not.toHaveProperty('idempotencyKey');
|
||||
// 恢复完成后新素材被选中;不再有"资源详情"面板。
|
||||
expect(
|
||||
await screen.findByRole('dialog', {
|
||||
name: /recovered-rules\.md/,
|
||||
}),
|
||||
).not.toBeNull();
|
||||
(
|
||||
await screen.findByRole('button', {
|
||||
name: /选中资源:\S+ \S*recovered-rules\.md$/,
|
||||
})
|
||||
).getAttribute('aria-pressed'),
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
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).not.toBe(reconciliationOperationId);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '收起资源' }));
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', {
|
||||
name: '管理未完成编辑 (2)',
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Sparkles,
|
||||
WandSparkles,
|
||||
} from 'lucide-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
@@ -19,7 +19,28 @@ import {
|
||||
isQuickEditSupportedLayer,
|
||||
} 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 = {
|
||||
/**
|
||||
* 宿主显式声明的可用动作集合。
|
||||
*
|
||||
* 传 `null`(默认)时完全沿用美术画布内置判定;传入集合时以集合为准,
|
||||
* 让没有对应后端链路的画布宿主不必把「点了没反应」的按钮渲染出来。
|
||||
*/
|
||||
supportedActions?: ReadonlySet<ImageCanvasSelectedToolbarAction> | null;
|
||||
/** 宿主在工具条上追加的动作节点,渲染在下载按钮之前。 */
|
||||
extraActions?: ReactNode;
|
||||
selectedLayer: CanvasLayer | null;
|
||||
selectedToolbarStyle: CSSProperties | null;
|
||||
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
|
||||
@@ -38,6 +59,8 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||
};
|
||||
|
||||
export function ImageCanvasSelectedLayerToolbarView({
|
||||
supportedActions = null,
|
||||
extraActions,
|
||||
selectedLayer,
|
||||
selectedToolbarStyle,
|
||||
onOpenQuickEditPanel,
|
||||
@@ -57,7 +80,20 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
if (!selectedLayer || !selectedToolbarStyle) {
|
||||
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') {
|
||||
return (
|
||||
@@ -79,12 +115,16 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
<span>改造</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
{extraActionDivider}
|
||||
{extraActions}
|
||||
{isActionSupported('download', true) ? (
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -93,6 +133,14 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
selectedLayer.mediaType !== 'video' &&
|
||||
selectedLayer.mediaType !== 'image-sequence' &&
|
||||
selectedLayer.assetKind !== 'character-animation';
|
||||
const showQuickEdit = isActionSupported(
|
||||
'quick-edit',
|
||||
isQuickEditSupportedLayer(selectedLayer),
|
||||
);
|
||||
const showCharacterAnimation = isActionSupported(
|
||||
'character-animation',
|
||||
selectedLayer.assetKind === 'character',
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -102,7 +150,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
aria-label="图片工具栏"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{isQuickEditSupportedLayer(selectedLayer) ? (
|
||||
{showQuickEdit ? (
|
||||
<>
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
@@ -119,74 +167,77 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{canRasterEdit ? (
|
||||
<>
|
||||
<EditorIconButton
|
||||
label="裁扩按钮"
|
||||
title="裁扩按钮"
|
||||
icon={Crop}
|
||||
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>
|
||||
</>
|
||||
{canRasterEdit && isActionSupported('crop-expand', true) ? (
|
||||
<EditorIconButton
|
||||
label="裁扩按钮"
|
||||
title="裁扩按钮"
|
||||
icon={Crop}
|
||||
onClick={() => onOpenCropExpandPanel(selectedLayer)}
|
||||
/>
|
||||
) : 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
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={
|
||||
@@ -223,7 +274,8 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{selectedLayer.assetKind === 'ui-design' ? (
|
||||
{selectedLayer.assetKind === 'ui-design' &&
|
||||
isActionSupported('extract-ui-design', true) ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="提取素材"
|
||||
@@ -234,7 +286,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
<span>提取素材</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{selectedLayer.assetKind === 'character' ? (
|
||||
{showCharacterAnimation ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="生成动画"
|
||||
@@ -262,12 +314,16 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
</CanvasChromeButton>
|
||||
</>
|
||||
) : null}
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
{extraActionDivider}
|
||||
{extraActions}
|
||||
{isActionSupported('download', true) ? (
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ export async function navigateWechatMiniProgramPage(
|
||||
success() {
|
||||
resolve();
|
||||
},
|
||||
fail(_error) {
|
||||
fail(_error: unknown) {
|
||||
console.error('[host-bridge] wechat mini program navigation failed');
|
||||
reject(new Error(errorMessage));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user