mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-24 02:36:04 +10:00
Add initial Pico-8 cartridges for testing
- Created test_cart.p8 with basic drawing functionality and a print statement. - Created test_unix.p8 with a simple initialization function.
This commit is contained in:
166
electron-main.js
166
electron-main.js
@@ -15,9 +15,10 @@ try {
|
|||||||
// Ignore errors in production (when electron-reloader isn't installed)
|
// Ignore errors in production (when electron-reloader isn't installed)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, Menu } = require("electron");
|
const { app, BrowserWindow, ipcMain, Menu, shell, dialog } = require("electron");
|
||||||
const fs = require("node:fs/promises");
|
const fs = require("node:fs/promises");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
const { spawn } = require("node:child_process");
|
||||||
|
|
||||||
const OUTPUT_FILENAME = "compiled.lua";
|
const OUTPUT_FILENAME = "compiled.lua";
|
||||||
|
|
||||||
@@ -40,6 +41,16 @@ function getOutputPath() {
|
|||||||
return path.join(getOutputDirectory(), OUTPUT_FILENAME);
|
return path.join(getOutputDirectory(), OUTPUT_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the absolute directory where tileset images are stored.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getTilesetsDirectory() {
|
||||||
|
const applicationData = app.getPath("appData");
|
||||||
|
return path.join(applicationData, "pico-8", "carts", "picograph", "tilesets");
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import("electron").BrowserWindow | null} */
|
/** @type {import("electron").BrowserWindow | null} */
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
|
||||||
@@ -54,6 +65,7 @@ function createMainWindow() {
|
|||||||
height: 900,
|
height: 900,
|
||||||
minWidth: 960,
|
minWidth: 960,
|
||||||
minHeight: 640,
|
minHeight: 640,
|
||||||
|
frame: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
@@ -78,6 +90,13 @@ function createMainWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emitMaximizedState = () => {
|
||||||
|
window.webContents.send("window:maximized-changed", window.isMaximized());
|
||||||
|
};
|
||||||
|
|
||||||
|
window.on("maximize", emitMaximizedState);
|
||||||
|
window.on("unmaximize", emitMaximizedState);
|
||||||
|
|
||||||
window.on("closed", () => {
|
window.on("closed", () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
@@ -135,6 +154,151 @@ function registerIpcHandlers() {
|
|||||||
|
|
||||||
return { filePath: cartPath };
|
return { filePath: cartPath };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window:minimize", () => {
|
||||||
|
mainWindow?.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window:toggle-maximize", () => {
|
||||||
|
if (!mainWindow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainWindow.isMaximized();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window:close", () => {
|
||||||
|
mainWindow?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window:is-maximized", () => {
|
||||||
|
return mainWindow?.isMaximized() ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tileset:save", async (_event, fileBuffer, suggestedName) => {
|
||||||
|
if (!(fileBuffer instanceof Uint8Array)) {
|
||||||
|
throw new TypeError("File buffer must be a Uint8Array");
|
||||||
|
}
|
||||||
|
if (typeof suggestedName !== "string") {
|
||||||
|
throw new TypeError("Suggested name must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tilesetsDir = getTilesetsDirectory();
|
||||||
|
await fs.mkdir(tilesetsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Ensure unique filename
|
||||||
|
let fileName = suggestedName.endsWith(".png") ? suggestedName : `${suggestedName}.png`;
|
||||||
|
let filePath = path.join(tilesetsDir, fileName);
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
while (await fs.access(filePath).then(() => true).catch(() => false)) {
|
||||||
|
const nameWithoutExt = suggestedName.replace(/\.png$/i, "");
|
||||||
|
fileName = `${nameWithoutExt}_${counter}.png`;
|
||||||
|
filePath = path.join(tilesetsDir, fileName);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, Buffer.from(fileBuffer));
|
||||||
|
|
||||||
|
return { filePath, fileName };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tileset:show-in-folder", async (_event, filePath) => {
|
||||||
|
if (typeof filePath !== "string") {
|
||||||
|
throw new TypeError("File path must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.showItemInFolder(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tileset:reload", async (_event, filePath) => {
|
||||||
|
if (typeof filePath !== "string") {
|
||||||
|
throw new TypeError("File path must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await fs.readFile(filePath);
|
||||||
|
return { buffer: Array.from(buffer) };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("cart:get-stats", async (_event, cartContent) => {
|
||||||
|
if (typeof cartContent !== "string") {
|
||||||
|
throw new TypeError("Cart content must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write cart to a temporary file
|
||||||
|
const tempDir = app.getPath("temp");
|
||||||
|
const tempCartPath = path.join(tempDir, `stats_${Date.now()}.p8`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(tempCartPath, cartContent, "utf8");
|
||||||
|
|
||||||
|
// Run p8tool stats command
|
||||||
|
const picotoolPath = path.join(__dirname, "picotool");
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const p8tool = spawn("p8tool", ["stats", tempCartPath], {
|
||||||
|
cwd: __dirname,
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
p8tool.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
p8tool.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
p8tool.on("close", async (code) => {
|
||||||
|
// Clean up temp file
|
||||||
|
try {
|
||||||
|
await fs.unlink(tempCartPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn("Failed to delete temp cart:", cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`p8tool stats exited with code ${code}: ${stderr}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the output to extract token count
|
||||||
|
// Expected format: "version: 0 lines: 48 chars: 419 tokens: 134"
|
||||||
|
const tokenMatch = stdout.match(/tokens:\s*(\d+)/);
|
||||||
|
const linesMatch = stdout.match(/lines:\s*(\d+)/);
|
||||||
|
const charsMatch = stdout.match(/chars:\s*(\d+)/);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
tokens: tokenMatch ? parseInt(tokenMatch[1], 10) : 0,
|
||||||
|
lines: linesMatch ? parseInt(linesMatch[1], 10) : 0,
|
||||||
|
chars: charsMatch ? parseInt(charsMatch[1], 10) : 0,
|
||||||
|
rawOutput: stdout
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
p8tool.on("error", (error) => {
|
||||||
|
reject(new Error(`Failed to run p8tool: ${error.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Clean up temp file if it exists
|
||||||
|
try {
|
||||||
|
await fs.unlink(tempCartPath);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
120
index.html
120
index.html
@@ -3,18 +3,23 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:;" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; font-src 'self' data:;" />
|
||||||
<title>picoGraph</title>
|
<title>picoGraph</title>
|
||||||
<link rel="stylesheet" href="styles/main.css" />
|
<link rel="stylesheet" href="styles/main.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-primary-actions">
|
<div class="header-actions header-actions--left" aria-label="Workspace tools">
|
||||||
<button id="compileButton" class="header-primary-button" type="button">Compile</button>
|
<button
|
||||||
<button id="previewButton" class="header-primary-button" type="button" title="Preview cart">Preview</button>
|
id="tileManagerButton"
|
||||||
</div>
|
class="header-icon-button"
|
||||||
<div class="header-title">picoGraph</div>
|
type="button"
|
||||||
<div class="header-actions">
|
aria-label="Tile manager"
|
||||||
|
title="Tile manager"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--tiles" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<span class="header-icon-spacer" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
id="projectSettingsButton"
|
id="projectSettingsButton"
|
||||||
class="header-icon-button"
|
class="header-icon-button"
|
||||||
@@ -24,6 +29,25 @@
|
|||||||
>
|
>
|
||||||
<span class="header-icon header-icon--settings" aria-hidden="true"></span>
|
<span class="header-icon header-icon--settings" aria-hidden="true"></span>
|
||||||
</button>
|
</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>
|
||||||
|
<span class="header-icon-spacer" aria-hidden="true"></span>
|
||||||
|
<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
|
<button
|
||||||
id="paletteToggleButton"
|
id="paletteToggleButton"
|
||||||
class="header-icon-button"
|
class="header-icon-button"
|
||||||
@@ -34,24 +58,58 @@
|
|||||||
>
|
>
|
||||||
<span class="header-icon header-icon--palette" aria-hidden="true"></span>
|
<span class="header-icon header-icon--palette" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-titlebar">
|
||||||
|
<div class="header-title">picoGraph</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-primary-actions">
|
||||||
<button
|
<button
|
||||||
id="frameSelectionButton"
|
id="saveButton"
|
||||||
class="header-icon-button"
|
class="header-icon-button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Frame selection"
|
aria-label="Save workspace"
|
||||||
title="Frame selection"
|
title="Save workspace"
|
||||||
>
|
>
|
||||||
<span class="header-icon header-icon--frame" aria-hidden="true"></span>
|
<span class="header-icon header-icon--save" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="exportLuaButton"
|
id="previewButton"
|
||||||
class="header-icon-button"
|
class="header-icon-button header-icon-button--accent"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Export Lua"
|
aria-label="Preview cart"
|
||||||
title="Export Lua"
|
title="Preview cart"
|
||||||
>
|
>
|
||||||
<span class="header-icon header-icon--export" aria-hidden="true"></span>
|
<span class="header-icon header-icon--play" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<div id="windowControls" class="window-controls" aria-label="Window controls">
|
||||||
|
<button
|
||||||
|
id="minimizeWindowButton"
|
||||||
|
class="window-control-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Minimize window"
|
||||||
|
title="Minimize"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--minimize" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="maximizeWindowButton"
|
||||||
|
class="window-control-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Maximize window"
|
||||||
|
title="Maximize"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--maximize" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="closeWindowButton"
|
||||||
|
class="window-control-button window-control-button--close"
|
||||||
|
type="button"
|
||||||
|
aria-label="Close window"
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<span class="header-icon header-icon--close-window" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main id="appBody" class="app-body">
|
<main id="appBody" class="app-body">
|
||||||
@@ -129,6 +187,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="tileModal"
|
||||||
|
class="modal"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="tileModalTitle"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<div class="modal__backdrop" data-modal-close></div>
|
||||||
|
<div class="modal__panel modal__panel--tile" role="document">
|
||||||
|
<header class="modal__header">
|
||||||
|
<h2 id="tileModalTitle">Tile Manager</h2>
|
||||||
|
<div class="modal__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal__icon-button close-modal"
|
||||||
|
aria-label="Close tile manager"
|
||||||
|
title="Close"
|
||||||
|
data-modal-close
|
||||||
|
>
|
||||||
|
<span class="modal-icon modal-icon--close" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal__body">
|
||||||
|
<div class="tile-browser-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template id="nodeTemplate">
|
<template id="nodeTemplate">
|
||||||
<article class="blueprint-node" draggable="false">
|
<article class="blueprint-node" draggable="false">
|
||||||
<header class="node-header">
|
<header class="node-header">
|
||||||
|
|||||||
116
preload.js
116
preload.js
@@ -59,6 +59,122 @@ const api = {
|
|||||||
|
|
||||||
return ipcRenderer.invoke("player:writeCart", cartContent);
|
return ipcRenderer.invoke("player:writeCart", cartContent);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimizes the current window.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
minimizeWindow() {
|
||||||
|
return ipcRenderer.invoke("window:minimize");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the current window maximize state.
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
toggleMaximizeWindow() {
|
||||||
|
return ipcRenderer.invoke("window:toggle-maximize");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the current window.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
closeWindow() {
|
||||||
|
return ipcRenderer.invoke("window:close");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the current maximize state for the window.
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
getWindowMaximized() {
|
||||||
|
return ipcRenderer.invoke("window:is-maximized");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to maximize state changes.
|
||||||
|
*
|
||||||
|
* @param {(isMaximized: boolean) => void} callback
|
||||||
|
* @returns {() => void}
|
||||||
|
*/
|
||||||
|
onWindowMaximizedChanged(callback) {
|
||||||
|
if (typeof callback !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = (_event, isMaximized) => {
|
||||||
|
callback(Boolean(isMaximized));
|
||||||
|
};
|
||||||
|
ipcRenderer.on("window:maximized-changed", listener);
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener("window:maximized-changed", listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a tileset image file to the application data directory.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} fileBuffer The PNG file data
|
||||||
|
* @param {string} suggestedName Suggested filename
|
||||||
|
* @returns {Promise<{ filePath: string, fileName: string }>}
|
||||||
|
*/
|
||||||
|
saveTileset(fileBuffer, suggestedName) {
|
||||||
|
if (!(fileBuffer instanceof Uint8Array)) {
|
||||||
|
return Promise.reject(new TypeError("File buffer must be a Uint8Array"));
|
||||||
|
}
|
||||||
|
if (typeof suggestedName !== "string") {
|
||||||
|
return Promise.reject(new TypeError("Suggested name must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipcRenderer.invoke("tileset:save", fileBuffer, suggestedName);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a tileset file in the system file explorer.
|
||||||
|
*
|
||||||
|
* @param {string} filePath Path to the tileset file
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
showTilesetInFolder(filePath) {
|
||||||
|
if (typeof filePath !== "string") {
|
||||||
|
return Promise.reject(new TypeError("File path must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipcRenderer.invoke("tileset:show-in-folder", filePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads a tileset image from disk.
|
||||||
|
*
|
||||||
|
* @param {string} filePath Path to the tileset file
|
||||||
|
* @returns {Promise<{ buffer: number[] }>}
|
||||||
|
*/
|
||||||
|
reloadTileset(filePath) {
|
||||||
|
if (typeof filePath !== "string") {
|
||||||
|
return Promise.reject(new TypeError("File path must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipcRenderer.invoke("tileset:reload", filePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets PICO-8 cart statistics using p8tool stats command.
|
||||||
|
*
|
||||||
|
* @param {string} cartContent Complete .p8 cart file content
|
||||||
|
* @returns {Promise<{ tokens: number, lines: number, chars: number, rawOutput: string }>}
|
||||||
|
*/
|
||||||
|
getCartStats(cartContent) {
|
||||||
|
if (typeof cartContent !== "string") {
|
||||||
|
return Promise.reject(new TypeError("Cart content must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipcRenderer.invoke("cart:get-stats", cartContent);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", api);
|
contextBridge.exposeInMainWorld("electronAPI", api);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
var p8_dropped_cart = null;
|
var p8_dropped_cart = null;
|
||||||
var p8_dropped_cart_name = '';
|
var p8_dropped_cart_name = '';
|
||||||
|
|
||||||
var CART_B64 = 'cGljby04IGNhcnRyaWRnZSAvLyBodHRwOi8vd3d3LnBpY28tOC5jb20KdmVyc2lvbiA0MQpfX2x1YV9fCi0tIEdlbmVyYXRlZCB3aXRoIHBpY29HcmFwaAoKb2Zmc2V0ID0gMApjb2xvciA9IDUKCmZ1bmN0aW9uIF91cGRhdGUoKQogIC0tIHNlcXVlbmNlIGEKICBjbHMoKQogIG9mZnNldCA9IDAKICAtLSBzZXF1ZW5jZSBiCiAgaWYgYnRuKDAsIDApIHRoZW4KICAgIHByaW50KCJsZWZ0IiwgMCwgb2Zmc2V0LCBybmQoKDE2KSArICgxKSkpCiAgICBjdXN0b21fYWRkb2Zmc2V0KCkKICBlbmQKICAtLSBzZXF1ZW5jZSBjCiAgaWYgYnRuKDEsIDApIHRoZW4KICAgIHByaW50KCJyaWdodCIsIDAsIG9mZnNldCwgY29sb3IpCiAgICBsb2NhbCBpdHNhbG9jYWx2YXJpYWJsZSA9IHRydWUKICAgIGlmIGl0c2Fsb2NhbHZhcmlhYmxlIHRoZW4KICAgICAgY3VzdG9tX2FkZG9mZnNldCgpCiAgICBlbmQKICBlbmQKICAtLSBzZXF1ZW5jZSBkCiAgaWYgYnRuKDIsIDApIHRoZW4KICAgIHByaW50KCJ1cCIsIDAsIG9mZnNldCwgY29sb3IpCiAgICBjdXN0b21fYWRkb2Zmc2V0KCkKICBlbmQKICAtLSBzZXF1ZW5jZSBlCiAgaWYgYnRuKDMsIDApIHRoZW4KICAgIHByaW50KCJkb3duIiwgMCwgb2Zmc2V0LCBjb2xvcikKICAgIGN1c3RvbV9hZGRvZmZzZXQoKQogIGVuZAplbmQKCmZ1bmN0aW9uIF9kcmF3KCkKICAtLSBzZXF1ZW5jZSBhCiAgY2xzKCkKICBvZmZzZXQgPSAwCiAgLS0gc2VxdWVuY2UgYgogIGlmIGJ0bigwLCAwKSB0aGVuCiAgICBwcmludCgibGVmdCIsIDAsIG9mZnNldCwgcm5kKCgxNikgKyAoMSkpKQogICAgY3VzdG9tX2FkZG9mZnNldCgpCiAgZW5kCiAgLS0gc2VxdWVuY2UgYwogIGlmIGJ0bigxLCAwKSB0aGVuCiAgICBwcmludCgicmlnaHQiLCAwLCBvZmZzZXQsIGNvbG9yKQogICAgbG9jYWwgaXRzYWxvY2FsdmFyaWFibGUgPSB0cnVlCiAgICBpZiBpdHNhbG9jYWx2YXJpYWJsZSB0aGVuCiAgICAgIGN1c3RvbV9hZGRvZmZzZXQoKQogICAgZW5kCiAgZW5kCiAgLS0gc2VxdWVuY2UgZAogIGlmIGJ0bigyLCAwKSB0aGVuCiAgICBwcmludCgidXAiLCAwLCBvZmZzZXQsIGNvbG9yKQogICAgY3VzdG9tX2FkZG9mZnNldCgpCiAgZW5kCiAgLS0gc2VxdWVuY2UgZQogIGlmIGJ0bigzLCAwKSB0aGVuCiAgICBwcmludCgiZG93biIsIDAsIG9mZnNldCwgY29sb3IpCiAgICBjdXN0b21fYWRkb2Zmc2V0KCkKICBlbmQKZW5kCgpmdW5jdGlvbiBjdXN0b21fYWRkb2Zmc2V0KCkKICBvZmZzZXQgPSAob2Zmc2V0KSArICg3KQplbmQKX19nZnhfXwowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAowMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAo=';
|
var CART_B64 = 'cGljby04IGNhcnRyaWRnZSAvLyBodHRwOi8vd3d3LnBpY28tOC5jb20KdmVyc2lvbiA0MQpfX2x1YV9fCi0tIEdlbmVyYXRlZCB3aXRoIHBpY29HcmFwaAoKZnVuY3Rpb24gX2RyYXcoKQogIGNscygwKQogIGNpcmNmaWxsKDMyLCAzMiwgMjQsIDgpCmVuZApmdW5jdGlvbiBfdXBkYXRlKCkKZW5kCgpfX2dmeF9fCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwCg==';
|
||||||
|
|
||||||
// Silence all XHR — PICO-8 makes BBS metadata calls that crash from file:// origin.
|
// Silence all XHR — PICO-8 makes BBS metadata calls that crash from file:// origin.
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { formatLiteral, defaultLiteralForKind } from './lua/LuaLiteralTypes.js';
|
||||||
|
import { normalizeVariableType, getTypeConverter } from '../types/TypeRegistry.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./NodeGraph.js').NodeGraph} NodeGraph
|
* @typedef {import('./NodeGraph.js').NodeGraph} NodeGraph
|
||||||
*/
|
*/
|
||||||
@@ -278,7 +281,7 @@ export class LuaGenerator {
|
|||||||
findExecTargets: (pinId) => this.#findExecTargets(node.id, pinId),
|
findExecTargets: (pinId) => this.#findExecTargets(node.id, pinId),
|
||||||
sanitizeIdentifier: (value) => this.#sanitizeIdentifier(String(value)),
|
sanitizeIdentifier: (value) => this.#sanitizeIdentifier(String(value)),
|
||||||
sanitizeOperator: (value) => this.#sanitizeOperator(String(value)),
|
sanitizeOperator: (value) => this.#sanitizeOperator(String(value)),
|
||||||
formatLiteral: (kind, value) => this.#formatLiteral(kind, value),
|
formatLiteral,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +341,7 @@ export class LuaGenerator {
|
|||||||
this.#resolveValueInput(node, inputPinId, fallback),
|
this.#resolveValueInput(node, inputPinId, fallback),
|
||||||
sanitizeIdentifier: (value) => this.#sanitizeIdentifier(String(value)),
|
sanitizeIdentifier: (value) => this.#sanitizeIdentifier(String(value)),
|
||||||
sanitizeOperator: (value) => this.#sanitizeOperator(String(value)),
|
sanitizeOperator: (value) => this.#sanitizeOperator(String(value)),
|
||||||
formatLiteral: (kind, value) => this.#formatLiteral(kind, value),
|
formatLiteral,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +519,7 @@ export class LuaGenerator {
|
|||||||
const argEntries = signature.map((entry) => {
|
const argEntries = signature.map((entry) => {
|
||||||
const fallback = entry.optional
|
const fallback = entry.optional
|
||||||
? "nil"
|
? "nil"
|
||||||
: this.#defaultLiteralForKind(entry.kind);
|
: defaultLiteralForKind(entry.kind);
|
||||||
const value = this.#resolveValueInput(
|
const value = this.#resolveValueInput(
|
||||||
node,
|
node,
|
||||||
entry.inputPinId,
|
entry.inputPinId,
|
||||||
@@ -602,6 +605,49 @@ export class LuaGenerator {
|
|||||||
.map((connection) => connection.to);
|
.map((connection) => connection.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies Lua type conversion when source and target types differ.
|
||||||
|
*
|
||||||
|
* @param {string} expression Source expression to convert.
|
||||||
|
* @param {string} fromType Source type identifier.
|
||||||
|
* @param {string} toType Target type identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#applyTypeConversion(expression, fromType, toType) {
|
||||||
|
if (fromType === toType || toType === "any" || fromType === "any") {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
const converter = getTypeConverter(fromType, toType);
|
||||||
|
if (!converter) {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Lua code for type conversions
|
||||||
|
if ((fromType === "number" || fromType === "color") && toType === "boolean") {
|
||||||
|
return `(${expression}) ~= 0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromType === "boolean" && toType === "number") {
|
||||||
|
return `((${expression}) and 1 or 0)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromType === "boolean" && toType === "color") {
|
||||||
|
return `((${expression}) and 7 or 0)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromType === "boolean" && toType === "string") {
|
||||||
|
return `((${expression}) and "true" or "false")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromType === "number" && toType === "color") {
|
||||||
|
return `flr(${expression}) % 16`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No conversion needed for color to number (colors are numbers in PICO-8)
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the expression bound to an input pin.
|
* Resolves the expression bound to an input pin.
|
||||||
*
|
*
|
||||||
@@ -613,7 +659,17 @@ export class LuaGenerator {
|
|||||||
#resolveValueInput(node, pinId, fallback) {
|
#resolveValueInput(node, pinId, fallback) {
|
||||||
const connection = this.#getConnectionTo(node.id, pinId);
|
const connection = this.#getConnectionTo(node.id, pinId);
|
||||||
if (connection) {
|
if (connection) {
|
||||||
return this.#evaluateValue(connection.from.nodeId, connection.from.pinId);
|
const sourceExpression = this.#evaluateValue(connection.from.nodeId, connection.from.pinId);
|
||||||
|
const targetPin = this.#findPin(node, pinId);
|
||||||
|
const targetType = targetPin?.kind ?? "any";
|
||||||
|
|
||||||
|
// Get the actual source type, handling special cases like get_var
|
||||||
|
const sourceNode = this.nodes.get(connection.from.nodeId);
|
||||||
|
const sourceType = sourceNode
|
||||||
|
? this.#getOutputPinType(sourceNode, connection.from.pinId)
|
||||||
|
: connection.kind ?? "any";
|
||||||
|
|
||||||
|
return this.#applyTypeConversion(sourceExpression, sourceType, targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pin = this.#findPin(node, pinId);
|
const pin = this.#findPin(node, pinId);
|
||||||
@@ -621,12 +677,12 @@ export class LuaGenerator {
|
|||||||
if (Object.prototype.hasOwnProperty.call(node.properties, inlineKey)) {
|
if (Object.prototype.hasOwnProperty.call(node.properties, inlineKey)) {
|
||||||
const inlineValue = node.properties[inlineKey];
|
const inlineValue = node.properties[inlineKey];
|
||||||
if (inlineValue !== undefined && inlineValue !== null) {
|
if (inlineValue !== undefined && inlineValue !== null) {
|
||||||
return this.#formatLiteral(pin?.kind ?? "any", inlineValue);
|
return formatLiteral(pin?.kind ?? "any", inlineValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pin && pin.defaultValue !== undefined && pin.defaultValue !== null) {
|
if (pin && pin.defaultValue !== undefined && pin.defaultValue !== null) {
|
||||||
return this.#formatLiteral(pin.kind, pin.defaultValue);
|
return formatLiteral(pin.kind, pin.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -691,24 +747,6 @@ export class LuaGenerator {
|
|||||||
|
|
||||||
if (expression === undefined) {
|
if (expression === undefined) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case "number_literal":
|
|
||||||
expression = this.#formatLiteral(
|
|
||||||
"number",
|
|
||||||
node.properties.value ?? 0
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "string_literal":
|
|
||||||
expression = this.#formatLiteral(
|
|
||||||
"string",
|
|
||||||
node.properties.value ?? ""
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "boolean_literal":
|
|
||||||
expression = this.#formatLiteral(
|
|
||||||
"boolean",
|
|
||||||
node.properties.value ?? "true"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "get_var": {
|
case "get_var": {
|
||||||
const name = this.#resolveWorkspaceVariableName(node);
|
const name = this.#resolveWorkspaceVariableName(node);
|
||||||
expression = name;
|
expression = name;
|
||||||
@@ -721,18 +759,32 @@ export class LuaGenerator {
|
|||||||
expression = parameterName ?? "nil";
|
expression = parameterName ?? "nil";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "add_number": {
|
case "add_number":
|
||||||
|
case "math_add": {
|
||||||
const a = this.#resolveValueInput(node, "a", "0");
|
const a = this.#resolveValueInput(node, "a", "0");
|
||||||
const b = this.#resolveValueInput(node, "b", "0");
|
const b = this.#resolveValueInput(node, "b", "0");
|
||||||
expression = `(${a}) + (${b})`;
|
expression = `(${a}) + (${b})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "multiply_number": {
|
case "math_subtract": {
|
||||||
|
const a = this.#resolveValueInput(node, "a", "0");
|
||||||
|
const b = this.#resolveValueInput(node, "b", "0");
|
||||||
|
expression = `(${a}) - (${b})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "multiply_number":
|
||||||
|
case "math_multiply": {
|
||||||
const a = this.#resolveValueInput(node, "a", "1");
|
const a = this.#resolveValueInput(node, "a", "1");
|
||||||
const b = this.#resolveValueInput(node, "b", "1");
|
const b = this.#resolveValueInput(node, "b", "1");
|
||||||
expression = `(${a}) * (${b})`;
|
expression = `(${a}) * (${b})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "math_divide": {
|
||||||
|
const a = this.#resolveValueInput(node, "a", "0");
|
||||||
|
const b = this.#resolveValueInput(node, "b", "1");
|
||||||
|
expression = `(${a}) / (${b})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "compare": {
|
case "compare": {
|
||||||
const a = this.#resolveValueInput(node, "a", "0");
|
const a = this.#resolveValueInput(node, "a", "0");
|
||||||
const b = this.#resolveValueInput(node, "b", "0");
|
const b = this.#resolveValueInput(node, "b", "0");
|
||||||
@@ -788,60 +840,6 @@ export class LuaGenerator {
|
|||||||
return "==";
|
return "==";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a literal based on pin kind.
|
|
||||||
*
|
|
||||||
* @param {string} kind Pin kind.
|
|
||||||
* @param {unknown} value Raw value.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
#formatLiteral(kind, value) {
|
|
||||||
switch (kind) {
|
|
||||||
case "number": {
|
|
||||||
const numeric = Number(value);
|
|
||||||
return Number.isFinite(numeric) ? String(numeric) : "0";
|
|
||||||
}
|
|
||||||
case "boolean": {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value === "false" ? "false" : "true";
|
|
||||||
}
|
|
||||||
return value ? "true" : "false";
|
|
||||||
}
|
|
||||||
case "table": {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed.length ? trimmed : "{}";
|
|
||||||
}
|
|
||||||
return "{}";
|
|
||||||
}
|
|
||||||
case "string":
|
|
||||||
return JSON.stringify(String(value ?? ""));
|
|
||||||
default:
|
|
||||||
return JSON.stringify(String(value ?? ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a default literal for the supplied pin kind.
|
|
||||||
*
|
|
||||||
* @param {string} kind Pin kind reference.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
#defaultLiteralForKind(kind) {
|
|
||||||
switch (kind) {
|
|
||||||
case "number":
|
|
||||||
return "0";
|
|
||||||
case "boolean":
|
|
||||||
return "false";
|
|
||||||
case "string":
|
|
||||||
return '""';
|
|
||||||
case "table":
|
|
||||||
return "{}";
|
|
||||||
default:
|
|
||||||
return "nil";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the display name for a custom event node.
|
* Determines the display name for a custom event node.
|
||||||
*
|
*
|
||||||
@@ -1012,14 +1010,11 @@ export class LuaGenerator {
|
|||||||
const value = variable?.defaultValue;
|
const value = variable?.defaultValue;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "number": {
|
case "number":
|
||||||
const numeric = Number(value);
|
|
||||||
return Number.isFinite(numeric) ? String(numeric) : "0";
|
|
||||||
}
|
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return value ? "true" : "false";
|
|
||||||
case "string":
|
case "string":
|
||||||
return JSON.stringify(String(value ?? ""));
|
case "color":
|
||||||
|
return formatLiteral(type, value);
|
||||||
case "table": {
|
case "table": {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const fragments = value
|
const fragments = value
|
||||||
@@ -1137,6 +1132,38 @@ export class LuaGenerator {
|
|||||||
return this.#sanitizeIdentifier(fallback);
|
return this.#sanitizeIdentifier(fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the type of a workspace variable by its ID.
|
||||||
|
*
|
||||||
|
* @param {string} variableId Variable identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#getVariableType(variableId) {
|
||||||
|
const variable = this.variables.find((v) => v.id === variableId);
|
||||||
|
return variable?.type ?? "any";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the output type for a node's output pin.
|
||||||
|
*
|
||||||
|
* @param {BlueprintNode} node Source node.
|
||||||
|
* @param {string} pinId Output pin identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#getOutputPinType(node, pinId) {
|
||||||
|
// For get_var nodes, return the actual variable type
|
||||||
|
if (node.type === "get_var") {
|
||||||
|
const variableId = node.properties?.variableId;
|
||||||
|
if (variableId) {
|
||||||
|
return this.#getVariableType(variableId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use the pin's declared kind
|
||||||
|
const pin = this.#findPin(node, pinId);
|
||||||
|
return pin?.kind ?? "any";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes a variable type descriptor.
|
* Normalizes a variable type descriptor.
|
||||||
*
|
*
|
||||||
@@ -1144,19 +1171,7 @@ export class LuaGenerator {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
#normalizeVariableType(value) {
|
#normalizeVariableType(value) {
|
||||||
if (typeof value === "string") {
|
return normalizeVariableType(value);
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
switch (normalized) {
|
|
||||||
case "number":
|
|
||||||
case "string":
|
|
||||||
case "boolean":
|
|
||||||
case "table":
|
|
||||||
return normalized;
|
|
||||||
default:
|
|
||||||
return "any";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "any";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BlueprintNode } from "./BlueprintNode.js";
|
import { BlueprintNode } from "./BlueprintNode.js";
|
||||||
import { Connection } from "./Connection.js";
|
import { Connection } from "./Connection.js";
|
||||||
|
import { areTypesCompatible } from "../types/TypeRegistry.js";
|
||||||
|
|
||||||
/** @typedef {import('./BlueprintNode.js').BlueprintNodeInit} BlueprintNodeInit */
|
/** @typedef {import('./BlueprintNode.js').BlueprintNodeInit} BlueprintNodeInit */
|
||||||
/** @typedef {import('./BlueprintNode.js').PinDescriptor} PinDescriptor */
|
/** @typedef {import('./BlueprintNode.js').PinDescriptor} PinDescriptor */
|
||||||
@@ -321,11 +322,7 @@ export class NodeGraph extends EventTarget {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!areTypesCompatible(fromPin.kind, toPin.kind)) {
|
||||||
toPin.kind !== "any" &&
|
|
||||||
fromPin.kind !== "any" &&
|
|
||||||
fromPin.kind !== toPin.kind
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
268
scripts/core/SpriteSheet.js
Normal file
268
scripts/core/SpriteSheet.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* Manages PICO-8 sprite sheet data with 256 8x8 sprites.
|
||||||
|
* Total sheet size: 128x128 pixels, 16-color palette.
|
||||||
|
*/
|
||||||
|
export class SpriteSheet {
|
||||||
|
/**
|
||||||
|
* @param {Object} [options={}] Configuration options.
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
/** @type {Uint8Array} Pixel data (128x128, each byte is a palette index 0-15) */
|
||||||
|
this.pixels = new Uint8Array(128 * 128);
|
||||||
|
|
||||||
|
/** @type {Uint8Array} Sprite flags (256 sprites, 8 flags each) */
|
||||||
|
this.flags = new Uint8Array(256);
|
||||||
|
|
||||||
|
/** @type {Array<string>} PICO-8 default 16-color palette */
|
||||||
|
this.palette = [
|
||||||
|
"#000000", "#1D2B53", "#7E2553", "#008751",
|
||||||
|
"#AB5236", "#5F574F", "#C2C3C7", "#FFF1E8",
|
||||||
|
"#FF004D", "#FFA300", "#FFEC27", "#00E436",
|
||||||
|
"#29ADFF", "#83769C", "#FF77A8", "#FFCCAA"
|
||||||
|
];
|
||||||
|
|
||||||
|
this.#initialize(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes sprite sheet data.
|
||||||
|
*
|
||||||
|
* @param {Object} options Configuration options.
|
||||||
|
*/
|
||||||
|
#initialize(options) {
|
||||||
|
if (options.pixels instanceof Uint8Array && options.pixels.length === 128 * 128) {
|
||||||
|
this.pixels.set(options.pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.flags instanceof Uint8Array && options.flags.length === 256) {
|
||||||
|
this.flags.set(options.flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets pixel color at coordinates.
|
||||||
|
*
|
||||||
|
* @param {number} x X coordinate (0-127).
|
||||||
|
* @param {number} y Y coordinate (0-127).
|
||||||
|
* @returns {number} Palette index (0-15).
|
||||||
|
*/
|
||||||
|
getPixel(x, y) {
|
||||||
|
if (x < 0 || x >= 128 || y < 0 || y >= 128) return 0;
|
||||||
|
return this.pixels[y * 128 + x];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets pixel color at coordinates.
|
||||||
|
*
|
||||||
|
* @param {number} x X coordinate (0-127).
|
||||||
|
* @param {number} y Y coordinate (0-127).
|
||||||
|
* @param {number} color Palette index (0-15).
|
||||||
|
*/
|
||||||
|
setPixel(x, y, color) {
|
||||||
|
if (x < 0 || x >= 128 || y < 0 || y >= 128) return;
|
||||||
|
this.pixels[y * 128 + x] = Math.max(0, Math.min(15, color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all pixels for a specific sprite.
|
||||||
|
*
|
||||||
|
* @param {number} spriteIndex Sprite number (0-255).
|
||||||
|
* @returns {Uint8Array} 8x8 pixel array.
|
||||||
|
*/
|
||||||
|
getSprite(spriteIndex) {
|
||||||
|
const spriteData = new Uint8Array(8 * 8);
|
||||||
|
const sx = (spriteIndex % 16) * 8;
|
||||||
|
const sy = Math.floor(spriteIndex / 16) * 8;
|
||||||
|
|
||||||
|
for (let y = 0; y < 8; y++) {
|
||||||
|
for (let x = 0; x < 8; x++) {
|
||||||
|
spriteData[y * 8 + x] = this.getPixel(sx + x, sy + y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spriteData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all pixels for a specific sprite.
|
||||||
|
*
|
||||||
|
* @param {number} spriteIndex Sprite number (0-255).
|
||||||
|
* @param {Uint8Array} data 8x8 pixel array.
|
||||||
|
*/
|
||||||
|
setSprite(spriteIndex, data) {
|
||||||
|
if (data.length !== 64) return;
|
||||||
|
|
||||||
|
const sx = (spriteIndex % 16) * 8;
|
||||||
|
const sy = Math.floor(spriteIndex / 16) * 8;
|
||||||
|
|
||||||
|
for (let y = 0; y < 8; y++) {
|
||||||
|
for (let x = 0; x < 8; x++) {
|
||||||
|
this.setPixel(sx + x, sy + y, data[y * 8 + x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets flags for a sprite.
|
||||||
|
*
|
||||||
|
* @param {number} spriteIndex Sprite number (0-255).
|
||||||
|
* @returns {number} 8-bit flag value.
|
||||||
|
*/
|
||||||
|
getFlags(spriteIndex) {
|
||||||
|
if (spriteIndex < 0 || spriteIndex >= 256) return 0;
|
||||||
|
return this.flags[spriteIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets flags for a sprite.
|
||||||
|
*
|
||||||
|
* @param {number} spriteIndex Sprite number (0-255).
|
||||||
|
* @param {number} flagValue 8-bit flag value.
|
||||||
|
*/
|
||||||
|
setFlags(spriteIndex, flagValue) {
|
||||||
|
if (spriteIndex < 0 || spriteIndex >= 256) return;
|
||||||
|
this.flags[spriteIndex] = flagValue & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a specific flag is set.
|
||||||
|
*
|
||||||
|
* @param {number} spriteIndex Sprite number (0-255).
|
||||||
|
* @param {number} flagBit Flag bit (0-7).
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasFlag(spriteIndex, flagBit) {
|
||||||
|
if (flagBit < 0 || flagBit > 7) return false;
|
||||||
|
return (this.getFlags(spriteIndex) & (1 << flagBit)) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a specific flag.
|
||||||
|
*
|
||||||
|
* @param {number} spriteIndex Sprite number (0-255).
|
||||||
|
* @param {number} flagBit Flag bit (0-7).
|
||||||
|
* @param {boolean} value Flag state.
|
||||||
|
*/
|
||||||
|
setFlag(spriteIndex, flagBit, value) {
|
||||||
|
if (flagBit < 0 || flagBit > 7) return;
|
||||||
|
|
||||||
|
const currentFlags = this.getFlags(spriteIndex);
|
||||||
|
const mask = 1 << flagBit;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.setFlags(spriteIndex, currentFlags | mask);
|
||||||
|
} else {
|
||||||
|
this.setFlags(spriteIndex, currentFlags & ~mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all sprite data.
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.pixels.fill(0);
|
||||||
|
this.flags.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes sprite sheet to JSON.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
pixels: Array.from(this.pixels),
|
||||||
|
flags: Array.from(this.flags)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates sprite sheet from JSON data.
|
||||||
|
*
|
||||||
|
* @param {Object} data Serialized sprite data.
|
||||||
|
* @returns {SpriteSheet}
|
||||||
|
*/
|
||||||
|
static fromJSON(data) {
|
||||||
|
return new SpriteSheet({
|
||||||
|
pixels: new Uint8Array(data.pixels || []),
|
||||||
|
flags: new Uint8Array(data.flags || [])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports to PICO-8 .p8 format gfx section.
|
||||||
|
*
|
||||||
|
* @returns {string} Hex string for __gfx__ section.
|
||||||
|
*/
|
||||||
|
toP8Gfx() {
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
for (let row = 0; row < 128; row++) {
|
||||||
|
let line = "";
|
||||||
|
for (let col = 0; col < 128; col += 2) {
|
||||||
|
const p1 = this.getPixel(col, row);
|
||||||
|
const p2 = this.getPixel(col + 1, row);
|
||||||
|
line += p1.toString(16) + p2.toString(16);
|
||||||
|
}
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports from PICO-8 .p8 format gfx section.
|
||||||
|
*
|
||||||
|
* @param {string} gfxData Hex string from __gfx__ section.
|
||||||
|
*/
|
||||||
|
fromP8Gfx(gfxData) {
|
||||||
|
const lines = gfxData.trim().split("\n");
|
||||||
|
|
||||||
|
for (let row = 0; row < 128 && row < lines.length; row++) {
|
||||||
|
const line = lines[row].trim();
|
||||||
|
for (let col = 0; col < 128; col += 2) {
|
||||||
|
const hexPair = line.substring(col, col + 2);
|
||||||
|
if (hexPair.length === 2) {
|
||||||
|
this.setPixel(col, row, parseInt(hexPair[0], 16));
|
||||||
|
this.setPixel(col + 1, row, parseInt(hexPair[1], 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports sprite flags to PICO-8 .p8 format.
|
||||||
|
*
|
||||||
|
* @returns {string} Hex string for __gff__ section.
|
||||||
|
*/
|
||||||
|
toP8Flags() {
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 256; i += 2) {
|
||||||
|
const f1 = this.getFlags(i).toString(16).padStart(2, "0");
|
||||||
|
const f2 = this.getFlags(i + 1).toString(16).padStart(2, "0");
|
||||||
|
lines.push(f1 + f2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports sprite flags from PICO-8 .p8 format.
|
||||||
|
*
|
||||||
|
* @param {string} flagData Hex string from __gff__ section.
|
||||||
|
*/
|
||||||
|
fromP8Flags(flagData) {
|
||||||
|
const lines = flagData.trim().split("\n");
|
||||||
|
|
||||||
|
for (let i = 0; i < 128 && i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (line.length >= 2) {
|
||||||
|
this.setFlags(i * 2, parseInt(line.substring(0, 2), 16));
|
||||||
|
}
|
||||||
|
if (line.length >= 4) {
|
||||||
|
this.setFlags(i * 2 + 1, parseInt(line.substring(2, 4), 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
scripts/core/TileSet.js
Normal file
167
scripts/core/TileSet.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Represents a single tileset with 8x8 tiles (128x128px total, 16x16 grid).
|
||||||
|
*/
|
||||||
|
export class TileSet {
|
||||||
|
/**
|
||||||
|
* @param {Object} options Configuration options.
|
||||||
|
* @param {string} options.id Unique identifier.
|
||||||
|
* @param {string} options.name Display name.
|
||||||
|
* @param {string} options.filePath Path or URL to PNG file (file:// or blob:).
|
||||||
|
* @param {string} [options.nativePath] Native file system path (for Electron).
|
||||||
|
* @param {number} [options.tileWidth=8] Tile width in pixels.
|
||||||
|
* @param {number} [options.tileHeight=8] Tile height in pixels.
|
||||||
|
* @param {number} [options.columns=16] Number of tiles per row (128px / 8px = 16).
|
||||||
|
* @param {number} [options.rows=16] Number of tile rows (128px / 8px = 16).
|
||||||
|
*/
|
||||||
|
constructor(options) {
|
||||||
|
this.id = options.id || this.#generateId();
|
||||||
|
this.name = options.name || "Untitled Tileset";
|
||||||
|
this.filePath = options.filePath || "";
|
||||||
|
this.nativePath = options.nativePath || "";
|
||||||
|
this.tileWidth = options.tileWidth || 8;
|
||||||
|
this.tileHeight = options.tileHeight || 8;
|
||||||
|
this.columns = options.columns || 16;
|
||||||
|
this.rows = options.rows || 16;
|
||||||
|
|
||||||
|
/** @type {HTMLImageElement | null} */
|
||||||
|
this.image = null;
|
||||||
|
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.isLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique identifier.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#generateId() {
|
||||||
|
return `tileset_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total number of tiles.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getTileCount() {
|
||||||
|
return this.columns * this.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total width of the tileset image.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getWidth() {
|
||||||
|
return this.columns * this.tileWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total height of the tileset image.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getHeight() {
|
||||||
|
return this.rows * this.tileHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the tileset image from file path.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
if (!this.filePath) {
|
||||||
|
throw new Error("No file path specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// Validate image is exactly 128x128px
|
||||||
|
if (img.width !== 128 || img.height !== 128) {
|
||||||
|
reject(new Error(`Tileset must be exactly 128x128px (found ${img.width}x${img.height}px)`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.image = img;
|
||||||
|
this.isLoaded = true;
|
||||||
|
|
||||||
|
// Fixed grid: 16x16 tiles for 128x128 image with 8x8 tiles
|
||||||
|
this.columns = 16;
|
||||||
|
this.rows = 16;
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error(`Failed to load image: ${this.filePath}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add cache-busting for file:// URLs to ensure reload works
|
||||||
|
const cacheBuster = this.filePath.startsWith("file://")
|
||||||
|
? `?t=${Date.now()}`
|
||||||
|
: "";
|
||||||
|
img.src = this.filePath + cacheBuster;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the tileset image.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async reload() {
|
||||||
|
this.isLoaded = false;
|
||||||
|
this.image = null;
|
||||||
|
return this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets tile coordinates for a specific tile index.
|
||||||
|
*
|
||||||
|
* @param {number} tileIndex Tile index (0-based).
|
||||||
|
* @returns {{x: number, y: number, width: number, height: number}}
|
||||||
|
*/
|
||||||
|
getTileRect(tileIndex) {
|
||||||
|
const col = tileIndex % this.columns;
|
||||||
|
const row = Math.floor(tileIndex / this.columns);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: col * this.tileWidth,
|
||||||
|
y: row * this.tileHeight,
|
||||||
|
width: this.tileWidth,
|
||||||
|
height: this.tileHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes tileset to JSON.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
filePath: this.filePath,
|
||||||
|
nativePath: this.nativePath,
|
||||||
|
tileWidth: this.tileWidth,
|
||||||
|
tileHeight: this.tileHeight,
|
||||||
|
columns: this.columns,
|
||||||
|
rows: this.rows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates tileset from JSON data.
|
||||||
|
*
|
||||||
|
* @param {Object} data Serialized tileset.
|
||||||
|
* @returns {TileSet}
|
||||||
|
*/
|
||||||
|
static fromJSON(data) {
|
||||||
|
return new TileSet(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
scripts/core/lua/LuaLiteralBoolean.js
Normal file
41
scripts/core/lua/LuaLiteralBoolean.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {LuaLiteralHandler} */
|
||||||
|
export const LuaLiteralBoolean = {
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
format(value) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value === "false" ? "false" : "true";
|
||||||
|
}
|
||||||
|
return value ? "true" : "false";
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
|
defaultLiteral() {
|
||||||
|
return "false";
|
||||||
|
},
|
||||||
|
|
||||||
|
spawnableByDefault: true,
|
||||||
|
|
||||||
|
nodeModule: {
|
||||||
|
definition: {
|
||||||
|
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 }],
|
||||||
|
},
|
||||||
|
behavior: {
|
||||||
|
evaluateValue: ({ node, formatLiteral }) =>
|
||||||
|
formatLiteral("boolean", node.properties.value ?? true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
39
scripts/core/lua/LuaLiteralColor.js
Normal file
39
scripts/core/lua/LuaLiteralColor.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {LuaLiteralHandler} */
|
||||||
|
export const LuaLiteralColor = {
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
format(value) {
|
||||||
|
const n = Math.max(0, Math.min(15, Math.floor(Number(value) || 0)));
|
||||||
|
return String(n);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
|
defaultLiteral() {
|
||||||
|
return "7";
|
||||||
|
},
|
||||||
|
|
||||||
|
spawnableByDefault: true,
|
||||||
|
|
||||||
|
nodeModule: {
|
||||||
|
definition: {
|
||||||
|
id: "color_literal",
|
||||||
|
title: "Color",
|
||||||
|
category: "Values",
|
||||||
|
description: "Constant color literal (0-15).",
|
||||||
|
searchTags: ["color", "literal", "constant", "value"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [{ id: "value", name: "Value", direction: "output", kind: "color" }],
|
||||||
|
properties: [{ key: "value", label: "Color", type: "number", defaultValue: 7 }],
|
||||||
|
},
|
||||||
|
behavior: {
|
||||||
|
evaluateValue: ({ node, formatLiteral }) =>
|
||||||
|
formatLiteral("color", node.properties.value ?? 7),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
39
scripts/core/lua/LuaLiteralNumber.js
Normal file
39
scripts/core/lua/LuaLiteralNumber.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {LuaLiteralHandler} */
|
||||||
|
export const LuaLiteralNumber = {
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
format(value) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
return Number.isFinite(numeric) ? String(numeric) : "0";
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
|
defaultLiteral() {
|
||||||
|
return "0";
|
||||||
|
},
|
||||||
|
|
||||||
|
spawnableByDefault: true,
|
||||||
|
|
||||||
|
nodeModule: {
|
||||||
|
definition: {
|
||||||
|
id: "number_literal",
|
||||||
|
title: "Number",
|
||||||
|
category: "Values",
|
||||||
|
description: "Constant numeric literal.",
|
||||||
|
searchTags: ["number", "literal", "constant", "value"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [{ id: "value", name: "Value", direction: "output", kind: "number" }],
|
||||||
|
properties: [{ key: "value", label: "Number", type: "number", defaultValue: 0 }],
|
||||||
|
},
|
||||||
|
behavior: {
|
||||||
|
evaluateValue: ({ node, formatLiteral }) =>
|
||||||
|
formatLiteral("number", node.properties.value ?? 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
38
scripts/core/lua/LuaLiteralString.js
Normal file
38
scripts/core/lua/LuaLiteralString.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {LuaLiteralHandler} */
|
||||||
|
export const LuaLiteralString = {
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
format(value) {
|
||||||
|
return JSON.stringify(String(value ?? ""));
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
|
defaultLiteral() {
|
||||||
|
return '""';
|
||||||
|
},
|
||||||
|
|
||||||
|
spawnableByDefault: true,
|
||||||
|
|
||||||
|
nodeModule: {
|
||||||
|
definition: {
|
||||||
|
id: "string_literal",
|
||||||
|
title: "String",
|
||||||
|
category: "Values",
|
||||||
|
description: "Constant string literal.",
|
||||||
|
searchTags: ["string", "literal", "text", "value"],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [{ id: "value", name: "Value", direction: "output", kind: "string" }],
|
||||||
|
properties: [{ key: "value", label: "Text", type: "string", defaultValue: "hello" }],
|
||||||
|
},
|
||||||
|
behavior: {
|
||||||
|
evaluateValue: ({ node, formatLiteral }) =>
|
||||||
|
formatLiteral("string", node.properties.value ?? ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
27
scripts/core/lua/LuaLiteralTable.js
Normal file
27
scripts/core/lua/LuaLiteralTable.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {LuaLiteralHandler} */
|
||||||
|
export const LuaLiteralTable = {
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
format(value) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length ? trimmed : "{}";
|
||||||
|
}
|
||||||
|
return "{}";
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
|
defaultLiteral() {
|
||||||
|
return "{}";
|
||||||
|
},
|
||||||
|
|
||||||
|
spawnableByDefault: false,
|
||||||
|
|
||||||
|
nodeModule: null,
|
||||||
|
};
|
||||||
56
scripts/core/lua/LuaLiteralTypes.js
Normal file
56
scripts/core/lua/LuaLiteralTypes.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { LuaLiteralNumber } from "./LuaLiteralNumber.js";
|
||||||
|
import { LuaLiteralBoolean } from "./LuaLiteralBoolean.js";
|
||||||
|
import { LuaLiteralString } from "./LuaLiteralString.js";
|
||||||
|
import { LuaLiteralTable } from "./LuaLiteralTable.js";
|
||||||
|
import { LuaLiteralColor } from "./LuaLiteralColor.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} LuaLiteralHandler
|
||||||
|
* @property {(value: unknown) => string} format Formats a raw value into a Lua literal string.
|
||||||
|
* @property {() => string} defaultLiteral Returns the default Lua literal string for this type.
|
||||||
|
* @property {boolean} [spawnableByDefault] Whether this type creates a spawnable literal node in the graph.
|
||||||
|
* @property {import('../../nodes/nodeTypes.js').NodeModule | null} [nodeModule] Node module definition, or null if no literal node exists.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Map<string, LuaLiteralHandler>} */
|
||||||
|
const HANDLERS = new Map([
|
||||||
|
["number", LuaLiteralNumber],
|
||||||
|
["boolean", LuaLiteralBoolean],
|
||||||
|
["string", LuaLiteralString],
|
||||||
|
["table", LuaLiteralTable],
|
||||||
|
["color", LuaLiteralColor],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a raw value into a Lua literal string for the given pin kind.
|
||||||
|
*
|
||||||
|
* @param {string} kind Pin kind.
|
||||||
|
* @param {unknown} value Raw value.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatLiteral(kind, value) {
|
||||||
|
const handler = HANDLERS.get(kind);
|
||||||
|
return handler ? handler.format(value) : LuaLiteralString.format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default Lua literal string for the given pin kind.
|
||||||
|
*
|
||||||
|
* @param {string} kind Pin kind.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function defaultLiteralForKind(kind) {
|
||||||
|
const handler = HANDLERS.get(kind);
|
||||||
|
return handler ? handler.defaultLiteral() : "nil";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all node modules defined by literal handlers that have a nodeModule.
|
||||||
|
*
|
||||||
|
* @returns {Array<import('../../nodes/nodeTypes.js').NodeModule>}
|
||||||
|
*/
|
||||||
|
export function getLiteralNodeModules() {
|
||||||
|
return [...HANDLERS.values()]
|
||||||
|
.map((handler) => handler.nodeModule)
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
263
scripts/main.js
263
scripts/main.js
@@ -2,6 +2,7 @@ import { BlueprintWorkspace } from "./ui/BlueprintWorkspace.js";
|
|||||||
import { NodeRegistry } from "./nodes/NodeRegistry.js";
|
import { NodeRegistry } from "./nodes/NodeRegistry.js";
|
||||||
import { LuaGenerator } from "./core/LuaGenerator.js";
|
import { LuaGenerator } from "./core/LuaGenerator.js";
|
||||||
import { EmbeddedPreview } from "./ui/EmbeddedPreview.js";
|
import { EmbeddedPreview } from "./ui/EmbeddedPreview.js";
|
||||||
|
import { TileManager } from "./ui/TileManager.js";
|
||||||
|
|
||||||
// Global error handlers
|
// Global error handlers
|
||||||
window.addEventListener('error', (event) => {
|
window.addEventListener('error', (event) => {
|
||||||
@@ -13,7 +14,18 @@ window.addEventListener('unhandledrejection', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{ compileLua(source: string): Promise<{ filePath: string }> }} ElectronAPI
|
* @typedef {{
|
||||||
|
* compileLua(source: string): Promise<{ filePath: string }>;
|
||||||
|
* previewCart?(cartContent: string): Promise<{ filePath: string }>;
|
||||||
|
* writePlayerHtml?(htmlContent: string): Promise<{ filePath: string }>;
|
||||||
|
* writePlayerCart?(cartContent: string): Promise<{ filePath: string }>;
|
||||||
|
* getCartStats?(cartContent: string): Promise<{ tokens: number, lines: number, chars: number, rawOutput: string }>;
|
||||||
|
* minimizeWindow?(): Promise<void>;
|
||||||
|
* toggleMaximizeWindow?(): Promise<boolean>;
|
||||||
|
* closeWindow?(): Promise<void>;
|
||||||
|
* getWindowMaximized?(): Promise<boolean>;
|
||||||
|
* onWindowMaximizedChanged?(callback: (isMaximized: boolean) => void): () => void;
|
||||||
|
* }} ElectronAPI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,8 +92,8 @@ const deleteNodeButton = /** @type {HTMLButtonElement} */ (
|
|||||||
"deleteNodeButton"
|
"deleteNodeButton"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const compileButton = /** @type {HTMLButtonElement} */ (
|
const saveButton = /** @type {HTMLButtonElement} */ (
|
||||||
requireElement(document.getElementById("compileButton"), "compileButton")
|
requireElement(document.getElementById("saveButton"), "saveButton")
|
||||||
);
|
);
|
||||||
const previewButton = /** @type {HTMLButtonElement} */ (
|
const previewButton = /** @type {HTMLButtonElement} */ (
|
||||||
requireElement(document.getElementById("previewButton"), "previewButton")
|
requireElement(document.getElementById("previewButton"), "previewButton")
|
||||||
@@ -107,6 +119,24 @@ const frameSelectionButton = /** @type {HTMLButtonElement} */ (
|
|||||||
"frameSelectionButton"
|
"frameSelectionButton"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const windowControls = /** @type {HTMLElement} */ (
|
||||||
|
requireElement(document.getElementById("windowControls"), "windowControls")
|
||||||
|
);
|
||||||
|
const minimizeWindowButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("minimizeWindowButton"),
|
||||||
|
"minimizeWindowButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const maximizeWindowButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("maximizeWindowButton"),
|
||||||
|
"maximizeWindowButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const closeWindowButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(document.getElementById("closeWindowButton"), "closeWindowButton")
|
||||||
|
);
|
||||||
const luaModal = /** @type {HTMLElement} */ (
|
const luaModal = /** @type {HTMLElement} */ (
|
||||||
requireElement(document.getElementById("luaModal"), "luaModal")
|
requireElement(document.getElementById("luaModal"), "luaModal")
|
||||||
);
|
);
|
||||||
@@ -128,6 +158,12 @@ const closeLuaModalButton = /** @type {HTMLButtonElement} */ (
|
|||||||
"closeLuaModalButton"
|
"closeLuaModalButton"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const tileManagerButton = /** @type {HTMLButtonElement} */ (
|
||||||
|
requireElement(
|
||||||
|
document.getElementById("tileManagerButton"),
|
||||||
|
"tileManagerButton"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const registry = new NodeRegistry();
|
const registry = new NodeRegistry();
|
||||||
const generator = new LuaGenerator(registry);
|
const generator = new LuaGenerator(registry);
|
||||||
@@ -146,6 +182,7 @@ const workspace = new BlueprintWorkspace({
|
|||||||
eventList,
|
eventList,
|
||||||
variableList,
|
variableList,
|
||||||
addVariableButton,
|
addVariableButton,
|
||||||
|
saveButton,
|
||||||
projectSettingsButton,
|
projectSettingsButton,
|
||||||
paletteToggleButton,
|
paletteToggleButton,
|
||||||
appBodyElement,
|
appBodyElement,
|
||||||
@@ -161,12 +198,99 @@ const embeddedPreview = new EmbeddedPreview({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize tile manager
|
||||||
|
const tileManager = new TileManager();
|
||||||
|
tileManager.initialize();
|
||||||
|
|
||||||
/** @type {ElectronAPI | undefined} */
|
/** @type {ElectronAPI | undefined} */
|
||||||
const electronAPI =
|
const electronAPI =
|
||||||
typeof window !== "undefined" && "electronAPI" in window
|
typeof window !== "undefined" && "electronAPI" in window
|
||||||
? /** @type {ElectronAPI} */ (window.electronAPI)
|
? /** @type {ElectronAPI} */ (window.electronAPI)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Get titlebar element
|
||||||
|
const titleBarElement = document.querySelector('.header-title');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the window titlebar with token count information.
|
||||||
|
*
|
||||||
|
* @param {number} tokenCount Number of tokens in the cart
|
||||||
|
*/
|
||||||
|
function updateTitleBarWithTokens(tokenCount) {
|
||||||
|
if (titleBarElement) {
|
||||||
|
const maxTokens = 8192;
|
||||||
|
const percentage = ((tokenCount / maxTokens) * 100).toFixed(1);
|
||||||
|
titleBarElement.textContent = `picoGraph - ${tokenCount} / ${maxTokens} tokens (${percentage}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates cart content from Lua code.
|
||||||
|
*
|
||||||
|
* @param {string} luaCode Generated Lua code
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function generateCartContent(luaCode) {
|
||||||
|
const hasUpdateCallback = /function\s+_update(?:60)?\b|_update(?:60)?\s*=/.test(luaCode);
|
||||||
|
const hasDrawCallback = /function\s+_draw\b|_draw\s*=/.test(luaCode);
|
||||||
|
const fallback = (hasUpdateCallback && hasDrawCallback) ? '' :
|
||||||
|
(!hasUpdateCallback ? '\nfunction _update()\nend\n' : '') +
|
||||||
|
(!hasDrawCallback ? '\nfunction _draw()\n cls()\n print("picograph ok",2,60,7)\nend\n' : '');
|
||||||
|
return `pico-8 cartridge // http://www.pico-8.com
|
||||||
|
version 41
|
||||||
|
__lua__
|
||||||
|
${luaCode}${fallback}
|
||||||
|
__gfx__
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates token count display after workspace changes.
|
||||||
|
*/
|
||||||
|
async function updateTokenCount() {
|
||||||
|
if (!electronAPI?.getCartStats) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const luaCode = workspace.exportLua();
|
||||||
|
let cartContent = generateCartContent(luaCode);
|
||||||
|
// Ensure Unix line endings (p8tool requires \n, not \r\n)
|
||||||
|
cartContent = cartContent.replace(/\r\n/g, '\n');
|
||||||
|
const stats = await electronAPI.getCartStats(cartContent);
|
||||||
|
updateTitleBarWithTokens(stats.tokens);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update token count:", error);
|
||||||
|
if (titleBarElement) {
|
||||||
|
titleBarElement.textContent = "picoGraph";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override handlePersistenceStateChange to also update token count
|
||||||
|
const originalHandlePersistenceStateChange = workspace.handlePersistenceStateChange.bind(workspace);
|
||||||
|
workspace.handlePersistenceStateChange = function(state) {
|
||||||
|
originalHandlePersistenceStateChange(state);
|
||||||
|
|
||||||
|
// Update token count when save completes (both auto and manual)
|
||||||
|
if (state?.status === "saved") {
|
||||||
|
updateTokenCount();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial token count update after workspace loads
|
||||||
|
if (electronAPI?.getCartStats) {
|
||||||
|
updateTokenCount();
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("contextmenu", (event) => {
|
document.addEventListener("contextmenu", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
@@ -232,7 +356,6 @@ let isLuaModalOpen = false;
|
|||||||
let lastFocusedElement = /** @type {HTMLElement | null} */ (null);
|
let lastFocusedElement = /** @type {HTMLElement | null} */ (null);
|
||||||
let copyFeedbackTimeout = /** @type {number | null} */ (null);
|
let copyFeedbackTimeout = /** @type {number | null} */ (null);
|
||||||
let currentLuaSource = "";
|
let currentLuaSource = "";
|
||||||
let compileFeedbackTimeout = /** @type {number | null} */ (null);
|
|
||||||
|
|
||||||
const scheduleMicrotask = (callback) => {
|
const scheduleMicrotask = (callback) => {
|
||||||
if (typeof queueMicrotask === "function") {
|
if (typeof queueMicrotask === "function") {
|
||||||
@@ -374,83 +497,69 @@ const closeLuaModal = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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";
|
|
||||||
}
|
|
||||||
|
|
||||||
previewButton.addEventListener("click", () => {
|
previewButton.addEventListener("click", () => {
|
||||||
embeddedPreview.open();
|
embeddedPreview.open();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setMaximizeButtonState = (isMaximized) => {
|
||||||
|
maximizeWindowButton.classList.toggle("is-active", Boolean(isMaximized));
|
||||||
|
maximizeWindowButton.setAttribute(
|
||||||
|
"title",
|
||||||
|
isMaximized ? "Restore" : "Maximize"
|
||||||
|
);
|
||||||
|
maximizeWindowButton.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
isMaximized ? "Restore window" : "Maximize window"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
electronAPI &&
|
||||||
|
typeof electronAPI.minimizeWindow === "function" &&
|
||||||
|
typeof electronAPI.toggleMaximizeWindow === "function" &&
|
||||||
|
typeof electronAPI.closeWindow === "function"
|
||||||
|
) {
|
||||||
|
minimizeWindowButton.addEventListener("click", () => {
|
||||||
|
electronAPI.minimizeWindow().catch((error) => {
|
||||||
|
console.error("Failed to minimize window", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
maximizeWindowButton.addEventListener("click", () => {
|
||||||
|
electronAPI.toggleMaximizeWindow()
|
||||||
|
.then((isMaximized) => {
|
||||||
|
setMaximizeButtonState(isMaximized);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to toggle maximize state", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
closeWindowButton.addEventListener("click", () => {
|
||||||
|
electronAPI.closeWindow().catch((error) => {
|
||||||
|
console.error("Failed to close window", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof electronAPI.getWindowMaximized === "function") {
|
||||||
|
electronAPI.getWindowMaximized()
|
||||||
|
.then((isMaximized) => {
|
||||||
|
setMaximizeButtonState(isMaximized);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to query maximize state", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof electronAPI.onWindowMaximizedChanged === "function") {
|
||||||
|
electronAPI.onWindowMaximizedChanged((isMaximized) => {
|
||||||
|
setMaximizeButtonState(isMaximized);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
windowControls.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
exportLuaButton.addEventListener("click", () => {
|
exportLuaButton.addEventListener("click", () => {
|
||||||
openLuaModal();
|
openLuaModal();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
|
import { getLiteralNodeModules } from "../../core/lua/LuaLiteralTypes.js";
|
||||||
import { eventInitNode } from "./eventStart.js";
|
import { eventInitNode } from "./eventStart.js";
|
||||||
import { eventUpdateNode } from "./eventUpdate.js";
|
import { eventUpdateNode } from "./eventUpdate.js";
|
||||||
import { eventDrawNode } from "./eventDraw.js";
|
import { eventDrawNode } from "./eventDraw.js";
|
||||||
import { printNode } from "./print.js";
|
import { printNode } from "./print.js";
|
||||||
import { setVariableNode } from "./setVariable.js";
|
import { setVariableNode } from "./setVariable.js";
|
||||||
import { getVariableNode } from "./getVariable.js";
|
import { getVariableNode } from "./getVariable.js";
|
||||||
import { numberLiteralNode } from "./numberLiteral.js";
|
|
||||||
import { stringLiteralNode } from "./stringLiteral.js";
|
|
||||||
import { booleanLiteralNode } from "./booleanLiteral.js";
|
|
||||||
import { colorLiteralNode } from "./colorLiteral.js";
|
|
||||||
import { addNumberNode } from "./addNumber.js";
|
|
||||||
import { multiplyNumberNode } from "./multiplyNumber.js";
|
|
||||||
import { compareNode } from "./compare.js";
|
import { compareNode } from "./compare.js";
|
||||||
import { sequenceNode } from "./sequence.js";
|
import { sequenceNode } from "./sequence.js";
|
||||||
import { ifNode } from "./ifNode.js";
|
import { ifNode } from "./ifNode.js";
|
||||||
@@ -33,20 +28,16 @@ import { devkitNodes, devkitFunctionChecklist } from "./devkit/index.js";
|
|||||||
import { luaNodes, luaFunctionChecklist } from "./lua/index.js";
|
import { luaNodes, luaFunctionChecklist } from "./lua/index.js";
|
||||||
import { localVariableNodes } from "./localVariables.js";
|
import { localVariableNodes } from "./localVariables.js";
|
||||||
import { converterNodes } from "./converters/index.js";
|
import { converterNodes } from "./converters/index.js";
|
||||||
|
import { rerouteNode, rerouteExecNode } from "./reroute.js";
|
||||||
|
|
||||||
export const nodeModules = [
|
export const nodeModules = [
|
||||||
|
...getLiteralNodeModules(),
|
||||||
eventInitNode,
|
eventInitNode,
|
||||||
eventUpdateNode,
|
eventUpdateNode,
|
||||||
eventDrawNode,
|
eventDrawNode,
|
||||||
printNode,
|
printNode,
|
||||||
setVariableNode,
|
setVariableNode,
|
||||||
getVariableNode,
|
getVariableNode,
|
||||||
numberLiteralNode,
|
|
||||||
stringLiteralNode,
|
|
||||||
booleanLiteralNode,
|
|
||||||
colorLiteralNode,
|
|
||||||
addNumberNode,
|
|
||||||
multiplyNumberNode,
|
|
||||||
compareNode,
|
compareNode,
|
||||||
sequenceNode,
|
sequenceNode,
|
||||||
ifNode,
|
ifNode,
|
||||||
@@ -55,6 +46,8 @@ export const nodeModules = [
|
|||||||
callCustomEventNode,
|
callCustomEventNode,
|
||||||
...localVariableNodes,
|
...localVariableNodes,
|
||||||
...converterNodes,
|
...converterNodes,
|
||||||
|
rerouteNode,
|
||||||
|
rerouteExecNode,
|
||||||
...graphicsNodes,
|
...graphicsNodes,
|
||||||
...systemNodes,
|
...systemNodes,
|
||||||
...tableNodes,
|
...tableNodes,
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export const controllerButtonsNode = createNodeModule(
|
|||||||
name: "Button",
|
name: "Button",
|
||||||
direction: "input",
|
direction: "input",
|
||||||
kind: "number",
|
kind: "number",
|
||||||
|
defaultValue: DEFAULT_BUTTON_VALUE,
|
||||||
|
options: BUTTON_OPTIONS,
|
||||||
description: "Optional button index override",
|
description: "Optional button index override",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -97,6 +99,8 @@ export const controllerButtonsNode = createNodeModule(
|
|||||||
name: "Player",
|
name: "Player",
|
||||||
direction: "input",
|
direction: "input",
|
||||||
kind: "number",
|
kind: "number",
|
||||||
|
defaultValue: DEFAULT_PLAYER_VALUE,
|
||||||
|
options: PLAYER_OPTIONS,
|
||||||
description: "Optional player index override",
|
description: "Optional player index override",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { createNodeModule } from "../nodeTypes.js";
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
import {
|
||||||
|
getLocalVariableTypeOptions,
|
||||||
|
variablePropertyKey,
|
||||||
|
localVariableDefault,
|
||||||
|
LOCAL_VARIABLE_TYPE_IDS,
|
||||||
|
} from "../../types/TypeRegistry.js";
|
||||||
|
|
||||||
const LOCAL_TYPES = [
|
const LOCAL_TYPES = getLocalVariableTypeOptions();
|
||||||
{ 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 DEFAULT_LOCAL_TYPE = LOCAL_TYPES[0]?.value ?? "number";
|
||||||
|
|
||||||
const sanitizeType = (raw) => {
|
const sanitizeType = (raw) => {
|
||||||
const candidate = typeof raw === "string" ? raw.toLowerCase() : DEFAULT_LOCAL_TYPE;
|
const candidate = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||||
return LOCAL_TYPES.some((entry) => entry.value === candidate)
|
return LOCAL_VARIABLE_TYPE_IDS.has(candidate) ? candidate : DEFAULT_LOCAL_TYPE;
|
||||||
? candidate
|
|
||||||
: DEFAULT_LOCAL_TYPE;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureName = (raw, fallback = "localVar") => {
|
const ensureName = (raw, fallback = "localVar") => {
|
||||||
@@ -21,33 +19,10 @@ const ensureName = (raw, fallback = "localVar") => {
|
|||||||
return value.length ? value : fallback;
|
return value.length ? value : fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveValuePropertyKey = (type) => {
|
const resolveValuePropertyKey = variablePropertyKey;
|
||||||
switch (type) {
|
|
||||||
case "string":
|
const defaultValueForType = localVariableDefault;
|
||||||
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 initializeLocalProperties = (properties, { includeValue }) => {
|
||||||
const type = sanitizeType(properties.variableType);
|
const type = sanitizeType(properties.variableType);
|
||||||
@@ -68,6 +43,15 @@ const initializeLocalProperties = (properties, { includeValue }) => {
|
|||||||
if (typeof properties.valueTable !== "string") {
|
if (typeof properties.valueTable !== "string") {
|
||||||
properties.valueTable = String(properties.valueTable ?? "{}");
|
properties.valueTable = String(properties.valueTable ?? "{}");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
typeof properties.valueColor !== "number" ||
|
||||||
|
!Number.isFinite(properties.valueColor)
|
||||||
|
) {
|
||||||
|
const n = Number(properties.valueColor);
|
||||||
|
properties.valueColor = Number.isFinite(n)
|
||||||
|
? Math.max(0, Math.min(15, Math.round(n)))
|
||||||
|
: 7;
|
||||||
|
}
|
||||||
|
|
||||||
if (includeValue) {
|
if (includeValue) {
|
||||||
const key = resolveValuePropertyKey(type);
|
const key = resolveValuePropertyKey(type);
|
||||||
|
|||||||
41
scripts/nodes/library/math/add.js
Normal file
41
scripts/nodes/library/math/add.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two numeric values.
|
||||||
|
*/
|
||||||
|
export const mathAddNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_add",
|
||||||
|
title: "Add",
|
||||||
|
category: "Math",
|
||||||
|
description: "Add two numbers.",
|
||||||
|
searchTags: ["add", "math", "sum", "plus", "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})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
41
scripts/nodes/library/math/divide.js
Normal file
41
scripts/nodes/library/math/divide.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divides one numeric value by another.
|
||||||
|
*/
|
||||||
|
export const mathDivideNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_divide",
|
||||||
|
title: "Divide",
|
||||||
|
category: "Math",
|
||||||
|
description: "Divide one number by another (floating point division).",
|
||||||
|
searchTags: ["divide", "division", "math", "slash", "number", "/"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
id: "a",
|
||||||
|
name: "A",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b",
|
||||||
|
name: "B",
|
||||||
|
direction: "input",
|
||||||
|
kind: "number",
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "res", name: "Result", direction: "output", kind: "number" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
const a = resolveValueInput("a", "0");
|
||||||
|
const b = resolveValueInput("b", "1");
|
||||||
|
return `(${a}) / (${b})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { mathAddNode } from "./add.js";
|
||||||
|
import { mathSubtractNode } from "./subtract.js";
|
||||||
|
import { mathMultiplyNode } from "./multiply.js";
|
||||||
|
import { mathDivideNode } from "./divide.js";
|
||||||
import { mathMaxNode } from "./max.js";
|
import { mathMaxNode } from "./max.js";
|
||||||
import { mathMinNode } from "./min.js";
|
import { mathMinNode } from "./min.js";
|
||||||
import { mathMidNode } from "./mid.js";
|
import { mathMidNode } from "./mid.js";
|
||||||
@@ -25,6 +29,10 @@ import { mathDivNode } from "./div.js";
|
|||||||
* All math-related node modules backed by PICO-8 helpers.
|
* All math-related node modules backed by PICO-8 helpers.
|
||||||
*/
|
*/
|
||||||
export const mathNodes = [
|
export const mathNodes = [
|
||||||
|
mathAddNode,
|
||||||
|
mathSubtractNode,
|
||||||
|
mathMultiplyNode,
|
||||||
|
mathDivideNode,
|
||||||
mathMaxNode,
|
mathMaxNode,
|
||||||
mathMinNode,
|
mathMinNode,
|
||||||
mathMidNode,
|
mathMidNode,
|
||||||
@@ -54,6 +62,10 @@ export const mathNodes = [
|
|||||||
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
|
||||||
*/
|
*/
|
||||||
export const mathFunctionChecklist = [
|
export const mathFunctionChecklist = [
|
||||||
|
{ function: "+", nodeId: "math_add", implemented: true },
|
||||||
|
{ function: "-", nodeId: "math_subtract", implemented: true },
|
||||||
|
{ function: "*", nodeId: "math_multiply", implemented: true },
|
||||||
|
{ function: "/", nodeId: "math_divide", implemented: true },
|
||||||
{ function: "MAX", nodeId: "math_max", implemented: true },
|
{ function: "MAX", nodeId: "math_max", implemented: true },
|
||||||
{ function: "MIN", nodeId: "math_min", implemented: true },
|
{ function: "MIN", nodeId: "math_min", implemented: true },
|
||||||
{ function: "MID", nodeId: "math_mid", implemented: true },
|
{ function: "MID", nodeId: "math_mid", implemented: true },
|
||||||
|
|||||||
41
scripts/nodes/library/math/multiply.js
Normal file
41
scripts/nodes/library/math/multiply.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies two numeric values.
|
||||||
|
*/
|
||||||
|
export const mathMultiplyNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_multiply",
|
||||||
|
title: "Multiply",
|
||||||
|
category: "Math",
|
||||||
|
description: "Multiply two numbers.",
|
||||||
|
searchTags: ["multiply", "product", "math", "times", "*"],
|
||||||
|
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})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
41
scripts/nodes/library/math/subtract.js
Normal file
41
scripts/nodes/library/math/subtract.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts one numeric value from another.
|
||||||
|
*/
|
||||||
|
export const mathSubtractNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "math_subtract",
|
||||||
|
title: "Subtract",
|
||||||
|
category: "Math",
|
||||||
|
description: "Subtract one number from another.",
|
||||||
|
searchTags: ["subtract", "math", "minus", "difference", "-"],
|
||||||
|
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})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
53
scripts/nodes/library/reroute.js
Normal file
53
scripts/nodes/library/reroute.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transparent pass-through node for redirecting data wires.
|
||||||
|
* Emits no code on its own — the value resolves directly through the input.
|
||||||
|
*/
|
||||||
|
export const rerouteNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "reroute",
|
||||||
|
title: "Reroute",
|
||||||
|
category: "Utility",
|
||||||
|
description: "Redirects a data wire without affecting the value.",
|
||||||
|
searchTags: ["reroute", "redirect", "wire", "relay", "pass", "through"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "value", name: "", direction: "input", kind: "any" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "value", name: "", direction: "output", kind: "any" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evaluateValue: ({ resolveValueInput }) => {
|
||||||
|
return resolveValueInput("value", "nil");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transparent pass-through node for redirecting exec wires.
|
||||||
|
* Emits no code on its own — execution continues directly to the next node.
|
||||||
|
*/
|
||||||
|
export const rerouteExecNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "reroute_exec",
|
||||||
|
title: "Reroute",
|
||||||
|
category: "Utility",
|
||||||
|
description: "Redirects an exec wire without generating any code.",
|
||||||
|
searchTags: ["reroute", "redirect", "exec", "wire", "relay", "pass", "through"],
|
||||||
|
inputs: [
|
||||||
|
{ id: "exec_in", name: "", direction: "input", kind: "exec" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ id: "exec_out", name: "", direction: "output", kind: "exec" },
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitExec: ({ emitNextExec }) => {
|
||||||
|
return emitNextExec("exec_out");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
41
scripts/nodes/library/subtractNumber.js
Normal file
41
scripts/nodes/library/subtractNumber.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { createNodeModule } from "../nodeTypes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts one numeric value from another.
|
||||||
|
*/
|
||||||
|
export const subtractNumberNode = createNodeModule(
|
||||||
|
{
|
||||||
|
id: "subtract_number",
|
||||||
|
title: "Subtract",
|
||||||
|
category: "Math",
|
||||||
|
description: "Subtract one number from another.",
|
||||||
|
searchTags: ["subtract", "math", "minus", "difference", "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})`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -41,7 +41,7 @@ export const extcmdNode = createNodeModule(
|
|||||||
],
|
],
|
||||||
inputs: [
|
inputs: [
|
||||||
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
|
||||||
{ id: "command", name: "Command", direction: "input", kind: "string" },
|
{ id: "command", name: "Command", direction: "input", kind: "string", defaultValue: EXT_CMD_DEFAULT, options: EXT_CMD_OPTIONS },
|
||||||
{ id: "param1", name: "Param 1", direction: "input", kind: "number" },
|
{ id: "param1", name: "Param 1", direction: "input", kind: "number" },
|
||||||
{ id: "param2", name: "Param 2", direction: "input", kind: "number" },
|
{ id: "param2", name: "Param 2", direction: "input", kind: "number" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
* @property {('exec'|'number'|'boolean'|'string'|'table'|'any'|'color')} kind Blueprint connection kind.
|
* @property {('exec'|'number'|'boolean'|'string'|'table'|'any'|'color')} kind Blueprint connection kind.
|
||||||
* @property {string} [description] Optional tooltip text.
|
* @property {string} [description] Optional tooltip text.
|
||||||
* @property {unknown} [defaultValue] Default literal for data pins.
|
* @property {unknown} [defaultValue] Default literal for data pins.
|
||||||
|
* @property {Array<{label: string, value: string}>} [options] Renders an inline select dropdown when the pin is unconnected.
|
||||||
|
* @property {boolean} [spawnLiteralNode] Whether dragging off this pin offers the default literal node. Defaults to the handler's spawnableByDefault.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
13
scripts/types/AnyType.js
Normal file
13
scripts/types/AnyType.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./TypeRegistry.js').TypeDescriptor} TypeDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {TypeDescriptor} */
|
||||||
|
export const AnyType = {
|
||||||
|
id: "any",
|
||||||
|
label: "Any",
|
||||||
|
color: "#ff7f11",
|
||||||
|
isVariable: true,
|
||||||
|
isLocalVariable: false,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
19
scripts/types/BooleanType.js
Normal file
19
scripts/types/BooleanType.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./TypeRegistry.js').TypeDescriptor} TypeDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {TypeDescriptor} */
|
||||||
|
export const BooleanType = {
|
||||||
|
id: "boolean",
|
||||||
|
label: "Boolean",
|
||||||
|
color: "#ff4f4f",
|
||||||
|
isVariable: true,
|
||||||
|
isLocalVariable: true,
|
||||||
|
defaultValue: false,
|
||||||
|
variablePropertyKey: "valueBoolean",
|
||||||
|
compatibleWith: [
|
||||||
|
{ type: "string", convert: (v) => v ? "true" : "false" },
|
||||||
|
{ type: "number", convert: (v) => v ? 1 : 0 },
|
||||||
|
{ type: "color", convert: (v) => v ? 7 : 0 }
|
||||||
|
],
|
||||||
|
};
|
||||||
18
scripts/types/ColorType.js
Normal file
18
scripts/types/ColorType.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./TypeRegistry.js').TypeDescriptor} TypeDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {TypeDescriptor} */
|
||||||
|
export const ColorType = {
|
||||||
|
id: "color",
|
||||||
|
label: "Color",
|
||||||
|
color: "#5b8ef5",
|
||||||
|
isVariable: true,
|
||||||
|
isLocalVariable: true,
|
||||||
|
defaultValue: 7,
|
||||||
|
variablePropertyKey: "valueColor",
|
||||||
|
compatibleWith: [
|
||||||
|
{ type: "number", convert: (v) => Number(v) },
|
||||||
|
{ type: "boolean", convert: (v) => v !== 0 }
|
||||||
|
],
|
||||||
|
};
|
||||||
13
scripts/types/ExecType.js
Normal file
13
scripts/types/ExecType.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Type descriptor for execution flow pins.
|
||||||
|
*
|
||||||
|
* @type {import('./TypeRegistry.js').TypeDescriptor}
|
||||||
|
*/
|
||||||
|
export const ExecType = {
|
||||||
|
id: "exec",
|
||||||
|
label: "Exec",
|
||||||
|
color: "#ffffff",
|
||||||
|
isVariable: false,
|
||||||
|
isLocalVariable: false,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
17
scripts/types/NumberType.js
Normal file
17
scripts/types/NumberType.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./TypeRegistry.js').TypeDescriptor} TypeDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {TypeDescriptor} */
|
||||||
|
export const NumberType = {
|
||||||
|
id: "number",
|
||||||
|
label: "Number",
|
||||||
|
color: "#3ee581",
|
||||||
|
isVariable: true,
|
||||||
|
isLocalVariable: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
variablePropertyKey: "valueNumber",
|
||||||
|
compatibleWith: [
|
||||||
|
{ type: "color", convert: (v) => Math.floor(Number(v)) % 16 },
|
||||||
|
],
|
||||||
|
};
|
||||||
14
scripts/types/StringType.js
Normal file
14
scripts/types/StringType.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./TypeRegistry.js').TypeDescriptor} TypeDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {TypeDescriptor} */
|
||||||
|
export const StringType = {
|
||||||
|
id: "string",
|
||||||
|
label: "String",
|
||||||
|
color: "#ff66ff",
|
||||||
|
isVariable: true,
|
||||||
|
isLocalVariable: true,
|
||||||
|
defaultValue: "",
|
||||||
|
variablePropertyKey: "valueString",
|
||||||
|
};
|
||||||
15
scripts/types/TableType.js
Normal file
15
scripts/types/TableType.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('./TypeRegistry.js').TypeDescriptor} TypeDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {TypeDescriptor} */
|
||||||
|
export const TableType = {
|
||||||
|
id: "table",
|
||||||
|
label: "Table",
|
||||||
|
color: "#5ec4ff",
|
||||||
|
isVariable: true,
|
||||||
|
isLocalVariable: true,
|
||||||
|
defaultValue: [],
|
||||||
|
localDefaultValue: "{}",
|
||||||
|
variablePropertyKey: "valueTable",
|
||||||
|
};
|
||||||
165
scripts/types/TypeRegistry.js
Normal file
165
scripts/types/TypeRegistry.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { AnyType } from "./AnyType.js";
|
||||||
|
import { NumberType } from "./NumberType.js";
|
||||||
|
import { StringType } from "./StringType.js";
|
||||||
|
import { BooleanType } from "./BooleanType.js";
|
||||||
|
import { TableType } from "./TableType.js";
|
||||||
|
import { ColorType } from "./ColorType.js";
|
||||||
|
import { ExecType } from "./ExecType.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {'any'|'number'|'string'|'boolean'|'table'|'color'} VariableType
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TypeCompatibility
|
||||||
|
* @property {string} type Target type ID this type can connect to.
|
||||||
|
* @property {(value: unknown) => unknown} [convert] Optional conversion function.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TypeDescriptor
|
||||||
|
* @property {string} id Type identifier.
|
||||||
|
* @property {string} label Display label.
|
||||||
|
* @property {string} color Accent color used for pins, wires, and variable labels (hex or CSS variable).
|
||||||
|
* @property {string} [icon] Optional icon character or symbol shown in type labels.
|
||||||
|
* @property {boolean} isVariable Whether usable as a workspace variable type.
|
||||||
|
* @property {boolean} isLocalVariable Whether usable as a local variable node type.
|
||||||
|
* @property {unknown} defaultValue Default value for workspace variables.
|
||||||
|
* @property {unknown} [localDefaultValue] Default for local variable nodes when different from defaultValue.
|
||||||
|
* @property {string} [variablePropertyKey] Property key used in local variable nodes.
|
||||||
|
* @property {ReadonlyArray<TypeCompatibility>} [compatibleWith] Types this type can implicitly convert to.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {ReadonlyArray<TypeDescriptor>} */
|
||||||
|
export const ALL_TYPES = [
|
||||||
|
ExecType,
|
||||||
|
AnyType,
|
||||||
|
NumberType,
|
||||||
|
StringType,
|
||||||
|
BooleanType,
|
||||||
|
TableType,
|
||||||
|
ColorType,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @type {Map<string, TypeDescriptor>} */
|
||||||
|
const TYPE_MAP = new Map(ALL_TYPES.map((t) => [t.id, t]));
|
||||||
|
|
||||||
|
/** @type {ReadonlySet<string>} */
|
||||||
|
export const LOCAL_VARIABLE_TYPE_IDS = new Set(
|
||||||
|
ALL_TYPES.filter((t) => t.isLocalVariable).map((t) => t.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns type options available for workspace variable type dropdowns.
|
||||||
|
*
|
||||||
|
* @returns {Array<{ value: string, label: string }>}
|
||||||
|
*/
|
||||||
|
export function getVariableTypeOptions() {
|
||||||
|
return ALL_TYPES.filter((t) => t.isVariable).map((t) => ({ value: t.id, label: t.label }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns type options available for local variable node type dropdowns.
|
||||||
|
*
|
||||||
|
* @returns {Array<{ value: string, label: string }>}
|
||||||
|
*/
|
||||||
|
export function getLocalVariableTypeOptions() {
|
||||||
|
return ALL_TYPES.filter((t) => t.isLocalVariable).map((t) => ({ value: t.id, label: t.label }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes an arbitrary value to a valid registered type id, falling back to "any".
|
||||||
|
*
|
||||||
|
* @param {unknown} value Candidate type value.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function normalizeVariableType(value) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (TYPE_MAP.has(normalized)) return normalized;
|
||||||
|
}
|
||||||
|
return "any";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default value for a workspace variable of the given type.
|
||||||
|
*
|
||||||
|
* @param {string} type Type identifier.
|
||||||
|
* @returns {unknown}
|
||||||
|
*/
|
||||||
|
export function defaultVariableValue(type) {
|
||||||
|
return TYPE_MAP.get(type)?.defaultValue ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default value for a local variable node property of the given type.
|
||||||
|
*
|
||||||
|
* @param {string} type Type identifier.
|
||||||
|
* @returns {unknown}
|
||||||
|
*/
|
||||||
|
export function localVariableDefault(type) {
|
||||||
|
const desc = TYPE_MAP.get(type);
|
||||||
|
if (!desc) return 0;
|
||||||
|
return desc.localDefaultValue !== undefined ? desc.localDefaultValue : desc.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the property key used to store the typed value in a local variable node.
|
||||||
|
*
|
||||||
|
* @param {string} type Type identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function variablePropertyKey(type) {
|
||||||
|
return TYPE_MAP.get(type)?.variablePropertyKey ?? "valueNumber";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the accent color for the given type (used for pins, wires, and variable labels).
|
||||||
|
*
|
||||||
|
* @param {string} type Type identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getTypeColor(type) {
|
||||||
|
return TYPE_MAP.get(type)?.color ?? "#ffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if two types are compatible for pin connections.
|
||||||
|
*
|
||||||
|
* @param {string} outputType The output pin type.
|
||||||
|
* @param {string} inputType The input pin type.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function areTypesCompatible(outputType, inputType) {
|
||||||
|
if (inputType === "any" || outputType === "any") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (outputType === inputType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const outputDesc = TYPE_MAP.get(outputType);
|
||||||
|
if (outputDesc?.compatibleWith?.some((c) => c.type === inputType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const inputDesc = TYPE_MAP.get(inputType);
|
||||||
|
if (inputDesc?.compatibleWith?.some((c) => c.type === outputType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the conversion function for converting from one type to another.
|
||||||
|
*
|
||||||
|
* @param {string} fromType Source type ID.
|
||||||
|
* @param {string} toType Target type ID.
|
||||||
|
* @returns {((value: unknown) => unknown) | undefined}
|
||||||
|
*/
|
||||||
|
export function getTypeConverter(fromType, toType) {
|
||||||
|
if (fromType === toType) {
|
||||||
|
return (v) => v;
|
||||||
|
}
|
||||||
|
const fromDesc = TYPE_MAP.get(fromType);
|
||||||
|
const compat = fromDesc?.compatibleWith?.find((c) => c.type === toType);
|
||||||
|
return compat?.convert;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NodeGraph } from "../core/NodeGraph.js";
|
import { NodeGraph } from "../core/NodeGraph.js";
|
||||||
|
import { createColorSwatchPicker } from "./ColorSwatchPicker.js";
|
||||||
import { WorkspaceGeometry } from "./workspace/WorkspaceGeometry.js";
|
import { WorkspaceGeometry } from "./workspace/WorkspaceGeometry.js";
|
||||||
import { WorkspaceDragManager } from "./workspace/WorkspaceDragManager.js";
|
import { WorkspaceDragManager } from "./workspace/WorkspaceDragManager.js";
|
||||||
import { WorkspaceHistoryManager } from "./workspace/WorkspaceHistoryManager.js";
|
import { WorkspaceHistoryManager } from "./workspace/WorkspaceHistoryManager.js";
|
||||||
@@ -18,25 +19,18 @@ import { renderVariableList } from "./workspace/renderVariableList.js";
|
|||||||
import {
|
import {
|
||||||
WORKSPACE_SHORTCUTS,
|
WORKSPACE_SHORTCUTS,
|
||||||
} from "./WorkspaceSpawnShortcuts.js";
|
} from "./WorkspaceSpawnShortcuts.js";
|
||||||
|
import {
|
||||||
|
normalizeVariableType,
|
||||||
|
getVariableTypeOptions,
|
||||||
|
defaultVariableValue as getDefaultVariableValue,
|
||||||
|
getTypeColor,
|
||||||
|
areTypesCompatible,
|
||||||
|
} from "../types/TypeRegistry.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {'any'|'number'|'string'|'boolean'|'table'|'color'} VariableType
|
* @typedef {import('../types/TypeRegistry.js').VariableType} VariableType
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {Array<{ value: VariableType, label: string }>} */
|
|
||||||
const VARIABLE_TYPE_DEFINITIONS = [
|
|
||||||
{ value: "any", label: "Any" },
|
|
||||||
{ value: "number", label: "Number" },
|
|
||||||
{ value: "string", label: "String" },
|
|
||||||
{ value: "boolean", label: "Boolean" },
|
|
||||||
{ value: "table", label: "Table" },
|
|
||||||
{ value: "color", label: "Color" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const SUPPORTED_VARIABLE_TYPES = new Set(
|
|
||||||
VARIABLE_TYPE_DEFINITIONS.map((entry) => entry.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** @typedef {import('./WorkspaceSpawnShortcuts.js').WorkspaceShortcut} WorkspaceShortcut */
|
/** @typedef {import('./WorkspaceSpawnShortcuts.js').WorkspaceShortcut} WorkspaceShortcut */
|
||||||
/** @typedef {import('./WorkspaceSpawnShortcuts.js').WorkspaceShortcutModifier} WorkspaceShortcutModifier */
|
/** @typedef {import('./WorkspaceSpawnShortcuts.js').WorkspaceShortcutModifier} WorkspaceShortcutModifier */
|
||||||
|
|
||||||
@@ -88,6 +82,7 @@ const SUPPORTED_VARIABLE_TYPES = new Set(
|
|||||||
* @property {HTMLUListElement} eventList Event summary list element.
|
* @property {HTMLUListElement} eventList Event summary list element.
|
||||||
* @property {HTMLUListElement} variableList Variable summary list element.
|
* @property {HTMLUListElement} variableList Variable summary list element.
|
||||||
* @property {HTMLButtonElement} addVariableButton Global variable creation button.
|
* @property {HTMLButtonElement} addVariableButton Global variable creation button.
|
||||||
|
* @property {HTMLButtonElement} saveButton Save action button.
|
||||||
* @property {HTMLButtonElement} projectSettingsButton Project settings toggle.
|
* @property {HTMLButtonElement} projectSettingsButton Project settings toggle.
|
||||||
* @property {HTMLButtonElement} paletteToggleButton Palette toggle control.
|
* @property {HTMLButtonElement} paletteToggleButton Palette toggle control.
|
||||||
* @property {HTMLElement} appBodyElement Layout container hosting the workspace panes.
|
* @property {HTMLElement} appBodyElement Layout container hosting the workspace panes.
|
||||||
@@ -146,6 +141,8 @@ export class BlueprintWorkspace {
|
|||||||
this.variableListElement = options.variableList;
|
this.variableListElement = options.variableList;
|
||||||
this.addVariableButton = options.addVariableButton;
|
this.addVariableButton = options.addVariableButton;
|
||||||
/** @type {HTMLButtonElement} */
|
/** @type {HTMLButtonElement} */
|
||||||
|
this.saveButton = options.saveButton;
|
||||||
|
/** @type {HTMLButtonElement} */
|
||||||
this.projectSettingsButton = options.projectSettingsButton;
|
this.projectSettingsButton = options.projectSettingsButton;
|
||||||
/** @type {HTMLButtonElement} */
|
/** @type {HTMLButtonElement} */
|
||||||
this.frameSelectionButton = options.frameSelectionButton;
|
this.frameSelectionButton = options.frameSelectionButton;
|
||||||
@@ -180,6 +177,10 @@ export class BlueprintWorkspace {
|
|||||||
this.connectionRefreshFrame = null;
|
this.connectionRefreshFrame = null;
|
||||||
/** @type {number | null} */
|
/** @type {number | null} */
|
||||||
this.periodicUpdateTimer = null;
|
this.periodicUpdateTimer = null;
|
||||||
|
/** @type {number | null} */
|
||||||
|
this.saveFeedbackTimer = null;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.hasPendingSave = false;
|
||||||
|
|
||||||
this.graph = new NodeGraph();
|
this.graph = new NodeGraph();
|
||||||
/** @type {Map<string, WorkspaceVariable>} */
|
/** @type {Map<string, WorkspaceVariable>} */
|
||||||
@@ -398,6 +399,7 @@ export class BlueprintWorkspace {
|
|||||||
|
|
||||||
this.navigationManager.updateZoomDisplay();
|
this.navigationManager.updateZoomDisplay();
|
||||||
this.navigationManager.setBackgroundOffset({ x: 0, y: 0 });
|
this.navigationManager.setBackgroundOffset({ x: 0, y: 0 });
|
||||||
|
this.#resetSaveButtonAppearance();
|
||||||
|
|
||||||
if (typeof this.generator.setProjectSettings === "function") {
|
if (typeof this.generator.setProjectSettings === "function") {
|
||||||
this.generator.setProjectSettings({ ...this.projectSettings });
|
this.generator.setProjectSettings({ ...this.projectSettings });
|
||||||
@@ -445,6 +447,101 @@ export class BlueprintWorkspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the active save feedback timer.
|
||||||
|
*/
|
||||||
|
#clearSaveFeedbackTimer() {
|
||||||
|
if (this.saveFeedbackTimer !== null && typeof window !== "undefined") {
|
||||||
|
window.clearTimeout(this.saveFeedbackTimer);
|
||||||
|
this.saveFeedbackTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the save button to its current base state.
|
||||||
|
*/
|
||||||
|
#resetSaveButtonAppearance() {
|
||||||
|
const button = this.saveButton;
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.classList.remove("is-busy", "is-success", "is-error", "is-pending");
|
||||||
|
button.disabled = !this.persistenceManager?.isStorageAvailable;
|
||||||
|
button.setAttribute(
|
||||||
|
"title",
|
||||||
|
this.persistenceManager?.isStorageAvailable
|
||||||
|
? this.hasPendingSave
|
||||||
|
? "Unsaved changes"
|
||||||
|
: "Workspace saved"
|
||||||
|
: "Saving is unavailable"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.hasPendingSave) {
|
||||||
|
button.classList.add("is-pending");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies transient visual state to the save button.
|
||||||
|
*
|
||||||
|
* @param {{ status: 'idle'|'pending'|'saving'|'saved'|'error', source: 'manual'|'auto' }} state
|
||||||
|
*/
|
||||||
|
#updateSaveButtonAppearance(state) {
|
||||||
|
const button = this.saveButton;
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#clearSaveFeedbackTimer();
|
||||||
|
this.#resetSaveButtonAppearance();
|
||||||
|
|
||||||
|
if (!this.persistenceManager?.isStorageAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "pending") {
|
||||||
|
button.classList.add("is-pending");
|
||||||
|
button.setAttribute("title", "Unsaved changes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "saving") {
|
||||||
|
button.classList.remove("is-pending");
|
||||||
|
button.classList.add("is-busy");
|
||||||
|
button.disabled = true;
|
||||||
|
button.setAttribute("title", "Saving workspace");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "saved") {
|
||||||
|
button.classList.remove("is-pending");
|
||||||
|
button.classList.add("is-success");
|
||||||
|
button.setAttribute(
|
||||||
|
"title",
|
||||||
|
state.source === "auto" ? "Workspace autosaved" : "Workspace saved"
|
||||||
|
);
|
||||||
|
this.saveFeedbackTimer = window.setTimeout(() => {
|
||||||
|
this.#resetSaveButtonAppearance();
|
||||||
|
this.saveFeedbackTimer = null;
|
||||||
|
}, 1400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "error") {
|
||||||
|
button.classList.remove("is-pending");
|
||||||
|
button.classList.add("is-error");
|
||||||
|
button.setAttribute("title", "Workspace save failed");
|
||||||
|
this.saveFeedbackTimer = window.setTimeout(() => {
|
||||||
|
this.#resetSaveButtonAppearance();
|
||||||
|
this.saveFeedbackTimer = null;
|
||||||
|
}, 1800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#resetSaveButtonAppearance();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverts the most recent checkpoint.
|
* Reverts the most recent checkpoint.
|
||||||
*/
|
*/
|
||||||
@@ -466,6 +563,41 @@ export class BlueprintWorkspace {
|
|||||||
return this.#refreshLuaOutput();
|
return this.#refreshLuaOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists the current workspace state immediately.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
saveWorkspace() {
|
||||||
|
if (!this.persistenceManager?.isStorageAvailable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.persistenceManager.persistImmediately("manual");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflects persistence lifecycle changes in the save button state.
|
||||||
|
*
|
||||||
|
* @param {{ status: 'idle'|'pending'|'saving'|'saved'|'error', source: 'manual'|'auto' }} state
|
||||||
|
*/
|
||||||
|
handlePersistenceStateChange(state) {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "pending") {
|
||||||
|
this.hasPendingSave = true;
|
||||||
|
} else if (state.status === "saved") {
|
||||||
|
this.hasPendingSave = false;
|
||||||
|
} else if (state.status === "error") {
|
||||||
|
this.hasPendingSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#updateSaveButtonAppearance(state);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to graph lifecycle changes.
|
* Subscribes to graph lifecycle changes.
|
||||||
*/
|
*/
|
||||||
@@ -699,6 +831,12 @@ export class BlueprintWorkspace {
|
|||||||
* Registers DOM event listeners for interactivity.
|
* Registers DOM event listeners for interactivity.
|
||||||
*/
|
*/
|
||||||
#bindUiEvents() {
|
#bindUiEvents() {
|
||||||
|
if (this.saveButton) {
|
||||||
|
this.saveButton.addEventListener("click", () => {
|
||||||
|
this.saveWorkspace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.paletteToggleButton) {
|
if (this.paletteToggleButton) {
|
||||||
this.paletteToggleButton.addEventListener("click", () => {
|
this.paletteToggleButton.addEventListener("click", () => {
|
||||||
this.#togglePaletteVisibility();
|
this.#togglePaletteVisibility();
|
||||||
@@ -1755,18 +1893,7 @@ export class BlueprintWorkspace {
|
|||||||
* @returns {VariableDefaultValue}
|
* @returns {VariableDefaultValue}
|
||||||
*/
|
*/
|
||||||
#defaultVariableValue(type) {
|
#defaultVariableValue(type) {
|
||||||
switch (type) {
|
return /** @type {VariableDefaultValue} */ (getDefaultVariableValue(type));
|
||||||
case "number":
|
|
||||||
return 0;
|
|
||||||
case "string":
|
|
||||||
return "";
|
|
||||||
case "table":
|
|
||||||
return [];
|
|
||||||
case "boolean":
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1799,6 +1926,11 @@ export class BlueprintWorkspace {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
case "color": {
|
||||||
|
const n = typeof value === "number" ? value : Number(value);
|
||||||
|
const safe = Number.isFinite(n) ? Math.round(n) : 7;
|
||||||
|
return Math.max(0, Math.min(15, safe));
|
||||||
|
}
|
||||||
case "boolean": {
|
case "boolean": {
|
||||||
if (typeof value === "boolean") {
|
if (typeof value === "boolean") {
|
||||||
return value;
|
return value;
|
||||||
@@ -2133,6 +2265,18 @@ export class BlueprintWorkspace {
|
|||||||
this.#setVariableDefault(variable.id, defaultToggle.checked);
|
this.#setVariableDefault(variable.id, defaultToggle.checked);
|
||||||
});
|
});
|
||||||
defaultField.appendChild(defaultToggle);
|
defaultField.appendChild(defaultToggle);
|
||||||
|
} else if (variable.type === "color") {
|
||||||
|
const rawDefault = variable.defaultValue;
|
||||||
|
const n = typeof rawDefault === "number" ? rawDefault : Number(rawDefault);
|
||||||
|
const initialIndex = Number.isFinite(n) ? Math.max(0, Math.min(15, Math.round(n))) : 7;
|
||||||
|
const picker = createColorSwatchPicker({
|
||||||
|
id: defaultId,
|
||||||
|
value: initialIndex,
|
||||||
|
onChange: (index) => {
|
||||||
|
this.#setVariableDefault(variable.id, index);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
defaultField.appendChild(picker.element);
|
||||||
} else if (variable.type === "number") {
|
} else if (variable.type === "number") {
|
||||||
const defaultInput = document.createElement("input");
|
const defaultInput = document.createElement("input");
|
||||||
defaultInput.type = "number";
|
defaultInput.type = "number";
|
||||||
@@ -2607,6 +2751,7 @@ export class BlueprintWorkspace {
|
|||||||
option.type = "button";
|
option.type = "button";
|
||||||
option.className = "variable-type-option";
|
option.className = "variable-type-option";
|
||||||
option.dataset.variableType = entry.value;
|
option.dataset.variableType = entry.value;
|
||||||
|
option.style.setProperty("--overview-variable-color", getTypeColor(entry.value));
|
||||||
option.textContent = entry.label;
|
option.textContent = entry.label;
|
||||||
option.setAttribute("role", "menuitemradio");
|
option.setAttribute("role", "menuitemradio");
|
||||||
option.setAttribute(
|
option.setAttribute(
|
||||||
@@ -3028,12 +3173,8 @@ export class BlueprintWorkspace {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = workspaceElement.getBoundingClientRect();
|
// Use dragManager.clientToWorkspace for proper coordinate conversion
|
||||||
const screenPoint = {
|
const worldPoint = this.dragManager.clientToWorkspace(event.clientX, event.clientY);
|
||||||
x: Math.max(0, Math.min(rect.width, event.clientX - rect.left)),
|
|
||||||
y: Math.max(0, Math.min(rect.height, event.clientY - rect.top)),
|
|
||||||
};
|
|
||||||
const worldPoint = navigator.screenPointToWorld(screenPoint);
|
|
||||||
return this.addNode(shortcut.definitionId, worldPoint);
|
return this.addNode(shortcut.definitionId, worldPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3406,9 +3547,7 @@ export class BlueprintWorkspace {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
#formatVariableType(type) {
|
#formatVariableType(type) {
|
||||||
const entry = VARIABLE_TYPE_DEFINITIONS.find(
|
const entry = getVariableTypeOptions().find((opt) => opt.value === type);
|
||||||
(definition) => definition.value === type
|
|
||||||
);
|
|
||||||
return entry ? entry.label : "Any";
|
return entry ? entry.label : "Any";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3419,17 +3558,7 @@ export class BlueprintWorkspace {
|
|||||||
* @returns {VariableType}
|
* @returns {VariableType}
|
||||||
*/
|
*/
|
||||||
#normalizeVariableType(value) {
|
#normalizeVariableType(value) {
|
||||||
if (typeof value === "string") {
|
return /** @type {VariableType} */ (normalizeVariableType(value));
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (
|
|
||||||
SUPPORTED_VARIABLE_TYPES.has(
|
|
||||||
/** @type {VariableType} */ (normalized)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return /** @type {VariableType} */ (normalized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "any";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3438,7 +3567,7 @@ export class BlueprintWorkspace {
|
|||||||
* @returns {Array<{ value: VariableType, label: string }>}
|
* @returns {Array<{ value: VariableType, label: string }>}
|
||||||
*/
|
*/
|
||||||
#getVariableTypeOptions() {
|
#getVariableTypeOptions() {
|
||||||
return VARIABLE_TYPE_DEFINITIONS.map((entry) => ({ ...entry }));
|
return getVariableTypeOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3641,12 +3770,14 @@ export class BlueprintWorkspace {
|
|||||||
node.type === "set_local_var" || node.type === "get_local_var";
|
node.type === "set_local_var" || node.type === "get_local_var";
|
||||||
article.classList.add("blueprint-node--variable");
|
article.classList.add("blueprint-node--variable");
|
||||||
article.dataset.variableType = binding.type;
|
article.dataset.variableType = binding.type;
|
||||||
|
article.style.setProperty("--overview-variable-color", getTypeColor(binding.type));
|
||||||
article.dataset.variableRole = role;
|
article.dataset.variableRole = role;
|
||||||
article.dataset.variableScope = isLocal ? "local" : "global";
|
article.dataset.variableScope = isLocal ? "local" : "global";
|
||||||
|
|
||||||
header.classList.add("node-header--variable");
|
header.classList.add("node-header--variable");
|
||||||
header.setAttribute("data-variable-role", role);
|
header.setAttribute("data-variable-role", role);
|
||||||
header.setAttribute("data-variable-type", binding.type);
|
header.setAttribute("data-variable-type", binding.type);
|
||||||
|
header.style.setProperty("--overview-variable-color", getTypeColor(binding.type));
|
||||||
header.setAttribute("data-variable-scope", isLocal ? "local" : "global");
|
header.setAttribute("data-variable-scope", isLocal ? "local" : "global");
|
||||||
const prefixBase = role === "set" ? "Set" : "Get";
|
const prefixBase = role === "set" ? "Set" : "Get";
|
||||||
const prefix = isLocal ? `${prefixBase} Local` : prefixBase;
|
const prefix = isLocal ? `${prefixBase} Local` : prefixBase;
|
||||||
@@ -4957,7 +5088,7 @@ export class BlueprintWorkspace {
|
|||||||
* Spawns a node at the requested context menu position.
|
* Spawns a node at the requested context menu position.
|
||||||
*
|
*
|
||||||
* @param {PaletteNodeDefinition} definition Node definition to instantiate.
|
* @param {PaletteNodeDefinition} definition Node definition to instantiate.
|
||||||
* @param {{ x: number, y: number }} spawnPosition Workspace-relative spawn coordinates.
|
* @param {{ x: number, y: number }} spawnPosition Screen-relative spawn coordinates (relative to workspace element).
|
||||||
* @returns {import('../core/BlueprintNode.js').BlueprintNode | null}
|
* @returns {import('../core/BlueprintNode.js').BlueprintNode | null}
|
||||||
*/
|
*/
|
||||||
spawnNodeFromContextMenu(definition, spawnPosition) {
|
spawnNodeFromContextMenu(definition, spawnPosition) {
|
||||||
@@ -4965,7 +5096,14 @@ export class BlueprintWorkspace {
|
|||||||
x: Math.max(0, spawnPosition.x),
|
x: Math.max(0, spawnPosition.x),
|
||||||
y: Math.max(0, spawnPosition.y),
|
y: Math.max(0, spawnPosition.y),
|
||||||
};
|
};
|
||||||
const worldSpawn = this.navigationManager.screenPointToWorld(clamped);
|
// Convert screen-relative position to world coordinates
|
||||||
|
const zoom = this.navigationManager?.getZoomLevel() ?? this.zoomLevel ?? 1;
|
||||||
|
const clampedZoom = Math.max(0.01, zoom);
|
||||||
|
const offset = this.navigationManager?.getEffectiveOffset() ?? { x: 0, y: 0 };
|
||||||
|
const worldSpawn = {
|
||||||
|
x: clamped.x / clampedZoom - offset.x,
|
||||||
|
y: clamped.y / clampedZoom - offset.y,
|
||||||
|
};
|
||||||
const spawn = WorkspaceGeometry.snapPositionToGrid(this, worldSpawn);
|
const spawn = WorkspaceGeometry.snapPositionToGrid(this, worldSpawn);
|
||||||
const pendingConnectionSnapshot = this.pendingConnectionSpawn
|
const pendingConnectionSnapshot = this.pendingConnectionSpawn
|
||||||
? {
|
? {
|
||||||
@@ -5500,10 +5638,88 @@ export class BlueprintWorkspace {
|
|||||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
path.classList.add("connection-path");
|
path.classList.add("connection-path");
|
||||||
path.dataset.type = kind;
|
path.dataset.type = kind;
|
||||||
|
path.style.stroke = getTypeColor(kind);
|
||||||
this.connectionLayer.appendChild(path);
|
this.connectionLayer.appendChild(path);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SVG defs element on the connection layer, creating it if absent.
|
||||||
|
*
|
||||||
|
* @returns {SVGDefsElement}
|
||||||
|
*/
|
||||||
|
#getOrCreateSvgDefs() {
|
||||||
|
let defs = this.connectionLayer.querySelector("defs");
|
||||||
|
if (!defs) {
|
||||||
|
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||||||
|
this.connectionLayer.prepend(defs);
|
||||||
|
}
|
||||||
|
return /** @type {SVGDefsElement} */ (defs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates a linearGradient element for a cross-type wire.
|
||||||
|
*
|
||||||
|
* @param {SVGDefsElement} defs Defs container.
|
||||||
|
* @param {string} connectionId Connection identifier.
|
||||||
|
* @param {string} fromKind Source pin kind.
|
||||||
|
* @param {string} toKind Target pin kind.
|
||||||
|
* @param {{x:number,y:number}} start Gradient start point.
|
||||||
|
* @param {{x:number,y:number}} end Gradient end point.
|
||||||
|
* @returns {string} The gradient element id.
|
||||||
|
*/
|
||||||
|
#upsertConnectionGradient(defs, connectionId, fromKind, toKind, start, end) {
|
||||||
|
const gradId = `cgrad-${connectionId}`;
|
||||||
|
let grad = defs.querySelector(`#${CSS.escape(gradId)}`);
|
||||||
|
if (!grad) {
|
||||||
|
grad = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"linearGradient"
|
||||||
|
);
|
||||||
|
grad.id = gradId;
|
||||||
|
grad.setAttribute("gradientUnits", "userSpaceOnUse");
|
||||||
|
const stop1 = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"stop"
|
||||||
|
);
|
||||||
|
stop1.setAttribute("offset", "0%");
|
||||||
|
const stop2 = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"stop"
|
||||||
|
);
|
||||||
|
stop2.setAttribute("offset", "100%");
|
||||||
|
grad.appendChild(stop1);
|
||||||
|
grad.appendChild(stop2);
|
||||||
|
defs.appendChild(grad);
|
||||||
|
}
|
||||||
|
|
||||||
|
grad.setAttribute("x1", String(start.x));
|
||||||
|
grad.setAttribute("y1", String(start.y));
|
||||||
|
grad.setAttribute("x2", String(end.x));
|
||||||
|
grad.setAttribute("y2", String(end.y));
|
||||||
|
|
||||||
|
const style = getComputedStyle(this.workspaceElement);
|
||||||
|
const stops = grad.querySelectorAll("stop");
|
||||||
|
stops[0].setAttribute(
|
||||||
|
"stop-color",
|
||||||
|
this.#getPinKindColor(style, fromKind)
|
||||||
|
);
|
||||||
|
stops[1].setAttribute("stop-color", this.#getPinKindColor(style, toKind));
|
||||||
|
|
||||||
|
return gradId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the color for a given pin kind.
|
||||||
|
*
|
||||||
|
* @param {CSSStyleDeclaration} _style Computed style (unused, kept for API compatibility).
|
||||||
|
* @param {string} kind Pin kind identifier.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#getPinKindColor(_style, kind) {
|
||||||
|
return getTypeColor(kind);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests a connection re-render on the next animation frame.
|
* Requests a connection re-render on the next animation frame.
|
||||||
*/
|
*/
|
||||||
@@ -5579,6 +5795,7 @@ export class BlueprintWorkspace {
|
|||||||
this.connectionLayer.removeAttribute("preserveAspectRatio");
|
this.connectionLayer.removeAttribute("preserveAspectRatio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const defs = this.#getOrCreateSvgDefs();
|
||||||
const activeIds = new Set();
|
const activeIds = new Set();
|
||||||
this.graph.getConnections().forEach((connection) => {
|
this.graph.getConnections().forEach((connection) => {
|
||||||
activeIds.add(connection.id);
|
activeIds.add(connection.id);
|
||||||
@@ -5588,7 +5805,6 @@ export class BlueprintWorkspace {
|
|||||||
this.connectionElements.set(connection.id, path);
|
this.connectionElements.set(connection.id, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
path.dataset.type = connection.kind;
|
|
||||||
path.dataset.connectionId = connection.id;
|
path.dataset.connectionId = connection.id;
|
||||||
const geometry = this.#computeConnectionGeometry(
|
const geometry = this.#computeConnectionGeometry(
|
||||||
connection.from,
|
connection.from,
|
||||||
@@ -5605,12 +5821,47 @@ export class BlueprintWorkspace {
|
|||||||
} ${end.x - controlOffset} ${end.y} ${end.x} ${end.y}`;
|
} ${end.x - controlOffset} ${end.y} ${end.x} ${end.y}`;
|
||||||
path.dataset.active = "false";
|
path.dataset.active = "false";
|
||||||
path.setAttribute("d", d);
|
path.setAttribute("d", d);
|
||||||
|
|
||||||
|
const fromPin = this.graph.nodes
|
||||||
|
.get(connection.from.nodeId)
|
||||||
|
?.getPin(connection.from.pinId);
|
||||||
|
const toPin = this.graph.nodes
|
||||||
|
.get(connection.to.nodeId)
|
||||||
|
?.getPin(connection.to.pinId);
|
||||||
|
const fromKind = fromPin?.kind;
|
||||||
|
const toKind = toPin?.kind;
|
||||||
|
|
||||||
|
if (
|
||||||
|
fromKind &&
|
||||||
|
toKind &&
|
||||||
|
fromKind !== toKind &&
|
||||||
|
fromKind !== "any" &&
|
||||||
|
toKind !== "any"
|
||||||
|
) {
|
||||||
|
path.dataset.type = "mixed";
|
||||||
|
const gradId = this.#upsertConnectionGradient(
|
||||||
|
defs,
|
||||||
|
connection.id,
|
||||||
|
fromKind,
|
||||||
|
toKind,
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
path.style.stroke = "";
|
||||||
|
path.setAttribute("stroke", `url(#${gradId})`);
|
||||||
|
} else {
|
||||||
|
path.dataset.type = connection.kind;
|
||||||
|
path.removeAttribute("stroke");
|
||||||
|
path.style.stroke = getTypeColor(connection.kind);
|
||||||
|
defs.querySelector(`#cgrad-${connection.id}`)?.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.connectionElements.forEach((path, id) => {
|
this.connectionElements.forEach((path, id) => {
|
||||||
if (!activeIds.has(id)) {
|
if (!activeIds.has(id)) {
|
||||||
path.remove();
|
path.remove();
|
||||||
this.connectionElements.delete(id);
|
this.connectionElements.delete(id);
|
||||||
|
defs.querySelector(`#cgrad-${id}`)?.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5886,10 +6137,7 @@ export class BlueprintWorkspace {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
#arePinKindsCompatible(outputKind, inputKind) {
|
#arePinKindsCompatible(outputKind, inputKind) {
|
||||||
if (inputKind === "any" || outputKind === "any") {
|
return areTypesCompatible(outputKind, inputKind);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return outputKind === inputKind;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6197,12 +6445,14 @@ export class BlueprintWorkspace {
|
|||||||
row.className = "custom-event-parameter-row";
|
row.className = "custom-event-parameter-row";
|
||||||
row.dataset.parameterId = parameter.id;
|
row.dataset.parameterId = parameter.id;
|
||||||
row.dataset.parameterType = parameter.type;
|
row.dataset.parameterType = parameter.type;
|
||||||
|
row.style.setProperty("--overview-variable-color", getTypeColor(parameter.type));
|
||||||
|
|
||||||
const typeButton = document.createElement("button");
|
const typeButton = document.createElement("button");
|
||||||
typeButton.type = "button";
|
typeButton.type = "button";
|
||||||
typeButton.className =
|
typeButton.className =
|
||||||
"variable-type-button custom-event-parameter-type-button";
|
"variable-type-button custom-event-parameter-type-button";
|
||||||
typeButton.dataset.variableType = parameter.type;
|
typeButton.dataset.variableType = parameter.type;
|
||||||
|
typeButton.style.setProperty("--overview-variable-color", getTypeColor(parameter.type));
|
||||||
typeButton.setAttribute("aria-haspopup", "menu");
|
typeButton.setAttribute("aria-haspopup", "menu");
|
||||||
typeButton.setAttribute("aria-expanded", "false");
|
typeButton.setAttribute("aria-expanded", "false");
|
||||||
row.appendChild(typeButton);
|
row.appendChild(typeButton);
|
||||||
@@ -6218,7 +6468,9 @@ export class BlueprintWorkspace {
|
|||||||
const display = this.#formatVariableType(normalized);
|
const display = this.#formatVariableType(normalized);
|
||||||
const referenceName = nameInputElement.value.trim() || parameter.id;
|
const referenceName = nameInputElement.value.trim() || parameter.id;
|
||||||
row.dataset.parameterType = normalized;
|
row.dataset.parameterType = normalized;
|
||||||
|
row.style.setProperty("--overview-variable-color", getTypeColor(normalized));
|
||||||
typeButton.dataset.variableType = normalized;
|
typeButton.dataset.variableType = normalized;
|
||||||
|
typeButton.style.setProperty("--overview-variable-color", getTypeColor(normalized));
|
||||||
typeButton.title = `Type: ${display}`;
|
typeButton.title = `Type: ${display}`;
|
||||||
typeButton.setAttribute(
|
typeButton.setAttribute(
|
||||||
"aria-label",
|
"aria-label",
|
||||||
@@ -6489,6 +6741,7 @@ export class BlueprintWorkspace {
|
|||||||
this.connectionElements.forEach((path) => path.remove());
|
this.connectionElements.forEach((path) => path.remove());
|
||||||
this.nodeElements.clear();
|
this.nodeElements.clear();
|
||||||
this.connectionElements.clear();
|
this.connectionElements.clear();
|
||||||
|
this.connectionLayer.querySelector("defs")?.remove();
|
||||||
this.inlineEditorManager.clear();
|
this.inlineEditorManager.clear();
|
||||||
this.nodeDragRotation.clear();
|
this.nodeDragRotation.clear();
|
||||||
this.dragManager.stopNodeRotationDecay();
|
this.dragManager.stopNodeRotationDecay();
|
||||||
@@ -6531,7 +6784,8 @@ export class BlueprintWorkspace {
|
|||||||
entry.type === "string" ||
|
entry.type === "string" ||
|
||||||
entry.type === "boolean" ||
|
entry.type === "boolean" ||
|
||||||
entry.type === "any" ||
|
entry.type === "any" ||
|
||||||
entry.type === "table"
|
entry.type === "table" ||
|
||||||
|
entry.type === "color"
|
||||||
? entry.type
|
? entry.type
|
||||||
: "any";
|
: "any";
|
||||||
const defaultValue = this.#normalizeVariableDefault(
|
const defaultValue = this.#normalizeVariableDefault(
|
||||||
|
|||||||
184
scripts/ui/ColorSwatchPicker.js
Normal file
184
scripts/ui/ColorSwatchPicker.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Ordered PICO-8 palette hex strings, indices 0–15.
|
||||||
|
*
|
||||||
|
* @type {ReadonlyArray<string>}
|
||||||
|
*/
|
||||||
|
const PICO8_COLORS = Object.freeze([
|
||||||
|
'#000002', '#1D2B53', '#7E2553', '#008751',
|
||||||
|
'#AB5236', '#5F574F', '#C2C3C7', '#FFF1E8',
|
||||||
|
'#FF004D', '#FFA300', '#FFEC27', '#00E436',
|
||||||
|
'#29ADFF', '#83769C', '#FF77A8', '#FFCCAA',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ element: HTMLElement, setValue: (index: number) => void }} ColorSwatchPickerInstance
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an inline PICO-8 color swatch picker control.
|
||||||
|
*
|
||||||
|
* Renders a colored trigger button that opens a 4×4 grid of all 16 PICO-8
|
||||||
|
* palette swatches. Clicking a swatch fires `onChange` with the selected
|
||||||
|
* palette index (0–15) and closes the dropdown.
|
||||||
|
*
|
||||||
|
* @param {{
|
||||||
|
* id?: string,
|
||||||
|
* value: number,
|
||||||
|
* onChange: (index: number) => void,
|
||||||
|
* }} options Picker configuration.
|
||||||
|
* @returns {ColorSwatchPickerInstance}
|
||||||
|
*/
|
||||||
|
export function createColorSwatchPicker({ id, value, onChange }) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'color-swatch-picker';
|
||||||
|
|
||||||
|
const trigger = document.createElement('button');
|
||||||
|
trigger.type = 'button';
|
||||||
|
trigger.className = 'color-swatch-trigger';
|
||||||
|
if (id) {
|
||||||
|
trigger.id = id;
|
||||||
|
}
|
||||||
|
trigger.setAttribute('aria-haspopup', 'menu');
|
||||||
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
wrapper.appendChild(trigger);
|
||||||
|
|
||||||
|
const dropdown = document.createElement('div');
|
||||||
|
dropdown.className = 'color-swatch-dropdown';
|
||||||
|
dropdown.setAttribute('role', 'menu');
|
||||||
|
// Appended to body on open so it escapes overflow:hidden on node cards.
|
||||||
|
|
||||||
|
/** @type {Array<HTMLButtonElement>} */
|
||||||
|
const swatchButtons = [];
|
||||||
|
|
||||||
|
/** @type {((event: MouseEvent) => void) | null} */
|
||||||
|
let outsideClickHandler = null;
|
||||||
|
|
||||||
|
/** @type {(() => void) | null} */
|
||||||
|
let scrollAbortHandler = null;
|
||||||
|
|
||||||
|
const repositionDropdown = () => {
|
||||||
|
const rect = trigger.getBoundingClientRect();
|
||||||
|
dropdown.style.top = `${rect.bottom + 4}px`;
|
||||||
|
dropdown.style.left = `${rect.left}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
if (dropdown.isConnected) {
|
||||||
|
dropdown.remove();
|
||||||
|
}
|
||||||
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
if (outsideClickHandler) {
|
||||||
|
document.removeEventListener('click', outsideClickHandler, true);
|
||||||
|
outsideClickHandler = null;
|
||||||
|
}
|
||||||
|
if (scrollAbortHandler) {
|
||||||
|
document.removeEventListener('scroll', scrollAbortHandler, true);
|
||||||
|
scrollAbortHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDropdown = () => {
|
||||||
|
repositionDropdown();
|
||||||
|
document.body.appendChild(dropdown);
|
||||||
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
outsideClickHandler = (event) => {
|
||||||
|
if (
|
||||||
|
!(event.target instanceof Node) ||
|
||||||
|
(!wrapper.contains(event.target) && !dropdown.contains(event.target))
|
||||||
|
) {
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
scrollAbortHandler = () => closeDropdown();
|
||||||
|
document.addEventListener('click', outsideClickHandler, true);
|
||||||
|
document.addEventListener('scroll', scrollAbortHandler, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentIndex = clampColorIndex(value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the trigger button to reflect the supplied palette index.
|
||||||
|
*
|
||||||
|
* @param {number} index Palette index (0–15).
|
||||||
|
*/
|
||||||
|
const updateTrigger = (index) => {
|
||||||
|
const hex = PICO8_COLORS[index] ?? '#000000';
|
||||||
|
trigger.style.backgroundColor = hex;
|
||||||
|
trigger.title = `Color ${index}`;
|
||||||
|
trigger.setAttribute('aria-label', `Color ${index}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
PICO8_COLORS.forEach((hex, index) => {
|
||||||
|
const swatch = document.createElement('button');
|
||||||
|
swatch.type = 'button';
|
||||||
|
swatch.className = 'color-swatch-option';
|
||||||
|
swatch.style.backgroundColor = hex;
|
||||||
|
swatch.dataset.colorIndex = String(index);
|
||||||
|
swatch.title = String(index);
|
||||||
|
swatch.setAttribute('role', 'menuitemradio');
|
||||||
|
swatch.setAttribute('aria-label', `Color ${index}`);
|
||||||
|
swatch.setAttribute('aria-checked', 'false');
|
||||||
|
|
||||||
|
swatch.addEventListener('pointerdown', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
swatch.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const selected = index;
|
||||||
|
closeDropdown();
|
||||||
|
if (selected !== currentIndex) {
|
||||||
|
currentIndex = selected;
|
||||||
|
updateTrigger(selected);
|
||||||
|
swatchButtons.forEach((btn, i) => {
|
||||||
|
btn.setAttribute('aria-checked', i === selected ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
onChange(selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.appendChild(swatch);
|
||||||
|
swatchButtons.push(swatch);
|
||||||
|
});
|
||||||
|
|
||||||
|
trigger.addEventListener('pointerdown', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
trigger.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (dropdown.isConnected) {
|
||||||
|
closeDropdown();
|
||||||
|
} else {
|
||||||
|
openDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the picker display without firing `onChange`.
|
||||||
|
*
|
||||||
|
* @param {number} newIndex Palette index (0–15).
|
||||||
|
*/
|
||||||
|
const setValue = (newIndex) => {
|
||||||
|
currentIndex = clampColorIndex(newIndex);
|
||||||
|
updateTrigger(currentIndex);
|
||||||
|
swatchButtons.forEach((btn, i) => {
|
||||||
|
btn.setAttribute('aria-checked', i === currentIndex ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setValue(currentIndex);
|
||||||
|
|
||||||
|
return { element: wrapper, setValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps an arbitrary value to a valid PICO-8 palette index (0–15).
|
||||||
|
*
|
||||||
|
* @param {unknown} value Raw index candidate.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function clampColorIndex(value) {
|
||||||
|
const n = typeof value === 'number' ? value : Number(value);
|
||||||
|
const safe = Number.isFinite(n) ? Math.round(n) : 0;
|
||||||
|
return Math.max(0, Math.min(15, safe));
|
||||||
|
}
|
||||||
@@ -331,7 +331,7 @@ export class EmbeddedPreview {
|
|||||||
const fallback = (hasUpdateCallback && hasDrawCallback) ? '' :
|
const fallback = (hasUpdateCallback && hasDrawCallback) ? '' :
|
||||||
(!hasUpdateCallback ? '\nfunction _update()\nend\n' : '') +
|
(!hasUpdateCallback ? '\nfunction _update()\nend\n' : '') +
|
||||||
(!hasDrawCallback ? '\nfunction _draw()\n cls()\n print("picograph ok",2,60,7)\nend\n' : '');
|
(!hasDrawCallback ? '\nfunction _draw()\n cls()\n print("picograph ok",2,60,7)\nend\n' : '');
|
||||||
return `pico-8 cartridge // http://www.pico-8.com
|
const content = `pico-8 cartridge // http://www.pico-8.com
|
||||||
version 41
|
version 41
|
||||||
__lua__
|
__lua__
|
||||||
${luaCode}${fallback}
|
${luaCode}${fallback}
|
||||||
@@ -345,6 +345,8 @@ __gfx__
|
|||||||
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
`;
|
`;
|
||||||
|
// Ensure Unix line endings (PICO-8 tools expect \n, not \r\n)
|
||||||
|
return content.replace(/\r\n/g, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
622
scripts/ui/SpriteEditor.js
Normal file
622
scripts/ui/SpriteEditor.js
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
import { SpriteSheet } from "../core/SpriteSheet.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive sprite sheet editor for PICO-8.
|
||||||
|
*/
|
||||||
|
export class SpriteEditor {
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} container Parent element for the editor.
|
||||||
|
* @param {SpriteSheet} [spriteSheet] Existing sprite sheet or creates new one.
|
||||||
|
*/
|
||||||
|
constructor(container, spriteSheet = null) {
|
||||||
|
this.container = container;
|
||||||
|
this.spriteSheet = spriteSheet || new SpriteSheet();
|
||||||
|
|
||||||
|
/** @type {number} Currently selected color (0-15) */
|
||||||
|
this.selectedColor = 7;
|
||||||
|
|
||||||
|
/** @type {number} Currently selected sprite (0-255) */
|
||||||
|
this.selectedSprite = 0;
|
||||||
|
|
||||||
|
/** @type {string} Current tool: "pen", "fill", "line", "rect", "circle", "erase" */
|
||||||
|
this.currentTool = "pen";
|
||||||
|
|
||||||
|
/** @type {number} Zoom level for sprite sheet canvas */
|
||||||
|
this.sheetZoom = 2;
|
||||||
|
|
||||||
|
/** @type {number} Zoom level for sprite preview */
|
||||||
|
this.previewZoom = 8;
|
||||||
|
|
||||||
|
/** @type {Object|null} Drawing state for line/rect tools */
|
||||||
|
this.drawStart = null;
|
||||||
|
|
||||||
|
this.#createUI();
|
||||||
|
this.#attachEvents();
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the UI structure.
|
||||||
|
*/
|
||||||
|
#createUI() {
|
||||||
|
this.container.innerHTML = "";
|
||||||
|
this.container.className = "sprite-editor";
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
const toolbar = document.createElement("div");
|
||||||
|
toolbar.className = "sprite-toolbar";
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{ id: "pen", label: "Pen", icon: "✏️" },
|
||||||
|
{ id: "fill", label: "Fill", icon: "🪣" },
|
||||||
|
{ id: "line", label: "Line", icon: "📏" },
|
||||||
|
{ id: "rect", label: "Rect", icon: "⬜" },
|
||||||
|
{ id: "circle", label: "Circle", icon: "⭕" },
|
||||||
|
{ id: "erase", label: "Erase", icon: "🧹" }
|
||||||
|
];
|
||||||
|
|
||||||
|
tools.forEach((tool) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "sprite-tool-btn";
|
||||||
|
btn.dataset.tool = tool.id;
|
||||||
|
btn.title = tool.label;
|
||||||
|
btn.textContent = tool.icon;
|
||||||
|
if (tool.id === this.currentTool) {
|
||||||
|
btn.classList.add("active");
|
||||||
|
}
|
||||||
|
toolbar.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.container.appendChild(toolbar);
|
||||||
|
|
||||||
|
// Main content area
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "sprite-content";
|
||||||
|
|
||||||
|
// Left panel: Sprite sheet
|
||||||
|
const leftPanel = document.createElement("div");
|
||||||
|
leftPanel.className = "sprite-panel sprite-sheet-panel";
|
||||||
|
|
||||||
|
const sheetLabel = document.createElement("div");
|
||||||
|
sheetLabel.className = "sprite-panel-label";
|
||||||
|
sheetLabel.textContent = "Sprite Sheet (128x128)";
|
||||||
|
leftPanel.appendChild(sheetLabel);
|
||||||
|
|
||||||
|
this.sheetCanvas = document.createElement("canvas");
|
||||||
|
this.sheetCanvas.className = "sprite-sheet-canvas";
|
||||||
|
this.sheetCanvas.width = 128 * this.sheetZoom;
|
||||||
|
this.sheetCanvas.height = 128 * this.sheetZoom;
|
||||||
|
this.sheetCtx = this.sheetCanvas.getContext("2d", { alpha: false });
|
||||||
|
this.sheetCtx.imageSmoothingEnabled = false;
|
||||||
|
leftPanel.appendChild(this.sheetCanvas);
|
||||||
|
|
||||||
|
content.appendChild(leftPanel);
|
||||||
|
|
||||||
|
// Right panel: Preview and palette
|
||||||
|
const rightPanel = document.createElement("div");
|
||||||
|
rightPanel.className = "sprite-panel sprite-controls-panel";
|
||||||
|
|
||||||
|
// Sprite preview
|
||||||
|
const previewContainer = document.createElement("div");
|
||||||
|
previewContainer.className = "sprite-preview-container";
|
||||||
|
|
||||||
|
const previewLabel = document.createElement("div");
|
||||||
|
previewLabel.className = "sprite-panel-label";
|
||||||
|
previewLabel.textContent = "Sprite Preview";
|
||||||
|
previewContainer.appendChild(previewLabel);
|
||||||
|
|
||||||
|
this.previewCanvas = document.createElement("canvas");
|
||||||
|
this.previewCanvas.className = "sprite-preview-canvas";
|
||||||
|
this.previewCanvas.width = 8 * this.previewZoom;
|
||||||
|
this.previewCanvas.height = 8 * this.previewZoom;
|
||||||
|
this.previewCtx = this.previewCanvas.getContext("2d", { alpha: false });
|
||||||
|
this.previewCtx.imageSmoothingEnabled = false;
|
||||||
|
previewContainer.appendChild(this.previewCanvas);
|
||||||
|
|
||||||
|
this.spriteIndexLabel = document.createElement("div");
|
||||||
|
this.spriteIndexLabel.className = "sprite-index-label";
|
||||||
|
this.spriteIndexLabel.textContent = `Sprite: ${this.selectedSprite}`;
|
||||||
|
previewContainer.appendChild(this.spriteIndexLabel);
|
||||||
|
|
||||||
|
rightPanel.appendChild(previewContainer);
|
||||||
|
|
||||||
|
// Color palette
|
||||||
|
const paletteContainer = document.createElement("div");
|
||||||
|
paletteContainer.className = "sprite-palette-container";
|
||||||
|
|
||||||
|
const paletteLabel = document.createElement("div");
|
||||||
|
paletteLabel.className = "sprite-panel-label";
|
||||||
|
paletteLabel.textContent = "Palette";
|
||||||
|
paletteContainer.appendChild(paletteLabel);
|
||||||
|
|
||||||
|
this.paletteElement = document.createElement("div");
|
||||||
|
this.paletteElement.className = "sprite-palette";
|
||||||
|
|
||||||
|
this.spriteSheet.palette.forEach((color, index) => {
|
||||||
|
const swatch = document.createElement("div");
|
||||||
|
swatch.className = "sprite-palette-swatch";
|
||||||
|
swatch.style.backgroundColor = color;
|
||||||
|
swatch.dataset.color = index;
|
||||||
|
swatch.title = `Color ${index}`;
|
||||||
|
if (index === this.selectedColor) {
|
||||||
|
swatch.classList.add("selected");
|
||||||
|
}
|
||||||
|
this.paletteElement.appendChild(swatch);
|
||||||
|
});
|
||||||
|
|
||||||
|
paletteContainer.appendChild(this.paletteElement);
|
||||||
|
rightPanel.appendChild(paletteContainer);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const actionsContainer = document.createElement("div");
|
||||||
|
actionsContainer.className = "sprite-actions";
|
||||||
|
|
||||||
|
const clearBtn = document.createElement("button");
|
||||||
|
clearBtn.className = "sprite-action-btn";
|
||||||
|
clearBtn.textContent = "Clear Sprite";
|
||||||
|
clearBtn.dataset.action = "clear-sprite";
|
||||||
|
actionsContainer.appendChild(clearBtn);
|
||||||
|
|
||||||
|
const clearAllBtn = document.createElement("button");
|
||||||
|
clearAllBtn.className = "sprite-action-btn";
|
||||||
|
clearAllBtn.textContent = "Clear All";
|
||||||
|
clearAllBtn.dataset.action = "clear-all";
|
||||||
|
actionsContainer.appendChild(clearAllBtn);
|
||||||
|
|
||||||
|
rightPanel.appendChild(actionsContainer);
|
||||||
|
|
||||||
|
content.appendChild(rightPanel);
|
||||||
|
this.container.appendChild(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches event listeners.
|
||||||
|
*/
|
||||||
|
#attachEvents() {
|
||||||
|
// Tool selection
|
||||||
|
this.container.querySelectorAll(".sprite-tool-btn").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", (e) => {
|
||||||
|
this.currentTool = btn.dataset.tool;
|
||||||
|
this.container.querySelectorAll(".sprite-tool-btn").forEach((b) => {
|
||||||
|
b.classList.remove("active");
|
||||||
|
});
|
||||||
|
btn.classList.add("active");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Color palette
|
||||||
|
this.paletteElement.querySelectorAll(".sprite-palette-swatch").forEach((swatch) => {
|
||||||
|
swatch.addEventListener("click", (e) => {
|
||||||
|
this.selectedColor = parseInt(swatch.dataset.color);
|
||||||
|
this.paletteElement.querySelectorAll(".sprite-palette-swatch").forEach((s) => {
|
||||||
|
s.classList.remove("selected");
|
||||||
|
});
|
||||||
|
swatch.classList.add("selected");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
this.container.querySelectorAll(".sprite-action-btn").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", (e) => {
|
||||||
|
const action = btn.dataset.action;
|
||||||
|
if (action === "clear-sprite") {
|
||||||
|
this.#clearCurrentSprite();
|
||||||
|
} else if (action === "clear-all") {
|
||||||
|
if (confirm("Clear all sprites? This cannot be undone.")) {
|
||||||
|
this.spriteSheet.clear();
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sheet canvas interactions
|
||||||
|
let isDrawing = false;
|
||||||
|
|
||||||
|
this.sheetCanvas.addEventListener("mousedown", (e) => {
|
||||||
|
isDrawing = true;
|
||||||
|
const coords = this.#getSheetCoords(e);
|
||||||
|
this.#handleToolStart(coords);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sheetCanvas.addEventListener("mousemove", (e) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
const coords = this.#getSheetCoords(e);
|
||||||
|
this.#handleToolMove(coords);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sheetCanvas.addEventListener("mouseup", (e) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
isDrawing = false;
|
||||||
|
const coords = this.#getSheetCoords(e);
|
||||||
|
this.#handleToolEnd(coords);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sheetCanvas.addEventListener("mouseleave", () => {
|
||||||
|
isDrawing = false;
|
||||||
|
this.drawStart = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sprite selection
|
||||||
|
this.sheetCanvas.addEventListener("click", (e) => {
|
||||||
|
const coords = this.#getSheetCoords(e);
|
||||||
|
const spriteX = Math.floor(coords.x / 8);
|
||||||
|
const spriteY = Math.floor(coords.y / 8);
|
||||||
|
this.selectedSprite = spriteY * 16 + spriteX;
|
||||||
|
this.spriteIndexLabel.textContent = `Sprite: ${this.selectedSprite}`;
|
||||||
|
this.#renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preview canvas editing
|
||||||
|
let isDrawingPreview = false;
|
||||||
|
|
||||||
|
this.previewCanvas.addEventListener("mousedown", (e) => {
|
||||||
|
isDrawingPreview = true;
|
||||||
|
const coords = this.#getPreviewCoords(e);
|
||||||
|
this.#drawPixelInPreview(coords);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.previewCanvas.addEventListener("mousemove", (e) => {
|
||||||
|
if (!isDrawingPreview) return;
|
||||||
|
const coords = this.#getPreviewCoords(e);
|
||||||
|
this.#drawPixelInPreview(coords);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.previewCanvas.addEventListener("mouseup", () => {
|
||||||
|
isDrawingPreview = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.previewCanvas.addEventListener("mouseleave", () => {
|
||||||
|
isDrawingPreview = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets sheet coordinates from mouse event.
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} e Mouse event.
|
||||||
|
* @returns {{x: number, y: number}}
|
||||||
|
*/
|
||||||
|
#getSheetCoords(e) {
|
||||||
|
const rect = this.sheetCanvas.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - rect.left) / this.sheetZoom);
|
||||||
|
const y = Math.floor((e.clientY - rect.top) / this.sheetZoom);
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets preview coordinates from mouse event.
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} e Mouse event.
|
||||||
|
* @returns {{x: number, y: number}}
|
||||||
|
*/
|
||||||
|
#getPreviewCoords(e) {
|
||||||
|
const rect = this.previewCanvas.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - rect.left) / this.previewZoom);
|
||||||
|
const y = Math.floor((e.clientY - rect.top) / this.previewZoom);
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles tool start.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Coordinates.
|
||||||
|
*/
|
||||||
|
#handleToolStart(coords) {
|
||||||
|
if (this.currentTool === "pen") {
|
||||||
|
this.#drawPixel(coords);
|
||||||
|
} else if (this.currentTool === "erase") {
|
||||||
|
this.#erasePixel(coords);
|
||||||
|
} else if (this.currentTool === "fill") {
|
||||||
|
this.#floodFill(coords);
|
||||||
|
} else if (["line", "rect", "circle"].includes(this.currentTool)) {
|
||||||
|
this.drawStart = coords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles tool move.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Coordinates.
|
||||||
|
*/
|
||||||
|
#handleToolMove(coords) {
|
||||||
|
if (this.currentTool === "pen") {
|
||||||
|
this.#drawPixel(coords);
|
||||||
|
} else if (this.currentTool === "erase") {
|
||||||
|
this.#erasePixel(coords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles tool end.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Coordinates.
|
||||||
|
*/
|
||||||
|
#handleToolEnd(coords) {
|
||||||
|
if (!this.drawStart) return;
|
||||||
|
|
||||||
|
if (this.currentTool === "line") {
|
||||||
|
this.#drawLine(this.drawStart, coords);
|
||||||
|
} else if (this.currentTool === "rect") {
|
||||||
|
this.#drawRect(this.drawStart, coords);
|
||||||
|
} else if (this.currentTool === "circle") {
|
||||||
|
this.#drawCircle(this.drawStart, coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawStart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a pixel.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Coordinates.
|
||||||
|
*/
|
||||||
|
#drawPixel(coords) {
|
||||||
|
this.spriteSheet.setPixel(coords.x, coords.y, this.selectedColor);
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erases a pixel.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Coordinates.
|
||||||
|
*/
|
||||||
|
#erasePixel(coords) {
|
||||||
|
this.spriteSheet.setPixel(coords.x, coords.y, 0);
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flood fill from a point.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Starting coordinates.
|
||||||
|
*/
|
||||||
|
#floodFill(coords) {
|
||||||
|
const targetColor = this.spriteSheet.getPixel(coords.x, coords.y);
|
||||||
|
if (targetColor === this.selectedColor) return;
|
||||||
|
|
||||||
|
const stack = [coords];
|
||||||
|
const visited = new Set();
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const { x, y } = stack.pop();
|
||||||
|
const key = `${x},${y}`;
|
||||||
|
|
||||||
|
if (visited.has(key)) continue;
|
||||||
|
if (x < 0 || x >= 128 || y < 0 || y >= 128) continue;
|
||||||
|
if (this.spriteSheet.getPixel(x, y) !== targetColor) continue;
|
||||||
|
|
||||||
|
visited.add(key);
|
||||||
|
this.spriteSheet.setPixel(x, y, this.selectedColor);
|
||||||
|
|
||||||
|
stack.push({ x: x + 1, y });
|
||||||
|
stack.push({ x: x - 1, y });
|
||||||
|
stack.push({ x, y: y + 1 });
|
||||||
|
stack.push({ x, y: y - 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a line.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} start Start coordinates.
|
||||||
|
* @param {{x: number, y: number}} end End coordinates.
|
||||||
|
*/
|
||||||
|
#drawLine(start, end) {
|
||||||
|
const dx = Math.abs(end.x - start.x);
|
||||||
|
const dy = Math.abs(end.y - start.y);
|
||||||
|
const sx = start.x < end.x ? 1 : -1;
|
||||||
|
const sy = start.y < end.y ? 1 : -1;
|
||||||
|
let err = dx - dy;
|
||||||
|
|
||||||
|
let x = start.x;
|
||||||
|
let y = start.y;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
this.spriteSheet.setPixel(x, y, this.selectedColor);
|
||||||
|
|
||||||
|
if (x === end.x && y === end.y) break;
|
||||||
|
|
||||||
|
const e2 = 2 * err;
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy;
|
||||||
|
x += sx;
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
y += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a rectangle.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} start Start coordinates.
|
||||||
|
* @param {{x: number, y: number}} end End coordinates.
|
||||||
|
*/
|
||||||
|
#drawRect(start, end) {
|
||||||
|
const x1 = Math.min(start.x, end.x);
|
||||||
|
const y1 = Math.min(start.y, end.y);
|
||||||
|
const x2 = Math.max(start.x, end.x);
|
||||||
|
const y2 = Math.max(start.y, end.y);
|
||||||
|
|
||||||
|
for (let x = x1; x <= x2; x++) {
|
||||||
|
this.spriteSheet.setPixel(x, y1, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(x, y2, this.selectedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = y1; y <= y2; y++) {
|
||||||
|
this.spriteSheet.setPixel(x1, y, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(x2, y, this.selectedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a circle.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} center Center coordinates.
|
||||||
|
* @param {{x: number, y: number}} edge Edge point.
|
||||||
|
*/
|
||||||
|
#drawCircle(center, edge) {
|
||||||
|
const radius = Math.round(
|
||||||
|
Math.sqrt((edge.x - center.x) ** 2 + (edge.y - center.y) ** 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
let x = radius;
|
||||||
|
let y = 0;
|
||||||
|
let err = 0;
|
||||||
|
|
||||||
|
while (x >= y) {
|
||||||
|
this.spriteSheet.setPixel(center.x + x, center.y + y, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x + y, center.y + x, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x - y, center.y + x, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x - x, center.y + y, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x - x, center.y - y, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x - y, center.y - x, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x + y, center.y - x, this.selectedColor);
|
||||||
|
this.spriteSheet.setPixel(center.x + x, center.y - y, this.selectedColor);
|
||||||
|
|
||||||
|
y += 1;
|
||||||
|
err += 1 + 2 * y;
|
||||||
|
if (2 * (err - x) + 1 > 0) {
|
||||||
|
x -= 1;
|
||||||
|
err += 1 - 2 * x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a pixel in the preview canvas.
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} coords Coordinates (0-7).
|
||||||
|
*/
|
||||||
|
#drawPixelInPreview(coords) {
|
||||||
|
if (coords.x < 0 || coords.x >= 8 || coords.y < 0 || coords.y >= 8) return;
|
||||||
|
|
||||||
|
const sx = (this.selectedSprite % 16) * 8;
|
||||||
|
const sy = Math.floor(this.selectedSprite / 16) * 8;
|
||||||
|
|
||||||
|
const color = this.currentTool === "erase" ? 0 : this.selectedColor;
|
||||||
|
this.spriteSheet.setPixel(sx + coords.x, sy + coords.y, color);
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the currently selected sprite.
|
||||||
|
*/
|
||||||
|
#clearCurrentSprite() {
|
||||||
|
const clearData = new Uint8Array(64);
|
||||||
|
this.spriteSheet.setSprite(this.selectedSprite, clearData);
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders all canvases.
|
||||||
|
*/
|
||||||
|
#render() {
|
||||||
|
this.#renderSheet();
|
||||||
|
this.#renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the sprite sheet canvas.
|
||||||
|
*/
|
||||||
|
#renderSheet() {
|
||||||
|
for (let y = 0; y < 128; y++) {
|
||||||
|
for (let x = 0; x < 128; x++) {
|
||||||
|
const color = this.spriteSheet.getPixel(x, y);
|
||||||
|
this.sheetCtx.fillStyle = this.spriteSheet.palette[color];
|
||||||
|
this.sheetCtx.fillRect(
|
||||||
|
x * this.sheetZoom,
|
||||||
|
y * this.sheetZoom,
|
||||||
|
this.sheetZoom,
|
||||||
|
this.sheetZoom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
this.sheetCtx.strokeStyle = "rgba(255, 255, 255, 0.2)";
|
||||||
|
this.sheetCtx.lineWidth = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i <= 16; i++) {
|
||||||
|
const pos = i * 8 * this.sheetZoom;
|
||||||
|
this.sheetCtx.beginPath();
|
||||||
|
this.sheetCtx.moveTo(pos, 0);
|
||||||
|
this.sheetCtx.lineTo(pos, 128 * this.sheetZoom);
|
||||||
|
this.sheetCtx.stroke();
|
||||||
|
|
||||||
|
this.sheetCtx.beginPath();
|
||||||
|
this.sheetCtx.moveTo(0, pos);
|
||||||
|
this.sheetCtx.lineTo(128 * this.sheetZoom, pos);
|
||||||
|
this.sheetCtx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the sprite preview canvas.
|
||||||
|
*/
|
||||||
|
#renderPreview() {
|
||||||
|
const sx = (this.selectedSprite % 16) * 8;
|
||||||
|
const sy = Math.floor(this.selectedSprite / 16) * 8;
|
||||||
|
|
||||||
|
for (let y = 0; y < 8; y++) {
|
||||||
|
for (let x = 0; x < 8; x++) {
|
||||||
|
const color = this.spriteSheet.getPixel(sx + x, sy + y);
|
||||||
|
this.previewCtx.fillStyle = this.spriteSheet.palette[color];
|
||||||
|
this.previewCtx.fillRect(
|
||||||
|
x * this.previewZoom,
|
||||||
|
y * this.previewZoom,
|
||||||
|
this.previewZoom,
|
||||||
|
this.previewZoom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
this.previewCtx.strokeStyle = "rgba(255, 255, 255, 0.3)";
|
||||||
|
this.previewCtx.lineWidth = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i <= 8; i++) {
|
||||||
|
const pos = i * this.previewZoom;
|
||||||
|
this.previewCtx.beginPath();
|
||||||
|
this.previewCtx.moveTo(pos, 0);
|
||||||
|
this.previewCtx.lineTo(pos, 8 * this.previewZoom);
|
||||||
|
this.previewCtx.stroke();
|
||||||
|
|
||||||
|
this.previewCtx.beginPath();
|
||||||
|
this.previewCtx.moveTo(0, pos);
|
||||||
|
this.previewCtx.lineTo(8 * this.previewZoom, pos);
|
||||||
|
this.previewCtx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the sprite sheet data.
|
||||||
|
*
|
||||||
|
* @returns {SpriteSheet}
|
||||||
|
*/
|
||||||
|
getSpriteSheet() {
|
||||||
|
return this.spriteSheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets sprite sheet data.
|
||||||
|
*
|
||||||
|
* @param {SpriteSheet} spriteSheet New sprite sheet.
|
||||||
|
*/
|
||||||
|
setSpriteSheet(spriteSheet) {
|
||||||
|
this.spriteSheet = spriteSheet;
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
}
|
||||||
205
scripts/ui/SpriteEditorManager.js
Normal file
205
scripts/ui/SpriteEditorManager.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { SpriteEditor } from './SpriteEditor.js';
|
||||||
|
import { SpriteSheet } from '../core/SpriteSheet.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the sprite editor modal integration.
|
||||||
|
*/
|
||||||
|
export class SpriteEditorManager {
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} modal Modal element.
|
||||||
|
* @param {HTMLElement} container Container for sprite editor.
|
||||||
|
*/
|
||||||
|
constructor(modal, container) {
|
||||||
|
this.modal = modal;
|
||||||
|
this.container = container;
|
||||||
|
this.spriteSheet = new SpriteSheet();
|
||||||
|
this.editor = null;
|
||||||
|
this.isOpen = false;
|
||||||
|
|
||||||
|
this.#setupModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up modal event listeners.
|
||||||
|
*/
|
||||||
|
#setupModal() {
|
||||||
|
// Close modal on backdrop click
|
||||||
|
const backdrop = this.modal.querySelector('.modal__backdrop');
|
||||||
|
backdrop?.addEventListener('click', () => this.close());
|
||||||
|
|
||||||
|
// Close modal on close button click
|
||||||
|
const closeButtons = this.modal.querySelectorAll('[data-modal-close]');
|
||||||
|
closeButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => this.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export to .p8
|
||||||
|
const exportBtn = document.getElementById('exportP8Button');
|
||||||
|
exportBtn?.addEventListener('click', () => this.#exportToP8());
|
||||||
|
|
||||||
|
// Import from .p8
|
||||||
|
const importBtn = document.getElementById('importP8Button');
|
||||||
|
importBtn?.addEventListener('click', () => this.#importFromP8());
|
||||||
|
|
||||||
|
// Initialize editor on first open
|
||||||
|
this.modal.addEventListener('transitionend', () => {
|
||||||
|
if (this.isOpen && !this.editor) {
|
||||||
|
this.#initializeEditor();
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the sprite editor.
|
||||||
|
*/
|
||||||
|
#initializeEditor() {
|
||||||
|
if (this.editor) return;
|
||||||
|
|
||||||
|
this.editor = new SpriteEditor(this.container, this.spriteSheet);
|
||||||
|
console.log('Sprite editor initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the sprite editor modal.
|
||||||
|
*/
|
||||||
|
open() {
|
||||||
|
if (this.isOpen) return;
|
||||||
|
|
||||||
|
this.isOpen = true;
|
||||||
|
this.modal.removeAttribute('hidden');
|
||||||
|
this.modal.setAttribute('aria-hidden', 'false');
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
|
// Initialize editor immediately if not already done
|
||||||
|
if (!this.editor) {
|
||||||
|
this.#initializeEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus trap
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const closeBtn = this.modal.querySelector('[data-modal-close]');
|
||||||
|
closeBtn?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the sprite editor modal.
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
if (!this.isOpen) return;
|
||||||
|
|
||||||
|
this.isOpen = false;
|
||||||
|
this.modal.setAttribute('hidden', '');
|
||||||
|
this.modal.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports sprite data to .p8 format.
|
||||||
|
*/
|
||||||
|
#exportToP8() {
|
||||||
|
if (!this.editor) return;
|
||||||
|
|
||||||
|
const gfxData = this.editor.getSpriteSheet().toP8Gfx();
|
||||||
|
const flagData = this.editor.getSpriteSheet().toP8Flags();
|
||||||
|
|
||||||
|
const p8Content = `pico-8 cartridge // http://www.pico-8.com
|
||||||
|
version 41
|
||||||
|
__lua__
|
||||||
|
-- generated by picoGraph
|
||||||
|
-- paste your blueprinted code here
|
||||||
|
|
||||||
|
__gfx__
|
||||||
|
${gfxData}
|
||||||
|
__gff__
|
||||||
|
${flagData}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([p8Content], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'sprites.p8';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
const exportBtn = document.getElementById('exportP8Button');
|
||||||
|
if (exportBtn) {
|
||||||
|
exportBtn.classList.add('is-success');
|
||||||
|
setTimeout(() => {
|
||||||
|
exportBtn.classList.remove('is-success');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports sprite data from .p8 format.
|
||||||
|
*/
|
||||||
|
#importFromP8() {
|
||||||
|
if (!this.editor) return;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.p8';
|
||||||
|
|
||||||
|
input.addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
|
||||||
|
// Extract __gfx__ section
|
||||||
|
const gfxMatch = text.match(/__gfx__\s*\n([\s\S]*?)(?=\n__|$)/);
|
||||||
|
if (gfxMatch) {
|
||||||
|
this.editor.getSpriteSheet().fromP8Gfx(gfxMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract __gff__ section
|
||||||
|
const gffMatch = text.match(/__gff__\s*\n([\s\S]*?)(?=\n__|$)/);
|
||||||
|
if (gffMatch) {
|
||||||
|
this.editor.getSpriteSheet().fromP8Flags(gffMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
this.editor.setSpriteSheet(this.editor.getSpriteSheet());
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
const importBtn = document.getElementById('importP8Button');
|
||||||
|
if (importBtn) {
|
||||||
|
importBtn.classList.add('is-success');
|
||||||
|
setTimeout(() => {
|
||||||
|
importBtn.classList.remove('is-success');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to import sprite data:', error);
|
||||||
|
alert('Failed to import sprite data. Please ensure the file is a valid .p8 file.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current sprite sheet data.
|
||||||
|
*
|
||||||
|
* @returns {SpriteSheet}
|
||||||
|
*/
|
||||||
|
getSpriteSheet() {
|
||||||
|
return this.spriteSheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets sprite sheet data.
|
||||||
|
*
|
||||||
|
* @param {SpriteSheet} spriteSheet New sprite sheet.
|
||||||
|
*/
|
||||||
|
setSpriteSheet(spriteSheet) {
|
||||||
|
this.spriteSheet = spriteSheet;
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.setSpriteSheet(spriteSheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
scripts/ui/TileManager.js
Normal file
114
scripts/ui/TileManager.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { TileSetBrowser } from "./TileSetBrowser.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the tile browser modal and provides access to tileset management.
|
||||||
|
*/
|
||||||
|
export class TileManager {
|
||||||
|
constructor() {
|
||||||
|
this.modal = null;
|
||||||
|
this.browser = null;
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the tile manager.
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
this.modal = document.getElementById("tileModal");
|
||||||
|
if (!this.modal) {
|
||||||
|
console.error("Tile modal element not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = this.modal.querySelector(".tile-browser-container");
|
||||||
|
if (!container) {
|
||||||
|
console.error("Tile browser container not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.browser = new TileSetBrowser(container);
|
||||||
|
|
||||||
|
const closeBtn = this.modal.querySelector(".close-modal");
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener("click", () => this.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target === this.modal) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && this.isOpen) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tileBtn = document.getElementById("tileManagerButton");
|
||||||
|
if (tileBtn) {
|
||||||
|
tileBtn.addEventListener("click", () => this.open());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#loadFromLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the tile manager modal.
|
||||||
|
*/
|
||||||
|
open() {
|
||||||
|
if (!this.modal) return;
|
||||||
|
this.modal.style.display = "flex";
|
||||||
|
this.isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the tile manager modal.
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
if (!this.modal) return;
|
||||||
|
this.modal.style.display = "none";
|
||||||
|
this.isOpen = false;
|
||||||
|
this.#saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all tilesets.
|
||||||
|
*
|
||||||
|
* @returns {Array<TileSet>}
|
||||||
|
*/
|
||||||
|
getTileSets() {
|
||||||
|
return this.browser ? this.browser.getTileSets() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves tileset data to localStorage.
|
||||||
|
*/
|
||||||
|
#saveToLocalStorage() {
|
||||||
|
if (!this.browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = this.browser.toJSON();
|
||||||
|
localStorage.setItem("picograph_tilesets", JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save tilesets:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads tileset data from localStorage.
|
||||||
|
*/
|
||||||
|
async #loadFromLocalStorage() {
|
||||||
|
if (!this.browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("picograph_tilesets");
|
||||||
|
if (stored) {
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
await this.browser.fromJSON(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load tilesets:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
511
scripts/ui/TileSetBrowser.js
Normal file
511
scripts/ui/TileSetBrowser.js
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
import { TileSet } from "../core/TileSet.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages multiple tilesets (8x8 tiles in 128x128px sheets) and provides UI for browsing and editing externally.
|
||||||
|
*/
|
||||||
|
export class TileSetBrowser {
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} container Parent element for the browser.
|
||||||
|
*/
|
||||||
|
constructor(container) {
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
|
/** @type {Map<string, TileSet>} */
|
||||||
|
this.tileSets = new Map();
|
||||||
|
|
||||||
|
/** @type {string | null} */
|
||||||
|
this.selectedTileSetId = null;
|
||||||
|
|
||||||
|
/** @type {number | null} */
|
||||||
|
this.selectedTileIndex = null;
|
||||||
|
|
||||||
|
this.#createUI();
|
||||||
|
this.#attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the UI structure.
|
||||||
|
*/
|
||||||
|
#createUI() {
|
||||||
|
this.container.innerHTML = "";
|
||||||
|
this.container.className = "tileset-browser";
|
||||||
|
|
||||||
|
// Header with actions
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "tileset-header";
|
||||||
|
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.className = "tileset-title";
|
||||||
|
title.textContent = "Tilesets";
|
||||||
|
header.appendChild(title);
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "tileset-actions";
|
||||||
|
|
||||||
|
const addBtn = document.createElement("button");
|
||||||
|
addBtn.className = "tileset-action-btn";
|
||||||
|
addBtn.textContent = "+ Add Tileset";
|
||||||
|
addBtn.dataset.action = "add";
|
||||||
|
actions.appendChild(addBtn);
|
||||||
|
|
||||||
|
header.appendChild(actions);
|
||||||
|
this.container.appendChild(header);
|
||||||
|
|
||||||
|
// Tileset list
|
||||||
|
this.tileSetList = document.createElement("div");
|
||||||
|
this.tileSetList.className = "tileset-list";
|
||||||
|
this.container.appendChild(this.tileSetList);
|
||||||
|
|
||||||
|
// Tile preview panel
|
||||||
|
this.previewPanel = document.createElement("div");
|
||||||
|
this.previewPanel.className = "tileset-preview-panel";
|
||||||
|
this.previewPanel.innerHTML = `
|
||||||
|
<div class="tileset-preview-header">
|
||||||
|
<span class="tileset-preview-title">Tile Preview</span>
|
||||||
|
</div>
|
||||||
|
<div class="tileset-preview-content">
|
||||||
|
<canvas class="tileset-preview-canvas"></canvas>
|
||||||
|
<div class="tileset-preview-info">Select a tileset to preview</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.container.appendChild(this.previewPanel);
|
||||||
|
|
||||||
|
this.previewCanvas = this.previewPanel.querySelector(".tileset-preview-canvas");
|
||||||
|
this.previewCtx = this.previewCanvas.getContext("2d", { alpha: false });
|
||||||
|
this.previewInfo = this.previewPanel.querySelector(".tileset-preview-info");
|
||||||
|
|
||||||
|
this.#renderEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches event listeners.
|
||||||
|
*/
|
||||||
|
#attachEvents() {
|
||||||
|
// Add tileset button
|
||||||
|
this.container.addEventListener("click", (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (!(target instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
const actionBtn = target.closest("[data-action]");
|
||||||
|
if (actionBtn) {
|
||||||
|
const action = actionBtn.dataset.action;
|
||||||
|
|
||||||
|
if (action === "add") {
|
||||||
|
this.#addTileSet();
|
||||||
|
} else if (action === "open") {
|
||||||
|
const id = actionBtn.dataset.id;
|
||||||
|
if (id) this.#openExternal(id);
|
||||||
|
} else if (action === "reload") {
|
||||||
|
const id = actionBtn.dataset.id;
|
||||||
|
if (id) this.#reloadTileSet(id);
|
||||||
|
} else if (action === "remove") {
|
||||||
|
const id = actionBtn.dataset.id;
|
||||||
|
if (id) this.#removeTileSet(id);
|
||||||
|
} else if (action === "edit-settings") {
|
||||||
|
const id = actionBtn.dataset.id;
|
||||||
|
if (id) this.#editTileSetSettings(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tileset selection
|
||||||
|
const tilesetItem = target.closest(".tileset-item");
|
||||||
|
if (tilesetItem && tilesetItem.dataset.id) {
|
||||||
|
this.#selectTileSet(tilesetItem.dataset.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Canvas tile selection
|
||||||
|
this.previewCanvas.addEventListener("click", (e) => {
|
||||||
|
if (!this.selectedTileSetId) return;
|
||||||
|
|
||||||
|
const tileSet = this.tileSets.get(this.selectedTileSetId);
|
||||||
|
if (!tileSet || !tileSet.isLoaded) return;
|
||||||
|
|
||||||
|
const rect = this.previewCanvas.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - rect.left) / (this.previewCanvas.width / tileSet.columns));
|
||||||
|
const y = Math.floor((e.clientY - rect.top) / (this.previewCanvas.height / tileSet.rows));
|
||||||
|
|
||||||
|
this.selectedTileIndex = y * tileSet.columns + x;
|
||||||
|
this.#renderPreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new tileset.
|
||||||
|
*/
|
||||||
|
async #addTileSet() {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = "image/png";
|
||||||
|
|
||||||
|
input.addEventListener("change", async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = file.name.replace(/\.[^/.]+$/, "");
|
||||||
|
|
||||||
|
let filePath;
|
||||||
|
let nativePath = "";
|
||||||
|
|
||||||
|
// If running in Electron, save the file to app data
|
||||||
|
if (window.electronAPI?.saveTileset) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
const result = await window.electronAPI.saveTileset(uint8Array, file.name);
|
||||||
|
nativePath = result.filePath;
|
||||||
|
// Convert to file:// URL for loading in Image element
|
||||||
|
filePath = `file:///${result.filePath.replace(/\\/g, "/")}`;
|
||||||
|
} else {
|
||||||
|
// Fallback for non-Electron environments (dev/testing)
|
||||||
|
filePath = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileSet = new TileSet({
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
nativePath,
|
||||||
|
tileWidth: 8,
|
||||||
|
tileHeight: 8
|
||||||
|
});
|
||||||
|
|
||||||
|
await tileSet.load();
|
||||||
|
|
||||||
|
this.tileSets.set(tileSet.id, tileSet);
|
||||||
|
this.#render();
|
||||||
|
this.#selectTileSet(tileSet.id);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add tileset:", error);
|
||||||
|
alert(`Failed to load tileset:\n${error.message || error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens tileset in external editor.
|
||||||
|
*
|
||||||
|
* @param {string} id Tileset ID.
|
||||||
|
*/
|
||||||
|
#openExternal(id) {
|
||||||
|
const tileSet = this.tileSets.get(id);
|
||||||
|
if (!tileSet) return;
|
||||||
|
|
||||||
|
// In Electron, show the file in explorer so user can open with their preferred editor
|
||||||
|
if (window.electronAPI?.showTilesetInFolder && tileSet.nativePath) {
|
||||||
|
window.electronAPI.showTilesetInFolder(tileSet.nativePath);
|
||||||
|
} else {
|
||||||
|
// Fallback: show message
|
||||||
|
const pathToShow = tileSet.nativePath || tileSet.filePath;
|
||||||
|
alert(`To edit this tileset, open:\n${pathToShow}\n\nIn your image editor of choice.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads a tileset from disk.
|
||||||
|
*
|
||||||
|
* @param {string} id Tileset ID.
|
||||||
|
*/
|
||||||
|
async #reloadTileSet(id) {
|
||||||
|
const tileSet = this.tileSets.get(id);
|
||||||
|
if (!tileSet) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tileSet.reload();
|
||||||
|
this.#render();
|
||||||
|
if (this.selectedTileSetId === id) {
|
||||||
|
this.#renderPreview();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reload tileset:", error);
|
||||||
|
alert(`Failed to reload tileset:\n${error.message || error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a tileset.
|
||||||
|
*
|
||||||
|
* @param {string} id Tileset ID.
|
||||||
|
*/
|
||||||
|
#removeTileSet(id) {
|
||||||
|
if (!confirm("Remove this tileset?")) return;
|
||||||
|
|
||||||
|
this.tileSets.delete(id);
|
||||||
|
|
||||||
|
if (this.selectedTileSetId === id) {
|
||||||
|
this.selectedTileSetId = null;
|
||||||
|
this.selectedTileIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
this.#renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits tileset settings.
|
||||||
|
*
|
||||||
|
* @param {string} id Tileset ID.
|
||||||
|
*/
|
||||||
|
#editTileSetSettings(id) {
|
||||||
|
const tileSet = this.tileSets.get(id);
|
||||||
|
if (!tileSet) return;
|
||||||
|
|
||||||
|
const name = prompt("Tileset name:", tileSet.name);
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
tileSet.name = name;
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a tileset.
|
||||||
|
*
|
||||||
|
* @param {string} id Tileset ID.
|
||||||
|
*/
|
||||||
|
#selectTileSet(id) {
|
||||||
|
this.selectedTileSetId = id;
|
||||||
|
this.selectedTileIndex = null;
|
||||||
|
this.#render();
|
||||||
|
this.#renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the tileset list.
|
||||||
|
*/
|
||||||
|
#render() {
|
||||||
|
if (this.tileSets.size === 0) {
|
||||||
|
this.#renderEmpty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileSetList.innerHTML = "";
|
||||||
|
|
||||||
|
this.tileSets.forEach((tileSet) => {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = "tileset-item";
|
||||||
|
item.dataset.id = tileSet.id;
|
||||||
|
|
||||||
|
if (this.selectedTileSetId === tileSet.id) {
|
||||||
|
item.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = document.createElement("div");
|
||||||
|
preview.className = "tileset-item-preview";
|
||||||
|
|
||||||
|
if (tileSet.isLoaded && tileSet.image) {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = 64;
|
||||||
|
canvas.height = 64;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
// Draw scaled down version
|
||||||
|
const scale = Math.min(64 / tileSet.image.width, 64 / tileSet.image.height);
|
||||||
|
const w = tileSet.image.width * scale;
|
||||||
|
const h = tileSet.image.height * scale;
|
||||||
|
ctx.drawImage(tileSet.image, 0, 0, w, h);
|
||||||
|
|
||||||
|
preview.appendChild(canvas);
|
||||||
|
} else {
|
||||||
|
preview.textContent = "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
item.appendChild(preview);
|
||||||
|
|
||||||
|
const info = document.createElement("div");
|
||||||
|
info.className = "tileset-item-info";
|
||||||
|
|
||||||
|
const nameEl = document.createElement("div");
|
||||||
|
nameEl.className = "tileset-item-name";
|
||||||
|
nameEl.textContent = tileSet.name;
|
||||||
|
info.appendChild(nameEl);
|
||||||
|
|
||||||
|
const meta = document.createElement("div");
|
||||||
|
meta.className = "tileset-item-meta";
|
||||||
|
meta.textContent = `128×128px • ${tileSet.getTileCount()} tiles (${tileSet.tileWidth}×${tileSet.tileHeight}px each)`;
|
||||||
|
info.appendChild(meta);
|
||||||
|
|
||||||
|
item.appendChild(info);
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "tileset-item-actions";
|
||||||
|
|
||||||
|
const openBtn = document.createElement("button");
|
||||||
|
openBtn.className = "tileset-item-btn";
|
||||||
|
openBtn.title = "Open in external editor";
|
||||||
|
openBtn.dataset.action = "open";
|
||||||
|
openBtn.dataset.id = tileSet.id;
|
||||||
|
openBtn.textContent = "✏️";
|
||||||
|
actions.appendChild(openBtn);
|
||||||
|
|
||||||
|
const reloadBtn = document.createElement("button");
|
||||||
|
reloadBtn.className = "tileset-item-btn";
|
||||||
|
reloadBtn.title = "Reload from disk";
|
||||||
|
reloadBtn.dataset.action = "reload";
|
||||||
|
reloadBtn.dataset.id = tileSet.id;
|
||||||
|
reloadBtn.textContent = "🔄";
|
||||||
|
actions.appendChild(reloadBtn);
|
||||||
|
|
||||||
|
const settingsBtn = document.createElement("button");
|
||||||
|
settingsBtn.className = "tileset-item-btn";
|
||||||
|
settingsBtn.title = "Settings";
|
||||||
|
settingsBtn.dataset.action = "edit-settings";
|
||||||
|
settingsBtn.dataset.id = tileSet.id;
|
||||||
|
settingsBtn.textContent = "⚙️";
|
||||||
|
actions.appendChild(settingsBtn);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("button");
|
||||||
|
removeBtn.className = "tileset-item-btn tileset-item-btn--danger";
|
||||||
|
removeBtn.title = "Remove tileset";
|
||||||
|
removeBtn.dataset.action = "remove";
|
||||||
|
removeBtn.dataset.id = tileSet.id;
|
||||||
|
removeBtn.textContent = "🗑️";
|
||||||
|
actions.appendChild(removeBtn);
|
||||||
|
|
||||||
|
item.appendChild(actions);
|
||||||
|
|
||||||
|
this.tileSetList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the preview panel.
|
||||||
|
*/
|
||||||
|
#renderPreview() {
|
||||||
|
if (!this.selectedTileSetId) {
|
||||||
|
this.previewInfo.textContent = "Select a tileset to preview";
|
||||||
|
this.previewInfo.style.display = "block";
|
||||||
|
this.previewCanvas.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileSet = this.tileSets.get(this.selectedTileSetId);
|
||||||
|
if (!tileSet || !tileSet.isLoaded || !tileSet.image) {
|
||||||
|
this.previewInfo.textContent = "Tileset not loaded";
|
||||||
|
this.previewInfo.style.display = "block";
|
||||||
|
this.previewCanvas.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previewInfo.style.display = "none";
|
||||||
|
this.previewCanvas.style.display = "block";
|
||||||
|
|
||||||
|
// Set canvas size
|
||||||
|
const scale = 2;
|
||||||
|
this.previewCanvas.width = tileSet.getWidth() * scale;
|
||||||
|
this.previewCanvas.height = tileSet.getHeight() * scale;
|
||||||
|
|
||||||
|
this.previewCtx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
// Draw tileset
|
||||||
|
this.previewCtx.drawImage(
|
||||||
|
tileSet.image,
|
||||||
|
0, 0,
|
||||||
|
tileSet.image.width, tileSet.image.height,
|
||||||
|
0, 0,
|
||||||
|
this.previewCanvas.width, this.previewCanvas.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
this.previewCtx.strokeStyle = "rgba(255, 255, 255, 0.3)";
|
||||||
|
this.previewCtx.lineWidth = 1;
|
||||||
|
|
||||||
|
for (let x = 0; x <= tileSet.columns; x++) {
|
||||||
|
const px = x * tileSet.tileWidth * scale;
|
||||||
|
this.previewCtx.beginPath();
|
||||||
|
this.previewCtx.moveTo(px, 0);
|
||||||
|
this.previewCtx.lineTo(px, this.previewCanvas.height);
|
||||||
|
this.previewCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = 0; y <= tileSet.rows; y++) {
|
||||||
|
const py = y * tileSet.tileHeight * scale;
|
||||||
|
this.previewCtx.beginPath();
|
||||||
|
this.previewCtx.moveTo(0, py);
|
||||||
|
this.previewCtx.lineTo(this.previewCanvas.width, py);
|
||||||
|
this.previewCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight selected tile
|
||||||
|
if (this.selectedTileIndex !== null) {
|
||||||
|
const rect = tileSet.getTileRect(this.selectedTileIndex);
|
||||||
|
this.previewCtx.strokeStyle = "rgba(255, 127, 17, 1)";
|
||||||
|
this.previewCtx.lineWidth = 2;
|
||||||
|
this.previewCtx.strokeRect(
|
||||||
|
rect.x * scale,
|
||||||
|
rect.y * scale,
|
||||||
|
rect.width * scale,
|
||||||
|
rect.height * scale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders empty state.
|
||||||
|
*/
|
||||||
|
#renderEmpty() {
|
||||||
|
this.tileSetList.innerHTML = `
|
||||||
|
<div class="tileset-empty">
|
||||||
|
<div class="tileset-empty-icon">🎨</div>
|
||||||
|
<div class="tileset-empty-text">No tilesets added</div>
|
||||||
|
<div class="tileset-empty-hint">Click "+ Add Tileset" to import a 128x128px PNG</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.#renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all tilesets.
|
||||||
|
*
|
||||||
|
* @returns {Array<TileSet>}
|
||||||
|
*/
|
||||||
|
getTileSets() {
|
||||||
|
return Array.from(this.tileSets.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a tileset programmatically.
|
||||||
|
*
|
||||||
|
* @param {TileSet} tileSet Tileset to add.
|
||||||
|
*/
|
||||||
|
addTileSet(tileSet) {
|
||||||
|
this.tileSets.set(tileSet.id, tileSet);
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes all tilesets to JSON.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
tileSets: Array.from(this.tileSets.values()).map((ts) => ts.toJSON())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads tilesets from JSON.
|
||||||
|
*
|
||||||
|
* @param {Object} data Serialized data.
|
||||||
|
*/
|
||||||
|
async fromJSON(data) {
|
||||||
|
if (!data.tileSets) return;
|
||||||
|
|
||||||
|
this.tileSets.clear();
|
||||||
|
|
||||||
|
for (const tsData of data.tileSets) {
|
||||||
|
try {
|
||||||
|
const tileSet = TileSet.fromJSON(tsData);
|
||||||
|
await tileSet.load();
|
||||||
|
this.tileSets.set(tileSet.id, tileSet);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load tileset:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ export class WorkspaceDragManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the palette ghost transform to match the supplied position.
|
* Updates the palette ghost transform to match the supplied position.
|
||||||
|
* The ghost is offset so the cursor appears at its center.
|
||||||
*
|
*
|
||||||
* @param {{ x: number, y: number }} position Snapped workspace position.
|
* @param {{ x: number, y: number }} position Snapped workspace position.
|
||||||
*/
|
*/
|
||||||
@@ -57,8 +58,14 @@ export class WorkspaceDragManager {
|
|||||||
if (!paletteDragGhost) {
|
if (!paletteDragGhost) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const width = paletteDragGhost.width || 220;
|
||||||
|
const height = paletteDragGhost.height || 140;
|
||||||
|
const centered = {
|
||||||
|
x: position.x - width / 2,
|
||||||
|
y: position.y - height / 2,
|
||||||
|
};
|
||||||
paletteDragGhost.element.style.transform =
|
paletteDragGhost.element.style.transform =
|
||||||
WorkspaceGeometry.positionToTransform(position);
|
WorkspaceGeometry.positionToTransform(centered);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +217,7 @@ export class WorkspaceDragManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerWorld = this.#clientToWorkspace(
|
const pointerWorld = this.clientToWorkspace(
|
||||||
hasClientX ? pointer.clientX : context.previousClientX,
|
hasClientX ? pointer.clientX : context.previousClientX,
|
||||||
hasClientY ? pointer.clientY : context.previousClientY
|
hasClientY ? pointer.clientY : context.previousClientY
|
||||||
);
|
);
|
||||||
@@ -526,7 +533,7 @@ export class WorkspaceDragManager {
|
|||||||
* @returns {{ x: number, y: number }}
|
* @returns {{ x: number, y: number }}
|
||||||
*/
|
*/
|
||||||
#eventToWorkspacePoint(event) {
|
#eventToWorkspacePoint(event) {
|
||||||
return this.#clientToWorkspace(event.clientX, event.clientY);
|
return this.clientToWorkspace(event.clientX, event.clientY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -560,15 +567,20 @@ export class WorkspaceDragManager {
|
|||||||
* @param {number | undefined} clientY Viewport Y coordinate.
|
* @param {number | undefined} clientY Viewport Y coordinate.
|
||||||
* @returns {{ x: number, y: number }}
|
* @returns {{ x: number, y: number }}
|
||||||
*/
|
*/
|
||||||
#clientToWorkspace(clientX, clientY) {
|
clientToWorkspace(clientX, clientY) {
|
||||||
const rect = this.workspace.workspaceElement.getBoundingClientRect();
|
const rect = this.workspace.workspaceElement.getBoundingClientRect();
|
||||||
const zoom = this.workspace.zoomLevel || 1;
|
const zoom = this.workspace.navigationManager?.getZoomLevel() ?? this.workspace.zoomLevel ?? 1;
|
||||||
|
const clampedZoom = Math.max(0.01, zoom);
|
||||||
const safeX = Number.isFinite(clientX) ? clientX : rect.left;
|
const safeX = Number.isFinite(clientX) ? clientX : rect.left;
|
||||||
const safeY = Number.isFinite(clientY) ? clientY : rect.top;
|
const safeY = Number.isFinite(clientY) ? clientY : rect.top;
|
||||||
const pending = this.workspace.navigationManager?.getPendingLayerOffset?.() ?? { x: 0, y: 0 };
|
const effectiveOffset = this.workspace.navigationManager?.getEffectiveOffset?.();
|
||||||
|
const offset = {
|
||||||
|
x: Number.isFinite(effectiveOffset?.x) ? effectiveOffset.x : 0,
|
||||||
|
y: Number.isFinite(effectiveOffset?.y) ? effectiveOffset.y : 0,
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
x: (safeX - rect.left) / zoom - pending.x,
|
x: (safeX - rect.left) / clampedZoom - offset.x,
|
||||||
y: (safeY - rect.top) / zoom - pending.y,
|
y: (safeY - rect.top) / clampedZoom - offset.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,10 +188,14 @@ export class WorkspaceDropManager {
|
|||||||
|
|
||||||
this.#workspace.paletteManager.handleDragEnd();
|
this.#workspace.paletteManager.handleDragEnd();
|
||||||
|
|
||||||
const spawn = WorkspaceGeometry.snapPositionToGrid(
|
// Snap to grid first, then offset for centering (consistent with ghost positioning)
|
||||||
this.#workspace,
|
const cursorWorld = WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event);
|
||||||
WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event)
|
const snapped = WorkspaceGeometry.snapPositionToGrid(this.#workspace, cursorWorld);
|
||||||
);
|
// Offset spawn to match the centered ghost position (roughly half ghost size)
|
||||||
|
const spawn = {
|
||||||
|
x: snapped.x - 110,
|
||||||
|
y: snapped.y - 70,
|
||||||
|
};
|
||||||
|
|
||||||
this.#workspace.addNode(definitionId, spawn);
|
this.#workspace.addNode(definitionId, spawn);
|
||||||
}
|
}
|
||||||
@@ -214,10 +218,12 @@ export class WorkspaceDropManager {
|
|||||||
|
|
||||||
const definitions = this.#getVariableSpawnDefinitions(variableId);
|
const definitions = this.#getVariableSpawnDefinitions(variableId);
|
||||||
if (!definitions.length) {
|
if (!definitions.length) {
|
||||||
const spawn = WorkspaceGeometry.snapPositionToGrid(
|
const cursorWorld = WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event);
|
||||||
this.#workspace,
|
const snapped = WorkspaceGeometry.snapPositionToGrid(this.#workspace, cursorWorld);
|
||||||
WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event)
|
const spawn = {
|
||||||
);
|
x: snapped.x - 110,
|
||||||
|
y: snapped.y - 70,
|
||||||
|
};
|
||||||
const fallback = this.#workspace.addNode("get_var", spawn);
|
const fallback = this.#workspace.addNode("get_var", spawn);
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
const nodeName = variable.name || variable.id;
|
const nodeName = variable.name || variable.id;
|
||||||
|
|||||||
@@ -29,20 +29,11 @@ export class WorkspaceGeometry {
|
|||||||
* Converts viewport coordinates into workspace-relative coordinates.
|
* Converts viewport coordinates into workspace-relative coordinates.
|
||||||
*
|
*
|
||||||
* @param {import('../BlueprintWorkspace.js').BlueprintWorkspace} workspace Workspace instance.
|
* @param {import('../BlueprintWorkspace.js').BlueprintWorkspace} workspace Workspace instance.
|
||||||
* @param {{ clientX: number, clientY: number }} event Pointer-like event payload.
|
* @param {{ clientX?: number, clientY?: number }} event Pointer-like event payload.
|
||||||
* @returns {{ x: number, y: number }}
|
* @returns {{ x: number, y: number }}
|
||||||
*/
|
*/
|
||||||
static eventToWorkspacePoint(workspace, event) {
|
static eventToWorkspacePoint(workspace, event) {
|
||||||
const rect = workspace.workspaceElement.getBoundingClientRect();
|
return workspace.dragManager.clientToWorkspace(event?.clientX, event?.clientY);
|
||||||
const rawX = event?.clientX ?? 0;
|
|
||||||
const rawY = event?.clientY ?? 0;
|
|
||||||
const x = rawX - rect.left;
|
|
||||||
const y = rawY - rect.top;
|
|
||||||
const zoom = workspace.zoomLevel || 1;
|
|
||||||
return {
|
|
||||||
x: Math.max(0, Math.min(rect.width, x)) / zoom,
|
|
||||||
y: Math.max(0, Math.min(rect.height, y)) / zoom,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,8 +45,9 @@ export class WorkspaceGeometry {
|
|||||||
*/
|
*/
|
||||||
static snapPositionToGrid(workspace, position) {
|
static snapPositionToGrid(workspace, position) {
|
||||||
const size = workspace.gridSize || DEFAULT_GRID_SIZE;
|
const size = workspace.gridSize || DEFAULT_GRID_SIZE;
|
||||||
const offsetX = workspace.workspaceBackgroundOffset?.x ?? 0;
|
const effectiveOffset = workspace.navigationManager?.getEffectiveOffset?.();
|
||||||
const offsetY = workspace.workspaceBackgroundOffset?.y ?? 0;
|
const offsetX = Number.isFinite(effectiveOffset?.x) ? effectiveOffset.x : 0;
|
||||||
|
const offsetY = Number.isFinite(effectiveOffset?.y) ? effectiveOffset.y : 0;
|
||||||
const snap = (value, offset) => {
|
const snap = (value, offset) => {
|
||||||
const local = value - offset;
|
const local = value - offset;
|
||||||
const snapped = Math.round(local / size) * size;
|
const snapped = Math.round(local / size) * size;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { createColorSwatchPicker } from '../ColorSwatchPicker.js';
|
||||||
|
import { getTypeColor } from '../../types/TypeRegistry.js';
|
||||||
|
|
||||||
/** @typedef {import('../../core/BlueprintNode.js').BlueprintNode} BlueprintNode */
|
/** @typedef {import('../../core/BlueprintNode.js').BlueprintNode} BlueprintNode */
|
||||||
/** @typedef {import('../../core/BlueprintNode.js').PinDescriptor} PinDescriptor */
|
/** @typedef {import('../../core/BlueprintNode.js').PinDescriptor} PinDescriptor */
|
||||||
/** @typedef {import('../../core/NodeGraph.js').NodeGraph} NodeGraph */
|
/** @typedef {import('../../core/NodeGraph.js').NodeGraph} NodeGraph */
|
||||||
@@ -75,6 +78,7 @@ export class WorkspaceInlineEditorManager {
|
|||||||
"number_literal",
|
"number_literal",
|
||||||
"string_literal",
|
"string_literal",
|
||||||
"boolean_literal",
|
"boolean_literal",
|
||||||
|
"color_literal",
|
||||||
]);
|
]);
|
||||||
if (!supported.has(node.type)) {
|
if (!supported.has(node.type)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -100,6 +104,27 @@ export class WorkspaceInlineEditorManager {
|
|||||||
editor.appendChild(label);
|
editor.appendChild(label);
|
||||||
|
|
||||||
const graph = this.#getGraph();
|
const graph = this.#getGraph();
|
||||||
|
|
||||||
|
if (node.type === "color_literal") {
|
||||||
|
const picker = createColorSwatchPicker({
|
||||||
|
id: controlId,
|
||||||
|
value: node.properties.value ?? 7,
|
||||||
|
onChange: (index) => {
|
||||||
|
graph.setNodeProperty(node.id, "value", index);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
editor.appendChild(picker.element);
|
||||||
|
|
||||||
|
const setValue = (rawValue) => {
|
||||||
|
const n = Number(rawValue);
|
||||||
|
picker.setValue(Number.isFinite(n) ? n : 7);
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry = this.#ensureEntry(node.id);
|
||||||
|
entry.literal = { setValue };
|
||||||
|
return entry.literal;
|
||||||
|
}
|
||||||
|
|
||||||
let control;
|
let control;
|
||||||
const commitNumber = () => {
|
const commitNumber = () => {
|
||||||
const numeric = Number(control.value);
|
const numeric = Number(control.value);
|
||||||
@@ -176,6 +201,7 @@ export class WorkspaceInlineEditorManager {
|
|||||||
{ value: "string", label: "String" },
|
{ value: "string", label: "String" },
|
||||||
{ value: "boolean", label: "Boolean" },
|
{ value: "boolean", label: "Boolean" },
|
||||||
{ value: "table", label: "Table" },
|
{ value: "table", label: "Table" },
|
||||||
|
{ value: "color", label: "Color" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ensureBody = () => {
|
const ensureBody = () => {
|
||||||
@@ -274,6 +300,8 @@ export class WorkspaceInlineEditorManager {
|
|||||||
return "valueBoolean";
|
return "valueBoolean";
|
||||||
case "table":
|
case "table":
|
||||||
return "valueTable";
|
return "valueTable";
|
||||||
|
case "color":
|
||||||
|
return "valueColor";
|
||||||
case "number":
|
case "number":
|
||||||
default:
|
default:
|
||||||
return "valueNumber";
|
return "valueNumber";
|
||||||
@@ -288,6 +316,8 @@ export class WorkspaceInlineEditorManager {
|
|||||||
return "string";
|
return "string";
|
||||||
case "table":
|
case "table":
|
||||||
return "table";
|
return "table";
|
||||||
|
case "color":
|
||||||
|
return "color";
|
||||||
default:
|
default:
|
||||||
return "number";
|
return "number";
|
||||||
}
|
}
|
||||||
@@ -301,6 +331,8 @@ export class WorkspaceInlineEditorManager {
|
|||||||
return false;
|
return false;
|
||||||
case "table":
|
case "table":
|
||||||
return "{}";
|
return "{}";
|
||||||
|
case "color":
|
||||||
|
return 7;
|
||||||
case "number":
|
case "number":
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
@@ -313,6 +345,7 @@ export class WorkspaceInlineEditorManager {
|
|||||||
const normalized = normalizeType(type);
|
const normalized = normalizeType(type);
|
||||||
activeType = normalized;
|
activeType = normalized;
|
||||||
typeButton.dataset.variableType = normalized;
|
typeButton.dataset.variableType = normalized;
|
||||||
|
typeButton.style.setProperty("--overview-variable-color", getTypeColor(normalized));
|
||||||
const label = formatType(normalized);
|
const label = formatType(normalized);
|
||||||
typeButton.title = `Type: ${label}`;
|
typeButton.title = `Type: ${label}`;
|
||||||
typeButton.setAttribute(
|
typeButton.setAttribute(
|
||||||
@@ -378,6 +411,20 @@ export class WorkspaceInlineEditorManager {
|
|||||||
graph.setNodeProperty(currentNode.id, key, textarea.value);
|
graph.setNodeProperty(currentNode.id, key, textarea.value);
|
||||||
});
|
});
|
||||||
control = textarea;
|
control = textarea;
|
||||||
|
} else if (desiredKind === "color") {
|
||||||
|
const rawColor = currentNode.properties?.[key];
|
||||||
|
const n = Number(rawColor);
|
||||||
|
const initialIndex = Number.isFinite(n) ? Math.max(0, Math.min(15, Math.round(n))) : 7;
|
||||||
|
const picker = createColorSwatchPicker({
|
||||||
|
value: initialIndex,
|
||||||
|
onChange: (index) => {
|
||||||
|
graph.setNodeProperty(currentNode.id, key, index);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
picker.element.dataset.localValueKind = "color";
|
||||||
|
/** @type {any} */ (picker.element)._setColorValue = picker.setValue;
|
||||||
|
valueField.appendChild(picker.element);
|
||||||
|
return picker.element;
|
||||||
} else {
|
} else {
|
||||||
const numberInput = document.createElement("input");
|
const numberInput = document.createElement("input");
|
||||||
numberInput.type = "number";
|
numberInput.type = "number";
|
||||||
@@ -407,6 +454,10 @@ export class WorkspaceInlineEditorManager {
|
|||||||
if (control.value !== nextValue) {
|
if (control.value !== nextValue) {
|
||||||
control.value = nextValue;
|
control.value = nextValue;
|
||||||
}
|
}
|
||||||
|
} else if (/** @type {any} */ (control)._setColorValue) {
|
||||||
|
const n = Number(current);
|
||||||
|
const index = Number.isFinite(n) ? Math.max(0, Math.min(15, Math.round(n))) : 7;
|
||||||
|
/** @type {any} */ (control)._setColorValue(index);
|
||||||
} else if (control instanceof HTMLTextAreaElement) {
|
} else if (control instanceof HTMLTextAreaElement) {
|
||||||
const nextValue = typeof current === "string" ? current : "{}";
|
const nextValue = typeof current === "string" ? current : "{}";
|
||||||
if (control.value !== nextValue) {
|
if (control.value !== nextValue) {
|
||||||
@@ -534,18 +585,10 @@ export class WorkspaceInlineEditorManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isIfCondition = node.type === "if" && pin.id === "condition";
|
const isIfCondition = node.type === "if" && pin.id === "condition";
|
||||||
const isExtcmdCommand =
|
const isColorPin = pin.kind === "color";
|
||||||
node.type === "system_extcmd" && pin.id === "command";
|
const pinOptions =
|
||||||
const isNamedButtonSelector =
|
Array.isArray(pin.options) && pin.options.length > 0 ? pin.options : null;
|
||||||
node.type === "input_button_constants" && pin.id === "button";
|
if (!isIfCondition && !pinOptions && !isColorPin) {
|
||||||
const isNamedPlayerSelector =
|
|
||||||
node.type === "input_button_constants" && pin.id === "player";
|
|
||||||
if (
|
|
||||||
!isIfCondition &&
|
|
||||||
!isExtcmdCommand &&
|
|
||||||
!isNamedButtonSelector &&
|
|
||||||
!isNamedPlayerSelector
|
|
||||||
) {
|
|
||||||
container.classList.remove("has-inline-dropdown");
|
container.classList.remove("has-inline-dropdown");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -555,31 +598,14 @@ export class WorkspaceInlineEditorManager {
|
|||||||
const graph = this.#getGraph();
|
const graph = this.#getGraph();
|
||||||
|
|
||||||
if (!(propertyKey in node.properties)) {
|
if (!(propertyKey in node.properties)) {
|
||||||
if (isExtcmdCommand || isNamedButtonSelector || isNamedPlayerSelector) {
|
if (pinOptions) {
|
||||||
const definition = this.registry.get(node.type);
|
const defaultStr =
|
||||||
const targetKey = isExtcmdCommand
|
pin.defaultValue != null
|
||||||
? "command"
|
? String(pin.defaultValue)
|
||||||
: isNamedButtonSelector
|
: String(pinOptions[0]?.value ?? "");
|
||||||
? "button"
|
node.properties[propertyKey] = defaultStr;
|
||||||
: "player";
|
if (!(pin.id in node.properties)) {
|
||||||
const schema = definition?.properties?.find(
|
node.properties[pin.id] = defaultStr;
|
||||||
(prop) => prop.key === targetKey
|
|
||||||
);
|
|
||||||
const options = Array.isArray(schema?.options) ? schema.options : [];
|
|
||||||
const defaultOption =
|
|
||||||
typeof node.properties[targetKey] === "string"
|
|
||||||
? node.properties[targetKey]
|
|
||||||
: String(schema?.defaultValue ?? options[0]?.value ?? "");
|
|
||||||
if (typeof node.properties[targetKey] !== "string") {
|
|
||||||
node.properties[targetKey] = defaultOption;
|
|
||||||
}
|
|
||||||
if (isExtcmdCommand) {
|
|
||||||
node.properties[propertyKey] = defaultOption;
|
|
||||||
} else {
|
|
||||||
const numericDefault = Number.parseInt(defaultOption, 10);
|
|
||||||
node.properties[propertyKey] = Number.isFinite(numericDefault)
|
|
||||||
? numericDefault
|
|
||||||
: 0;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
node.properties[propertyKey] = this.#defaultValueForPin(pin);
|
node.properties[propertyKey] = this.#defaultValueForPin(pin);
|
||||||
@@ -591,7 +617,7 @@ export class WorkspaceInlineEditorManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
if (isExtcmdCommand || isNamedButtonSelector || isNamedPlayerSelector) {
|
if (pinOptions) {
|
||||||
wrapper = document.createElement("div");
|
wrapper = document.createElement("div");
|
||||||
wrapper.className = "pin-inline-control pin-inline-dropdown";
|
wrapper.className = "pin-inline-control pin-inline-dropdown";
|
||||||
container.appendChild(wrapper);
|
container.appendChild(wrapper);
|
||||||
@@ -631,29 +657,14 @@ export class WorkspaceInlineEditorManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExtcmdCommand || isNamedButtonSelector || isNamedPlayerSelector) {
|
if (pinOptions) {
|
||||||
const definition = this.registry.get(node.type);
|
const optionValues = pinOptions.map((o) => String(o.value));
|
||||||
const targetKey = isExtcmdCommand
|
|
||||||
? "command"
|
|
||||||
: isNamedButtonSelector
|
|
||||||
? "button"
|
|
||||||
: "player";
|
|
||||||
const schema = definition?.properties?.find(
|
|
||||||
(prop) => prop.key === targetKey
|
|
||||||
);
|
|
||||||
const options = Array.isArray(schema?.options) ? schema.options : [];
|
|
||||||
|
|
||||||
const select = document.createElement("select");
|
const select = document.createElement("select");
|
||||||
select.className = "pin-inline-select";
|
select.className = "pin-inline-select";
|
||||||
const ariaLabel = isExtcmdCommand
|
select.setAttribute("aria-label", pin.name || pin.id);
|
||||||
? "Command"
|
|
||||||
: isNamedButtonSelector
|
|
||||||
? "Button"
|
|
||||||
: "Player";
|
|
||||||
select.setAttribute("aria-label", ariaLabel);
|
|
||||||
|
|
||||||
const values = options.map((option) => String(option.value));
|
pinOptions.forEach((option) => {
|
||||||
options.forEach((option) => {
|
|
||||||
const element = document.createElement("option");
|
const element = document.createElement("option");
|
||||||
element.value = String(option.value);
|
element.value = String(option.value);
|
||||||
element.textContent = option.label ?? String(option.value);
|
element.textContent = option.label ?? String(option.value);
|
||||||
@@ -661,30 +672,14 @@ export class WorkspaceInlineEditorManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const normalizeValue = (value) => {
|
const normalizeValue = (value) => {
|
||||||
const candidate =
|
const candidate = value == null ? "" : String(value);
|
||||||
typeof value === "string" ? value : String(value ?? "");
|
return optionValues.includes(candidate)
|
||||||
if (values.length === 0) {
|
? candidate
|
||||||
return candidate;
|
: (optionValues[0] ?? "");
|
||||||
}
|
|
||||||
if (values.includes(candidate)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
const schemaDefault =
|
|
||||||
schema?.defaultValue != null ? String(schema.defaultValue) : null;
|
|
||||||
if (schemaDefault && values.includes(schemaDefault)) {
|
|
||||||
return schemaDefault;
|
|
||||||
}
|
|
||||||
return values[0];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setValue = (value) => {
|
const setValue = (value) => {
|
||||||
const normalized = normalizeValue(
|
select.value = normalizeValue(value);
|
||||||
(isNamedButtonSelector || isNamedPlayerSelector) &&
|
|
||||||
typeof node.properties[targetKey] === "string"
|
|
||||||
? node.properties[targetKey]
|
|
||||||
: value
|
|
||||||
);
|
|
||||||
select.value = normalized;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setConnected = (connected) => {
|
const setConnected = (connected) => {
|
||||||
@@ -694,23 +689,42 @@ export class WorkspaceInlineEditorManager {
|
|||||||
|
|
||||||
select.addEventListener("change", () => {
|
select.addEventListener("change", () => {
|
||||||
const nextValue = select.value;
|
const nextValue = select.value;
|
||||||
if (isExtcmdCommand) {
|
|
||||||
graph.setNodeProperty(node.id, propertyKey, nextValue);
|
graph.setNodeProperty(node.id, propertyKey, nextValue);
|
||||||
graph.setNodeProperty(node.id, "command", nextValue);
|
graph.setNodeProperty(node.id, pin.id, nextValue);
|
||||||
} else {
|
|
||||||
const normalized = normalizeValue(nextValue);
|
|
||||||
const numeric = Number.parseInt(normalized, 10);
|
|
||||||
graph.setNodeProperty(
|
|
||||||
node.id,
|
|
||||||
propertyKey,
|
|
||||||
Number.isFinite(numeric) ? numeric : 0
|
|
||||||
);
|
|
||||||
graph.setNodeProperty(node.id, targetKey, normalized);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.appendChild(select);
|
wrapper.appendChild(select);
|
||||||
|
|
||||||
|
entry.pinEditors.set(pin.id, { setValue, setConnected });
|
||||||
|
setValue(node.properties[propertyKey]);
|
||||||
|
setConnected(this.#hasConnection(node.id, pin.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isColorPin) {
|
||||||
|
const rawDefault = node.properties[propertyKey];
|
||||||
|
const n = Number(rawDefault);
|
||||||
|
const initialIndex = Number.isFinite(n)
|
||||||
|
? Math.max(0, Math.min(15, Math.round(n)))
|
||||||
|
: 7;
|
||||||
|
|
||||||
|
const picker = createColorSwatchPicker({
|
||||||
|
value: initialIndex,
|
||||||
|
onChange: (index) => {
|
||||||
|
graph.setNodeProperty(node.id, propertyKey, index);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
wrapper.appendChild(picker.element);
|
||||||
|
|
||||||
|
const setValue = (value) => {
|
||||||
|
const num = Number(value);
|
||||||
|
picker.setValue(Number.isFinite(num) ? num : 7);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setConnected = (connected) => {
|
||||||
|
wrapper.classList.toggle("is-hidden", connected);
|
||||||
|
};
|
||||||
|
|
||||||
entry.pinEditors.set(pin.id, { setValue, setConnected });
|
entry.pinEditors.set(pin.id, { setValue, setConnected });
|
||||||
setValue(node.properties[propertyKey]);
|
setValue(node.properties[propertyKey]);
|
||||||
setConnected(this.#hasConnection(node.id, pin.id));
|
setConnected(this.#hasConnection(node.id, pin.id));
|
||||||
@@ -765,45 +779,12 @@ export class WorkspaceInlineEditorManager {
|
|||||||
const propertyKey = this.#pinPropertyKey(pinId);
|
const propertyKey = this.#pinPropertyKey(pinId);
|
||||||
let value;
|
let value;
|
||||||
|
|
||||||
if (node.type === "system_extcmd" && pinId === "command") {
|
if (Array.isArray(pin?.options) && pin.options.length > 0) {
|
||||||
const commandValue =
|
const canonical = node.properties[pinId];
|
||||||
typeof node.properties.command === "string"
|
if (canonical != null) {
|
||||||
? node.properties.command
|
const strValue = String(canonical);
|
||||||
: null;
|
if (node.properties[propertyKey] !== strValue) {
|
||||||
if (
|
node.properties[propertyKey] = strValue;
|
||||||
commandValue !== null &&
|
|
||||||
node.properties[propertyKey] !== commandValue
|
|
||||||
) {
|
|
||||||
node.properties[propertyKey] = commandValue;
|
|
||||||
}
|
|
||||||
} else if (node.type === "input_button_constants") {
|
|
||||||
if (pinId === "button") {
|
|
||||||
const rawButton =
|
|
||||||
typeof node.properties.button === "string"
|
|
||||||
? node.properties.button
|
|
||||||
: null;
|
|
||||||
if (rawButton !== null) {
|
|
||||||
const numericButton = Number.parseInt(rawButton, 10);
|
|
||||||
const safeButton = Number.isFinite(numericButton)
|
|
||||||
? numericButton
|
|
||||||
: 0;
|
|
||||||
if (node.properties[propertyKey] !== safeButton) {
|
|
||||||
node.properties[propertyKey] = safeButton;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pinId === "player") {
|
|
||||||
const rawPlayer =
|
|
||||||
typeof node.properties.player === "string"
|
|
||||||
? node.properties.player
|
|
||||||
: null;
|
|
||||||
if (rawPlayer !== null) {
|
|
||||||
const numericPlayer = Number.parseInt(rawPlayer, 10);
|
|
||||||
const safePlayer = Number.isFinite(numericPlayer)
|
|
||||||
? numericPlayer
|
|
||||||
: 0;
|
|
||||||
if (node.properties[propertyKey] !== safePlayer) {
|
|
||||||
node.properties[propertyKey] = safePlayer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -905,6 +886,8 @@ export class WorkspaceInlineEditorManager {
|
|||||||
return 0;
|
return 0;
|
||||||
case "string":
|
case "string":
|
||||||
return "";
|
return "";
|
||||||
|
case "color":
|
||||||
|
return 7;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,10 +106,6 @@ export class WorkspaceNavigationManager {
|
|||||||
this.#targetZoomLevel = clamped;
|
this.#targetZoomLevel = clamped;
|
||||||
this.#zoomLevel = clamped;
|
this.#zoomLevel = clamped;
|
||||||
this.updateZoomDisplay(skipConnectionRender);
|
this.updateZoomDisplay(skipConnectionRender);
|
||||||
if (!silent) {
|
|
||||||
this.#markViewDirty("view", 200);
|
|
||||||
this.#schedulePersist();
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +147,9 @@ export class WorkspaceNavigationManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a screen-space point to workspace coordinates.
|
* Converts a screen-space point to workspace coordinates.
|
||||||
|
* Note: This returns screen coordinates divided by zoom only, without subtracting
|
||||||
|
* the background offset. This is intentional for zoom pivot calculations.
|
||||||
|
* For true world coordinates, use dragManager.clientToWorkspace().
|
||||||
*
|
*
|
||||||
* @param {{ x: number, y: number }} point Screen-space point relative to the workspace element.
|
* @param {{ x: number, y: number }} point Screen-space point relative to the workspace element.
|
||||||
* @returns {{ x: number, y: number }}
|
* @returns {{ x: number, y: number }}
|
||||||
@@ -273,8 +272,6 @@ export class WorkspaceNavigationManager {
|
|||||||
}
|
}
|
||||||
// Restore clean scale-only transform now that node positions are committed.
|
// Restore clean scale-only transform now that node positions are committed.
|
||||||
this.updateZoomDisplay(false);
|
this.updateZoomDisplay(false);
|
||||||
this.#markViewDirty("view", 0);
|
|
||||||
this.#schedulePersist();
|
|
||||||
} else {
|
} else {
|
||||||
this.#smoothZoomRafId = requestAnimationFrame(() => this.#tickSmoothZoom());
|
this.#smoothZoomRafId = requestAnimationFrame(() => this.#tickSmoothZoom());
|
||||||
}
|
}
|
||||||
@@ -320,6 +317,7 @@ export class WorkspaceNavigationManager {
|
|||||||
this.#rebasePanGestureAfterZoom(pointer);
|
this.#rebasePanGestureAfterZoom(pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateZoomDisplay(true);
|
||||||
this.#renderConnections();
|
this.#renderConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +415,7 @@ export class WorkspaceNavigationManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.translateWorkspace(delta);
|
this.translateWorkspace(delta);
|
||||||
|
this.updateZoomDisplay(false);
|
||||||
this.#workspace.selectNode(nodeId);
|
this.#workspace.selectNode(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,30 +488,29 @@ export class WorkspaceNavigationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetZoom = Math.max(this.#zoomConfig.min, Math.min(this.#zoomConfig.max, targetZoom));
|
targetZoom = Math.max(this.#zoomConfig.min, Math.min(this.#zoomConfig.max, targetZoom));
|
||||||
this.setZoomLevel(targetZoom);
|
|
||||||
|
|
||||||
const contentCenter = {
|
const contentCenter = {
|
||||||
x: minX + boundsWidth / 2,
|
x: minX + boundsWidth / 2,
|
||||||
y: minY + boundsHeight / 2,
|
y: minY + boundsHeight / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewportCenter = this.screenPointToWorld({
|
// Directly compute the background offset that places contentCenter at screen center,
|
||||||
x: rect.width / 2,
|
// then set zoom + offset together so a single updateZoomDisplay call applies both.
|
||||||
y: rect.height / 2,
|
const newOffsetX = rect.width / 2 / targetZoom - contentCenter.x;
|
||||||
});
|
const newOffsetY = rect.height / 2 / targetZoom - contentCenter.y;
|
||||||
|
|
||||||
const delta = {
|
// Cancel any running smooth zoom before snapping.
|
||||||
x: viewportCenter.x - contentCenter.x,
|
if (this.#smoothZoomRafId !== null) {
|
||||||
y: viewportCenter.y - contentCenter.y,
|
cancelAnimationFrame(this.#smoothZoomRafId);
|
||||||
};
|
this.#smoothZoomRafId = null;
|
||||||
|
this.#smoothZoomFocus = null;
|
||||||
if (Math.abs(delta.x) > 0.0001 || Math.abs(delta.y) > 0.0001) {
|
this.#pendingLayerOffset = { x: 0, y: 0 };
|
||||||
this.translateWorkspace(delta);
|
|
||||||
this.#renderConnections();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#markViewDirty("view", 200);
|
this.#targetZoomLevel = targetZoom;
|
||||||
this.#schedulePersist();
|
this.#zoomLevel = targetZoom;
|
||||||
|
this.setBackgroundOffset({ x: newOffsetX, y: newOffsetY });
|
||||||
|
this.updateZoomDisplay(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -536,6 +534,7 @@ export class WorkspaceNavigationManager {
|
|||||||
startY: event.clientY,
|
startY: event.clientY,
|
||||||
hasMoved: false,
|
hasMoved: false,
|
||||||
backgroundOrigin: { ...this.#backgroundOffset },
|
backgroundOrigin: { ...this.#backgroundOffset },
|
||||||
|
lastDelta: { x: 0, y: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerMove = (moveEvent) => {
|
const handlePointerMove = (moveEvent) => {
|
||||||
@@ -564,12 +563,14 @@ export class WorkspaceNavigationManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#panState.lastDelta = worldDelta;
|
||||||
|
|
||||||
const backgroundOffset = {
|
const backgroundOffset = {
|
||||||
x: this.#panState.backgroundOrigin.x + worldDelta.x,
|
x: this.#panState.backgroundOrigin.x + worldDelta.x,
|
||||||
y: this.#panState.backgroundOrigin.y + worldDelta.y,
|
y: this.#panState.backgroundOrigin.y + worldDelta.y,
|
||||||
};
|
};
|
||||||
this.setBackgroundOffset(backgroundOffset);
|
this.setBackgroundOffset(backgroundOffset);
|
||||||
this.updateZoomDisplay();
|
this.updateZoomDisplay(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (upEvent) => {
|
const handlePointerUp = (upEvent) => {
|
||||||
@@ -589,6 +590,7 @@ export class WorkspaceNavigationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.#lastPanTimestamp = this.#timestamp();
|
this.#lastPanTimestamp = this.#timestamp();
|
||||||
|
this.#renderConnections();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.#panState = state;
|
this.#panState = state;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { WorkspaceGeometry } from "./WorkspaceGeometry.js";
|
import { WorkspaceGeometry } from "./WorkspaceGeometry.js";
|
||||||
|
import { getTypeColor } from "../../types/TypeRegistry.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {"input"|"output"} PinDirection
|
* @typedef {"input"|"output"} PinDirection
|
||||||
@@ -300,6 +301,7 @@ export class WorkspaceNodeRenderer {
|
|||||||
container.dataset.pinId = pin.id;
|
container.dataset.pinId = pin.id;
|
||||||
container.dataset.nodeId = node.id;
|
container.dataset.nodeId = node.id;
|
||||||
container.dataset.type = pin.kind;
|
container.dataset.type = pin.kind;
|
||||||
|
container.style.setProperty("--pin-kind-color", getTypeColor(pin.kind));
|
||||||
const isStandardExec =
|
const isStandardExec =
|
||||||
pin.kind === "exec" && (pin.id === "exec_in" || pin.id === "exec_out");
|
pin.kind === "exec" && (pin.id === "exec_in" || pin.id === "exec_out");
|
||||||
if (isStandardExec) {
|
if (isStandardExec) {
|
||||||
|
|||||||
@@ -54,28 +54,48 @@ export class WorkspacePersistenceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.cancelScheduledPersist();
|
this.cancelScheduledPersist();
|
||||||
|
this.#workspace.handlePersistenceStateChange?.({
|
||||||
|
status: "pending",
|
||||||
|
source: "auto",
|
||||||
|
});
|
||||||
this.#timerId = window.setTimeout(() => {
|
this.#timerId = window.setTimeout(() => {
|
||||||
this.#timerId = null;
|
this.#timerId = null;
|
||||||
this.persistImmediately();
|
this.persistImmediately("auto");
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes current workspace state and writes it to localStorage immediately.
|
* Serializes current workspace state and writes it to localStorage immediately.
|
||||||
|
*
|
||||||
|
* @param {'manual'|'auto'} [source]
|
||||||
*/
|
*/
|
||||||
persistImmediately() {
|
persistImmediately(source = "manual") {
|
||||||
if (!this.#storageAvailable || typeof window === "undefined") {
|
if (!this.#storageAvailable || typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cancelScheduledPersist();
|
||||||
|
this.#workspace.handlePersistenceStateChange?.({
|
||||||
|
status: "saving",
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = this.serializeWorkspace();
|
const payload = this.serializeWorkspace();
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
WORKSPACE_STORAGE_KEY,
|
WORKSPACE_STORAGE_KEY,
|
||||||
JSON.stringify(payload)
|
JSON.stringify(payload)
|
||||||
);
|
);
|
||||||
|
this.#workspace.handlePersistenceStateChange?.({
|
||||||
|
status: "saved",
|
||||||
|
source,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to persist workspace state", error);
|
console.error("Failed to persist workspace state", error);
|
||||||
|
this.#workspace.handlePersistenceStateChange?.({
|
||||||
|
status: "error",
|
||||||
|
source,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +110,7 @@ export class WorkspacePersistenceManager {
|
|||||||
.filter((entry) => Boolean(entry))
|
.filter((entry) => Boolean(entry))
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const variable =
|
const variable =
|
||||||
/** @type {{ id: string, name: string, type: 'any'|'number'|'string'|'boolean'|'table', defaultValue: import('../BlueprintWorkspace.js').VariableDefaultValue }} */ (
|
/** @type {{ id: string, name: string, type: 'any'|'number'|'string'|'boolean'|'table'|'color', defaultValue: import('../BlueprintWorkspace.js').VariableDefaultValue }} */ (
|
||||||
entry
|
entry
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -90,10 +90,12 @@ export class WorkspaceWireCutManager {
|
|||||||
*/
|
*/
|
||||||
#clientToSvg(clientX, clientY) {
|
#clientToSvg(clientX, clientY) {
|
||||||
const zoom = this.#workspace.zoomLevel || 1;
|
const zoom = this.#workspace.zoomLevel || 1;
|
||||||
|
const offset = this.#workspace.navigationManager?.getEffectiveOffset() ??
|
||||||
|
this.#workspace.workspaceBackgroundOffset ?? { x: 0, y: 0 };
|
||||||
const rect = this.#workspace.workspaceElement.getBoundingClientRect();
|
const rect = this.#workspace.workspaceElement.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: (clientX - rect.left) / zoom,
|
x: (clientX - rect.left) / zoom - offset.x,
|
||||||
y: (clientY - rect.top) / zoom,
|
y: (clientY - rect.top) / zoom - offset.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { VARIABLE_SORT_MIME } from "../BlueprintWorkspaceConstants.js";
|
import { VARIABLE_SORT_MIME } from "../BlueprintWorkspaceConstants.js";
|
||||||
|
import { getTypeColor } from "../../types/TypeRegistry.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the global variable summary list.
|
* Renders the global variable summary list.
|
||||||
@@ -225,6 +226,7 @@ export function renderVariableList({
|
|||||||
row.setAttribute("role", "button");
|
row.setAttribute("role", "button");
|
||||||
row.dataset.variableId = variable.id;
|
row.dataset.variableId = variable.id;
|
||||||
row.dataset.variableType = variable.type;
|
row.dataset.variableType = variable.type;
|
||||||
|
row.style.setProperty("--overview-variable-color", getTypeColor(variable.type));
|
||||||
row.draggable = true;
|
row.draggable = true;
|
||||||
row.setAttribute("aria-pressed", "false");
|
row.setAttribute("aria-pressed", "false");
|
||||||
|
|
||||||
@@ -330,6 +332,7 @@ export function renderVariableList({
|
|||||||
typeButton.type = "button";
|
typeButton.type = "button";
|
||||||
typeButton.className = "variable-type-button";
|
typeButton.className = "variable-type-button";
|
||||||
typeButton.dataset.variableType = variable.type;
|
typeButton.dataset.variableType = variable.type;
|
||||||
|
typeButton.style.setProperty("--overview-variable-color", getTypeColor(variable.type));
|
||||||
typeButton.setAttribute("aria-haspopup", "menu");
|
typeButton.setAttribute("aria-haspopup", "menu");
|
||||||
typeButton.setAttribute("aria-expanded", "false");
|
typeButton.setAttribute("aria-expanded", "false");
|
||||||
typeButton.title = `Type: ${formatVariableType(variable.type)}`;
|
typeButton.title = `Type: ${formatVariableType(variable.type)}`;
|
||||||
|
|||||||
176
sprite-editor.html
Normal file
176
sprite-editor.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PICO-8 Sprite Editor - Picograph</title>
|
||||||
|
<link rel="stylesheet" href="styles/main.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprite-editor-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-bottom: 1px solid var(--edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprite-editor-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprite-editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprite-editor-btn {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sprite-editor-header">
|
||||||
|
<div class="sprite-editor-title">🎨 Sprite Editor</div>
|
||||||
|
<div></div>
|
||||||
|
<div class="sprite-editor-actions">
|
||||||
|
<button class="sprite-editor-btn" id="export-btn">Export to .p8</button>
|
||||||
|
<button class="sprite-editor-btn" id="import-btn">Import from .p8</button>
|
||||||
|
<button class="sprite-editor-btn" id="save-json-btn">Save JSON</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editor-container"></div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { SpriteEditor } from './scripts/ui/SpriteEditor.js';
|
||||||
|
import { SpriteSheet } from './scripts/core/SpriteSheet.js';
|
||||||
|
|
||||||
|
// Initialize the sprite editor
|
||||||
|
const container = document.getElementById('editor-container');
|
||||||
|
const spriteSheet = new SpriteSheet();
|
||||||
|
const editor = new SpriteEditor(container, spriteSheet);
|
||||||
|
|
||||||
|
// Export to .p8 format
|
||||||
|
document.getElementById('export-btn').addEventListener('click', () => {
|
||||||
|
const gfxData = editor.getSpriteSheet().toP8Gfx();
|
||||||
|
const flagData = editor.getSpriteSheet().toP8Flags();
|
||||||
|
|
||||||
|
const p8Content = `pico-8 cartridge // http://www.pico-8.com
|
||||||
|
version 41
|
||||||
|
__lua__
|
||||||
|
-- your code here
|
||||||
|
__gfx__
|
||||||
|
${gfxData}
|
||||||
|
__gff__
|
||||||
|
${flagData}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Download the .p8 file
|
||||||
|
const blob = new Blob([p8Content], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'sprites.p8';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import from .p8 format
|
||||||
|
document.getElementById('import-btn').addEventListener('click', () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.p8';
|
||||||
|
|
||||||
|
input.addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const text = await file.text();
|
||||||
|
|
||||||
|
// Extract __gfx__ section
|
||||||
|
const gfxMatch = text.match(/__gfx__\s*\n([\s\S]*?)(?=\n__|$)/);
|
||||||
|
if (gfxMatch) {
|
||||||
|
editor.getSpriteSheet().fromP8Gfx(gfxMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract __gff__ section
|
||||||
|
const gffMatch = text.match(/__gff__\s*\n([\s\S]*?)(?=\n__|$)/);
|
||||||
|
if (gffMatch) {
|
||||||
|
editor.getSpriteSheet().fromP8Flags(gffMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
editor.setSpriteSheet(editor.getSpriteSheet());
|
||||||
|
});
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save as JSON
|
||||||
|
document.getElementById('save-json-btn').addEventListener('click', () => {
|
||||||
|
const json = JSON.stringify(editor.getSpriteSheet().toJSON(), null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'sprites.json';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example: Draw a smiley face on sprite 0
|
||||||
|
function drawExampleSprite() {
|
||||||
|
const sheet = editor.getSpriteSheet();
|
||||||
|
|
||||||
|
// Draw a simple smiley face
|
||||||
|
// Face outline (yellow)
|
||||||
|
for (let x = 1; x < 7; x++) {
|
||||||
|
sheet.setPixel(x, 1, 10); // Top
|
||||||
|
sheet.setPixel(x, 6, 10); // Bottom
|
||||||
|
}
|
||||||
|
for (let y = 2; y < 6; y++) {
|
||||||
|
sheet.setPixel(1, y, 10); // Left
|
||||||
|
sheet.setPixel(6, y, 10); // Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eyes (black)
|
||||||
|
sheet.setPixel(2, 3, 0);
|
||||||
|
sheet.setPixel(5, 3, 0);
|
||||||
|
|
||||||
|
// Smile (black)
|
||||||
|
sheet.setPixel(2, 5, 0);
|
||||||
|
sheet.setPixel(3, 5, 0);
|
||||||
|
sheet.setPixel(4, 5, 0);
|
||||||
|
sheet.setPixel(5, 5, 0);
|
||||||
|
|
||||||
|
editor.setSpriteSheet(sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to draw example sprite on load
|
||||||
|
// drawExampleSprite();
|
||||||
|
|
||||||
|
console.log('Sprite Editor initialized!');
|
||||||
|
console.log('Available tools: Pen, Fill, Line, Rect, Circle, Erase');
|
||||||
|
console.log('Click sprites in the sheet to edit them in the preview panel');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
789
styles/main.css
789
styles/main.css
@@ -108,88 +108,74 @@ input[type='search'] {
|
|||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem 1.25rem;
|
min-height: 46px;
|
||||||
background: linear-gradient(90deg, var(--surface-1), var(--surface-2));
|
padding: 0.375rem 0.5rem 0.375rem 0.625rem;
|
||||||
|
background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 88%, #0a0b10 12%), var(--surface-2));
|
||||||
border-bottom: 1px solid var(--edge);
|
border-bottom: 1px solid var(--edge);
|
||||||
box-shadow: var(--shadow-0);
|
box-shadow: var(--shadow-0);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-primary-actions {
|
.header-primary-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.45rem;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-titlebar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
font-size: 1.1rem;
|
font-size: 0.82rem;
|
||||||
letter-spacing: 0.04em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
justify-self: center;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-primary-button {
|
.header-icon-button.is-pending {
|
||||||
min-width: 7rem;
|
opacity: 0.8;
|
||||||
padding: 0.55rem 1.25rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
background: var(--accent);
|
|
||||||
color: #1e1e28;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
|
||||||
box-shadow: 0 0 0 0 rgba(255, 127, 17, 0.45);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-primary-button:hover:enabled,
|
.header-icon-button.is-busy {
|
||||||
.header-primary-button:focus-visible:enabled {
|
opacity: 0.7;
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 10px 18px rgba(255, 127, 17, 0.35);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-primary-button:active:enabled {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 4px 10px rgba(255, 127, 17, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-primary-button:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-primary-button.is-busy:disabled {
|
|
||||||
cursor: progress;
|
cursor: progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-primary-button.is-success {
|
.header-icon-button.is-success {
|
||||||
background: #3ee581;
|
opacity: 1;
|
||||||
color: #06100a;
|
|
||||||
box-shadow: 0 0 0 0 rgba(62, 229, 129, 0.45);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-primary-button.is-error {
|
.header-icon-button.is-error {
|
||||||
background: #ff4f4f;
|
opacity: 0.8;
|
||||||
color: #1e0a0a;
|
|
||||||
box-shadow: 0 0 0 0 rgba(255, 79, 79, 0.45);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 0.5rem;
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions--left {
|
||||||
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions .header-icon-button {
|
.header-actions .header-icon-button {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 2.5rem;
|
width: 2.1rem;
|
||||||
height: 2.5rem;
|
height: 2.1rem;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -197,16 +183,36 @@ input[type='search'] {
|
|||||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-icon-button--accent {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #1e1e28;
|
||||||
|
box-shadow: 0 10px 18px rgba(255, 127, 17, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon-button--accent:hover:enabled,
|
||||||
|
.header-icon-button--accent:focus-visible:enabled {
|
||||||
|
color: #1e1e28;
|
||||||
|
background: color-mix(in srgb, var(--accent) 84%, white 16%);
|
||||||
|
}
|
||||||
|
|
||||||
.header-icon-button .header-icon {
|
.header-icon-button .header-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.3rem;
|
width: 1.05rem;
|
||||||
height: 1.3rem;
|
height: 1.05rem;
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-icon-spacer {
|
||||||
|
width: 1px;
|
||||||
|
height: 1.2rem;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
background: color-mix(in srgb, var(--edge) 78%, transparent 22%);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-icon--settings {
|
.header-icon--settings {
|
||||||
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/%3E%3Cpath d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c0 .69.4 1.31 1.02 1.58.21.09.44.14.67.14H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/%3E%3C/svg%3E');
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/%3E%3Cpath d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c0 .69.4 1.31 1.02 1.58.21.09.44.14.67.14H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/%3E%3C/svg%3E');
|
||||||
}
|
}
|
||||||
@@ -219,10 +225,34 @@ input[type='search'] {
|
|||||||
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 4a8 8 0 1 0 7.54 5.14 1.5 1.5 0 0 0-1.54-1.14h-1.38a1.5 1.5 0 0 1-1.5-1.5V5.5A1.5 1.5 0 0 0 12.62 4Z"/%3E%3Ccircle cx="7.5" cy="10.5" r="1"/%3E%3Ccircle cx="12" cy="7.5" r="1"/%3E%3Ccircle cx="16.5" cy="10.5" r="1"/%3E%3Ccircle cx="11" cy="14.5" r="1"/%3E%3C/svg%3E');
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 4a8 8 0 1 0 7.54 5.14 1.5 1.5 0 0 0-1.54-1.14h-1.38a1.5 1.5 0 0 1-1.5-1.5V5.5A1.5 1.5 0 0 0 12.62 4Z"/%3E%3Ccircle cx="7.5" cy="10.5" r="1"/%3E%3Ccircle cx="12" cy="7.5" r="1"/%3E%3Ccircle cx="16.5" cy="10.5" r="1"/%3E%3Ccircle cx="11" cy="14.5" r="1"/%3E%3C/svg%3E');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-icon--tiles {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Crect x="3" y="3" width="7" height="7"/%3E%3Crect x="14" y="3" width="7" height="7"/%3E%3Crect x="14" y="14" width="7" height="7"/%3E%3Crect x="3" y="14" width="7" height="7"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
.header-icon--export {
|
.header-icon--export {
|
||||||
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 3v12"/%3E%3Cpath d="m8 11 4 4 4-4"/%3E%3Cpath d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/%3E%3C/svg%3E');
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 3v12"/%3E%3Cpath d="m8 11 4 4 4-4"/%3E%3Cpath d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/%3E%3C/svg%3E');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-icon--save {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"%3E%3Cpath d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2zM7 3v6h10V3M7 13h10v8H7"%2F%3E%3C%2Fsvg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon--play {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"%3E%3Cpath d="M8 5.14v13.72c0 .8.87 1.28 1.54.85l10.03-6.86a1.03 1.03 0 0 0 0-1.7L9.54 4.29A1.03 1.03 0 0 0 8 5.14Z"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon--minimize {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"%3E%3Cpath d="M6 12h12"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon--maximize {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"%3E%3Crect x="5" y="5" width="14" height="14" rx="1.5"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon--close-window {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"%3E%3Cpath d="M6 6l12 12M18 6L6 18"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
.header-icon-button.is-active {
|
.header-icon-button.is-active {
|
||||||
background: rgba(255, 127, 17, 0.2);
|
background: rgba(255, 127, 17, 0.2);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
@@ -243,11 +273,49 @@ input[type='search'] {
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
border-left: 1px solid color-mix(in srgb, var(--edge) 72%, transparent 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-button {
|
||||||
|
min-width: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-button:hover,
|
||||||
|
.window-control-button:focus-visible {
|
||||||
|
background: var(--surface-3);
|
||||||
|
color: var(--text-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-button--close:hover,
|
||||||
|
.window-control-button--close:focus-visible {
|
||||||
|
background: #e04a4a;
|
||||||
|
color: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
.app-body {
|
.app-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px minmax(0, 1fr) 280px 280px;
|
grid-template-columns: 280px minmax(0, 1fr) 280px 280px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: "overview workspace palette inspector";
|
||||||
gap: 0;
|
gap: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -261,12 +329,29 @@ input[type='search'] {
|
|||||||
|
|
||||||
.app-body--palette-hidden {
|
.app-body--palette-hidden {
|
||||||
grid-template-columns: 240px minmax(0, 1fr) 280px;
|
grid-template-columns: 240px minmax(0, 1fr) 280px;
|
||||||
|
grid-template-areas: "overview workspace inspector";
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-body--palette-hidden .palette {
|
.app-body--palette-hidden .palette {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
grid-area: overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
grid-area: workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette {
|
||||||
|
grid-area: palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector {
|
||||||
|
grid-area: inspector;
|
||||||
|
}
|
||||||
|
|
||||||
.overview,
|
.overview,
|
||||||
.palette,
|
.palette,
|
||||||
.inspector {
|
.inspector {
|
||||||
@@ -587,10 +672,10 @@ input[type='search'] {
|
|||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table)),
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color)),
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table)),
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color)),
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table)),
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color)),
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table));
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color));
|
||||||
background-size: calc(50% - 1px) calc(50% - 1px);
|
background-size: calc(50% - 1px) calc(50% - 1px);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: left top, right top, left bottom, right bottom;
|
background-position: left top, right top, left bottom, right bottom;
|
||||||
@@ -617,29 +702,7 @@ input[type='search'] {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-item[data-variable-type='number'] {
|
/* --overview-variable-color is set inline from JS via getTypeColor() */
|
||||||
--overview-variable-color: var(--variable-color-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-item[data-variable-type='string'] {
|
|
||||||
--overview-variable-color: var(--variable-color-string);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-item[data-variable-type='boolean'] {
|
|
||||||
--overview-variable-color: var(--variable-color-boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-item[data-variable-type='any'] {
|
|
||||||
--overview-variable-color: var(--variable-color-any);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-item[data-variable-type='table'] {
|
|
||||||
--overview-variable-color: var(--variable-color-table);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-item[data-variable-type='color'] {
|
|
||||||
--overview-variable-color: var(--variable-color-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-item[data-variable-type] .overview-item-label {
|
.overview-item[data-variable-type] .overview-item-label {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
@@ -686,44 +749,24 @@ input[type='search'] {
|
|||||||
width: 0.55rem;
|
width: 0.55rem;
|
||||||
height: 0.55rem;
|
height: 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--variable-color-any);
|
background: var(--overview-variable-color, var(--variable-color-any));
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-type-option[data-variable-type='number']::before {
|
|
||||||
background: var(--variable-color-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.variable-type-option[data-variable-type='string']::before {
|
|
||||||
background: var(--variable-color-string);
|
|
||||||
}
|
|
||||||
|
|
||||||
.variable-type-option[data-variable-type='boolean']::before {
|
|
||||||
background: var(--variable-color-boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
.variable-type-option[data-variable-type='any']::before {
|
|
||||||
background: var(--variable-color-any);
|
|
||||||
}
|
|
||||||
|
|
||||||
.variable-type-option[data-variable-type='table']::before {
|
.variable-type-option[data-variable-type='table']::before {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table)),
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color)),
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table)),
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color)),
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table)),
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color)),
|
||||||
linear-gradient(var(--variable-color-table), var(--variable-color-table));
|
linear-gradient(var(--overview-variable-color), var(--overview-variable-color));
|
||||||
background-size: calc(50% - 1px) calc(50% - 1px);
|
background-size: calc(50% - 1px) calc(50% - 1px);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: left top, right top, left bottom, right bottom;
|
background-position: left top, right top, left bottom, right bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-type-option[data-variable-type='color']::before {
|
|
||||||
background: var(--variable-color-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.variable-type-option:hover,
|
.variable-type-option:hover,
|
||||||
.variable-type-option:focus-visible {
|
.variable-type-option:focus-visible {
|
||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
@@ -1272,7 +1315,7 @@ input[type='search'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blueprint-node--variable {
|
.blueprint-node--variable {
|
||||||
--node-variable-color: var(--variable-color-any);
|
--overview-variable-color: var(--variable-color-any);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blueprint-node[data-variable-scope='local'] {
|
.blueprint-node[data-variable-scope='local'] {
|
||||||
@@ -1280,29 +1323,7 @@ input[type='search'] {
|
|||||||
z-index: 12;
|
z-index: 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blueprint-node[data-variable-type='number'] {
|
/* --overview-variable-color is set inline from JS via getTypeColor() */
|
||||||
--node-variable-color: var(--variable-color-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueprint-node[data-variable-type='string'] {
|
|
||||||
--node-variable-color: var(--variable-color-string);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueprint-node[data-variable-type='boolean'] {
|
|
||||||
--node-variable-color: var(--variable-color-boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueprint-node[data-variable-type='any'] {
|
|
||||||
--node-variable-color: var(--variable-color-any);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueprint-node[data-variable-type='table'] {
|
|
||||||
--node-variable-color: var(--variable-color-table);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueprint-node[data-variable-type='color'] {
|
|
||||||
--node-variable-color: var(--variable-color-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueprint-node--variable .node-header {
|
.blueprint-node--variable .node-header {
|
||||||
padding-left: 1.3rem;
|
padding-left: 1.3rem;
|
||||||
@@ -1316,18 +1337,83 @@ input[type='search'] {
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
background: var(--node-variable-color);
|
background: var(--overview-variable-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blueprint-node--variable .node-title-type {
|
.blueprint-node--variable .node-title-type {
|
||||||
border-color: var(--node-variable-color);
|
border-color: var(--overview-variable-color);
|
||||||
color: var(--node-variable-color);
|
color: var(--overview-variable-color);
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blueprint-node--variable.is-selected {
|
.blueprint-node--variable.is-selected {
|
||||||
border-color: var(--node-variable-color);
|
border-color: var(--overview-variable-color);
|
||||||
box-shadow: 0 0 0 1px var(--node-variable-color);
|
box-shadow: 0 0 0 1px var(--overview-variable-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Reroute nodes ── */
|
||||||
|
.blueprint-node[data-node-type='reroute'],
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'] {
|
||||||
|
min-width: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: 'inputs outputs';
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute']::before,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec']::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--surface-3);
|
||||||
|
border: 1px solid var(--edge);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute'].is-hovered::before,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'].is-hovered::before {
|
||||||
|
border-color: rgba(255, 127, 17, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute'].is-selected::before,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'].is-selected::before {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute']::after,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec']::after {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute'] .node-header,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'] .node-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute'] .node-inputs,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'] .node-inputs,
|
||||||
|
.blueprint-node[data-node-type='reroute'] .node-outputs,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'] .node-outputs {
|
||||||
|
padding: 0;
|
||||||
|
min-height: 0;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute'] .pin-label,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'] .pin-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprint-node[data-node-type='reroute'] .pin,
|
||||||
|
.blueprint-node[data-node-type='reroute_exec'] .pin {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-body {
|
.node-body {
|
||||||
@@ -1485,6 +1571,73 @@ input[type='search'] {
|
|||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Color swatch picker ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
.color-swatch-picker {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch-trigger {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: border-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch-trigger:hover,
|
||||||
|
.color-swatch-trigger:focus-visible {
|
||||||
|
border-color: rgba(255, 255, 255, 0.65);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch-dropdown {
|
||||||
|
position: fixed;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 3px;
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--edge);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-0);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch-option {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch-option:hover,
|
||||||
|
.color-swatch-option:focus-visible {
|
||||||
|
border-color: rgba(255, 255, 255, 0.8);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch-option[aria-checked='true'] {
|
||||||
|
border-color: #fff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 0 0 3px rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Align picker inside the local-variable and inspector default fields */
|
||||||
|
.local-variable-inline-value-field .color-swatch-picker,
|
||||||
|
.inspector-field .color-swatch-picker {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.pin-inline-checkbox {
|
.pin-inline-checkbox {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -1753,32 +1906,16 @@ input[type='search'] {
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin[data-type='exec'] .pin-handle {
|
/* --pin-kind-color is set inline from JS via getTypeColor() */
|
||||||
color: var(--pin-exec);
|
.pin .pin-handle {
|
||||||
}
|
color: var(--pin-kind-color, var(--variable-color-any));
|
||||||
|
|
||||||
.pin[data-type='number'] .pin-handle {
|
|
||||||
color: var(--variable-color-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin[data-type='boolean'] .pin-handle {
|
|
||||||
color: var(--variable-color-boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin[data-type='string'] .pin-handle {
|
|
||||||
color: var(--variable-color-string);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin[data-type='table'] .pin-handle {
|
.pin[data-type='table'] .pin-handle {
|
||||||
color: var(--variable-color-table);
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin[data-type='color'] .pin-handle {
|
|
||||||
color: var(--variable-color-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin[data-type='table']:not(.is-disconnected) .pin-handle {
|
.pin[data-type='table']:not(.is-disconnected) .pin-handle {
|
||||||
border-color: var(--edge);
|
border-color: var(--edge);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@@ -1860,6 +1997,8 @@ input[type='search'] {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --overview-variable-color is set inline from JS via getTypeColor() */
|
||||||
|
|
||||||
.custom-event-parameter-row .custom-event-parameter-action {
|
.custom-event-parameter-row .custom-event-parameter-action {
|
||||||
width: 1.35rem;
|
width: 1.35rem;
|
||||||
height: 1.35rem;
|
height: 1.35rem;
|
||||||
@@ -1870,22 +2009,6 @@ input[type='search'] {
|
|||||||
height: 0.9rem;
|
height: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-event-parameter-row[data-parameter-type='number'] {
|
|
||||||
--overview-variable-color: var(--variable-color-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-event-parameter-row[data-parameter-type='string'] {
|
|
||||||
--overview-variable-color: var(--variable-color-string);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-event-parameter-row[data-parameter-type='boolean'] {
|
|
||||||
--overview-variable-color: var(--variable-color-boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-event-parameter-row[data-parameter-type='table'] {
|
|
||||||
--overview-variable-color: var(--variable-color-table);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-event-parameter-row input[type='text'] {
|
.custom-event-parameter-row input[type='text'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.2rem 0.35rem;
|
padding: 0.2rem 0.35rem;
|
||||||
@@ -2225,6 +2348,21 @@ body.modal-open {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal__panel--sprite {
|
||||||
|
width: min(1200px, calc(100vw - 4rem));
|
||||||
|
max-height: calc(100vh - 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__panel--sprite .modal__body {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__panel--sprite #spriteEditorContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.modal__header {
|
.modal__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2291,6 +2429,14 @@ body.modal-open {
|
|||||||
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"/%3E%3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/%3E%3C/svg%3E');
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"/%3E%3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/%3E%3C/svg%3E');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-icon--import {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 3v12"/%3E%3Cpath d="m16 11-4-4-4 4"/%3E%3Cpath d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon--export {
|
||||||
|
mask-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M12 3v12"/%3E%3Cpath d="m8 11 4 4 4-4"/%3E%3Cpath d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/%3E%3C/svg%3E');
|
||||||
|
}
|
||||||
|
|
||||||
.modal__body {
|
.modal__body {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -2356,33 +2502,7 @@ body.modal-open {
|
|||||||
pointer-events: stroke;
|
pointer-events: stroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-path[data-type='exec'] {
|
/* stroke is set inline from JS via getTypeColor() */
|
||||||
stroke: var(--pin-exec);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-type='number'] {
|
|
||||||
stroke: var(--variable-color-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-type='boolean'] {
|
|
||||||
stroke: var(--variable-color-boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-type='string'] {
|
|
||||||
stroke: var(--variable-color-string);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-type='table'] {
|
|
||||||
stroke: var(--variable-color-table);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-type='color'] {
|
|
||||||
stroke: var(--variable-color-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-type='any'] {
|
|
||||||
stroke: var(--variable-color-any);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-path[data-active='true'] {
|
.connection-path[data-active='true'] {
|
||||||
stroke-dasharray: 6;
|
stroke-dasharray: 6;
|
||||||
@@ -2408,6 +2528,20 @@ body.modal-open {
|
|||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.app-body {
|
.app-body {
|
||||||
grid-template-columns: 220px minmax(0, 1fr) 220px;
|
grid-template-columns: 220px minmax(0, 1fr) 220px;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"overview workspace inspector"
|
||||||
|
"overview workspace palette";
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-body--palette-hidden {
|
||||||
|
grid-template-columns: 220px minmax(0, 1fr) 220px;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: "overview workspace inspector";
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector {
|
||||||
|
border-bottom: 1px solid var(--edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blueprint-node {
|
.blueprint-node {
|
||||||
@@ -2625,3 +2759,266 @@ body.modal-open {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Tile Browser ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.tile-browser-container {
|
||||||
|
height: 600px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-browser {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--surface-1);
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-header {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--edge);
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-action-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-action-btn:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 127, 17, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-action-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-list {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--surface-0);
|
||||||
|
border-right: 1px solid var(--edge);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 1fr auto;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border: 1px solid var(--edge);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item:hover {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item.selected {
|
||||||
|
background: var(--surface-3);
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 127, 17, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-preview {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--edge);
|
||||||
|
background: repeating-conic-gradient(
|
||||||
|
rgba(255, 255, 255, 0.05) 0% 25%,
|
||||||
|
rgba(0, 0, 0, 0.1) 0% 50%
|
||||||
|
) 50% / 16px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-preview canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-meta {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-btn {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--edge);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-btn:hover {
|
||||||
|
background: var(--surface-3);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-item-btn--danger:hover {
|
||||||
|
background: var(--error);
|
||||||
|
border-color: var(--error);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-panel {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--surface-0);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-header {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--edge);
|
||||||
|
background: var(--surface-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-canvas {
|
||||||
|
border: 2px solid var(--edge);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: repeating-conic-gradient(
|
||||||
|
rgba(255, 255, 255, 0.05) 0% 25%,
|
||||||
|
rgba(0, 0, 0, 0.1) 0% 50%
|
||||||
|
) 50% / 16px 16px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-canvas:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-preview-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-empty-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileset-empty-hint {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__panel--tile {
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 1200px;
|
||||||
|
height: 85vh;
|
||||||
|
max-height: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
302
test2.p8
Normal file
302
test2.p8
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
pico-8 cartridge // http://www.pico-8.com
|
||||||
|
version 8
|
||||||
|
__lua__
|
||||||
|
print("0.1.10c")
|
||||||
|
|
||||||
|
__gfx__
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00700700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00077000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00077000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00700700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
__gff__
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
__map__
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
__sfx__
|
||||||
|
000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
__music__
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
00 41424344
|
||||||
|
|
||||||
24
test_cart.p8
Normal file
24
test_cart.p8
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
pico-8 cartridge // http://www.pico-8.com
|
||||||
|
version 41
|
||||||
|
__lua__
|
||||||
|
function _init()
|
||||||
|
x=64
|
||||||
|
y=64
|
||||||
|
end
|
||||||
|
|
||||||
|
function _update()
|
||||||
|
end
|
||||||
|
|
||||||
|
function _draw()
|
||||||
|
cls()
|
||||||
|
print("picograph ok",2,60,7)
|
||||||
|
end
|
||||||
|
__gfx__
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
8
test_unix.p8
Normal file
8
test_unix.p8
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pico-8 cartridge // http://www.pico-8.com
|
||||||
|
version 41
|
||||||
|
__lua__
|
||||||
|
function _init()
|
||||||
|
x=64
|
||||||
|
end
|
||||||
|
__gfx__
|
||||||
|
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
Reference in New Issue
Block a user