188 lines
5.9 KiB
JavaScript
188 lines
5.9 KiB
JavaScript
|
|
import { app } from '../../scripts/app.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(inputName, defaultValue) {
|
|
this.name = inputName || "Curve";
|
|
this._value = defaultValue || [{ x: 0, y: 0 }, { x: 1, y: 1 }];
|
|
this.type = "FLOAT_CURVE";
|
|
this.selectedPointIndex = null;
|
|
this.resize
|
|
}
|
|
|
|
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 < 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 };
|
|
}
|
|
|
|
draw(ctx, node, width, posY, height) {
|
|
const [cw, ch] = this.computeSize(width)
|
|
|
|
ctx.beginPath();
|
|
ctx.fillStyle = "#000";
|
|
//ctx.fillRect(0, posY, cw, ch);
|
|
ctx.strokeStyle = "#fff";
|
|
ctx.lineWidth = 2;
|
|
|
|
// normalized coordinates -> canvas coordinates
|
|
for (let i = 0; i < 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();
|
|
// this.drawBSpline(ctx, width, height, posY);
|
|
|
|
// points
|
|
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) {
|
|
// console.debug(event.type, pos, node)
|
|
let x = pos[0] - node.pos[0]
|
|
let y = pos[1] - node.pos[1]
|
|
let 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;
|
|
}
|
|
|
|
|
|
detectPoint(localPos, width, height) {
|
|
const threshold = 20; // TODO: extract
|
|
for (let i = 0; i < this._value.length; i++) {
|
|
const p = this._value[i];
|
|
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 i;
|
|
}
|
|
}
|
|
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 };
|
|
this._value.push(normalizedPoint);
|
|
this._value.sort((a, b) => a.x - b.x);
|
|
this.value = JSON.stringify(this._value);
|
|
}
|
|
|
|
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;
|
|
this.value = JSON.stringify(this._value);
|
|
}
|
|
|
|
computeSize(width) {
|
|
return [width, 300];
|
|
}
|
|
|
|
configure(data) {
|
|
console.log(data)
|
|
}
|
|
|
|
value() {
|
|
console.debug('Returning value', this._value)
|
|
return this._value
|
|
}
|
|
setValue(value) {
|
|
console.debug('Setting value', value)
|
|
this._value = value
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: 'mtb.curves',
|
|
getCustomWidgets: function () {
|
|
return {
|
|
FLOAT_CURVE: (node, inputName, inputData, app) => {
|
|
console.debug('Registering float curve widget');
|
|
|
|
return {
|
|
widget: node.addCustomWidget(
|
|
new CurveWidget(inputName, inputData[1]?.default)
|
|
),
|
|
minWidth: 150,
|
|
minHeight: 30,
|
|
}
|
|
},
|
|
|
|
|
|
}
|
|
},
|
|
|
|
})
|