mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Layout change
This commit is contained in:
@@ -38,6 +38,27 @@ export class CanvasRenderer {
|
|||||||
});
|
});
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Helper function to draw rectangle with stroke style
|
||||||
|
* @param ctx Canvas context
|
||||||
|
* @param rect Rectangle bounds {x, y, width, height}
|
||||||
|
* @param options Styling options
|
||||||
|
*/
|
||||||
|
drawStyledRect(ctx, rect, options = {}) {
|
||||||
|
const { strokeStyle = "rgba(255, 255, 255, 0.8)", lineWidth = 2, dashPattern = null } = options;
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = strokeStyle;
|
||||||
|
ctx.lineWidth = lineWidth / this.canvas.viewport.zoom;
|
||||||
|
if (dashPattern) {
|
||||||
|
const scaledDash = dashPattern.map((d) => d / this.canvas.viewport.zoom);
|
||||||
|
ctx.setLineDash(scaledDash);
|
||||||
|
}
|
||||||
|
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
if (dashPattern) {
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
if (this.renderAnimationFrame) {
|
if (this.renderAnimationFrame) {
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
@@ -148,13 +169,11 @@ export class CanvasRenderer {
|
|||||||
const interaction = this.canvas.interaction;
|
const interaction = this.canvas.interaction;
|
||||||
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
||||||
const rect = interaction.canvasResizeRect;
|
const rect = interaction.canvasResizeRect;
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, rect, {
|
||||||
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
|
strokeStyle: 'rgba(0, 255, 0, 0.8)',
|
||||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
lineWidth: 2,
|
||||||
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
dashPattern: [8, 4]
|
||||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.restore();
|
|
||||||
if (rect.width > 0 && rect.height > 0) {
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||||
const textWorldX = rect.x + rect.width / 2;
|
const textWorldX = rect.x + rect.width / 2;
|
||||||
@@ -166,13 +185,11 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
||||||
const rect = interaction.canvasMoveRect;
|
const rect = interaction.canvasMoveRect;
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, rect, {
|
||||||
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
|
strokeStyle: 'rgba(0, 150, 255, 0.8)',
|
||||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
lineWidth: 2,
|
||||||
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
|
dashPattern: [10, 5]
|
||||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.restore();
|
|
||||||
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
|
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
|
||||||
const textWorldX = rect.x + rect.width / 2;
|
const textWorldX = rect.x + rect.width / 2;
|
||||||
const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);
|
const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);
|
||||||
@@ -327,13 +344,11 @@ export class CanvasRenderer {
|
|||||||
width: baseWidth + ext.left + ext.right,
|
width: baseWidth + ext.left + ext.right,
|
||||||
height: baseHeight + ext.top + ext.bottom
|
height: baseHeight + ext.top + ext.bottom
|
||||||
};
|
};
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, previewBounds, {
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; // Yellow color for preview
|
strokeStyle: 'rgba(255, 255, 0, 0.8)',
|
||||||
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
|
lineWidth: 3,
|
||||||
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
dashPattern: [8, 4]
|
||||||
ctx.strokeRect(previewBounds.x, previewBounds.y, previewBounds.width, previewBounds.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
drawPendingGenerationAreas(ctx) {
|
drawPendingGenerationAreas(ctx) {
|
||||||
const areasToDraw = [];
|
const areasToDraw = [];
|
||||||
@@ -354,12 +369,11 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
// 3. Draw all collected areas
|
// 3. Draw all collected areas
|
||||||
areasToDraw.forEach(area => {
|
areasToDraw.forEach(area => {
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, area, {
|
||||||
ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color
|
strokeStyle: 'rgba(0, 150, 255, 0.9)',
|
||||||
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
|
lineWidth: 3,
|
||||||
ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
|
dashPattern: [12, 6]
|
||||||
ctx.strokeRect(area.x, area.y, area.width, area.height);
|
});
|
||||||
ctx.restore();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
drawMaskAreaBounds(ctx) {
|
drawMaskAreaBounds(ctx) {
|
||||||
@@ -375,12 +389,11 @@ export class CanvasRenderer {
|
|||||||
width: maskTool.getMask().width,
|
width: maskTool.getMask().width,
|
||||||
height: maskTool.getMask().height
|
height: maskTool.getMask().height
|
||||||
};
|
};
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, maskBounds, {
|
||||||
ctx.strokeStyle = 'rgba(255, 100, 100, 0.7)'; // Red color for mask area bounds
|
strokeStyle: 'rgba(255, 100, 100, 0.7)',
|
||||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
lineWidth: 2,
|
||||||
ctx.setLineDash([6 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
|
dashPattern: [6, 6]
|
||||||
ctx.strokeRect(maskBounds.x, maskBounds.y, maskBounds.width, maskBounds.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
// Add text label to show this is the mask drawing area
|
// Add text label to show this is the mask drawing area
|
||||||
const textWorldX = maskBounds.x + maskBounds.width / 2;
|
const textWorldX = maskBounds.x + maskBounds.width / 2;
|
||||||
const textWorldY = maskBounds.y - (10 / this.canvas.viewport.zoom);
|
const textWorldY = maskBounds.y - (10 / this.canvas.viewport.zoom);
|
||||||
@@ -389,6 +402,5 @@ export class CanvasRenderer {
|
|||||||
backgroundColor: "rgba(255, 100, 100, 0.8)",
|
backgroundColor: "rgba(255, 100, 100, 0.8)",
|
||||||
padding: 8
|
padding: 8
|
||||||
});
|
});
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
193
js/CanvasView.js
193
js/CanvasView.js
@@ -65,20 +65,14 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
$el("div.painter-button-group", {}, [
|
$el("div.painter-button-group", {}, [
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.icon-button", {
|
||||||
id: `open-editor-btn-${node.id}`,
|
id: `open-editor-btn-${node.id}`,
|
||||||
textContent: "⛶",
|
textContent: "⛶",
|
||||||
title: "Open in Editor",
|
title: "Open in Editor",
|
||||||
style: { minWidth: "40px", maxWidth: "40px", fontWeight: "bold" },
|
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.icon-button", {
|
||||||
textContent: "?",
|
textContent: "?",
|
||||||
title: "Show shortcuts",
|
title: "Show shortcuts",
|
||||||
style: {
|
|
||||||
minWidth: "30px",
|
|
||||||
maxWidth: "30px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
onmouseenter: (e) => {
|
onmouseenter: (e) => {
|
||||||
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
|
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
|
||||||
showTooltip(e.target, content);
|
showTooltip(e.target, content);
|
||||||
@@ -131,38 +125,63 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
canvas.canvasLayers.handlePaste(addMode);
|
canvas.canvasLayers.handlePaste(addMode);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
(() => {
|
||||||
id: `clipboard-toggle-${node.id}`,
|
// Modern clipboard switch
|
||||||
textContent: "📋 System",
|
// Initial state: checked = clipspace, unchecked = system
|
||||||
title: "Toggle clipboard source: System Clipboard",
|
const isClipspace = canvas.canvasLayers.clipboardPreference === 'clipspace';
|
||||||
style: {
|
const switchId = `clipboard-switch-${node.id}`;
|
||||||
minWidth: "100px",
|
const switchEl = $el("label.clipboard-switch", { id: switchId }, [
|
||||||
fontSize: "11px",
|
$el("input", {
|
||||||
backgroundColor: "#4a4a4a"
|
type: "checkbox",
|
||||||
},
|
checked: isClipspace,
|
||||||
onclick: (e) => {
|
onchange: (e) => {
|
||||||
const button = e.target;
|
const checked = e.target.checked;
|
||||||
if (canvas.canvasLayers.clipboardPreference === 'system') {
|
canvas.canvasLayers.clipboardPreference = checked ? 'clipspace' : 'system';
|
||||||
canvas.canvasLayers.clipboardPreference = 'clipspace';
|
// For accessibility, update ARIA label
|
||||||
button.textContent = "📋 Clipspace";
|
switchEl.setAttribute('aria-label', checked ? "Clipboard: Clipspace" : "Clipboard: System");
|
||||||
button.title = "Toggle clipboard source: ComfyUI Clipspace";
|
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
|
||||||
button.style.backgroundColor = "#4a6cd4";
|
}
|
||||||
|
}),
|
||||||
|
$el("span.switch-track"),
|
||||||
|
$el("span.switch-labels", {}, [
|
||||||
|
$el("span.text-clipspace", {}, ["Clipspace"]),
|
||||||
|
$el("span.text-system", {}, ["System"])
|
||||||
|
]),
|
||||||
|
$el("span.switch-knob", {}, [
|
||||||
|
$el("span.switch-icon")
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
// Tooltip logic
|
||||||
|
switchEl.addEventListener("mouseenter", (e) => {
|
||||||
|
const checked = switchEl.querySelector('input[type="checkbox"]').checked;
|
||||||
|
const tooltipContent = checked ? clipspaceClipboardTooltip : systemClipboardTooltip;
|
||||||
|
showTooltip(switchEl, tooltipContent);
|
||||||
|
});
|
||||||
|
switchEl.addEventListener("mouseleave", hideTooltip);
|
||||||
|
// Dynamic icon and text update on toggle
|
||||||
|
const input = switchEl.querySelector('input[type="checkbox"]');
|
||||||
|
const knobIcon = switchEl.querySelector('.switch-knob .switch-icon');
|
||||||
|
const updateSwitchView = (isClipspace) => {
|
||||||
|
const iconTool = isClipspace ? LAYERFORGE_TOOLS.CLIPSPACE : LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD;
|
||||||
|
const icon = iconLoader.getIcon(iconTool);
|
||||||
|
if (icon instanceof HTMLImageElement) {
|
||||||
|
knobIcon.innerHTML = '';
|
||||||
|
const clonedIcon = icon.cloneNode();
|
||||||
|
clonedIcon.style.width = '20px';
|
||||||
|
clonedIcon.style.height = '20px';
|
||||||
|
knobIcon.appendChild(clonedIcon);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
canvas.canvasLayers.clipboardPreference = 'system';
|
knobIcon.textContent = isClipspace ? "🗂️" : "📋";
|
||||||
button.textContent = "📋 System";
|
|
||||||
button.title = "Toggle clipboard source: System Clipboard";
|
|
||||||
button.style.backgroundColor = "#4a4a4a";
|
|
||||||
}
|
}
|
||||||
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
|
};
|
||||||
},
|
input.addEventListener('change', () => updateSwitchView(input.checked));
|
||||||
onmouseenter: (e) => {
|
// Initial state
|
||||||
const currentPreference = canvas.canvasLayers.clipboardPreference;
|
iconLoader.preloadToolIcons().then(() => {
|
||||||
const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip;
|
updateSwitchView(isClipspace);
|
||||||
showTooltip(e.target, tooltipContent);
|
});
|
||||||
},
|
return switchEl;
|
||||||
onmouseleave: hideTooltip
|
})()
|
||||||
})
|
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
@@ -440,8 +459,15 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
min: "1",
|
min: "1",
|
||||||
max: "200",
|
max: "200",
|
||||||
value: "20",
|
value: "20",
|
||||||
oninput: (e) => canvas.maskTool.setBrushSize(parseInt(e.target.value))
|
oninput: (e) => {
|
||||||
})
|
const value = e.target.value;
|
||||||
|
canvas.maskTool.setBrushSize(parseInt(value));
|
||||||
|
const valueEl = document.getElementById('brush-size-value');
|
||||||
|
if (valueEl)
|
||||||
|
valueEl.textContent = `${value}px`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("div.slider-value", { id: "brush-size-value" }, ["20px"])
|
||||||
]),
|
]),
|
||||||
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
||||||
$el("label", { for: "brush-strength-slider", textContent: "Strength:" }),
|
$el("label", { for: "brush-strength-slider", textContent: "Strength:" }),
|
||||||
@@ -452,8 +478,15 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
max: "1",
|
max: "1",
|
||||||
step: "0.05",
|
step: "0.05",
|
||||||
value: "0.5",
|
value: "0.5",
|
||||||
oninput: (e) => canvas.maskTool.setBrushStrength(parseFloat(e.target.value))
|
oninput: (e) => {
|
||||||
})
|
const value = e.target.value;
|
||||||
|
canvas.maskTool.setBrushStrength(parseFloat(value));
|
||||||
|
const valueEl = document.getElementById('brush-strength-value');
|
||||||
|
if (valueEl)
|
||||||
|
valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("div.slider-value", { id: "brush-strength-value" }, ["50%"])
|
||||||
]),
|
]),
|
||||||
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
||||||
$el("label", { for: "brush-hardness-slider", textContent: "Hardness:" }),
|
$el("label", { for: "brush-hardness-slider", textContent: "Hardness:" }),
|
||||||
@@ -464,8 +497,15 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
max: "1",
|
max: "1",
|
||||||
step: "0.05",
|
step: "0.05",
|
||||||
value: "0.5",
|
value: "0.5",
|
||||||
oninput: (e) => canvas.maskTool.setBrushHardness(parseFloat(e.target.value))
|
oninput: (e) => {
|
||||||
})
|
const value = e.target.value;
|
||||||
|
canvas.maskTool.setBrushHardness(parseFloat(value));
|
||||||
|
const valueEl = document.getElementById('brush-hardness-value');
|
||||||
|
if (valueEl)
|
||||||
|
valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("div.slider-value", { id: "brush-hardness-value" }, ["50%"])
|
||||||
]),
|
]),
|
||||||
$el("button.painter-button.mask-control", {
|
$el("button.painter-button.mask-control", {
|
||||||
textContent: "Clear Mask",
|
textContent: "Clear Mask",
|
||||||
@@ -481,10 +521,9 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
]),
|
]),
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
$el("div.painter-button-group", {}, [
|
$el("div.painter-button-group", {}, [
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.success", {
|
||||||
textContent: "Run GC",
|
textContent: "Run GC",
|
||||||
title: "Run Garbage Collection to clean unused images",
|
title: "Run Garbage Collection to clean unused images",
|
||||||
style: { backgroundColor: "#4a7c59", borderColor: "#3a6c49" },
|
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
try {
|
try {
|
||||||
const stats = canvas.imageReferenceManager.getStats();
|
const stats = canvas.imageReferenceManager.getStats();
|
||||||
@@ -500,10 +539,9 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.danger", {
|
||||||
textContent: "Clear Cache",
|
textContent: "Clear Cache",
|
||||||
title: "Clear all saved canvas states from browser storage",
|
title: "Clear all saved canvas states from browser storage",
|
||||||
style: { backgroundColor: "#c54747", borderColor: "#a53737" },
|
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
|
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
|
||||||
try {
|
try {
|
||||||
@@ -736,36 +774,33 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
let backdrop = null;
|
let backdrop = null;
|
||||||
let originalParent = null;
|
let originalParent = null;
|
||||||
let isEditorOpen = false;
|
let isEditorOpen = false;
|
||||||
|
let viewportAdjustment = { x: 0, y: 0 };
|
||||||
/**
|
/**
|
||||||
* Adjusts the viewport to keep the content centered when the container size changes.
|
* Adjusts the viewport when entering fullscreen mode.
|
||||||
* @param rectA The original rectangle.
|
|
||||||
* @param rectB The new rectangle.
|
|
||||||
* @param direction Determines whether to apply the adjustment for opening (-1) or closing (1).
|
|
||||||
*/
|
*/
|
||||||
const adjustViewportForCentering = (rectA, rectB, direction) => {
|
const adjustViewportOnOpen = (originalRect) => {
|
||||||
if (!rectA || !rectB)
|
const fullscreenRect = canvasContainer.getBoundingClientRect();
|
||||||
return;
|
const widthDiff = fullscreenRect.width - originalRect.width;
|
||||||
const widthDiff = rectB.width - rectA.width;
|
const heightDiff = fullscreenRect.height - originalRect.height;
|
||||||
const heightDiff = rectB.height - rectA.height;
|
|
||||||
const adjustX = (widthDiff / 2) / canvas.viewport.zoom;
|
const adjustX = (widthDiff / 2) / canvas.viewport.zoom;
|
||||||
const adjustY = (heightDiff / 2) / canvas.viewport.zoom;
|
const adjustY = (heightDiff / 2) / canvas.viewport.zoom;
|
||||||
canvas.viewport.x -= adjustX * direction;
|
// Store the adjustment
|
||||||
canvas.viewport.y -= adjustY * direction;
|
viewportAdjustment = { x: adjustX, y: adjustY };
|
||||||
const action = direction === 1 ? 'OPENING' : 'CLOSING';
|
// Apply the adjustment
|
||||||
log.info(`FULLSCREEN ${action} - Viewport adjusted for centering:`, {
|
canvas.viewport.x -= viewportAdjustment.x;
|
||||||
widthDiff, heightDiff, adjustX, adjustY,
|
canvas.viewport.y -= viewportAdjustment.y;
|
||||||
viewport_after: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
|
};
|
||||||
});
|
/**
|
||||||
|
* Restores the viewport when exiting fullscreen mode.
|
||||||
|
*/
|
||||||
|
const adjustViewportOnClose = () => {
|
||||||
|
// Apply the stored adjustment in reverse
|
||||||
|
canvas.viewport.x += viewportAdjustment.x;
|
||||||
|
canvas.viewport.y += viewportAdjustment.y;
|
||||||
|
// Reset adjustment
|
||||||
|
viewportAdjustment = { x: 0, y: 0 };
|
||||||
};
|
};
|
||||||
const closeEditor = () => {
|
const closeEditor = () => {
|
||||||
// Get fullscreen rect BEFORE removing from DOM
|
|
||||||
const fullscreenRect = backdrop?.querySelector('.painter-modal-content')?.getBoundingClientRect();
|
|
||||||
const currentRect = originalParent?.getBoundingClientRect();
|
|
||||||
log.info(`FULLSCREEN CLOSING - Window sizes:`, {
|
|
||||||
current: { width: currentRect?.width, height: currentRect?.height },
|
|
||||||
fullscreen: { width: fullscreenRect?.width, height: fullscreenRect?.height },
|
|
||||||
viewport_before: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
|
|
||||||
});
|
|
||||||
if (originalParent && backdrop) {
|
if (originalParent && backdrop) {
|
||||||
originalParent.appendChild(mainContainer);
|
originalParent.appendChild(mainContainer);
|
||||||
document.body.removeChild(backdrop);
|
document.body.removeChild(backdrop);
|
||||||
@@ -776,12 +811,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
// Remove ESC key listener when editor closes
|
// Remove ESC key listener when editor closes
|
||||||
document.removeEventListener('keydown', handleEscKey);
|
document.removeEventListener('keydown', handleEscKey);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Use the actual canvas container for centering calculation
|
adjustViewportOnClose();
|
||||||
const currentCanvasContainer = originalParent.querySelector('.painterCanvasContainer.painter-container');
|
|
||||||
const fullscreenCanvasContainer = backdrop.querySelector('.painterCanvasContainer.painter-container');
|
|
||||||
const currentRect = currentCanvasContainer.getBoundingClientRect();
|
|
||||||
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
|
|
||||||
adjustViewportForCentering(currentRect, fullscreenRect, -1);
|
|
||||||
canvas.render();
|
canvas.render();
|
||||||
if (node.onResize) {
|
if (node.onResize) {
|
||||||
node.onResize();
|
node.onResize();
|
||||||
@@ -794,7 +824,6 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeEditor();
|
closeEditor();
|
||||||
log.info("Fullscreen editor closed via ESC key");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
openEditorBtn.onclick = () => {
|
openEditorBtn.onclick = () => {
|
||||||
@@ -802,6 +831,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
closeEditor();
|
closeEditor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const originalRect = canvasContainer.getBoundingClientRect();
|
||||||
originalParent = mainContainer.parentElement;
|
originalParent = mainContainer.parentElement;
|
||||||
if (!originalParent) {
|
if (!originalParent) {
|
||||||
log.error("Could not find original parent of the canvas container!");
|
log.error("Could not find original parent of the canvas container!");
|
||||||
@@ -818,12 +848,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
// Add ESC key listener when editor opens
|
// Add ESC key listener when editor opens
|
||||||
document.addEventListener('keydown', handleEscKey);
|
document.addEventListener('keydown', handleEscKey);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Use the actual canvas container for centering calculation
|
adjustViewportOnOpen(originalRect);
|
||||||
const originalCanvasContainer = originalParent.querySelector('.painterCanvasContainer.painter-container');
|
|
||||||
const fullscreenCanvasContainer = modalContent.querySelector('.painterCanvasContainer.painter-container');
|
|
||||||
const originalRect = originalCanvasContainer.getBoundingClientRect();
|
|
||||||
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
|
|
||||||
adjustViewportForCentering(originalRect, fullscreenRect, 1);
|
|
||||||
canvas.render();
|
canvas.render();
|
||||||
if (node.onResize) {
|
if (node.onResize) {
|
||||||
node.onResize();
|
node.onResize();
|
||||||
|
|||||||
@@ -1,54 +1,96 @@
|
|||||||
.painter-button {
|
.painter-button {
|
||||||
background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
|
background-color: #444;
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 5px;
|
||||||
color: #ffffff;
|
color: #e0e0e0;
|
||||||
padding: 6px 12px;
|
padding: 6px 14px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease-in-out;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button:hover {
|
.painter-button:hover {
|
||||||
background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
|
background-color: #555;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
border-color: #666;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button:active {
|
.painter-button:active {
|
||||||
background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
|
background-color: #3a3a3a;
|
||||||
transform: translateY(1px);
|
transform: translateY(0);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button:disabled,
|
.painter-button:disabled,
|
||||||
.painter-button:disabled:hover {
|
.painter-button:disabled:hover {
|
||||||
background: #555;
|
background-color: #3a3a3a;
|
||||||
color: #888;
|
color: #777;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-color: #444;
|
border-color: #4a4a4a;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button.primary {
|
.painter-button.primary {
|
||||||
background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
|
background-color: #3a76d6;
|
||||||
border-color: #2a4cb4;
|
border-color: #2a6ac4;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button.primary:hover {
|
.painter-button.primary:hover {
|
||||||
background: linear-gradient(to bottom, #5a7ce4, #4a6cd4);
|
background-color: #4a86e4;
|
||||||
|
border-color: #3a76d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.success {
|
||||||
|
background-color: #4a7c59;
|
||||||
|
border-color: #3a6c49;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.success:hover {
|
||||||
|
background-color: #5a8c69;
|
||||||
|
border-color: #4a7c59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.danger {
|
||||||
|
background-color: #c54747;
|
||||||
|
border-color: #a53737;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.danger:hover {
|
||||||
|
background-color: #d55757;
|
||||||
|
border-color: #c54747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.icon-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 30px; /* Match height */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-controls {
|
.painter-controls {
|
||||||
background: linear-gradient(to bottom, #404040, #383838);
|
background-color: #2f2f2f;
|
||||||
border-bottom: 1px solid #2a2a2a;
|
border-bottom: 1px solid #202020;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
padding: 8px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -56,57 +98,198 @@
|
|||||||
|
|
||||||
.painter-slider-container {
|
.painter-slider-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-slider-container input[type="range"] {
|
.painter-slider-container input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
|
height: 4px;
|
||||||
|
background: #555;
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.painter-slider-container input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #555;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-slider-container input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-slider-container input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #bbb;
|
||||||
|
margin-top: 2px;
|
||||||
|
min-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.painter-button-group {
|
.painter-button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
background-color: rgba(0,0,0,0.2);
|
background-color: transparent;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-clipboard-group {
|
.painter-clipboard-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 4px;
|
||||||
background-color: rgba(0,0,0,0.15);
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.painter-clipboard-group::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 20px;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(74, 108, 212, 0.6), transparent);
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-clipboard-group .painter-button {
|
.painter-clipboard-group .painter-button {
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
height: 30px; /* Match switch height */
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Clipboard Switch Modern --- */
|
||||||
|
.clipboard-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 90px;
|
||||||
|
height: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease-in-out;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.clipboard-switch:hover {
|
||||||
|
background-color: #555;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
.clipboard-switch:active {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-track {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #5a5a5a;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-labels {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e0e0e0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.clipboard-switch .switch-labels .text-clipspace,
|
||||||
|
.clipboard-switch .switch-labels .text-system {
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.clipboard-switch .switch-labels .text-clipspace { opacity: 0; }
|
||||||
|
.clipboard-switch .switch-labels .text-system { opacity: 1; padding-left: 20px; }
|
||||||
|
|
||||||
|
.clipboard-switch .switch-knob .switch-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-knob .switch-icon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checked state */
|
||||||
|
.clipboard-switch:has(input:checked) {
|
||||||
|
background-color: #3a76d6;
|
||||||
|
border-color: #2a6ac4;
|
||||||
|
}
|
||||||
|
.clipboard-switch:has(input:checked):hover {
|
||||||
|
background-color: #4a86e4;
|
||||||
|
border-color: #3a76d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch input:checked ~ .switch-knob {
|
||||||
|
left: calc(100% - 26px);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.clipboard-switch input:checked ~ .switch-knob .switch-icon img {
|
||||||
|
filter: invert(35%) sepia(100%) saturate(1500%) hue-rotate(200deg) brightness(90%) contrast(100%);
|
||||||
|
}
|
||||||
|
.clipboard-switch input:checked ~ .switch-labels .text-clipspace {
|
||||||
|
opacity: 1;
|
||||||
|
color: #fff;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.clipboard-switch input:checked ~ .switch-labels .text-system {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.painter-separator {
|
.painter-separator {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 28px;
|
height: 24px;
|
||||||
background-color: #2a2a2a;
|
background-color: #444;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,16 @@ export const LAYERFORGE_TOOLS = {
|
|||||||
BRUSH: 'brush',
|
BRUSH: 'brush',
|
||||||
ERASER: 'eraser',
|
ERASER: 'eraser',
|
||||||
SHAPE: 'shape',
|
SHAPE: 'shape',
|
||||||
SETTINGS: 'settings'
|
SETTINGS: 'settings',
|
||||||
|
SYSTEM_CLIPBOARD: 'system_clipboard',
|
||||||
|
CLIPSPACE: 'clipspace',
|
||||||
};
|
};
|
||||||
// SVG Icons for LayerForge tools
|
// SVG Icons for LayerForge tools
|
||||||
|
const SYSTEM_CLIPBOARD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 15H7v-2h10v2zm0-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>`;
|
||||||
|
const CLIPSPACE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M17 7H7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm0 12H7V9h10v10z"/><path d="M19 3H9c-1.1 0-2 .9-2 2v2h2V5h10v10h-2v2h2c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`;
|
||||||
const LAYERFORGE_TOOL_ICONS = {
|
const LAYERFORGE_TOOL_ICONS = {
|
||||||
|
[LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SYSTEM_CLIPBOARD_ICON_SVG)}`,
|
||||||
|
[LAYERFORGE_TOOLS.CLIPSPACE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CLIPSPACE_ICON_SVG)}`,
|
||||||
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
|
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
|
||||||
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
|
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
|
||||||
[LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z"/></svg>')}`,
|
[LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z"/></svg>')}`,
|
||||||
|
|||||||
@@ -59,6 +59,37 @@ export class CanvasRenderer {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to draw rectangle with stroke style
|
||||||
|
* @param ctx Canvas context
|
||||||
|
* @param rect Rectangle bounds {x, y, width, height}
|
||||||
|
* @param options Styling options
|
||||||
|
*/
|
||||||
|
drawStyledRect(ctx: any, rect: any, options: any = {}) {
|
||||||
|
const {
|
||||||
|
strokeStyle = "rgba(255, 255, 255, 0.8)",
|
||||||
|
lineWidth = 2,
|
||||||
|
dashPattern = null
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = strokeStyle;
|
||||||
|
ctx.lineWidth = lineWidth / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
if (dashPattern) {
|
||||||
|
const scaledDash = dashPattern.map((d: number) => d / this.canvas.viewport.zoom);
|
||||||
|
ctx.setLineDash(scaledDash);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
|
||||||
|
if (dashPattern) {
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.renderAnimationFrame) {
|
if (this.renderAnimationFrame) {
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
@@ -187,13 +218,11 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
||||||
const rect = interaction.canvasResizeRect;
|
const rect = interaction.canvasResizeRect;
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, rect, {
|
||||||
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
|
strokeStyle: 'rgba(0, 255, 0, 0.8)',
|
||||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
lineWidth: 2,
|
||||||
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
dashPattern: [8, 4]
|
||||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.restore();
|
|
||||||
if (rect.width > 0 && rect.height > 0) {
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||||
const textWorldX = rect.x + rect.width / 2;
|
const textWorldX = rect.x + rect.width / 2;
|
||||||
@@ -207,13 +236,11 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
||||||
const rect = interaction.canvasMoveRect;
|
const rect = interaction.canvasMoveRect;
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, rect, {
|
||||||
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
|
strokeStyle: 'rgba(0, 150, 255, 0.8)',
|
||||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
lineWidth: 2,
|
||||||
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
|
dashPattern: [10, 5]
|
||||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
|
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
|
||||||
const textWorldX = rect.x + rect.width / 2;
|
const textWorldX = rect.x + rect.width / 2;
|
||||||
@@ -397,13 +424,11 @@ export class CanvasRenderer {
|
|||||||
height: baseHeight + ext.top + ext.bottom
|
height: baseHeight + ext.top + ext.bottom
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, previewBounds, {
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; // Yellow color for preview
|
strokeStyle: 'rgba(255, 255, 0, 0.8)',
|
||||||
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
|
lineWidth: 3,
|
||||||
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
dashPattern: [8, 4]
|
||||||
ctx.strokeRect(previewBounds.x, previewBounds.y, previewBounds.width, previewBounds.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPendingGenerationAreas(ctx: any) {
|
drawPendingGenerationAreas(ctx: any) {
|
||||||
@@ -429,12 +454,11 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
// 3. Draw all collected areas
|
// 3. Draw all collected areas
|
||||||
areasToDraw.forEach(area => {
|
areasToDraw.forEach(area => {
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, area, {
|
||||||
ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color
|
strokeStyle: 'rgba(0, 150, 255, 0.9)',
|
||||||
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
|
lineWidth: 3,
|
||||||
ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
|
dashPattern: [12, 6]
|
||||||
ctx.strokeRect(area.x, area.y, area.width, area.height);
|
});
|
||||||
ctx.restore();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,12 +478,11 @@ export class CanvasRenderer {
|
|||||||
height: maskTool.getMask().height
|
height: maskTool.getMask().height
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.save();
|
this.drawStyledRect(ctx, maskBounds, {
|
||||||
ctx.strokeStyle = 'rgba(255, 100, 100, 0.7)'; // Red color for mask area bounds
|
strokeStyle: 'rgba(255, 100, 100, 0.7)',
|
||||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
lineWidth: 2,
|
||||||
ctx.setLineDash([6 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
|
dashPattern: [6, 6]
|
||||||
ctx.strokeRect(maskBounds.x, maskBounds.y, maskBounds.width, maskBounds.height);
|
});
|
||||||
ctx.setLineDash([]);
|
|
||||||
|
|
||||||
// Add text label to show this is the mask drawing area
|
// Add text label to show this is the mask drawing area
|
||||||
const textWorldX = maskBounds.x + maskBounds.width / 2;
|
const textWorldX = maskBounds.x + maskBounds.width / 2;
|
||||||
@@ -470,7 +493,5 @@ export class CanvasRenderer {
|
|||||||
backgroundColor: "rgba(255, 100, 100, 0.8)",
|
backgroundColor: "rgba(255, 100, 100, 0.8)",
|
||||||
padding: 8
|
padding: 8
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,20 +90,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
$el("div.painter-button-group", {}, [
|
$el("div.painter-button-group", {}, [
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.icon-button", {
|
||||||
id: `open-editor-btn-${node.id}`,
|
id: `open-editor-btn-${node.id}`,
|
||||||
textContent: "⛶",
|
textContent: "⛶",
|
||||||
title: "Open in Editor",
|
title: "Open in Editor",
|
||||||
style: {minWidth: "40px", maxWidth: "40px", fontWeight: "bold"},
|
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.icon-button", {
|
||||||
textContent: "?",
|
textContent: "?",
|
||||||
title: "Show shortcuts",
|
title: "Show shortcuts",
|
||||||
style: {
|
|
||||||
minWidth: "30px",
|
|
||||||
maxWidth: "30px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
onmouseenter: (e: MouseEvent) => {
|
onmouseenter: (e: MouseEvent) => {
|
||||||
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
|
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
|
||||||
showTooltip(e.target as HTMLElement, content);
|
showTooltip(e.target as HTMLElement, content);
|
||||||
@@ -155,37 +149,68 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
canvas.canvasLayers.handlePaste(addMode);
|
canvas.canvasLayers.handlePaste(addMode);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
(() => {
|
||||||
id: `clipboard-toggle-${node.id}`,
|
// Modern clipboard switch
|
||||||
textContent: "📋 System",
|
// Initial state: checked = clipspace, unchecked = system
|
||||||
title: "Toggle clipboard source: System Clipboard",
|
const isClipspace = canvas.canvasLayers.clipboardPreference === 'clipspace';
|
||||||
style: {
|
const switchId = `clipboard-switch-${node.id}`;
|
||||||
minWidth: "100px",
|
const switchEl = $el("label.clipboard-switch", { id: switchId }, [
|
||||||
fontSize: "11px",
|
$el("input", {
|
||||||
backgroundColor: "#4a4a4a"
|
type: "checkbox",
|
||||||
},
|
checked: isClipspace,
|
||||||
onclick: (e: MouseEvent) => {
|
onchange: (e: Event) => {
|
||||||
const button = e.target as HTMLButtonElement;
|
const checked = (e.target as HTMLInputElement).checked;
|
||||||
if (canvas.canvasLayers.clipboardPreference === 'system') {
|
canvas.canvasLayers.clipboardPreference = checked ? 'clipspace' : 'system';
|
||||||
canvas.canvasLayers.clipboardPreference = 'clipspace';
|
// For accessibility, update ARIA label
|
||||||
button.textContent = "📋 Clipspace";
|
switchEl.setAttribute('aria-label', checked ? "Clipboard: Clipspace" : "Clipboard: System");
|
||||||
button.title = "Toggle clipboard source: ComfyUI Clipspace";
|
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
|
||||||
button.style.backgroundColor = "#4a6cd4";
|
}
|
||||||
} else {
|
}),
|
||||||
canvas.canvasLayers.clipboardPreference = 'system';
|
$el("span.switch-track"),
|
||||||
button.textContent = "📋 System";
|
$el("span.switch-labels", {}, [
|
||||||
button.title = "Toggle clipboard source: System Clipboard";
|
$el("span.text-clipspace", {}, ["Clipspace"]),
|
||||||
button.style.backgroundColor = "#4a4a4a";
|
$el("span.text-system", {}, ["System"])
|
||||||
}
|
]),
|
||||||
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
|
$el("span.switch-knob", {}, [
|
||||||
},
|
$el("span.switch-icon")
|
||||||
onmouseenter: (e: MouseEvent) => {
|
])
|
||||||
const currentPreference = canvas.canvasLayers.clipboardPreference;
|
]);
|
||||||
const tooltipContent = currentPreference === 'system' ? systemClipboardTooltip : clipspaceClipboardTooltip;
|
|
||||||
showTooltip(e.target as HTMLElement, tooltipContent);
|
// Tooltip logic
|
||||||
},
|
switchEl.addEventListener("mouseenter", (e: MouseEvent) => {
|
||||||
onmouseleave: hideTooltip
|
const checked = (switchEl.querySelector('input[type="checkbox"]') as HTMLInputElement).checked;
|
||||||
})
|
const tooltipContent = checked ? clipspaceClipboardTooltip : systemClipboardTooltip;
|
||||||
|
showTooltip(switchEl, tooltipContent);
|
||||||
|
});
|
||||||
|
switchEl.addEventListener("mouseleave", hideTooltip);
|
||||||
|
|
||||||
|
// Dynamic icon and text update on toggle
|
||||||
|
const input = switchEl.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||||
|
const knobIcon = switchEl.querySelector('.switch-knob .switch-icon') as HTMLElement;
|
||||||
|
|
||||||
|
const updateSwitchView = (isClipspace: boolean) => {
|
||||||
|
const iconTool = isClipspace ? LAYERFORGE_TOOLS.CLIPSPACE : LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD;
|
||||||
|
const icon = iconLoader.getIcon(iconTool);
|
||||||
|
if (icon instanceof HTMLImageElement) {
|
||||||
|
knobIcon.innerHTML = '';
|
||||||
|
const clonedIcon = icon.cloneNode() as HTMLImageElement;
|
||||||
|
clonedIcon.style.width = '20px';
|
||||||
|
clonedIcon.style.height = '20px';
|
||||||
|
knobIcon.appendChild(clonedIcon);
|
||||||
|
} else {
|
||||||
|
knobIcon.textContent = isClipspace ? "🗂️" : "📋";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener('change', () => updateSwitchView(input.checked));
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
iconLoader.preloadToolIcons().then(() => {
|
||||||
|
updateSwitchView(isClipspace);
|
||||||
|
});
|
||||||
|
|
||||||
|
return switchEl;
|
||||||
|
})()
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@@ -477,8 +502,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
min: "1",
|
min: "1",
|
||||||
max: "200",
|
max: "200",
|
||||||
value: "20",
|
value: "20",
|
||||||
oninput: (e: Event) => canvas.maskTool.setBrushSize(parseInt((e.target as HTMLInputElement).value))
|
oninput: (e: Event) => {
|
||||||
})
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
canvas.maskTool.setBrushSize(parseInt(value));
|
||||||
|
const valueEl = document.getElementById('brush-size-value');
|
||||||
|
if (valueEl) valueEl.textContent = `${value}px`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("div.slider-value", {id: "brush-size-value"}, ["20px"])
|
||||||
]),
|
]),
|
||||||
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
|
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
|
||||||
$el("label", {for: "brush-strength-slider", textContent: "Strength:"}),
|
$el("label", {for: "brush-strength-slider", textContent: "Strength:"}),
|
||||||
@@ -489,8 +520,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
max: "1",
|
max: "1",
|
||||||
step: "0.05",
|
step: "0.05",
|
||||||
value: "0.5",
|
value: "0.5",
|
||||||
oninput: (e: Event) => canvas.maskTool.setBrushStrength(parseFloat((e.target as HTMLInputElement).value))
|
oninput: (e: Event) => {
|
||||||
})
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
canvas.maskTool.setBrushStrength(parseFloat(value));
|
||||||
|
const valueEl = document.getElementById('brush-strength-value');
|
||||||
|
if (valueEl) valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("div.slider-value", {id: "brush-strength-value"}, ["50%"])
|
||||||
]),
|
]),
|
||||||
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
|
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
|
||||||
$el("label", {for: "brush-hardness-slider", textContent: "Hardness:"}),
|
$el("label", {for: "brush-hardness-slider", textContent: "Hardness:"}),
|
||||||
@@ -501,8 +538,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
max: "1",
|
max: "1",
|
||||||
step: "0.05",
|
step: "0.05",
|
||||||
value: "0.5",
|
value: "0.5",
|
||||||
oninput: (e: Event) => canvas.maskTool.setBrushHardness(parseFloat((e.target as HTMLInputElement).value))
|
oninput: (e: Event) => {
|
||||||
})
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
canvas.maskTool.setBrushHardness(parseFloat(value));
|
||||||
|
const valueEl = document.getElementById('brush-hardness-value');
|
||||||
|
if (valueEl) valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("div.slider-value", {id: "brush-hardness-value"}, ["50%"])
|
||||||
]),
|
]),
|
||||||
$el("button.painter-button.mask-control", {
|
$el("button.painter-button.mask-control", {
|
||||||
textContent: "Clear Mask",
|
textContent: "Clear Mask",
|
||||||
@@ -519,10 +562,9 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
|
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
$el("div.painter-button-group", {}, [
|
$el("div.painter-button-group", {}, [
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.success", {
|
||||||
textContent: "Run GC",
|
textContent: "Run GC",
|
||||||
title: "Run Garbage Collection to clean unused images",
|
title: "Run Garbage Collection to clean unused images",
|
||||||
style: {backgroundColor: "#4a7c59", borderColor: "#3a6c49"},
|
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
try {
|
try {
|
||||||
const stats = canvas.imageReferenceManager.getStats();
|
const stats = canvas.imageReferenceManager.getStats();
|
||||||
@@ -540,10 +582,9 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button.danger", {
|
||||||
textContent: "Clear Cache",
|
textContent: "Clear Cache",
|
||||||
title: "Clear all saved canvas states from browser storage",
|
title: "Clear all saved canvas states from browser storage",
|
||||||
style: {backgroundColor: "#c54747", borderColor: "#a53737"},
|
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
|
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
|
||||||
try {
|
try {
|
||||||
@@ -796,43 +837,41 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
let backdrop: HTMLDivElement | null = null;
|
let backdrop: HTMLDivElement | null = null;
|
||||||
let originalParent: HTMLElement | null = null;
|
let originalParent: HTMLElement | null = null;
|
||||||
let isEditorOpen = false;
|
let isEditorOpen = false;
|
||||||
|
let viewportAdjustment = { x: 0, y: 0 };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjusts the viewport to keep the content centered when the container size changes.
|
* Adjusts the viewport when entering fullscreen mode.
|
||||||
* @param rectA The original rectangle.
|
|
||||||
* @param rectB The new rectangle.
|
|
||||||
* @param direction Determines whether to apply the adjustment for opening (-1) or closing (1).
|
|
||||||
*/
|
*/
|
||||||
const adjustViewportForCentering = (rectA: DOMRect, rectB: DOMRect, direction: 1 | -1) => {
|
const adjustViewportOnOpen = (originalRect: DOMRect) => {
|
||||||
if (!rectA || !rectB) return;
|
const fullscreenRect = canvasContainer.getBoundingClientRect();
|
||||||
|
|
||||||
|
const widthDiff = fullscreenRect.width - originalRect.width;
|
||||||
|
const heightDiff = fullscreenRect.height - originalRect.height;
|
||||||
|
|
||||||
const widthDiff = rectB.width - rectA.width;
|
|
||||||
const heightDiff = rectB.height - rectA.height;
|
|
||||||
|
|
||||||
const adjustX = (widthDiff / 2) / canvas.viewport.zoom;
|
const adjustX = (widthDiff / 2) / canvas.viewport.zoom;
|
||||||
const adjustY = (heightDiff / 2) / canvas.viewport.zoom;
|
const adjustY = (heightDiff / 2) / canvas.viewport.zoom;
|
||||||
|
|
||||||
canvas.viewport.x -= adjustX * direction;
|
|
||||||
canvas.viewport.y -= adjustY * direction;
|
|
||||||
|
|
||||||
const action = direction === 1 ? 'OPENING' : 'CLOSING';
|
// Store the adjustment
|
||||||
log.info(`FULLSCREEN ${action} - Viewport adjusted for centering:`, {
|
viewportAdjustment = { x: adjustX, y: adjustY };
|
||||||
widthDiff, heightDiff, adjustX, adjustY,
|
|
||||||
viewport_after: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
|
// Apply the adjustment
|
||||||
});
|
canvas.viewport.x -= viewportAdjustment.x;
|
||||||
}
|
canvas.viewport.y -= viewportAdjustment.y;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the viewport when exiting fullscreen mode.
|
||||||
|
*/
|
||||||
|
const adjustViewportOnClose = () => {
|
||||||
|
// Apply the stored adjustment in reverse
|
||||||
|
canvas.viewport.x += viewportAdjustment.x;
|
||||||
|
canvas.viewport.y += viewportAdjustment.y;
|
||||||
|
|
||||||
|
// Reset adjustment
|
||||||
|
viewportAdjustment = { x: 0, y: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
const closeEditor = () => {
|
const closeEditor = () => {
|
||||||
// Get fullscreen rect BEFORE removing from DOM
|
|
||||||
const fullscreenRect = backdrop?.querySelector('.painter-modal-content')?.getBoundingClientRect();
|
|
||||||
const currentRect = originalParent?.getBoundingClientRect();
|
|
||||||
|
|
||||||
log.info(`FULLSCREEN CLOSING - Window sizes:`, {
|
|
||||||
current: { width: currentRect?.width, height: currentRect?.height },
|
|
||||||
fullscreen: { width: fullscreenRect?.width, height: fullscreenRect?.height },
|
|
||||||
viewport_before: { x: canvas.viewport.x, y: canvas.viewport.y, zoom: canvas.viewport.zoom }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (originalParent && backdrop) {
|
if (originalParent && backdrop) {
|
||||||
originalParent.appendChild(mainContainer);
|
originalParent.appendChild(mainContainer);
|
||||||
document.body.removeChild(backdrop);
|
document.body.removeChild(backdrop);
|
||||||
@@ -846,12 +885,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
document.removeEventListener('keydown', handleEscKey);
|
document.removeEventListener('keydown', handleEscKey);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Use the actual canvas container for centering calculation
|
adjustViewportOnClose();
|
||||||
const currentCanvasContainer = originalParent!.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
|
|
||||||
const fullscreenCanvasContainer = backdrop!.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
|
|
||||||
const currentRect = currentCanvasContainer.getBoundingClientRect();
|
|
||||||
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
|
|
||||||
adjustViewportForCentering(currentRect, fullscreenRect, -1);
|
|
||||||
canvas.render();
|
canvas.render();
|
||||||
if (node.onResize) {
|
if (node.onResize) {
|
||||||
node.onResize();
|
node.onResize();
|
||||||
@@ -865,7 +899,6 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeEditor();
|
closeEditor();
|
||||||
log.info("Fullscreen editor closed via ESC key");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -875,13 +908,14 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalRect = canvasContainer.getBoundingClientRect();
|
||||||
|
|
||||||
originalParent = mainContainer.parentElement;
|
originalParent = mainContainer.parentElement;
|
||||||
if (!originalParent) {
|
if (!originalParent) {
|
||||||
log.error("Could not find original parent of the canvas container!");
|
log.error("Could not find original parent of the canvas container!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
backdrop = $el("div.painter-modal-backdrop") as HTMLDivElement;
|
backdrop = $el("div.painter-modal-backdrop") as HTMLDivElement;
|
||||||
const modalContent = $el("div.painter-modal-content") as HTMLDivElement;
|
const modalContent = $el("div.painter-modal-content") as HTMLDivElement;
|
||||||
|
|
||||||
@@ -897,12 +931,8 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
document.addEventListener('keydown', handleEscKey);
|
document.addEventListener('keydown', handleEscKey);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Use the actual canvas container for centering calculation
|
adjustViewportOnOpen(originalRect);
|
||||||
const originalCanvasContainer = originalParent!.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
|
|
||||||
const fullscreenCanvasContainer = modalContent.querySelector('.painterCanvasContainer.painter-container') as HTMLElement;
|
|
||||||
const originalRect = originalCanvasContainer.getBoundingClientRect();
|
|
||||||
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
|
|
||||||
adjustViewportForCentering(originalRect, fullscreenRect, 1);
|
|
||||||
canvas.render();
|
canvas.render();
|
||||||
if (node.onResize) {
|
if (node.onResize) {
|
||||||
node.onResize();
|
node.onResize();
|
||||||
|
|||||||
@@ -1,54 +1,96 @@
|
|||||||
.painter-button {
|
.painter-button {
|
||||||
background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
|
background-color: #444;
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 5px;
|
||||||
color: #ffffff;
|
color: #e0e0e0;
|
||||||
padding: 6px 12px;
|
padding: 6px 14px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease-in-out;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button:hover {
|
.painter-button:hover {
|
||||||
background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
|
background-color: #555;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
border-color: #666;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button:active {
|
.painter-button:active {
|
||||||
background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
|
background-color: #3a3a3a;
|
||||||
transform: translateY(1px);
|
transform: translateY(0);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button:disabled,
|
.painter-button:disabled,
|
||||||
.painter-button:disabled:hover {
|
.painter-button:disabled:hover {
|
||||||
background: #555;
|
background-color: #3a3a3a;
|
||||||
color: #888;
|
color: #777;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-color: #444;
|
border-color: #4a4a4a;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button.primary {
|
.painter-button.primary {
|
||||||
background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
|
background-color: #3a76d6;
|
||||||
border-color: #2a4cb4;
|
border-color: #2a6ac4;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-button.primary:hover {
|
.painter-button.primary:hover {
|
||||||
background: linear-gradient(to bottom, #5a7ce4, #4a6cd4);
|
background-color: #4a86e4;
|
||||||
|
border-color: #3a76d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.success {
|
||||||
|
background-color: #4a7c59;
|
||||||
|
border-color: #3a6c49;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.success:hover {
|
||||||
|
background-color: #5a8c69;
|
||||||
|
border-color: #4a7c59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.danger {
|
||||||
|
background-color: #c54747;
|
||||||
|
border-color: #a53737;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.danger:hover {
|
||||||
|
background-color: #d55757;
|
||||||
|
border-color: #c54747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-button.icon-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 30px; /* Match height */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-controls {
|
.painter-controls {
|
||||||
background: linear-gradient(to bottom, #404040, #383838);
|
background-color: #2f2f2f;
|
||||||
border-bottom: 1px solid #2a2a2a;
|
border-bottom: 1px solid #202020;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
padding: 8px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -56,57 +98,198 @@
|
|||||||
|
|
||||||
.painter-slider-container {
|
.painter-slider-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-slider-container input[type="range"] {
|
.painter-slider-container input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
|
height: 4px;
|
||||||
|
background: #555;
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.painter-slider-container input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #555;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-slider-container input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.painter-slider-container input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #bbb;
|
||||||
|
margin-top: 2px;
|
||||||
|
min-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.painter-button-group {
|
.painter-button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
background-color: rgba(0,0,0,0.2);
|
background-color: transparent;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-clipboard-group {
|
.painter-clipboard-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 4px;
|
||||||
background-color: rgba(0,0,0,0.15);
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.painter-clipboard-group::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 20px;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(74, 108, 212, 0.6), transparent);
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.painter-clipboard-group .painter-button {
|
.painter-clipboard-group .painter-button {
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
height: 30px; /* Match switch height */
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Clipboard Switch Modern --- */
|
||||||
|
.clipboard-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 90px;
|
||||||
|
height: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease-in-out;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.clipboard-switch:hover {
|
||||||
|
background-color: #555;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
.clipboard-switch:active {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-track {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #5a5a5a;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-labels {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e0e0e0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.clipboard-switch .switch-labels .text-clipspace,
|
||||||
|
.clipboard-switch .switch-labels .text-system {
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.clipboard-switch .switch-labels .text-clipspace { opacity: 0; }
|
||||||
|
.clipboard-switch .switch-labels .text-system { opacity: 1; padding-left: 20px; }
|
||||||
|
|
||||||
|
.clipboard-switch .switch-knob .switch-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch .switch-knob .switch-icon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checked state */
|
||||||
|
.clipboard-switch:has(input:checked) {
|
||||||
|
background-color: #3a76d6;
|
||||||
|
border-color: #2a6ac4;
|
||||||
|
}
|
||||||
|
.clipboard-switch:has(input:checked):hover {
|
||||||
|
background-color: #4a86e4;
|
||||||
|
border-color: #3a76d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-switch input:checked ~ .switch-knob {
|
||||||
|
left: calc(100% - 26px);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.clipboard-switch input:checked ~ .switch-knob .switch-icon img {
|
||||||
|
filter: invert(35%) sepia(100%) saturate(1500%) hue-rotate(200deg) brightness(90%) contrast(100%);
|
||||||
|
}
|
||||||
|
.clipboard-switch input:checked ~ .switch-labels .text-clipspace {
|
||||||
|
opacity: 1;
|
||||||
|
color: #fff;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.clipboard-switch input:checked ~ .switch-labels .text-system {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.painter-separator {
|
.painter-separator {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 28px;
|
height: 24px;
|
||||||
background-color: #2a2a2a;
|
background-color: #444;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,18 @@ export const LAYERFORGE_TOOLS = {
|
|||||||
BRUSH: 'brush',
|
BRUSH: 'brush',
|
||||||
ERASER: 'eraser',
|
ERASER: 'eraser',
|
||||||
SHAPE: 'shape',
|
SHAPE: 'shape',
|
||||||
SETTINGS: 'settings'
|
SETTINGS: 'settings',
|
||||||
|
SYSTEM_CLIPBOARD: 'system_clipboard',
|
||||||
|
CLIPSPACE: 'clipspace',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// SVG Icons for LayerForge tools
|
// SVG Icons for LayerForge tools
|
||||||
|
const SYSTEM_CLIPBOARD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 15H7v-2h10v2zm0-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>`;
|
||||||
|
const CLIPSPACE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M17 7H7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm0 12H7V9h10v10z"/><path d="M19 3H9c-1.1 0-2 .9-2 2v2h2V5h10v10h-2v2h2c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`;
|
||||||
|
|
||||||
const LAYERFORGE_TOOL_ICONS = {
|
const LAYERFORGE_TOOL_ICONS = {
|
||||||
|
[LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SYSTEM_CLIPBOARD_ICON_SVG)}`,
|
||||||
|
[LAYERFORGE_TOOLS.CLIPSPACE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CLIPSPACE_ICON_SVG)}`,
|
||||||
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
|
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
|
||||||
|
|
||||||
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
|
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user