feat(canvas): frame-based layer separation UI
Render and manage the separated layers produced by the edit-elements action as a manual-frame container with attached image and text children. - canvasNodeOps: frame container/child helpers, render ordering, auto-attach nodes to containing frames, and layer reordering - CanvasWorkspace wires frame-aware selection, drag, resize, hide/lock, and layer reorder through the new ops - CanvasPanels: expandable layer tree with rename/reorder - designGateway.submitGeneratorTask + GeneratorTaskSubmitResponse and bbox/generator_name artifact metadata in the domain - ArtboardPreview shows a working state for generating frame layers - i18n: expandLayer / renameLayer / layerTitlePlaceholder; styles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,11 +36,11 @@ import { visiblePromptText } from "@/ui/lib/promptImageReferences";
|
||||
import { isImageGeneratorThreadMessage, visibleAgentMessages } from "./canvas/agentMessageFilters";
|
||||
import { CanvasColorPopover } from "./canvas/CanvasColorPopover";
|
||||
import { buildAlignmentGuides, clamp, minGroupNodeScale, nodeBounds, nodeIntersectsRect, normalizeScreenRect, pointInsideNode, scaleGroupNode, screenBoundsForNodes, screenRectToWorldRect, type AlignmentGuide, type NodeAlignment, type NodeDistribution, type NodeTransform, type ResizeHandle, type SelectionBox } from "@/ui/pages/CanvasWorkspace/canvas/canvasGeometry";
|
||||
import { alignNodes, autoLayoutAroundNode as autoLayoutAroundNodeOp, autoLayoutNodes, canResizeCanvasNode, canUseNodeContextMenu, cloneCanvasNode, distributeNodes, groupNodes, isCanvasPenStrokeNode, isRealCanvasImageNode, moveNodesByDelta, patchFrameStyleNode, removeNodeById, removeNodesById, reorderNode as reorderNodeOp, resizeVisualNodeByHandle, toggleSetValue, toggleSetValues, ungroupNodes, updateNodeContent as updateNodeContentOp, updateNodeTransform as updateNodeTransformOp, visibleCanvasNodes as getVisibleCanvasNodes } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
|
||||
import { alignNodes, attachNodesToContainingFrames, autoLayoutAroundNode as autoLayoutAroundNodeOp, autoLayoutNodes, canResizeCanvasNode, canUseNodeContextMenu, cloneCanvasNode, distributeNodes, groupNodes, isCanvasPenStrokeNode, isFrameChildNode, isFrameContainerNode, isRealCanvasImageNode, moveNodesByDelta, nodeIdsWithFrameChildren, orderedCanvasNodesForRender, patchFrameStyleNode, removeNodesById, reorderLayer as reorderLayerOp, reorderNode as reorderNodeOp, resizeFrameWithChildren, resizeVisualNodeByHandle, toggleSetValues, ungroupNodes, updateNodeContent as updateNodeContentOp, updateNodeTransform as updateNodeTransformOp, visibleCanvasNodes as getVisibleCanvasNodes, type LayerReorderRequest } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
|
||||
import { CanvasSelectionFrame } from "@/ui/pages/CanvasWorkspace/canvas/CanvasSelectionFrame";
|
||||
import { beginFrameDrawing, FrameDraftOverlay, type FrameDraft, type FrameWorldRect } from "./canvas/FrameToolOverlay";
|
||||
import { FrameStyleToolbar } from "./canvas/FrameStyleToolbar";
|
||||
import { defaultFrameFillColor, defaultFrameStrokeColor, defaultFrameStrokeStyle, defaultFrameStrokeWidth, normalizeHexColor } from "./canvas/frameStyle";
|
||||
import { defaultFrameStrokeColor, defaultFrameStrokeStyle, defaultFrameStrokeWidth, normalizeHexColor } from "./canvas/frameStyle";
|
||||
import { generatorTaskPollIntervalMs } from "@/ui/pages/CanvasWorkspace/canvas/generatorTaskPolling";
|
||||
import { useCanvasDocument } from "./canvas/hooks/useCanvasDocument";
|
||||
import { useCanvasPersistence } from "./canvas/hooks/useCanvasPersistence";
|
||||
@@ -925,7 +925,8 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
}, [project?.id, projectId]);
|
||||
|
||||
const visibleCanvasNodes = useMemo(() => getVisibleCanvasNodes(nodes, hiddenNodeIds), [hiddenNodeIds, nodes]);
|
||||
const renderedCanvasNodes = useMemo(() => nodes.filter((node) => !isBooleanShapeOperandNode(node, nodes) && (!hiddenNodeIds.has(node.id) || selectedIds.has(node.id))), [hiddenNodeIds, nodes, selectedIds]);
|
||||
const renderOrderedCanvasNodes = useMemo(() => orderedCanvasNodesForRender(nodes), [nodes]);
|
||||
const renderedCanvasNodes = useMemo(() => renderOrderedCanvasNodes.filter((node) => !isBooleanShapeOperandNode(node, nodes) && (!hiddenNodeIds.has(node.id) || selectedIds.has(node.id))), [hiddenNodeIds, nodes, renderOrderedCanvasNodes, selectedIds]);
|
||||
const imageModels = useMemo(() => getImageModels(locale), [locale]);
|
||||
const agentHistoryThreads = useMemo(() => agentHistoryListItems(agentThreads, project, imageGeneratorThreadIdsRef.current), [agentThreads, project]);
|
||||
const filteredAgentHistoryThreads = useMemo(() => {
|
||||
@@ -1253,8 +1254,21 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
showToast(message, tone);
|
||||
};
|
||||
|
||||
const attachFrameChildren = (current: CanvasNode[], candidateIds: Set<string>) => attachNodesToContainingFrames(current, candidateIds);
|
||||
|
||||
const setNodesWithFrameAttachment = (updater: (current: CanvasNode[]) => { nodes: CanvasNode[]; candidateIds: Set<string> }) => {
|
||||
setNodes((current) => {
|
||||
const result = updater(current);
|
||||
const next = attachFrameChildren(result.nodes, result.candidateIds);
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const frameAwareNodeIds = (nodeIds: Iterable<string>) => nodeIdsWithFrameChildren(nodesRef.current, new Set(nodeIds));
|
||||
|
||||
const findAnnotationTarget = (x: number, y: number, candidate?: CanvasNode) => {
|
||||
const visualNodes = nodesRef.current.filter((node) => (node.type === "image" || node.type === "frame") && node.status !== "generating");
|
||||
const visualNodes = orderedCanvasNodesForRender(nodesRef.current).filter((node) => (node.type === "image" || node.type === "frame") && node.status !== "generating");
|
||||
if (candidate && visualNodes.some((node) => node.id === candidate.id) && pointInsideNode(x, y, candidate)) return candidate;
|
||||
return [...visualNodes].reverse().find((node) => pointInsideNode(x, y, node)) ?? null;
|
||||
};
|
||||
@@ -1292,12 +1306,17 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
status: "draft",
|
||||
layerRole: "manual-frame",
|
||||
opacity: 1,
|
||||
fillColor: defaultFrameFillColor,
|
||||
fillColor: "transparent",
|
||||
strokeColor: defaultFrameStrokeColor,
|
||||
strokeWidth: defaultFrameStrokeWidth,
|
||||
strokeStyle: defaultFrameStrokeStyle
|
||||
};
|
||||
setNodes((current) => [...current, node]);
|
||||
setNodes((current) => {
|
||||
const candidateIds = new Set(current.map((item) => item.id));
|
||||
const next = attachFrameChildren([...current, node], candidateIds);
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
selectOnlyNode(node.id);
|
||||
setActiveTool("select");
|
||||
setMode("select");
|
||||
@@ -1314,11 +1333,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
kind: selectedShapeKind,
|
||||
draft
|
||||
});
|
||||
setNodes((current) => {
|
||||
const next = [...current, node];
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, node],
|
||||
candidateIds: new Set([node.id])
|
||||
}));
|
||||
selectOnlyNode(node.id);
|
||||
setActiveTool("select");
|
||||
setMode("select");
|
||||
@@ -1342,11 +1360,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
onDraftChange: setPenDraft,
|
||||
onCommit: (draft) => {
|
||||
const node = createPenStrokeNode(draft, t("newPenStrokeNode"));
|
||||
setNodes((current) => {
|
||||
const next = [...current, node];
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, node],
|
||||
candidateIds: new Set([node.id])
|
||||
}));
|
||||
setDirty(true);
|
||||
}
|
||||
});
|
||||
@@ -1598,24 +1615,30 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
activeTool === "select" &&
|
||||
mode === "select" &&
|
||||
!lockedNodeIds.has(node.id) &&
|
||||
!node.parentId &&
|
||||
!editingTextNodeId;
|
||||
const nodeGroupIds = node.parentId ? nodesRef.current.filter((item) => item.parentId === node.parentId).map((item) => item.id) : [];
|
||||
const frameChild = isFrameChildNode(node, nodesRef.current);
|
||||
const nodeGroupIds = node.parentId && !frameChild ? nodesRef.current.filter((item) => item.parentId === node.parentId).map((item) => item.id) : [];
|
||||
const useExistingSelection = selectedIds.has(node.id) && selectedIds.size > 1;
|
||||
const groupDrag = useExistingSelection || nodeGroupIds.length > 1;
|
||||
const dragIds = groupDrag ? new Set(useExistingSelection ? selectedIds : nodeGroupIds) : new Set([node.id]);
|
||||
const selectedDragIds = groupDrag ? new Set(useExistingSelection ? selectedIds : nodeGroupIds) : new Set([node.id]);
|
||||
const dragIds = nodeIdsWithFrameChildren(nodesRef.current, selectedDragIds);
|
||||
if (groupDrag) {
|
||||
selectNodes(Array.from(dragIds));
|
||||
selectNodes(Array.from(selectedDragIds));
|
||||
} else {
|
||||
selectOnlyNode(node.id);
|
||||
}
|
||||
if (lockedNodeIds.has(node.id)) return;
|
||||
|
||||
const dragNodeById = new Map(nodesRef.current.map((item) => [item.id, item]));
|
||||
const movesWithSelectedFrame = (item: CanvasNode) => {
|
||||
const parent = item.parentId ? dragNodeById.get(item.parentId) : null;
|
||||
return Boolean(parent && selectedDragIds.has(parent.id) && isFrameContainerNode(parent));
|
||||
};
|
||||
const start = {
|
||||
pointerX: event.clientX,
|
||||
pointerY: event.clientY,
|
||||
positions: nodesRef.current
|
||||
.filter((item) => dragIds.has(item.id) && !lockedNodeIds.has(item.id))
|
||||
.filter((item) => dragIds.has(item.id) && (!lockedNodeIds.has(item.id) || movesWithSelectedFrame(item)))
|
||||
.map((item) => ({ id: item.id, x: item.x, y: item.y })),
|
||||
scale: viewport.k
|
||||
};
|
||||
@@ -1628,7 +1651,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const dx = (moveEvent.clientX - start.pointerX) / start.scale;
|
||||
const dy = (moveEvent.clientY - start.pointerY) / start.scale;
|
||||
const nextNodes = moveNodesByDelta(nodesRef.current, start.positions, dx, dy);
|
||||
const movedNodes = nextNodes.filter((item) => dragIds.has(item.id) && !lockedNodeIds.has(item.id));
|
||||
const movedNodes = nextNodes.filter((item) => selectedDragIds.has(item.id) && !lockedNodeIds.has(item.id));
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
setAlignmentGuides(buildAlignmentGuides(movedNodes, nextNodes, viewportRef.current));
|
||||
@@ -1643,6 +1666,11 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
setEditingTextNodeId(node.id);
|
||||
return;
|
||||
}
|
||||
if (dragged) {
|
||||
const nextNodes = attachFrameChildren(nodesRef.current, dragIds);
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
}
|
||||
if (dragged && Array.from(dragIds).some((nodeId) => nodesRef.current.find((item) => item.id === nodeId)?.layerRole === "mockup-print")) {
|
||||
void refreshMockupPrintTextures(Array.from(dragIds));
|
||||
return;
|
||||
@@ -1702,6 +1730,9 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
setAlignmentGuides([]);
|
||||
const nextNodes = attachFrameChildren(nodesRef.current, new Set([node.id]));
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
if (node.layerRole === "mockup-print") {
|
||||
void refreshMockupPrintTextures([node.id]);
|
||||
return;
|
||||
@@ -1734,8 +1765,9 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const dx = (moveEvent.clientX - start.pointerX) / start.scale;
|
||||
const dy = (moveEvent.clientY - start.pointerY) / start.scale;
|
||||
const resizedNode = resizeVisualNodeByHandle(node, handle, { dx, dy });
|
||||
nodesRef.current = nodesRef.current.map((item) => (item.id === node.id ? resizedNode : item));
|
||||
setNodes((current) => current.map((item) => (item.id === node.id ? resizedNode : item)));
|
||||
const nextNodes = isFrameContainerNode(node) ? resizeFrameWithChildren(nodesRef.current, node.id, resizedNode) : nodesRef.current.map((item) => (item.id === node.id ? resizedNode : item));
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
setAlignmentGuides(buildAlignmentGuides([resizedNode], nodesRef.current, viewportRef.current));
|
||||
setDirty(true);
|
||||
};
|
||||
@@ -1744,6 +1776,11 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
setAlignmentGuides([]);
|
||||
if (!isFrameContainerNode(node)) {
|
||||
const nextNodes = attachFrameChildren(nodesRef.current, new Set([node.id]));
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
}
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
@@ -1848,9 +1885,12 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
};
|
||||
|
||||
const updateFrameStyle = (nodeId: string, patch: FrameStylePatch) => {
|
||||
const applyPatch = (item: CanvasNode) => (item.id === nodeId ? patchFrameStyleNode(item, patch) : item);
|
||||
nodesRef.current = nodesRef.current.map(applyPatch);
|
||||
setNodes((current) => current.map(applyPatch));
|
||||
const frame = nodesRef.current.find((item) => item.id === nodeId);
|
||||
if (!frame) return;
|
||||
const nextFrame = patchFrameStyleNode(frame, patch);
|
||||
const nextNodes = isFrameContainerNode(frame) ? resizeFrameWithChildren(nodesRef.current, nodeId, nextFrame) : nodesRef.current.map((item) => (item.id === nodeId ? nextFrame : item));
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
@@ -1884,25 +1924,26 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
};
|
||||
|
||||
const removeNode = (nodeId: string) => {
|
||||
nodesRef.current = removeNodeById(nodesRef.current, nodeId);
|
||||
setNodes((current) => removeNodeById(current, nodeId));
|
||||
setEditingTextNodeId((current) => (current === nodeId ? null : current));
|
||||
const ids = frameAwareNodeIds([nodeId]);
|
||||
nodesRef.current = removeNodesById(nodesRef.current, ids);
|
||||
setNodes((current) => removeNodesById(current, ids));
|
||||
setEditingTextNodeId((current) => (current && ids.has(current) ? null : current));
|
||||
setHiddenNodeIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(nodeId);
|
||||
ids.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
setLockedNodeIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(nodeId);
|
||||
ids.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
setSelectedIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(nodeId);
|
||||
ids.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
if (selectedId === nodeId) selectOnlyNode(null);
|
||||
if (selectedId && ids.has(selectedId)) selectOnlyNode(null);
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
@@ -1928,7 +1969,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
|
||||
const duplicateNode = (node: CanvasNode) => {
|
||||
const copy = cloneCanvasNode(node, cryptoFallbackId());
|
||||
setNodes((current) => [...current, copy]);
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, copy],
|
||||
candidateIds: new Set([copy.id])
|
||||
}));
|
||||
selectOnlyNode(copy.id);
|
||||
setDirty(true);
|
||||
};
|
||||
@@ -1940,29 +1984,44 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
};
|
||||
|
||||
const reorderNode = (nodeId: string, action: "forward" | "backward" | "front" | "back") => {
|
||||
setNodes((current) => reorderNodeOp(current, nodeId, action));
|
||||
setNodes((current) => {
|
||||
const next = reorderNodeOp(current, nodeId, action);
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const reorderLayer = (request: LayerReorderRequest) => {
|
||||
setNodes((current) => {
|
||||
const next = reorderLayerOp(current, request);
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const toggleLayerHidden = (nodeId: string) => {
|
||||
setHiddenNodeIds((current) => toggleSetValue(current, nodeId));
|
||||
const ids = Array.from(frameAwareNodeIds([nodeId]));
|
||||
setHiddenNodeIds((current) => toggleSetValues(current, ids));
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const toggleLayerLocked = (nodeId: string) => {
|
||||
setLockedNodeIds((current) => toggleSetValue(current, nodeId));
|
||||
const ids = Array.from(frameAwareNodeIds([nodeId]));
|
||||
setLockedNodeIds((current) => toggleSetValues(current, ids));
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const toggleLayersHidden = (nodeIds: string[]) => {
|
||||
if (nodeIds.length === 0) return;
|
||||
setHiddenNodeIds((current) => toggleSetValues(current, nodeIds));
|
||||
setHiddenNodeIds((current) => toggleSetValues(current, Array.from(frameAwareNodeIds(nodeIds))));
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const toggleLayersLocked = (nodeIds: string[]) => {
|
||||
if (nodeIds.length === 0) return;
|
||||
setLockedNodeIds((current) => toggleSetValues(current, nodeIds));
|
||||
setLockedNodeIds((current) => toggleSetValues(current, Array.from(frameAwareNodeIds(nodeIds))));
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
@@ -2116,7 +2175,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
};
|
||||
|
||||
const deleteSelectedNodes = () => {
|
||||
const ids = new Set(selectedNodes.filter((node) => !lockedNodeIds.has(node.id) && !hiddenNodeIds.has(node.id)).map((node) => node.id));
|
||||
const ids = frameAwareNodeIds(selectedNodes.filter((node) => !lockedNodeIds.has(node.id) && !hiddenNodeIds.has(node.id)).map((node) => node.id));
|
||||
if (ids.size === 0) return;
|
||||
nodesRef.current = removeNodesById(nodesRef.current, ids);
|
||||
setNodes((current) => removeNodesById(current, ids));
|
||||
@@ -2163,11 +2222,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
textDecoration: "none",
|
||||
textTransform: "none"
|
||||
});
|
||||
setNodes((current) => {
|
||||
const next = [...current, node];
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, node],
|
||||
candidateIds: new Set([node.id])
|
||||
}));
|
||||
selectOnlyNode(node.id);
|
||||
setDirty(true);
|
||||
return true;
|
||||
@@ -2335,7 +2393,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
tone: "natural",
|
||||
status: "draft"
|
||||
};
|
||||
setNodes((current) => [...current, node]);
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, node],
|
||||
candidateIds: new Set([node.id])
|
||||
}));
|
||||
selectOnlyNode(node.id);
|
||||
setDirty(true);
|
||||
};
|
||||
@@ -2368,11 +2429,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
setComposerMode("image");
|
||||
setActiveTool("select");
|
||||
setMode("select");
|
||||
setNodes((current) => {
|
||||
const next = [...current, node];
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, node],
|
||||
candidateIds: new Set([node.id])
|
||||
}));
|
||||
selectOnlyNode(node.id);
|
||||
setDirty(true);
|
||||
};
|
||||
@@ -2442,11 +2502,10 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
textDecoration: "none",
|
||||
textTransform: "none"
|
||||
};
|
||||
setNodes((current) => {
|
||||
const next = [...current, node];
|
||||
nodesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setNodesWithFrameAttachment((current) => ({
|
||||
nodes: [...current, node],
|
||||
candidateIds: new Set([node.id])
|
||||
}));
|
||||
selectOnlyNode(node.id);
|
||||
setEditingTextNodeId(node.id);
|
||||
setDirty(true);
|
||||
@@ -3270,7 +3329,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
}
|
||||
|
||||
const optimisticNodes = preparedUploads.map((item) => item.node);
|
||||
const nextNodesWithUploads = [...nodesRef.current, ...optimisticNodes];
|
||||
const nextNodesWithUploads = attachFrameChildren([...nodesRef.current, ...optimisticNodes], new Set(optimisticNodes.map((node) => node.id)));
|
||||
nodesRef.current = nextNodesWithUploads;
|
||||
setNodes(nextNodesWithUploads);
|
||||
selectNodes(optimisticNodes.map((node) => node.id));
|
||||
@@ -3435,6 +3494,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
onToggleLocked={toggleLayerLocked}
|
||||
onToggleHiddenMany={toggleLayersHidden}
|
||||
onToggleLockedMany={toggleLayersLocked}
|
||||
onReorderLayer={reorderLayer}
|
||||
onClose={() => setLayersPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -4159,9 +4219,9 @@ function CanvasNodeView({
|
||||
selectionScale={selectionScale}
|
||||
transform={transform}
|
||||
onResizePointerDown={onResizePointerDown}
|
||||
onTitleRename={node.type === "image" ? onTitleRename : undefined}
|
||||
renameLabel={t("renameImageNode")}
|
||||
titlePlaceholder={t("imageTitlePlaceholder")}
|
||||
onTitleRename={onTitleRename}
|
||||
renameLabel={t("renameLayer")}
|
||||
titlePlaceholder={t("layerTitlePlaceholder")}
|
||||
/>
|
||||
)}
|
||||
{selected && lineShapeNode && !locked && (
|
||||
|
||||
Reference in New Issue
Block a user