From 0758da015bbb289d3ea47d8a9b069780093c17aa Mon Sep 17 00:00:00 2001 From: IAMCCS Date: Thu, 13 Nov 2025 18:16:57 +0100 Subject: [PATCH] v1.0.0 - Minor bug fixes, add pen-mode toogle --- README.md | 1 + js/index.js | 231 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 206 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 7ee3d0b..107b24d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A powerful drawing and annotation tool for ComfyUI that lets you add notes, sket - Dashed line option for different line styles - Constant width option (maintains width at any zoom level) - HiDPI ×2 rendering toggle for crisper strokes +- pen-only mode (4 tablet use) 🎨 **Layer System** - Create unlimited layers for organized annotations diff --git a/js/index.js b/js/index.js index 65ade48..26fff6e 100644 --- a/js/index.js +++ b/js/index.js @@ -20,7 +20,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { const state = { enabled: false, color: '#ff4444', - width: 15, + width: 7, paths: [], // {points:[{x,y}], color, width} - kept for backward compatibility current: null, saveWithWorkflow: true, @@ -29,15 +29,18 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { constantScreen: false, dashed: false, hidden: false, + penOnly: false, + activePointerId: null, hiDPIx2: false, // Per-mode memory for brush and opacity - widthDraw: 15, + widthDraw: 7, widthErase: 48, opacityDraw: 1.0, opacityErase: 1.0, // Layer system currentLayerIdx: 0, - layers: [{ name: 'Layer 1', visible: true, locked: false, paths: [] }], + layers: [{ name: 'Layer 1', visible: true, locked: false, paths: [], style: { color: '#ff4444', dashed: false, widthDraw: 7, widthErase: 48, opacityDraw: 1.0, opacityErase: 1.0 } }], + hydrated: false, uiShown: false, }; @@ -150,6 +153,8 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { if (hidpi) hidpi.checked = !!state.hiDPIx2; const hidden = document.getElementById('iam_hidden'); if (hidden) hidden.checked = !!state.hidden; + const penonly = document.getElementById('iam_penonly'); + if (penonly) penonly.checked = !!state.penOnly; } catch {} // Sync checkboxes in context menu try { @@ -162,6 +167,8 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { if (hdp) hdp.checked = !!state.hiDPIx2; const hid = ui.contextMenu.querySelector('#ctx_hidden'); if (hid) hid.checked = !!state.hidden; + const pen = ui.contextMenu.querySelector('#ctx_penonly'); + if (pen) pen.checked = !!state.penOnly; } } catch {} } @@ -218,18 +225,23 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { function setEraserMode(on) { const next = !!on; if (state.eraser === next) return; + const sty = getCurrentLayerStyle(); if (next) { // switching to eraser: remember current draw values, apply eraser values - state.widthDraw = state.width; - state.opacityDraw = state.opacity; - state.width = state.widthErase || state.width; - state.opacity = (typeof state.opacityErase === 'number') ? state.opacityErase : state.opacity; + sty.widthDraw = state.width; + sty.opacityDraw = state.opacity; + state.widthDraw = sty.widthDraw; + state.opacityDraw = sty.opacityDraw; + state.width = sty.widthErase || state.width; + state.opacity = (typeof sty.opacityErase === 'number') ? sty.opacityErase : state.opacity; } else { // switching to draw: remember current eraser values, apply draw values - state.widthErase = state.width; - state.opacityErase = state.opacity; - state.width = state.widthDraw || state.width; - state.opacity = (typeof state.opacityDraw === 'number') ? state.opacityDraw : state.opacity; + sty.widthErase = state.width; + sty.opacityErase = state.opacity; + state.widthErase = sty.widthErase; + state.opacityErase = sty.opacityErase; + state.width = sty.widthDraw || state.width; + state.opacity = (typeof sty.opacityDraw === 'number') ? sty.opacityDraw : state.opacity; } state.eraser = next; if (ui.eraserBtn) { @@ -283,7 +295,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) {
- +
Brush: 15px
@@ -311,6 +323,10 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { Hide notes +
@@ -340,6 +356,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { const dashedChk = section.querySelector('#iam_dashed'); const hidpiChk = section.querySelector('#iam_hidpi'); const hiddenChk = section.querySelector('#iam_hidden'); + const penOnlyChk = section.querySelector('#iam_penonly'); const saveChk = section.querySelector('#iam_save_with_wf'); const saveNow = section.querySelector('#iam_save_now'); const loadNow = section.querySelector('#iam_load_now'); @@ -389,6 +406,9 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { hiddenChk.addEventListener('change', () => { state.hidden = !!hiddenChk.checked; app?.canvas?.setDirty(true, true); + }); + penOnlyChk.addEventListener('change', () => { + state.penOnly = !!penOnlyChk.checked; }); saveChk?.addEventListener('change', () => { state.saveWithWorkflow = !!saveChk.checked; @@ -445,7 +465,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) {
- +
Brush: 15px
@@ -473,6 +493,10 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { Hide notes +
@@ -499,6 +523,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { const dashedChk = panel.querySelector('#iam_dashed'); const hidpiChk = panel.querySelector('#iam_hidpi'); const hiddenChk = panel.querySelector('#iam_hidden'); + const penOnlyChk = panel.querySelector('#iam_penonly'); const saveChk = panel.querySelector('#iam_save_with_wf'); const saveNow = panel.querySelector('#iam_save_now'); const loadNow = panel.querySelector('#iam_load_now'); @@ -545,6 +570,9 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { state.hidden = !!hiddenChk.checked; app?.canvas?.setDirty(true, true); }); + penOnlyChk.addEventListener('change', () => { + state.penOnly = !!penOnlyChk.checked; + }); saveChk?.addEventListener('change', () => { state.saveWithWorkflow = !!saveChk.checked; if (state.saveWithWorkflow) persistToGraphExtra(); else removeFromGraphExtra(); @@ -582,9 +610,78 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { return panel; } + function ensureStateHydratedFromExisting() { + try { + if (state.hydrated) return; + const existing = app?.graph?.extra?.iamccs_annotations; + if (!existing) return; + // Build merged layers from existing + let mergedLayers = []; + if (Array.isArray(existing.layers) && existing.layers.length) { + mergedLayers = existing.layers.map((layer) => ({ + name: layer.name || 'Layer', + visible: layer.visible !== false, + locked: !!layer.locked, + paths: Array.isArray(layer.paths) ? layer.paths.map(p => ({ + color: p.color || '#ff4444', + width: p.width || 3, + opacity: (typeof p.opacity === 'number') ? p.opacity : 1, + mode: p.mode === 'erase' ? 'erase' : 'draw', + dashed: !!p.dashed, + points: Array.isArray(p.points) ? p.points.map(pt => ({ x: pt.x, y: pt.y })) : [], + })) : [], + style: (layer.style && typeof layer.style === 'object') ? { + color: typeof layer.style.color === 'string' ? layer.style.color : (state.color || '#ff4444'), + dashed: !!layer.style.dashed, + widthDraw: (typeof layer.style.widthDraw === 'number') ? layer.style.widthDraw : (state.widthDraw || 7), + widthErase: (typeof layer.style.widthErase === 'number') ? layer.style.widthErase : (state.widthErase || 48), + opacityDraw: (typeof layer.style.opacityDraw === 'number') ? layer.style.opacityDraw : (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), + opacityErase: (typeof layer.style.opacityErase === 'number') ? layer.style.opacityErase : (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0), + } : { + color: state.color || '#ff4444', dashed: !!state.dashed, + widthDraw: state.widthDraw || 7, widthErase: state.widthErase || 48, + opacityDraw: (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), + opacityErase: (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0), + }, + })); + } else if (Array.isArray(existing.paths)) { + mergedLayers = [{ name: 'Layer 1', visible: true, locked: false, paths: existing.paths.map(p => ({ + color: p.color || '#ff4444', + width: p.width || 3, + opacity: (typeof p.opacity === 'number') ? p.opacity : 1, + mode: p.mode === 'erase' ? 'erase' : 'draw', + dashed: !!p.dashed, + points: Array.isArray(p.points) ? p.points.map(pt => ({ x: pt.x, y: pt.y })) : [], + })), style: { color: state.color || '#ff4444', dashed: !!state.dashed, widthDraw: state.widthDraw || 7, widthErase: state.widthErase || 48, opacityDraw: (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), opacityErase: (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0) } }]; + } + // Overlay current, appending new paths and style tweaks + for (let i = 0; i < state.layers.length; i++) { + const cur = state.layers[i]; + ensureLayerStyle(cur); + if (!mergedLayers[i]) { + mergedLayers[i] = { name: cur.name || `Layer ${i+1}`, visible: cur.visible !== false, locked: !!cur.locked, paths: [], style: { ...cur.style } }; + } + if (Array.isArray(cur.paths) && cur.paths.length) mergedLayers[i].paths.push(...cur.paths); + // Update style with current layer's style as most recent + mergedLayers[i].style = { ...mergedLayers[i].style, ...cur.style }; + } + state.layers = mergedLayers; + // Flatten paths for backward compatibility + state.paths = []; + for (const lyr of state.layers) for (const p of lyr.paths) state.paths.push(p); + // Clamp current layer index + if (state.currentLayerIdx >= state.layers.length) state.currentLayerIdx = Math.max(0, state.layers.length - 1); + state.hydrated = true; + } catch (e) { + console.warn('[IAMCCS] ensureStateHydratedFromExisting failed:', e); + } + } + function persistToGraphExtra(force = false) { if (!app?.graph) return; if (!state.saveWithWorkflow && !force) return; + // Merge existing annotations if we haven't hydrated yet (prevents overwriting) + ensureStateHydratedFromExisting(); app.graph.extra = app.graph.extra || {}; app.graph.extra.iamccs_annotations = { version: 2, @@ -624,6 +721,14 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { dashed: !!p.dashed, points: Array.isArray(p.points) ? p.points.map(pt => ({ x: pt.x, y: pt.y })) : [], })) : [], + style: (layer.style && typeof layer.style === 'object') ? { + color: typeof layer.style.color === 'string' ? layer.style.color : (state.color || '#ff4444'), + dashed: !!layer.style.dashed, + widthDraw: (typeof layer.style.widthDraw === 'number') ? layer.style.widthDraw : (state.widthDraw || 7), + widthErase: (typeof layer.style.widthErase === 'number') ? layer.style.widthErase : (state.widthErase || 48), + opacityDraw: (typeof layer.style.opacityDraw === 'number') ? layer.style.opacityDraw : (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), + opacityErase: (typeof layer.style.opacityErase === 'number') ? layer.style.opacityErase : (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0), + } : { color: state.color || '#ff4444', dashed: !!state.dashed, widthDraw: state.widthDraw || 7, widthErase: state.widthErase || 48, opacityDraw: (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), opacityErase: (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0) }, })); state.currentLayerIdx = typeof data.currentLayerIdx === 'number' ? data.currentLayerIdx : 0; } else if (Array.isArray(data.paths)) { @@ -637,13 +742,23 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { points: Array.isArray(p.points) ? p.points.map(pt => ({ x: pt.x, y: pt.y })) : [], })); // Also populate first layer - if (state.layers[0]) state.layers[0].paths = [...state.paths]; + if (state.layers[0]) { + ensureLayerStyle(state.layers[0]); + state.layers[0].paths = [...state.paths]; + } else { + state.layers = [{ name: 'Layer 1', visible: true, locked: false, paths: [...state.paths], style: { color: state.color || '#ff4444', dashed: !!state.dashed, widthDraw: state.widthDraw || 7, widthErase: state.widthErase || 48, opacityDraw: (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), opacityErase: (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0) } }]; + } } else { return false; } - + // Refresh flat paths from layers to keep v1 compatibility + state.paths = []; + for (const lyr of state.layers) for (const p of lyr.paths) state.paths.push(p); + state.hydrated = true; app?.canvas?.setDirty(true, true); console.log('[IAMCCS] Annotazioni caricate da workflow.extra'); + // Apply style of selected layer to current brush + try { applyLayerStyleToState(); } catch {} return true; } @@ -688,6 +803,11 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { points: Array.isArray(p.points) ? p.points.map(pt => ({ x: pt.x, y: pt.y })) : [], })); app?.canvas?.setDirty(true, true); + // Put into first layer for compatibility + if (!state.layers[0]) state.layers[0] = { name: 'Layer 1', visible: true, locked: false, paths: [], style: { color: state.color || '#ff4444', dashed: !!state.dashed, widthDraw: state.widthDraw || 7, widthErase: state.widthErase || 48, opacityDraw: (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), opacityErase: (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0) } }; + ensureLayerStyle(state.layers[0]); + state.layers[0].paths = [...state.paths]; + state.hydrated = true; if (state.saveWithWorkflow) persistToGraphExtra(true); console.log('[IAMCCS] Annotations imported:', state.paths.length); } catch (err) { @@ -730,6 +850,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { // Put into the first layer for compatibility if (state.layers[0]) state.layers[0].paths = [...state.paths]; app?.canvas?.setDirty(true, true); + state.hydrated = true; if (state.saveWithWorkflow) persistToGraphExtra(true); console.log('[IAMCCS] Annotations imported (from object):', state.paths.length); return true; @@ -781,20 +902,64 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { state.paths = []; state.current = null; state.currentLayerIdx = 0; - state.layers = [{ name: 'Layer 1', visible: true, locked: false, paths: [] }]; + state.layers = [{ name: 'Layer 1', visible: true, locked: false, paths: [], style: { color: state.color || '#ff4444', dashed: !!state.dashed, widthDraw: state.widthDraw || 7, widthErase: state.widthErase || 48, opacityDraw: (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), opacityErase: (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0) } }]; app?.canvas?.setDirty(true, true); } } }; } catch {} - // Layer management functions + // Layer management + per-layer style + function ensureLayerStyle(layer) { + if (!layer) return; + layer.style = layer.style || {}; + if (typeof layer.style.color !== 'string') layer.style.color = state.color || '#ff4444'; + if (typeof layer.style.dashed !== 'boolean') layer.style.dashed = !!state.dashed; + if (typeof layer.style.widthDraw !== 'number') layer.style.widthDraw = state.widthDraw || 7; + if (typeof layer.style.widthErase !== 'number') layer.style.widthErase = state.widthErase || 48; + if (typeof layer.style.opacityDraw !== 'number') layer.style.opacityDraw = (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0); + if (typeof layer.style.opacityErase !== 'number') layer.style.opacityErase = (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0); + } function getCurrentLayer() { - return state.layers[state.currentLayerIdx]; + const lyr = state.layers[state.currentLayerIdx]; + ensureLayerStyle(lyr); + return lyr; + } + function getCurrentLayerStyle() { + const lyr = getCurrentLayer(); + return lyr?.style || {}; + } + function applyLayerStyleToState() { + const style = getCurrentLayerStyle(); + state.color = style.color; + state.dashed = !!style.dashed; + if (state.eraser) { + state.width = style.widthErase; + state.opacity = style.opacityErase; + } else { + state.width = style.widthDraw; + state.opacity = style.opacityDraw; + } + state.widthDraw = style.widthDraw; + state.widthErase = style.widthErase; + state.opacityDraw = style.opacityDraw; + state.opacityErase = style.opacityErase; + syncFlagsUI(); + syncBrushOpacityUI(); + syncUI(); + app?.canvas?.setDirty(true, true); } function addLayer() { const layerNum = state.layers.length + 1; - state.layers.push({ name: `Layer ${layerNum}`, visible: true, locked: false, paths: [] }); + const base = { ...getCurrentLayerStyle() }; + state.layers.push({ name: `Layer ${layerNum}`, visible: true, locked: false, paths: [], style: { + color: typeof base.color === 'string' ? base.color : (state.color || '#ff4444'), + dashed: !!base.dashed, + widthDraw: typeof base.widthDraw === 'number' ? base.widthDraw : (state.widthDraw || 7), + widthErase: typeof base.widthErase === 'number' ? base.widthErase : (state.widthErase || 48), + opacityDraw: typeof base.opacityDraw === 'number' ? base.opacityDraw : (typeof state.opacityDraw === 'number' ? state.opacityDraw : 1.0), + opacityErase: typeof base.opacityErase === 'number' ? base.opacityErase : (typeof state.opacityErase === 'number' ? state.opacityErase : 1.0), + } }); state.currentLayerIdx = state.layers.length - 1; app?.canvas?.setDirty(true, true); return state.layers.length - 1; @@ -816,7 +981,7 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { function setCurrentLayer(idx) { if (idx >= 0 && idx < state.layers.length) { state.currentLayerIdx = idx; - app?.canvas?.setDirty(true, true); + applyLayerStyleToState(); } } @@ -879,15 +1044,17 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { setEnabled(!state.enabled); } }, true); - // Right-click toggles context menu + // Right-click toggles context menu (and always closes if currently open) btn.addEventListener('contextmenu', (e) => { e.preventDefault(); - if (ui.contextMenu && ui.contextAnchor && ui.contextAnchor.anchor === btn) { + // If any menu is open, close it and stop propagation so global right-click closer doesn't interfere + if (ui.contextMenu) { + e.stopPropagation(); closeContextMenu('toggle-btn'); - } else { - showContextMenu(e.clientX, e.clientY, btn); + return; } - }); + showContextMenu(e.clientX, e.clientY, btn); + }, true); document.body.appendChild(btn); ui.floating = btn; // Make draggable (whole button) and move menu with it if open @@ -981,6 +1148,10 @@ if (!window.IAMCCS_ANNOTATE_LOADED) { Hide notes +