@blksails/pi-web-canvas-ui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +45 -0
- package/src/aigc-model-meta.tsx +53 -0
- package/src/aigc-quick-settings.tsx +345 -0
- package/src/canvas-gallery.tsx +292 -0
- package/src/canvas-launcher.tsx +218 -0
- package/src/canvas-workbench.tsx +2156 -0
- package/src/client-image-ops.ts +50 -0
- package/src/generate-actions.ts +155 -0
- package/src/index.ts +122 -0
- package/src/lineage-view.tsx +170 -0
- package/src/plugin-aggregation.ts +51 -0
- package/src/styles.css +24 -0
- package/src/use-canvas-view.ts +214 -0
- package/src/vision-op.ts +112 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated 转发兼容层(canvas-kit-m1 · Req 5.3/5.4/7.1)——保留**一个大版本**。
|
|
3
|
+
*
|
|
4
|
+
* client-image-ops 已整体迁入 `@blksails/pi-web-canvas-kit`(bitmap-io + 类型 canonical 家),
|
|
5
|
+
* 本模块只做**显式子集转发**:原模块曾有的 31 项导出(19 值 + 12 类型)逐一对应,
|
|
6
|
+
* 深路径 import(`.../src/canvas/client-image-ops.js`)与 ui 包入口 `export *` 链双兼容。
|
|
7
|
+
*
|
|
8
|
+
* - 新代码请直接 `import ... from "@blksails/pi-web-canvas-kit"`;
|
|
9
|
+
* - 刻意用显式 `export {...} from` 而非 `export *`:canvas-kit 出口另含
|
|
10
|
+
* WorkLayer/LoadedImage/CanvasOp 等新家类型,不得经本兼容层泄漏成 ui 的既成公开面。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── 值导出(19)──────────────────────────────────────────────────────────────
|
|
14
|
+
export {
|
|
15
|
+
clampRect,
|
|
16
|
+
rotatedSize,
|
|
17
|
+
cropImage,
|
|
18
|
+
rotateImage,
|
|
19
|
+
createMask,
|
|
20
|
+
hasExpand,
|
|
21
|
+
expandedSize,
|
|
22
|
+
outpaintImage,
|
|
23
|
+
outpaintMask,
|
|
24
|
+
flattenLayers,
|
|
25
|
+
hasMaskContent,
|
|
26
|
+
strokesToMask,
|
|
27
|
+
ANNOTATION_COLOR,
|
|
28
|
+
ANNOTATION_PALETTE,
|
|
29
|
+
drawAnnotations,
|
|
30
|
+
annotationsToImage,
|
|
31
|
+
compositeByMask,
|
|
32
|
+
parseDataUri,
|
|
33
|
+
uploadDataUri,
|
|
34
|
+
} from "@blksails/pi-web-canvas-kit";
|
|
35
|
+
|
|
36
|
+
// ── 类型导出(12)────────────────────────────────────────────────────────────
|
|
37
|
+
export type {
|
|
38
|
+
Ctx2DLike,
|
|
39
|
+
CanvasLike,
|
|
40
|
+
CanvasFactory,
|
|
41
|
+
Rect,
|
|
42
|
+
ImageSourceLike,
|
|
43
|
+
ClientImageOpsDeps,
|
|
44
|
+
ExpandEdges,
|
|
45
|
+
FlattenLayer,
|
|
46
|
+
MaskStroke,
|
|
47
|
+
Annotation,
|
|
48
|
+
UploadFn,
|
|
49
|
+
UploadDataUriInput,
|
|
50
|
+
} from "@blksails/pi-web-canvas-kit";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-actions — 六内置生成动作插件(task 3.1,Req 2.1/2.2/2.3/2.6)。
|
|
3
|
+
*
|
|
4
|
+
* design.md「canvas-ui · generate-actions.ts(六内置动作)」:把 canvas-workbench 内封闭的
|
|
5
|
+
* `decideGenerate` if 链自举迁移为评分制动作插件(defineCanvasAction<SurfaceOp>)。评分与
|
|
6
|
+
* buildArgs 逐分支复刻黄金基准(HEAD 的 decideGenerate 本体);execution 全为对话流通道
|
|
7
|
+
* (via:"prompt"),buildOp 内部走既有 buildSurfaceOp(golden 锁定)。内置六动作 = 行为回归线。
|
|
8
|
+
*
|
|
9
|
+
* 依赖方向(design Allowed Dependencies):本模块单向引 canvas-workbench 的**类型 GenerateDecision**
|
|
10
|
+
* 与**值 buildSurfaceOp**(3.2 才反向让 workbench 引本文件,本任务先单向,零循环);动作契约与
|
|
11
|
+
* SurfaceOp 载荷类型自 canvas-kit / web-kit。
|
|
12
|
+
*
|
|
13
|
+
* mask 透传约定(buildOp 签名保持 (args, input)):调用方(workbench)在掩码上传后把 `att_` 掩码
|
|
14
|
+
* id 经 `args.mask`(string)透传;buildOp 将其取作 buildSurfaceOp 的 opts.maskId 并从传给
|
|
15
|
+
* buildSurfaceOp 的 decision.args 中剔除 —— 保证与现 workbench 调用
|
|
16
|
+
* `buildSurfaceOp(decision, { maskId })`(decision.args 本不含 mask)逐字节等价。
|
|
17
|
+
*/
|
|
18
|
+
import { defineCanvasAction, type ActionInput, type CanvasRegistry } from "@blksails/pi-web-canvas-kit";
|
|
19
|
+
import type { SurfaceOp } from "@blksails/pi-web-kit";
|
|
20
|
+
import { buildSurfaceOp, type GenerateDecision } from "./canvas-workbench.js";
|
|
21
|
+
|
|
22
|
+
/** 内置动作 id(`builtin:` 前缀)→ GenerateDecision 动作(union 字面量保持)。 */
|
|
23
|
+
const ACTION_BY_ID = {
|
|
24
|
+
"builtin:outpaint": "outpaint",
|
|
25
|
+
"builtin:inpaint": "inpaint",
|
|
26
|
+
"builtin:reference": "reference",
|
|
27
|
+
"builtin:variants": "variants",
|
|
28
|
+
"builtin:reframe": "reframe",
|
|
29
|
+
"builtin:edit": "edit",
|
|
30
|
+
} satisfies Record<string, GenerateDecision["action"]>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 内置插件 id → {@link GenerateDecision}(action + args 透传;args 同引用不复制)。
|
|
34
|
+
* 未知 id 防御性回退 edit(内置六动作恒命中映射表,回退仅为类型完备)。
|
|
35
|
+
*/
|
|
36
|
+
export function toGenerateDecision(
|
|
37
|
+
pluginId: string,
|
|
38
|
+
args: Record<string, unknown>,
|
|
39
|
+
): GenerateDecision {
|
|
40
|
+
const action = (ACTION_BY_ID as Record<string, GenerateDecision["action"]>)[pluginId] ?? "edit";
|
|
41
|
+
return { action, args };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 公共 base 参数(image + prompt + 非空才带 model/size);逐字复刻 decideGenerate 本体。 */
|
|
45
|
+
function baseArgs(input: ActionInput): Record<string, unknown> {
|
|
46
|
+
const base: Record<string, unknown> = { image: input.imageId, prompt: input.prompt };
|
|
47
|
+
if (input.model !== "") base.model = input.model;
|
|
48
|
+
if (input.size !== "") base.size = input.size;
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* prompt 通道 buildOp 工厂:mask 经 args.mask 透传约定剔除后转 opts.maskId,其余 args 原样
|
|
54
|
+
* 组装为 {@link GenerateDecision} 交 buildSurfaceOp(golden 锁定的参数渲染)。
|
|
55
|
+
*/
|
|
56
|
+
function makeBuildOp(id: string): (args: Record<string, unknown>) => SurfaceOp {
|
|
57
|
+
return (args) => {
|
|
58
|
+
const { mask, ...rest } = args;
|
|
59
|
+
const maskId = typeof mask === "string" ? mask : undefined;
|
|
60
|
+
return buildSurfaceOp(toGenerateDecision(id, rest), { maskId });
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 六内置动作插件(评分制;buildArgs 逐分支复刻 decideGenerate)──────────────────
|
|
65
|
+
|
|
66
|
+
/** 扩图(hasExpand===true;删 size,image 由调用方替换为大画布合成图,mask 同步补充)。 */
|
|
67
|
+
const outpaintAction = defineCanvasAction<SurfaceOp>({
|
|
68
|
+
id: "builtin:outpaint",
|
|
69
|
+
label: "扩图",
|
|
70
|
+
match: (input) => (input.hasExpand === true ? 100 : false),
|
|
71
|
+
buildArgs: (input) => {
|
|
72
|
+
const { size: _drop, ...rest } = baseArgs(input);
|
|
73
|
+
void _drop;
|
|
74
|
+
return rest;
|
|
75
|
+
},
|
|
76
|
+
execution: { via: "prompt", buildOp: makeBuildOp("builtin:outpaint") },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** 局部重绘(hasMask;mask 由调用方在上传后经 args.mask 补充)。 */
|
|
80
|
+
const inpaintAction = defineCanvasAction<SurfaceOp>({
|
|
81
|
+
id: "builtin:inpaint",
|
|
82
|
+
label: "局部重绘",
|
|
83
|
+
match: (input) => (input.hasMask ? 90 : false),
|
|
84
|
+
buildArgs: (input) => baseArgs(input),
|
|
85
|
+
execution: { via: "prompt", buildOp: makeBuildOp("builtin:inpaint") },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** 融合生成(referenceIds 非空;附 reference_images,variants>=2 才附 n)。 */
|
|
89
|
+
const referenceAction = defineCanvasAction<SurfaceOp>({
|
|
90
|
+
id: "builtin:reference",
|
|
91
|
+
label: "融合生成",
|
|
92
|
+
match: (input) => (input.referenceIds.length > 0 ? 80 : false),
|
|
93
|
+
buildArgs: (input) => {
|
|
94
|
+
const args: Record<string, unknown> = {
|
|
95
|
+
...baseArgs(input),
|
|
96
|
+
reference_images: [...input.referenceIds],
|
|
97
|
+
};
|
|
98
|
+
if (input.variants >= 2) args.n = input.variants;
|
|
99
|
+
return args;
|
|
100
|
+
},
|
|
101
|
+
execution: { via: "prompt", buildOp: makeBuildOp("builtin:reference") },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** 生成变体(variants>=2;附 n)。 */
|
|
105
|
+
const variantsAction = defineCanvasAction<SurfaceOp>({
|
|
106
|
+
id: "builtin:variants",
|
|
107
|
+
label: "生成变体",
|
|
108
|
+
match: (input) => (input.variants >= 2 ? 70 : false),
|
|
109
|
+
buildArgs: (input) => ({ ...baseArgs(input), n: input.variants }),
|
|
110
|
+
execution: { via: "prompt", buildOp: makeBuildOp("builtin:variants") },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/** 重构比例(prompt 空 && size 非空;base 原样,reframe 默认提示词由 buildSurfaceOp 补)。 */
|
|
114
|
+
const reframeAction = defineCanvasAction<SurfaceOp>({
|
|
115
|
+
id: "builtin:reframe",
|
|
116
|
+
label: "重构比例",
|
|
117
|
+
match: (input) => (input.prompt.trim() === "" && input.size !== "" ? 60 : false),
|
|
118
|
+
buildArgs: (input) => baseArgs(input),
|
|
119
|
+
execution: { via: "prompt", buildOp: makeBuildOp("builtin:reframe") },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** 整图编辑(恒适用兜底)。 */
|
|
123
|
+
const editAction = defineCanvasAction<SurfaceOp>({
|
|
124
|
+
id: "builtin:edit",
|
|
125
|
+
label: "生成",
|
|
126
|
+
match: () => 10,
|
|
127
|
+
buildArgs: (input) => baseArgs(input),
|
|
128
|
+
execution: { via: "prompt", buildOp: makeBuildOp("builtin:edit") },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 六内置生成动作(注册序 = 评分降序,便于阅读;resolveAction 按分排序,注册序仅决定同分先者
|
|
133
|
+
* 与 registry.actions 枚举序)。decideGenerate 优先级链的自举复刻。
|
|
134
|
+
*/
|
|
135
|
+
export const BUILTIN_GENERATE_ACTIONS: readonly CanvasActionPluginOp[] = [
|
|
136
|
+
outpaintAction,
|
|
137
|
+
inpaintAction,
|
|
138
|
+
referenceAction,
|
|
139
|
+
variantsAction,
|
|
140
|
+
reframeAction,
|
|
141
|
+
editAction,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// 载荷类型别名(六插件的 TOp 恒为 SurfaceOp)。
|
|
145
|
+
type CanvasActionPluginOp = ReturnType<typeof defineCanvasAction<SurfaceOp>>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 六内置动作按序注册进 per-instance 注册表(registerBuiltinTools 同构);返回聚合退订。
|
|
149
|
+
*/
|
|
150
|
+
export function registerBuiltinGenerateActions(reg: CanvasRegistry): () => void {
|
|
151
|
+
const unregisters = BUILTIN_GENERATE_ACTIONS.map((a) => reg.registerAction(a));
|
|
152
|
+
return () => {
|
|
153
|
+
for (const off of unregisters) off();
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blksails/pi-web-canvas-ui — canvas 领域组件 canonical 家唯一出口(出口纪律)。
|
|
3
|
+
*
|
|
4
|
+
* 纪律(Req 2.1/2.2,canvas-ui-m15 design「Boundary Commitments」):
|
|
5
|
+
* - 出口面 = 自 packages/ui/src/canvas/ 迁入的 8 文件 **HEAD 导出全集的去重并集**
|
|
6
|
+
* (严格超集于迁移前 ui index 的 canvas 兼容导出块:深路径 named import 与
|
|
7
|
+
* 设置面板消费——aigc-model-meta 三导出、workbench 的 decideGenerate/
|
|
8
|
+
* buildSurfaceOp/buildToolPrompt、use-canvas-view 的 canvasViewStore 等——
|
|
9
|
+
* 均须从包入口可达);
|
|
10
|
+
* - 组件语义/DOM/data-* 锚点与迁移前逐一致(原样迁入,仅 import 来源改线);
|
|
11
|
+
* - 依赖方向:ui 经转发层消费本包,反向禁止(本包零 @blksails/pi-web-ui 依赖);
|
|
12
|
+
* canvas-kit 消费只走主入口,禁深路径;
|
|
13
|
+
* - 此出口是 semver 承诺面:任何导出的增删改按 semver 语义对待;
|
|
14
|
+
* 显式清单 re-export,禁 export *(防内部件经链泄漏成既成公开面);
|
|
15
|
+
* - client-image-ops 显式清单刻意排除 LoadedImage(canvas-workbench 单点
|
|
16
|
+
* re-export),8 文件无跨文件重名。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// aigc-model-meta — provider 元数据(设置面板 aigc-model-toggles-field 跨包消费)。
|
|
20
|
+
export { PROVIDER_META, displayNameOf, ProviderBadge } from "./aigc-model-meta.js";
|
|
21
|
+
|
|
22
|
+
// aigc-quick-settings — 输入区工具排 AIGC 快捷设置(模型/尺寸偏好;promptToolbar 槽挂载)。
|
|
23
|
+
export { AigcQuickSettings } from "./aigc-quick-settings.js";
|
|
24
|
+
export type { AigcQuickSettingsProps } from "./aigc-quick-settings.js";
|
|
25
|
+
|
|
26
|
+
// canvas-gallery — 画廊(domain="canvas" AAS 投影)。
|
|
27
|
+
export { CanvasGallery } from "./canvas-gallery.js";
|
|
28
|
+
export type { CanvasGalleryProps } from "./canvas-gallery.js";
|
|
29
|
+
|
|
30
|
+
// canvas-launcher — 门控(NEXT_PUBLIC_PI_WEB_CANVAS)+ 画布面板装配。
|
|
31
|
+
export { CanvasLauncher, CanvasPanel, isCanvasEnabled } from "./canvas-launcher.js";
|
|
32
|
+
export type { CanvasLauncherProps, CanvasPanelProps } from "./canvas-launcher.js";
|
|
33
|
+
|
|
34
|
+
// canvas-workbench — 二创工作台(编辑器 + 生成决策纯函数 + inpaint 回合成)。
|
|
35
|
+
export {
|
|
36
|
+
CanvasWorkbench,
|
|
37
|
+
decideGenerate,
|
|
38
|
+
buildSurfaceOp,
|
|
39
|
+
buildToolPrompt,
|
|
40
|
+
composeInpaintBack,
|
|
41
|
+
resolveToolRailTitle,
|
|
42
|
+
} from "./canvas-workbench.js";
|
|
43
|
+
export type {
|
|
44
|
+
CanvasWorkbenchProps,
|
|
45
|
+
GenerateDecision,
|
|
46
|
+
GenerateDecisionInput,
|
|
47
|
+
ImageLoader,
|
|
48
|
+
LoadedImage,
|
|
49
|
+
} from "./canvas-workbench.js";
|
|
50
|
+
|
|
51
|
+
// generate-actions — 六内置生成动作插件(评分制决策链;canvas-kit resolveAction 消费,
|
|
52
|
+
// workbench 装配期注册进 per-instance 注册表)。
|
|
53
|
+
export {
|
|
54
|
+
BUILTIN_GENERATE_ACTIONS,
|
|
55
|
+
registerBuiltinGenerateActions,
|
|
56
|
+
} from "./generate-actions.js";
|
|
57
|
+
|
|
58
|
+
// plugin-aggregation — 宿主中立扩展描述符 → canvas 插件捆聚合纯函数(task 3.1;
|
|
59
|
+
// CanvasPanel extensions → workbench plugins 的领域聚合点)。
|
|
60
|
+
export { collectCanvasPluginBundles } from "./plugin-aggregation.js";
|
|
61
|
+
export type { NamespacedPluginBundles } from "./plugin-aggregation.js";
|
|
62
|
+
|
|
63
|
+
// lineage-view — 血缘树视图。
|
|
64
|
+
export { LineageView, buildLineageTree } from "./lineage-view.js";
|
|
65
|
+
export type { LineageViewProps, LineageNode } from "./lineage-view.js";
|
|
66
|
+
|
|
67
|
+
// use-canvas-view — 画布视图状态(open/density/group 外部 store + hooks)。
|
|
68
|
+
export {
|
|
69
|
+
useCanvasView,
|
|
70
|
+
useCanvasOpen,
|
|
71
|
+
canvasOpenStore,
|
|
72
|
+
canvasViewStore,
|
|
73
|
+
CANVAS_PAGE_SIZE,
|
|
74
|
+
} from "./use-canvas-view.js";
|
|
75
|
+
export type {
|
|
76
|
+
CanvasDensity,
|
|
77
|
+
CanvasGroupMode,
|
|
78
|
+
CanvasViewState,
|
|
79
|
+
UseCanvasViewResult,
|
|
80
|
+
} from "./use-canvas-view.js";
|
|
81
|
+
|
|
82
|
+
// client-image-ops — @deprecated 转发兼容层(canvas-kit canonical 家的显式子集
|
|
83
|
+
// 转发:19 值 + 12 类型;刻意不含 LoadedImage/WorkLayer/CanvasOp 等新家类型)。
|
|
84
|
+
export {
|
|
85
|
+
clampRect,
|
|
86
|
+
rotatedSize,
|
|
87
|
+
cropImage,
|
|
88
|
+
rotateImage,
|
|
89
|
+
createMask,
|
|
90
|
+
hasExpand,
|
|
91
|
+
expandedSize,
|
|
92
|
+
outpaintImage,
|
|
93
|
+
outpaintMask,
|
|
94
|
+
flattenLayers,
|
|
95
|
+
hasMaskContent,
|
|
96
|
+
strokesToMask,
|
|
97
|
+
ANNOTATION_COLOR,
|
|
98
|
+
ANNOTATION_PALETTE,
|
|
99
|
+
drawAnnotations,
|
|
100
|
+
annotationsToImage,
|
|
101
|
+
compositeByMask,
|
|
102
|
+
parseDataUri,
|
|
103
|
+
uploadDataUri,
|
|
104
|
+
} from "./client-image-ops.js";
|
|
105
|
+
export type {
|
|
106
|
+
Ctx2DLike,
|
|
107
|
+
CanvasLike,
|
|
108
|
+
CanvasFactory,
|
|
109
|
+
Rect,
|
|
110
|
+
ImageSourceLike,
|
|
111
|
+
ClientImageOpsDeps,
|
|
112
|
+
ExpandEdges,
|
|
113
|
+
FlattenLayer,
|
|
114
|
+
MaskStroke,
|
|
115
|
+
Annotation,
|
|
116
|
+
UploadFn,
|
|
117
|
+
UploadDataUriInput,
|
|
118
|
+
} from "./client-image-ops.js";
|
|
119
|
+
|
|
120
|
+
// ── vision readout(spec canvas-vision-readout):解读载荷构造器 + 模型选项类型 ──────
|
|
121
|
+
export { buildVisionOp, DEFAULT_READOUT_QUESTION } from "./vision-op.js";
|
|
122
|
+
export type { BuildVisionOpInput, VisionModelOption } from "./vision-op.js";
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LineageView — C 档灵感放大(aigc-canvas · Req 6.1 / 6.2 / 6.3 / 6.4)。
|
|
3
|
+
*
|
|
4
|
+
* 全部从快照派生(UI 本地,不发命令):
|
|
5
|
+
* - **血缘树**:读 `derivedFrom` 建父子关系;
|
|
6
|
+
* - **参数复用**:读选中资产 `genParams` 预填 A 档表单(经 `onReuseParams` 回调);
|
|
7
|
+
* - **A-B 对比**:选两张图并排(读 `displayUrl`);
|
|
8
|
+
* - **当前工作图链**:沿 `derivedFrom` 的一条 UI 本地路径,支持前进 / 回退。
|
|
9
|
+
*/
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import type { GalleryAsset } from "@blksails/pi-web-tool-kit/aigc-canvas-schema";
|
|
12
|
+
import { Button } from "@blksails/pi-web-primitives";
|
|
13
|
+
import { Card } from "@blksails/pi-web-primitives";
|
|
14
|
+
import { cn } from "@blksails/pi-web-primitives";
|
|
15
|
+
|
|
16
|
+
export interface LineageNode {
|
|
17
|
+
asset: GalleryAsset;
|
|
18
|
+
children: LineageNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 从扁平资产列表按 `derivedFrom` 建血缘树(纯函数;缺失父节点者作为根)。 */
|
|
22
|
+
export function buildLineageTree(assets: readonly GalleryAsset[]): LineageNode[] {
|
|
23
|
+
const byId = new Map<string, LineageNode>();
|
|
24
|
+
for (const a of assets) byId.set(a.attachmentId, { asset: a, children: [] });
|
|
25
|
+
const roots: LineageNode[] = [];
|
|
26
|
+
for (const node of byId.values()) {
|
|
27
|
+
const parentId = node.asset.derivedFrom;
|
|
28
|
+
const parent = parentId !== undefined ? byId.get(parentId) : undefined;
|
|
29
|
+
if (parent !== undefined) parent.children.push(node);
|
|
30
|
+
else roots.push(node);
|
|
31
|
+
}
|
|
32
|
+
return roots;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function TreeNode({
|
|
36
|
+
node,
|
|
37
|
+
depth,
|
|
38
|
+
onReuseParams,
|
|
39
|
+
currentId,
|
|
40
|
+
}: {
|
|
41
|
+
node: LineageNode;
|
|
42
|
+
depth: number;
|
|
43
|
+
onReuseParams?: (asset: GalleryAsset) => void;
|
|
44
|
+
currentId?: string;
|
|
45
|
+
}): React.JSX.Element {
|
|
46
|
+
const a = node.asset;
|
|
47
|
+
const isCurrent = currentId !== undefined && a.attachmentId === currentId;
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<Card
|
|
51
|
+
data-lineage-node
|
|
52
|
+
data-att-id={a.attachmentId}
|
|
53
|
+
data-depth={depth}
|
|
54
|
+
className={cn(
|
|
55
|
+
"group relative w-20 shrink-0 overflow-hidden p-0",
|
|
56
|
+
isCurrent && "ring-2 ring-[hsl(var(--ring))]",
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
60
|
+
<img src={a.displayUrl} alt={a.name} width={80} height={80} className="h-20 w-20 object-cover" />
|
|
61
|
+
<div className="pointer-events-none absolute inset-x-0 top-0 truncate bg-gradient-to-b from-black/50 to-transparent px-1 py-0.5 text-[10px] text-white">
|
|
62
|
+
{a.name}
|
|
63
|
+
</div>
|
|
64
|
+
{a.genParams !== undefined ? (
|
|
65
|
+
<Button
|
|
66
|
+
variant="secondary"
|
|
67
|
+
size="sm"
|
|
68
|
+
data-lineage-reuse
|
|
69
|
+
data-att-id={a.attachmentId}
|
|
70
|
+
onClick={() => onReuseParams?.(a)}
|
|
71
|
+
className="absolute inset-x-0 bottom-0 hidden h-6 rounded-none px-1 text-[10px] group-hover:flex"
|
|
72
|
+
>
|
|
73
|
+
复用参数
|
|
74
|
+
</Button>
|
|
75
|
+
) : null}
|
|
76
|
+
</Card>
|
|
77
|
+
{node.children.map((c) => (
|
|
78
|
+
<TreeNode
|
|
79
|
+
key={c.asset.attachmentId}
|
|
80
|
+
node={c}
|
|
81
|
+
depth={depth + 1}
|
|
82
|
+
{...(onReuseParams !== undefined ? { onReuseParams } : {})}
|
|
83
|
+
{...(currentId !== undefined ? { currentId } : {})}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface LineageViewProps {
|
|
91
|
+
readonly assets: readonly GalleryAsset[];
|
|
92
|
+
/** A-B 对比:选中的两个 att_id(取前两项)。 */
|
|
93
|
+
readonly compareIds?: readonly string[];
|
|
94
|
+
/** 当前工作图链(att_id 序列)。 */
|
|
95
|
+
readonly chain?: readonly string[];
|
|
96
|
+
readonly onReuseParams?: (asset: GalleryAsset) => void;
|
|
97
|
+
readonly onChainStep?: (direction: "back" | "forward", id: string) => void;
|
|
98
|
+
/** 当前工作图 att_id(仅高亮,可选增强)。 */
|
|
99
|
+
readonly currentId?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function LineageView({
|
|
103
|
+
assets,
|
|
104
|
+
compareIds,
|
|
105
|
+
chain,
|
|
106
|
+
onReuseParams,
|
|
107
|
+
currentId,
|
|
108
|
+
}: LineageViewProps): React.JSX.Element {
|
|
109
|
+
const tree = React.useMemo(() => buildLineageTree(assets), [assets]);
|
|
110
|
+
const byId = React.useMemo(() => {
|
|
111
|
+
const m = new Map<string, GalleryAsset>();
|
|
112
|
+
for (const a of assets) m.set(a.attachmentId, a);
|
|
113
|
+
return m;
|
|
114
|
+
}, [assets]);
|
|
115
|
+
|
|
116
|
+
const comparePair = (compareIds ?? [])
|
|
117
|
+
.slice(0, 2)
|
|
118
|
+
.map((id) => byId.get(id))
|
|
119
|
+
.filter((a): a is GalleryAsset => a !== undefined);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div data-lineage-view className="flex flex-col gap-2 text-xs">
|
|
123
|
+
{/* 血缘树(横向缩略条)。 */}
|
|
124
|
+
<div className="mb-0.5 font-medium text-[hsl(var(--muted-foreground))]">版本血缘</div>
|
|
125
|
+
<div
|
|
126
|
+
data-lineage-tree
|
|
127
|
+
className="pi-scrollbar-thin flex gap-2 overflow-x-auto pb-1"
|
|
128
|
+
>
|
|
129
|
+
{tree.map((n) => (
|
|
130
|
+
<TreeNode
|
|
131
|
+
key={n.asset.attachmentId}
|
|
132
|
+
node={n}
|
|
133
|
+
depth={0}
|
|
134
|
+
{...(onReuseParams !== undefined ? { onReuseParams } : {})}
|
|
135
|
+
{...(currentId !== undefined ? { currentId } : {})}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* A-B 对比。 */}
|
|
141
|
+
{comparePair.length === 2 ? (
|
|
142
|
+
<div data-lineage-compare className="grid grid-cols-2 gap-1">
|
|
143
|
+
{comparePair.map((a) => (
|
|
144
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
145
|
+
<img
|
|
146
|
+
key={a.attachmentId}
|
|
147
|
+
data-compare-img
|
|
148
|
+
data-att-id={a.attachmentId}
|
|
149
|
+
src={a.displayUrl}
|
|
150
|
+
alt={a.name}
|
|
151
|
+
className="w-full rounded object-contain"
|
|
152
|
+
/>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
) : null}
|
|
156
|
+
|
|
157
|
+
{/* 当前工作图链。 */}
|
|
158
|
+
{chain !== undefined && chain.length > 0 ? (
|
|
159
|
+
<div data-lineage-chain className="flex flex-wrap items-center gap-1">
|
|
160
|
+
{chain.map((id, i) => (
|
|
161
|
+
<span key={id} data-chain-step data-att-id={id} className="opacity-70">
|
|
162
|
+
{byId.get(id)?.name ?? id}
|
|
163
|
+
{i < chain.length - 1 ? " → " : ""}
|
|
164
|
+
</span>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
) : null}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-aggregation — 宿主中立扩展描述符 → canvas 插件捆聚合(task 3.1,
|
|
3
|
+
* Req 4.1/4.2/4.3/5.1/5.2/5.3;design「宿主中立注入与聚合」)。
|
|
4
|
+
*
|
|
5
|
+
* 宿主(pi-chat)对 canvas 领域中立:只把全部已装载扩展描述符(readonly WebExtension[])
|
|
6
|
+
* 整体经 SlotHost 搬运进 CanvasPanel,不解析内容。真正的**领域聚合**发生在此:提取各扩展
|
|
7
|
+
* 声明的 `canvasPlugins` 捆,附来源命名空间(= manifestId),交由 CanvasWorkbench 的
|
|
8
|
+
* registerPluginBundles 施加 `<namespace>:` 前缀与拓扑校验。
|
|
9
|
+
*
|
|
10
|
+
* 类型收敛(design「Allowed Dependencies」):web-kit 的 CanvasPluginBundle 是**最小结构镜像**
|
|
11
|
+
* (组件位 unknown 宽型,便于宿主中立搬运);canonical 家在 canvas-kit(具体插件形状)。二者
|
|
12
|
+
* 无包依赖边,结构上 kit→web-kit 单向可赋值(窄→宽)。聚合是反向的 web-kit→kit **窄化**:
|
|
13
|
+
* 运行期安全(source 作者以 canvas-kit defineXxx 声明捆,transport 仅把组件位擦除为 unknown),
|
|
14
|
+
* 类型层以断言收窄(见 collectCanvasPluginBundles 的 `as`;双向可赋值防漂移断言在
|
|
15
|
+
* plugin-aggregation.test.ts,capability-type-sync M2 先例)。
|
|
16
|
+
*/
|
|
17
|
+
import type { WebExtension } from "@blksails/pi-web-kit";
|
|
18
|
+
import type { CanvasPluginBundle } from "@blksails/pi-web-canvas-kit";
|
|
19
|
+
|
|
20
|
+
/** 单个来源(扩展)贡献的 canvas 插件捆集合 + 其命名空间(= manifestId)。 */
|
|
21
|
+
export interface NamespacedPluginBundles {
|
|
22
|
+
/** 来源命名空间(registerPluginBundles 据此前缀化捆内 id/type;= WebExtension.manifestId)。 */
|
|
23
|
+
readonly namespace: string;
|
|
24
|
+
/** 该来源声明的插件捆(canonical canvas-kit 型;已从 web-kit 镜像窄化)。 */
|
|
25
|
+
readonly bundles: readonly CanvasPluginBundle[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 从已装载扩展描述符集提取各自的 canvas 插件捆,附来源命名空间(manifestId)。
|
|
30
|
+
*
|
|
31
|
+
* - 无 `canvasPlugins` 声明或空数组的扩展被**剔除**(不产生空条目 → 零影响,Req 4.3);
|
|
32
|
+
* - 验签失败的插件包根本不进 `extensions` 列表(装载链既有容错)→ 聚合天然不含、不崩(Req 5.3);
|
|
33
|
+
* - `undefined` 入参(宿主未注入)→ 空聚合(现状路径,Req 4.3);
|
|
34
|
+
* - 车道①(source 自带)与车道②(已装包 webext,同 defineWebExtension 形态)统一经此消费(Req 5.1/5.2)。
|
|
35
|
+
*
|
|
36
|
+
* 纯函数(无副作用;bundles 引用原样透传,仅类型收窄不拷贝/不 mutate 作者只读声明)。
|
|
37
|
+
*/
|
|
38
|
+
export function collectCanvasPluginBundles(
|
|
39
|
+
extensions?: readonly WebExtension[],
|
|
40
|
+
): readonly NamespacedPluginBundles[] {
|
|
41
|
+
if (extensions === undefined) return [];
|
|
42
|
+
const out: NamespacedPluginBundles[] = [];
|
|
43
|
+
for (const ext of extensions) {
|
|
44
|
+
const plugins = ext.canvasPlugins;
|
|
45
|
+
if (plugins === undefined || plugins.length === 0) continue;
|
|
46
|
+
// web-kit 镜像型 → canvas-kit canonical 型窄化(结构上 kit 可赋给 web-kit,反向窄化断言;
|
|
47
|
+
// 运行期安全见模块头注)。整捆引用透传,前缀化留 registerPluginBundles(装配层)。
|
|
48
|
+
out.push({ namespace: ext.manifestId, bundles: plugins as readonly CanvasPluginBundle[] });
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @blksails/pi-web-canvas-ui — canvas 领域样式(自 packages/ui/src/styles.css 随迁,canvas-ui-m15 Req 2.4)。
|
|
3
|
+
* 两段原样迁入:工具图 affordance + 棋盘透明底;由 app 装配层 @import 引入。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Canvas 活跃期:对话流工具卡里带 att_ 的生成图可点(→ 进 Canvas 工作台编辑)。悬浮示可点。 */
|
|
7
|
+
[data-canvas-tool-image-clickable] [data-pi-tool-images] img[data-att-id] {
|
|
8
|
+
cursor: pointer;
|
|
9
|
+
}
|
|
10
|
+
[data-canvas-tool-image-clickable] [data-pi-tool-images] img[data-att-id]:hover {
|
|
11
|
+
outline: 2px solid hsl(var(--ring));
|
|
12
|
+
outline-offset: 1px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* 棋盘透明底:Canvas 舞台展示透明图区(16px 方格,主题 muted 半透)。 */
|
|
16
|
+
.canvas-checkerboard {
|
|
17
|
+
background-image:
|
|
18
|
+
linear-gradient(45deg, hsl(var(--muted) / 0.4) 25%, transparent 25%),
|
|
19
|
+
linear-gradient(-45deg, hsl(var(--muted) / 0.4) 25%, transparent 25%),
|
|
20
|
+
linear-gradient(45deg, transparent 75%, hsl(var(--muted) / 0.4) 75%),
|
|
21
|
+
linear-gradient(-45deg, transparent 75%, hsl(var(--muted) / 0.4) 75%);
|
|
22
|
+
background-size: 16px 16px;
|
|
23
|
+
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
|
24
|
+
}
|