More spline editor work

This commit is contained in:
Kijai 2024-04-15 19:09:03 +03:00
parent b1c10499fe
commit bdf21c4201
3 changed files with 185 additions and 252 deletions

2
kjweb_async/d3.v6.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -4511,69 +4511,51 @@ class SplineEditor:
return { return {
"required": { "required": {
"coordinates": ("STRING", {"multiline": True}), "coordinates": ("STRING", {"multiline": True}),
"mask_width": ("INT", {"default": 512, "min": 8, "max": MAX_RESOLUTION, "step": 8}),
"mask_height": ("INT", {"default": 512, "min": 8, "max": MAX_RESOLUTION, "step": 18}),
"points_to_sample": ("INT", {"default": 4, "min": 2, "max": 1000, "step": 1}),
"interpolation": (
[
'cardinal',
'monotone',
'basis',
'linear',
'step-before',
'step-after',
],
{
"default": 'cardinal'
}),
}, },
} }
RETURN_TYPES = ("STRING", "FLOAT") RETURN_TYPES = ("MASK", "STRING", "FLOAT")
FUNCTION = "splinedata" FUNCTION = "splinedata"
CATEGORY = "KJNodes/experimental" CATEGORY = "KJNodes/experimental"
def splinedata(self, coordinates): def splinedata(self, mask_width, mask_height, coordinates, interpolation, points_to_sample):
coordinates = self.parse_custom_format(coordinates) coordinates = json.loads(coordinates)
print(coordinates) print(coordinates)
# Step 1: Calculate distance from bottom for each point
distances_from_bottom = [512 - point['y'] for point in coordinates]
# Step 2: Normalize the values normalized_y_values = [
max_distance = max(distances_from_bottom) 1.0 - (point['y'] / 512)
normalized_values = [distance / max_distance for distance in distances_from_bottom] for point in coordinates
# Step 3: Calculate distances between points
distances_between_points = [
abs(coordinates[i+1]['x'] - coordinates[i]['x']) for i in range(len(coordinates)-1)
] ]
# Step 4: Interpolate x distances based on normalized distances from bottom # Create a color map for grayscale intensities
interpolated_x_distances = [ color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32)
distance * normalized_value for distance, normalized_value in zip(distances_between_points, normalized_values[:-1])
]
print(interpolated_x_distances) # Create image tensors for each normalized y value
return (coordinates, interpolated_x_distances,) image_tensors = [color_map(y) for y in normalized_y_values]
def parse_custom_format(self, coords_str):
# Remove the square brackets and split the string into key-value pairs # Batch the tensors
pairs = coords_str.strip('[]').split(',') masks_out = torch.stack(image_tensors)
print(pairs) masks_out = masks_out.mean(dim=-1)
print(masks_out.shape)
# Initialize an empty list to hold the dictionaries return (masks_out, coordinates, normalized_y_values,)
coordinates = []
# Initialize an empty dictionary to hold the current point
current_point = {}
# Iterate over the pairs
for pair in pairs:
# Split the pair into key and value
key, value = pair.strip('"').split(':')
print(key)
# Strip any whitespace and convert the value to float
key = key.strip('"')
value = float(value)
print(value)
# Add the key-value pair to the current point
current_point[key] = value
# If the current point has both 'x' and 'y' keys, it's complete
if 'x' in current_point and 'y' in current_point:
# Add the current point to the list of coordinates
coordinates.append(current_point)
# Reset the current point for the next iteration
current_point = {}
return coordinates
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"INTConstant": INTConstant, "INTConstant": INTConstant,

View File

@ -60,34 +60,8 @@ export const loadScript = (
border-style: solid; border-style: solid;
border-width: medium; border-width: medium;
border-color: var(--border-color); border-color: var(--border-color);
height: 512px; height: 544px;
width: 512px; width: 544px;
max-height: 100%;
/* Scrollbar styling for Chrome */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: var(--bg-color);
}
&::-webkit-scrollbar-thumb {
background-color: var(--fg-color);
border-radius: 6px;
border: 3px solid var(--bg-color);
}
/* Scrollbar styling for Firefox */
scrollbar-width: thin;
scrollbar-color: var(--fg-color) var(--bg-color);
a {
color: yellow;
}
a:visited {
color: orange;
}
a:hover {
color: red;
}
} }
` `
document.head.appendChild(styleTag) document.head.appendChild(styleTag)
@ -97,76 +71,52 @@ export const loadScript = (
loadScript('/kjweb_async/svg-path-properties.min.js').catch((e) => { loadScript('/kjweb_async/svg-path-properties.min.js').catch((e) => {
console.log(e) console.log(e)
}) })
loadScript('/kjweb_async/protovis.min.js').catch((e) => {
class SplineEditorWidget { console.log(e)
constructor(inputName, defaultValue) { })
//this.name = inputName || "Spline"; create_documentation_stylesheet()
//this._value = defaultValue || [{ x: 0, y: 0 }];
this.type = "SPLINE";
//this.selectedPointIndex = null;
this.resize
}
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({ app.registerExtension({
name: 'KJNodes.curves', name: 'KJNodes.curves',
async beforeRegisterNodeDef(nodeType, nodeData) { async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name == 'SplineEditor') { if (nodeData.name == 'SplineEditor') {
addElement(nodeData, nodeType); addElement(nodeData, nodeType);
} }
}, },
// getCustomWidgets: function () {
// return {
// SPLINE: (node, inputName, inputData, app) => {
// console.log(inputName)
// console.log(inputData)
// console.log("Registering KJNodes curve widget");
// return {
// widget: node.addCustomWidget(
// new SplineEditorWidget(inputName, inputData[1]?.default)
// ),
// }
// }
// }
// }
}) })
export const addElement = (nodeData,nodeType, opts = { icon_size: 24, icon_margin: 4 }) => { export const addElement = (nodeData,nodeType) => {
opts = opts || {}
const iconSize = opts.icon_size ? opts.icon_size : 14
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
console.log("Creating spline editor") console.log("Creating spline editor")
const iconSize = 24
const iconMargin = 4
let splineEditor = null let splineEditor = null
let vis = null let vis = null
const drawFg = nodeType.prototype.onDrawForeground const drawFg = nodeType.prototype.onDrawForeground
nodeType.prototype.onNodeCreated = function () { nodeType.prototype.onNodeCreated = function () {
this.coordWidget = this.widgets.find(w => w.name === "coordinates"); console.log("Node created")
this.coordWidget = this.widgets.find(w => w.name === "coordinates");
this.interpolationWidget = this.widgets.find(w => w.name === "interpolation");
this.pointsWidget = this.widgets.find(w => w.name === "points_to_sample");
}
nodeType.prototype.onRemoved = function () {
console.log("Node removed")
if (splineEditor !== null) {
splineEditor.parentNode.removeChild(splineEditor)
splineEditor = null
}
} }
nodeType.prototype.onDrawForeground = function (ctx) { nodeType.prototype.onDrawForeground = function (ctx) {
const r = drawFg ? drawFg.apply(this, arguments) : undefined console.log("Drawing foreground")
if (this.flags.collapsed) return r const r = drawFg ? drawFg.apply(this, arguments) : undefined
const x = this.size[0] - iconSize - iconMargin if (this.flags.collapsed) return r
if (splineEditor === null) {
create_documentation_stylesheet() const x = this.size[0] - iconSize - iconMargin
if (this.show_doc && splineEditor === null) {
console.log("Drawing spline editor")
splineEditor = document.createElement('div'); splineEditor = document.createElement('div');
splineEditor.classList.add('spline-editor'); splineEditor.classList.add('spline-editor');
@ -185,141 +135,153 @@ export const addElement = (nodeData,nodeType, opts = { icon_size: 24, icon_margi
this.show_doc = !this.show_doc this.show_doc = !this.show_doc
splineEditor.parentNode.removeChild(splineEditor) splineEditor.parentNode.removeChild(splineEditor)
splineEditor = null splineEditor = null
}); });
splineEditor.appendChild(closeButton) splineEditor.appendChild(closeButton)
document.body.appendChild(splineEditor)
var w = 512 var w = 512
var h = 512 var h = 512
var i = 3 var i = 3
loadScript('/kjweb_async/protovis.min.js').then(() => {
console.log('Protovis loaded successfully'); if (points == null) {
var points = pv.range(1, 5).map(i => ({
var points = pv.range(1, 5).map(i => ({
x: i * w / 5, x: i * w / 5,
y: 50 + Math.random() * (h - 100) y: 50 + Math.random() * (h - 100)
})); }));
var interpolate = "cardinal" }
var segmented = false var segmented = false
vis = new pv.Panel() vis = new pv.Panel()
.width(w) .width(w)
.height(h) .height(h)
.fillStyle("var(--comfy-menu-bg)") .fillStyle("var(--comfy-menu-bg)")
//.strokeStyle("orange") .strokeStyle("orange")
.lineWidth(0) .lineWidth(0)
.antialias(false) .antialias(false)
.margin(2) .margin(10)
.event("mousedown", function() { .event("mousedown", function() {
if (pv.event.shiftKey) { // Use pv.event to access the event object
i = points.push(this.mouse()) - 1; i = points.push(this.mouse()) - 1;
return this; return this;
}); }
});
vis.add(pv.Rule) vis.add(pv.Rule)
.data(pv.range(0, 8, .5)) .data(pv.range(0, 8, .5))
.bottom(d => d * 70 + 9.5) .bottom(d => d * 70 + 9.5)
.strokeStyle("white") .strokeStyle("gray")
.lineWidth(2) .lineWidth(1)
vis.add(pv.Line) vis.add(pv.Line)
.data(() => points) .data(() => points)
.left(d => d.x) .left(d => d.x)
.top(d => d.y) .top(d => d.y)
.interpolate(() => interpolate) .interpolate(() => this.interpolationWidget.value)
.segmented(() => segmented) .segmented(() => segmented)
.strokeStyle(pv.Colors.category10().by(pv.index)) .strokeStyle(pv.Colors.category10().by(pv.index))
.tension(0.5) .tension(0.5)
.lineWidth(3); .lineWidth(3)
vis.add(pv.Dot) vis.add(pv.Dot)
.data(() => points) .data(() => points)
.left(d => d.x) .left(d => d.x)
.top(d => d.y) .top(d => d.y)
.radius(7) .radius(7)
.cursor("move") .cursor("move")
.strokeStyle(function() { return i == this.index ? "#ff7f0e" : "#1f77b4"; }) .strokeStyle(function() { return i == this.index ? "#ff7f0e" : "#1f77b4"; })
.fillStyle(function() { return "rgba(100, 100, 100, 0.2)"; }) .fillStyle(function() { return "rgba(100, 100, 100, 0.2)"; })
//.anchor("center").add(pv.Label) .event("mousedown", pv.Behavior.drag())
//.font(d => Math.sqrt(d[2]) * 20 + "px sans-serif") .event("dragstart", function() {
// .text(d => d[2]) i = this.index;
.event("mousedown", pv.Behavior.drag()) return this;
.event("dragstart", function() { })
i = this.index; .event("drag", vis)
return this; .anchor("top").add(pv.Label)
.font(d => Math.sqrt(d[2]) * 32 + "px sans-serif")
//.text(d => `(${Math.round(d.x)}, ${Math.round(d.y)})`)
.text(d => {
// Normalize y to range 0.0 to 1.0, considering the inverted y-axis
var normalizedY = 1.0 - (d.y / h);
return `${normalizedY.toFixed(2)}`;
}) })
.event("drag", vis) .textStyle("orange")
pv.listen(window, "mousedown", () => { //disable context menu on right click
document.addEventListener('contextmenu', function(e) {
if (e.button === 2) { // Right mouse button
e.preventDefault();
e.stopPropagation();
}
})
//right click remove dot
pv.listen(window, "mousedown", () => {
window.focus(); window.focus();
//logPathLength(); if (pv.event.button === 2) {
points.splice(i--, 1);
vis.render();
}
}); });
pv.listen(window, "mouseup", () => { //send coordinates to node on mouseup
//logPathLength(); pv.listen(window, "mouseup", () => {
if (svgPathElement !== null) { if (pathElements !== null) {
let coords = samplePoints(svgPathElement, points.length); let coords = samplePoints(pathElements[0], this.pointsWidget.value);
let coordsString = JSON.stringify(coords); let coordsString = JSON.stringify(coords);
if (this.coordWidget) this.coordWidget.value = coordsString; if (this.coordWidget) {
this.coordWidget.value = coordsString;
}
} }
}); });
pv.listen(window, "keydown", (e) => {
console.log("key code: " + e.keyCode)
// code 8 is backspace, code 46 is delete
if ((e.keyCode == 16 || e.keyCode == 46) && (i >= 0)) {
points.splice(i--, 1);
vis.render();
e.preventDefault();
}
})
vis.render(); vis.render();
var svgElement = vis.canvas(); var svgElement = vis.canvas();
var svgPathElement = document.querySelector('path'); splineEditor.appendChild(svgElement);
splineEditor.appendChild(svgElement); // this.addDOMWidget("videopreview", "preview", splineEditor, {
// serialize: false,
}) // hideOnZoom: false,
.catch((error) => {
console.error('Failed to load Protovis:', error); // });
}); document.body.appendChild(splineEditor)
} var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements
if (this.show_doc && splineEditor !== null && vis !== null) { }
const rect = ctx.canvas.getBoundingClientRect() // close the popup
const scaleX = rect.width / ctx.canvas.width else if (!this.show_doc && splineEditor !== null) {
const scaleY = rect.height / ctx.canvas.height splineEditor.parentNode.removeChild(splineEditor)
splineEditor = null
const transform = new DOMMatrix() }
.scaleSelf(scaleX -0.05, scaleY -0.05)
.multiplySelf(ctx.getTransform()) if (this.show_doc && splineEditor !== null && vis !== null) {
.translateSelf(470, -10) const rect = ctx.canvas.getBoundingClientRect()
const scaleX = rect.width / ctx.canvas.width
const scale = new DOMMatrix() const scaleY = rect.height / ctx.canvas.height
.scaleSelf(transform.a, transform.d);
const transform = new DOMMatrix()
.scaleSelf(scaleX, scaleY)
.translateSelf(this.size[0] * scaleX, 0)
.multiplySelf(ctx.getTransform())
.translateSelf(10, -32)
const scale = new DOMMatrix()
.scaleSelf(transform.a, transform.d);
const styleObject = { const styleObject = {
transformOrigin: '0 0', transformOrigin: '0 0',
transform: scale, transform: scale,
left: `${transform.a + transform.e}px`, left: `${transform.a + transform.e}px`,
top: `${transform.d + transform.f}px`, top: `${transform.d + transform.f}px`,
}; };
Object.assign(splineEditor.style, styleObject); Object.assign(splineEditor.style, styleObject);
}
//vis.render(); ctx.save()
ctx.translate(x - 2, iconSize - 45)
//logPathLength(svgPathElement); ctx.scale(iconSize / 32, iconSize / 32)
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
ctx.lineCap = 'round'
} ctx.lineJoin = 'round'
ctx.save() ctx.lineWidth = 2.4
ctx.translate(x - 2, iconSize - 45) ctx.font = 'bold 36px monospace'
ctx.scale(iconSize / 32, iconSize / 32) ctx.fillStyle = 'orange';
ctx.strokeStyle = 'rgba(255,255,255,0.3)' ctx.fillText('📈', 0, 24)
ctx.lineCap = 'round' ctx.restore()
ctx.lineJoin = 'round' return r
ctx.lineWidth = 2.4
ctx.font = 'bold 36px monospace'
ctx.fillStyle = 'orange';
ctx.fillText('📈', 0, 24)
ctx.restore()
return r
} }
// handle clicking of the icon // handle clicking of the icon
const mouseDown = nodeType.prototype.onMouseDown const mouseDown = nodeType.prototype.onMouseDown
@ -342,22 +304,9 @@ export const addElement = (nodeData,nodeType, opts = { icon_size: 24, icon_margi
} }
return r; return r;
} }
} }
function logPathLength(svgPathElement) {
//var svgPathElement = document.querySelector('path');
// If the SVG path element exists, get the path string from the 'd' attribute
if (svgPathElement) {
var pathString = svgPathElement.getAttribute('d');
// Now, you can use the svg-path-properties library on this path string.
const properties = new svgPathProperties.svgPathProperties(pathString);
const length = properties.getTotalLength();
// ...and other properties as needed
console.log(length);
}
}
function samplePoints(svgPathElement, numSamples) { function samplePoints(svgPathElement, numSamples) {
var pathLength = svgPathElement.getTotalLength(); var pathLength = svgPathElement.getTotalLength();