Fix settings
This commit is contained in:
+85
-25
@@ -49,6 +49,7 @@ class TextRenderConfig:
|
||||
shadow_color: Tuple[int, int, int, int]
|
||||
shadow_offset: Tuple[int, int]
|
||||
shadow_blur: float
|
||||
shadow_opacity: float
|
||||
text_opacity: int = 100
|
||||
use_supersampling: bool = False
|
||||
use_edge_smoothing: bool = True
|
||||
@@ -60,7 +61,7 @@ class TextRenderConfig:
|
||||
|
||||
@property
|
||||
def has_shadow(self) -> bool:
|
||||
return self.enable_shadow and any(self.shadow_offset)
|
||||
return self.enable_shadow
|
||||
|
||||
class LRUCache:
|
||||
def __init__(self, max_size: int = 100):
|
||||
@@ -105,7 +106,8 @@ class HighQualityTextRenderer:
|
||||
shadow_color=config.shadow_color,
|
||||
shadow_offset=(config.shadow_offset[0] * scale_factor,
|
||||
config.shadow_offset[1] * scale_factor),
|
||||
shadow_blur=config.shadow_blur,
|
||||
shadow_blur=config.shadow_blur * scale_factor,
|
||||
shadow_opacity=config.shadow_opacity,
|
||||
text_opacity=config.text_opacity,
|
||||
use_supersampling=False,
|
||||
use_edge_smoothing=False
|
||||
@@ -613,6 +615,7 @@ class RS_TextOverlay:
|
||||
self.log(f"Error calculating mask rotation: {e}", level=2)
|
||||
return 0.0
|
||||
|
||||
# ✅ ИСПРАВЛЕННАЯ ФУНКЦИЯ — обводка с letter_spacing
|
||||
def _draw_single_line(self, draw, position, text, font, color, letter_spacing, text_orientation):
|
||||
x, y = position
|
||||
|
||||
@@ -628,19 +631,44 @@ class RS_TextOverlay:
|
||||
bbox = draw.textbbox((x, current_y), char, font=font)
|
||||
current_y += (bbox[3] - bbox[1]) + letter_spacing
|
||||
|
||||
def _draw_outline_optimized(self, draw, position, text, font, color, thickness):
|
||||
# ✅ ИСПРАВЛЕННАЯ ФУНКЦИЯ — обводка рисуется для КАЖДОЙ буквы с letter_spacing
|
||||
def _draw_outline_optimized(self, draw, position, text, font, color, thickness, letter_spacing, text_orientation):
|
||||
x, y = position
|
||||
|
||||
if thickness == 1:
|
||||
directions = [(-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1)]
|
||||
for dx, dy in directions:
|
||||
self._draw_single_line(draw, (x + dx, y + dy), text, font, color, 0, "horizontal")
|
||||
if text_orientation == "horizontal":
|
||||
current_x = x
|
||||
for char in text:
|
||||
# Рисуем обводку для каждой буквы
|
||||
if thickness == 1:
|
||||
directions = [(-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1)]
|
||||
for dx, dy in directions:
|
||||
draw.text((current_x + dx, y + dy), char, font=font, fill=color)
|
||||
else:
|
||||
for radius in range(1, thickness + 1):
|
||||
for angle in range(0, 360, 45):
|
||||
dx = int(radius * math.cos(math.radians(angle)))
|
||||
dy = int(radius * math.sin(math.radians(angle)))
|
||||
draw.text((current_x + dx, y + dy), char, font=font, fill=color)
|
||||
|
||||
# ✅ Применяем letter_spacing к обводке
|
||||
current_x += draw.textlength(char, font=font) + letter_spacing
|
||||
else:
|
||||
for radius in range(1, thickness + 1):
|
||||
for angle in range(0, 360, 45):
|
||||
dx = int(radius * math.cos(math.radians(angle)))
|
||||
dy = int(radius * math.sin(math.radians(angle)))
|
||||
self._draw_single_line(draw, (x + dx, y + dy), text, font, color, 0, "horizontal")
|
||||
current_y = y
|
||||
for char in text:
|
||||
# Рисуем обводку для каждой буквы
|
||||
if thickness == 1:
|
||||
directions = [(-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1)]
|
||||
for dx, dy in directions:
|
||||
draw.text((x + dx, current_y + dy), char, font=font, fill=color)
|
||||
else:
|
||||
for radius in range(1, thickness + 1):
|
||||
for angle in range(0, 360, 45):
|
||||
dx = int(radius * math.cos(math.radians(angle)))
|
||||
dy = int(radius * math.sin(math.radians(angle)))
|
||||
draw.text((x + dx, current_y + dy), char, font=font, fill=color)
|
||||
|
||||
bbox = draw.textbbox((x, current_y), char, font=font)
|
||||
current_y += (bbox[3] - bbox[1]) + letter_spacing
|
||||
|
||||
def _render_text_core(self, text: str, config: TextRenderConfig,
|
||||
container_size: Tuple[int, int]) -> Image.Image:
|
||||
@@ -664,9 +692,11 @@ class RS_TextOverlay:
|
||||
|
||||
start_x = (container_size[0] - line_w) // 2
|
||||
|
||||
# ✅ ИСПРАВЛЕНИЕ: передаём letter_spacing в обводку
|
||||
if config.has_outline:
|
||||
self._draw_outline_optimized(draw, (start_x, current_y), line, config.font,
|
||||
config.outline_color, config.outline_thickness)
|
||||
config.outline_color, config.outline_thickness,
|
||||
config.letter_spacing, config.text_orientation)
|
||||
|
||||
self._draw_single_line(draw, (start_x, current_y), line, config.font,
|
||||
config.text_color, config.letter_spacing, config.text_orientation)
|
||||
@@ -685,14 +715,16 @@ class RS_TextOverlay:
|
||||
|
||||
text_layer = self._render_text_core(text, config, container_size)
|
||||
|
||||
if config.has_shadow:
|
||||
text_layer = self._apply_shadow_effect(text_layer, config)
|
||||
|
||||
# Сначала supersampling
|
||||
if config.use_supersampling and config.supersampling_factor > 1:
|
||||
text_layer = self.quality_renderer.render_with_supersampling(
|
||||
text, config, container_size, self._render_text_core, config.supersampling_factor
|
||||
)
|
||||
|
||||
# ПОТОМ тень (после масштабирования!)
|
||||
if config.has_shadow:
|
||||
text_layer = self._apply_shadow_effect(text_layer, config)
|
||||
|
||||
if config.use_edge_smoothing:
|
||||
text_layer = self.quality_renderer.apply_edge_smoothing(text_layer)
|
||||
|
||||
@@ -700,23 +732,48 @@ class RS_TextOverlay:
|
||||
return text_layer
|
||||
|
||||
def _apply_shadow_effect(self, text_layer: Image.Image, config: TextRenderConfig) -> Image.Image:
|
||||
# Получаем альфа-канал текста (форма букв)
|
||||
alpha = text_layer.getchannel("A")
|
||||
shadow = Image.new("RGBA", text_layer.size, config.shadow_color)
|
||||
shadow.putalpha(alpha)
|
||||
|
||||
# Создаём тень с альфа-каналом текста
|
||||
shadow = Image.new("RGBA", text_layer.size, (0, 0, 0, 0))
|
||||
shadow.putalpha(alpha.copy())
|
||||
|
||||
# Применяем размытие если нужно
|
||||
if config.shadow_blur > 0:
|
||||
blur_radius = int(config.shadow_blur * 20)
|
||||
if blur_radius > 0:
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(blur_radius))
|
||||
|
||||
|
||||
# Смещаем тень
|
||||
offset_x, offset_y = config.shadow_offset
|
||||
if offset_x or offset_y:
|
||||
shadow = shadow.transform(shadow.size, Image.AFFINE, (1, 0, offset_x, 0, 1, offset_y),
|
||||
Image.Resampling.BICUBIC, fill=(0, 0, 0, 0))
|
||||
|
||||
shadow = shadow.transform(shadow.size, Image.AFFINE, (1, 0, offset_x, 0, 1, offset_y),
|
||||
Image.Resampling.BICUBIC, fill=(0, 0, 0, 0))
|
||||
|
||||
# Применяем цвет и opacity тени
|
||||
shadow_data = np.array(shadow)
|
||||
mask = shadow_data[:, :, 3] > 0
|
||||
|
||||
# Применяем shadow_opacity к альфа-каналу
|
||||
shadow_data[:, :, 3] = (shadow_data[:, :, 3].astype(np.float32) * config.shadow_opacity).astype(np.uint8)
|
||||
|
||||
# Создаём цвет тени (RGB без alpha)
|
||||
shadow_rgb = np.array([
|
||||
config.shadow_color[0],
|
||||
config.shadow_color[1],
|
||||
config.shadow_color[2]
|
||||
], dtype=np.uint8)
|
||||
|
||||
# Применяем цвет только к пикселям где есть тень
|
||||
shadow_data[mask, :3] = shadow_rgb
|
||||
|
||||
shadow_colored = Image.fromarray(shadow_data, 'RGBA')
|
||||
|
||||
# Композит: сначала тень, потом текст
|
||||
result = Image.new("RGBA", text_layer.size, (0, 0, 0, 0))
|
||||
result.paste(shadow, (0, 0), shadow)
|
||||
result.paste(shadow_colored, (0, 0), shadow_colored)
|
||||
result.paste(text_layer, (0, 0), text_layer)
|
||||
|
||||
return result
|
||||
|
||||
def _process_single(self, image, mask, text, font_name, config, padding,
|
||||
@@ -825,6 +882,8 @@ class RS_TextOverlay:
|
||||
max_font_size = data.get('max_font_size', 500)
|
||||
padding = data.get('padding', 5)
|
||||
|
||||
self.log(f"Shadow settings: enable={enable_shadow}, color={shadow_color}, offset=({shadow_offset_x}, {shadow_offset_y}), blur={shadow_blur}, opacity={shadow_opacity}", level=1)
|
||||
|
||||
config = TextRenderConfig(
|
||||
font=None,
|
||||
text_color=self.parse_color(text_color, text_opacity),
|
||||
@@ -834,9 +893,10 @@ class RS_TextOverlay:
|
||||
line_spacing=line_spacing,
|
||||
text_orientation=text_orientation,
|
||||
enable_shadow=enable_shadow,
|
||||
shadow_color=self.parse_color(shadow_color, shadow_opacity),
|
||||
shadow_color=self.parse_color(shadow_color, 1.0),
|
||||
shadow_offset=(shadow_offset_x, shadow_offset_y),
|
||||
shadow_blur=shadow_blur,
|
||||
shadow_opacity=shadow_opacity,
|
||||
text_opacity=int(text_opacity * 100),
|
||||
use_supersampling=use_supersampling,
|
||||
use_edge_smoothing=use_edge_smoothing,
|
||||
|
||||
+176
-65
@@ -10,11 +10,9 @@ app.registerExtension({
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
// Отключаем стандартное превью ComfyUI
|
||||
this.allow_preview = false;
|
||||
this._outputNode = false;
|
||||
|
||||
// Дефолтные данные интерфейса
|
||||
this.data = {
|
||||
text: "",
|
||||
font_name: "default",
|
||||
@@ -34,7 +32,6 @@ app.registerExtension({
|
||||
this.widgetsHeight = 260;
|
||||
this.previewHeight = 0;
|
||||
|
||||
// === ПРЕВЮ ИЗОБРАЖЕНИЙ ===
|
||||
this.imgs = [];
|
||||
this.imageIndex = 0;
|
||||
|
||||
@@ -60,11 +57,9 @@ app.registerExtension({
|
||||
this.widgets.forEach(w => w.hidden = true);
|
||||
}
|
||||
|
||||
// Устанавливаем начальный размер ноды
|
||||
this.setSize([this.targetWidth, 450]);
|
||||
const self = this;
|
||||
|
||||
// Обработка полученных изображений
|
||||
const onExecuted = this.onExecuted;
|
||||
this.onExecuted = function(message) {
|
||||
const r = onExecuted ? onExecuted.apply(this, arguments) : undefined;
|
||||
@@ -117,9 +112,7 @@ app.registerExtension({
|
||||
}
|
||||
};
|
||||
|
||||
// === ПОЛНОСТЬЮ ПЕРЕОПРЕДЕЛЯЕМ onDrawBackground ===
|
||||
this.onDrawBackground = function(ctx) {
|
||||
// Рисуем превью ниже виджетов
|
||||
if (this.imgs.length > 0 && this.imgs[this.imageIndex]) {
|
||||
const img = this.imgs[this.imageIndex];
|
||||
const previewY = this.widgetsHeight + 20;
|
||||
@@ -135,24 +128,20 @@ app.registerExtension({
|
||||
drawWidth = maxPreviewHeight / aspectRatio;
|
||||
}
|
||||
|
||||
// === ЦЕНТРИРОВАНИЕ ПО ГОРИЗОНТАЛИ ===
|
||||
const previewX = (this.size[0] - drawWidth) / 2;
|
||||
|
||||
// Рамка превью
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(previewX - 5, previewY - 5, drawWidth + 10, drawHeight + 10);
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(previewX - 5, previewY - 5, drawWidth + 10, drawHeight + 10);
|
||||
|
||||
// Рисуем изображение
|
||||
try {
|
||||
ctx.drawImage(img, previewX, previewY, drawWidth, drawHeight);
|
||||
} catch (e) {
|
||||
console.error("Error drawing preview:", e);
|
||||
}
|
||||
|
||||
// Индикатор количества изображений
|
||||
if (this.imgs.length > 1) {
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
||||
ctx.fillRect(previewX + drawWidth - 60, previewY + drawHeight - 25, 55, 20);
|
||||
@@ -164,10 +153,8 @@ app.registerExtension({
|
||||
}
|
||||
};
|
||||
|
||||
// === ОТРИСОВКА НА ПЕРЕДНЕМ ПЛАНЕ (виджеты) ===
|
||||
const onDrawForeground = this.onDrawForeground;
|
||||
this.onDrawForeground = function(ctx, visibleRect) {
|
||||
// Вызываем оригинальную отрисовку переднего плана (для заголовка, рамок и т.д.)
|
||||
if (onDrawForeground) {
|
||||
onDrawForeground.apply(this, arguments);
|
||||
}
|
||||
@@ -181,21 +168,21 @@ app.registerExtension({
|
||||
const inputW = this.size[0] - pad*2 - labelW;
|
||||
let y = startY;
|
||||
|
||||
// === TEXT ===
|
||||
// TEXT
|
||||
this.drawLabel(ctx, "TEXT", pad, y, labelW, rowH * 2);
|
||||
this.drawMultilineField(ctx, this.data.text, pad + labelW, y, inputW, rowH * 2);
|
||||
this.clickZones.push({ type: "text", x: pad + labelW, y: y, w: inputW, h: rowH * 2 });
|
||||
this.widgetYPositions["text"] = y;
|
||||
y += rowH * 2 + 10;
|
||||
|
||||
// === FONT ===
|
||||
// FONT
|
||||
this.drawLabel(ctx, "FONT", pad, y, labelW, rowH);
|
||||
this.drawComboField(ctx, this.data.font_name, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["font_name"] = y;
|
||||
this.clickZones.push({ type: "combo", field: "font_name", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === FONT SIZE ===
|
||||
// FONT SIZE
|
||||
this.drawLabel(ctx, "FONT SIZE", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.font_size, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["font_size"] = y;
|
||||
@@ -204,7 +191,7 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "font_size", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 1, max: 512, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === FOOTER SIZE ===
|
||||
// FOOTER SIZE
|
||||
this.drawLabel(ctx, "FOOTER SIZE", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.footer_height, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["footer_height"] = y;
|
||||
@@ -213,14 +200,14 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "footer_height", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0, max: 1024, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === THEME ===
|
||||
// THEME
|
||||
this.drawLabel(ctx, "THEME", pad, y, labelW, rowH);
|
||||
this.drawComboField(ctx, this.data.theme, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["theme"] = y;
|
||||
this.clickZones.push({ type: "combo", field: "theme", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === PREFIX ===
|
||||
// PREFIX
|
||||
this.drawLabel(ctx, "PREFIX", pad, y, labelW, rowH);
|
||||
this.drawStringField(ctx, this.data.filename_prefix, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["filename_prefix"] = y;
|
||||
@@ -233,7 +220,7 @@ app.registerExtension({
|
||||
this.onMouseDown = function(e, pos, canvas) {
|
||||
if (!this.clickZones.length) return false;
|
||||
|
||||
// Проверка клика по превью для переключения изображений
|
||||
// Клик по превью для переключения изображений
|
||||
if (this.imgs.length > 1 && this.imgs[this.imageIndex]) {
|
||||
const img = this.imgs[this.imageIndex];
|
||||
const previewY = this.widgetsHeight + 20;
|
||||
@@ -247,7 +234,6 @@ app.registerExtension({
|
||||
drawWidth = maxPreviewHeight / aspectRatio;
|
||||
}
|
||||
|
||||
// === ЦЕНТРИРОВАНИЕ ПО ГОРИЗОНТАЛИ (для клика) ===
|
||||
const previewX = (this.size[0] - drawWidth) / 2;
|
||||
|
||||
if (pos[0] >= previewX + drawWidth - 60 && pos[0] <= previewX + drawWidth &&
|
||||
@@ -265,26 +251,32 @@ app.registerExtension({
|
||||
const inY = pos[1] >= zone.y && pos[1] <= zone.y + zone.h;
|
||||
if (inX && inY) {
|
||||
if (zone.type === "text") {
|
||||
self.showTextInput();
|
||||
self.showTextInput(e);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "string") {
|
||||
self.showStringInput(zone.field);
|
||||
self.showInlineInput(zone.field, e);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "combo") {
|
||||
self.showComboSelector(zone.field);
|
||||
self.showComboSelector(zone.field, e);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "number") {
|
||||
const current = self.data[zone.field];
|
||||
const newVal = prompt(`${zone.field}:`, current);
|
||||
if (newVal !== null) {
|
||||
let parsed = parseInt(newVal);
|
||||
if (!isNaN(parsed)) {
|
||||
parsed = Math.max(zone.min, Math.min(zone.max, parsed));
|
||||
self.data[zone.field] = parsed;
|
||||
self.updateUI();
|
||||
if (zone.field === "font_size") {
|
||||
self.showInlineInput(zone.field, e, { isNumber: true, min: 1, max: 512, step: 1 });
|
||||
} else if (zone.field === "footer_height") {
|
||||
self.showInlineInput(zone.field, e, { isNumber: true, min: 0, max: 1024, step: 1 });
|
||||
} else {
|
||||
const current = self.data[zone.field];
|
||||
const newVal = prompt(`${zone.field}:`, current);
|
||||
if (newVal !== null) {
|
||||
let parsed = parseInt(newVal);
|
||||
if (!isNaN(parsed)) {
|
||||
parsed = Math.max(zone.min, Math.min(zone.max, parsed));
|
||||
self.data[zone.field] = parsed;
|
||||
self.updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -308,6 +300,7 @@ app.registerExtension({
|
||||
return false;
|
||||
};
|
||||
|
||||
// === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ОТРИСОВКИ ===
|
||||
this.drawLabel = function(ctx, text, x, y, w, h) {
|
||||
ctx.fillStyle = "#aaa";
|
||||
ctx.font = "11px sans-serif";
|
||||
@@ -315,7 +308,6 @@ app.registerExtension({
|
||||
ctx.fillText(text, x, y + h/2 + 4);
|
||||
};
|
||||
|
||||
// === ИСПРАВЛЕННАЯ ФУНКЦИЯ — МНОГОСТРОЧНЫЙ ТЕКСТ ===
|
||||
this.drawMultilineField = function(ctx, value, x, y, w, h) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
@@ -331,23 +323,19 @@ app.registerExtension({
|
||||
return;
|
||||
}
|
||||
|
||||
// Разбиваем текст на строки и рисуем каждую
|
||||
const lines = value.split('\n');
|
||||
const lineHeight = 14;
|
||||
const startY = y + 10;
|
||||
|
||||
// Рисуем максимум 3 строки для отображения
|
||||
const maxLines = 3;
|
||||
|
||||
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
||||
let line = lines[i];
|
||||
// Обрезаем если строка слишком длинная
|
||||
if (line.length > 35) {
|
||||
line = line.substring(0, 32) + "...";
|
||||
}
|
||||
ctx.fillText(line, x + 5, startY + (i * lineHeight));
|
||||
}
|
||||
|
||||
// Если строк больше чем показываем
|
||||
if (lines.length > maxLines) {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("...", x + 5, startY + (maxLines * lineHeight));
|
||||
@@ -407,7 +395,8 @@ app.registerExtension({
|
||||
ctx.fillText(value, x + w/2, y + h/2 + 4);
|
||||
};
|
||||
|
||||
this.showComboSelector = function(fieldName) {
|
||||
// === POPUP: ВЫПАДАЮЩИЙ СПИСОК (FONT, THEME) ===
|
||||
this.showComboSelector = function(fieldName, clickEvent) {
|
||||
let list = [];
|
||||
if (fieldName === "font_name") {
|
||||
list = self.data.font_list || ["default"];
|
||||
@@ -442,9 +431,9 @@ app.registerExtension({
|
||||
`;
|
||||
item.onmouseover = () => item.style.background = "#333";
|
||||
item.onmouseout = () => item.style.background = "#1a1a1a";
|
||||
item.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
item.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
self.data[fieldName] = opt;
|
||||
self.updateUI();
|
||||
menu.remove();
|
||||
@@ -452,15 +441,9 @@ app.registerExtension({
|
||||
menu.appendChild(item);
|
||||
});
|
||||
|
||||
const widgetY = self.widgetYPositions[fieldName] || 200;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect() ||
|
||||
document.querySelector("canvas")?.getBoundingClientRect();
|
||||
|
||||
if (canvasRect && self.pos) {
|
||||
const menuX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const menuY = canvasRect.top + self.pos[1] + widgetY + self.rowHeight;
|
||||
menu.style.left = menuX + "px";
|
||||
menu.style.top = menuY + "px";
|
||||
if (clickEvent) {
|
||||
menu.style.left = (clickEvent.clientX + 8) + "px";
|
||||
menu.style.top = clickEvent.clientY + "px";
|
||||
} else {
|
||||
menu.style.left = "250px";
|
||||
menu.style.top = "200px";
|
||||
@@ -469,8 +452,8 @@ app.registerExtension({
|
||||
document.body.appendChild(menu);
|
||||
|
||||
setTimeout(() => {
|
||||
const closeHandler = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
const closeHandler = (ev) => {
|
||||
if (!menu.contains(ev.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener("mousedown", closeHandler);
|
||||
}
|
||||
@@ -479,7 +462,8 @@ app.registerExtension({
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.showTextInput = function() {
|
||||
// === POPUP: МНОГОСТРОЧНЫЙ ТЕКСТ (TEXT) — С ИСПРАВЛЕННЫМ ВЫДЕЛЕНИЕМ ===
|
||||
this.showTextInput = function(clickEvent) {
|
||||
const currentValue = self.data.text || '';
|
||||
|
||||
const popup = document.createElement('div');
|
||||
@@ -527,27 +511,154 @@ app.registerExtension({
|
||||
popup.appendChild(input);
|
||||
popup.appendChild(saveBtn);
|
||||
|
||||
const widgetY = self.widgetYPositions["text"] || 80;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect();
|
||||
if (canvasRect && self.pos) {
|
||||
const inputX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const inputY = canvasRect.top + self.pos[1] + widgetY + (self.rowHeight * 2);
|
||||
popup.style.left = inputX + 'px';
|
||||
popup.style.top = inputY + 'px';
|
||||
if (clickEvent) {
|
||||
popup.style.left = (clickEvent.clientX + 8) + 'px';
|
||||
popup.style.top = clickEvent.clientY + 'px';
|
||||
}
|
||||
|
||||
document.body.appendChild(popup);
|
||||
input.focus();
|
||||
|
||||
// === ИСПРАВЛЕННЫЙ ФОКУС И ВЫДЕЛЕНИЕ — ДВОЙНАЯ ЗАДЕРЖКА ===
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
setTimeout(() => {
|
||||
if (currentValue && currentValue.length > 0) {
|
||||
input.select();
|
||||
}
|
||||
}, 10);
|
||||
}, 50);
|
||||
|
||||
saveBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
saveBtn.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
self.data.text = input.value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
};
|
||||
|
||||
input.onkeydown = (ev) => {
|
||||
if (ev.key === 'Enter' && ev.ctrlKey) {
|
||||
ev.preventDefault();
|
||||
self.data.text = input.value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// === POPUP: ОДНОСТРОЧНЫЙ ВВОД — С ИСПРАВЛЕННЫМ ВЫДЕЛЕНИЕМ ДЛЯ ВСЕХ ПОЛЕЙ ===
|
||||
this.showInlineInput = function(fieldName, clickEvent, options = {}) {
|
||||
const currentValue = self.data[fieldName] || '';
|
||||
const isNumber = options.isNumber || false;
|
||||
const min = options.min !== undefined ? options.min : null;
|
||||
const max = options.max !== undefined ? options.max : null;
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10003;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = isNumber ? 'number' : 'text';
|
||||
input.value = currentValue;
|
||||
if (isNumber) {
|
||||
input.step = options.step || 1;
|
||||
if (min !== null) input.min = min;
|
||||
if (max !== null) input.max = max;
|
||||
}
|
||||
input.style.cssText = `
|
||||
width: 100px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = '✅';
|
||||
saveBtn.title = 'Save';
|
||||
saveBtn.style.cssText = `
|
||||
background: #4CAF50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
min-width: 28px;
|
||||
`;
|
||||
saveBtn.onmouseover = () => saveBtn.style.background = "#45a049";
|
||||
saveBtn.onmouseout = () => saveBtn.style.background = "#4CAF50";
|
||||
|
||||
popup.appendChild(input);
|
||||
popup.appendChild(saveBtn);
|
||||
|
||||
if (clickEvent) {
|
||||
popup.style.left = (clickEvent.clientX + 8) + 'px';
|
||||
popup.style.top = clickEvent.clientY + 'px';
|
||||
}
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// === ИСПРАВЛЕННЫЙ ФОКУС И ВЫДЕЛЕНИЕ — ДВОЙНАЯ ЗАДЕРЖКА ДЛЯ ВСЕХ ПОЛЕЙ ===
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
setTimeout(() => {
|
||||
if (currentValue && currentValue.toString().length > 0) {
|
||||
// === ТЕПЕРЬ select() ДЛЯ ВСЕХ ПОЛЕЙ, ВКЛЮЧАЯ ЧИСЛОВЫЕ ===
|
||||
input.select();
|
||||
}
|
||||
}, 10);
|
||||
}, 50);
|
||||
|
||||
const doSave = () => {
|
||||
let value = input.value;
|
||||
if (isNumber) {
|
||||
let num = parseInt(value);
|
||||
if (isNaN(num)) num = self.data[fieldName];
|
||||
if (min !== null) num = Math.max(min, num);
|
||||
if (max !== null) num = Math.min(max, num);
|
||||
value = num;
|
||||
}
|
||||
self.data[fieldName] = value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
};
|
||||
|
||||
saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); };
|
||||
input.onkeydown = (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
doSave();
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
const closeHandler = (ev) => {
|
||||
if (!popup.contains(ev.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener("mousedown", closeHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", closeHandler);
|
||||
}, 50);
|
||||
|
||||
return popup;
|
||||
};
|
||||
|
||||
this.showStringInput = function(fieldName) {
|
||||
const currentValue = self.data[fieldName] || '';
|
||||
const newVal = prompt(`Enter ${fieldName}:`, currentValue);
|
||||
|
||||
+196
-121
@@ -83,28 +83,28 @@ app.registerExtension({
|
||||
const inputW = this.size[0] - pad*2 - labelW;
|
||||
let y = startY;
|
||||
|
||||
// === TEXT ===
|
||||
// TEXT
|
||||
this.drawLabel(ctx, "TEXT", pad, y, labelW, rowH * 2);
|
||||
this.drawMultilineField(ctx, this.data.text, pad + labelW, y, inputW, rowH * 2);
|
||||
this.clickZones.push({ type: "text", x: pad + labelW, y: y, w: inputW, h: rowH * 2 });
|
||||
this.widgetYPositions["text"] = y;
|
||||
y += rowH * 2 + 5;
|
||||
|
||||
// === FONT NAME ===
|
||||
// FONT
|
||||
this.drawLabel(ctx, "FONT", pad, y, labelW, rowH);
|
||||
this.drawComboField(ctx, this.data.font_name, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["font_name"] = y;
|
||||
this.clickZones.push({ type: "combo", field: "font_name", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === TEXT COLOR ===
|
||||
// TEXT COLOR
|
||||
this.drawLabel(ctx, "TEXT COLOR", pad, y, labelW, rowH);
|
||||
this.drawColorField(ctx, this.data.text_color, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["text_color"] = y;
|
||||
this.clickZones.push({ type: "color", field: "text_color", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === OUTLINE THICKNESS ===
|
||||
// OUTLINE THICKNESS
|
||||
this.drawLabel(ctx, "OUTLINE THICK", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.outline_thickness, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["outline_thickness"] = y;
|
||||
@@ -113,25 +113,25 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "outline_thickness", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0, max: 50, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === OUTLINE COLOR ===
|
||||
// OUTLINE COLOR
|
||||
this.drawLabel(ctx, "OUTLINE COLOR", pad, y, labelW, rowH);
|
||||
this.drawColorField(ctx, this.data.outline_color, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["outline_color"] = y;
|
||||
this.clickZones.push({ type: "color", field: "outline_color", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === FORMATTING TEXT SEPARATOR ===
|
||||
// FORMATTING TEXT SEPARATOR
|
||||
this.drawSeparator(ctx, "FORMATTING TEXT", pad, y, inputW + labelW, rowH);
|
||||
y += rowH + 5;
|
||||
|
||||
// === ROTATE WITH MASK ===
|
||||
// ROTATE WITH MASK
|
||||
this.drawLabel(ctx, "ROTATE WITH MASK", pad, y, labelW, rowH);
|
||||
this.drawBooleanField(ctx, this.data.rotate_with_mask, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["rotate_with_mask"] = y;
|
||||
this.clickZones.push({ type: "boolean", field: "rotate_with_mask", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === TEXT OPACITY ===
|
||||
// TEXT OPACITY
|
||||
this.drawLabel(ctx, "TEXT OPACITY", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.text_opacity, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["text_opacity"] = y;
|
||||
@@ -140,7 +140,7 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "text_opacity", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0, max: 1, step: 0.05, float: true });
|
||||
y += rowH + 5;
|
||||
|
||||
// === LINE SPACING ===
|
||||
// LINE SPACING
|
||||
this.drawLabel(ctx, "LINE SPACING", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.line_spacing, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["line_spacing"] = y;
|
||||
@@ -149,7 +149,7 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "line_spacing", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0.5, max: 3, step: 0.1, float: true });
|
||||
y += rowH + 5;
|
||||
|
||||
// === LETTER SPACING ===
|
||||
// LETTER SPACING
|
||||
this.drawLabel(ctx, "LETTER SPACING", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.letter_spacing, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["letter_spacing"] = y;
|
||||
@@ -158,32 +158,32 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "letter_spacing", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: -20, max: 100, step: 0.5, float: true });
|
||||
y += rowH + 5;
|
||||
|
||||
// === TEXT ORIENTATION ===
|
||||
// TEXT ORIENTATION
|
||||
this.drawLabel(ctx, "ORIENTATION", pad, y, labelW, rowH);
|
||||
this.drawComboField(ctx, this.data.text_orientation, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["text_orientation"] = y;
|
||||
this.clickZones.push({ type: "combo", field: "text_orientation", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SHADOW SETTINGS SEPARATOR ===
|
||||
// SHADOW SETTINGS SEPARATOR
|
||||
this.drawSeparator(ctx, "SHADOW SETTINGS", pad, y, inputW + labelW, rowH);
|
||||
y += rowH + 5;
|
||||
|
||||
// === ENABLE SHADOW ===
|
||||
// ENABLE SHADOW
|
||||
this.drawLabel(ctx, "ENABLE SHADOW", pad, y, labelW, rowH);
|
||||
this.drawBooleanField(ctx, this.data.enable_shadow, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["enable_shadow"] = y;
|
||||
this.clickZones.push({ type: "boolean", field: "enable_shadow", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SHADOW COLOR ===
|
||||
// SHADOW COLOR
|
||||
this.drawLabel(ctx, "SHADOW COLOR", pad, y, labelW, rowH);
|
||||
this.drawColorField(ctx, this.data.shadow_color, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["shadow_color"] = y;
|
||||
this.clickZones.push({ type: "color", field: "shadow_color", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SHADOW OFFSET X ===
|
||||
// SHADOW OFFSET X
|
||||
this.drawLabel(ctx, "SHADOW OFFSET X", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.shadow_offset_x, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["shadow_offset_x"] = y;
|
||||
@@ -192,7 +192,7 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "shadow_offset_x", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: -50, max: 50, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SHADOW OFFSET Y ===
|
||||
// SHADOW OFFSET Y
|
||||
this.drawLabel(ctx, "SHADOW OFFSET Y", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.shadow_offset_y, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["shadow_offset_y"] = y;
|
||||
@@ -201,7 +201,7 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "shadow_offset_y", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: -50, max: 50, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SHADOW BLUR ===
|
||||
// SHADOW BLUR
|
||||
this.drawLabel(ctx, "SHADOW BLUR", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.shadow_blur, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["shadow_blur"] = y;
|
||||
@@ -210,7 +210,7 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "shadow_blur", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0, max: 1, step: 0.05, float: true });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SHADOW OPACITY ===
|
||||
// SHADOW OPACITY
|
||||
this.drawLabel(ctx, "SHADOW OPACITY", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.shadow_opacity, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["shadow_opacity"] = y;
|
||||
@@ -219,18 +219,18 @@ app.registerExtension({
|
||||
this.clickZones.push({ type: "arrow_right", field: "shadow_opacity", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0, max: 1, step: 0.05, float: true });
|
||||
y += rowH + 5;
|
||||
|
||||
// === QUALITY SETTINGS SEPARATOR ===
|
||||
// QUALITY SETTINGS SEPARATOR
|
||||
this.drawSeparator(ctx, "QUALITY SETTINGS", pad, y, inputW + labelW, rowH);
|
||||
y += rowH + 5;
|
||||
|
||||
// === USE SUPERSAMPLING ===
|
||||
// USE SUPERSAMPLING
|
||||
this.drawLabel(ctx, "SUPERSAMPLING", pad, y, labelW, rowH);
|
||||
this.drawBooleanField(ctx, this.data.use_supersampling, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["use_supersampling"] = y;
|
||||
this.clickZones.push({ type: "boolean", field: "use_supersampling", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === SUPERSAMPLING FACTOR ===
|
||||
// SUPERSAMPLING FACTOR
|
||||
if (this.data.use_supersampling) {
|
||||
this.drawLabel(ctx, "SUPERSAMPLE FACTOR", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.supersampling_factor, pad + labelW, y, inputW, rowH, arrowW);
|
||||
@@ -241,7 +241,7 @@ app.registerExtension({
|
||||
y += rowH + 5;
|
||||
}
|
||||
|
||||
// === USE EDGE SMOOTHING ===
|
||||
// USE EDGE SMOOTHING
|
||||
this.drawLabel(ctx, "EDGE SMOOTHING", pad, y, labelW, rowH);
|
||||
this.drawBooleanField(ctx, this.data.use_edge_smoothing, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["use_edge_smoothing"] = y;
|
||||
@@ -256,22 +256,21 @@ app.registerExtension({
|
||||
|
||||
this.onMouseDown = function(e, pos, canvas) {
|
||||
if (!this.clickZones.length) return false;
|
||||
|
||||
for (const zone of this.clickZones) {
|
||||
const inX = pos[0] >= zone.x && pos[0] <= zone.x + zone.w;
|
||||
const inY = pos[1] >= zone.y && pos[1] <= zone.y + zone.h;
|
||||
if (inX && inY) {
|
||||
if (zone.type === "text") {
|
||||
self.showTextInput();
|
||||
self.showTextInput(e);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "combo") {
|
||||
console.log("🔽 Combo clicked:", zone.field);
|
||||
self.showComboSelector(zone.field);
|
||||
self.showComboSelector(zone.field, e);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "color") {
|
||||
console.log("🎨 Color clicked:", zone.field);
|
||||
self.openColorPicker(zone.field);
|
||||
self.openColorPicker(zone.field, e);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "boolean") {
|
||||
@@ -280,16 +279,13 @@ app.registerExtension({
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "number") {
|
||||
const current = self.data[zone.field];
|
||||
const newVal = prompt(`${zone.field}:`, current);
|
||||
if (newVal !== null) {
|
||||
let parsed = zone.float ? parseFloat(newVal) : parseInt(newVal);
|
||||
if (!isNaN(parsed)) {
|
||||
parsed = Math.max(zone.min, Math.min(zone.max, parsed));
|
||||
self.data[zone.field] = zone.float ? Math.round(parsed * 100) / 100 : parsed;
|
||||
self.updateUI();
|
||||
}
|
||||
}
|
||||
self.showInlineInput(zone.field, e, {
|
||||
isNumber: true,
|
||||
min: zone.min,
|
||||
max: zone.max,
|
||||
step: zone.step || 1,
|
||||
float: zone.float || false
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "arrow_left") {
|
||||
@@ -313,6 +309,7 @@ app.registerExtension({
|
||||
return false;
|
||||
};
|
||||
|
||||
// === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ОТРИСОВКИ ===
|
||||
this.drawLabel = function(ctx, text, x, y, w, h) {
|
||||
ctx.fillStyle = "#aaa";
|
||||
ctx.font = "11px sans-serif";
|
||||
@@ -320,7 +317,6 @@ app.registerExtension({
|
||||
ctx.fillText(text, x, y + h/2 + 4);
|
||||
};
|
||||
|
||||
// === ИСПРАВЛЕННАЯ ФУНКЦИЯ — МНОГОСТРОЧНЫЙ ТЕКСТ ===
|
||||
this.drawMultilineField = function(ctx, value, x, y, w, h) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
@@ -336,23 +332,19 @@ app.registerExtension({
|
||||
return;
|
||||
}
|
||||
|
||||
// Разбиваем текст на строки и рисуем каждую
|
||||
const lines = value.split('\n');
|
||||
const lineHeight = 14;
|
||||
const startY = y + 10;
|
||||
|
||||
// Рисуем максимум 3 строки для отображения
|
||||
const maxLines = 3;
|
||||
|
||||
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
||||
let line = lines[i];
|
||||
// Обрезаем если строка слишком длинная
|
||||
if (line.length > 35) {
|
||||
line = line.substring(0, 32) + "...";
|
||||
}
|
||||
ctx.fillText(line, x + 5, startY + (i * lineHeight));
|
||||
}
|
||||
|
||||
// Если строк больше чем показываем
|
||||
if (lines.length > maxLines) {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("...", x + 5, startY + (maxLines * lineHeight));
|
||||
@@ -439,7 +431,8 @@ app.registerExtension({
|
||||
ctx.fillText(value ? "ON" : "OFF", x + 8, y + h/2 + 4);
|
||||
};
|
||||
|
||||
this.showComboSelector = function(fieldName) {
|
||||
// === POPUP: ВЫПАДАЮЩИЙ СПИСОК — ПОЗИЦИЯ СПРАВА ОТ КЛИКА ===
|
||||
this.showComboSelector = function(fieldName, clickEvent) {
|
||||
const options = {
|
||||
font_name: self.data.font_list || ["default"],
|
||||
text_orientation: ["horizontal", "vertical"]
|
||||
@@ -473,10 +466,9 @@ app.registerExtension({
|
||||
`;
|
||||
item.onmouseover = () => item.style.background = "#333";
|
||||
item.onmouseout = () => item.style.background = "#1a1a1a";
|
||||
item.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log("✅ Combo selected:", opt);
|
||||
item.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
self.data[fieldName] = opt;
|
||||
self.updateUI();
|
||||
menu.remove();
|
||||
@@ -484,15 +476,9 @@ app.registerExtension({
|
||||
menu.appendChild(item);
|
||||
});
|
||||
|
||||
const widgetY = self.widgetYPositions[fieldName] || 200;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect() ||
|
||||
document.querySelector("canvas")?.getBoundingClientRect();
|
||||
|
||||
if (canvasRect && self.pos) {
|
||||
const menuX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const menuY = canvasRect.top + self.pos[1] + widgetY + self.rowHeight;
|
||||
menu.style.left = menuX + "px";
|
||||
menu.style.top = menuY + "px";
|
||||
if (clickEvent) {
|
||||
menu.style.left = (clickEvent.clientX + 8) + "px";
|
||||
menu.style.top = clickEvent.clientY + "px";
|
||||
} else {
|
||||
menu.style.left = "250px";
|
||||
menu.style.top = "200px";
|
||||
@@ -501,8 +487,8 @@ app.registerExtension({
|
||||
document.body.appendChild(menu);
|
||||
|
||||
setTimeout(() => {
|
||||
const closeHandler = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
const closeHandler = (ev) => {
|
||||
if (!menu.contains(ev.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener("mousedown", closeHandler);
|
||||
}
|
||||
@@ -511,7 +497,8 @@ app.registerExtension({
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.showTextInput = function() {
|
||||
// === POPUP: МНОГОСТРОЧНЫЙ ТЕКСТ — С ФОКУСОМ И ВЫДЕЛЕНИЕМ ===
|
||||
this.showTextInput = function(clickEvent) {
|
||||
const currentValue = self.data.text || '';
|
||||
|
||||
const popup = document.createElement('div');
|
||||
@@ -559,89 +546,177 @@ app.registerExtension({
|
||||
popup.appendChild(input);
|
||||
popup.appendChild(saveBtn);
|
||||
|
||||
const widgetY = self.widgetYPositions["text"] || 80;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect();
|
||||
if (canvasRect && self.pos) {
|
||||
const inputX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const inputY = canvasRect.top + self.pos[1] + widgetY + (self.rowHeight * 2);
|
||||
popup.style.left = inputX + 'px';
|
||||
popup.style.top = inputY + 'px';
|
||||
if (clickEvent) {
|
||||
popup.style.left = (clickEvent.clientX + 8) + 'px';
|
||||
popup.style.top = clickEvent.clientY + 'px';
|
||||
}
|
||||
|
||||
document.body.appendChild(popup);
|
||||
input.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
setTimeout(() => {
|
||||
if (currentValue && currentValue.length > 0) {
|
||||
input.select();
|
||||
}
|
||||
}, 10);
|
||||
}, 50);
|
||||
|
||||
saveBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
saveBtn.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
self.data.text = input.value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
};
|
||||
|
||||
input.onkeydown = (ev) => {
|
||||
if (ev.key === 'Enter' && ev.ctrlKey) {
|
||||
ev.preventDefault();
|
||||
self.data.text = input.value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// === POPUP: ОДНОСТРОЧНЫЙ ВВОД — С ФОКУСОМ И ВЫДЕЛЕНИЕМ ===
|
||||
this.showInlineInput = function(fieldName, clickEvent, options = {}) {
|
||||
const currentValue = self.data[fieldName] || '';
|
||||
const isNumber = options.isNumber || false;
|
||||
const min = options.min !== undefined ? options.min : null;
|
||||
const max = options.max !== undefined ? options.max : null;
|
||||
const isFloat = options.float || false;
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10003;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = isNumber ? 'number' : 'text';
|
||||
input.value = currentValue;
|
||||
if (isNumber) {
|
||||
input.step = options.step || 1;
|
||||
if (min !== null) input.min = min;
|
||||
if (max !== null) input.max = max;
|
||||
}
|
||||
input.style.cssText = `
|
||||
width: 100px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = '✅';
|
||||
saveBtn.title = 'Save';
|
||||
saveBtn.style.cssText = `
|
||||
background: #4CAF50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
min-width: 28px;
|
||||
`;
|
||||
saveBtn.onmouseover = () => saveBtn.style.background = "#45a049";
|
||||
saveBtn.onmouseout = () => saveBtn.style.background = "#4CAF50";
|
||||
|
||||
popup.appendChild(input);
|
||||
popup.appendChild(saveBtn);
|
||||
|
||||
if (clickEvent) {
|
||||
popup.style.left = (clickEvent.clientX + 8) + 'px';
|
||||
popup.style.top = clickEvent.clientY + 'px';
|
||||
}
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
setTimeout(() => {
|
||||
if (currentValue !== null && currentValue !== undefined && String(currentValue).length > 0) {
|
||||
input.select();
|
||||
}
|
||||
}, 10);
|
||||
}, 50);
|
||||
|
||||
const doSave = () => {
|
||||
let value = input.value;
|
||||
if (isNumber) {
|
||||
let num = isFloat ? parseFloat(value) : parseInt(value);
|
||||
if (isNaN(num)) num = self.data[fieldName];
|
||||
if (min !== null) num = Math.max(min, num);
|
||||
if (max !== null) num = Math.min(max, num);
|
||||
value = isFloat ? Math.round(num * 100) / 100 : num;
|
||||
}
|
||||
self.data[fieldName] = value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
};
|
||||
|
||||
saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); };
|
||||
input.onkeydown = (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
doSave();
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
const closeHandler = (ev) => {
|
||||
if (!popup.contains(ev.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener("mousedown", closeHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", closeHandler);
|
||||
}, 50);
|
||||
|
||||
return popup;
|
||||
};
|
||||
|
||||
this.openColorPicker = function(fieldName) {
|
||||
// === НАТИВНЫЙ КОЛОРПИКЕР — ПРОСТОЙ И НАДЁЖНЫЙ ===
|
||||
this.openColorPicker = function(fieldName, clickEvent) {
|
||||
const currentColor = self.data[fieldName] || '#FFFFFF';
|
||||
console.log("🎨 Opening color picker:", fieldName, currentColor);
|
||||
|
||||
const colorInput = document.createElement('input');
|
||||
colorInput.type = 'color';
|
||||
colorInput.value = currentColor;
|
||||
colorInput.style.display = 'none';
|
||||
document.body.appendChild(colorInput);
|
||||
|
||||
colorInput.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const newColor = e.target.value.toUpperCase();
|
||||
self.data[fieldName] = newColor;
|
||||
console.log("🎨 Color changed:", newColor);
|
||||
self.updateUI();
|
||||
}, { once: false });
|
||||
colorInput.remove();
|
||||
}, { once: true });
|
||||
|
||||
const widgetY = self.widgetYPositions[fieldName] || 200;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect() ||
|
||||
document.querySelector("canvas")?.getBoundingClientRect();
|
||||
|
||||
if (canvasRect && self.pos) {
|
||||
const pickerX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const pickerY = canvasRect.top + self.pos[1] + widgetY;
|
||||
colorInput.style.position = 'fixed';
|
||||
colorInput.style.left = pickerX + 'px';
|
||||
colorInput.style.top = pickerY + 'px';
|
||||
colorInput.style.zIndex = '10002';
|
||||
colorInput.style.width = '1px';
|
||||
colorInput.style.height = '1px';
|
||||
colorInput.style.opacity = '0';
|
||||
console.log("📍 Picker positioned at:", pickerX, pickerY);
|
||||
requestAnimationFrame(() => {
|
||||
if (colorInput.showPicker) {
|
||||
colorInput.showPicker();
|
||||
} else {
|
||||
colorInput.click();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
colorInput.style.position = 'fixed';
|
||||
colorInput.style.left = (window.innerWidth / 2) + 'px';
|
||||
colorInput.style.top = (window.innerHeight / 2) + 'px';
|
||||
colorInput.style.zIndex = '10002';
|
||||
requestAnimationFrame(() => {
|
||||
if (colorInput.showPicker) {
|
||||
colorInput.showPicker();
|
||||
} else {
|
||||
colorInput.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clickOutsideHandler = (e) => {
|
||||
if (e.target !== colorInput) {
|
||||
colorInput.remove();
|
||||
document.removeEventListener("mousedown", clickOutsideHandler);
|
||||
}
|
||||
};
|
||||
// Открываем нативный пикер
|
||||
setTimeout(() => {
|
||||
document.addEventListener("mousedown", clickOutsideHandler);
|
||||
}, 200);
|
||||
if (colorInput.showPicker) {
|
||||
colorInput.showPicker();
|
||||
} else {
|
||||
colorInput.click();
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
this.syncData = function() {
|
||||
|
||||
Reference in New Issue
Block a user