class FSMDocument { constructor(name) { this.id = guid(); this.name = name || null; this.createdAt = Date.now(); this.lastModified = Date.now(); this.element = null; this.unsaved = true; this.lastSavedHash = ''; this.states = []; this.connections = []; this.changes = []; this.changesIndex = -1; } physicsTick() { this.states.forEach(stateA => { this.states.forEach(stateB => { if (stateA !== stateB && stateA.intersects(stateB)) { const inter = stateA.intersection(stateB); const angle1 = stateA.directionTo(stateB); const angle2 = angle1 + Math.PI; stateA.x += Math.cos(angle1) * inter; stateA.y += Math.sin(angle1) * inter; stateB.x += Math.cos(angle2) * inter; stateB.y += Math.sin(angle2) * inter; } }); }); } tick() { this.states.forEach(state => { if (state.v) { state.moveToStep(); } }); if (!this.unsaved) { if (this.hashCode() !== this.lastSavedHash) { this.unsaved = true; this.element.classList.add('unsaved'); } } } draw() { this.states.forEach(state => state.draw()); this.connections.forEach(connection => connection.draw()); } onDblClick(x, y) { if (!selectedObject) { selectedObject = new State(x, y); this.states.push(selectedObject); this.addChange('state', selectedObject.id, 'add', Object.keys(selectedObject), Object.values(selectedObject)); resetCaret(); } else if (selectedObject instanceof State) { selectedObject.isAcceptState = !selectedObject.isAcceptState; this.addChange('state', selectedObject.id, 'edit', ['isAcceptState'], [!selectedObject.isAcceptState]); } } onRightClick(x, y) { if (!selectedObject) { } } deleteCurrentObject() { if (!!selectedObject) { if (selectedObject instanceof State) { this.states.splice(this.states.findIndex(state => state === selectedObject), 1); } for (let i = 0; i < this.connections.length; i++) { const con = this.connections[i]; if (con === selectedObject || con.state === selectedObject || con.stateA === selectedObject || con.stateB === selectedObject) { this.connections.splice(i--, 1); } } selectedObject = null; } } getConnectionsOfState(state) { return this.connections.filter(connection => connection instanceof Connection && (connection.stateA === state || connection.stateB === state)); } getConnectionsBetweenStates(stateA,stateB) { return this.getConnectionsOfState(stateA).filter(connection => connection.stateA === stateB || connection.stateB === stateB); } addChange(objectType, id, action, fields, oldValues, tryCombiningChanges) { if (this.changesIndex < this.changes.length - 1) { this.changes.splice(this.changesIndex + 1, this.changes.length - this.changesIndex - 1); } if (tryCombiningChanges) { let prevChange = this.changes[this.changesIndex]; while (!!prevChange && prevChange.type === objectType && prevChange.id === id && prevChange.action === action && prevChange.fields.length === fields.length) { oldValues = prevChange.prevValues; this.changes.splice(this.changesIndex, 1); this.changesIndex--; prevChange = this.changes[this.changesIndex]; } } this.changes.push({ type: objectType, id, action, fields, prevValues: oldValues, date: Date.now() }); this.changesIndex++; } undo(stepAmount) { if (stepAmount === 0) return; const loopAmount = Math.abs(stepAmount); const isRedo = stepAmount < 0; for (let i = 0; i < loopAmount; i++) { if (this.changes.length === 0 || (!isRedo && this.changesIndex < 0) || (isRedo && this.changesIndex >= this.changes.length - 1)) { return; } let change; if (isRedo) { this.changesIndex++; change = this.changes[this.changesIndex]; } else { change = Object.assign({}, this.changes[this.changesIndex]); this.changesIndex--; change.action = change.action === 'add' ? 'remove' : change.action === 'remove' ? 'add' : 'edit'; } if (change.type === 'state') { if (change.action === 'remove') { this.states.splice(this.states.findIndex(item => item.id === change.id), 1); return; } let state; if (change.action === 'add') state = new State(); else if (change.action === 'edit') state = this.states.find(item => item.id === change.id); change.fields.forEach((field, index) => { state[field] = change.prevValues[index]; }); if (change.action === 'add') this.states.push(state); } if (change.type.indexOf('connection') !== -1) { if (change.action === 'remove') { this.connections.splice(this.connections.findIndex(item => item.id === change.id), 1); return; } let connection; if (change.action === 'add') { if(change.type === 'startconnection') connection = new StartConnection(); else if(change.type === 'selfconnection') connection = new SelfConnection(); else connection = new Connection(); } else if (change.action === 'edit') { connection = this.connections.find(item => item.id === change.id); } change.fields.forEach((field, index) => { connection[field] = change.prevValues[index]; }); if (change.action === 'add') this.connections.push(connection); } } } }