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:
2026-03-21 18:38:43 +11:00
parent 71354ad463
commit f917cf6ad5
56 changed files with 5200 additions and 688 deletions

View File

@@ -49,6 +49,7 @@ export class WorkspaceDragManager {
/**
* 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.
*/
@@ -57,8 +58,14 @@ export class WorkspaceDragManager {
if (!paletteDragGhost) {
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 =
WorkspaceGeometry.positionToTransform(position);
WorkspaceGeometry.positionToTransform(centered);
}
/**
@@ -210,7 +217,7 @@ export class WorkspaceDragManager {
return;
}
const pointerWorld = this.#clientToWorkspace(
const pointerWorld = this.clientToWorkspace(
hasClientX ? pointer.clientX : context.previousClientX,
hasClientY ? pointer.clientY : context.previousClientY
);
@@ -526,7 +533,7 @@ export class WorkspaceDragManager {
* @returns {{ x: number, y: number }}
*/
#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.
* @returns {{ x: number, y: number }}
*/
#clientToWorkspace(clientX, clientY) {
clientToWorkspace(clientX, clientY) {
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 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 {
x: (safeX - rect.left) / zoom - pending.x,
y: (safeY - rect.top) / zoom - pending.y,
x: (safeX - rect.left) / clampedZoom - offset.x,
y: (safeY - rect.top) / clampedZoom - offset.y,
};
}

View File

@@ -188,10 +188,14 @@ export class WorkspaceDropManager {
this.#workspace.paletteManager.handleDragEnd();
const spawn = WorkspaceGeometry.snapPositionToGrid(
this.#workspace,
WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event)
);
// Snap to grid first, then offset for centering (consistent with ghost positioning)
const cursorWorld = 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);
}
@@ -214,10 +218,12 @@ export class WorkspaceDropManager {
const definitions = this.#getVariableSpawnDefinitions(variableId);
if (!definitions.length) {
const spawn = WorkspaceGeometry.snapPositionToGrid(
this.#workspace,
WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event)
);
const cursorWorld = WorkspaceGeometry.eventToWorkspacePoint(this.#workspace, event);
const snapped = WorkspaceGeometry.snapPositionToGrid(this.#workspace, cursorWorld);
const spawn = {
x: snapped.x - 110,
y: snapped.y - 70,
};
const fallback = this.#workspace.addNode("get_var", spawn);
if (fallback) {
const nodeName = variable.name || variable.id;

View File

@@ -29,20 +29,11 @@ export class WorkspaceGeometry {
* Converts viewport coordinates into workspace-relative coordinates.
*
* @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 }}
*/
static eventToWorkspacePoint(workspace, event) {
const rect = workspace.workspaceElement.getBoundingClientRect();
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,
};
return workspace.dragManager.clientToWorkspace(event?.clientX, event?.clientY);
}
/**
@@ -54,8 +45,9 @@ export class WorkspaceGeometry {
*/
static snapPositionToGrid(workspace, position) {
const size = workspace.gridSize || DEFAULT_GRID_SIZE;
const offsetX = workspace.workspaceBackgroundOffset?.x ?? 0;
const offsetY = workspace.workspaceBackgroundOffset?.y ?? 0;
const effectiveOffset = workspace.navigationManager?.getEffectiveOffset?.();
const offsetX = Number.isFinite(effectiveOffset?.x) ? effectiveOffset.x : 0;
const offsetY = Number.isFinite(effectiveOffset?.y) ? effectiveOffset.y : 0;
const snap = (value, offset) => {
const local = value - offset;
const snapped = Math.round(local / size) * size;

View File

@@ -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').PinDescriptor} PinDescriptor */
/** @typedef {import('../../core/NodeGraph.js').NodeGraph} NodeGraph */
@@ -75,6 +78,7 @@ export class WorkspaceInlineEditorManager {
"number_literal",
"string_literal",
"boolean_literal",
"color_literal",
]);
if (!supported.has(node.type)) {
return null;
@@ -100,6 +104,27 @@ export class WorkspaceInlineEditorManager {
editor.appendChild(label);
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;
const commitNumber = () => {
const numeric = Number(control.value);
@@ -176,6 +201,7 @@ export class WorkspaceInlineEditorManager {
{ value: "string", label: "String" },
{ value: "boolean", label: "Boolean" },
{ value: "table", label: "Table" },
{ value: "color", label: "Color" },
];
const ensureBody = () => {
@@ -274,6 +300,8 @@ export class WorkspaceInlineEditorManager {
return "valueBoolean";
case "table":
return "valueTable";
case "color":
return "valueColor";
case "number":
default:
return "valueNumber";
@@ -288,6 +316,8 @@ export class WorkspaceInlineEditorManager {
return "string";
case "table":
return "table";
case "color":
return "color";
default:
return "number";
}
@@ -301,6 +331,8 @@ export class WorkspaceInlineEditorManager {
return false;
case "table":
return "{}";
case "color":
return 7;
case "number":
default:
return 0;
@@ -313,6 +345,7 @@ export class WorkspaceInlineEditorManager {
const normalized = normalizeType(type);
activeType = normalized;
typeButton.dataset.variableType = normalized;
typeButton.style.setProperty("--overview-variable-color", getTypeColor(normalized));
const label = formatType(normalized);
typeButton.title = `Type: ${label}`;
typeButton.setAttribute(
@@ -378,6 +411,20 @@ export class WorkspaceInlineEditorManager {
graph.setNodeProperty(currentNode.id, key, textarea.value);
});
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 {
const numberInput = document.createElement("input");
numberInput.type = "number";
@@ -407,6 +454,10 @@ export class WorkspaceInlineEditorManager {
if (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) {
const nextValue = typeof current === "string" ? current : "{}";
if (control.value !== nextValue) {
@@ -534,18 +585,10 @@ export class WorkspaceInlineEditorManager {
}
const isIfCondition = node.type === "if" && pin.id === "condition";
const isExtcmdCommand =
node.type === "system_extcmd" && pin.id === "command";
const isNamedButtonSelector =
node.type === "input_button_constants" && pin.id === "button";
const isNamedPlayerSelector =
node.type === "input_button_constants" && pin.id === "player";
if (
!isIfCondition &&
!isExtcmdCommand &&
!isNamedButtonSelector &&
!isNamedPlayerSelector
) {
const isColorPin = pin.kind === "color";
const pinOptions =
Array.isArray(pin.options) && pin.options.length > 0 ? pin.options : null;
if (!isIfCondition && !pinOptions && !isColorPin) {
container.classList.remove("has-inline-dropdown");
return;
}
@@ -555,31 +598,14 @@ export class WorkspaceInlineEditorManager {
const graph = this.#getGraph();
if (!(propertyKey in node.properties)) {
if (isExtcmdCommand || isNamedButtonSelector || isNamedPlayerSelector) {
const definition = this.registry.get(node.type);
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 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;
if (pinOptions) {
const defaultStr =
pin.defaultValue != null
? String(pin.defaultValue)
: String(pinOptions[0]?.value ?? "");
node.properties[propertyKey] = defaultStr;
if (!(pin.id in node.properties)) {
node.properties[pin.id] = defaultStr;
}
} else {
node.properties[propertyKey] = this.#defaultValueForPin(pin);
@@ -591,7 +617,7 @@ export class WorkspaceInlineEditorManager {
});
let wrapper;
if (isExtcmdCommand || isNamedButtonSelector || isNamedPlayerSelector) {
if (pinOptions) {
wrapper = document.createElement("div");
wrapper.className = "pin-inline-control pin-inline-dropdown";
container.appendChild(wrapper);
@@ -631,29 +657,14 @@ export class WorkspaceInlineEditorManager {
return;
}
if (isExtcmdCommand || isNamedButtonSelector || isNamedPlayerSelector) {
const definition = this.registry.get(node.type);
const targetKey = isExtcmdCommand
? "command"
: isNamedButtonSelector
? "button"
: "player";
const schema = definition?.properties?.find(
(prop) => prop.key === targetKey
);
const options = Array.isArray(schema?.options) ? schema.options : [];
if (pinOptions) {
const optionValues = pinOptions.map((o) => String(o.value));
const select = document.createElement("select");
select.className = "pin-inline-select";
const ariaLabel = isExtcmdCommand
? "Command"
: isNamedButtonSelector
? "Button"
: "Player";
select.setAttribute("aria-label", ariaLabel);
select.setAttribute("aria-label", pin.name || pin.id);
const values = options.map((option) => String(option.value));
options.forEach((option) => {
pinOptions.forEach((option) => {
const element = document.createElement("option");
element.value = String(option.value);
element.textContent = option.label ?? String(option.value);
@@ -661,30 +672,14 @@ export class WorkspaceInlineEditorManager {
});
const normalizeValue = (value) => {
const candidate =
typeof value === "string" ? value : String(value ?? "");
if (values.length === 0) {
return candidate;
}
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 candidate = value == null ? "" : String(value);
return optionValues.includes(candidate)
? candidate
: (optionValues[0] ?? "");
};
const setValue = (value) => {
const normalized = normalizeValue(
(isNamedButtonSelector || isNamedPlayerSelector) &&
typeof node.properties[targetKey] === "string"
? node.properties[targetKey]
: value
);
select.value = normalized;
select.value = normalizeValue(value);
};
const setConnected = (connected) => {
@@ -694,23 +689,42 @@ export class WorkspaceInlineEditorManager {
select.addEventListener("change", () => {
const nextValue = select.value;
if (isExtcmdCommand) {
graph.setNodeProperty(node.id, propertyKey, nextValue);
graph.setNodeProperty(node.id, "command", 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);
}
graph.setNodeProperty(node.id, propertyKey, nextValue);
graph.setNodeProperty(node.id, pin.id, nextValue);
});
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 });
setValue(node.properties[propertyKey]);
setConnected(this.#hasConnection(node.id, pin.id));
@@ -765,45 +779,12 @@ export class WorkspaceInlineEditorManager {
const propertyKey = this.#pinPropertyKey(pinId);
let value;
if (node.type === "system_extcmd" && pinId === "command") {
const commandValue =
typeof node.properties.command === "string"
? node.properties.command
: null;
if (
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;
}
if (Array.isArray(pin?.options) && pin.options.length > 0) {
const canonical = node.properties[pinId];
if (canonical != null) {
const strValue = String(canonical);
if (node.properties[propertyKey] !== strValue) {
node.properties[propertyKey] = strValue;
}
}
}
@@ -905,6 +886,8 @@ export class WorkspaceInlineEditorManager {
return 0;
case "string":
return "";
case "color":
return 7;
default:
return null;
}

View File

@@ -106,10 +106,6 @@ export class WorkspaceNavigationManager {
this.#targetZoomLevel = clamped;
this.#zoomLevel = clamped;
this.updateZoomDisplay(skipConnectionRender);
if (!silent) {
this.#markViewDirty("view", 200);
this.#schedulePersist();
}
return true;
}
@@ -151,6 +147,9 @@ export class WorkspaceNavigationManager {
/**
* 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.
* @returns {{ x: number, y: number }}
@@ -273,8 +272,6 @@ export class WorkspaceNavigationManager {
}
// Restore clean scale-only transform now that node positions are committed.
this.updateZoomDisplay(false);
this.#markViewDirty("view", 0);
this.#schedulePersist();
} else {
this.#smoothZoomRafId = requestAnimationFrame(() => this.#tickSmoothZoom());
}
@@ -320,6 +317,7 @@ export class WorkspaceNavigationManager {
this.#rebasePanGestureAfterZoom(pointer);
}
this.updateZoomDisplay(true);
this.#renderConnections();
}
@@ -417,6 +415,7 @@ export class WorkspaceNavigationManager {
});
this.translateWorkspace(delta);
this.updateZoomDisplay(false);
this.#workspace.selectNode(nodeId);
}
@@ -489,30 +488,29 @@ export class WorkspaceNavigationManager {
}
targetZoom = Math.max(this.#zoomConfig.min, Math.min(this.#zoomConfig.max, targetZoom));
this.setZoomLevel(targetZoom);
const contentCenter = {
x: minX + boundsWidth / 2,
y: minY + boundsHeight / 2,
};
const viewportCenter = this.screenPointToWorld({
x: rect.width / 2,
y: rect.height / 2,
});
// Directly compute the background offset that places contentCenter at screen center,
// then set zoom + offset together so a single updateZoomDisplay call applies both.
const newOffsetX = rect.width / 2 / targetZoom - contentCenter.x;
const newOffsetY = rect.height / 2 / targetZoom - contentCenter.y;
const delta = {
x: viewportCenter.x - contentCenter.x,
y: viewportCenter.y - contentCenter.y,
};
if (Math.abs(delta.x) > 0.0001 || Math.abs(delta.y) > 0.0001) {
this.translateWorkspace(delta);
this.#renderConnections();
// Cancel any running smooth zoom before snapping.
if (this.#smoothZoomRafId !== null) {
cancelAnimationFrame(this.#smoothZoomRafId);
this.#smoothZoomRafId = null;
this.#smoothZoomFocus = null;
this.#pendingLayerOffset = { x: 0, y: 0 };
}
this.#markViewDirty("view", 200);
this.#schedulePersist();
this.#targetZoomLevel = targetZoom;
this.#zoomLevel = targetZoom;
this.setBackgroundOffset({ x: newOffsetX, y: newOffsetY });
this.updateZoomDisplay(false);
}
/**
@@ -536,6 +534,7 @@ export class WorkspaceNavigationManager {
startY: event.clientY,
hasMoved: false,
backgroundOrigin: { ...this.#backgroundOffset },
lastDelta: { x: 0, y: 0 },
};
const handlePointerMove = (moveEvent) => {
@@ -564,12 +563,14 @@ export class WorkspaceNavigationManager {
return;
}
this.#panState.lastDelta = worldDelta;
const backgroundOffset = {
x: this.#panState.backgroundOrigin.x + worldDelta.x,
y: this.#panState.backgroundOrigin.y + worldDelta.y,
};
this.setBackgroundOffset(backgroundOffset);
this.updateZoomDisplay();
this.updateZoomDisplay(true);
};
const handlePointerUp = (upEvent) => {
@@ -589,6 +590,7 @@ export class WorkspaceNavigationManager {
}
this.#lastPanTimestamp = this.#timestamp();
this.#renderConnections();
};
this.#panState = state;

View File

@@ -6,6 +6,7 @@
*/
import { WorkspaceGeometry } from "./WorkspaceGeometry.js";
import { getTypeColor } from "../../types/TypeRegistry.js";
/**
* @typedef {"input"|"output"} PinDirection
@@ -300,6 +301,7 @@ export class WorkspaceNodeRenderer {
container.dataset.pinId = pin.id;
container.dataset.nodeId = node.id;
container.dataset.type = pin.kind;
container.style.setProperty("--pin-kind-color", getTypeColor(pin.kind));
const isStandardExec =
pin.kind === "exec" && (pin.id === "exec_in" || pin.id === "exec_out");
if (isStandardExec) {

View File

@@ -54,28 +54,48 @@ export class WorkspacePersistenceManager {
}
this.cancelScheduledPersist();
this.#workspace.handlePersistenceStateChange?.({
status: "pending",
source: "auto",
});
this.#timerId = window.setTimeout(() => {
this.#timerId = null;
this.persistImmediately();
this.persistImmediately("auto");
}, 200);
}
/**
* Serializes current workspace state and writes it to localStorage immediately.
*
* @param {'manual'|'auto'} [source]
*/
persistImmediately() {
persistImmediately(source = "manual") {
if (!this.#storageAvailable || typeof window === "undefined") {
return;
}
this.cancelScheduledPersist();
this.#workspace.handlePersistenceStateChange?.({
status: "saving",
source,
});
try {
const payload = this.serializeWorkspace();
window.localStorage.setItem(
WORKSPACE_STORAGE_KEY,
JSON.stringify(payload)
);
this.#workspace.handlePersistenceStateChange?.({
status: "saved",
source,
});
} catch (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))
.map((entry) => {
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
);
return {

View File

@@ -90,10 +90,12 @@ export class WorkspaceWireCutManager {
*/
#clientToSvg(clientX, clientY) {
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();
return {
x: (clientX - rect.left) / zoom,
y: (clientY - rect.top) / zoom,
x: (clientX - rect.left) / zoom - offset.x,
y: (clientY - rect.top) / zoom - offset.y,
};
}

View File

@@ -1,4 +1,5 @@
import { VARIABLE_SORT_MIME } from "../BlueprintWorkspaceConstants.js";
import { getTypeColor } from "../../types/TypeRegistry.js";
/**
* Renders the global variable summary list.
@@ -225,6 +226,7 @@ export function renderVariableList({
row.setAttribute("role", "button");
row.dataset.variableId = variable.id;
row.dataset.variableType = variable.type;
row.style.setProperty("--overview-variable-color", getTypeColor(variable.type));
row.draggable = true;
row.setAttribute("aria-pressed", "false");
@@ -330,6 +332,7 @@ export function renderVariableList({
typeButton.type = "button";
typeButton.className = "variable-type-button";
typeButton.dataset.variableType = variable.type;
typeButton.style.setProperty("--overview-variable-color", getTypeColor(variable.type));
typeButton.setAttribute("aria-haspopup", "menu");
typeButton.setAttribute("aria-expanded", "false");
typeButton.title = `Type: ${formatVariableType(variable.type)}`;