|
import { app } from '../../../scripts/app.js' |
|
|
|
|
|
export function makeUUID() { |
|
let dt = new Date().getTime() |
|
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { |
|
const r = ((dt + Math.random() * 16) % 16) | 0 |
|
dt = Math.floor(dt / 16) |
|
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) |
|
}) |
|
return uuid |
|
} |
|
|
|
export const loadScript = ( |
|
FILE_URL, |
|
async = true, |
|
type = 'text/javascript', |
|
) => { |
|
return new Promise((resolve, reject) => { |
|
try { |
|
|
|
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`) |
|
if (existingScript) { |
|
resolve({ status: true, message: 'Script already loaded' }) |
|
return |
|
} |
|
|
|
const scriptEle = document.createElement('script') |
|
scriptEle.type = type |
|
scriptEle.async = async |
|
scriptEle.src = FILE_URL |
|
|
|
scriptEle.addEventListener('load', (ev) => { |
|
resolve({ status: true }) |
|
}) |
|
|
|
scriptEle.addEventListener('error', (ev) => { |
|
reject({ |
|
status: false, |
|
message: `Failed to load the script ${FILE_URL}`, |
|
}) |
|
}) |
|
|
|
document.body.appendChild(scriptEle) |
|
} catch (error) { |
|
reject(error) |
|
} |
|
}) |
|
} |
|
const create_documentation_stylesheet = () => { |
|
const tag = 'kj-splineditor-stylesheet' |
|
|
|
let styleTag = document.head.querySelector(tag) |
|
|
|
if (!styleTag) { |
|
styleTag = document.createElement('style') |
|
styleTag.type = 'text/css' |
|
styleTag.id = tag |
|
styleTag.innerHTML = ` |
|
.spline-editor { |
|
|
|
position: absolute; |
|
|
|
font: 12px monospace; |
|
line-height: 1.5em; |
|
padding: 10px; |
|
z-index: 0; |
|
overflow: hidden; |
|
} |
|
` |
|
document.head.appendChild(styleTag) |
|
} |
|
} |
|
|
|
loadScript('/kjweb_async/svg-path-properties.min.js').catch((e) => { |
|
console.log(e) |
|
}) |
|
loadScript('/kjweb_async/protovis.min.js').catch((e) => { |
|
console.log(e) |
|
}) |
|
create_documentation_stylesheet() |
|
|
|
function chainCallback(object, property, callback) { |
|
if (object == undefined) { |
|
|
|
console.error("Tried to add callback to non-existant object") |
|
return; |
|
} |
|
if (property in object) { |
|
const callback_orig = object[property] |
|
object[property] = function () { |
|
const r = callback_orig.apply(this, arguments); |
|
callback.apply(this, arguments); |
|
return r |
|
}; |
|
} else { |
|
object[property] = callback; |
|
} |
|
} |
|
app.registerExtension({ |
|
name: 'KJNodes.SplineEditor', |
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData) { |
|
if (nodeData?.name === 'SplineEditor') { |
|
chainCallback(nodeType.prototype, "onNodeCreated", function () { |
|
|
|
hideWidgetForGood(this, this.widgets.find(w => w.name === "coordinates")) |
|
|
|
var element = document.createElement("div"); |
|
this.uuid = makeUUID() |
|
element.id = `spline-editor-${this.uuid}` |
|
|
|
|
|
const fakeimagewidget = this.addWidget("COMBO", "image", null, () => { }, {}); |
|
hideWidgetForGood(this, fakeimagewidget) |
|
|
|
this.splineEditor = this.addDOMWidget(nodeData.name, "SplineEditorWidget", element, { |
|
serialize: false, |
|
hideOnZoom: false, |
|
}); |
|
|
|
|
|
this.contextMenu = document.createElement("div"); |
|
this.contextMenu.className = 'spline-editor-context-menu'; |
|
this.contextMenu.id = "context-menu"; |
|
this.contextMenu.style.display = "none"; |
|
this.contextMenu.style.position = "absolute"; |
|
this.contextMenu.style.backgroundColor = "#202020"; |
|
this.contextMenu.style.minWidth = "100px"; |
|
this.contextMenu.style.boxShadow = "0px 8px 16px 0px rgba(0,0,0,0.2)"; |
|
this.contextMenu.style.zIndex = "100"; |
|
this.contextMenu.style.padding = "5px"; |
|
|
|
function styleMenuItem(menuItem) { |
|
menuItem.style.display = "block"; |
|
menuItem.style.padding = "5px"; |
|
menuItem.style.color = "#FFF"; |
|
menuItem.style.fontFamily = "Arial, sans-serif"; |
|
menuItem.style.fontSize = "16px"; |
|
menuItem.style.textDecoration = "none"; |
|
menuItem.style.marginBottom = "5px"; |
|
} |
|
function createMenuItem(id, textContent) { |
|
let menuItem = document.createElement("a"); |
|
menuItem.href = "#"; |
|
menuItem.id = `menu-item-${id}`; |
|
menuItem.textContent = textContent; |
|
styleMenuItem(menuItem); |
|
return menuItem; |
|
} |
|
|
|
|
|
this.menuItems = [ |
|
createMenuItem(0, "Toggle handles"), |
|
createMenuItem(1, "Display sample points"), |
|
createMenuItem(2, "Switch point shape"), |
|
createMenuItem(3, "Background image"), |
|
createMenuItem(4, "Invert point order"), |
|
createMenuItem(5, "Clear Image"), |
|
]; |
|
|
|
|
|
this.menuItems.forEach(menuItem => { |
|
menuItem.addEventListener('mouseover', function() { |
|
this.style.backgroundColor = "gray"; |
|
}); |
|
|
|
menuItem.addEventListener('mouseout', function() { |
|
this.style.backgroundColor = "#202020"; |
|
}); |
|
}); |
|
|
|
|
|
this.menuItems.forEach(menuItem => { |
|
this.contextMenu.appendChild(menuItem); |
|
}); |
|
|
|
document.body.appendChild(this.contextMenu); |
|
|
|
this.addWidget("button", "New spline", null, () => { |
|
if (!this.properties || !("points" in this.properties)) { |
|
this.editor = new SplineEditor(this); |
|
this.addProperty("points", this.constructor.type, "string"); |
|
} |
|
else { |
|
this.editor = new SplineEditor(this, true); |
|
} |
|
}); |
|
|
|
this.setSize([550, 950]); |
|
this.resizable = false; |
|
this.splineEditor.parentEl = document.createElement("div"); |
|
this.splineEditor.parentEl.className = "spline-editor"; |
|
this.splineEditor.parentEl.id = `spline-editor-${this.uuid}` |
|
element.appendChild(this.splineEditor.parentEl); |
|
|
|
chainCallback(this, "onConfigure", function () { |
|
try { |
|
this.editor = new SplineEditor(this); |
|
} catch (error) { |
|
console.error("An error occurred while configuring the editor:", error); |
|
} |
|
}); |
|
chainCallback(this, "onExecuted", function (message) { |
|
let bg_image = message["bg_image"]; |
|
this.properties.imgData = { |
|
name: "bg_image", |
|
base64: bg_image |
|
}; |
|
this.editor.refreshBackgroundImage(this); |
|
}); |
|
|
|
}); |
|
} |
|
} |
|
}) |
|
|
|
|
|
class SplineEditor{ |
|
constructor(context, reset = false) { |
|
this.node = context; |
|
this.reset=reset; |
|
const self = this; |
|
console.log("creatingSplineEditor") |
|
|
|
this.node.pasteFile = (file) => { |
|
if (file.type.startsWith("image/")) { |
|
this.handleImageFile(file); |
|
return true; |
|
} |
|
return false; |
|
}; |
|
|
|
this.node.onDragOver = function (e) { |
|
if (e.dataTransfer && e.dataTransfer.items) { |
|
return [...e.dataTransfer.items].some(f => f.kind === "file" && f.type.startsWith("image/")); |
|
} |
|
return false; |
|
}; |
|
|
|
|
|
this.node.onDragDrop = (e) => { |
|
console.log("onDragDrop called"); |
|
let handled = false; |
|
for (const file of e.dataTransfer.files) { |
|
if (file.type.startsWith("image/")) { |
|
this.handleImageFile(file); |
|
handled = true; |
|
} |
|
} |
|
return handled; |
|
}; |
|
|
|
|
|
this.createContextMenu(); |
|
|
|
|
|
this.dotShape = "circle"; |
|
this.drawSamplePoints = false; |
|
|
|
if (reset && context.splineEditor.element) { |
|
context.splineEditor.element.innerHTML = ''; |
|
} |
|
this.coordWidget = context.widgets.find(w => w.name === "coordinates"); |
|
this.interpolationWidget = context.widgets.find(w => w.name === "interpolation"); |
|
this.pointsWidget = context.widgets.find(w => w.name === "points_to_sample"); |
|
this.pointsStoreWidget = context.widgets.find(w => w.name === "points_store"); |
|
this.tensionWidget = context.widgets.find(w => w.name === "tension"); |
|
this.minValueWidget = context.widgets.find(w => w.name === "min_value"); |
|
this.maxValueWidget = context.widgets.find(w => w.name === "max_value"); |
|
this.samplingMethodWidget = context.widgets.find(w => w.name === "sampling_method"); |
|
this.widthWidget = context.widgets.find(w => w.name === "mask_width"); |
|
this.heightWidget = context.widgets.find(w => w.name === "mask_height"); |
|
|
|
this.interpolation = this.interpolationWidget.value |
|
this.tension = this.tensionWidget.value |
|
this.points_to_sample = this.pointsWidget.value |
|
this.rangeMin = this.minValueWidget.value |
|
this.rangeMax = this.maxValueWidget.value |
|
this.pointsLayer = null; |
|
this.samplingMethod = this.samplingMethodWidget.value |
|
|
|
if (this.samplingMethod == "path") { |
|
this.dotShape = "triangle" |
|
} |
|
|
|
|
|
this.interpolationWidget.callback = () => { |
|
this.interpolation = this.interpolationWidget.value |
|
this.updatePath(); |
|
} |
|
this.samplingMethodWidget.callback = () => { |
|
this.samplingMethod = this.samplingMethodWidget.value |
|
if (this.samplingMethod == "path") { |
|
this.dotShape = "triangle" |
|
} |
|
else if (this.samplingMethod == "controlpoints") { |
|
this.dotShape = "circle" |
|
this.drawSamplePoints = true; |
|
} |
|
this.updatePath(); |
|
} |
|
this.tensionWidget.callback = () => { |
|
this.tension = this.tensionWidget.value |
|
this.updatePath(); |
|
} |
|
this.pointsWidget.callback = () => { |
|
this.points_to_sample = this.pointsWidget.value |
|
this.updatePath(); |
|
} |
|
this.minValueWidget.callback = () => { |
|
this.rangeMin = this.minValueWidget.value |
|
this.updatePath(); |
|
} |
|
this.maxValueWidget.callback = () => { |
|
this.rangeMax = this.maxValueWidget.value |
|
this.updatePath(); |
|
} |
|
this.widthWidget.callback = () => { |
|
this.width = this.widthWidget.value; |
|
if (this.width > 256) { |
|
context.setSize([this.width + 45, context.size[1]]); |
|
} |
|
this.vis.width(this.width); |
|
this.updatePath(); |
|
} |
|
this.heightWidget.callback = () => { |
|
this.height = this.heightWidget.value |
|
this.vis.height(this.height) |
|
context.setSize([context.size[0], this.height + 430]); |
|
this.updatePath(); |
|
} |
|
this.pointsStoreWidget.callback = () => { |
|
points = JSON.parse(this.pointsStoreWidget.value); |
|
this.updatePath(); |
|
} |
|
|
|
|
|
this.drawHandles = false; |
|
this.drawRuler = true; |
|
var hoverIndex = -1; |
|
var isDragging = false; |
|
this.width = this.widthWidget.value; |
|
this.height = this.heightWidget.value; |
|
var i = 3; |
|
this.points = []; |
|
|
|
if (!reset && this.pointsStoreWidget.value != "") { |
|
this.points = JSON.parse(this.pointsStoreWidget.value); |
|
} else { |
|
this.points = pv.range(1, 4).map((i, index) => { |
|
if (index === 0) { |
|
|
|
return { x: 0, y: this.height }; |
|
} else if (index === 2) { |
|
|
|
return { x: this.width, y: 0 }; |
|
} else { |
|
|
|
return { |
|
x: i * this.width / 5, |
|
y: 50 + Math.random() * (this.height - 100) |
|
}; |
|
} |
|
}); |
|
this.pointsStoreWidget.value = JSON.stringify(this.points); |
|
} |
|
|
|
this.vis = new pv.Panel() |
|
.width(this.width) |
|
.height(this.height) |
|
.fillStyle("#222") |
|
.strokeStyle("gray") |
|
.lineWidth(2) |
|
.antialias(false) |
|
.margin(10) |
|
.event("mousedown", function () { |
|
if (pv.event.shiftKey) { |
|
let scaledMouse = { |
|
x: this.mouse().x / app.canvas.ds.scale, |
|
y: this.mouse().y / app.canvas.ds.scale |
|
}; |
|
i = self.points.push(scaledMouse) - 1; |
|
self.updatePath(); |
|
return this; |
|
} |
|
else if (pv.event.ctrlKey) { |
|
|
|
let clickedPoint = { |
|
x: this.mouse().x / app.canvas.ds.scale, |
|
y: this.mouse().y / app.canvas.ds.scale |
|
}; |
|
|
|
|
|
let { point1Index, point2Index } = self.findClosestPoints(self.points, clickedPoint); |
|
|
|
|
|
let midpoint = { |
|
x: (self.points[point1Index].x + self.points[point2Index].x) / 2, |
|
y: (self.points[point1Index].y + self.points[point2Index].y) / 2 |
|
}; |
|
|
|
|
|
self.points.splice(point2Index, 0, midpoint); |
|
i = point2Index; |
|
self.updatePath(); |
|
} |
|
else if (pv.event.button === 2) { |
|
self.node.contextMenu.style.display = 'block'; |
|
self.node.contextMenu.style.left = `${pv.event.clientX}px`; |
|
self.node.contextMenu.style.top = `${pv.event.clientY}px`; |
|
} |
|
}) |
|
this.backgroundImage = this.vis.add(pv.Image).visible(false) |
|
|
|
this.vis.add(pv.Rule) |
|
.data(pv.range(0, this.height, 64)) |
|
.bottom(d => d) |
|
.strokeStyle("gray") |
|
.lineWidth(3) |
|
.visible(() => self.drawRuler) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.vis.add(pv.Line) |
|
.data(() => this.points) |
|
.left(d => d.x) |
|
.top(d => d.y) |
|
.interpolate(() => this.interpolation) |
|
.tension(() => this.tension) |
|
.segmented(() => false) |
|
.strokeStyle(pv.Colors.category10().by(pv.index)) |
|
.lineWidth(3) |
|
|
|
this.vis.add(pv.Dot) |
|
.data(() => this.points) |
|
.left(d => d.x) |
|
.top(d => d.y) |
|
.radius(10) |
|
.shape(function() { |
|
return self.dotShape; |
|
}) |
|
.angle(function() { |
|
const index = this.index; |
|
let angle = 0; |
|
|
|
if (self.dotShape === "triangle") { |
|
let dxNext = 0, dyNext = 0; |
|
if (index < self.points.length - 1) { |
|
dxNext = self.points[index + 1].x - self.points[index].x; |
|
dyNext = self.points[index + 1].y - self.points[index].y; |
|
} |
|
|
|
let dxPrev = 0, dyPrev = 0; |
|
if (index > 0) { |
|
dxPrev = self.points[index].x - self.points[index - 1].x; |
|
dyPrev = self.points[index].y - self.points[index - 1].y; |
|
} |
|
|
|
const dx = (dxNext + dxPrev) / 2; |
|
const dy = (dyNext + dyPrev) / 2; |
|
|
|
angle = Math.atan2(dy, dx); |
|
angle -= Math.PI / 2; |
|
angle = (angle + 2 * Math.PI) % (2 * Math.PI); |
|
} |
|
|
|
return angle; |
|
}) |
|
.cursor("move") |
|
.strokeStyle(function () { return i == this.index ? "#ff7f0e" : "#1f77b4"; }) |
|
.fillStyle(function () { return "rgba(100, 100, 100, 0.3)"; }) |
|
.event("mousedown", pv.Behavior.drag()) |
|
.event("dragstart", function () { |
|
i = this.index; |
|
hoverIndex = this.index; |
|
isDragging = true; |
|
if (pv.event.button === 2 && i !== 0 && i !== self.points.length - 1) { |
|
self.points.splice(i--, 1); |
|
self.vis.render(); |
|
} |
|
return this; |
|
}) |
|
.event("dragend", function() { |
|
if (this.pathElements !== null) { |
|
self.updatePath(); |
|
} |
|
isDragging = false; |
|
}) |
|
.event("drag", function () { |
|
let adjustedX = this.mouse().x / app.canvas.ds.scale; |
|
let adjustedY = this.mouse().y / app.canvas.ds.scale; |
|
|
|
const panelWidth = self.vis.width(); |
|
const panelHeight = self.vis.height(); |
|
|
|
|
|
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX)); |
|
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY)); |
|
self.points[this.index] = { x: adjustedX, y: adjustedY }; |
|
self.vis.render(); |
|
}) |
|
.event("mouseover", function() { |
|
hoverIndex = this.index; |
|
self.vis.render(); |
|
}) |
|
.event("mouseout", function() { |
|
!isDragging && (hoverIndex = -1); |
|
self.vis.render(); |
|
}) |
|
.anchor("center") |
|
.add(pv.Label) |
|
.visible(function() { |
|
return hoverIndex === this.index; |
|
}) |
|
.left(d => d.x < this.width / 2 ? d.x + 80 : d.x - 70) |
|
.top(d => d.y < this.height / 2 ? d.y + 20 : d.y - 20) |
|
.font(12 + "px sans-serif") |
|
.text(d => { |
|
if (this.samplingMethod == "path") { |
|
return `X: ${Math.round(d.x)}, Y: ${Math.round(d.y)}`; |
|
} else { |
|
let frame = Math.round((d.x / self.width) * self.points_to_sample); |
|
let normalizedY = (1.0 - (d.y / self.height) - 0.0) * (self.rangeMax - self.rangeMin) + self.rangeMin; |
|
let normalizedX = (d.x / self.width); |
|
return `F: ${frame}, X: ${normalizedX.toFixed(2)}, Y: ${normalizedY.toFixed(2)}`; |
|
} |
|
}) |
|
.textStyle("orange") |
|
|
|
if (this.points.length != 0) { |
|
this.vis.render(); |
|
} |
|
var svgElement = this.vis.canvas(); |
|
svgElement.style['zIndex'] = "2" |
|
svgElement.style['position'] = "relative" |
|
this.node.splineEditor.element.appendChild(svgElement); |
|
this.pathElements = svgElement.getElementsByTagName('path'); |
|
|
|
if (this.width > 256) { |
|
this.node.setSize([this.width + 45, this.node.size[1]]); |
|
} |
|
this.node.setSize([this.node.size[0], this.height + 430]); |
|
this.updatePath(); |
|
this.refreshBackgroundImage(); |
|
} |
|
|
|
updatePath = () => { |
|
if (!this.points || this.points.length === 0) { |
|
console.log("no points"); |
|
return; |
|
} |
|
if (this.samplingMethod != "controlpoints") { |
|
var coords = this.samplePoints(this.pathElements[0], this.points_to_sample, this.samplingMethod, this.width); |
|
} |
|
else { |
|
var coords = this.points |
|
} |
|
|
|
if (this.drawSamplePoints) { |
|
if (this.pointsLayer) { |
|
|
|
this.pointsLayer.data(coords); |
|
} else { |
|
|
|
this.pointsLayer = this.vis.add(pv.Dot) |
|
.data(coords) |
|
.left(function(d) { return d.x; }) |
|
.top(function(d) { return d.y; }) |
|
.radius(5) |
|
.fillStyle("red") |
|
.strokeStyle("black") |
|
.lineWidth(1); |
|
} |
|
} else { |
|
if (this.pointsLayer) { |
|
|
|
this.pointsLayer.data([]); |
|
this.vis.render(); |
|
} |
|
} |
|
let coordsString = JSON.stringify(coords); |
|
this.pointsStoreWidget.value = JSON.stringify(this.points); |
|
if (this.coordWidget) { |
|
this.coordWidget.value = coordsString; |
|
} |
|
this.vis.render(); |
|
}; |
|
handleImageLoad = (img, file, base64String) => { |
|
console.log(img.width, img.height); |
|
this.widthWidget.value = img.width; |
|
this.heightWidget.value = img.height; |
|
this.drawRuler = false; |
|
|
|
if (img.width != this.vis.width() || img.height != this.vis.height()) { |
|
if (img.width > 256) { |
|
this.node.setSize([img.width + 45, this.node.size[1]]); |
|
} |
|
this.node.setSize([this.node.size[0], img.height + 500]); |
|
this.vis.width(img.width); |
|
this.vis.height(img.height); |
|
this.height = img.height; |
|
this.width = img.width; |
|
|
|
this.updatePath(); |
|
} |
|
this.backgroundImage.url(file ? URL.createObjectURL(file) : `data:${this.node.properties.imgData.type};base64,${base64String}`).visible(true).root.render(); |
|
}; |
|
|
|
processImage = (img, file) => { |
|
const canvas = document.createElement('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
const maxWidth = 800; |
|
const maxHeight = 600; |
|
let width = img.width; |
|
let height = img.height; |
|
|
|
|
|
if (width > height) { |
|
if (width > maxWidth) { |
|
height *= maxWidth / width; |
|
width = maxWidth; |
|
} |
|
} else { |
|
if (height > maxHeight) { |
|
width *= maxHeight / height; |
|
height = maxHeight; |
|
} |
|
} |
|
|
|
canvas.width = width; |
|
canvas.height = height; |
|
ctx.drawImage(img, 0, 0, width, height); |
|
|
|
|
|
const base64String = canvas.toDataURL('image/jpeg', 0.5).replace('data:', '').replace(/^.+,/, ''); |
|
|
|
this.node.properties.imgData = { |
|
name: file.name, |
|
lastModified: file.lastModified, |
|
size: file.size, |
|
type: file.type, |
|
base64: base64String |
|
}; |
|
handleImageLoad(img, file, base64String); |
|
}; |
|
|
|
handleImageFile = (file) => { |
|
const reader = new FileReader(); |
|
reader.onloadend = () => { |
|
const img = new Image(); |
|
img.src = reader.result; |
|
img.onload = () => processImage(img, file); |
|
}; |
|
reader.readAsDataURL(file); |
|
|
|
const imageUrl = URL.createObjectURL(file); |
|
const img = new Image(); |
|
img.src = imageUrl; |
|
img.onload = () => this.handleImageLoad(img, file, null); |
|
}; |
|
|
|
refreshBackgroundImage = () => { |
|
if (this.node.properties.imgData && this.node.properties.imgData.base64) { |
|
const base64String = this.node.properties.imgData.base64; |
|
const imageUrl = `data:${this.node.properties.imgData.type};base64,${base64String}`; |
|
const img = new Image(); |
|
img.src = imageUrl; |
|
img.onload = () => this.handleImageLoad(img, null, base64String); |
|
} |
|
}; |
|
|
|
createContextMenu = () => { |
|
self = this; |
|
document.addEventListener('contextmenu', function (e) { |
|
e.preventDefault(); |
|
|
|
}); |
|
|
|
document.addEventListener('click', function (e) { |
|
document.querySelectorAll('.spline-editor-context-menu').forEach(menu => { |
|
menu.style.display = 'none'; |
|
}); |
|
}); |
|
|
|
this.node.menuItems.forEach((menuItem, index) => { |
|
self = this; |
|
menuItem.addEventListener('click', function (e) { |
|
e.preventDefault(); |
|
switch (index) { |
|
case 0: |
|
e.preventDefault(); |
|
if (!self.drawHandles) { |
|
self.drawHandles = true |
|
self.vis.add(pv.Line) |
|
.data(() => self.points.map((point, index) => ({ |
|
start: point, |
|
end: [index] |
|
}))) |
|
.left(d => d.start.x) |
|
.top(d => d.start.y) |
|
.interpolate("linear") |
|
.tension(0) |
|
.strokeStyle("#ff7f0e") |
|
.lineWidth(1) |
|
.visible(() => self.drawHandles); |
|
self.vis.render(); |
|
} else { |
|
self.drawHandles = false |
|
self.vis.render(); |
|
} |
|
self.node.contextMenu.style.display = 'none'; |
|
break; |
|
case 1: |
|
e.preventDefault(); |
|
self.drawSamplePoints = !self.drawSamplePoints; |
|
self.updatePath(); |
|
break; |
|
case 2: |
|
e.preventDefault(); |
|
if (self.dotShape == "circle"){ |
|
self.dotShape = "triangle" |
|
} |
|
else { |
|
self.dotShape = "circle" |
|
} |
|
console.log(self.dotShape) |
|
self.updatePath(); |
|
break; |
|
case 3: |
|
|
|
const fileInput = document.createElement('input'); |
|
fileInput.type = 'file'; |
|
fileInput.accept = 'image/*'; |
|
|
|
|
|
fileInput.addEventListener('change', function (event) { |
|
const file = event.target.files[0]; |
|
|
|
if (file) { |
|
const imageUrl = URL.createObjectURL(file); |
|
let img = new Image(); |
|
img.src = imageUrl; |
|
img.onload = () => self.handleImageLoad(img, file, null); |
|
} |
|
}); |
|
|
|
fileInput.click(); |
|
|
|
self.node.contextMenu.style.display = 'none'; |
|
break; |
|
case 4: |
|
e.preventDefault(); |
|
self.points.reverse(); |
|
self.updatePath(); |
|
break; |
|
case 5: |
|
self.backgroundImage.visible(false).root.render(); |
|
self.node.properties.imgData = null; |
|
self.node.contextMenu.style.display = 'none'; |
|
break; |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
samplePoints(svgPathElement, numSamples, samplingMethod, width) { |
|
var svgWidth = width; |
|
var pathLength = svgPathElement.getTotalLength(); |
|
var points = []; |
|
|
|
for (var i = 0; i < numSamples; i++) { |
|
if (samplingMethod === "time") { |
|
|
|
var x = (svgWidth / (numSamples - 1)) * i; |
|
|
|
var point = this.findPointAtX(svgPathElement, x, pathLength); |
|
} |
|
else if (samplingMethod === "path") { |
|
|
|
var distance = (pathLength / (numSamples - 1)) * i; |
|
|
|
var point = svgPathElement.getPointAtLength(distance); |
|
} |
|
|
|
|
|
points.push({ x: point.x, y: point.y }); |
|
} |
|
return points; |
|
} |
|
|
|
findClosestPoints(points, clickedPoint) { |
|
|
|
let distances = points.map(point => { |
|
let dx = clickedPoint.x - point.x; |
|
let dy = clickedPoint.y - point.y; |
|
return { index: points.indexOf(point), distance: Math.sqrt(dx * dx + dy * dy) }; |
|
}); |
|
|
|
let sortedDistances = distances.sort((a, b) => a.distance - b.distance); |
|
let closestPoint1Index = sortedDistances[0].index; |
|
let closestPoint2Index = sortedDistances[1].index; |
|
|
|
if (closestPoint1Index > closestPoint2Index) { |
|
[closestPoint1Index, closestPoint2Index] = [closestPoint2Index, closestPoint1Index]; |
|
} |
|
return { point1Index: closestPoint1Index, point2Index: closestPoint2Index }; |
|
} |
|
|
|
findPointAtX(svgPathElement, targetX, pathLength) { |
|
let low = 0; |
|
let high = pathLength; |
|
let bestPoint = svgPathElement.getPointAtLength(0); |
|
|
|
while (low <= high) { |
|
let mid = low + (high - low) / 2; |
|
let point = svgPathElement.getPointAtLength(mid); |
|
|
|
if (Math.abs(point.x - targetX) < 1) { |
|
return point; |
|
} |
|
|
|
if (point.x < targetX) { |
|
low = mid + 1; |
|
} else { |
|
high = mid - 1; |
|
} |
|
|
|
|
|
if (Math.abs(point.x - targetX) < Math.abs(bestPoint.x - targetX)) { |
|
bestPoint = point; |
|
} |
|
} |
|
|
|
|
|
return bestPoint; |
|
} |
|
} |
|
|
|
export function hideWidgetForGood(node, widget, suffix = '') { |
|
widget.origType = widget.type |
|
widget.origComputeSize = widget.computeSize |
|
widget.origSerializeValue = widget.serializeValue |
|
widget.computeSize = () => [0, -4] |
|
widget.type = "converted-widget" + suffix |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (widget.linkedWidgets) { |
|
for (const w of widget.linkedWidgets) { |
|
hideWidgetForGood(node, w, ':' + widget.name) |
|
} |
|
} |
|
} |