mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-23 18:28:49 +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