mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-24 02:36:04 +10:00
Initial commit
moved from private version control
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/exports
|
||||||
128
BlueprintWorkspace.outline.txt
Normal file
128
BlueprintWorkspace.outline.txt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
Outline for scripts\ui\BlueprintWorkspace.js
|
||||||
|
|
||||||
|
77 : class BlueprintWorkspace
|
||||||
|
81 : method BlueprintWorkspace.constructor
|
||||||
|
242 : method BlueprintWorkspace.static #requireTemplate
|
||||||
|
253 : method BlueprintWorkspace.initialize
|
||||||
|
329 : method BlueprintWorkspace.redo
|
||||||
|
336 : method BlueprintWorkspace.exportLua
|
||||||
|
343 : method BlueprintWorkspace.#bindGraphEvents
|
||||||
|
536 : method BlueprintWorkspace.#bindUiEvents
|
||||||
|
779 : method BlueprintWorkspace.removeNode
|
||||||
|
811 : method BlueprintWorkspace.clearSelection
|
||||||
|
834 : method BlueprintWorkspace.selectNode
|
||||||
|
859 : method BlueprintWorkspace.setSelectionState
|
||||||
|
866 : method BlueprintWorkspace.#frameSelection
|
||||||
|
892 : method BlueprintWorkspace.#togglePaletteVisibility
|
||||||
|
901 : method BlueprintWorkspace.#setPaletteVisibility
|
||||||
|
919 : method BlueprintWorkspace.#applyPaletteVisibility
|
||||||
|
956 : method BlueprintWorkspace.#selectVariable
|
||||||
|
977 : method BlueprintWorkspace.#renderInspectorState
|
||||||
|
1025 : method BlueprintWorkspace.#enhanceInspectorTooltip
|
||||||
|
1041 : arrow clearRaf
|
||||||
|
1048 : arrow schedulePosition
|
||||||
|
1055 : arrow handleScroll
|
||||||
|
1063 : arrow handleResize
|
||||||
|
1070 : arrow handleDocumentPointerDown
|
||||||
|
1078 : arrow close
|
||||||
|
1097 : arrow position
|
||||||
|
1168 : arrow open
|
||||||
|
1181 : arrow handlePointerLeave
|
||||||
|
1185 : arrow handleFocusOut
|
||||||
|
1193 : arrow handleKeyDown
|
||||||
|
1226 : method BlueprintWorkspace.#renderProjectSettingsInspector
|
||||||
|
1319 : method BlueprintWorkspace.#setInspectorView
|
||||||
|
1330 : method BlueprintWorkspace.#updateProjectSettingsToggle
|
||||||
|
1345 : method BlueprintWorkspace.#handleProjectSettingsChanged
|
||||||
|
1356 : method BlueprintWorkspace.#highlightVariableSelection
|
||||||
|
1386 : method BlueprintWorkspace.#renderVariableInspector
|
||||||
|
1452 : method BlueprintWorkspace.#setVariableName
|
||||||
|
1508 : method BlueprintWorkspace.#setVariableType
|
||||||
|
1542 : method BlueprintWorkspace.#openVariableTypeDropdown
|
||||||
|
1631 : method BlueprintWorkspace.#closeVariableTypeDropdown
|
||||||
|
1657 : method BlueprintWorkspace.#startVariableRename
|
||||||
|
1689 : arrow finish
|
||||||
|
1735 : method BlueprintWorkspace.#cancelVariableRename
|
||||||
|
1766 : method BlueprintWorkspace.#setSelection
|
||||||
|
1794 : method BlueprintWorkspace.#applySelectionState
|
||||||
|
1828 : method BlueprintWorkspace.#isWorkspaceBackgroundTarget
|
||||||
|
1852 : method BlueprintWorkspace.#renderOverview
|
||||||
|
1863 : method BlueprintWorkspace.#createGlobalVariable
|
||||||
|
1886 : method BlueprintWorkspace.#registerVariable
|
||||||
|
1905 : method BlueprintWorkspace.#suggestVariableName
|
||||||
|
1947 : method BlueprintWorkspace.#normalizeVariableName
|
||||||
|
1957 : method BlueprintWorkspace.#formatVariableType
|
||||||
|
1976 : method BlueprintWorkspace.#findVariableByName
|
||||||
|
2003 : method BlueprintWorkspace.#nodeReferencesVariable
|
||||||
|
2035 : method BlueprintWorkspace.#resolveVariableBinding
|
||||||
|
2066 : method BlueprintWorkspace.#refreshVariableNode
|
||||||
|
2089 : method BlueprintWorkspace.#applyVariableNodeTitle
|
||||||
|
2131 : method BlueprintWorkspace.#applyVariableNodePins
|
||||||
|
2137 : arrow updatePin
|
||||||
|
2222 : method BlueprintWorkspace.#getCustomEventNodes
|
||||||
|
2235 : method BlueprintWorkspace.#generateUniqueCustomEventName
|
||||||
|
2274 : method BlueprintWorkspace.#resolveCustomEventLabel
|
||||||
|
2288 : method BlueprintWorkspace.#customEventOutputPinId
|
||||||
|
2298 : method BlueprintWorkspace.#callCustomEventInputPinId
|
||||||
|
2308 : method BlueprintWorkspace.#allocateCustomEventParameterId
|
||||||
|
2323 : method BlueprintWorkspace.#sanitizeCustomEventParameters
|
||||||
|
2431 : method BlueprintWorkspace.#applyCustomEventNodePins
|
||||||
|
2506 : method BlueprintWorkspace.#applyCallCustomEventNodePins
|
||||||
|
2581 : method BlueprintWorkspace.#ensureConnectionsReflectPinKinds
|
||||||
|
2620 : method BlueprintWorkspace.#syncCallCustomEventArguments
|
||||||
|
2651 : method BlueprintWorkspace.#addCustomEventParameter
|
||||||
|
2679 : method BlueprintWorkspace.#removeCustomEventParameter
|
||||||
|
2702 : method BlueprintWorkspace.#renameCustomEventParameter
|
||||||
|
2724 : method BlueprintWorkspace.#setCustomEventParameterType
|
||||||
|
2746 : method BlueprintWorkspace.#ensureCustomEventStructure
|
||||||
|
2764 : method BlueprintWorkspace.#refreshCustomEventNode
|
||||||
|
2802 : method BlueprintWorkspace.#syncCallNodesForEvent
|
||||||
|
2833 : method BlueprintWorkspace.#refreshCustomEventCallNode
|
||||||
|
2899 : method BlueprintWorkspace.#refreshInspectorIfActive
|
||||||
|
2917 : method BlueprintWorkspace.#refreshCallNodesForEvent
|
||||||
|
2931 : method BlueprintWorkspace.#handleCustomEventRemoval
|
||||||
|
2950 : method BlueprintWorkspace.#renderEventList
|
||||||
|
2970 : method BlueprintWorkspace.#renderVariableList
|
||||||
|
3005 : method BlueprintWorkspace.#renderOverviewEmpty
|
||||||
|
3021 : method BlueprintWorkspace.#isEventNode
|
||||||
|
3049 : method BlueprintWorkspace.#handleConnectionPointerDown
|
||||||
|
3076 : method BlueprintWorkspace.createNodeFromDefinition
|
||||||
|
3142 : method BlueprintWorkspace.spawnNodeFromContextMenu
|
||||||
|
3187 : method BlueprintWorkspace.#computeSpawnPosition
|
||||||
|
3211 : method BlueprintWorkspace.#startDraggingNode
|
||||||
|
3223 : method BlueprintWorkspace.#tryAutoConnectSelectedNode
|
||||||
|
3271 : method BlueprintWorkspace.#isAutoConnectStartPinAvailable
|
||||||
|
3283 : method BlueprintWorkspace.#findAutoConnectTargets
|
||||||
|
3398 : method BlueprintWorkspace.#beginConnection
|
||||||
|
3421 : arrow handlePointerMove
|
||||||
|
3431 : arrow handlePointerUp
|
||||||
|
3458 : method BlueprintWorkspace.#updatePendingConnectionPath
|
||||||
|
3497 : method BlueprintWorkspace.#finalizeConnection
|
||||||
|
3515 : method BlueprintWorkspace.#cancelPendingConnection
|
||||||
|
3529 : method BlueprintWorkspace.#tryOpenPendingConnectionMenu
|
||||||
|
3590 : method BlueprintWorkspace.#getCompatibleDefinitionsForPending
|
||||||
|
3610 : method BlueprintWorkspace.#connectSpawnedNodeToPending
|
||||||
|
3657 : method BlueprintWorkspace.#createConnectionPath
|
||||||
|
3668 : method BlueprintWorkspace.scheduleConnectionRefresh
|
||||||
|
3675 : method BlueprintWorkspace.cancelConnectionRefresh
|
||||||
|
3682 : method BlueprintWorkspace.flushConnectionRefresh
|
||||||
|
3689 : method BlueprintWorkspace.#scheduleConnectionRefresh
|
||||||
|
3703 : method BlueprintWorkspace.#cancelConnectionRefresh
|
||||||
|
3713 : method BlueprintWorkspace.#flushConnectionRefresh
|
||||||
|
3724 : method BlueprintWorkspace.#renderConnections
|
||||||
|
3769 : method BlueprintWorkspace.#computeConnectionGeometry
|
||||||
|
3806 : method BlueprintWorkspace.#getPinHandle
|
||||||
|
3825 : method BlueprintWorkspace.#getPinHandleCenter
|
||||||
|
3853 : method BlueprintWorkspace.#getPinElement
|
||||||
|
3871 : method BlueprintWorkspace.#highlightPinsForPending
|
||||||
|
3932 : method BlueprintWorkspace.#shouldHighlightHover
|
||||||
|
3944 : method BlueprintWorkspace.#isPinCompatibleWithPending
|
||||||
|
3983 : method BlueprintWorkspace.#clearPinHighlights
|
||||||
|
4040 : method BlueprintWorkspace.#arePinKindsCompatible
|
||||||
|
4052 : method BlueprintWorkspace.#renderInspector
|
||||||
|
4178 : method BlueprintWorkspace.#renderCustomEventInspector
|
||||||
|
4331 : method BlueprintWorkspace.#renderCallCustomEventInspector
|
||||||
|
4415 : method BlueprintWorkspace.#refreshLuaOutput
|
||||||
|
4430 : method BlueprintWorkspace.serializeWorkspace
|
||||||
|
4439 : method BlueprintWorkspace.applySerializedWorkspace
|
||||||
|
4559 : method BlueprintWorkspace.#rebuildNodeCounters
|
||||||
104
electron-main.js
Normal file
104
electron-main.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain } = require("electron");
|
||||||
|
const fs = require("node:fs/promises");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const OUTPUT_FILENAME = "compiled.lua";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the absolute directory where compiled Lua output is stored.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getOutputDirectory() {
|
||||||
|
const applicationData = app.getPath("appData");
|
||||||
|
return path.join(applicationData, "pico-8", "carts", "picograph");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the absolute path to the generated Lua file.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getOutputPath() {
|
||||||
|
return path.join(getOutputDirectory(), OUTPUT_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("electron").BrowserWindow | null} */
|
||||||
|
let mainWindow = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the primary application window and loads the web UI.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function createMainWindow() {
|
||||||
|
const window = new BrowserWindow({
|
||||||
|
width: 1440,
|
||||||
|
height: 900,
|
||||||
|
minWidth: 960,
|
||||||
|
minHeight: 640,
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.once("ready-to-show", () => {
|
||||||
|
window.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.loadFile(path.join(__dirname, "index.html"));
|
||||||
|
|
||||||
|
window.on("closed", () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers IPC handlers used by renderer processes.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function registerIpcHandlers() {
|
||||||
|
ipcMain.handle("lua:compile", async (_event, source) => {
|
||||||
|
if (typeof source !== "string") {
|
||||||
|
throw new TypeError("Lua source must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = getOutputPath();
|
||||||
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||||
|
await fs.writeFile(outputPath, source, "utf8");
|
||||||
|
|
||||||
|
return { filePath: outputPath };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires core Electron lifecycle events for the application.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function registerAppLifecycle() {
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createMainWindow();
|
||||||
|
registerIpcHandlers();
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createMainWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerAppLifecycle();
|
||||||
147
index.html
Normal file
147
index.html
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>picoGraph</title>
|
||||||
|
<link rel="stylesheet" href="styles/main.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="header-primary-actions">
|
||||||
|
<button id="compileButton" class="header-primary-button" type="button">Compile</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-title">picoGraph</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button
|
||||||
|
id="projectSettingsButton"
|
||||||
|
class="header-icon-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Project settings"
|
||||||
|
title="Project settings"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--settings" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="paletteToggleButton"
|
||||||
|
class="header-icon-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Toggle palette"
|
||||||
|
title="Toggle palette"
|
||||||
|
aria-pressed="true"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--palette" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="frameSelectionButton"
|
||||||
|
class="header-icon-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Frame selection"
|
||||||
|
title="Frame selection"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--frame" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="exportLuaButton"
|
||||||
|
class="header-icon-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Export Lua"
|
||||||
|
title="Export Lua"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--export" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main id="appBody" class="app-body">
|
||||||
|
<aside class="overview" aria-label="Graph overview">
|
||||||
|
<div class="overview-section">
|
||||||
|
<div class="overview-title">Events</div>
|
||||||
|
<ul id="eventList" class="overview-list" aria-label="Existing events"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="overview-section">
|
||||||
|
<div class="overview-title overview-title--with-action">
|
||||||
|
<span>Variables</span>
|
||||||
|
<button id="addVariableButton" type="button" class="overview-action" aria-label="Add global variable">+</button>
|
||||||
|
</div>
|
||||||
|
<ul id="variableList" class="overview-list" aria-label="Defined variables"></ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<section class="workspace" aria-label="Blueprint workspace">
|
||||||
|
<div id="workspaceCanvas" class="workspace-canvas">
|
||||||
|
<svg id="connectionLayer" class="connection-layer" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||||
|
<div id="nodeLayer" class="node-layer"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<aside id="palettePanel" class="palette" aria-label="Node palette">
|
||||||
|
<div class="palette-search">
|
||||||
|
<input id="paletteSearch" type="search" placeholder="Search nodes" />
|
||||||
|
</div>
|
||||||
|
<div id="paletteList" class="palette-list" role="listbox" aria-label="Available nodes"></div>
|
||||||
|
</aside>
|
||||||
|
<aside class="inspector" aria-label="Node inspector">
|
||||||
|
<div class="inspector-header">Details</div>
|
||||||
|
<div id="inspectorContent" class="inspector-content"></div>
|
||||||
|
<div class="inspector-footer">
|
||||||
|
<button id="duplicateNodeButton" type="button" disabled>Duplicate</button>
|
||||||
|
<button id="deleteNodeButton" type="button" disabled>Delete</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
<div
|
||||||
|
id="luaModal"
|
||||||
|
class="modal"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="luaModalTitle"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<div class="modal__backdrop" data-modal-close></div>
|
||||||
|
<div class="modal__panel" role="document">
|
||||||
|
<header class="modal__header">
|
||||||
|
<h2 id="luaModalTitle">Lua Preview</h2>
|
||||||
|
<div class="modal__actions">
|
||||||
|
<button
|
||||||
|
id="copyLuaButton"
|
||||||
|
type="button"
|
||||||
|
class="modal__icon-button"
|
||||||
|
aria-label="Copy to clipboard"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
<span class="modal-icon modal-icon--copy" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="closeLuaModalButton"
|
||||||
|
type="button"
|
||||||
|
class="modal__icon-button"
|
||||||
|
aria-label="Close Lua preview"
|
||||||
|
title="Close"
|
||||||
|
data-modal-close
|
||||||
|
>
|
||||||
|
<span class="modal-icon modal-icon--close" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal__body">
|
||||||
|
<pre class="lua-modal__code" tabindex="0"><code id="luaModalCode"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template id="nodeTemplate">
|
||||||
|
<article class="blueprint-node" draggable="false">
|
||||||
|
<header class="node-header">
|
||||||
|
<div class="node-title"></div>
|
||||||
|
</header>
|
||||||
|
<section class="node-io node-inputs"></section>
|
||||||
|
<section class="node-io node-outputs"></section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
<template id="pinTemplate">
|
||||||
|
<div class="pin" draggable="false">
|
||||||
|
<span class="pin-label"></span>
|
||||||
|
<span class="pin-handle" tabindex="0"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script type="module" src="scripts/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
871
package-lock.json
generated
Normal file
871
package-lock.json
generated
Normal file
@@ -0,0 +1,871 @@
|
|||||||
|
{
|
||||||
|
"name": "picograph",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "picograph",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^30.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/get": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"env-paths": "^2.2.0",
|
||||||
|
"fs-extra": "^8.1.0",
|
||||||
|
"got": "^11.8.5",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"semver": "^6.2.0",
|
||||||
|
"sumchecker": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"global-agent": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sindresorhus/is": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@szmarczak/http-timer": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"defer-to-connect": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/cacheable-request": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/http-cache-semantics": "*",
|
||||||
|
"@types/keyv": "^3.1.4",
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/responselike": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/http-cache-semantics": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/keyv": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.22.tgz",
|
||||||
|
"integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/responselike": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/yauzl": {
|
||||||
|
"version": "2.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boolean": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable-lookup": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable-request": {
|
||||||
|
"version": "7.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||||
|
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clone-response": "^1.0.2",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"http-cache-semantics": "^4.0.0",
|
||||||
|
"keyv": "^4.0.0",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"normalize-url": "^6.0.1",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone-response": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/defer-to-connect": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-node": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/electron": {
|
||||||
|
"version": "30.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz",
|
||||||
|
"integrity": "sha512-AhL7+mZ8Lg14iaNfoYTkXQ2qee8mmsQyllKdqxlpv/zrKgfxz6jNVtcRRbQtLxtF8yzcImWdfTQROpYiPumdbw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/get": "^2.0.0",
|
||||||
|
"@types/node": "^20.9.0",
|
||||||
|
"extract-zip": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"electron": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.20.55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/env-paths": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-error": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/extract-zip": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"extract-zip": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.17.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/yauzl": "^2.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"boolean": "^3.0.1",
|
||||||
|
"es6-error": "^4.1.1",
|
||||||
|
"matcher": "^3.0.0",
|
||||||
|
"roarr": "^2.15.3",
|
||||||
|
"semver": "^7.3.2",
|
||||||
|
"serialize-error": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent/node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/globalthis": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"define-properties": "^1.2.1",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/got": {
|
||||||
|
"version": "11.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||||
|
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sindresorhus/is": "^4.0.0",
|
||||||
|
"@szmarczak/http-timer": "^4.0.5",
|
||||||
|
"@types/cacheable-request": "^6.0.1",
|
||||||
|
"@types/responselike": "^1.0.0",
|
||||||
|
"cacheable-lookup": "^5.0.3",
|
||||||
|
"cacheable-request": "^7.0.2",
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"p-cancelable": "^2.0.0",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-cache-semantics": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/http2-wrapper": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"quick-lru": "^5.1.1",
|
||||||
|
"resolve-alpn": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-buffer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/json-stringify-safe": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/keyv": {
|
||||||
|
"version": "4.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-buffer": "3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lowercase-keys": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/matcher": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/normalize-url": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-cancelable": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/progress": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quick-lru": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-alpn": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/responselike": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lowercase-keys": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/roarr": {
|
||||||
|
"version": "2.15.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||||
|
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"boolean": "^3.0.1",
|
||||||
|
"detect-node": "^2.0.4",
|
||||||
|
"globalthis": "^1.0.1",
|
||||||
|
"json-stringify-safe": "^5.0.1",
|
||||||
|
"semver-compare": "^1.0.0",
|
||||||
|
"sprintf-js": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver-compare": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/serialize-error": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^0.13.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/sumchecker": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "0.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||||
|
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "picograph",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "PicoGraph desktop wrapper",
|
||||||
|
"main": "electron-main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^30.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
preload.js
Normal file
22
preload.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides renderer processes with limited access to Electron IPC bridges.
|
||||||
|
*/
|
||||||
|
const api = {
|
||||||
|
/**
|
||||||
|
* Requests that the main process compile the supplied Lua source to disk.
|
||||||
|
*
|
||||||
|
* @param {string} source Lua program text emitted from the workspace.
|
||||||
|
* @returns {Promise<{ filePath: string }>}
|
||||||
|
*/
|
||||||
|
compileLua(source) {
|
||||||
|
if (typeof source !== "string") {
|
||||||
|
return Promise.reject(new TypeError("Lua source must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipcRenderer.invoke("lua:compile", source);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("electronAPI", api);
|
||||||
74
scripts/core/BlueprintNode.js
Normal file
74
scripts/core/BlueprintNode.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} PinDescriptor
|
||||||
|
* @property {string} id Unique pin identifier scoped to the node.
|
||||||
|
* @property {string} name Public display name.
|
||||||
|
* @property {('input'|'output')} direction Pin direction relative to the node.
|
||||||
|
* @property {('exec'|'number'|'boolean'|'string'|'table'|'any')} kind Pin kind describing blueprint behavior.
|
||||||
|
* @property {string} [description] Optional tooltip/description.
|
||||||
|
* @property {unknown} [defaultValue] Default value for data pins.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} BlueprintNodeInit
|
||||||
|
* @property {string} id Node identifier.
|
||||||
|
* @property {string} type Node definition identifier.
|
||||||
|
* @property {string} title Display title for the node.
|
||||||
|
* @property {{x:number,y:number}} position Node position within the workspace.
|
||||||
|
* @property {Array<PinDescriptor>} inputs Input pin descriptors.
|
||||||
|
* @property {Array<PinDescriptor>} outputs Output pin descriptors.
|
||||||
|
* @property {Record<string, unknown>} properties Arbitrary node properties.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a blueprint node instance within the graph.
|
||||||
|
*/
|
||||||
|
export class BlueprintNode {
|
||||||
|
/**
|
||||||
|
* @param {BlueprintNodeInit} init Initialization payload.
|
||||||
|
*/
|
||||||
|
constructor(init) {
|
||||||
|
this.id = init.id;
|
||||||
|
this.type = init.type;
|
||||||
|
this.title = init.title;
|
||||||
|
this.position = { ...init.position };
|
||||||
|
this.inputs = init.inputs.map((pin) => ({ ...pin }));
|
||||||
|
this.outputs = init.outputs.map((pin) => ({ ...pin }));
|
||||||
|
this.properties = { ...init.properties };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the node position immutably.
|
||||||
|
*
|
||||||
|
* @param {{x:number,y:number}} position New position coordinates.
|
||||||
|
*/
|
||||||
|
setPosition(position) {
|
||||||
|
this.position = { x: position.x, y: position.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a shallow clone suitable for serialization.
|
||||||
|
*
|
||||||
|
* @returns {BlueprintNodeInit}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
type: this.type,
|
||||||
|
title: this.title,
|
||||||
|
position: { ...this.position },
|
||||||
|
inputs: this.inputs.map((pin) => ({ ...pin })),
|
||||||
|
outputs: this.outputs.map((pin) => ({ ...pin })),
|
||||||
|
properties: { ...this.properties },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a pin descriptor matching the supplied identifier.
|
||||||
|
*
|
||||||
|
* @param {string} pinId Pin identifier.
|
||||||
|
* @returns {PinDescriptor | undefined}
|
||||||
|
*/
|
||||||
|
getPin(pinId) {
|
||||||
|
return [...this.inputs, ...this.outputs].find((pin) => pin.id === pinId);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
scripts/core/Connection.js
Normal file
46
scripts/core/Connection.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Generates a lightweight unique identifier.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const createConnectionId = () =>
|
||||||
|
globalThis.crypto?.randomUUID?.() ??
|
||||||
|
`conn_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PinReference
|
||||||
|
* @property {string} nodeId Linked node identifier.
|
||||||
|
* @property {string} pinId Pin identifier within the node.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a directional connection between two node pins.
|
||||||
|
*/
|
||||||
|
export class Connection {
|
||||||
|
/**
|
||||||
|
* @param {PinReference} from Origin pin (must be output).
|
||||||
|
* @param {PinReference} to Target pin (must be input).
|
||||||
|
* @param {('exec'|'number'|'boolean'|'string'|'table'|'any')} kind Pin kind carried by the connection.
|
||||||
|
* @param {string} [id] Optional connection identifier.
|
||||||
|
*/
|
||||||
|
constructor(from, to, kind, id) {
|
||||||
|
this.id = id ?? createConnectionId();
|
||||||
|
this.from = { ...from };
|
||||||
|
this.to = { ...to };
|
||||||
|
this.kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the connection for persistence.
|
||||||
|
*
|
||||||
|
* @returns {{from: PinReference, to: PinReference, kind: Connection['kind']}}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
from: { ...this.from },
|
||||||
|
to: { ...this.to },
|
||||||
|
kind: this.kind,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
1181
scripts/core/LuaGenerator.js
Normal file
1181
scripts/core/LuaGenerator.js
Normal file
File diff suppressed because it is too large
Load Diff
355
scripts/core/NodeGraph.js
Normal file
355
scripts/core/NodeGraph.js
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import { BlueprintNode } from "./BlueprintNode.js";
|
||||||
|
import { Connection } from "./Connection.js";
|
||||||
|
|
||||||
|
/** @typedef {import('./BlueprintNode.js').BlueprintNodeInit} BlueprintNodeInit */
|
||||||
|
/** @typedef {import('./BlueprintNode.js').PinDescriptor} PinDescriptor */
|
||||||
|
/** @typedef {import('./Connection.js').PinReference} PinReference */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains the canonical blueprint data model: nodes, pins, and their connections.
|
||||||
|
*/
|
||||||
|
export class NodeGraph extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
/** @type {Map<string, BlueprintNode>} */
|
||||||
|
this.nodes = new Map();
|
||||||
|
/** @type {Map<string, Connection>} */
|
||||||
|
this.connections = new Map();
|
||||||
|
this.nodeCounters = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a custom event notifying listeners about a change.
|
||||||
|
*
|
||||||
|
* @param {string} type Event name.
|
||||||
|
* @param {unknown} detail Event payload.
|
||||||
|
*/
|
||||||
|
emit(type, detail) {
|
||||||
|
this.dispatchEvent(new CustomEvent(type, { detail }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique node identifier scoped by node type.
|
||||||
|
*
|
||||||
|
* @param {string} type Node definition identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
createNodeId(type) {
|
||||||
|
const current = this.nodeCounters.get(type) ?? 0;
|
||||||
|
const next = current + 1;
|
||||||
|
this.nodeCounters.set(type, next);
|
||||||
|
return `${type}_${String(next).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a node instance within the graph.
|
||||||
|
*
|
||||||
|
* @param {BlueprintNode} node Node to add.
|
||||||
|
*/
|
||||||
|
addNode(node) {
|
||||||
|
this.nodes.set(node.id, node);
|
||||||
|
this.emit("nodeschanged", { type: "add", node });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a node and any associated connections.
|
||||||
|
*
|
||||||
|
* @param {string} nodeId Target node identifier.
|
||||||
|
*/
|
||||||
|
removeNode(nodeId) {
|
||||||
|
const node = this.nodes.get(nodeId);
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[...this.connections.values()].forEach((connection) => {
|
||||||
|
if (
|
||||||
|
connection.from.nodeId === nodeId ||
|
||||||
|
connection.to.nodeId === nodeId
|
||||||
|
) {
|
||||||
|
this.connections.delete(connection.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nodes.delete(nodeId);
|
||||||
|
this.emit("nodeschanged", { type: "remove", nodeId });
|
||||||
|
this.emit("connectionschanged", { type: "prune", nodeId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the node position within the graph.
|
||||||
|
*
|
||||||
|
* @param {string} nodeId Node identifier.
|
||||||
|
* @param {{x:number,y:number}} position New coordinates to store.
|
||||||
|
*/
|
||||||
|
setNodePosition(nodeId, position) {
|
||||||
|
const node = this.nodes.get(nodeId);
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.setPosition(position);
|
||||||
|
this.emit("nodepositionchanged", { nodeId, position: { ...position } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a node property bag value.
|
||||||
|
*
|
||||||
|
* @param {string} nodeId Node identifier.
|
||||||
|
* @param {string} key Property key.
|
||||||
|
* @param {unknown} value Property value.
|
||||||
|
*/
|
||||||
|
setNodeProperty(nodeId, key, value) {
|
||||||
|
const node = this.nodes.get(nodeId);
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.properties[key] = value;
|
||||||
|
this.emit("nodepropertychanged", { nodeId, key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an existing connection.
|
||||||
|
*
|
||||||
|
* @param {string} connectionId Identifier for the connection.
|
||||||
|
*/
|
||||||
|
removeConnection(connectionId) {
|
||||||
|
if (!this.connections.has(connectionId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connections.delete(connectionId);
|
||||||
|
this.emit("connectionschanged", { type: "remove", connectionId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes connections linked to a specific pin.
|
||||||
|
*
|
||||||
|
* @param {PinReference} ref Pin reference to clear.
|
||||||
|
*/
|
||||||
|
removeConnectionsForPin(ref) {
|
||||||
|
let mutated = false;
|
||||||
|
const affectedNodes = new Set();
|
||||||
|
[...this.connections.values()].forEach((connection) => {
|
||||||
|
if (
|
||||||
|
(connection.from.nodeId === ref.nodeId &&
|
||||||
|
connection.from.pinId === ref.pinId) ||
|
||||||
|
(connection.to.nodeId === ref.nodeId &&
|
||||||
|
connection.to.pinId === ref.pinId)
|
||||||
|
) {
|
||||||
|
this.connections.delete(connection.id);
|
||||||
|
mutated = true;
|
||||||
|
affectedNodes.add(connection.from.nodeId);
|
||||||
|
affectedNodes.add(connection.to.nodeId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mutated) {
|
||||||
|
this.emit("connectionschanged", {
|
||||||
|
type: "prune",
|
||||||
|
ref,
|
||||||
|
nodes: [...affectedNodes],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to connect two pins if the contract is valid.
|
||||||
|
*
|
||||||
|
* @param {PinReference} from Output pin reference.
|
||||||
|
* @param {PinReference} to Input pin reference.
|
||||||
|
* @returns {boolean} Whether the connection was created.
|
||||||
|
*/
|
||||||
|
connect(from, to) {
|
||||||
|
const fromNode = this.nodes.get(from.nodeId);
|
||||||
|
const toNode = this.nodes.get(to.nodeId);
|
||||||
|
if (!fromNode || !toNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromPin = fromNode.getPin(from.pinId);
|
||||||
|
const toPin = toNode.getPin(to.pinId);
|
||||||
|
if (!fromPin || !toPin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#isConnectionValid(fromNode, fromPin, toNode, toPin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromPin.kind === "exec") {
|
||||||
|
this.removeConnectionsForPin(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate single-input connections except for exec inputs which may stack.
|
||||||
|
if (toPin.kind !== "exec") {
|
||||||
|
this.removeConnectionsForPin(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = new Connection(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
toPin.kind === "any" ? fromPin.kind : toPin.kind
|
||||||
|
);
|
||||||
|
this.connections.set(connection.id, connection);
|
||||||
|
this.emit("connectionschanged", { type: "add", connection });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an immutable list of nodes.
|
||||||
|
*
|
||||||
|
* @returns {Array<BlueprintNode>}
|
||||||
|
*/
|
||||||
|
getNodes() {
|
||||||
|
return [...this.nodes.values()].map((node) => node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all active connections.
|
||||||
|
*
|
||||||
|
* @returns {Array<Connection>}
|
||||||
|
*/
|
||||||
|
getConnections() {
|
||||||
|
return [...this.connections.values()].map((connection) => connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns connections targeting a particular node and optional pin.
|
||||||
|
*
|
||||||
|
* @param {string} nodeId Node identifier.
|
||||||
|
* @param {string} [pinId] Optional pin identifier.
|
||||||
|
* @returns {Array<Connection>}
|
||||||
|
*/
|
||||||
|
getConnectionsForNode(nodeId, pinId) {
|
||||||
|
return this.getConnections().filter((connection) => {
|
||||||
|
const matchesNode =
|
||||||
|
connection.from.nodeId === nodeId || connection.to.nodeId === nodeId;
|
||||||
|
const matchesPin = !pinId
|
||||||
|
? true
|
||||||
|
: connection.from.pinId === pinId || connection.to.pinId === pinId;
|
||||||
|
return matchesNode && matchesPin;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a serializable payload for the graph.
|
||||||
|
*
|
||||||
|
* @returns {{nodes: Array<ReturnType<BlueprintNode['toJSON']>>, connections: Array<ReturnType<Connection['toJSON']>>}}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
nodes: this.getNodes().map((node) => node.toJSON()),
|
||||||
|
connections: this.getConnections().map((connection) =>
|
||||||
|
connection.toJSON()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a graph from serialized payload.
|
||||||
|
*
|
||||||
|
* @param {{nodes:Array<BlueprintNodeInit>, connections:Array<ReturnType<Connection['toJSON']>>}} payload Serialized graph data.
|
||||||
|
* @returns {NodeGraph}
|
||||||
|
*/
|
||||||
|
static fromJSON(payload) {
|
||||||
|
const graph = new NodeGraph();
|
||||||
|
payload.nodes.forEach((nodeData) => {
|
||||||
|
const node = new BlueprintNode(nodeData);
|
||||||
|
graph.nodes.set(node.id, node);
|
||||||
|
});
|
||||||
|
payload.connections.forEach((connectionData) => {
|
||||||
|
const connection = new Connection(
|
||||||
|
connectionData.from,
|
||||||
|
connectionData.to,
|
||||||
|
connectionData.kind,
|
||||||
|
connectionData.id
|
||||||
|
);
|
||||||
|
graph.connections.set(connection.id, connection);
|
||||||
|
});
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the current graph contents using the provided serialized payload.
|
||||||
|
*
|
||||||
|
* @param {{nodes:Array<BlueprintNodeInit>, connections:Array<ReturnType<Connection['toJSON']>>}} payload Serialized graph data.
|
||||||
|
*/
|
||||||
|
replaceState(payload) {
|
||||||
|
this.nodes.clear();
|
||||||
|
this.connections.clear();
|
||||||
|
this.nodeCounters.clear();
|
||||||
|
|
||||||
|
payload.nodes.forEach((nodeData) => {
|
||||||
|
const node = new BlueprintNode(nodeData);
|
||||||
|
this.nodes.set(node.id, node);
|
||||||
|
this.#trackNodeCounter(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
payload.connections.forEach((connectionData) => {
|
||||||
|
const connection = new Connection(
|
||||||
|
connectionData.from,
|
||||||
|
connectionData.to,
|
||||||
|
connectionData.kind,
|
||||||
|
connectionData.id
|
||||||
|
);
|
||||||
|
this.connections.set(connection.id, connection);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit("graphrestored", {
|
||||||
|
nodes: this.getNodes(),
|
||||||
|
connections: this.getConnections(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a connection between the provided pins is permitted.
|
||||||
|
*
|
||||||
|
* @param {BlueprintNode} fromNode Source node.
|
||||||
|
* @param {PinDescriptor} fromPin Source pin descriptor.
|
||||||
|
* @param {BlueprintNode} toNode Target node.
|
||||||
|
* @param {PinDescriptor} toPin Target pin descriptor.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
#isConnectionValid(fromNode, fromPin, toNode, toPin) {
|
||||||
|
if (fromNode.id === toNode.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromPin.direction !== "output" || toPin.direction !== "input") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
toPin.kind !== "any" &&
|
||||||
|
fromPin.kind !== "any" &&
|
||||||
|
fromPin.kind !== toPin.kind
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid duplicate identical connections.
|
||||||
|
const duplicated = [...this.connections.values()].some(
|
||||||
|
(existing) =>
|
||||||
|
existing.from.nodeId === fromNode.id &&
|
||||||
|
existing.from.pinId === fromPin.id &&
|
||||||
|
existing.to.nodeId === toNode.id &&
|
||||||
|
existing.to.pinId === toPin.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return !duplicated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the node counter registry for the supplied node instance.
|
||||||
|
*
|
||||||
|
* @param {BlueprintNode} node Node reference used to update counters.
|
||||||
|
*/
|
||||||
|
#trackNodeCounter(node) {
|
||||||
|
const match = /_(\d+)$/.exec(node.id);
|
||||||
|
const numeric = match ? Number.parseInt(match[1], 10) : 0;
|
||||||
|
const current = this.nodeCounters.get(node.type) ?? 0;
|
||||||
|
this.nodeCounters.set(node.type, Math.max(current, numeric));
|
||||||
|
}
|
||||||
|
}
|
||||||
469
scripts/main.js
Normal file
469
scripts/main.js
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
import { BlueprintWorkspace } from "./ui/BlueprintWorkspace.js";
|
||||||
|
import { NodeRegistry } from "./nodes/NodeRegistry.js";
|
||||||
|
import { LuaGenerator } from "./core/LuaGenerator.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ compileLua(source: string): Promise<{ filePath: string }> }} ElectronAPI
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T | null} element DOM element reference.
|
||||||
|
* @param {string} id Identifier used for error reporting.
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
const requireElement = (element, id) => {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error(`Missing required element: ${id}`);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appBodyElement = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("appBody"), "appBody")
|
||||||
|
);
|
||||||
|
const workspaceElement = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("workspaceCanvas"), "workspaceCanvas")
|
||||||
|
);
|
||||||
|
const nodeLayer = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("nodeLayer"), "nodeLayer")
|
||||||
|
);
|
||||||
|
const connectionLayer = /** @type {SVGElement} */ (
|
||||||
|
requireElement(document.getElementById("connectionLayer"), "connectionLayer")
|
||||||
|
);
|
||||||
|
const eventList = /** @type {HTMLUListElement} */ (
|
||||||
|
requireElement(document.getElementById("eventList"), "eventList")
|
||||||
|
);
|
||||||
|
const variableList = /** @type {HTMLUListElement} */ (
|
||||||
|
requireElement(document.getElementById("variableList"), "variableList")
|
||||||
|
);
|
||||||
|
const addVariableButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("addVariableButton"),
|
||||||
|
"addVariableButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const paletteList = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("paletteList"), "paletteList")
|
||||||
|
);
|
||||||
|
const paletteSearch = /** @type {HTMLInputElement} */ (
|
||||||
|
requireElement(document.getElementById("paletteSearch"), "paletteSearch")
|
||||||
|
);
|
||||||
|
const paletteElement = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("palettePanel"), "palettePanel")
|
||||||
|
);
|
||||||
|
const inspectorContent = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("inspectorContent"),
|
||||||
|
"inspectorContent"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const duplicateNodeButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("duplicateNodeButton"),
|
||||||
|
"duplicateNodeButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const deleteNodeButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("deleteNodeButton"),
|
||||||
|
"deleteNodeButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const compileButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(document.getElementById("compileButton"), "compileButton")
|
||||||
|
);
|
||||||
|
const exportLuaButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(document.getElementById("exportLuaButton"), "exportLuaButton")
|
||||||
|
);
|
||||||
|
const projectSettingsButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("projectSettingsButton"),
|
||||||
|
"projectSettingsButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const paletteToggleButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("paletteToggleButton"),
|
||||||
|
"paletteToggleButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const frameSelectionButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("frameSelectionButton"),
|
||||||
|
"frameSelectionButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const luaModal = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("luaModal"), "luaModal")
|
||||||
|
);
|
||||||
|
const luaModalCode = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("luaModalCode"), "luaModalCode")
|
||||||
|
);
|
||||||
|
const luaModalCodeContainer = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(
|
||||||
|
luaModal.querySelector(".lua-modal__code"),
|
||||||
|
"luaModalCodeContainer"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const copyLuaButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(document.getElementById("copyLuaButton"), "copyLuaButton")
|
||||||
|
);
|
||||||
|
const closeLuaModalButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("closeLuaModalButton"),
|
||||||
|
"closeLuaModalButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const registry = new NodeRegistry();
|
||||||
|
const generator = new LuaGenerator(registry);
|
||||||
|
const workspace = new BlueprintWorkspace({
|
||||||
|
workspaceElement,
|
||||||
|
nodeLayer,
|
||||||
|
connectionLayer,
|
||||||
|
paletteList,
|
||||||
|
paletteSearch,
|
||||||
|
paletteElement,
|
||||||
|
inspectorContent,
|
||||||
|
duplicateNodeButton,
|
||||||
|
deleteNodeButton,
|
||||||
|
generator,
|
||||||
|
registry,
|
||||||
|
eventList,
|
||||||
|
variableList,
|
||||||
|
addVariableButton,
|
||||||
|
projectSettingsButton,
|
||||||
|
paletteToggleButton,
|
||||||
|
appBodyElement,
|
||||||
|
frameSelectionButton,
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.initialize();
|
||||||
|
|
||||||
|
/** @type {ElectronAPI | undefined} */
|
||||||
|
const electronAPI =
|
||||||
|
typeof window !== "undefined" && "electronAPI" in window
|
||||||
|
? /** @type {ElectronAPI} */ (window.electronAPI)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
document.addEventListener("contextmenu", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
const escapeHtml = (value) =>
|
||||||
|
value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
|
const luaTokenPattern =
|
||||||
|
/(--.*$)|("(?:\\.|[^"\\])*")|('(?!\[)(?:\\.|[^'\\])*')|(\b\d+(?:\.\d+)?\b)|\b(and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/gm;
|
||||||
|
|
||||||
|
const highlightLua = (source) => {
|
||||||
|
if (!source) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
source.replace(
|
||||||
|
luaTokenPattern,
|
||||||
|
(
|
||||||
|
match,
|
||||||
|
comment,
|
||||||
|
doubleQuoted,
|
||||||
|
singleQuoted,
|
||||||
|
numberLiteral,
|
||||||
|
keyword,
|
||||||
|
offset
|
||||||
|
) => {
|
||||||
|
result += escapeHtml(source.slice(lastIndex, offset));
|
||||||
|
|
||||||
|
let tokenType = "";
|
||||||
|
if (comment) {
|
||||||
|
tokenType = "comment";
|
||||||
|
} else if (doubleQuoted || singleQuoted) {
|
||||||
|
tokenType = "string";
|
||||||
|
} else if (numberLiteral) {
|
||||||
|
tokenType = "number";
|
||||||
|
} else {
|
||||||
|
tokenType = "keyword";
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `<span class="lua-token lua-token--${tokenType}">${escapeHtml(
|
||||||
|
match
|
||||||
|
)}</span>`;
|
||||||
|
lastIndex = offset + match.length;
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
result += escapeHtml(source.slice(lastIndex));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusableSelector =
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||||
|
let isLuaModalOpen = false;
|
||||||
|
let lastFocusedElement = /** @type {HTMLElement | null} */ (null);
|
||||||
|
let copyFeedbackTimeout = /** @type {number | null} */ (null);
|
||||||
|
let currentLuaSource = "";
|
||||||
|
let compileFeedbackTimeout = /** @type {number | null} */ (null);
|
||||||
|
|
||||||
|
const scheduleMicrotask = (callback) => {
|
||||||
|
if (typeof queueMicrotask === "function") {
|
||||||
|
queueMicrotask(callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.resolve().then(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModalFocusableElements = () => {
|
||||||
|
return /** @type {Array<HTMLElement>} */ (
|
||||||
|
Array.from(luaModal.querySelectorAll(focusableSelector)).filter(
|
||||||
|
(element) =>
|
||||||
|
element instanceof HTMLElement && !element.hasAttribute("disabled")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalKeydown = (event) => {
|
||||||
|
if (!isLuaModalOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeLuaModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key !== "Tab") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusable = getModalFocusableElements();
|
||||||
|
if (!focusable.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = focusable.indexOf(document.activeElement);
|
||||||
|
let nextIndex = currentIndex;
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
nextIndex = currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1;
|
||||||
|
} else {
|
||||||
|
nextIndex =
|
||||||
|
currentIndex === -1 || currentIndex === focusable.length - 1
|
||||||
|
? 0
|
||||||
|
: currentIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
focusable[nextIndex].focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeClipboard = async (text) => {
|
||||||
|
if (
|
||||||
|
navigator.clipboard &&
|
||||||
|
typeof navigator.clipboard.writeText === "function"
|
||||||
|
) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const previousRange =
|
||||||
|
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
||||||
|
const fallback = document.createElement("textarea");
|
||||||
|
fallback.value = text;
|
||||||
|
fallback.setAttribute("readonly", "true");
|
||||||
|
fallback.style.position = "fixed";
|
||||||
|
fallback.style.opacity = "0";
|
||||||
|
document.body.appendChild(fallback);
|
||||||
|
fallback.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(fallback);
|
||||||
|
|
||||||
|
if (previousRange && selection) {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(previousRange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetCopyFeedback = () => {
|
||||||
|
copyLuaButton.classList.remove("is-success");
|
||||||
|
copyLuaButton.setAttribute("aria-label", "Copy to clipboard");
|
||||||
|
copyLuaButton.setAttribute("title", "Copy to clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLuaModal = () => {
|
||||||
|
currentLuaSource = workspace.exportLua();
|
||||||
|
luaModalCode.innerHTML = highlightLua(currentLuaSource);
|
||||||
|
luaModalCodeContainer.scrollTop = 0;
|
||||||
|
if (copyFeedbackTimeout !== null) {
|
||||||
|
window.clearTimeout(copyFeedbackTimeout);
|
||||||
|
copyFeedbackTimeout = null;
|
||||||
|
}
|
||||||
|
resetCopyFeedback();
|
||||||
|
|
||||||
|
if (isLuaModalOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFocusedElement =
|
||||||
|
document.activeElement instanceof HTMLElement
|
||||||
|
? document.activeElement
|
||||||
|
: null;
|
||||||
|
luaModal.removeAttribute("hidden");
|
||||||
|
luaModal.setAttribute("aria-hidden", "false");
|
||||||
|
document.body.classList.add("modal-open");
|
||||||
|
isLuaModalOpen = true;
|
||||||
|
document.addEventListener("keydown", handleModalKeydown);
|
||||||
|
|
||||||
|
scheduleMicrotask(() => {
|
||||||
|
const focusTarget = luaModalCodeContainer;
|
||||||
|
focusTarget.focus();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeLuaModal = () => {
|
||||||
|
if (!isLuaModalOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLuaModalOpen = false;
|
||||||
|
luaModal.setAttribute("aria-hidden", "true");
|
||||||
|
luaModal.setAttribute("hidden", "");
|
||||||
|
document.body.classList.remove("modal-open");
|
||||||
|
document.removeEventListener("keydown", handleModalKeydown);
|
||||||
|
|
||||||
|
if (copyFeedbackTimeout !== null) {
|
||||||
|
window.clearTimeout(copyFeedbackTimeout);
|
||||||
|
copyFeedbackTimeout = null;
|
||||||
|
}
|
||||||
|
resetCopyFeedback();
|
||||||
|
|
||||||
|
if (lastFocusedElement && document.contains(lastFocusedElement)) {
|
||||||
|
lastFocusedElement.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the compile button to its idle appearance.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const resetCompileButton = () => {
|
||||||
|
if (compileFeedbackTimeout !== null) {
|
||||||
|
window.clearTimeout(compileFeedbackTimeout);
|
||||||
|
compileFeedbackTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
compileButton.classList.remove("is-success", "is-error", "is-busy");
|
||||||
|
compileButton.disabled = false;
|
||||||
|
compileButton.textContent = "Compile";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles compile button clicks by requesting a Lua export from Electron.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const handleCompileClick = async () => {
|
||||||
|
if (!electronAPI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compileFeedbackTimeout !== null) {
|
||||||
|
window.clearTimeout(compileFeedbackTimeout);
|
||||||
|
compileFeedbackTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
compileButton.classList.remove("is-success", "is-error");
|
||||||
|
compileButton.classList.add("is-busy");
|
||||||
|
compileButton.disabled = true;
|
||||||
|
compileButton.textContent = "Compiling...";
|
||||||
|
|
||||||
|
const source = workspace.exportLua();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await electronAPI.compileLua(source);
|
||||||
|
console.info("Lua compiled to", result.filePath);
|
||||||
|
compileButton.classList.remove("is-busy");
|
||||||
|
compileButton.classList.add("is-success");
|
||||||
|
compileButton.disabled = false;
|
||||||
|
compileButton.textContent = "Compiled!";
|
||||||
|
compileFeedbackTimeout = window.setTimeout(() => {
|
||||||
|
resetCompileButton();
|
||||||
|
}, 1800);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to compile Lua", error);
|
||||||
|
compileButton.classList.remove("is-busy");
|
||||||
|
compileButton.classList.add("is-error");
|
||||||
|
compileButton.disabled = false;
|
||||||
|
compileButton.textContent = "Failed";
|
||||||
|
compileFeedbackTimeout = window.setTimeout(() => {
|
||||||
|
resetCompileButton();
|
||||||
|
}, 2200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (electronAPI && typeof electronAPI.compileLua === "function") {
|
||||||
|
compileButton.disabled = false;
|
||||||
|
compileButton.addEventListener("click", () => {
|
||||||
|
handleCompileClick().catch((error) => {
|
||||||
|
console.error("Unhandled compile error", error);
|
||||||
|
resetCompileButton();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
compileButton.disabled = true;
|
||||||
|
compileButton.title = "Compile is available in the desktop app";
|
||||||
|
}
|
||||||
|
|
||||||
|
exportLuaButton.addEventListener("click", () => {
|
||||||
|
openLuaModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
luaModal.addEventListener("click", (event) => {
|
||||||
|
const target =
|
||||||
|
event.target instanceof HTMLElement
|
||||||
|
? event.target.closest("[data-modal-close]")
|
||||||
|
: null;
|
||||||
|
if (target) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeLuaModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyLuaButton.addEventListener("click", async () => {
|
||||||
|
if (!currentLuaSource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeClipboard(currentLuaSource);
|
||||||
|
copyLuaButton.classList.add("is-success");
|
||||||
|
copyLuaButton.setAttribute("aria-label", "Copied!");
|
||||||
|
copyLuaButton.setAttribute("title", "Copied!");
|
||||||
|
if (copyFeedbackTimeout !== null) {
|
||||||
|
window.clearTimeout(copyFeedbackTimeout);
|
||||||
|
}
|
||||||
|
copyFeedbackTimeout = window.setTimeout(() => {
|
||||||
|
resetCopyFeedback();
|
||||||
|
copyFeedbackTimeout = null;
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Copy failed", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeLuaModalButton.addEventListener("click", () => {
|
||||||
|
closeLuaModal();
|
||||||
|
});
|
||||||
296
scripts/nodes/NodeRegistry.js
Normal file
296
scripts/nodes/NodeRegistry.js
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { BlueprintNode } from "../core/BlueprintNode.js";
|
||||||
|
import { nodeModules } from "./library/index.js";
|
||||||
|
|
||||||
|
/** @typedef {import('./nodeTypes.js').NodeDefinition} NodeDefinition */
|
||||||
|
/** @typedef {import('./nodeTypes.js').NodeBehavior} NodeBehavior */
|
||||||
|
/** @typedef {import('./nodeTypes.js').NodeModule} NodeModule */
|
||||||
|
/** @typedef {{ value: string, weight: number }} SearchField */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central registry providing node definitions, behavior hooks, and factory helpers.
|
||||||
|
*/
|
||||||
|
export class NodeRegistry {
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, NodeDefinition>} */
|
||||||
|
this.definitions = new Map();
|
||||||
|
/** @type {Map<string, NodeBehavior>} */
|
||||||
|
this.behaviors = new Map();
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
this.entryNodeTypes = new Set();
|
||||||
|
/** @type {Map<string, '_init'|'_update'|'_draw'|'_update60'>} */
|
||||||
|
this.entryPointEvents = new Map();
|
||||||
|
|
||||||
|
nodeModules.forEach((module) => this.registerModule(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes search inputs for case-insensitive comparisons.
|
||||||
|
*
|
||||||
|
* @param {string} query Raw query string.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#normalizeQuery(query) {
|
||||||
|
return (query ?? "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a node definition matches the provided query.
|
||||||
|
*
|
||||||
|
* @param {NodeDefinition} definition Definition to inspect.
|
||||||
|
* @param {string} query Search query.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
matchesDefinition(definition, query) {
|
||||||
|
const normalized = this.#normalizeQuery(query);
|
||||||
|
if (!normalized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const score = this.#scoreDefinition(definition, normalized);
|
||||||
|
return Number.isFinite(score);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates every registered definition.
|
||||||
|
*
|
||||||
|
* @returns {Array<NodeDefinition>}
|
||||||
|
*/
|
||||||
|
list() {
|
||||||
|
return [...this.definitions.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single node definition.
|
||||||
|
*
|
||||||
|
* @param {string} id Definition identifier.
|
||||||
|
* @returns {NodeDefinition | undefined}
|
||||||
|
*/
|
||||||
|
get(id) {
|
||||||
|
return this.definitions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a fuzzy search across node metadata and returns results sorted by distance.
|
||||||
|
*
|
||||||
|
* @param {string} query Search input.
|
||||||
|
* @returns {Array<NodeDefinition>}
|
||||||
|
*/
|
||||||
|
search(query) {
|
||||||
|
const normalized = this.#normalizeQuery(query);
|
||||||
|
if (!normalized) {
|
||||||
|
return this.list().sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Array<{ definition: NodeDefinition, score: number }>} */
|
||||||
|
const ranked = [];
|
||||||
|
this.definitions.forEach((definition) => {
|
||||||
|
const score = this.#scoreDefinition(definition, normalized);
|
||||||
|
if (Number.isFinite(score)) {
|
||||||
|
ranked.push({ definition, score });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ranked.sort((a, b) => {
|
||||||
|
if (a.score === b.score) {
|
||||||
|
return a.definition.title.localeCompare(b.definition.title);
|
||||||
|
}
|
||||||
|
return a.score - b.score;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ranked.map((entry) => entry.definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates searchable fields for a node definition and assigns weighting.
|
||||||
|
*
|
||||||
|
* @param {NodeDefinition} definition Definition to inspect.
|
||||||
|
* @returns {Array<SearchField>} Normalized field collection with weights.
|
||||||
|
*/
|
||||||
|
#collectSearchFields(definition) {
|
||||||
|
const fields = /** @type {Array<SearchField>} */ ([
|
||||||
|
{ value: definition.title, weight: 0.8 },
|
||||||
|
{ value: definition.category, weight: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (definition.description) {
|
||||||
|
fields.push({ value: definition.description, weight: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(definition.searchTags)) {
|
||||||
|
definition.searchTags.forEach((tag) => {
|
||||||
|
fields.push({ value: tag, weight: 1 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
.filter((field) => typeof field.value === "string" && field.value.trim().length)
|
||||||
|
.map((field) => ({
|
||||||
|
value: field.value.toLowerCase(),
|
||||||
|
weight: field.weight,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a fuzzy score representing how closely a definition matches a query.
|
||||||
|
*
|
||||||
|
* @param {NodeDefinition} definition Target definition.
|
||||||
|
* @param {string} normalizedQuery Lowercase trimmed query string.
|
||||||
|
* @returns {number} Matching score where lower values are closer; Infinity means no match.
|
||||||
|
*/
|
||||||
|
#scoreDefinition(definition, normalizedQuery) {
|
||||||
|
const fields = this.#collectSearchFields(definition);
|
||||||
|
if (!fields.length) {
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
let best = Number.POSITIVE_INFINITY;
|
||||||
|
for (const field of fields) {
|
||||||
|
const score = this.#computeFuzzyScore(normalizedQuery, field.value);
|
||||||
|
if (!Number.isFinite(score)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const weighted = score * field.weight;
|
||||||
|
if (weighted < best) {
|
||||||
|
best = weighted;
|
||||||
|
}
|
||||||
|
if (best === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a fuzzy matching distance between the query and a candidate string.
|
||||||
|
*
|
||||||
|
* @param {string} query Normalized query string.
|
||||||
|
* @param {string} candidate Normalized candidate string.
|
||||||
|
* @returns {number} Distance score; Infinity indicates the query is not a subsequence of the candidate.
|
||||||
|
*/
|
||||||
|
#computeFuzzyScore(query, candidate) {
|
||||||
|
if (!candidate) {
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qLength = query.length;
|
||||||
|
const cLength = candidate.length;
|
||||||
|
if (!qLength) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let qIndex = 0;
|
||||||
|
let score = 0;
|
||||||
|
let lastMatch = -1;
|
||||||
|
|
||||||
|
for (let cIndex = 0; cIndex < cLength; cIndex += 1) {
|
||||||
|
if (candidate[cIndex] !== query[qIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastMatch === -1) {
|
||||||
|
score += cIndex;
|
||||||
|
} else {
|
||||||
|
score += cIndex - lastMatch - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMatch = cIndex;
|
||||||
|
qIndex += 1;
|
||||||
|
|
||||||
|
if (qIndex === qLength) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qIndex !== qLength) {
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
score += cLength - lastMatch - 1;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a blueprint node from a definition.
|
||||||
|
*
|
||||||
|
* @param {string} definitionId Definition identifier.
|
||||||
|
* @param {{id: string, position: {x:number,y:number}}} options Initialization options.
|
||||||
|
* @returns {BlueprintNode}
|
||||||
|
*/
|
||||||
|
createNode(definitionId, options) {
|
||||||
|
const definition = this.get(definitionId);
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error(`Unknown node definition: ${definitionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties = {};
|
||||||
|
definition.properties.forEach((schema) => {
|
||||||
|
const value = schema.defaultValue ?? null;
|
||||||
|
properties[schema.key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
definition.initializeProperties?.(properties);
|
||||||
|
|
||||||
|
return new BlueprintNode({
|
||||||
|
id: options.id,
|
||||||
|
type: definition.id,
|
||||||
|
title: definition.title,
|
||||||
|
position: { ...options.position },
|
||||||
|
inputs: definition.inputs.map((pin) => ({
|
||||||
|
...pin,
|
||||||
|
})),
|
||||||
|
outputs: definition.outputs.map((pin) => ({
|
||||||
|
...pin,
|
||||||
|
})),
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a node module exposing editor metadata and Lua behavior.
|
||||||
|
*
|
||||||
|
* @param {NodeModule} module Node module to register.
|
||||||
|
*/
|
||||||
|
registerModule(module) {
|
||||||
|
const { definition, behavior } = module;
|
||||||
|
this.definitions.set(definition.id, definition);
|
||||||
|
|
||||||
|
if (behavior) {
|
||||||
|
this.behaviors.set(definition.id, behavior);
|
||||||
|
if (behavior.isEntryPoint) {
|
||||||
|
this.entryNodeTypes.add(definition.id);
|
||||||
|
if (behavior.eventName) {
|
||||||
|
this.entryPointEvents.set(definition.id, behavior.eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves behavior metadata for a node type.
|
||||||
|
*
|
||||||
|
* @param {string} id Node identifier.
|
||||||
|
* @returns {NodeBehavior | undefined}
|
||||||
|
*/
|
||||||
|
getBehavior(id) {
|
||||||
|
return this.behaviors.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates node types flagged as entry points.
|
||||||
|
*
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
getEntryNodeTypes() {
|
||||||
|
return [...this.entryNodeTypes];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes lifecycle event bindings for entry nodes.
|
||||||
|
*
|
||||||
|
* @returns {Map<string, '_init'|'_update'|'_draw'|'_update60'>}
|
||||||
|
*/
|
||||||
|
getEntryPointEvents() {
|
||||||
|
return new Map(this.entryPointEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
scripts/nodes/library/addNumber.js
Normal file
41
scripts/nodes/library/addNumber.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two numeric values.
|
||||||
|
*/
|
||||||
|
export const addNumberNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "add_number",
|
||||||
|
title: "Add",
|
||||||
|
category: "Math",
|
||||||
|
description: "Add two numbers.",
|
||||||
|
searchTags: ["add", "math", "sum", "number"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "a",
|
||||||
|
name: "A",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b",
|
||||||
|
name: "B",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "res", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "0");
|
||||||
|
return `(${a}) + (${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
16
scripts/nodes/library/audio/index.js
Normal file
16
scripts/nodes/library/audio/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { sfxNode } from "./sfx.js";
|
||||||
|
import { musicNode } from "./music.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All audio-related node modules backed by PICO-8 helpers.
|
||||||
|
*/
|
||||||
|
export const audioNodes = [sfxNode, musicNode];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of documented audio helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const audioFunctionChecklist = [
|
||||||
|
{ function: "SFX", nodeId: "audio_sfx", implemented: true },
|
||||||
|
{ function: "MUSIC", nodeId: "audio_music", implemented: true },
|
||||||
|
];
|
||||||
64
scripts/nodes/library/audio/music.js
Normal file
64
scripts/nodes/library/audio/music.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays or stops music using the PICO-8 music helper.
|
||||||
|
*/
|
||||||
|
export const musicNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "audio_music",
|
||||||
|
title: "Play Music",
|
||||||
|
category: "Audio",
|
||||||
|
description:
|
||||||
|
"Start, fade, or stop music playback using MUSIC().",
|
||||||
|
searchTags: ["music", "audio", "song", "pattern"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "pattern",
|
||||||
|
name: "Pattern",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Pattern index or command",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fade",
|
||||||
|
name: "Fade (ms)",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Fade duration in milliseconds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mask",
|
||||||
|
name: "Channel Mask",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Bitmask reserving channels",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const pattern = resolveValueInput("pattern", "0");
|
||||||
|
const fade = resolveValueInput("fade", OMIT);
|
||||||
|
const mask = resolveValueInput("mask", OMIT);
|
||||||
|
|
||||||
|
const args = [pattern];
|
||||||
|
if (fade !== OMIT || mask !== OMIT) {
|
||||||
|
const fadeArg = fade === OMIT ? "nil" : fade;
|
||||||
|
args.push(fadeArg);
|
||||||
|
if (mask !== OMIT) {
|
||||||
|
args.push(mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = `${indent(indentLevel)}music(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
76
scripts/nodes/library/audio/sfx.js
Normal file
76
scripts/nodes/library/audio/sfx.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays or controls a sound effect using the PICO-8 sfx helper.
|
||||||
|
*/
|
||||||
|
export const sfxNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "audio_sfx",
|
||||||
|
title: "Play SFX",
|
||||||
|
category: "Audio",
|
||||||
|
description:
|
||||||
|
"Trigger a sound effect, control looping, or stop playback via SFX().",
|
||||||
|
searchTags: ["sfx", "audio", "sound", "effect"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
name: "SFX",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Sound effect slot or command",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "channel",
|
||||||
|
name: "Channel",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Playback channel (-1 auto)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "offset",
|
||||||
|
name: "Offset",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Note offset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "length",
|
||||||
|
name: "Length",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Number of notes to play",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const id = resolveValueInput("id", "0");
|
||||||
|
const channel = resolveValueInput("channel", OMIT);
|
||||||
|
const offset = resolveValueInput("offset", OMIT);
|
||||||
|
const length = resolveValueInput("length", OMIT);
|
||||||
|
|
||||||
|
const args = [id];
|
||||||
|
if (channel !== OMIT || offset !== OMIT || length !== OMIT) {
|
||||||
|
const channelArg = channel === OMIT ? "nil" : channel;
|
||||||
|
args.push(channelArg);
|
||||||
|
if (offset !== OMIT || length !== OMIT) {
|
||||||
|
const offsetArg = offset === OMIT ? "nil" : offset;
|
||||||
|
args.push(offsetArg);
|
||||||
|
if (length !== OMIT) {
|
||||||
|
args.push(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = `${indent(indentLevel)}sfx(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
26
scripts/nodes/library/booleanLiteral.js
Normal file
26
scripts/nodes/library/booleanLiteral.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant boolean literal.
|
||||||
|
*/
|
||||||
|
export const booleanLiteralNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "boolean_literal",
|
||||||
|
title: "Boolean",
|
||||||
|
category: "Values",
|
||||||
|
description: "Constant boolean literal.",
|
||||||
|
searchTags: ["boolean", "literal", "true", "false"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "boolean" },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{ key: "value", label: "Value", type: "boolean", defaultValue: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ node, formatLiteral }) => {
|
||||||
|
return formatLiteral("boolean", node.properties.value ?? true);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
32
scripts/nodes/library/callCustomEvent.js
Normal file
32
scripts/nodes/library/callCustomEvent.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes a custom event by reference.
|
||||||
|
* @type {import('../nodeTypes.js').NodeModule}
|
||||||
|
*/
|
||||||
|
export const callCustomEventNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "call_custom_event",
|
||||||
|
title: "Call Custom Event",
|
||||||
|
category: "Events",
|
||||||
|
description: "Invokes a custom event defined elsewhere in the graph.",
|
||||||
|
searchTags: ["event", "call", "trigger", "custom"],
|
||||||
|
inputs: [{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" }],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
initializeProperties: (properties) => {
|
||||||
|
properties.eventId =
|
||||||
|
typeof properties.eventId === "string" ? properties.eventId : "";
|
||||||
|
if (
|
||||||
|
!properties.arguments ||
|
||||||
|
typeof properties.arguments !== "object" ||
|
||||||
|
Array.isArray(properties.arguments)
|
||||||
|
) {
|
||||||
|
properties.arguments = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
47
scripts/nodes/library/compare.js
Normal file
47
scripts/nodes/library/compare.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two values using a selected operator.
|
||||||
|
*/
|
||||||
|
export const compareNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "compare",
|
||||||
|
title: "Compare",
|
||||||
|
category: "Logic",
|
||||||
|
description: "Compare two values with a selected operator.",
|
||||||
|
searchTags: ["compare", "logic", "condition", "branch"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "a", name: "A", direction: "input", kind: "any" },
|
||||||
|
{ id: "b", name: "B", direction: "input", kind: "any" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "res", name: "Result", direction: "output", kind: "boolean" },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
key: "operator",
|
||||||
|
label: "Operator",
|
||||||
|
type: "enum",
|
||||||
|
defaultValue: "==",
|
||||||
|
options: [
|
||||||
|
{ label: "Equal", value: "==" },
|
||||||
|
{ label: "Not Equal", value: "!=" },
|
||||||
|
{ label: "Greater", value: ">" },
|
||||||
|
{ label: "Less", value: "<" },
|
||||||
|
{ label: "Greater Or Equal", value: ">=" },
|
||||||
|
{ label: "Less Or Equal", value: "<=" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ node, resolveValueInput, sanitizeOperator }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "0");
|
||||||
|
const operator = sanitizeOperator(
|
||||||
|
String(node.properties.operator ?? "==")
|
||||||
|
);
|
||||||
|
return `(${a}) ${operator} (${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
44
scripts/nodes/library/customEvent.js
Normal file
44
scripts/nodes/library/customEvent.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a reusable custom event entry point.
|
||||||
|
* @type {import('../nodeTypes.js').NodeModule}
|
||||||
|
*/
|
||||||
|
export const customEventNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "custom_event",
|
||||||
|
title: "Custom Event",
|
||||||
|
category: "Events",
|
||||||
|
description: "Defines a custom event that can be triggered elsewhere.",
|
||||||
|
searchTags: ["event", "custom", "broadcast", "trigger"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Event Name",
|
||||||
|
type: "string",
|
||||||
|
defaultValue: "CustomEvent",
|
||||||
|
placeholder: "Enter event name",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initializeProperties: (properties) => {
|
||||||
|
if (typeof properties.name !== "string" || !properties.name.trim()) {
|
||||||
|
properties.name = "CustomEvent";
|
||||||
|
}
|
||||||
|
if (!Array.isArray(properties.parameters)) {
|
||||||
|
properties.parameters = [];
|
||||||
|
}
|
||||||
|
const counter = Number.isFinite(properties.parameterCounter)
|
||||||
|
? Number(properties.parameterCounter)
|
||||||
|
: 0;
|
||||||
|
properties.parameterCounter = counter;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isEntryPoint: true,
|
||||||
|
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
|
||||||
|
}
|
||||||
|
);
|
||||||
33
scripts/nodes/library/data/cartdata.js
Normal file
33
scripts/nodes/library/data/cartdata.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens persistent cartridge storage using cartdata.
|
||||||
|
*/
|
||||||
|
export const cartdataNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "data_cartdata",
|
||||||
|
title: "Cart Data Init",
|
||||||
|
category: "Data",
|
||||||
|
description: "Setup persistent storage for the cartridge using CARTDATA().",
|
||||||
|
searchTags: ["cartdata", "save", "persistent", "data"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
name: "Identifier",
|
||||||
|
direction: "input",
|
||||||
|
kind: "string",
|
||||||
|
defaultValue: "\"my_cart\"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "loaded", name: "Loaded", direction: "output", kind: "boolean" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const id = resolveValueInput("id", '"pico_cart"');
|
||||||
|
return `cartdata(${id})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
33
scripts/nodes/library/data/dget.js
Normal file
33
scripts/nodes/library/data/dget.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a persistent value using dget.
|
||||||
|
*/
|
||||||
|
export const dgetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "data_dget",
|
||||||
|
title: "Data Get",
|
||||||
|
category: "Data",
|
||||||
|
description: "Retrieve a persistent number using DGET().",
|
||||||
|
searchTags: ["dget", "load", "data", "persistent"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
name: "Index",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const index = resolveValueInput("index", "0");
|
||||||
|
return `dget(${index})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
43
scripts/nodes/library/data/dset.js
Normal file
43
scripts/nodes/library/data/dset.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a persistent value using dset.
|
||||||
|
*/
|
||||||
|
export const dsetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "data_dset",
|
||||||
|
title: "Data Set",
|
||||||
|
category: "Data",
|
||||||
|
description: "Store a persistent number using DSET().",
|
||||||
|
searchTags: ["dset", "save", "data", "persistent"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
name: "Index",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "value",
|
||||||
|
name: "Value",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const index = resolveValueInput("index", "0");
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
const line = `${indent(indentLevel)}dset(${index}, ${value})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
18
scripts/nodes/library/data/index.js
Normal file
18
scripts/nodes/library/data/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { cartdataNode } from "./cartdata.js";
|
||||||
|
import { dsetNode } from "./dset.js";
|
||||||
|
import { dgetNode } from "./dget.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All data persistence node modules backed by PICO-8 helpers.
|
||||||
|
*/
|
||||||
|
export const dataNodes = [cartdataNode, dsetNode, dgetNode];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of documented cart data helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const dataFunctionChecklist = [
|
||||||
|
{ function: "CARTDATA", nodeId: "data_cartdata", implemented: true },
|
||||||
|
{ function: "DSET", nodeId: "data_dset", implemented: true },
|
||||||
|
{ function: "DGET", nodeId: "data_dget", implemented: true },
|
||||||
|
];
|
||||||
35
scripts/nodes/library/devkit/devkitConfig.js
Normal file
35
scripts/nodes/library/devkit/devkitConfig.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies host configuration flags via devkit_config().
|
||||||
|
*/
|
||||||
|
export const devkitConfigNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "devkit_config",
|
||||||
|
title: "Devkit Config",
|
||||||
|
category: "Devkit",
|
||||||
|
description: "Send a configuration string to devkit_config().",
|
||||||
|
searchTags: ["devkit", "config", "host"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "config",
|
||||||
|
name: "Config",
|
||||||
|
direction: "input",
|
||||||
|
kind: "string",
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const config = resolveValueInput("config", '""');
|
||||||
|
const line = `${indent(indentLevel)}devkit_config(${config})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
33
scripts/nodes/library/devkit/devkitInputRead.js
Normal file
33
scripts/nodes/library/devkit/devkitInputRead.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads values from devkit-provided inputs via devkit_input().
|
||||||
|
*/
|
||||||
|
export const devkitInputReadNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "devkit_input_read",
|
||||||
|
title: "Devkit Input Read",
|
||||||
|
category: "Devkit",
|
||||||
|
description: "Fetch the value of a devkit input index.",
|
||||||
|
searchTags: ["devkit", "input", "read"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
name: "Index",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "any" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const index = resolveValueInput("index", "0");
|
||||||
|
return `devkit_input(${index})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
43
scripts/nodes/library/devkit/devkitInputWrite.js
Normal file
43
scripts/nodes/library/devkit/devkitInputWrite.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends values to devkit inputs via devkit_input().
|
||||||
|
*/
|
||||||
|
export const devkitInputWriteNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "devkit_input_write",
|
||||||
|
title: "Devkit Input Write",
|
||||||
|
category: "Devkit",
|
||||||
|
description: "Push a value to a devkit input index.",
|
||||||
|
searchTags: ["devkit", "input", "write"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
name: "Index",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "value",
|
||||||
|
name: "Value",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const index = resolveValueInput("index", "0");
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
const line = `${indent(indentLevel)}devkit_input(${index}, ${value})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
22
scripts/nodes/library/devkit/index.js
Normal file
22
scripts/nodes/library/devkit/index.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { devkitConfigNode } from "./devkitConfig.js";
|
||||||
|
import { devkitInputReadNode } from "./devkitInputRead.js";
|
||||||
|
import { devkitInputWriteNode } from "./devkitInputWrite.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node modules covering devkit helpers.
|
||||||
|
*/
|
||||||
|
export const devkitNodes = [
|
||||||
|
devkitConfigNode,
|
||||||
|
devkitInputReadNode,
|
||||||
|
devkitInputWriteNode,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of devkit helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const devkitFunctionChecklist = [
|
||||||
|
{ function: "DEVKIT_CONFIG", nodeId: "devkit_config", implemented: true },
|
||||||
|
{ function: "DEVKIT_INPUT (read)", nodeId: "devkit_input_read", implemented: true },
|
||||||
|
{ function: "DEVKIT_INPUT (write)", nodeId: "devkit_input_write", implemented: true },
|
||||||
|
];
|
||||||
26
scripts/nodes/library/eventDraw.js
Normal file
26
scripts/nodes/library/eventDraw.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle event triggered once per visible frame.
|
||||||
|
* @type {import('../nodeTypes.js').NodeModule}
|
||||||
|
*/
|
||||||
|
export const eventDrawNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "event_draw",
|
||||||
|
title: "Event Draw",
|
||||||
|
category: "Events",
|
||||||
|
description: "Called once per visible frame.",
|
||||||
|
searchTags: ["draw", "render", "frame", "loop"],
|
||||||
|
unique: true,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isEntryPoint: true,
|
||||||
|
eventName: "_draw",
|
||||||
|
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
|
||||||
|
}
|
||||||
|
);
|
||||||
26
scripts/nodes/library/eventStart.js
Normal file
26
scripts/nodes/library/eventStart.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle entry triggered once when the cart boots.
|
||||||
|
* @type {import('../nodeTypes.js').NodeModule}
|
||||||
|
*/
|
||||||
|
export const eventInitNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "event_start",
|
||||||
|
title: "Event Init",
|
||||||
|
category: "Events",
|
||||||
|
description: "Called once on cart startup.",
|
||||||
|
searchTags: ["start", "init", "boot", "lifecycle"],
|
||||||
|
unique: true,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isEntryPoint: true,
|
||||||
|
eventName: "_init",
|
||||||
|
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
|
||||||
|
}
|
||||||
|
);
|
||||||
25
scripts/nodes/library/eventUpdate.js
Normal file
25
scripts/nodes/library/eventUpdate.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('../nodeTypes.js').NodeModule}
|
||||||
|
*/
|
||||||
|
export const eventUpdateNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "event_update",
|
||||||
|
title: "Event Update",
|
||||||
|
category: "Events",
|
||||||
|
description: "Called once per update",
|
||||||
|
searchTags: ["update", "loop", "frame", "tick"],
|
||||||
|
unique: true,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isEntryPoint: true,
|
||||||
|
eventName: "_update",
|
||||||
|
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
|
||||||
|
}
|
||||||
|
);
|
||||||
92
scripts/nodes/library/forLoop.js
Normal file
92
scripts/nodes/library/forLoop.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates from start to end inclusive using PICO-8 for semantics.
|
||||||
|
*/
|
||||||
|
export const forLoopNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "for_loop",
|
||||||
|
title: "For Loop",
|
||||||
|
category: "Logic",
|
||||||
|
description:
|
||||||
|
"Iterate from start to end inclusive using PICO-8 for semantics.",
|
||||||
|
searchTags: ["loop", "for", "iterate", "counter"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "start",
|
||||||
|
name: "Start",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "end",
|
||||||
|
name: "End",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "step",
|
||||||
|
name: "Step",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "loop", name: "Loop", direction: "output", kind: "exec" },
|
||||||
|
{ id: "completed", name: "Completed", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
key: "index",
|
||||||
|
label: "Index Variable",
|
||||||
|
type: "string",
|
||||||
|
defaultValue: "i",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({
|
||||||
|
node,
|
||||||
|
indent,
|
||||||
|
indentLevel,
|
||||||
|
resolveValueInput,
|
||||||
|
sanitizeIdentifier,
|
||||||
|
emitBranch,
|
||||||
|
path,
|
||||||
|
}) => {
|
||||||
|
const index = sanitizeIdentifier(String(node.properties.index ?? "i"));
|
||||||
|
const startValue = resolveValueInput("start", "0");
|
||||||
|
const endValue = resolveValueInput("end", "0");
|
||||||
|
const stepValue = resolveValueInput("step", "1");
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`${indent(
|
||||||
|
indentLevel
|
||||||
|
)}for ${index} = ${startValue}, ${endValue}, ${stepValue} do`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const loopLines = emitBranch("loop", {
|
||||||
|
indentLevel: indentLevel + 1,
|
||||||
|
path: new Set(path),
|
||||||
|
});
|
||||||
|
if (!loopLines.length) {
|
||||||
|
lines.push(`${indent(indentLevel + 1)}-- loop body`);
|
||||||
|
} else {
|
||||||
|
lines.push(...loopLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`${indent(indentLevel)}end`);
|
||||||
|
lines.push(
|
||||||
|
...emitBranch("completed", {
|
||||||
|
indentLevel,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
0
scripts/nodes/library/getLocalVariable.js
Normal file
0
scripts/nodes/library/getLocalVariable.js
Normal file
30
scripts/nodes/library/getVariable.js
Normal file
30
scripts/nodes/library/getVariable.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the current value of a variable.
|
||||||
|
*/
|
||||||
|
export const getVariableNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "get_var",
|
||||||
|
title: "Get Variable",
|
||||||
|
category: "Logic",
|
||||||
|
description: "Expose the current value of a variable.",
|
||||||
|
searchTags: ["get", "read", "variable", "access"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [{ id: "value", name: "Value", direction: "output", kind: "any" }],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Variable Name",
|
||||||
|
type: "string",
|
||||||
|
defaultValue: "score",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ node, sanitizeIdentifier }) => {
|
||||||
|
const name = sanitizeIdentifier(String(node.properties.name ?? "var"));
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
33
scripts/nodes/library/gpio/gpioRead.js
Normal file
33
scripts/nodes/library/gpio/gpioRead.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a value from a GPIO pin using gpio().
|
||||||
|
*/
|
||||||
|
export const gpioReadNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "gpio_read",
|
||||||
|
title: "GPIO Read",
|
||||||
|
category: "IO",
|
||||||
|
description: "Fetch the current value of a GPIO pin.",
|
||||||
|
searchTags: ["gpio", "hardware", "input"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
name: "Pin",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const pin = resolveValueInput("index", "0");
|
||||||
|
return `gpio(${pin})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
43
scripts/nodes/library/gpio/gpioWrite.js
Normal file
43
scripts/nodes/library/gpio/gpioWrite.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a value to a GPIO pin using gpio().
|
||||||
|
*/
|
||||||
|
export const gpioWriteNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "gpio_write",
|
||||||
|
title: "GPIO Write",
|
||||||
|
category: "IO",
|
||||||
|
description: "Set the value for a GPIO pin.",
|
||||||
|
searchTags: ["gpio", "hardware", "output"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
name: "Pin",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "value",
|
||||||
|
name: "Value",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const pin = resolveValueInput("index", "0");
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
const line = `${indent(indentLevel)}gpio(${pin}, ${value})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
16
scripts/nodes/library/gpio/index.js
Normal file
16
scripts/nodes/library/gpio/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { gpioReadNode } from "./gpioRead.js";
|
||||||
|
import { gpioWriteNode } from "./gpioWrite.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node modules covering GPIO helpers.
|
||||||
|
*/
|
||||||
|
export const gpioNodes = [gpioReadNode, gpioWriteNode];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of GPIO read/write helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const gpioFunctionChecklist = [
|
||||||
|
{ function: "GPIO (read)", nodeId: "gpio_read", implemented: true },
|
||||||
|
{ function: "GPIO (write)", nodeId: "gpio_write", implemented: true },
|
||||||
|
];
|
||||||
37
scripts/nodes/library/graphics/camera.js
Normal file
37
scripts/nodes/library/graphics/camera.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a camera offset to subsequent draw calls.
|
||||||
|
*/
|
||||||
|
export const cameraNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_camera",
|
||||||
|
title: "Add Camera Offset",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Shift the draw origin by the specified camera offset.",
|
||||||
|
searchTags: ["camera", "offset", "scroll", "view"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "x", name: "X", direction: "input", kind: "number" },
|
||||||
|
{ id: "y", name: "Y", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const x = resolveValueInput("x", OMIT);
|
||||||
|
const y = resolveValueInput("y", OMIT);
|
||||||
|
let call = "camera()";
|
||||||
|
if (x !== OMIT) {
|
||||||
|
call = `camera(${x}, ${y === OMIT ? "0" : y})`;
|
||||||
|
}
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
57
scripts/nodes/library/graphics/circ.js
Normal file
57
scripts/nodes/library/graphics/circ.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an outlined circle.
|
||||||
|
*/
|
||||||
|
export const circNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_circ",
|
||||||
|
title: "Draw Circle",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render an outlined circle at the specified position.",
|
||||||
|
searchTags: ["circle", "outline", "shape", "draw"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "radius",
|
||||||
|
name: "Radius",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 4,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const radius = resolveValueInput("radius", "4");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x, y, radius];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}circ(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
57
scripts/nodes/library/graphics/circfill.js
Normal file
57
scripts/nodes/library/graphics/circfill.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a filled circle.
|
||||||
|
*/
|
||||||
|
export const circfillNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_circfill",
|
||||||
|
title: "Draw Filled Circle",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render a filled circle at the specified position.",
|
||||||
|
searchTags: ["circle", "filled", "shape", "draw"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "radius",
|
||||||
|
name: "Radius",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 4,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const radius = resolveValueInput("radius", "4");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x, y, radius];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}circfill(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
63
scripts/nodes/library/graphics/clip.js
Normal file
63
scripts/nodes/library/graphics/clip.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the active clipping rectangle for subsequent draw calls.
|
||||||
|
*/
|
||||||
|
export const clipNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_clip",
|
||||||
|
title: "Set Clipping Rectangle",
|
||||||
|
category: "Graphics",
|
||||||
|
description:
|
||||||
|
"Set or reset the clipping rectangle that constrains all draw operations.",
|
||||||
|
searchTags: ["clip", "clipping", "graphics", "viewport"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "x", name: "X", direction: "input", kind: "number" },
|
||||||
|
{ id: "y", name: "Y", direction: "input", kind: "number" },
|
||||||
|
{ id: "w", name: "Width", direction: "input", kind: "number" },
|
||||||
|
{ id: "h", name: "Height", direction: "input", kind: "number" },
|
||||||
|
{
|
||||||
|
id: "clip_previous",
|
||||||
|
name: "Clip Previous",
|
||||||
|
direction: "input",
|
||||||
|
kind: "boolean",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const x = resolveValueInput("x", OMIT);
|
||||||
|
const y = resolveValueInput("y", OMIT);
|
||||||
|
const w = resolveValueInput("w", OMIT);
|
||||||
|
const h = resolveValueInput("h", OMIT);
|
||||||
|
const clipPrevious = resolveValueInput("clip_previous", OMIT);
|
||||||
|
|
||||||
|
let call;
|
||||||
|
if (x === OMIT && y === OMIT && w === OMIT && h === OMIT) {
|
||||||
|
call = "clip()";
|
||||||
|
} else {
|
||||||
|
const args = [
|
||||||
|
x === OMIT ? "0" : x,
|
||||||
|
y === OMIT ? "0" : y,
|
||||||
|
w === OMIT ? "128" : w,
|
||||||
|
h === OMIT ? "128" : h,
|
||||||
|
];
|
||||||
|
if (clipPrevious !== OMIT) {
|
||||||
|
args.push(clipPrevious);
|
||||||
|
}
|
||||||
|
call = `clip(${args.join(", ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
32
scripts/nodes/library/graphics/cls.js
Normal file
32
scripts/nodes/library/graphics/cls.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the screen and resets clipping.
|
||||||
|
*/
|
||||||
|
export const clsNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_cls",
|
||||||
|
title: "Clear Screen",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Clear the display, optionally filling with a specific color.",
|
||||||
|
searchTags: ["cls", "clear", "screen", "background"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const color = resolveValueInput("color", OMIT);
|
||||||
|
const call = color === OMIT ? "cls()" : `cls(${color})`;
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
32
scripts/nodes/library/graphics/color.js
Normal file
32
scripts/nodes/library/graphics/color.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current draw color.
|
||||||
|
*/
|
||||||
|
export const colorNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_color",
|
||||||
|
title: "Set Active Color",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Set the active draw color used by subsequent graphics calls.",
|
||||||
|
searchTags: ["color", "color", "ink", "draw"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const color = resolveValueInput("color", OMIT);
|
||||||
|
const call = color === OMIT ? "color()" : `color(${color})`;
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
46
scripts/nodes/library/graphics/cursor.js
Normal file
46
scripts/nodes/library/graphics/cursor.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions the print cursor and optionally sets the draw color.
|
||||||
|
*/
|
||||||
|
export const cursorNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_cursor",
|
||||||
|
title: "Set Cursor",
|
||||||
|
category: "Graphics",
|
||||||
|
description:
|
||||||
|
"Move the print cursor and optionally change the active color.",
|
||||||
|
searchTags: ["cursor", "print", "text", "position"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "x", name: "X", direction: "input", kind: "number" },
|
||||||
|
{ id: "y", name: "Y", direction: "input", kind: "number" },
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const x = resolveValueInput("x", OMIT);
|
||||||
|
const y = resolveValueInput("y", OMIT);
|
||||||
|
const color = resolveValueInput("color", OMIT);
|
||||||
|
|
||||||
|
let call = "cursor()";
|
||||||
|
if (x !== OMIT) {
|
||||||
|
const args = [x, y === OMIT ? "0" : y];
|
||||||
|
if (color !== OMIT) {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
call = `cursor(${args.join(", ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
39
scripts/nodes/library/graphics/fget.js
Normal file
39
scripts/nodes/library/graphics/fget.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads sprite flag data.
|
||||||
|
*/
|
||||||
|
export const fgetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_fget",
|
||||||
|
title: "Get Sprite Flag",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Retrieve the bitfield or specific flag value for a sprite.",
|
||||||
|
searchTags: ["fget", "flag", "sprite", "meta"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "sprite",
|
||||||
|
name: "Sprite",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "flag", name: "Flag", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const sprite = resolveValueInput("sprite", "0");
|
||||||
|
const flag = resolveValueInput("flag", OMIT);
|
||||||
|
if (flag === OMIT) {
|
||||||
|
return `fget(${sprite})`;
|
||||||
|
}
|
||||||
|
return `fget(${sprite}, ${flag})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
31
scripts/nodes/library/graphics/fillp.js
Normal file
31
scripts/nodes/library/graphics/fillp.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the active fill pattern.
|
||||||
|
*/
|
||||||
|
export const fillpNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_fillp",
|
||||||
|
title: "Set Fill Pattern",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Set the 4x4 fill pattern bitfield used by many draw calls.",
|
||||||
|
searchTags: ["fillp", "pattern", "fill", "hatch"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "pattern", name: "Pattern", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const pattern = resolveValueInput("pattern", "__pg_omit__");
|
||||||
|
const call = pattern === "__pg_omit__" ? "fillp()" : `fillp(${pattern})`;
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
52
scripts/nodes/library/graphics/fset.js
Normal file
52
scripts/nodes/library/graphics/fset.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates sprite flag values.
|
||||||
|
*/
|
||||||
|
export const fsetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_fset",
|
||||||
|
title: "Set Sprite Flag",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Set a sprite's flag bitfield or a specific flag value.",
|
||||||
|
searchTags: ["fset", "flag", "sprite", "meta"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "sprite",
|
||||||
|
name: "Sprite",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "flag", name: "Flag", direction: "input", kind: "number" },
|
||||||
|
{
|
||||||
|
id: "value",
|
||||||
|
name: "Value",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const sprite = resolveValueInput("sprite", "0");
|
||||||
|
const flag = resolveValueInput("flag", OMIT);
|
||||||
|
const value = resolveValueInput("value", "false");
|
||||||
|
const args = [sprite];
|
||||||
|
if (flag === OMIT) {
|
||||||
|
args.push(value);
|
||||||
|
} else {
|
||||||
|
args.push(flag, value);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}fset(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
88
scripts/nodes/library/graphics/index.js
Normal file
88
scripts/nodes/library/graphics/index.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { clipNode } from "./clip.js";
|
||||||
|
import { psetNode } from "./pset.js";
|
||||||
|
import { pgetNode } from "./pget.js";
|
||||||
|
import { sgetNode } from "./sget.js";
|
||||||
|
import { ssetNode } from "./sset.js";
|
||||||
|
import { fgetNode } from "./fget.js";
|
||||||
|
import { fsetNode } from "./fset.js";
|
||||||
|
import { cursorNode } from "./cursor.js";
|
||||||
|
import { colorNode } from "./color.js";
|
||||||
|
import { clsNode } from "./cls.js";
|
||||||
|
import { cameraNode } from "./camera.js";
|
||||||
|
import { circNode } from "./circ.js";
|
||||||
|
import { circfillNode } from "./circfill.js";
|
||||||
|
import { ovalNode } from "./oval.js";
|
||||||
|
import { ovalfillNode } from "./ovalfill.js";
|
||||||
|
import { lineNode } from "./line.js";
|
||||||
|
import { rectNode } from "./rect.js";
|
||||||
|
import { rectfillNode } from "./rectfill.js";
|
||||||
|
import { rrectNode } from "./rrect.js";
|
||||||
|
import { rrectfillNode } from "./rrectfill.js";
|
||||||
|
import { palNode } from "./pal.js";
|
||||||
|
import { paltNode } from "./palt.js";
|
||||||
|
import { sprNode } from "./spr.js";
|
||||||
|
import { ssprNode } from "./sspr.js";
|
||||||
|
import { fillpNode } from "./fillp.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All graphics related node modules backed by PICO-8 draw helpers.
|
||||||
|
*/
|
||||||
|
export const graphicsNodes = [
|
||||||
|
clipNode,
|
||||||
|
psetNode,
|
||||||
|
pgetNode,
|
||||||
|
sgetNode,
|
||||||
|
ssetNode,
|
||||||
|
fgetNode,
|
||||||
|
fsetNode,
|
||||||
|
cursorNode,
|
||||||
|
colorNode,
|
||||||
|
clsNode,
|
||||||
|
cameraNode,
|
||||||
|
circNode,
|
||||||
|
circfillNode,
|
||||||
|
ovalNode,
|
||||||
|
ovalfillNode,
|
||||||
|
lineNode,
|
||||||
|
rectNode,
|
||||||
|
rectfillNode,
|
||||||
|
rrectNode,
|
||||||
|
rrectfillNode,
|
||||||
|
palNode,
|
||||||
|
paltNode,
|
||||||
|
sprNode,
|
||||||
|
ssprNode,
|
||||||
|
fillpNode,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of documented graphics helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const graphicsFunctionChecklist = [
|
||||||
|
{ function: "CLIP", nodeId: "graphics_clip", implemented: true },
|
||||||
|
{ function: "PSET", nodeId: "graphics_pset", implemented: true },
|
||||||
|
{ function: "PGET", nodeId: "graphics_pget", implemented: true },
|
||||||
|
{ function: "SGET", nodeId: "graphics_sget", implemented: true },
|
||||||
|
{ function: "SSET", nodeId: "graphics_sset", implemented: true },
|
||||||
|
{ function: "FGET", nodeId: "graphics_fget", implemented: true },
|
||||||
|
{ function: "FSET", nodeId: "graphics_fset", implemented: true },
|
||||||
|
{ function: "CURSOR", nodeId: "graphics_cursor", implemented: true },
|
||||||
|
{ function: "COLOR", nodeId: "graphics_color", implemented: true },
|
||||||
|
{ function: "CLS", nodeId: "graphics_cls", implemented: true },
|
||||||
|
{ function: "CAMERA", nodeId: "graphics_camera", implemented: true },
|
||||||
|
{ function: "CIRC", nodeId: "graphics_circ", implemented: true },
|
||||||
|
{ function: "CIRCFILL", nodeId: "graphics_circfill", implemented: true },
|
||||||
|
{ function: "OVAL", nodeId: "graphics_oval", implemented: true },
|
||||||
|
{ function: "OVALFILL", nodeId: "graphics_ovalfill", implemented: true },
|
||||||
|
{ function: "LINE", nodeId: "graphics_line", implemented: true },
|
||||||
|
{ function: "RECT", nodeId: "graphics_rect", implemented: true },
|
||||||
|
{ function: "RECTFILL", nodeId: "graphics_rectfill", implemented: true },
|
||||||
|
{ function: "RRECT", nodeId: "graphics_rrect", implemented: true },
|
||||||
|
{ function: "RRECTFILL", nodeId: "graphics_rrectfill", implemented: true },
|
||||||
|
{ function: "PAL", nodeId: "graphics_pal", implemented: true },
|
||||||
|
{ function: "PALT", nodeId: "graphics_palt", implemented: true },
|
||||||
|
{ function: "SPR", nodeId: "graphics_spr", implemented: true },
|
||||||
|
{ function: "SSPR", nodeId: "graphics_sspr", implemented: true },
|
||||||
|
{ function: "FILLP", nodeId: "graphics_fillp", implemented: true },
|
||||||
|
];
|
||||||
60
scripts/nodes/library/graphics/line.js
Normal file
60
scripts/nodes/library/graphics/line.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a line segment.
|
||||||
|
*/
|
||||||
|
export const lineNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_line",
|
||||||
|
title: "Draw Line",
|
||||||
|
category: "Graphics",
|
||||||
|
description:
|
||||||
|
"Draw a line between two points or update the current pen position.",
|
||||||
|
searchTags: ["line", "draw", "segment", "stroke"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x0",
|
||||||
|
name: "X0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y0",
|
||||||
|
name: "Y0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "x1", name: "X1", direction: "input", kind: "number" },
|
||||||
|
{ id: "y1", name: "Y1", direction: "input", kind: "number" },
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const x0 = resolveValueInput("x0", "0");
|
||||||
|
const y0 = resolveValueInput("y0", "0");
|
||||||
|
const x1 = resolveValueInput("x1", OMIT);
|
||||||
|
const y1 = resolveValueInput("y1", OMIT);
|
||||||
|
const color = resolveValueInput("color", OMIT);
|
||||||
|
|
||||||
|
const args = [x0, y0];
|
||||||
|
if (x1 !== OMIT && y1 !== OMIT) {
|
||||||
|
args.push(x1, y1);
|
||||||
|
if (color !== OMIT) {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = `${indent(indentLevel)}line(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
65
scripts/nodes/library/graphics/oval.js
Normal file
65
scripts/nodes/library/graphics/oval.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an outlined oval.
|
||||||
|
*/
|
||||||
|
export const ovalNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_oval",
|
||||||
|
title: "Draw Oval",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render an ellipse using the provided bounding box.",
|
||||||
|
searchTags: ["oval", "ellipse", "shape", "outline"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x0",
|
||||||
|
name: "X0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y0",
|
||||||
|
name: "Y0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "x1",
|
||||||
|
name: "X1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y1",
|
||||||
|
name: "Y1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x0 = resolveValueInput("x0", "0");
|
||||||
|
const y0 = resolveValueInput("y0", "0");
|
||||||
|
const x1 = resolveValueInput("x1", "8");
|
||||||
|
const y1 = resolveValueInput("y1", "8");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x0, y0, x1, y1];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}oval(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
65
scripts/nodes/library/graphics/ovalfill.js
Normal file
65
scripts/nodes/library/graphics/ovalfill.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a filled oval.
|
||||||
|
*/
|
||||||
|
export const ovalfillNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_ovalfill",
|
||||||
|
title: "Draw Filled Oval",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render a filled ellipse using the provided bounding box.",
|
||||||
|
searchTags: ["oval", "ellipse", "filled", "shape"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x0",
|
||||||
|
name: "X0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y0",
|
||||||
|
name: "Y0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "x1",
|
||||||
|
name: "X1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y1",
|
||||||
|
name: "Y1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x0 = resolveValueInput("x0", "0");
|
||||||
|
const y0 = resolveValueInput("y0", "0");
|
||||||
|
const x1 = resolveValueInput("x1", "8");
|
||||||
|
const y1 = resolveValueInput("y1", "8");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x0, y0, x1, y1];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}ovalfill(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
51
scripts/nodes/library/graphics/pal.js
Normal file
51
scripts/nodes/library/graphics/pal.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remaps palette colors.
|
||||||
|
*/
|
||||||
|
export const palNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_pal",
|
||||||
|
title: "Remap Palette Color",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Swap one palette color for another or reset the palette.",
|
||||||
|
searchTags: ["pal", "palette", "color", "remap"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "c0", name: "Color 0", direction: "input", kind: "number" },
|
||||||
|
{
|
||||||
|
id: "c1",
|
||||||
|
name: "Color 1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "p", name: "Palette", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const c0 = resolveValueInput("c0", OMIT);
|
||||||
|
const c1 = resolveValueInput("c1", "0");
|
||||||
|
const p = resolveValueInput("p", OMIT);
|
||||||
|
|
||||||
|
let call = "pal()";
|
||||||
|
if (c0 !== OMIT) {
|
||||||
|
const args = [c0, c1];
|
||||||
|
if (p !== OMIT) {
|
||||||
|
args.push(p);
|
||||||
|
}
|
||||||
|
call = `pal(${args.join(", ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
47
scripts/nodes/library/graphics/palt.js
Normal file
47
scripts/nodes/library/graphics/palt.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts sprite transparency settings.
|
||||||
|
*/
|
||||||
|
export const paltNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_palt",
|
||||||
|
title: "Set Sprite Transparency",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Set or reset sprite transparency flags.",
|
||||||
|
searchTags: ["palt", "transparency", "palette", "sprite"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
{
|
||||||
|
id: "transparent",
|
||||||
|
name: "Transparent",
|
||||||
|
direction: "input",
|
||||||
|
kind: "boolean",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const color = resolveValueInput("color", OMIT);
|
||||||
|
const transparent = resolveValueInput("transparent", OMIT);
|
||||||
|
|
||||||
|
let call = "palt()";
|
||||||
|
if (color !== OMIT) {
|
||||||
|
if (transparent === OMIT) {
|
||||||
|
call = `palt(${color})`;
|
||||||
|
} else {
|
||||||
|
call = `palt(${color}, ${transparent})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lines = [`${indent(indentLevel)}${call}`];
|
||||||
|
lines.push(...emitNextExec("exec_out"));
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
41
scripts/nodes/library/graphics/pget.js
Normal file
41
scripts/nodes/library/graphics/pget.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the color of a screen pixel.
|
||||||
|
*/
|
||||||
|
export const pgetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_pget",
|
||||||
|
title: "Get Pixel Color",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Fetch the color index at the given screen coordinate.",
|
||||||
|
searchTags: ["pget", "pixel", "read", "sample"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Color", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
return `pget(${x}, ${y})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
50
scripts/nodes/library/graphics/pset.js
Normal file
50
scripts/nodes/library/graphics/pset.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a single pixel using the current or specified color.
|
||||||
|
*/
|
||||||
|
export const psetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_pset",
|
||||||
|
title: "Set Pixel Color",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Set a pixel on screen to a color index.",
|
||||||
|
searchTags: ["pset", "pixel", "draw", "plot"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const color = resolveValueInput("color", OMIT);
|
||||||
|
const args = [x, y];
|
||||||
|
if (color !== OMIT) {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}pset(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
65
scripts/nodes/library/graphics/rect.js
Normal file
65
scripts/nodes/library/graphics/rect.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an outlined rectangle.
|
||||||
|
*/
|
||||||
|
export const rectNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_rect",
|
||||||
|
title: "Draw Rectangle",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render a rectangle outline defined by two corners.",
|
||||||
|
searchTags: ["rect", "rectangle", "outline", "shape"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x0",
|
||||||
|
name: "X0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y0",
|
||||||
|
name: "Y0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "x1",
|
||||||
|
name: "X1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y1",
|
||||||
|
name: "Y1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x0 = resolveValueInput("x0", "0");
|
||||||
|
const y0 = resolveValueInput("y0", "0");
|
||||||
|
const x1 = resolveValueInput("x1", "8");
|
||||||
|
const y1 = resolveValueInput("y1", "8");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x0, y0, x1, y1];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}rect(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
65
scripts/nodes/library/graphics/rectfill.js
Normal file
65
scripts/nodes/library/graphics/rectfill.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a filled rectangle.
|
||||||
|
*/
|
||||||
|
export const rectfillNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_rectfill",
|
||||||
|
title: "Draw Filled Rectangle",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render a filled rectangle defined by two corners.",
|
||||||
|
searchTags: ["rect", "rectangle", "filled", "shape"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x0",
|
||||||
|
name: "X0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y0",
|
||||||
|
name: "Y0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "x1",
|
||||||
|
name: "X1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y1",
|
||||||
|
name: "Y1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x0 = resolveValueInput("x0", "0");
|
||||||
|
const y0 = resolveValueInput("y0", "0");
|
||||||
|
const x1 = resolveValueInput("x1", "8");
|
||||||
|
const y1 = resolveValueInput("y1", "8");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x0, y0, x1, y1];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}rectfill(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
73
scripts/nodes/library/graphics/rrect.js
Normal file
73
scripts/nodes/library/graphics/rrect.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an outlined rounded rectangle.
|
||||||
|
*/
|
||||||
|
export const rrectNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_rrect",
|
||||||
|
title: "Draw Rounded Rectangle",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render a rounded rectangle outline with the supplied radius.",
|
||||||
|
searchTags: ["rounded", "rectangle", "outline", "shape"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "w",
|
||||||
|
name: "Width",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "h",
|
||||||
|
name: "Height",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "r",
|
||||||
|
name: "Radius",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 2,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const w = resolveValueInput("w", "8");
|
||||||
|
const h = resolveValueInput("h", "8");
|
||||||
|
const r = resolveValueInput("r", "2");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x, y, w, h, r];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}rrect(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
73
scripts/nodes/library/graphics/rrectfill.js
Normal file
73
scripts/nodes/library/graphics/rrectfill.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a filled rounded rectangle.
|
||||||
|
*/
|
||||||
|
export const rrectfillNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_rrectfill",
|
||||||
|
title: "Draw Filled Rounded Rectangle",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Render a filled rounded rectangle with the supplied radius.",
|
||||||
|
searchTags: ["rounded", "rectangle", "filled", "shape"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "w",
|
||||||
|
name: "Width",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "h",
|
||||||
|
name: "Height",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "r",
|
||||||
|
name: "Radius",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 2,
|
||||||
|
},
|
||||||
|
{ id: "color", name: "Color", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const w = resolveValueInput("w", "8");
|
||||||
|
const h = resolveValueInput("h", "8");
|
||||||
|
const r = resolveValueInput("r", "2");
|
||||||
|
const color = resolveValueInput("color", "__pg_omit__");
|
||||||
|
const args = [x, y, w, h, r];
|
||||||
|
if (color !== "__pg_omit__") {
|
||||||
|
args.push(color);
|
||||||
|
}
|
||||||
|
const line = `${indent(indentLevel)}rrectfill(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
42
scripts/nodes/library/graphics/sget.js
Normal file
42
scripts/nodes/library/graphics/sget.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a pixel from the sprite sheet.
|
||||||
|
*/
|
||||||
|
export const sgetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_sget",
|
||||||
|
title: "Get Sprite Pixel",
|
||||||
|
category: "Graphics",
|
||||||
|
description:
|
||||||
|
"Retrieve the color index from the sprite sheet at the given coordinate.",
|
||||||
|
searchTags: ["sget", "sprite", "sheet", "read"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Color", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
return `sget(${x}, ${y})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
76
scripts/nodes/library/graphics/spr.js
Normal file
76
scripts/nodes/library/graphics/spr.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws sprites from the sprite sheet.
|
||||||
|
*/
|
||||||
|
export const sprNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_spr",
|
||||||
|
title: "Draw Sprite",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Blit one or more sprites at the given screen position.",
|
||||||
|
searchTags: ["spr", "sprite", "draw", "blit"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "sprite",
|
||||||
|
name: "Sprite",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "w", name: "Width", direction: "input", kind: "number" },
|
||||||
|
{ id: "h", name: "Height", direction: "input", kind: "number" },
|
||||||
|
{ id: "flip_x", name: "Flip X", direction: "input", kind: "boolean" },
|
||||||
|
{ id: "flip_y", name: "Flip Y", direction: "input", kind: "boolean" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const sprite = resolveValueInput("sprite", "0");
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const w = resolveValueInput("w", OMIT);
|
||||||
|
const h = resolveValueInput("h", OMIT);
|
||||||
|
const flipX = resolveValueInput("flip_x", OMIT);
|
||||||
|
const flipY = resolveValueInput("flip_y", OMIT);
|
||||||
|
|
||||||
|
const args = [sprite, x, y];
|
||||||
|
const includeSize =
|
||||||
|
w !== OMIT || h !== OMIT || flipX !== OMIT || flipY !== OMIT;
|
||||||
|
|
||||||
|
if (includeSize) {
|
||||||
|
args.push(w === OMIT ? "1" : w, h === OMIT ? "1" : h);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flipX !== OMIT || flipY !== OMIT) {
|
||||||
|
args.push(
|
||||||
|
flipX === OMIT ? "false" : flipX,
|
||||||
|
flipY === OMIT ? "false" : flipY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = `${indent(indentLevel)}spr(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
51
scripts/nodes/library/graphics/sset.js
Normal file
51
scripts/nodes/library/graphics/sset.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a pixel to the sprite sheet.
|
||||||
|
*/
|
||||||
|
export const ssetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_sset",
|
||||||
|
title: "Set Sprite Pixel",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Assign a color to the sprite sheet at the given coordinate.",
|
||||||
|
searchTags: ["sset", "sprite", "sheet", "write"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "color",
|
||||||
|
name: "Color",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const color = resolveValueInput("color", "0");
|
||||||
|
const line = `${indent(indentLevel)}sset(${x}, ${y}, ${color})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
100
scripts/nodes/library/graphics/sspr.js
Normal file
100
scripts/nodes/library/graphics/sspr.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a rectangular region of the sprite sheet with scaling support.
|
||||||
|
*/
|
||||||
|
export const ssprNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "graphics_sspr",
|
||||||
|
title: "Draw Scaled Sprite",
|
||||||
|
category: "Graphics",
|
||||||
|
description: "Stretch or flip a sprite sheet region to the screen.",
|
||||||
|
searchTags: ["sspr", "sprite", "scale", "blit"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "sx",
|
||||||
|
name: "SX",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sy",
|
||||||
|
name: "SY",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sw",
|
||||||
|
name: "SW",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sh",
|
||||||
|
name: "SH",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dx",
|
||||||
|
name: "DX",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dy",
|
||||||
|
name: "DY",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{ id: "dw", name: "DW", direction: "input", kind: "number" },
|
||||||
|
{ id: "dh", name: "DH", direction: "input", kind: "number" },
|
||||||
|
{ id: "flip_x", name: "Flip X", direction: "input", kind: "boolean" },
|
||||||
|
{ id: "flip_y", name: "Flip Y", direction: "input", kind: "boolean" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const sx = resolveValueInput("sx", "0");
|
||||||
|
const sy = resolveValueInput("sy", "0");
|
||||||
|
const sw = resolveValueInput("sw", "8");
|
||||||
|
const sh = resolveValueInput("sh", "8");
|
||||||
|
const dx = resolveValueInput("dx", "0");
|
||||||
|
const dy = resolveValueInput("dy", "0");
|
||||||
|
const dw = resolveValueInput("dw", OMIT);
|
||||||
|
const dh = resolveValueInput("dh", OMIT);
|
||||||
|
const flipX = resolveValueInput("flip_x", OMIT);
|
||||||
|
const flipY = resolveValueInput("flip_y", OMIT);
|
||||||
|
|
||||||
|
const args = [sx, sy, sw, sh, dx, dy];
|
||||||
|
const includeDestSize =
|
||||||
|
dw !== OMIT || dh !== OMIT || flipX !== OMIT || flipY !== OMIT;
|
||||||
|
|
||||||
|
if (includeDestSize) {
|
||||||
|
args.push(dw === OMIT ? sw : dw, dh === OMIT ? sh : dh);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flipX !== OMIT || flipY !== OMIT) {
|
||||||
|
args.push(
|
||||||
|
flipX === OMIT ? "false" : flipX,
|
||||||
|
flipY === OMIT ? "false" : flipY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = `${indent(indentLevel)}sspr(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
67
scripts/nodes/library/ifNode.js
Normal file
67
scripts/nodes/library/ifNode.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branches execution based on a boolean condition.
|
||||||
|
*/
|
||||||
|
export const ifNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "if",
|
||||||
|
title: "If",
|
||||||
|
category: "Logic",
|
||||||
|
description: "Branch execution based on a boolean condition.",
|
||||||
|
searchTags: ["if", "condition", "branch", "logic"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "condition",
|
||||||
|
name: "Condition",
|
||||||
|
direction: "input",
|
||||||
|
kind: "boolean",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "then", name: "Then", direction: "output", kind: "exec" },
|
||||||
|
{ id: "else", name: "Else", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({
|
||||||
|
indent,
|
||||||
|
indentLevel,
|
||||||
|
resolveValueInput,
|
||||||
|
emitBranch,
|
||||||
|
findExecTargets,
|
||||||
|
path,
|
||||||
|
}) => {
|
||||||
|
const condition = resolveValueInput("condition", "false");
|
||||||
|
const lines = [`${indent(indentLevel)}if ${condition} then`];
|
||||||
|
|
||||||
|
const thenLines = emitBranch("then", {
|
||||||
|
indentLevel: indentLevel + 1,
|
||||||
|
path: new Set(path),
|
||||||
|
});
|
||||||
|
if (!thenLines.length) {
|
||||||
|
lines.push(`${indent(indentLevel + 1)}-- then branch`);
|
||||||
|
} else {
|
||||||
|
lines.push(...thenLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findExecTargets("else").length) {
|
||||||
|
lines.push(`${indent(indentLevel)}else`);
|
||||||
|
const elseLines = emitBranch("else", {
|
||||||
|
indentLevel: indentLevel + 1,
|
||||||
|
path: new Set(path),
|
||||||
|
});
|
||||||
|
if (!elseLines.length) {
|
||||||
|
lines.push(`${indent(indentLevel + 1)}-- else branch`);
|
||||||
|
} else {
|
||||||
|
lines.push(...elseLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`${indent(indentLevel)}end`);
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
87
scripts/nodes/library/index.js
Normal file
87
scripts/nodes/library/index.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { eventInitNode } from "./eventStart.js";
|
||||||
|
import { eventUpdateNode } from "./eventUpdate.js";
|
||||||
|
import { eventDrawNode } from "./eventDraw.js";
|
||||||
|
import { printNode } from "./print.js";
|
||||||
|
import { setVariableNode } from "./setVariable.js";
|
||||||
|
import { getVariableNode } from "./getVariable.js";
|
||||||
|
import { numberLiteralNode } from "./numberLiteral.js";
|
||||||
|
import { stringLiteralNode } from "./stringLiteral.js";
|
||||||
|
import { booleanLiteralNode } from "./booleanLiteral.js";
|
||||||
|
import { addNumberNode } from "./addNumber.js";
|
||||||
|
import { multiplyNumberNode } from "./multiplyNumber.js";
|
||||||
|
import { compareNode } from "./compare.js";
|
||||||
|
import { sequenceNode } from "./sequence.js";
|
||||||
|
import { ifNode } from "./ifNode.js";
|
||||||
|
import { forLoopNode } from "./forLoop.js";
|
||||||
|
import { customEventNode } from "./customEvent.js";
|
||||||
|
import { callCustomEventNode } from "./callCustomEvent.js";
|
||||||
|
import { graphicsNodes, graphicsFunctionChecklist } from "./graphics/index.js";
|
||||||
|
import { systemNodes, systemFunctionChecklist } from "./system/index.js";
|
||||||
|
import { tableNodes, tableFunctionChecklist } from "./table/index.js";
|
||||||
|
import { inputNodes, inputFunctionChecklist } from "./input/index.js";
|
||||||
|
import { audioNodes, audioFunctionChecklist } from "./audio/index.js";
|
||||||
|
import { mapNodes, mapFunctionChecklist } from "./map/index.js";
|
||||||
|
import { memoryNodes, memoryFunctionChecklist } from "./memory/index.js";
|
||||||
|
import { mathNodes, mathFunctionChecklist } from "./math/index.js";
|
||||||
|
import { menuNodes, menuFunctionChecklist } from "./menu/index.js";
|
||||||
|
import { stringNodes, stringFunctionChecklist } from "./strings/index.js";
|
||||||
|
import { dataNodes, dataFunctionChecklist } from "./data/index.js";
|
||||||
|
import { gpioNodes, gpioFunctionChecklist } from "./gpio/index.js";
|
||||||
|
import { serialNodes, serialFunctionChecklist } from "./serial/index.js";
|
||||||
|
import { devkitNodes, devkitFunctionChecklist } from "./devkit/index.js";
|
||||||
|
import { luaNodes, luaFunctionChecklist } from "./lua/index.js";
|
||||||
|
import { localVariableNodes } from "./localVariables.js";
|
||||||
|
|
||||||
|
export const nodeModules = [
|
||||||
|
eventInitNode,
|
||||||
|
eventUpdateNode,
|
||||||
|
eventDrawNode,
|
||||||
|
printNode,
|
||||||
|
setVariableNode,
|
||||||
|
getVariableNode,
|
||||||
|
numberLiteralNode,
|
||||||
|
stringLiteralNode,
|
||||||
|
booleanLiteralNode,
|
||||||
|
addNumberNode,
|
||||||
|
multiplyNumberNode,
|
||||||
|
compareNode,
|
||||||
|
sequenceNode,
|
||||||
|
ifNode,
|
||||||
|
forLoopNode,
|
||||||
|
customEventNode,
|
||||||
|
callCustomEventNode,
|
||||||
|
...localVariableNodes,
|
||||||
|
...graphicsNodes,
|
||||||
|
...systemNodes,
|
||||||
|
...tableNodes,
|
||||||
|
...inputNodes,
|
||||||
|
...audioNodes,
|
||||||
|
...mapNodes,
|
||||||
|
...memoryNodes,
|
||||||
|
...mathNodes,
|
||||||
|
...menuNodes,
|
||||||
|
...stringNodes,
|
||||||
|
...dataNodes,
|
||||||
|
...gpioNodes,
|
||||||
|
...serialNodes,
|
||||||
|
...devkitNodes,
|
||||||
|
...luaNodes,
|
||||||
|
];
|
||||||
|
|
||||||
|
export {
|
||||||
|
systemFunctionChecklist,
|
||||||
|
graphicsFunctionChecklist,
|
||||||
|
tableFunctionChecklist,
|
||||||
|
inputFunctionChecklist,
|
||||||
|
audioFunctionChecklist,
|
||||||
|
mapFunctionChecklist,
|
||||||
|
memoryFunctionChecklist,
|
||||||
|
mathFunctionChecklist,
|
||||||
|
menuFunctionChecklist,
|
||||||
|
stringFunctionChecklist,
|
||||||
|
dataFunctionChecklist,
|
||||||
|
gpioFunctionChecklist,
|
||||||
|
serialFunctionChecklist,
|
||||||
|
devkitFunctionChecklist,
|
||||||
|
luaFunctionChecklist,
|
||||||
|
};
|
||||||
55
scripts/nodes/library/input/btn.js
Normal file
55
scripts/nodes/library/input/btn.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the state of a PICO-8 button using BTN.
|
||||||
|
*/
|
||||||
|
export const btnNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "input_btn",
|
||||||
|
title: "Button State",
|
||||||
|
category: "Input",
|
||||||
|
description: "Read the current pressed state for a controller button.",
|
||||||
|
searchTags: ["btn", "input", "button", "press"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "button",
|
||||||
|
name: "Button",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Button index or glyph code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "player",
|
||||||
|
name: "Player",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Optional player index",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Pressed", direction: "output", kind: "boolean" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const button = resolveValueInput("button", OMIT);
|
||||||
|
const player = resolveValueInput("player", OMIT);
|
||||||
|
|
||||||
|
if (button === OMIT && player === OMIT) {
|
||||||
|
return "btn()";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button === OMIT) {
|
||||||
|
return `btn(nil, ${player})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player === OMIT) {
|
||||||
|
return `btn(${button})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `btn(${button}, ${player})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
56
scripts/nodes/library/input/btnp.js
Normal file
56
scripts/nodes/library/input/btnp.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads button press transitions using BTNP.
|
||||||
|
*/
|
||||||
|
export const btnpNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "input_btnp",
|
||||||
|
title: "Button Press",
|
||||||
|
category: "Input",
|
||||||
|
description:
|
||||||
|
"Detect when a button is initially pressed or repeats using BTNP.",
|
||||||
|
searchTags: ["btnp", "input", "button", "press", "repeat"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "button",
|
||||||
|
name: "Button",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Button index or glyph code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "player",
|
||||||
|
name: "Player",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Optional player index",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Pressed", direction: "output", kind: "boolean" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const button = resolveValueInput("button", OMIT);
|
||||||
|
const player = resolveValueInput("player", OMIT);
|
||||||
|
|
||||||
|
if (button === OMIT && player === OMIT) {
|
||||||
|
return "btnp()";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button === OMIT) {
|
||||||
|
return `btnp(nil, ${player})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player === OMIT) {
|
||||||
|
return `btnp(${button})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `btnp(${button}, ${player})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
175
scripts/nodes/library/input/controllerButtons.js
Normal file
175
scripts/nodes/library/input/controllerButtons.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
const DEFAULT_PLAYER_VALUE = "0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the stored player property to a valid controller index.
|
||||||
|
*
|
||||||
|
* @param {unknown} value Raw inspector value.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const resolvePlayerIndex = (value) => {
|
||||||
|
const numeric = Number.parseInt(
|
||||||
|
typeof value === "string" ? value : String(value ?? DEFAULT_PLAYER_VALUE),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
if (!Number.isFinite(numeric) || numeric < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (numeric > 7) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
return numeric;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BUTTON_OPTIONS = [
|
||||||
|
{ value: "0", label: "Left" },
|
||||||
|
{ value: "1", label: "Right" },
|
||||||
|
{ value: "2", label: "Up" },
|
||||||
|
{ value: "3", label: "Down" },
|
||||||
|
{ value: "4", label: "O" },
|
||||||
|
{ value: "5", label: "X" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const BUTTON_VALUE_SET = new Set(BUTTON_OPTIONS.map((option) => option.value));
|
||||||
|
|
||||||
|
const DEFAULT_BUTTON_VALUE = BUTTON_OPTIONS[0]?.value ?? "0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes button property values to a valid BTN index represented as a string.
|
||||||
|
*
|
||||||
|
* @param {unknown} value Raw inspector value.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const resolveButtonValue = (value) => {
|
||||||
|
const numeric = Number.parseInt(
|
||||||
|
typeof value === "string" ? value : String(value ?? DEFAULT_BUTTON_VALUE),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const candidate = Number.isFinite(numeric) ? String(numeric) : DEFAULT_BUTTON_VALUE;
|
||||||
|
if (BUTTON_VALUE_SET.has(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return DEFAULT_BUTTON_VALUE;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAYER_OPTIONS = [
|
||||||
|
{ value: "0", label: "Player 0 (Cursors / Z X)" },
|
||||||
|
{ value: "1", label: "Player 1 (SFED / Tab Q W A)" },
|
||||||
|
{ value: "2", label: "Player 2" },
|
||||||
|
{ value: "3", label: "Player 3" },
|
||||||
|
{ value: "4", label: "Player 4" },
|
||||||
|
{ value: "5", label: "Player 5" },
|
||||||
|
{ value: "6", label: "Player 6" },
|
||||||
|
{ value: "7", label: "Player 7" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether the configured button is currently pressed.
|
||||||
|
*/
|
||||||
|
export const controllerButtonsNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "input_button_constants",
|
||||||
|
title: "Named Button",
|
||||||
|
category: "Input",
|
||||||
|
description:
|
||||||
|
"Read a specific controller button with a dropdown selector and optional player override.",
|
||||||
|
searchTags: [
|
||||||
|
"button",
|
||||||
|
"controller",
|
||||||
|
"dpad",
|
||||||
|
"input",
|
||||||
|
"player",
|
||||||
|
"named",
|
||||||
|
"press",
|
||||||
|
],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "button",
|
||||||
|
name: "Button",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Optional button index override",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "player",
|
||||||
|
name: "Player",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Optional player index override",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
id: "pressed",
|
||||||
|
name: "Pressed",
|
||||||
|
direction: "output",
|
||||||
|
kind: "boolean",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
key: "button",
|
||||||
|
label: "Button",
|
||||||
|
type: "enum",
|
||||||
|
defaultValue: DEFAULT_BUTTON_VALUE,
|
||||||
|
options: BUTTON_OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "player",
|
||||||
|
label: "Default Player",
|
||||||
|
type: "enum",
|
||||||
|
defaultValue: DEFAULT_PLAYER_VALUE,
|
||||||
|
options: PLAYER_OPTIONS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initializeProperties: (properties) => {
|
||||||
|
properties.button = resolveButtonValue(properties.button);
|
||||||
|
const normalizedPlayer = resolvePlayerIndex(properties.player);
|
||||||
|
properties.player = String(normalizedPlayer);
|
||||||
|
const buttonInlineKey = "pin:button";
|
||||||
|
const playerInlineKey = "pin:player";
|
||||||
|
const numericButton = Number.parseInt(properties.button, 10);
|
||||||
|
const numericPlayer = normalizedPlayer;
|
||||||
|
properties[buttonInlineKey] = Number.isFinite(numericButton)
|
||||||
|
? numericButton
|
||||||
|
: 0;
|
||||||
|
properties[playerInlineKey] = Number.isFinite(numericPlayer)
|
||||||
|
? numericPlayer
|
||||||
|
: 0;
|
||||||
|
},
|
||||||
|
onPropertiesChanged: (properties) => {
|
||||||
|
properties.button = resolveButtonValue(properties.button);
|
||||||
|
const normalizedPlayer = resolvePlayerIndex(properties.player);
|
||||||
|
properties.player = String(normalizedPlayer);
|
||||||
|
const buttonInlineKey = "pin:button";
|
||||||
|
const playerInlineKey = "pin:player";
|
||||||
|
const numericButton = Number.parseInt(properties.button, 10);
|
||||||
|
const numericPlayer = normalizedPlayer;
|
||||||
|
properties[buttonInlineKey] = Number.isFinite(numericButton)
|
||||||
|
? numericButton
|
||||||
|
: 0;
|
||||||
|
properties[playerInlineKey] = Number.isFinite(numericPlayer)
|
||||||
|
? numericPlayer
|
||||||
|
: 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ node, resolveValueInput, formatLiteral }) => {
|
||||||
|
const buttonValue = resolveButtonValue(node.properties.button);
|
||||||
|
const parsedButton = Number.parseInt(buttonValue, 10);
|
||||||
|
const buttonIndex = Number.isFinite(parsedButton) ? parsedButton : 0;
|
||||||
|
const fallbackButton = formatLiteral("number", buttonIndex);
|
||||||
|
const buttonInput = resolveValueInput("button", fallbackButton);
|
||||||
|
|
||||||
|
const fallbackPlayer = formatLiteral(
|
||||||
|
"number",
|
||||||
|
resolvePlayerIndex(node.properties.player)
|
||||||
|
);
|
||||||
|
const playerInput = resolveValueInput("player", fallbackPlayer);
|
||||||
|
|
||||||
|
return `btn(${buttonInput}, ${playerInput})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
17
scripts/nodes/library/input/index.js
Normal file
17
scripts/nodes/library/input/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { btnNode } from "./btn.js";
|
||||||
|
import { btnpNode } from "./btnp.js";
|
||||||
|
import { controllerButtonsNode } from "./controllerButtons.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All input-related node modules backed by PICO-8 helpers.
|
||||||
|
*/
|
||||||
|
export const inputNodes = [controllerButtonsNode, btnNode, btnpNode];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of documented input helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const inputFunctionChecklist = [
|
||||||
|
{ function: "BTN", nodeId: "input_btn", implemented: true },
|
||||||
|
{ function: "BTNP", nodeId: "input_btnp", implemented: true },
|
||||||
|
];
|
||||||
0
scripts/nodes/library/localVariable.js
Normal file
0
scripts/nodes/library/localVariable.js
Normal file
169
scripts/nodes/library/localVariables.js
Normal file
169
scripts/nodes/library/localVariables.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
const LOCAL_TYPES = [
|
||||||
|
{ value: "number", label: "Number" },
|
||||||
|
{ value: "string", label: "String" },
|
||||||
|
{ value: "boolean", label: "Boolean" },
|
||||||
|
{ value: "table", label: "Table" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_LOCAL_TYPE = LOCAL_TYPES[0]?.value ?? "number";
|
||||||
|
|
||||||
|
const sanitizeType = (raw) => {
|
||||||
|
const candidate = typeof raw === "string" ? raw.toLowerCase() : DEFAULT_LOCAL_TYPE;
|
||||||
|
return LOCAL_TYPES.some((entry) => entry.value === candidate)
|
||||||
|
? candidate
|
||||||
|
: DEFAULT_LOCAL_TYPE;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureName = (raw, fallback = "localVar") => {
|
||||||
|
const value = typeof raw === "string" ? raw.trim() : "";
|
||||||
|
return value.length ? value : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveValuePropertyKey = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
return "valueString";
|
||||||
|
case "boolean":
|
||||||
|
return "valueBoolean";
|
||||||
|
case "table":
|
||||||
|
return "valueTable";
|
||||||
|
case "number":
|
||||||
|
default:
|
||||||
|
return "valueNumber";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValueForType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
return "";
|
||||||
|
case "boolean":
|
||||||
|
return false;
|
||||||
|
case "table":
|
||||||
|
return "{}";
|
||||||
|
case "number":
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeLocalProperties = (properties, { includeValue }) => {
|
||||||
|
const type = sanitizeType(properties.variableType);
|
||||||
|
properties.variableType = type;
|
||||||
|
properties.name = ensureName(properties.name);
|
||||||
|
|
||||||
|
if (typeof properties.valueNumber !== "number") {
|
||||||
|
properties.valueNumber = Number.isFinite(Number(properties.valueNumber))
|
||||||
|
? Number(properties.valueNumber)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
if (typeof properties.valueString !== "string") {
|
||||||
|
properties.valueString = String(properties.valueString ?? "");
|
||||||
|
}
|
||||||
|
if (typeof properties.valueBoolean !== "boolean") {
|
||||||
|
properties.valueBoolean = Boolean(properties.valueBoolean);
|
||||||
|
}
|
||||||
|
if (typeof properties.valueTable !== "string") {
|
||||||
|
properties.valueTable = String(properties.valueTable ?? "{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeValue) {
|
||||||
|
const key = resolveValuePropertyKey(type);
|
||||||
|
const value = properties[key] ?? defaultValueForType(type);
|
||||||
|
properties[key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeLocalProperties = (properties, { includeValue }) => {
|
||||||
|
const type = sanitizeType(properties.variableType);
|
||||||
|
properties.variableType = type;
|
||||||
|
properties.name = ensureName(properties.name);
|
||||||
|
if (includeValue) {
|
||||||
|
const key = resolveValuePropertyKey(type);
|
||||||
|
const value = properties[key] ?? defaultValueForType(type);
|
||||||
|
properties[key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLocalVariableNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "set_local_var",
|
||||||
|
title: "Set Local",
|
||||||
|
category: "Logic",
|
||||||
|
description: "Declare or assign a local variable with an inline literal.",
|
||||||
|
searchTags: ["local", "set", "assign", "variable"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "value",
|
||||||
|
name: "Value",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
description: "Optional override for the inline literal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
initializeProperties: (properties) => {
|
||||||
|
initializeLocalProperties(properties, { includeValue: true });
|
||||||
|
},
|
||||||
|
onPropertiesChanged: (properties) => {
|
||||||
|
normalizeLocalProperties(properties, { includeValue: true });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({
|
||||||
|
node,
|
||||||
|
indent,
|
||||||
|
indentLevel,
|
||||||
|
resolveValueInput,
|
||||||
|
sanitizeIdentifier,
|
||||||
|
emitNextExec,
|
||||||
|
formatLiteral,
|
||||||
|
}) => {
|
||||||
|
const type = sanitizeType(node.properties.variableType);
|
||||||
|
const name = sanitizeIdentifier(ensureName(node.properties.name));
|
||||||
|
const valueKey = resolveValuePropertyKey(type);
|
||||||
|
const inlineValue = node.properties[valueKey] ?? defaultValueForType(type);
|
||||||
|
const fallbackLiteral =
|
||||||
|
type === "table"
|
||||||
|
? String(inlineValue ?? "{}") || "{}"
|
||||||
|
: formatLiteral(type, inlineValue);
|
||||||
|
const valueExpression = resolveValueInput("value", fallbackLiteral);
|
||||||
|
const statement = `${indent(indentLevel)}local ${name} = ${valueExpression}`;
|
||||||
|
return [statement, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getLocalVariableNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "get_local_var",
|
||||||
|
title: "Get Local",
|
||||||
|
category: "Logic",
|
||||||
|
description: "Access a local variable declared earlier in the flow.",
|
||||||
|
searchTags: ["local", "get", "variable", "read"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "any" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
initializeProperties: (properties) => {
|
||||||
|
initializeLocalProperties(properties, { includeValue: false });
|
||||||
|
},
|
||||||
|
onPropertiesChanged: (properties) => {
|
||||||
|
normalizeLocalProperties(properties, { includeValue: false });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ node, sanitizeIdentifier }) => {
|
||||||
|
return sanitizeIdentifier(ensureName(node.properties.name));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const localVariableNodes = [setLocalVariableNode, getLocalVariableNode];
|
||||||
38
scripts/nodes/library/lua/coroutineCreate.js
Normal file
38
scripts/nodes/library/lua/coroutineCreate.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a coroutine from a function using coroutine.create().
|
||||||
|
*/
|
||||||
|
export const luaCoroutineCreateNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_coroutine_create",
|
||||||
|
title: "Coroutine Create",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Wrap a function in a coroutine via coroutine.create().",
|
||||||
|
searchTags: ["coroutine", "create", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "fn",
|
||||||
|
name: "Function",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
description: "Function to run inside the coroutine",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
id: "coroutine",
|
||||||
|
name: "Coroutine",
|
||||||
|
direction: "output",
|
||||||
|
kind: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const fn = resolveValueInput("fn", "function() end");
|
||||||
|
return `coroutine.create(${fn})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
91
scripts/nodes/library/lua/coroutineResume.js
Normal file
91
scripts/nodes/library/lua/coroutineResume.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumes a coroutine and exposes the success flag and first result.
|
||||||
|
*/
|
||||||
|
export const luaCoroutineResumeNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_coroutine_resume",
|
||||||
|
title: "Coroutine Resume",
|
||||||
|
category: "Lua",
|
||||||
|
description:
|
||||||
|
"Resume a coroutine and capture the success flag alongside the first returned value.",
|
||||||
|
searchTags: ["coroutine", "resume", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "coroutine",
|
||||||
|
name: "Coroutine",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "arg1",
|
||||||
|
name: "Arg 1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
description: "Optional first value passed to coroutine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "arg2",
|
||||||
|
name: "Arg 2",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
description: "Optional second value passed to coroutine",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "success",
|
||||||
|
name: "Success",
|
||||||
|
direction: "output",
|
||||||
|
kind: "boolean",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "result",
|
||||||
|
name: "Result",
|
||||||
|
direction: "output",
|
||||||
|
kind: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({
|
||||||
|
node,
|
||||||
|
indent,
|
||||||
|
indentLevel,
|
||||||
|
resolveValueInput,
|
||||||
|
sanitizeIdentifier,
|
||||||
|
emitNextExec,
|
||||||
|
}) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const target = resolveValueInput("coroutine", "nil");
|
||||||
|
const arg1 = resolveValueInput("arg1", OMIT);
|
||||||
|
const arg2 = resolveValueInput("arg2", OMIT);
|
||||||
|
const args = [target];
|
||||||
|
if (arg1 !== OMIT) {
|
||||||
|
args.push(arg1);
|
||||||
|
}
|
||||||
|
if (arg2 !== OMIT) {
|
||||||
|
args.push(arg2);
|
||||||
|
}
|
||||||
|
const successName = sanitizeIdentifier(`__pg_${node.id}_co_success`);
|
||||||
|
const resultName = sanitizeIdentifier(`__pg_${node.id}_co_result`);
|
||||||
|
const line = `${indent(indentLevel)}local ${successName}, ${resultName} = coroutine.resume(${args.join(", ")})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
evaluateValue: ({ node, pinId, sanitizeIdentifier }) => {
|
||||||
|
const successName = sanitizeIdentifier(`__pg_${node.id}_co_success`);
|
||||||
|
const resultName = sanitizeIdentifier(`__pg_${node.id}_co_result`);
|
||||||
|
if (pinId === "success") {
|
||||||
|
return successName;
|
||||||
|
}
|
||||||
|
if (pinId === "result") {
|
||||||
|
return resultName;
|
||||||
|
}
|
||||||
|
return "nil";
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
27
scripts/nodes/library/lua/coroutineRunning.js
Normal file
27
scripts/nodes/library/lua/coroutineRunning.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently running coroutine via coroutine.running().
|
||||||
|
*/
|
||||||
|
export const luaCoroutineRunningNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_coroutine_running",
|
||||||
|
title: "Coroutine Running",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Access the currently running coroutine reference.",
|
||||||
|
searchTags: ["coroutine", "running", "lua"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
id: "coroutine",
|
||||||
|
name: "Coroutine",
|
||||||
|
direction: "output",
|
||||||
|
kind: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: () => "coroutine.running()",
|
||||||
|
}
|
||||||
|
);
|
||||||
32
scripts/nodes/library/lua/coroutineStatus.js
Normal file
32
scripts/nodes/library/lua/coroutineStatus.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports a coroutine's status via coroutine.status().
|
||||||
|
*/
|
||||||
|
export const luaCoroutineStatusNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_coroutine_status",
|
||||||
|
title: "Coroutine Status",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Retrieve a coroutine's status string using coroutine.status().",
|
||||||
|
searchTags: ["coroutine", "status", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "coroutine",
|
||||||
|
name: "Coroutine",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "status", name: "Status", direction: "output", kind: "string" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const target = resolveValueInput("coroutine", "nil");
|
||||||
|
return `coroutine.status(${target})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
53
scripts/nodes/library/lua/coroutineYield.js
Normal file
53
scripts/nodes/library/lua/coroutineYield.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yields from within a coroutine using coroutine.yield().
|
||||||
|
*/
|
||||||
|
export const luaCoroutineYieldNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_coroutine_yield",
|
||||||
|
title: "Coroutine Yield",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Yield execution from within a coroutine and optionally return values.",
|
||||||
|
searchTags: ["coroutine", "yield", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "value1",
|
||||||
|
name: "Value 1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
description: "Optional first value yielded to the caller",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "value2",
|
||||||
|
name: "Value 2",
|
||||||
|
direction: "input",
|
||||||
|
kind: "any",
|
||||||
|
description: "Optional second value yielded to the caller",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const value1 = resolveValueInput("value1", OMIT);
|
||||||
|
const value2 = resolveValueInput("value2", OMIT);
|
||||||
|
const args = [];
|
||||||
|
if (value1 !== OMIT) {
|
||||||
|
args.push(value1);
|
||||||
|
}
|
||||||
|
if (value2 !== OMIT) {
|
||||||
|
args.push(value2);
|
||||||
|
}
|
||||||
|
const call = args.length
|
||||||
|
? `${indent(indentLevel)}coroutine.yield(${args.join(", ")})`
|
||||||
|
: `${indent(indentLevel)}coroutine.yield()`;
|
||||||
|
return [call, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
27
scripts/nodes/library/lua/getMetatable.js
Normal file
27
scripts/nodes/library/lua/getMetatable.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the metatable associated with a table via getmetatable().
|
||||||
|
*/
|
||||||
|
export const luaGetMetatableNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_getmetatable",
|
||||||
|
title: "Get Metatable",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Fetch a table's metatable using getmetatable().",
|
||||||
|
searchTags: ["getmetatable", "metatable", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "table", name: "Table", direction: "input", kind: "table" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "metatable", name: "Metatable", direction: "output", kind: "table" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const tableValue = resolveValueInput("table", "{}");
|
||||||
|
return `getmetatable(${tableValue})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
63
scripts/nodes/library/lua/index.js
Normal file
63
scripts/nodes/library/lua/index.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { luaSetMetatableNode } from "./setMetatable.js";
|
||||||
|
import { luaGetMetatableNode } from "./getMetatable.js";
|
||||||
|
import { luaRawGetNode } from "./rawGet.js";
|
||||||
|
import { luaRawSetNode } from "./rawSet.js";
|
||||||
|
import { luaRawEqualNode } from "./rawEqual.js";
|
||||||
|
import { luaCoroutineCreateNode } from "./coroutineCreate.js";
|
||||||
|
import { luaCoroutineResumeNode } from "./coroutineResume.js";
|
||||||
|
import { luaCoroutineYieldNode } from "./coroutineYield.js";
|
||||||
|
import { luaCoroutineStatusNode } from "./coroutineStatus.js";
|
||||||
|
import { luaCoroutineRunningNode } from "./coroutineRunning.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node modules covering Lua metatable and coroutine helpers.
|
||||||
|
*/
|
||||||
|
export const luaNodes = [
|
||||||
|
luaSetMetatableNode,
|
||||||
|
luaGetMetatableNode,
|
||||||
|
luaRawGetNode,
|
||||||
|
luaRawSetNode,
|
||||||
|
luaRawEqualNode,
|
||||||
|
luaCoroutineCreateNode,
|
||||||
|
luaCoroutineResumeNode,
|
||||||
|
luaCoroutineYieldNode,
|
||||||
|
luaCoroutineStatusNode,
|
||||||
|
luaCoroutineRunningNode,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of Lua metatable and coroutine helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const luaFunctionChecklist = [
|
||||||
|
{ function: "SETMETATABLE", nodeId: "lua_setmetatable", implemented: true },
|
||||||
|
{ function: "GETMETATABLE", nodeId: "lua_getmetatable", implemented: true },
|
||||||
|
{ function: "RAWGET", nodeId: "lua_rawget", implemented: true },
|
||||||
|
{ function: "RAWSET", nodeId: "lua_rawset", implemented: true },
|
||||||
|
{ function: "RAWEQUAL", nodeId: "lua_rawequal", implemented: true },
|
||||||
|
{
|
||||||
|
function: "COROUTINE.CREATE",
|
||||||
|
nodeId: "lua_coroutine_create",
|
||||||
|
implemented: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: "COROUTINE.RESUME",
|
||||||
|
nodeId: "lua_coroutine_resume",
|
||||||
|
implemented: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: "COROUTINE.YIELD",
|
||||||
|
nodeId: "lua_coroutine_yield",
|
||||||
|
implemented: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: "COROUTINE.STATUS",
|
||||||
|
nodeId: "lua_coroutine_status",
|
||||||
|
implemented: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: "COROUTINE.RUNNING",
|
||||||
|
nodeId: "lua_coroutine_running",
|
||||||
|
implemented: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
29
scripts/nodes/library/lua/rawEqual.js
Normal file
29
scripts/nodes/library/lua/rawEqual.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two values using rawequal().
|
||||||
|
*/
|
||||||
|
export const luaRawEqualNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_rawequal",
|
||||||
|
title: "Raw Equal",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Compare values without metamethods using rawequal().",
|
||||||
|
searchTags: ["rawequal", "compare", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "a", name: "A", direction: "input", kind: "any" },
|
||||||
|
{ id: "b", name: "B", direction: "input", kind: "any" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "equal", name: "Equal", direction: "output", kind: "boolean" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "nil");
|
||||||
|
const b = resolveValueInput("b", "nil");
|
||||||
|
return `rawequal(${a}, ${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
29
scripts/nodes/library/lua/rawGet.js
Normal file
29
scripts/nodes/library/lua/rawGet.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a raw table entry using rawget().
|
||||||
|
*/
|
||||||
|
export const luaRawGetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_rawget",
|
||||||
|
title: "Raw Get",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Access a table entry without metamethods using rawget().",
|
||||||
|
searchTags: ["rawget", "table", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "table", name: "Table", direction: "input", kind: "table" },
|
||||||
|
{ id: "key", name: "Key", direction: "input", kind: "any" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "any" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const tableValue = resolveValueInput("table", "{}");
|
||||||
|
const key = resolveValueInput("key", "nil");
|
||||||
|
return `rawget(${tableValue}, ${key})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
33
scripts/nodes/library/lua/rawSet.js
Normal file
33
scripts/nodes/library/lua/rawSet.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a raw table entry using rawset().
|
||||||
|
*/
|
||||||
|
export const luaRawSetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_rawset",
|
||||||
|
title: "Raw Set",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Assign a table entry without metamethods using rawset().",
|
||||||
|
searchTags: ["rawset", "table", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{ id: "table", name: "Table", direction: "input", kind: "table" },
|
||||||
|
{ id: "key", name: "Key", direction: "input", kind: "any" },
|
||||||
|
{ id: "value", name: "Value", direction: "input", kind: "any" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const tableValue = resolveValueInput("table", "{}");
|
||||||
|
const key = resolveValueInput("key", "nil");
|
||||||
|
const value = resolveValueInput("value", "nil");
|
||||||
|
const line = `${indent(indentLevel)}rawset(${tableValue}, ${key}, ${value})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
35
scripts/nodes/library/lua/setMetatable.js
Normal file
35
scripts/nodes/library/lua/setMetatable.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a metatable to a table via setmetatable().
|
||||||
|
*/
|
||||||
|
export const luaSetMetatableNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "lua_setmetatable",
|
||||||
|
title: "Set Metatable",
|
||||||
|
category: "Lua",
|
||||||
|
description: "Assign a metatable to a table using setmetatable().",
|
||||||
|
searchTags: ["setmetatable", "metatable", "lua"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "table", name: "Table", direction: "input", kind: "table" },
|
||||||
|
{
|
||||||
|
id: "metatable",
|
||||||
|
name: "Metatable",
|
||||||
|
direction: "input",
|
||||||
|
kind: "table",
|
||||||
|
defaultValue: "nil",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "table_out", name: "Table", direction: "output", kind: "table" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const tableValue = resolveValueInput("table", "{}");
|
||||||
|
const metatable = resolveValueInput("metatable", "nil");
|
||||||
|
return `setmetatable(${tableValue}, ${metatable})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
20
scripts/nodes/library/map/index.js
Normal file
20
scripts/nodes/library/map/index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { mapMgetNode } from "./mget.js";
|
||||||
|
import { mapMsetNode } from "./mset.js";
|
||||||
|
import { mapDrawNode } from "./map.js";
|
||||||
|
import { mapTlineNode } from "./tline.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All map-related node modules backed by PICO-8 helpers.
|
||||||
|
*/
|
||||||
|
export const mapNodes = [mapMgetNode, mapMsetNode, mapDrawNode, mapTlineNode];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of documented map helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const mapFunctionChecklist = [
|
||||||
|
{ function: "MGET", nodeId: "map_mget", implemented: true },
|
||||||
|
{ function: "MSET", nodeId: "map_mset", implemented: true },
|
||||||
|
{ function: "MAP", nodeId: "map_map", implemented: true },
|
||||||
|
{ function: "TLINE", nodeId: "map_tline", implemented: true },
|
||||||
|
];
|
||||||
108
scripts/nodes/library/map/map.js
Normal file
108
scripts/nodes/library/map/map.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws map tiles using PICO-8's map helper.
|
||||||
|
*/
|
||||||
|
export const mapDrawNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "map_map",
|
||||||
|
title: "Draw Map",
|
||||||
|
category: "Map",
|
||||||
|
description:
|
||||||
|
"Render a section of the map to the screen with optional layers.",
|
||||||
|
searchTags: ["map", "draw", "tiles", "layer"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "tile_x",
|
||||||
|
name: "Tile X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Starting tile X coordinate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tile_y",
|
||||||
|
name: "Tile Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Starting tile Y coordinate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "screen_x",
|
||||||
|
name: "Screen X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Screen X position in pixels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "screen_y",
|
||||||
|
name: "Screen Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Screen Y position in pixels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tile_w",
|
||||||
|
name: "Tile Width",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Number of tiles across",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tile_h",
|
||||||
|
name: "Tile Height",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Number of tiles down",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "layers",
|
||||||
|
name: "Layers",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Sprite flag bitmask",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const tileX = resolveValueInput("tile_x", OMIT);
|
||||||
|
const tileY = resolveValueInput("tile_y", OMIT);
|
||||||
|
const screenX = resolveValueInput("screen_x", OMIT);
|
||||||
|
const screenY = resolveValueInput("screen_y", OMIT);
|
||||||
|
const tileW = resolveValueInput("tile_w", OMIT);
|
||||||
|
const tileH = resolveValueInput("tile_h", OMIT);
|
||||||
|
const layers = resolveValueInput("layers", OMIT);
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{ value: tileX, placeholder: "0" },
|
||||||
|
{ value: tileY, placeholder: "0" },
|
||||||
|
{ value: screenX, placeholder: "0" },
|
||||||
|
{ value: screenY, placeholder: "0" },
|
||||||
|
{ value: tileW, placeholder: "nil" },
|
||||||
|
{ value: tileH, placeholder: "nil" },
|
||||||
|
{ value: layers, placeholder: "nil" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastIndex = values.length - 1;
|
||||||
|
while (lastIndex >= 0 && values[lastIndex].value === OMIT) {
|
||||||
|
lastIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [];
|
||||||
|
for (let index = 0; index <= lastIndex; index += 1) {
|
||||||
|
const entry = values[index];
|
||||||
|
args.push(entry.value === OMIT ? entry.placeholder : entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = args.length ? `map(${args.join(", ")})` : "map()";
|
||||||
|
const line = `${indent(indentLevel)}${call}`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
41
scripts/nodes/library/map/mget.js
Normal file
41
scripts/nodes/library/map/mget.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a map tile value using PICO-8's mget helper.
|
||||||
|
*/
|
||||||
|
export const mapMgetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "map_mget",
|
||||||
|
title: "Get Map Tile",
|
||||||
|
category: "Map",
|
||||||
|
description: "Fetch the numeric map value at a tile coordinate.",
|
||||||
|
searchTags: ["mget", "map", "tile", "read"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "Tile X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Tile Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
return `mget(${x}, ${y})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
51
scripts/nodes/library/map/mset.js
Normal file
51
scripts/nodes/library/map/mset.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a map tile value using PICO-8's mset helper.
|
||||||
|
*/
|
||||||
|
export const mapMsetNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "map_mset",
|
||||||
|
title: "Set Map Tile",
|
||||||
|
category: "Map",
|
||||||
|
description: "Store a numeric value at a tile coordinate.",
|
||||||
|
searchTags: ["mset", "map", "tile", "write"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x",
|
||||||
|
name: "Tile X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y",
|
||||||
|
name: "Tile Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "value",
|
||||||
|
name: "Value",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
|
||||||
|
const x = resolveValueInput("x", "0");
|
||||||
|
const y = resolveValueInput("y", "0");
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
const line = `${indent(indentLevel)}mset(${x}, ${y}, ${value})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
157
scripts/nodes/library/map/tline.js
Normal file
157
scripts/nodes/library/map/tline.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws textured lines or configures precision using PICO-8's tline helper.
|
||||||
|
*/
|
||||||
|
export const mapTlineNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "map_tline",
|
||||||
|
title: "Draw Textured Line",
|
||||||
|
category: "Map",
|
||||||
|
description:
|
||||||
|
"Render a textured line from map data or adjust tline precision.",
|
||||||
|
searchTags: ["tline", "map", "texture", "line"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
|
{
|
||||||
|
id: "x0",
|
||||||
|
name: "X0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Line start X",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y0",
|
||||||
|
name: "Y0",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Line start Y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "x1",
|
||||||
|
name: "X1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Line end X",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "y1",
|
||||||
|
name: "Y1",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Line end Y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mx",
|
||||||
|
name: "Map X",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Map sample X (tiles)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "my",
|
||||||
|
name: "Map Y",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Map sample Y (tiles)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mdx",
|
||||||
|
name: "MDX",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Map X delta",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mdy",
|
||||||
|
name: "MDY",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Map Y delta",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "layers",
|
||||||
|
name: "Layers",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Sprite flag bitmask",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "precision",
|
||||||
|
name: "Precision",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
description: "Precision override when setting mode",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
key: "mode",
|
||||||
|
label: "Mode",
|
||||||
|
type: "enum",
|
||||||
|
defaultValue: "draw",
|
||||||
|
options: [
|
||||||
|
{ label: "Draw Line", value: "draw" },
|
||||||
|
{ label: "Set Precision", value: "precision" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({
|
||||||
|
node,
|
||||||
|
indent,
|
||||||
|
indentLevel,
|
||||||
|
resolveValueInput,
|
||||||
|
emitNextExec,
|
||||||
|
}) => {
|
||||||
|
const mode = String(node.properties.mode ?? "draw");
|
||||||
|
if (mode === "precision") {
|
||||||
|
const precision = resolveValueInput("precision", "16");
|
||||||
|
const line = `${indent(indentLevel)}tline(${precision})`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
}
|
||||||
|
|
||||||
|
const OMIT = "__pg_omit__";
|
||||||
|
const x0 = resolveValueInput("x0", OMIT);
|
||||||
|
const y0 = resolveValueInput("y0", OMIT);
|
||||||
|
const x1 = resolveValueInput("x1", OMIT);
|
||||||
|
const y1 = resolveValueInput("y1", OMIT);
|
||||||
|
const mx = resolveValueInput("mx", OMIT);
|
||||||
|
const my = resolveValueInput("my", OMIT);
|
||||||
|
const mdx = resolveValueInput("mdx", OMIT);
|
||||||
|
const mdy = resolveValueInput("mdy", OMIT);
|
||||||
|
const layers = resolveValueInput("layers", OMIT);
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{ value: x0, placeholder: "0" },
|
||||||
|
{ value: y0, placeholder: "0" },
|
||||||
|
{ value: x1, placeholder: "0" },
|
||||||
|
{ value: y1, placeholder: "0" },
|
||||||
|
{ value: mx, placeholder: "0" },
|
||||||
|
{ value: my, placeholder: "0" },
|
||||||
|
{ value: mdx, placeholder: "0.125" },
|
||||||
|
{ value: mdy, placeholder: "0" },
|
||||||
|
{ value: layers, placeholder: "nil" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastIndex = values.length - 1;
|
||||||
|
while (lastIndex >= 0 && values[lastIndex].value === OMIT) {
|
||||||
|
lastIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [];
|
||||||
|
for (let index = 0; index <= lastIndex; index += 1) {
|
||||||
|
const entry = values[index];
|
||||||
|
args.push(entry.value === OMIT ? entry.placeholder : entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = args.length ? `tline(${args.join(", ")})` : "tline()";
|
||||||
|
const line = `${indent(indentLevel)}${call}`;
|
||||||
|
return [line, ...emitNextExec("exec_out")];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
25
scripts/nodes/library/math/abs.js
Normal file
25
scripts/nodes/library/math/abs.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the absolute value using abs.
|
||||||
|
*/
|
||||||
|
export const mathAbsNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_abs",
|
||||||
|
title: "Abs",
|
||||||
|
category: "Math",
|
||||||
|
description: "Return the absolute value of a number.",
|
||||||
|
searchTags: ["abs", "absolute", "math"],
|
||||||
|
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
return `abs(${value})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
30
scripts/nodes/library/math/atan2.js
Normal file
30
scripts/nodes/library/math/atan2.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an angle from delta components using atan2.
|
||||||
|
*/
|
||||||
|
export const mathAtan2Node = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_atan2",
|
||||||
|
title: "Atan2",
|
||||||
|
category: "Math",
|
||||||
|
description:
|
||||||
|
"Return the screenspace angle for a delta X and Y pair.",
|
||||||
|
searchTags: ["atan2", "angle", "math", "trig"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "dx", name: "DX", direction: "input", kind: "number" },
|
||||||
|
{ id: "dy", name: "DY", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const dx = resolveValueInput("dx", "0");
|
||||||
|
const dy = resolveValueInput("dy", "0");
|
||||||
|
return `atan2(${dx}, ${dy})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
29
scripts/nodes/library/math/band.js
Normal file
29
scripts/nodes/library/math/band.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs bitwise and using band.
|
||||||
|
*/
|
||||||
|
export const mathBandNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_band",
|
||||||
|
title: "Bitwise AND",
|
||||||
|
category: "Math",
|
||||||
|
description: "Return bits set in both operands using BAND().",
|
||||||
|
searchTags: ["band", "bitwise", "and", "math"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "a", name: "A", direction: "input", kind: "number" },
|
||||||
|
{ id: "b", name: "B", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "0");
|
||||||
|
return `band(${a}, ${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
25
scripts/nodes/library/math/bnot.js
Normal file
25
scripts/nodes/library/math/bnot.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs bitwise not using bnot.
|
||||||
|
*/
|
||||||
|
export const mathBnotNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_bnot",
|
||||||
|
title: "Bitwise NOT",
|
||||||
|
category: "Math",
|
||||||
|
description: "Invert every bit in a value using BNOT().",
|
||||||
|
searchTags: ["bnot", "bitwise", "not", "math"],
|
||||||
|
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
return `bnot(${value})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
29
scripts/nodes/library/math/bor.js
Normal file
29
scripts/nodes/library/math/bor.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs bitwise or using bor.
|
||||||
|
*/
|
||||||
|
export const mathBorNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_bor",
|
||||||
|
title: "Bitwise OR",
|
||||||
|
category: "Math",
|
||||||
|
description: "Return bits set in either operand using BOR().",
|
||||||
|
searchTags: ["bor", "bitwise", "or", "math"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "a", name: "A", direction: "input", kind: "number" },
|
||||||
|
{ id: "b", name: "B", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "0");
|
||||||
|
return `bor(${a}, ${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
29
scripts/nodes/library/math/bxor.js
Normal file
29
scripts/nodes/library/math/bxor.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs bitwise exclusive-or using bxor.
|
||||||
|
*/
|
||||||
|
export const mathBxorNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_bxor",
|
||||||
|
title: "Bitwise XOR",
|
||||||
|
category: "Math",
|
||||||
|
description: "Return bits set in exactly one operand using BXOR().",
|
||||||
|
searchTags: ["bxor", "bitwise", "xor", "math"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "a", name: "A", direction: "input", kind: "number" },
|
||||||
|
{ id: "b", name: "B", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "0");
|
||||||
|
return `bxor(${a}, ${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
25
scripts/nodes/library/math/ceil.js
Normal file
25
scripts/nodes/library/math/ceil.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ceils a numeric value using ceil.
|
||||||
|
*/
|
||||||
|
export const mathCeilNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_ceil",
|
||||||
|
title: "Ceil",
|
||||||
|
category: "Math",
|
||||||
|
description: "Round a value up to the nearest integer.",
|
||||||
|
searchTags: ["ceil", "ceiling", "math", "round"],
|
||||||
|
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
return `ceil(${value})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
26
scripts/nodes/library/math/cos.js
Normal file
26
scripts/nodes/library/math/cos.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the cosine of a value using cos.
|
||||||
|
*/
|
||||||
|
export const mathCosNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_cos",
|
||||||
|
title: "Cos",
|
||||||
|
category: "Math",
|
||||||
|
description:
|
||||||
|
"Return the cosine of an angle where 1.0 represents a full turn.",
|
||||||
|
searchTags: ["cos", "cosine", "math", "trig"],
|
||||||
|
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
return `cos(${value})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
29
scripts/nodes/library/math/div.js
Normal file
29
scripts/nodes/library/math/div.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs integer division using the floor operation.
|
||||||
|
*/
|
||||||
|
export const mathDivNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_div",
|
||||||
|
title: "Integer Divide",
|
||||||
|
category: "Math",
|
||||||
|
description: "Return the floored quotient of two numbers (A \\ B).",
|
||||||
|
searchTags: ["div", "integer", "divide", "math"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "a", name: "A", direction: "input", kind: "number" },
|
||||||
|
{ id: "b", name: "B", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "1");
|
||||||
|
return `(${a}) \\ (${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
25
scripts/nodes/library/math/flr.js
Normal file
25
scripts/nodes/library/math/flr.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floors a numeric value using flr.
|
||||||
|
*/
|
||||||
|
export const mathFlrNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_flr",
|
||||||
|
title: "Floor",
|
||||||
|
category: "Math",
|
||||||
|
description: "Round a value down to the nearest integer.",
|
||||||
|
searchTags: ["flr", "floor", "math", "round"],
|
||||||
|
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
return `flr(${value})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
79
scripts/nodes/library/math/index.js
Normal file
79
scripts/nodes/library/math/index.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { mathMaxNode } from "./max.js";
|
||||||
|
import { mathMinNode } from "./min.js";
|
||||||
|
import { mathMidNode } from "./mid.js";
|
||||||
|
import { mathFlrNode } from "./flr.js";
|
||||||
|
import { mathCeilNode } from "./ceil.js";
|
||||||
|
import { mathCosNode } from "./cos.js";
|
||||||
|
import { mathSinNode } from "./sin.js";
|
||||||
|
import { mathAtan2Node } from "./atan2.js";
|
||||||
|
import { mathSqrtNode } from "./sqrt.js";
|
||||||
|
import { mathAbsNode } from "./abs.js";
|
||||||
|
import { mathRndNode } from "./rnd.js";
|
||||||
|
import { mathSrandNode } from "./srand.js";
|
||||||
|
import { mathBandNode } from "./band.js";
|
||||||
|
import { mathBorNode } from "./bor.js";
|
||||||
|
import { mathBxorNode } from "./bxor.js";
|
||||||
|
import { mathBnotNode } from "./bnot.js";
|
||||||
|
import { mathShlNode } from "./shl.js";
|
||||||
|
import { mathShrNode } from "./shr.js";
|
||||||
|
import { mathLshrNode } from "./lshr.js";
|
||||||
|
import { mathRotlNode } from "./rotl.js";
|
||||||
|
import { mathRotrNode } from "./rotr.js";
|
||||||
|
import { mathDivNode } from "./div.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All math-related node modules backed by PICO-8 helpers.
|
||||||
|
*/
|
||||||
|
export const mathNodes = [
|
||||||
|
mathMaxNode,
|
||||||
|
mathMinNode,
|
||||||
|
mathMidNode,
|
||||||
|
mathFlrNode,
|
||||||
|
mathCeilNode,
|
||||||
|
mathCosNode,
|
||||||
|
mathSinNode,
|
||||||
|
mathAtan2Node,
|
||||||
|
mathSqrtNode,
|
||||||
|
mathAbsNode,
|
||||||
|
mathRndNode,
|
||||||
|
mathSrandNode,
|
||||||
|
mathBandNode,
|
||||||
|
mathBorNode,
|
||||||
|
mathBxorNode,
|
||||||
|
mathBnotNode,
|
||||||
|
mathShlNode,
|
||||||
|
mathShrNode,
|
||||||
|
mathLshrNode,
|
||||||
|
mathRotlNode,
|
||||||
|
mathRotrNode,
|
||||||
|
mathDivNode,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist tracking coverage of documented math helpers.
|
||||||
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
|
*/
|
||||||
|
export const mathFunctionChecklist = [
|
||||||
|
{ function: "MAX", nodeId: "math_max", implemented: true },
|
||||||
|
{ function: "MIN", nodeId: "math_min", implemented: true },
|
||||||
|
{ function: "MID", nodeId: "math_mid", implemented: true },
|
||||||
|
{ function: "FLR", nodeId: "math_flr", implemented: true },
|
||||||
|
{ function: "CEIL", nodeId: "math_ceil", implemented: true },
|
||||||
|
{ function: "COS", nodeId: "math_cos", implemented: true },
|
||||||
|
{ function: "SIN", nodeId: "math_sin", implemented: true },
|
||||||
|
{ function: "ATAN2", nodeId: "math_atan2", implemented: true },
|
||||||
|
{ function: "SQRT", nodeId: "math_sqrt", implemented: true },
|
||||||
|
{ function: "ABS", nodeId: "math_abs", implemented: true },
|
||||||
|
{ function: "RND", nodeId: "math_rnd", implemented: true },
|
||||||
|
{ function: "SRAND", nodeId: "math_srand", implemented: true },
|
||||||
|
{ function: "BAND", nodeId: "math_band", implemented: true },
|
||||||
|
{ function: "BOR", nodeId: "math_bor", implemented: true },
|
||||||
|
{ function: "BXOR", nodeId: "math_bxor", implemented: true },
|
||||||
|
{ function: "BNOT", nodeId: "math_bnot", implemented: true },
|
||||||
|
{ function: "SHL", nodeId: "math_shl", implemented: true },
|
||||||
|
{ function: "SHR", nodeId: "math_shr", implemented: true },
|
||||||
|
{ function: "LSHR", nodeId: "math_lshr", implemented: true },
|
||||||
|
{ function: "ROTL", nodeId: "math_rotl", implemented: true },
|
||||||
|
{ function: "ROTR", nodeId: "math_rotr", implemented: true },
|
||||||
|
{ function: "\\", nodeId: "math_div", implemented: true },
|
||||||
|
];
|
||||||
29
scripts/nodes/library/math/lshr.js
Normal file
29
scripts/nodes/library/math/lshr.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a logical right bit shift using lshr.
|
||||||
|
*/
|
||||||
|
export const mathLshrNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_lshr",
|
||||||
|
title: "Logical Shift Right",
|
||||||
|
category: "Math",
|
||||||
|
description: "Shift bits right with zeros entering from the left.",
|
||||||
|
searchTags: ["lshr", "shift", "bitwise", "math"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "value", name: "Value", direction: "input", kind: "number" },
|
||||||
|
{ id: "amount", name: "Amount", direction: "input", kind: "number" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "result", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const value = resolveValueInput("value", "0");
|
||||||
|
const amount = resolveValueInput("amount", "0");
|
||||||
|
return `lshr(${value}, ${amount})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user