569 lines
22 KiB
JavaScript
569 lines
22 KiB
JavaScript
// ComfyUI extension registration and node lifecycle wiring.
|
|
import { app } from "../../../../scripts/app.js";
|
|
import {
|
|
CANVAS_SIZE_PROPERTY,
|
|
CONTROL_TAB_PROPERTY,
|
|
LAYER_LOCK_PROPERTY,
|
|
OUTPUT_SETTINGS_PROPERTY,
|
|
UI_INSTANCE_PROPERTY,
|
|
UPLOAD_MODE_PROPERTY,
|
|
} from "./constants.js";
|
|
import {
|
|
applyBackendTimelineSettings,
|
|
installBackendTimelineSettingsListener,
|
|
isTargetNode,
|
|
normalizeTimelineSettings,
|
|
payloadsMatchNode,
|
|
setTimelineStatus,
|
|
uiPayloadValue,
|
|
} from "./runtime.js";
|
|
import {
|
|
activeLayer,
|
|
controlsTab,
|
|
deleteActiveLayer,
|
|
ensureSelectedKeyframe,
|
|
layerEditingLocked,
|
|
normalizeKeyframes,
|
|
normalizePan,
|
|
preloadImage,
|
|
preserveExecutedFilterFields,
|
|
refreshTimeline,
|
|
rejectLayerEditIfLocked,
|
|
selectedKeyframe,
|
|
selectedKeyframeIndex,
|
|
serializableTimelinePayload,
|
|
setActiveLayerId,
|
|
setControlsTab,
|
|
setSelectedKeyframe,
|
|
syncTimelineWidget,
|
|
timelineReloadSource,
|
|
toggleLayerEditingLock,
|
|
toggleUploadMode,
|
|
updateActiveLayer,
|
|
uploadMode,
|
|
} from "./model.js";
|
|
import {
|
|
fixPromptWidgetHeights,
|
|
migrateDisableRelayWidgetOrder,
|
|
syncChainInheritedWidgets,
|
|
syncKeyframePromptEditor,
|
|
syncTextLayerEditor,
|
|
} from "./editors.js";
|
|
import {
|
|
configuredCanvasSize,
|
|
rememberTimelineCanvasSize,
|
|
scheduleTimelineCanvasSizeRestore,
|
|
setupNode,
|
|
} from "./node_setup.js";
|
|
import {
|
|
addKeyframeAt,
|
|
bounds,
|
|
deleteKeyframeAtIndex,
|
|
duplicateKeyframeAtIndex,
|
|
findKeyframeAt,
|
|
previewImageBox,
|
|
slotConflict,
|
|
slotToSeconds,
|
|
updateTimelineSliderDrag,
|
|
widgetOutputSettings,
|
|
xToSlot,
|
|
} from "./timeline_ops.js";
|
|
import {
|
|
controlTabAt,
|
|
layerChipAt,
|
|
previewPlacement,
|
|
previewSourceForLayer,
|
|
refreshButtonBounds,
|
|
} from "./rendering.js";
|
|
import {
|
|
addTextLayer,
|
|
deleteLayerButtonBounds,
|
|
imageButtonBounds,
|
|
installKeyboardShortcuts,
|
|
installPointerRelease,
|
|
installWheelCapture,
|
|
keyframeStrengthSliderAt,
|
|
layerFilterSliderAt,
|
|
lockLayerButtonBounds,
|
|
modeButtonBounds,
|
|
releaseDrag,
|
|
textLayerButtonBounds,
|
|
uploadModeButtonBounds,
|
|
zoomPreviewAt,
|
|
} from "./interaction.js";
|
|
import { installClipboardPasteCapture, installFileDropCapture } from "./media.js";
|
|
export function selectedKeyframeIndexForMenu(node) {
|
|
setupNode(node);
|
|
return selectedKeyframeIndex(node, normalizeKeyframes(node.properties.etk_ltxv_timeline_keyframes, node));
|
|
}
|
|
|
|
export function timelineNodeMenuItems(node) {
|
|
if (!isTargetNode(node)) return [];
|
|
return [
|
|
null,
|
|
{
|
|
content: "ETK Timeline: Refresh previews",
|
|
callback: () => {
|
|
setupNode(node);
|
|
refreshTimeline(node);
|
|
},
|
|
},
|
|
{
|
|
content: `ETK Timeline: Upload mode: ${uploadMode(node) === "layer" ? "add layer" : "replace"}`,
|
|
callback: () => {
|
|
setupNode(node);
|
|
toggleUploadMode(node);
|
|
},
|
|
},
|
|
{
|
|
content: "ETK Timeline: Upload image to selected keyframe...",
|
|
callback: () => {
|
|
setupNode(node);
|
|
const keyframe = ensureSelectedKeyframe(node);
|
|
if (!keyframe) return;
|
|
if (node.etkTimelineFileInput) node.etkTimelineFileInput.click();
|
|
},
|
|
},
|
|
{
|
|
content: "ETK Timeline: Duplicate selected keyframe",
|
|
callback: () => {
|
|
const index = selectedKeyframeIndexForMenu(node);
|
|
if (index < 0 || !duplicateKeyframeAtIndex(node, index)) {
|
|
setTimelineStatus(node, "Select a keyframe to duplicate.", "error");
|
|
}
|
|
},
|
|
},
|
|
{
|
|
content: "ETK Timeline: Delete selected keyframe",
|
|
callback: () => {
|
|
const index = selectedKeyframeIndexForMenu(node);
|
|
if (index < 0 || !deleteKeyframeAtIndex(node, index)) {
|
|
setTimelineStatus(node, "Select a keyframe to delete.", "error");
|
|
}
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "ETK.LTXVTimelineImageEditor",
|
|
getNodeMenuItems(node) {
|
|
return timelineNodeMenuItems(node);
|
|
},
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (!isTargetNode(nodeData.name)) return;
|
|
if (nodeType.prototype.etkTimelinePatched) return;
|
|
nodeType.prototype.etkTimelinePatched = true;
|
|
installBackendTimelineSettingsListener();
|
|
installWheelCapture();
|
|
installPointerRelease();
|
|
installKeyboardShortcuts();
|
|
installFileDropCapture();
|
|
installClipboardPasteCapture();
|
|
|
|
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
|
nodeType.prototype.getExtraMenuOptions = function (canvas, options) {
|
|
const extra = originalGetExtraMenuOptions ? originalGetExtraMenuOptions.apply(this, arguments) : undefined;
|
|
const timelineItems = timelineNodeMenuItems(this);
|
|
if (Array.isArray(extra)) return extra.concat(timelineItems);
|
|
if (Array.isArray(options)) options.push(...timelineItems);
|
|
return extra;
|
|
};
|
|
|
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments);
|
|
setupNode(this);
|
|
scheduleTimelineCanvasSizeRestore(this);
|
|
};
|
|
|
|
const originalOnConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function (config) {
|
|
if (originalOnConfigure) originalOnConfigure.apply(this, arguments);
|
|
if (!this.properties) this.properties = {};
|
|
const migratedWidgetOrder = migrateDisableRelayWidgetOrder(this);
|
|
this.properties.etk_ltxv_timeline_keyframes = normalizeKeyframes(timelineReloadSource(this, migratedWidgetOrder), this);
|
|
setupNode(this);
|
|
scheduleTimelineCanvasSizeRestore(this, configuredCanvasSize(config));
|
|
selectedKeyframeIndex(this, this.properties.etk_ltxv_timeline_keyframes);
|
|
};
|
|
|
|
const originalOnSerialize = nodeType.prototype.onSerialize;
|
|
nodeType.prototype.onSerialize = function (serialized) {
|
|
if (originalOnSerialize) originalOnSerialize.apply(this, arguments);
|
|
if (!serialized) return;
|
|
setupNode(this);
|
|
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
|
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
|
syncTimelineWidget(this);
|
|
if (!serialized.properties) serialized.properties = {};
|
|
serialized.properties.etk_ltxv_timeline_keyframes = serializableTimelinePayload(this, keyframes);
|
|
serialized.properties.etk_ltxv_selected_keyframe = this.properties.etk_ltxv_selected_keyframe;
|
|
serialized.properties.etk_ltxv_selected_keyframe_id = this.properties.etk_ltxv_selected_keyframe_id;
|
|
serialized.properties[UPLOAD_MODE_PROPERTY] = uploadMode(this);
|
|
serialized.properties[LAYER_LOCK_PROPERTY] = layerEditingLocked(this);
|
|
serialized.properties[CONTROL_TAB_PROPERTY] = controlsTab(this);
|
|
const outputSettings =
|
|
normalizeTimelineSettings(this.etkTimelineBackendSettings || this.properties[OUTPUT_SETTINGS_PROPERTY]) ||
|
|
widgetOutputSettings(this);
|
|
if (outputSettings) {
|
|
serialized.properties[OUTPUT_SETTINGS_PROPERTY] = outputSettings;
|
|
}
|
|
const canvasSize = rememberTimelineCanvasSize(this);
|
|
if (canvasSize) {
|
|
serialized.size = [...canvasSize];
|
|
serialized.properties[CANVAS_SIZE_PROPERTY] = [...canvasSize];
|
|
}
|
|
delete serialized.properties[UI_INSTANCE_PROPERTY];
|
|
};
|
|
|
|
const originalOnExecuted = nodeType.prototype.onExecuted;
|
|
nodeType.prototype.onExecuted = function (message) {
|
|
if (originalOnExecuted) originalOnExecuted.apply(this, arguments);
|
|
const payload = uiPayloadValue(message, "etk_ltxv_timeline_keyframes");
|
|
const settingsPayload = uiPayloadValue(message, "etk_ltxv_timeline_settings");
|
|
if (!payload && !settingsPayload) return;
|
|
if (!payloadsMatchNode(this, [payload, settingsPayload])) return;
|
|
setupNode(this);
|
|
applyBackendTimelineSettings(this, settingsPayload);
|
|
if (payload) {
|
|
const keyframes = preserveExecutedFilterFields(this, payload);
|
|
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
|
const selectedIndex = selectedKeyframeIndex(this, keyframes);
|
|
if (selectedIndex < 0 && keyframes.length) {
|
|
setSelectedKeyframe(this, 0, keyframes);
|
|
} else {
|
|
syncKeyframePromptEditor(this, selectedIndex >= 0 ? keyframes[selectedIndex] : null);
|
|
syncTextLayerEditor(this);
|
|
}
|
|
for (const keyframe of keyframes) preloadImage(this, keyframe);
|
|
}
|
|
syncTimelineWidget(this);
|
|
this.setDirtyCanvas(true, true);
|
|
if (this.graph && typeof this.graph.setDirtyCanvas === "function") {
|
|
this.graph.setDirtyCanvas(true, true);
|
|
}
|
|
};
|
|
|
|
const originalOnRemoved = nodeType.prototype.onRemoved;
|
|
nodeType.prototype.onRemoved = function () {
|
|
releaseDrag(this);
|
|
if (this.etkTimelineFileInput) {
|
|
this.etkTimelineFileInput.remove();
|
|
this.etkTimelineFileInput = null;
|
|
}
|
|
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
|
|
};
|
|
|
|
const originalComputeSize = nodeType.prototype.computeSize;
|
|
nodeType.prototype.computeSize = function () {
|
|
fixPromptWidgetHeights(this);
|
|
const size = originalComputeSize ? originalComputeSize.apply(this, arguments) : [360, 260];
|
|
size[0] = Math.max(size[0], 460);
|
|
return size;
|
|
};
|
|
|
|
const originalOnResize = nodeType.prototype.onResize;
|
|
nodeType.prototype.onResize = function (size) {
|
|
fixPromptWidgetHeights(this);
|
|
syncChainInheritedWidgets(this);
|
|
if (originalOnResize) originalOnResize.apply(this, arguments);
|
|
if (!size) return;
|
|
size[0] = Math.max(size[0], 460);
|
|
rememberTimelineCanvasSize(this, size);
|
|
if (this.setDirtyCanvas) this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
const originalOnConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function () {
|
|
if (originalOnConnectionsChange) originalOnConnectionsChange.apply(this, arguments);
|
|
setupNode(this);
|
|
syncChainInheritedWidgets(this);
|
|
if (this.setDirtyCanvas) this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
const handleTimelineDoubleClick = function () {
|
|
const pos = Array.from(arguments).find((arg) => Array.isArray(arg) && arg.length >= 2);
|
|
if (!pos) return false;
|
|
setupNode(this);
|
|
const { padX, width, timelineY, timelineH } = bounds(this);
|
|
const [x, y] = pos;
|
|
if (x < padX || x > padX + width || y < timelineY || y > timelineY + timelineH) return false;
|
|
if (findKeyframeAt(this, pos) >= 0) return false;
|
|
addKeyframeAt(this, x);
|
|
return true;
|
|
};
|
|
|
|
const originalMouseDblClick = nodeType.prototype.onMouseDblClick;
|
|
nodeType.prototype.onMouseDblClick = function (_event, pos) {
|
|
if (handleTimelineDoubleClick.apply(this, arguments)) return true;
|
|
return originalMouseDblClick ? originalMouseDblClick.apply(this, arguments) : false;
|
|
};
|
|
|
|
const originalDblClick = nodeType.prototype.onDblClick;
|
|
nodeType.prototype.onDblClick = function () {
|
|
if (handleTimelineDoubleClick.apply(this, arguments)) return true;
|
|
return originalDblClick ? originalDblClick.apply(this, arguments) : false;
|
|
};
|
|
|
|
const originalMouseDown = nodeType.prototype.onMouseDown;
|
|
nodeType.prototype.onMouseDown = function (event, pos) {
|
|
if (event && event.button != null && event.button !== 0) {
|
|
return originalMouseDown ? originalMouseDown.apply(this, arguments) : false;
|
|
}
|
|
setupNode(this);
|
|
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
|
const hitIndex = findKeyframeAt(this, pos);
|
|
const { padX, width, timelineY, timelineH, controlsY, controlsH, previewY, previewH } = bounds(this);
|
|
const [x, y] = pos;
|
|
const inControls = x >= padX && x <= padX + width && y >= controlsY && y <= controlsY + controlsH;
|
|
const inPreview = x >= padX && x <= padX + width && y >= previewY && y <= previewY + previewH;
|
|
const refreshButton = refreshButtonBounds(this);
|
|
|
|
if (
|
|
x >= refreshButton.x &&
|
|
x <= refreshButton.x + refreshButton.w &&
|
|
y >= refreshButton.y &&
|
|
y <= refreshButton.y + refreshButton.h
|
|
) {
|
|
refreshTimeline(this);
|
|
return true;
|
|
}
|
|
|
|
if (hitIndex >= 0) {
|
|
setSelectedKeyframe(this, hitIndex, keyframes);
|
|
this.etkTimelineDragging = true;
|
|
this.etkTimelineDraggingId = keyframes[hitIndex].id;
|
|
window.etkTimelineActiveDragNode = this;
|
|
this.setDirtyCanvas(true, true);
|
|
return true;
|
|
}
|
|
|
|
if (x >= padX && x <= padX + width && y >= timelineY && y <= timelineY + timelineH) {
|
|
const now = performance.now();
|
|
const isDoubleClick =
|
|
(event && event.detail && event.detail >= 2) ||
|
|
(this.etkTimelineLastClick && now - this.etkTimelineLastClick < 350);
|
|
if (isDoubleClick) {
|
|
addKeyframeAt(this, x);
|
|
this.etkTimelineLastClick = 0;
|
|
} else {
|
|
this.etkTimelineLastClick = now;
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (inControls || inPreview) {
|
|
if (inControls) {
|
|
const tab = controlTabAt(this, x, y);
|
|
if (tab) {
|
|
setControlsTab(this, tab);
|
|
return true;
|
|
}
|
|
}
|
|
const selected = selectedKeyframe(this);
|
|
if (selected) {
|
|
const activeControlTab = controlsTab(this);
|
|
if (inControls && activeControlTab === "layers") {
|
|
const button = modeButtonBounds(this);
|
|
const imageButton = imageButtonBounds(this);
|
|
const textButton = textLayerButtonBounds(this);
|
|
const uploadButton = uploadModeButtonBounds(this);
|
|
const lockLayerButton = lockLayerButtonBounds(this);
|
|
const deleteLayerButton = deleteLayerButtonBounds(this);
|
|
if (x >= lockLayerButton.x && x <= lockLayerButton.x + lockLayerButton.w && y >= lockLayerButton.y && y <= lockLayerButton.y + lockLayerButton.h) {
|
|
toggleLayerEditingLock(this);
|
|
return true;
|
|
}
|
|
if (x >= button.x && x <= button.x + button.w && y >= button.y && y <= button.y + button.h) {
|
|
if (rejectLayerEditIfLocked(this, "Fit mode change")) return true;
|
|
const layer = activeLayer(selected);
|
|
if (layer) {
|
|
updateActiveLayer(this, selected.id, (current) => ({
|
|
...current,
|
|
fitMode: current.fitMode === "crop" ? "pad" : "crop",
|
|
}));
|
|
} else {
|
|
selected.fitMode = selected.fitMode === "crop" ? "pad" : "crop";
|
|
const selectedIndex = selectedKeyframeIndex(this, keyframes);
|
|
if (selectedIndex < 0) return true;
|
|
keyframes[selectedIndex] = selected;
|
|
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
|
syncTimelineWidget(this);
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
return true;
|
|
}
|
|
if (x >= imageButton.x && x <= imageButton.x + imageButton.w && y >= imageButton.y && y <= imageButton.y + imageButton.h) {
|
|
if (rejectLayerEditIfLocked(this, "Image upload")) return true;
|
|
this.etkTimelineFileInput.click();
|
|
return true;
|
|
}
|
|
if (x >= textButton.x && x <= textButton.x + textButton.w && y >= textButton.y && y <= textButton.y + textButton.h) {
|
|
addTextLayer(this, selected.id);
|
|
return true;
|
|
}
|
|
if (x >= uploadButton.x && x <= uploadButton.x + uploadButton.w && y >= uploadButton.y && y <= uploadButton.y + uploadButton.h) {
|
|
if (rejectLayerEditIfLocked(this, "Upload mode change")) return true;
|
|
toggleUploadMode(this);
|
|
return true;
|
|
}
|
|
if (
|
|
x >= deleteLayerButton.x &&
|
|
x <= deleteLayerButton.x + deleteLayerButton.w &&
|
|
y >= deleteLayerButton.y &&
|
|
y <= deleteLayerButton.y + deleteLayerButton.h
|
|
) {
|
|
deleteActiveLayer(this, selected.id);
|
|
return true;
|
|
}
|
|
const chip = layerChipAt(this, selected, x, y);
|
|
if (chip) {
|
|
setActiveLayerId(this, selected.id, chip.layerId);
|
|
return true;
|
|
}
|
|
} else if (inControls && activeControlTab === "filters") {
|
|
const layer = activeLayer(selected);
|
|
const filterSlider = layer ? layerFilterSliderAt(this, x, y) : null;
|
|
if (layer && filterSlider) {
|
|
this.etkSliderDragging = { family: "filter", keyframeId: selected.id, layerId: layer.id, kind: filterSlider.kind };
|
|
window.etkTimelineActiveDragNode = this;
|
|
updateTimelineSliderDrag(this, pos);
|
|
return true;
|
|
}
|
|
} else if (inControls && activeControlTab === "strength") {
|
|
const strengthSlider = keyframeStrengthSliderAt(this, x, y);
|
|
if (strengthSlider) {
|
|
this.etkSliderDragging = { family: "strength", keyframeId: selected.id, kind: strengthSlider.kind };
|
|
window.etkTimelineActiveDragNode = this;
|
|
updateTimelineSliderDrag(this, pos);
|
|
return true;
|
|
}
|
|
}
|
|
const layer = activeLayer(selected);
|
|
if (layer && inPreview) {
|
|
if (layerEditingLocked(this)) {
|
|
rejectLayerEditIfLocked(this, "Layer pan");
|
|
return true;
|
|
}
|
|
const { boxX, boxY, boxW, boxH } = previewImageBox(this);
|
|
if (x >= boxX && x <= boxX + boxW && y >= boxY && y <= boxY + boxH) {
|
|
this.etkPreviewPanning = {
|
|
keyframeId: selected.id,
|
|
layerId: layer.id,
|
|
startX: x,
|
|
startY: y,
|
|
startPanX: normalizePan(layer.panX),
|
|
startPanY: normalizePan(layer.panY),
|
|
};
|
|
window.etkTimelineActiveDragNode = this;
|
|
return true;
|
|
}
|
|
} else if (!layer && inPreview) {
|
|
if (rejectLayerEditIfLocked(this, "Image upload")) return true;
|
|
this.etkTimelineFileInput.click();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return originalMouseDown ? originalMouseDown.apply(this, arguments) : false;
|
|
};
|
|
|
|
const originalMouseMove = nodeType.prototype.onMouseMove;
|
|
nodeType.prototype.onMouseMove = function (event, pos) {
|
|
if (this.etkSliderDragging) {
|
|
if (event && event.buttons === 0) {
|
|
releaseDrag(this);
|
|
return true;
|
|
}
|
|
updateTimelineSliderDrag(this, pos);
|
|
return true;
|
|
}
|
|
if (this.etkPreviewPanning) {
|
|
if (event && event.buttons === 0) {
|
|
releaseDrag(this);
|
|
return true;
|
|
}
|
|
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
|
const keyframe = keyframes.find((candidate) => candidate.id === this.etkPreviewPanning.keyframeId);
|
|
const layer = keyframe && (keyframe.layers || []).find((candidate) => candidate.id === this.etkPreviewPanning.layerId);
|
|
const image = layer ? previewSourceForLayer(this, layer) : null;
|
|
if (keyframe && layer && image && image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) {
|
|
const { boxW, boxH } = previewImageBox(this);
|
|
const placement = previewPlacement(
|
|
image,
|
|
boxW,
|
|
boxH,
|
|
layer.fitMode,
|
|
layer.panX,
|
|
layer.panY,
|
|
layer.zoom
|
|
);
|
|
const dx = pos[0] - this.etkPreviewPanning.startX;
|
|
const dy = pos[1] - this.etkPreviewPanning.startY;
|
|
const nextPanX =
|
|
placement.panRangeX > 0
|
|
? this.etkPreviewPanning.startPanX + dx / (placement.panRangeX / 2)
|
|
: 0;
|
|
const nextPanY =
|
|
placement.panRangeY > 0
|
|
? this.etkPreviewPanning.startPanY + dy / (placement.panRangeY / 2)
|
|
: 0;
|
|
updateActiveLayer(this, keyframe.id, (current) => ({
|
|
...current,
|
|
panX: normalizePan(nextPanX),
|
|
panY: normalizePan(nextPanY),
|
|
}));
|
|
}
|
|
return true;
|
|
}
|
|
if (this.etkTimelineDragging) {
|
|
if (event && event.buttons === 0) {
|
|
releaseDrag(this);
|
|
return true;
|
|
}
|
|
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
|
const draggingId = this.etkTimelineDraggingId || this.properties.etk_ltxv_selected_keyframe_id;
|
|
const index = draggingId
|
|
? keyframes.findIndex((keyframe) => keyframe.id === draggingId)
|
|
: selectedKeyframeIndex(this, keyframes);
|
|
if (index >= 0 && index < keyframes.length) {
|
|
const slot = xToSlot(this, pos[0]);
|
|
if (slotConflict(this, slot, index) < 0) {
|
|
const selected = keyframes[index];
|
|
selected.slot = slot;
|
|
selected.time = slotToSeconds(this, slot);
|
|
keyframes.sort((a, b) => a.time - b.time || a.slot - b.slot);
|
|
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
|
setSelectedKeyframe(this, keyframes.findIndex((keyframe) => keyframe.id === selected.id), keyframes);
|
|
syncTimelineWidget(this);
|
|
}
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
return true;
|
|
}
|
|
return originalMouseMove ? originalMouseMove.apply(this, arguments) : false;
|
|
};
|
|
|
|
const originalMouseWheel = nodeType.prototype.onMouseWheel;
|
|
nodeType.prototype.onMouseWheel = function () {
|
|
const pos = Array.from(arguments).find((arg) => Array.isArray(arg) && arg.length >= 2);
|
|
const event = Array.from(arguments).find((arg) => arg && typeof arg === "object" && !Array.isArray(arg));
|
|
if (zoomPreviewAt(this, event, pos)) {
|
|
if (event && event.preventDefault) event.preventDefault();
|
|
if (event && event.stopPropagation) event.stopPropagation();
|
|
return true;
|
|
}
|
|
return originalMouseWheel ? originalMouseWheel.apply(this, arguments) : false;
|
|
};
|
|
|
|
const originalMouseUp = nodeType.prototype.onMouseUp;
|
|
nodeType.prototype.onMouseUp = function () {
|
|
if (releaseDrag(this)) return true;
|
|
return originalMouseUp ? originalMouseUp.apply(this, arguments) : false;
|
|
};
|
|
},
|
|
});
|