Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fabric v6 support #48

Open
hanna-becker opened this issue Jun 2, 2023 · 2 comments
Open

Fabric v6 support #48

hanna-becker opened this issue Jun 2, 2023 · 2 comments

Comments

@hanna-becker
Copy link

hanna-becker commented Jun 2, 2023

Fabric v6 is currently in beta, "pretty close to the official release" (fabricjs/fabric.js#8299 (comment)). Would be awesome to see fabric-history supporting this new version.

Currently "import 'fabric-history';" crashes my application when used with fabric v6 (ERROR Error: Uncaught (in promise): ReferenceError: fabric is not defined), probably because of the changes in imports mentioned here.

@StringKe
Copy link

StringKe commented Dec 1, 2023

try this

histroy.ts

import { Canvas } from 'fabric';

type HistoryEventCallback = () => void;

class HistoryFeature {
    private canvas: Canvas;
    private historyUndo: string[];
    private historyRedo: string[];
    private extraProps: string[];
    private historyNextState: string;
    private historyProcessing: boolean;

    constructor(canvas: Canvas) {
        this.canvas = canvas;
        this.historyUndo = [];
        this.historyRedo = [];
        this.extraProps = ['selectable', 'editable'];
        this.historyNextState = this._historyNext();
        this.historyProcessing = false;

        this._historyInit();
    }

    private _historyNext(): string {
        return JSON.stringify(this.canvas.toDatalessJSON(this.extraProps));
    }

    private _historyEvents() {
        return {
            'object:added': this._historySaveAction.bind(this),
            'object:removed': this._historySaveAction.bind(this),
            'object:modified': this._historySaveAction.bind(this),
            'object:skewing': this._historySaveAction.bind(this),
        };
    }

    private _historyInit() {
        this.canvas.on(this._historyEvents());
    }

    private _historyDispose() {
        this.canvas.off(this._historyEvents());
    }

    private _historySaveAction() {
        if (this.historyProcessing) return;

        const json = this.historyNextState;
        this.historyUndo.push(json);
        this.historyNextState = this._historyNext();
        this.canvas.fire('history:append', { json: json });
    }

    undo(callback?: HistoryEventCallback) {
        this.historyProcessing = true;

        const history = this.historyUndo.pop();
        if (history) {
            this.historyRedo.push(this._historyNext());
            this.historyNextState = history;
            this._loadHistory(history, 'history:undo', callback);
        } else {
            this.historyProcessing = false;
        }
    }

    redo(callback?: HistoryEventCallback) {
        this.historyProcessing = true;

        const history = this.historyRedo.pop();
        if (history) {
            this.historyUndo.push(this._historyNext());
            this.historyNextState = history;
            this._loadHistory(history, 'history:redo', callback);
        } else {
            this.historyProcessing = false;
        }
    }

    private _loadHistory(history: string, event: string, callback?: HistoryEventCallback) {
        this.canvas.loadFromJSON(history, () => {
            this.canvas.renderAll();
            this.canvas.fire(event);
            this.historyProcessing = false;

            if (callback) callback();
        });
    }

    clearHistory() {
        this.historyUndo = [];
        this.historyRedo = [];
        this.canvas.fire('history:clear');
    }

    onHistory() {
        this.historyProcessing = false;
        this._historySaveAction();
    }

    canUndo(): boolean {
        return this.historyUndo.length > 0;
    }

    canRedo(): boolean {
        return this.historyRedo.length > 0;
    }

    offHistory() {
        this.historyProcessing = true;
    }
}

export default HistoryFeature;

@insinfo
Copy link

insinfo commented Jan 12, 2024

In Dart I did this because I believe it is more efficient than saving the entire canvas at all times

import 'package:rava_frontend/src/shared/js_interop/fabric_interop.dart';

extension ListExtension on List {
  List splice2(int index, [num howMany = 0, dynamic elements]) {
    var endIndex = index + howMany.truncate();
    removeRange(index, endIndex >= length ? length : endIndex);
    if (elements != null) {
      insertAll(index, elements is List ? elements : [elements]);
    }
    return this;
  }

  List splice(int start, [int deleteCount = 0]) {
    final deletedElements = sublist(start, start + deleteCount);
    removeRange(start, start + deleteCount);
    return deletedElements;
  }
}

enum StateAction { create, modify, remove, none }

class TransformCommand {
  FabricObject element;
  Map<String, dynamic> state = {};
  Map<String, dynamic> prevState = {};
  List<String> statePropertiesKeys = [];
  StateAction action = StateAction.none;
  CommandHistory? history;

  TransformCommand(this.element, this.action) {
    switch (action) {
      case StateAction.create:
        element.saveState(); // save state for initialize
        init();
        break;
      case StateAction.modify:
        init();
        element.saveState(); // save new state
        break;
      case StateAction.remove:
        break;
      case StateAction.none:
    }
  }
  void init() {
    _initStateProperties();
    _saveState();
    _savePrevState();
  }

  /// redo
  void execute() {
    // print('TransformCommand execute  ${action.name}');
    if (action == StateAction.create) {
      history?.canvas.add(element);
    } else if (action == StateAction.remove) {
      history?.canvas.remove(element);
    } else {
      _restoreState();
      element.setCoords();
    }
  }

  /// undo
  void undo() {
    // print('TransformCommand undo ${action.name}');
    if (action == StateAction.create) {
      history?.canvas.remove(element);
    } else if (action == StateAction.remove) {
      history?.canvas.add(element);
    } else {
      _restorePrevState();
      element.setCoords();
    }
  }

  // private
  void _initStateProperties() {
    statePropertiesKeys = element.statePropertiesKeys;
  }

  void _restoreState() {
    _restore(state);
  }

  void _restorePrevState() {
    _restore(prevState);
  }

  void _restore(Map<String, dynamic> state) {
    for (var prop in statePropertiesKeys) {
      element.set(prop, state[prop]);
    }
  }

  void _saveState() {
    for (var prop in statePropertiesKeys) {
      state[prop] = element.get(prop);
    }
    //print('_saveState $state');
  }

  void _savePrevState() {
    final oldState = element.statePropertiesAsMap;

    if (oldState != null) {
      for (var prop in statePropertiesKeys) {
        prevState[prop] = oldState[prop];
      }
    } else {
      throw Exception(
          'TransformCommand@_savePrevState não foi possivel obter o estado anterior');
    }
    //print('_savePrevState $oldState');
  }
}

class CommandHistory {
  final commands = <TransformCommand>[];
  int index = 0;

  final Canvas canvas;

  CommandHistory(this.canvas);

  int getIndex() {
    return index;
  }

  CommandHistory add(TransformCommand command) {
    command.history = this;
    if (commands.isNotEmpty) {
      commands.splice(index, commands.length - index);
    }
    commands.add(command);
    index++;
    return this;
  }

  /// undo
  CommandHistory back() {
    //print(' CommandHistory back() ${commands.length} $index');
    if (index > 0) {
      final command = commands[--index];
      command.undo();
    }
    return this;
  }

  /// redo
  CommandHistory forward() {
    //print(' CommandHistory forward()  ${commands.length} $index');
    if (index < commands.length) {
      final command = commands[index++];
      command.execute();
    }
    return this;
  }

  CommandHistory clear() {
    commands.clear();
    commands.length = 0;
    index = 0;
    return this;
  }
}
...
  final canvas = Canvas( canvasEl );
  final history = CommandHistory(canvas);
...
 void onModifiedObject(ModifiedEvent event) {  
    final target = event.target;
    history.add(TransformCommand(target, StateAction.modify));    
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants