You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

415 lines
13 KiB

/**
* Command Pattern Implementation
*
* This system encapsulates user actions as command objects, making the codebase
* more maintainable and providing a foundation for features like undo/redo.
*/
import { PluginRegistry } from './pluginRegistry.js';
import { generateDefaultProps } from './utils.js';
import { ComponentNode } from './node.js';
// Base Command interface
export class Command {
/**
* Execute the command
* @param {CanvasApp} app - The application context
*/
execute(app) {
throw new Error('Command must implement execute() method');
}
/**
* Optional: Undo the command (for future undo/redo system)
* @param {CanvasApp} app - The application context
*/
undo(app) {
// Optional implementation - most commands won't need this initially
}
/**
* Optional: Get command description for logging/debugging
*/
getDescription() {
return this.constructor.name;
}
}
// Command Invoker - manages command execution and history
export class CommandInvoker {
constructor(app) {
this.app = app;
this.history = [];
this.maxHistorySize = 100; // Prevent memory leaks
}
/**
* Execute a command and add it to history
* @param {Command} command
*/
execute(command) {
try {
command.execute(this.app);
// Add to history (for future undo system)
this.history.push(command);
if (this.history.length > this.maxHistorySize) {
this.history.shift();
}
console.log(`Executed: ${command.getDescription()}`);
} catch (error) {
console.error(`Command execution failed: ${command.getDescription()}`, error);
throw error;
}
}
/**
* Future: Undo last command
*/
undo() {
if (this.history.length === 0) return;
const command = this.history.pop();
if (command.undo) {
command.undo(this.app);
console.log(`Undid: ${command.getDescription()}`);
}
}
/**
* Get command history for debugging
*/
getHistory() {
return this.history.map(cmd => cmd.getDescription());
}
}
// =============================================================================
// TAB NAVIGATION COMMANDS
// =============================================================================
export class SwitchToResourcesTabCommand extends Command {
execute(app) {
const requirementstab = app.tabs[1];
const resourcestab = app.tabs[2];
requirementstab.checked = false;
resourcestab.checked = true;
}
}
export class SwitchToDesignTabCommand extends Command {
execute(app) {
const requirementstab = app.tabs[1];
const designtab = app.tabs[1]; // Note: This looks like a bug in original - should be tabs[0]?
requirementstab.checked = false;
designtab.checked = true;
}
}
// =============================================================================
// TOOL COMMANDS
// =============================================================================
export class ToggleArrowModeCommand extends Command {
execute(app) {
app.arrowMode = !app.arrowMode;
if (app.arrowMode) {
app.arrowToolBtn.classList.add('active');
// Use observer to notify that arrow mode is enabled (will hide props panel)
app.connectionModeSubject.notifyConnectionModeChanged(true);
} else {
app.arrowToolBtn.classList.remove('active');
if (app.connectionStart) {
app.connectionStart.group.classList.remove('selected');
app.connectionStart = null;
}
// Use observer to notify that arrow mode is disabled
app.connectionModeSubject.notifyConnectionModeChanged(false);
}
}
}
// =============================================================================
// CHAT COMMANDS
// =============================================================================
export class StartChatCommand extends Command {
execute(app) {
const scheme = location.protocol === "https:" ? "wss://" : "ws://";
app.ws = new WebSocket(scheme + location.host + "/ws");
app.ws.onopen = () => {
app.ws.send(JSON.stringify({
'designPayload': JSON.stringify(app.exportDesign()),
'message': ''
}));
};
app.ws.onmessage = (e) => {
app.chatLoadingIndicator.style.display = 'none';
app.chatTextField.disabled = false;
app.chatTextField.focus();
const message = document.createElement('p');
message.innerHTML = e.data;
message.className = "other";
app.chatMessages.insertBefore(message, app.chatLoadingIndicator);
};
app.ws.onerror = (err) => {
console.log("ws error:", err);
app._scheduleReconnect();
};
app.ws.onclose = () => {
console.log("leaving chat...");
app.ws = null;
app._sentJoin = false;
delete app.players[app.pageData.username];
app._scheduleReconnect();
};
}
}
export class SendChatMessageCommand extends Command {
constructor(message) {
super();
this.message = message;
}
execute(app) {
const messageElement = document.createElement('p');
messageElement.innerHTML = this.message;
messageElement.className = "me";
app.chatMessages.insertBefore(messageElement, app.chatLoadingIndicator);
app.ws.send(JSON.stringify({
'message': this.message,
'designPayload': JSON.stringify(app.exportDesign()),
}));
app.chatTextField.value = '';
app.chatLoadingIndicator.style.display = 'block';
}
}
// =============================================================================
// DRAG & DROP COMMANDS
// =============================================================================
export class HandleDragStartCommand extends Command {
constructor(event) {
super();
this.event = event;
}
execute(app) {
const type = this.event.target.getAttribute('data-type');
const plugin = PluginRegistry.get(type);
if (!plugin) return;
this.event.dataTransfer.setData('text/plain', type);
}
}
export class HandleDragEndCommand extends Command {
constructor(event) {
super();
this.event = event;
}
execute(app) {
if (this.event.target.classList.contains('component-icon')) {
this.event.target.classList.remove('dragging');
}
}
}
export class DropComponentCommand extends Command {
constructor(event) {
super();
this.event = event;
}
execute(app) {
const type = this.event.dataTransfer.getData('text/plain');
const plugin = PluginRegistry.get(type);
if (!plugin) return;
const pt = app.canvas.createSVGPoint();
pt.x = this.event.clientX;
pt.y = this.event.clientY;
const svgP = pt.matrixTransform(app.canvas.getScreenCTM().inverse());
const x = svgP.x - app.componentSize.width / 2;
const y = svgP.y - app.componentSize.height / 2;
const props = generateDefaultProps(plugin);
const node = new ComponentNode(type, x, y, app, props);
node.x = x;
node.y = y;
}
}
// =============================================================================
// SIMULATION COMMANDS
// =============================================================================
export class RunSimulationCommand extends Command {
async execute(app) {
const designData = app.exportDesign();
// Try to get level info from URL or page context
const levelInfo = app.getLevelInfo();
const requestBody = {
design: designData,
...levelInfo
};
console.log('Sending design to simulation:', JSON.stringify(requestBody));
// Disable button and show loading state
app.runButton.disabled = true;
app.runButton.textContent = 'Running Simulation...';
try {
const response = await fetch('/simulate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.Success) {
console.log('Simulation successful:', result);
app.showResults(result);
} else {
console.error('Simulation failed:', result.Error);
app.showError(result.Error || 'Simulation failed');
}
} catch (error) {
console.error('Network error:', error);
app.showError('Failed to run simulation: ' + error.message);
} finally {
// Re-enable button
app.runButton.disabled = false;
app.runButton.textContent = 'Test Design';
}
}
}
// =============================================================================
// CANVAS INTERACTION COMMANDS
// =============================================================================
export class HandleCanvasClickCommand extends Command {
constructor(event) {
super();
this.event = event;
}
execute(app) {
// If this is part of a double-click sequence (detail > 1), ignore it
if (this.event.detail > 1) {
return;
}
if (app.connectionStart) {
app.connectionStart.group.classList.remove('selected');
app.connectionStart = null;
}
// Don't hide props panel if clicking on it
if (!app.nodePropsPanel.contains(this.event.target)) {
// Use observer to notify that properties panel should be closed
if (app.selectedNode) {
app.propertiesPanelSubject.notifyPropertiesPanelClosed(app.selectedNode);
}
}
// Use observer to notify that current node should be deselected
if (app.selectedNode) {
app.nodeSelectionSubject.notifyNodeDeselected(app.selectedNode);
app.selectedNode = null;
}
}
}
export class SaveNodePropertiesCommand extends Command {
execute(app) {
if (!app.activeNode) return;
const node = app.activeNode;
const panel = app.nodePropsPanel;
const plugin = PluginRegistry.get(node.type);
if (!plugin || !plugin.props) {
return;
}
// Loop through plugin-defined props and update the node
for (const prop of plugin.props) {
const input = panel.querySelector(`[name='${prop.name}']`);
if (!input) continue;
let value;
if (prop.type === 'number') {
value = parseFloat(input.value);
if (isNaN(value)) value = prop.default ?? 0;
} else {
value = input.value;
}
node.props[prop.name] = value;
if (prop.name === 'label') {
node.updateLabel(value);
}
}
}
}
export class DeleteSelectionCommand extends Command {
constructor(key) {
super();
this.key = key;
}
execute(app) {
if (this.key === 'Backspace' || this.key === 'Delete') {
if (app.selectedConnection) {
app.canvas.removeChild(app.selectedConnection.line);
app.canvas.removeChild(app.selectedConnection.text);
const index = app.connections.indexOf(app.selectedConnection);
if (index !== -1) app.connections.splice(index, 1);
app.selectedConnection = null;
} else if (app.selectedNode) {
app.canvas.removeChild(app.selectedNode.group);
app.placedComponents = app.placedComponents.filter(n => n !== app.selectedNode);
app.connections = app.connections.filter(conn => {
if (conn.start === app.selectedNode || conn.end === app.selectedNode) {
app.canvas.removeChild(conn.line);
app.canvas.removeChild(conn.text);
return false;
}
return true;
});
app.selectedNode = null;
app.activeNode = null;
}
}
}
}