feat(canvas): finalize mixer reconnect swap and related updates
This commit is contained in:
@@ -107,7 +107,8 @@ isNodePaletteEnabled // true wenn: implementiert + kein systemOutput + Template
|
||||
|
||||
**Kategorien:**
|
||||
- `source` — Quelle (image, text, video, asset, color)
|
||||
- `ai-output` — KI-Ausgabe (prompt, video-prompt, ai-text, ai-video, agent-output)
|
||||
- `ai-output` — KI-Ausgabe (prompt, video-prompt, ai-text)
|
||||
- `agents` — Agents (agent, agent-output)
|
||||
- `transform` — Transformation (crop, bg-remove, upscale)
|
||||
- `image-edit` — Bildbearbeitung (adjustments)
|
||||
- `control` — Steuerung & Flow
|
||||
|
||||
@@ -215,6 +215,24 @@ function formatExecutionRequirements(plan: AgentExecutionPlan): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function formatDeliverableFirstInstructions(definition: AgentDefinition): string {
|
||||
const rules = [
|
||||
"Prioritize publishable, user-facing deliverables for every execution step.",
|
||||
"Lead with final copy/content that can be shipped immediately.",
|
||||
"Keep assumptions, rationale, and risk notes secondary and concise.",
|
||||
"Do not produce reasoning-dominant output or long meta commentary.",
|
||||
"When context is partial, deliver the best safe draft first and clearly note assumptions in brief form.",
|
||||
];
|
||||
|
||||
if (definition.id === "campaign-distributor") {
|
||||
rules.push(
|
||||
"For Campaign Distributor steps, output channel-ready publishable copy first, then short format/assumption notes.",
|
||||
);
|
||||
}
|
||||
|
||||
return `deliverable-first rules:\n- ${rules.join("\n- ")}`;
|
||||
}
|
||||
|
||||
export function buildExecuteMessages(input: {
|
||||
definition: AgentDefinition;
|
||||
locale: AgentLocale;
|
||||
@@ -235,6 +253,7 @@ export function buildExecuteMessages(input: {
|
||||
"Use the following compiled prompt segments:",
|
||||
formatPromptSegments(segments),
|
||||
`execution rules:\n- ${input.definition.executionRules.join("\n- ")}`,
|
||||
formatDeliverableFirstInstructions(input.definition),
|
||||
"Return one output payload per execution step keyed by step id.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
|
||||
@@ -26,16 +26,23 @@ export type AgentStructuredOutput = {
|
||||
previewText: string;
|
||||
sections: AgentOutputSection[];
|
||||
metadata: Record<string, string | string[]>;
|
||||
metadataLabels: Record<string, string>;
|
||||
qualityChecks: string[];
|
||||
body: string;
|
||||
};
|
||||
|
||||
export type AgentStructuredMetadataEntry = {
|
||||
key: string;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type AgentStructuredOutputDraft = Partial<
|
||||
AgentStructuredOutput & {
|
||||
sections: Array<Partial<AgentOutputSection> | null>;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
>;
|
||||
Omit<AgentStructuredOutput, "sections" | "metadata">
|
||||
> & {
|
||||
sections?: unknown[];
|
||||
metadata?: Record<string, unknown>;
|
||||
metadataEntries?: unknown[];
|
||||
};
|
||||
|
||||
export type AgentExecutionStep = {
|
||||
id: string;
|
||||
@@ -178,6 +185,93 @@ function normalizeStructuredMetadata(raw: unknown): Record<string, string | stri
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function normalizeStructuredMetadataEntries(raw: unknown): Record<string, string | string[]> {
|
||||
if (!Array.isArray(raw)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const metadata: Record<string, string | string[]> = {};
|
||||
|
||||
for (const item of raw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>;
|
||||
const key = trimString(record.key);
|
||||
if (key === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const values = normalizeStringArray(record.values);
|
||||
const singleValue = trimString(record.value);
|
||||
|
||||
if (values.length > 1) {
|
||||
metadata[key] = values;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (values.length === 1) {
|
||||
metadata[key] = values[0]!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (singleValue !== "") {
|
||||
metadata[key] = singleValue;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function slugifyMetadataKey(value: string): string {
|
||||
const normalized = value
|
||||
.replace(/ä/g, "ae")
|
||||
.replace(/ö/g, "oe")
|
||||
.replace(/ü/g, "ue")
|
||||
.replace(/Ä/g, "ae")
|
||||
.replace(/Ö/g, "oe")
|
||||
.replace(/Ü/g, "ue")
|
||||
.replace(/ß/g, "ss")
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^\x20-\x7e]+/g, " ")
|
||||
.replace(/[^a-z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "");
|
||||
|
||||
return normalized || "metadata";
|
||||
}
|
||||
|
||||
function sanitizeStructuredMetadata(raw: Record<string, string | string[]>): {
|
||||
metadata: Record<string, string | string[]>;
|
||||
metadataLabels: Record<string, string>;
|
||||
} {
|
||||
const metadata: Record<string, string | string[]> = {};
|
||||
const metadataLabels: Record<string, string> = {};
|
||||
|
||||
for (const [rawKey, value] of Object.entries(raw)) {
|
||||
const trimmedKey = trimString(rawKey);
|
||||
if (trimmedKey === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const slugBase = slugifyMetadataKey(trimmedKey);
|
||||
let slug = slugBase;
|
||||
let suffix = 2;
|
||||
|
||||
while (slug in metadata) {
|
||||
slug = `${slugBase}_${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
metadata[slug] = value;
|
||||
metadataLabels[slug] = trimmedKey;
|
||||
}
|
||||
|
||||
return { metadata, metadataLabels };
|
||||
}
|
||||
|
||||
function derivePreviewTextFromSections(sections: AgentOutputSection[]): string {
|
||||
return sections[0]?.content ?? "";
|
||||
}
|
||||
@@ -365,7 +459,12 @@ export function normalizeAgentStructuredOutput(
|
||||
trimString(draft.artifactType) || trimString(fallback.artifactType) || SAFE_FALLBACK_OUTPUT_TYPE;
|
||||
const sections = normalizeOutputSections(draft.sections);
|
||||
const previewText = trimString(draft.previewText) || derivePreviewTextFromSections(sections);
|
||||
const metadata = normalizeStructuredMetadata(draft.metadata);
|
||||
const metadataFromEntries = normalizeStructuredMetadataEntries(draft.metadataEntries);
|
||||
const rawMetadata =
|
||||
Object.keys(metadataFromEntries).length > 0
|
||||
? metadataFromEntries
|
||||
: normalizeStructuredMetadata(draft.metadata);
|
||||
const { metadata, metadataLabels } = sanitizeStructuredMetadata(rawMetadata);
|
||||
const qualityChecks = normalizeStringArray(draft.qualityChecks);
|
||||
const body =
|
||||
trimString(draft.body) ||
|
||||
@@ -382,6 +481,7 @@ export function normalizeAgentStructuredOutput(
|
||||
previewText,
|
||||
sections,
|
||||
metadata,
|
||||
metadataLabels,
|
||||
qualityChecks,
|
||||
body,
|
||||
};
|
||||
|
||||
@@ -50,6 +50,23 @@ const AGENT_ALLOWED_SOURCE_TYPES = new Set<string>([
|
||||
"ai-video",
|
||||
]);
|
||||
|
||||
const MIXER_ALLOWED_SOURCE_TYPES = new Set<string>([
|
||||
"image",
|
||||
"asset",
|
||||
"ai-image",
|
||||
"render",
|
||||
]);
|
||||
|
||||
const MIXER_TARGET_HANDLES = new Set<string>(["base", "overlay"]);
|
||||
|
||||
function normalizeMixerHandle(handle: string | null | undefined): string {
|
||||
if (handle == null || handle === "" || handle === "null") {
|
||||
return "base";
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
const ADJUSTMENT_DISALLOWED_TARGET_TYPES = new Set<string>(["prompt", "ai-image"]);
|
||||
|
||||
export type CanvasConnectionValidationReason =
|
||||
@@ -66,14 +83,52 @@ export type CanvasConnectionValidationReason =
|
||||
| "adjustment-target-forbidden"
|
||||
| "render-source-invalid"
|
||||
| "agent-source-invalid"
|
||||
| "agent-output-source-invalid";
|
||||
| "agent-output-source-invalid"
|
||||
| "mixer-source-invalid"
|
||||
| "mixer-target-handle-invalid"
|
||||
| "mixer-handle-incoming-limit"
|
||||
| "mixer-incoming-limit";
|
||||
|
||||
export function validateCanvasConnectionPolicy(args: {
|
||||
sourceType: string;
|
||||
targetType: string;
|
||||
targetIncomingCount: number;
|
||||
targetHandle?: string | null;
|
||||
targetIncomingHandles?: Array<string | null | undefined>;
|
||||
}): CanvasConnectionValidationReason | null {
|
||||
const { sourceType, targetType, targetIncomingCount } = args;
|
||||
const {
|
||||
sourceType,
|
||||
targetType,
|
||||
targetIncomingCount,
|
||||
targetHandle,
|
||||
targetIncomingHandles,
|
||||
} = args;
|
||||
|
||||
if (targetType === "mixer") {
|
||||
if (!MIXER_ALLOWED_SOURCE_TYPES.has(sourceType)) {
|
||||
return "mixer-source-invalid";
|
||||
}
|
||||
|
||||
const normalizedTargetHandle = normalizeMixerHandle(targetHandle);
|
||||
if (!MIXER_TARGET_HANDLES.has(normalizedTargetHandle)) {
|
||||
return "mixer-target-handle-invalid";
|
||||
}
|
||||
|
||||
if (targetIncomingCount >= 2) {
|
||||
return "mixer-incoming-limit";
|
||||
}
|
||||
|
||||
const normalizedIncomingHandles = (targetIncomingHandles ?? []).map((handle) =>
|
||||
normalizeMixerHandle(handle),
|
||||
);
|
||||
const incomingOnHandle = normalizedIncomingHandles.filter(
|
||||
(handle) => handle === normalizedTargetHandle,
|
||||
).length;
|
||||
|
||||
if (incomingOnHandle >= 1) {
|
||||
return "mixer-handle-incoming-limit";
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType === "agent-output" && sourceType !== "agent") {
|
||||
return "agent-output-source-invalid";
|
||||
@@ -159,6 +214,14 @@ export function getCanvasConnectionValidationMessage(
|
||||
return "Agent-Nodes akzeptieren nur Content- und Kontext-Inputs, keine Generierungs-Steuerknoten wie Prompt.";
|
||||
case "agent-output-source-invalid":
|
||||
return "Agent-Ausgabe akzeptiert nur Eingaben von Agent-Nodes.";
|
||||
case "mixer-source-invalid":
|
||||
return "Mixer akzeptiert nur Bild-, Asset-, KI-Bild- oder Render-Input.";
|
||||
case "mixer-target-handle-invalid":
|
||||
return "Mixer akzeptiert nur die Ziel-Handles 'base' und 'overlay'.";
|
||||
case "mixer-handle-incoming-limit":
|
||||
return "Jeder Mixer-Handle akzeptiert nur eine eingehende Verbindung.";
|
||||
case "mixer-incoming-limit":
|
||||
return "Mixer-Nodes erlauben maximal zwei eingehende Verbindungen.";
|
||||
default:
|
||||
return "Verbindung ist fuer diese Node-Typen nicht erlaubt.";
|
||||
}
|
||||
|
||||
216
lib/canvas-mixer-preview.ts
Normal file
216
lib/canvas-mixer-preview.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
buildGraphSnapshot,
|
||||
resolveNodeImageUrl,
|
||||
resolveRenderPreviewInputFromGraph,
|
||||
type CanvasGraphEdgeLike,
|
||||
type CanvasGraphNodeLike,
|
||||
type CanvasGraphSnapshot,
|
||||
} from "@/lib/canvas-render-preview";
|
||||
|
||||
export type MixerBlendMode = "normal" | "multiply" | "screen" | "overlay";
|
||||
|
||||
export type MixerPreviewStatus = "empty" | "partial" | "ready" | "error";
|
||||
|
||||
export type MixerPreviewError = "duplicate-handle-edge";
|
||||
|
||||
export type MixerPreviewState = {
|
||||
status: MixerPreviewStatus;
|
||||
baseUrl?: string;
|
||||
overlayUrl?: string;
|
||||
blendMode: MixerBlendMode;
|
||||
opacity: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
error?: MixerPreviewError;
|
||||
};
|
||||
|
||||
const MIXER_SOURCE_NODE_TYPES = new Set(["image", "asset", "ai-image", "render"]);
|
||||
const MIXER_BLEND_MODES = new Set<MixerBlendMode>([
|
||||
"normal",
|
||||
"multiply",
|
||||
"screen",
|
||||
"overlay",
|
||||
]);
|
||||
const DEFAULT_BLEND_MODE: MixerBlendMode = "normal";
|
||||
const DEFAULT_OPACITY = 100;
|
||||
const MIN_OPACITY = 0;
|
||||
const MAX_OPACITY = 100;
|
||||
const DEFAULT_OFFSET = 0;
|
||||
const MIN_OFFSET = -2048;
|
||||
const MAX_OFFSET = 2048;
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function parseNumeric(value: unknown): number | null {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeOpacity(value: unknown): number {
|
||||
const parsed = parseNumeric(value);
|
||||
if (parsed === null) {
|
||||
return DEFAULT_OPACITY;
|
||||
}
|
||||
|
||||
return clamp(parsed, MIN_OPACITY, MAX_OPACITY);
|
||||
}
|
||||
|
||||
function normalizeOffset(value: unknown): number {
|
||||
const parsed = parseNumeric(value);
|
||||
if (parsed === null) {
|
||||
return DEFAULT_OFFSET;
|
||||
}
|
||||
|
||||
return clamp(parsed, MIN_OFFSET, MAX_OFFSET);
|
||||
}
|
||||
|
||||
export function normalizeMixerPreviewData(data: unknown): Pick<
|
||||
MixerPreviewState,
|
||||
"blendMode" | "opacity" | "offsetX" | "offsetY"
|
||||
> {
|
||||
const record = (data ?? {}) as Record<string, unknown>;
|
||||
const blendMode = MIXER_BLEND_MODES.has(record.blendMode as MixerBlendMode)
|
||||
? (record.blendMode as MixerBlendMode)
|
||||
: DEFAULT_BLEND_MODE;
|
||||
|
||||
return {
|
||||
blendMode,
|
||||
opacity: normalizeOpacity(record.opacity),
|
||||
offsetX: normalizeOffset(record.offsetX),
|
||||
offsetY: normalizeOffset(record.offsetY),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHandleEdge(args: {
|
||||
incomingEdges: readonly CanvasGraphEdgeLike[];
|
||||
handle: "base" | "overlay";
|
||||
}): { edge: CanvasGraphEdgeLike | null; duplicate: boolean } {
|
||||
const edges = args.incomingEdges.filter((edge) => {
|
||||
if (args.handle === "base") {
|
||||
return edge.targetHandle === "base" || edge.targetHandle == null || edge.targetHandle === "";
|
||||
}
|
||||
|
||||
return edge.targetHandle === "overlay";
|
||||
});
|
||||
|
||||
if (edges.length > 1) {
|
||||
return { edge: null, duplicate: true };
|
||||
}
|
||||
|
||||
return { edge: edges[0] ?? null, duplicate: false };
|
||||
}
|
||||
|
||||
function resolveSourceUrlFromNode(args: {
|
||||
sourceNode: CanvasGraphNodeLike;
|
||||
graph: CanvasGraphSnapshot;
|
||||
}): string | undefined {
|
||||
if (!MIXER_SOURCE_NODE_TYPES.has(args.sourceNode.type)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (args.sourceNode.type === "render") {
|
||||
const renderData = (args.sourceNode.data ?? {}) as Record<string, unknown>;
|
||||
const renderOutputUrl =
|
||||
typeof renderData.lastUploadUrl === "string" && renderData.lastUploadUrl.length > 0
|
||||
? renderData.lastUploadUrl
|
||||
: undefined;
|
||||
if (renderOutputUrl) {
|
||||
return renderOutputUrl;
|
||||
}
|
||||
|
||||
const directRenderUrl = resolveNodeImageUrl(args.sourceNode.data);
|
||||
if (directRenderUrl) {
|
||||
return directRenderUrl;
|
||||
}
|
||||
|
||||
const preview = resolveRenderPreviewInputFromGraph({
|
||||
nodeId: args.sourceNode.id,
|
||||
graph: args.graph,
|
||||
});
|
||||
return preview.sourceUrl ?? undefined;
|
||||
}
|
||||
|
||||
return resolveNodeImageUrl(args.sourceNode.data) ?? undefined;
|
||||
}
|
||||
|
||||
function resolveSourceUrlFromEdge(args: {
|
||||
edge: CanvasGraphEdgeLike | null;
|
||||
graph: CanvasGraphSnapshot;
|
||||
}): string | undefined {
|
||||
if (!args.edge) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sourceNode = args.graph.nodesById.get(args.edge.source);
|
||||
if (!sourceNode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolveSourceUrlFromNode({ sourceNode, graph: args.graph });
|
||||
}
|
||||
|
||||
export function resolveMixerPreviewFromGraph(args: {
|
||||
nodeId: string;
|
||||
graph: CanvasGraphSnapshot;
|
||||
}): MixerPreviewState {
|
||||
const node = args.graph.nodesById.get(args.nodeId);
|
||||
const normalized = normalizeMixerPreviewData(node?.data);
|
||||
const incomingEdges = args.graph.incomingEdgesByTarget.get(args.nodeId) ?? [];
|
||||
const base = resolveHandleEdge({ incomingEdges, handle: "base" });
|
||||
const overlay = resolveHandleEdge({ incomingEdges, handle: "overlay" });
|
||||
|
||||
if (base.duplicate || overlay.duplicate) {
|
||||
return {
|
||||
status: "error",
|
||||
...normalized,
|
||||
error: "duplicate-handle-edge",
|
||||
};
|
||||
}
|
||||
|
||||
const baseUrl = resolveSourceUrlFromEdge({ edge: base.edge, graph: args.graph });
|
||||
const overlayUrl = resolveSourceUrlFromEdge({ edge: overlay.edge, graph: args.graph });
|
||||
|
||||
if (baseUrl && overlayUrl) {
|
||||
return {
|
||||
status: "ready",
|
||||
...normalized,
|
||||
baseUrl,
|
||||
overlayUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (baseUrl || overlayUrl) {
|
||||
return {
|
||||
status: "partial",
|
||||
...normalized,
|
||||
baseUrl,
|
||||
overlayUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "empty",
|
||||
...normalized,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMixerPreview(args: {
|
||||
nodeId: string;
|
||||
nodes: readonly CanvasGraphNodeLike[];
|
||||
edges: readonly CanvasGraphEdgeLike[];
|
||||
}): MixerPreviewState {
|
||||
return resolveMixerPreviewFromGraph({
|
||||
nodeId: args.nodeId,
|
||||
graph: buildGraphSnapshot(args.nodes, args.edges),
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type { CanvasNodeType } from "@/lib/canvas-node-types";
|
||||
export type NodeCategoryId =
|
||||
| "source"
|
||||
| "ai-output"
|
||||
| "agents"
|
||||
| "transform"
|
||||
| "image-edit"
|
||||
| "control"
|
||||
@@ -20,10 +21,11 @@ export const NODE_CATEGORY_META: Record<
|
||||
> = {
|
||||
source: { label: "Quelle", order: 0 },
|
||||
"ai-output": { label: "KI-Ausgabe", order: 1 },
|
||||
transform: { label: "Transformation", order: 2 },
|
||||
"image-edit": { label: "Bildbearbeitung", order: 3 },
|
||||
control: { label: "Steuerung & Flow", order: 4 },
|
||||
layout: { label: "Canvas & Layout", order: 5 },
|
||||
agents: { label: "Agents", order: 2 },
|
||||
transform: { label: "Transformation", order: 3 },
|
||||
"image-edit": { label: "Bildbearbeitung", order: 4 },
|
||||
control: { label: "Steuerung & Flow", order: 5 },
|
||||
layout: { label: "Canvas & Layout", order: 6 },
|
||||
};
|
||||
|
||||
export const NODE_CATEGORIES_ORDERED: NodeCategoryId[] = (
|
||||
@@ -85,6 +87,14 @@ export const NODE_CATALOG: readonly NodeCatalogEntry[] = [
|
||||
category: "source",
|
||||
phase: 2,
|
||||
}),
|
||||
entry({
|
||||
type: "ai-video",
|
||||
label: "KI-Video-Ausgabe",
|
||||
category: "source",
|
||||
phase: 2,
|
||||
systemOutput: true,
|
||||
disabledHint: "Wird von der KI erzeugt",
|
||||
}),
|
||||
entry({
|
||||
type: "asset",
|
||||
label: "Asset (Stock)",
|
||||
@@ -112,18 +122,17 @@ export const NODE_CATALOG: readonly NodeCatalogEntry[] = [
|
||||
systemOutput: true,
|
||||
disabledHint: "Wird von der KI erzeugt",
|
||||
}),
|
||||
// Agents
|
||||
entry({
|
||||
type: "ai-video",
|
||||
label: "KI-Video-Ausgabe",
|
||||
category: "ai-output",
|
||||
type: "agent",
|
||||
label: "Campaign Orchestrator",
|
||||
category: "agents",
|
||||
phase: 2,
|
||||
systemOutput: true,
|
||||
disabledHint: "Wird von der KI erzeugt",
|
||||
}),
|
||||
entry({
|
||||
type: "agent-output",
|
||||
label: "Agent-Ausgabe",
|
||||
category: "ai-output",
|
||||
category: "agents",
|
||||
phase: 2,
|
||||
implemented: true,
|
||||
systemOutput: true,
|
||||
@@ -216,19 +225,11 @@ export const NODE_CATALOG: readonly NodeCatalogEntry[] = [
|
||||
implemented: false,
|
||||
disabledHint: "Folgt in Phase 2",
|
||||
}),
|
||||
entry({
|
||||
type: "agent",
|
||||
label: "Agent",
|
||||
category: "control",
|
||||
phase: 2,
|
||||
}),
|
||||
entry({
|
||||
type: "mixer",
|
||||
label: "Mixer / Merge",
|
||||
category: "control",
|
||||
phase: 3,
|
||||
implemented: false,
|
||||
disabledHint: "Folgt in Phase 3",
|
||||
phase: 1,
|
||||
}),
|
||||
entry({
|
||||
type: "switch",
|
||||
|
||||
@@ -43,6 +43,18 @@ export const CANVAS_NODE_TEMPLATES = [
|
||||
templateId: "campaign-distributor",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mixer",
|
||||
label: "Mixer / Merge",
|
||||
width: 360,
|
||||
height: 320,
|
||||
defaultData: {
|
||||
blendMode: "normal",
|
||||
opacity: 100,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "note",
|
||||
label: "Notiz",
|
||||
|
||||
@@ -72,6 +72,7 @@ export type CanvasSyncOpPayloadByType = {
|
||||
targetNodeId: Id<"nodes">;
|
||||
sourceHandle?: string;
|
||||
targetHandle?: string;
|
||||
edgeIdToIgnore?: Id<"edges">;
|
||||
clientRequestId: string;
|
||||
};
|
||||
removeEdge: {
|
||||
@@ -477,6 +478,10 @@ function normalizeOp(raw: unknown): CanvasSyncOp | null {
|
||||
typeof payload.targetHandle === "string"
|
||||
? payload.targetHandle
|
||||
: undefined,
|
||||
edgeIdToIgnore:
|
||||
typeof payload.edgeIdToIgnore === "string"
|
||||
? (payload.edgeIdToIgnore as Id<"edges">)
|
||||
: undefined,
|
||||
clientRequestId: payload.clientRequestId,
|
||||
},
|
||||
enqueuedAt,
|
||||
|
||||
@@ -231,6 +231,7 @@ export const NODE_HANDLE_MAP: Record<
|
||||
crop: { source: undefined, target: undefined },
|
||||
render: { source: undefined, target: undefined },
|
||||
agent: { target: "agent-in" },
|
||||
mixer: { source: "mixer-out", target: "base" },
|
||||
"agent-output": { target: "agent-output-in" },
|
||||
};
|
||||
|
||||
@@ -292,6 +293,16 @@ export const NODE_DEFAULTS: Record<
|
||||
outputNodeIds: [],
|
||||
},
|
||||
},
|
||||
mixer: {
|
||||
width: 360,
|
||||
height: 320,
|
||||
data: {
|
||||
blendMode: "normal",
|
||||
opacity: 100,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
},
|
||||
"agent-output": {
|
||||
width: 360,
|
||||
height: 260,
|
||||
|
||||
Reference in New Issue
Block a user