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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user