// Reference the shared typedefs file /// import { app } from '../../scripts/app.js' import { infoLogger } from './comfy_shared.js' function B0(t) { return (1 - t) ** 3 / 6 } function B1(t) { return (3 * t ** 3 - 6 * t ** 2 + 4) / 6 } function B2(t) { return (-3 * t ** 3 + 3 * t ** 2 + 3 * t + 1) / 6 } function B3(t) { return t ** 3 / 6 } class CurveWidget { constructor(...args) { const [inputName, opts] = args this.name = inputName || 'Curve' this.type = 'FLOAT_CURVE' this.selectedPointIndex = null this.options = opts this.value = this.value || { 0: { x: 0, y: 0 }, 1: { x: 1, y: 1 } } } drawBSpline(ctx, width, height, posY) { const n = this.value.length - 1 const numSegments = n - 2 const numPoints = this.value.length if (numPoints < 4) { this.drawLinear(ctx, width, height, posY) } else { for (let j = 0; j <= numSegments; j++) { for (let t = 0; t <= 1; t += 0.01) { let pt = this.getBSplinePoint(j, t) let x = pt.x * width let y = posY + height - pt.y * height if (t === 0) ctx.moveTo(x, y) else ctx.lineTo(x, y) } } ctx.stroke() } } drawLinear(ctx, width, height, posY) { for (let i = 0; i < Object.keys(this.value).length - 1; i++) { let p1 = this.value[i] let p2 = this.value[i + 1] ctx.moveTo(p1.x * width, posY + height - p1.y * height) ctx.lineTo(p2.x * width, posY + height - p2.y * height) } ctx.stroke() } getBSplinePoint(i, t) { // Control points for this segment const p0 = this.value[i] const p1 = this.value[i + 1] const p2 = this.value[i + 2] const p3 = this.value[i + 3] const x = B0(t) * p0.x + B1(t) * p1.x + B2(t) * p2.x + B3(t) * p3.x const y = B0(t) * p0.y + B1(t) * p1.y + B2(t) * p2.y + B3(t) * p3.y return { x, y } } /** * @param {OnDrawWidgetParams} args */ draw(...args) { const hide = this.type !== 'FLOAT_CURVE' if (hide) { return } const [ctx, node, width, posY, height] = args const [cw, ch] = this.computeSize(width) ctx.beginPath() ctx.fillStyle = '#000' ctx.strokeStyle = '#fff' ctx.lineWidth = 2 // normalized coordinates -> canvas coordinates for (let i = 0; i < Object.keys(this.value || {}).length - 1; i++) { let p1 = this.value[i] let p2 = this.value[i + 1] ctx.moveTo(p1.x * cw, posY + ch - p1.y * ch) ctx.lineTo(p2.x * cw, posY + ch - p2.y * ch) } ctx.stroke() // points Object.values(this.value || {}).forEach((point) => { ctx.beginPath() ctx.arc(point.x * cw, posY + ch - point.y * ch, 5, 0, 2 * Math.PI) ctx.fill() }) } mouse(event, pos, node) { let x = pos[0] - node.pos[0] let y = pos[1] - node.pos[1] const width = node.size[0] const height = 300 // TODO: compute const posY = node.pos[1] const localPos = { x: pos[0], y: pos[1] - LiteGraph.NODE_WIDGET_HEIGHT } if (event.type === LiteGraph.pointerevents_method + 'down') { console.debug('Checking if a point was clicked') const clickedPointIndex = this.detectPoint(localPos, width, height) if (clickedPointIndex !== null) { this.selectedPointIndex = clickedPointIndex } else { this.addPoint(localPos, width, height) } return true } else if ( event.type === LiteGraph.pointerevents_method + 'move' && this.selectedPointIndex !== null ) { this.movePoint(this.selectedPointIndex, localPos, width, height) return true } else if ( event.type === LiteGraph.pointerevents_method + 'up' && this.selectedPointIndex !== null ) { this.selectedPointIndex = null return true } return false } callback(...args) { //value, that, node, pos, event) { } detectPoint(localPos, width, height) { const threshold = 20 // TODO: extract const keys = Object.keys(this.value) for (let i = 0; i < keys.length; i++) { const key = keys[i] const p = this.value[key] const px = p.x * width const py = height - p.y * height if ( Math.abs(localPos.x - px) < threshold && Math.abs(localPos.y - py) < threshold ) { return key } } return null } addPoint(localPos, width, height) { // add a new point based on click position const normalizedPoint = { x: localPos.x / width, y: 1 - localPos.y / height, } const keys = Object.keys(this.value) let insertIndex = keys.length for (let i = 0; i < keys.length; i++) { if (normalizedPoint.x < this.value[keys[i]].x) { insertIndex = i break } } // shift for (let i = keys.length; i > insertIndex; i--) { this.value[i] = this.value[i - 1] } this.value[insertIndex] = normalizedPoint } movePoint(index, localPos, width, height) { const point = this.value[index] point.x = Math.max(0, Math.min(1, localPos.x / width)) point.y = Math.max(0, Math.min(1, 1 - localPos.y / height)) this.value[index] = point } computeSize(width) { return [width, 300] } configure(data) { } } app.registerExtension({ name: 'mtb.curves', getCustomWidgets: () => { return { /** * @param {LGraphNode} node * @param {str} inputName * @param {[str,*]} inputData * @param {*} app * */ FLOAT_CURVE: (node, inputName, inputData, app) => { // const c = node.widgets.find((w) => w.type === "FLOAT_CURVE") const wid = node.addCustomWidget(new CurveWidget(inputName, inputData)) return { widget: wid, minWidth: 150, minHeight: 30, } }, } }, })