From 7c695651c1dcc768dc07b7fe9ba2a1d7d042e9b9 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 11:57:54 +0900 Subject: [PATCH 01/16] add patchmap class emit --- src/display/update.js | 1 + src/patchmap.js | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/display/update.js b/src/display/update.js index 6f654266..1fedd173 100644 --- a/src/display/update.js +++ b/src/display/update.js @@ -38,6 +38,7 @@ export const update = (viewport, opts) => { refresh: config.refresh, }); } + return elements; }; const applyRelativeTransform = (element, changes) => { diff --git a/src/patchmap.js b/src/patchmap.js index 2173da4a..05b9c408 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { Application, UPDATE_PRIORITY } from 'pixi.js'; +import { Application, EventEmitter, UPDATE_PRIORITY } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; import { UndoRedoManager } from './command/undo-redo-manager'; import { draw } from './display/draw'; @@ -23,7 +23,7 @@ import StateManager from './events/StateManager'; import SelectionState from './events/states/SelectionState'; import Transformer from './transformer/Transformer'; -class Patchmap { +class Patchmap extends EventEmitter { _app = null; _viewport = null; _resizeObserver = null; @@ -131,6 +131,7 @@ class Patchmap { this._stateManager.register('selection', SelectionState, true); this.transformer = transformer; this.isInit = true; + this.emit('initialized', this); } destroy() { @@ -188,6 +189,7 @@ class Patchmap { ); this.app.start(); + this.emit('draw', { data: validatedData }); return validatedData; function processData(data) { @@ -202,7 +204,8 @@ class Patchmap { } update(opts) { - update(this.viewport, opts); + const updatedElements = update(this.viewport, opts); + this.emit('updated', updatedElements); } focus(ids) { From b6cc1ab8c9a3e9b231342805389dcf9acb978853 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 11:58:14 +0900 Subject: [PATCH 02/16] fix transformer eventEmitter --- src/transformer/SelectionModel.js | 47 +++++++++++++++++++++++++++++++ src/transformer/Transformer.js | 20 ++++++++++--- src/utils/convert.js | 2 +- 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/transformer/SelectionModel.js diff --git a/src/transformer/SelectionModel.js b/src/transformer/SelectionModel.js new file mode 100644 index 00000000..ebaecffe --- /dev/null +++ b/src/transformer/SelectionModel.js @@ -0,0 +1,47 @@ +import { EventEmitter } from 'pixi.js'; +import { convertArray } from '../utils/convert'; + +export default class SelectionModel extends EventEmitter { + #elements = []; + + get elements() { + return this.#elements; + } + + set(elements) { + const newElements = elements ? convertArray(elements) : []; + const oldElements = this.#elements; + this.#elements = newElements; + + const added = newElements.filter((el) => !oldElements.includes(el)); + const removed = oldElements.filter((el) => !newElements.includes(el)); + this.emit('update', { current: this.#elements, added, removed }); + } + + add(elementsToAdd) { + const added = convertArray(elementsToAdd).filter( + (el) => !this.#elements.includes(el), + ); + if (added.length > 0) { + this.#elements.push(...added); + this.emit('update', { current: this.#elements, added, removed: [] }); + } + } + + remove(elementsToRemove) { + const toRemove = convertArray(elementsToRemove); + const removed = []; + const newElements = this.#elements.filter((el) => { + if (toRemove.includes(el)) { + removed.push(el); + return false; + } + return true; + }); + + if (removed.length > 0) { + this.#elements = newElements; + this.emit('update', { current: this.#elements, added: [], removed }); + } + } +} diff --git a/src/transformer/Transformer.js b/src/transformer/Transformer.js index a8774b48..4dbedc3d 100644 --- a/src/transformer/Transformer.js +++ b/src/transformer/Transformer.js @@ -4,6 +4,7 @@ import { isValidationError } from 'zod-validation-error'; import { calcGroupOrientedBounds, calcOrientedBounds } from '../utils/bounds'; import { getViewport } from '../utils/get'; import { validate } from '../utils/validator'; +import SelectionModel from './SelectionModel'; import { Wireframe } from './Wireframe'; const DEFAULT_WIREFRAME_STYLE = { @@ -85,6 +86,8 @@ export default class Transformer extends Container { */ _viewport = null; + _selection = null; + /** * @param {TransformerOptions} [opts] - The options for the transformer. */ @@ -94,6 +97,7 @@ export default class Transformer extends Container { const options = validate(opts, TransformerSchema); if (isValidationError(options)) throw options; + this._selection = new SelectionModel(); this.#wireframe = this.addChild(new Wireframe({ label: 'wireframe' })); this.wireframeStyle = DEFAULT_WIREFRAME_STYLE; this.onRender = this.#refresh.bind(this); @@ -105,6 +109,11 @@ export default class Transformer extends Container { } } + this._selection.on('update', ({ current, added, removed }) => { + this.update(); + this.emit('update_elements', { current, added, removed }); + }); + this.on('added', () => { this._viewport = getViewport(this); if (this._viewport) { @@ -137,21 +146,23 @@ export default class Transformer extends Container { this.update(); } + get selection() { + return this._selection; + } + /** * The array of elements to be transformed. * @returns {PIXI.DisplayObject[]} */ get elements() { - return this._elements; + return this._selection.elements; } /** * @param {PIXI.DisplayObject | PIXI.DisplayObject[]} value */ set elements(value) { - this._elements = value ? (Array.isArray(value) ? value : [value]) : []; - this.update(); - this.emit('update_elements'); + this._selection.set(value); } /** @@ -181,6 +192,7 @@ export default class Transformer extends Container { if (this._viewport) { this._viewport.off('zoomed', this.update); } + this.selection.removeAllListeners(); super.destroy(options); } diff --git a/src/utils/convert.js b/src/utils/convert.js index fcd07f39..f7a06565 100644 --- a/src/utils/convert.js +++ b/src/utils/convert.js @@ -1,7 +1,7 @@ import { uid } from './uuid'; export const convertArray = (items) => { - return Array.isArray(items) ? items : [items]; + return Array.isArray(items) ? [...items] : [items]; }; export const convertLegacyData = (data) => { From 6fc089ce8ab857363339b341dd8c8b54244c5512 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 12:00:50 +0900 Subject: [PATCH 03/16] fix transformer --- src/transformer/Transformer.js | 63 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/transformer/Transformer.js b/src/transformer/Transformer.js index 4dbedc3d..24b30544 100644 --- a/src/transformer/Transformer.js +++ b/src/transformer/Transformer.js @@ -14,6 +14,11 @@ const DEFAULT_WIREFRAME_STYLE = { /** * @typedef {'all' | 'groupOnly' | 'elementOnly' | 'none'} BoundsDisplayMode + * The mode for displaying wireframe bounds. + * - 'all': Show bounds for both the group and individual elements. + * - 'groupOnly': Show only the encompassing bounds of all elements. + * - 'elementOnly': Show bounds for each individual element. + * - 'none': Do not show any bounds. */ /** @@ -42,6 +47,7 @@ const TransformerSchema = z * It draws a wireframe around the elements and can be configured to show bounds * for individual elements, the entire group, or both. * @extends PIXI.Container + * @fires Transformer#update_elements */ export default class Transformer extends Container { /** @private */ @@ -49,22 +55,11 @@ export default class Transformer extends Container { /** * The mode for displaying the wireframe bounds. - * - 'all': Show bounds for both the group and individual elements. - * - 'groupOnly': Show only the encompassing bounds of all elements. - * - 'elementOnly': Show bounds for each individual element. - * - 'none': Do not show any bounds. * @private * @type {BoundsDisplayMode} */ _boundsDisplayMode = 'all'; - /** - * The array of elements currently being transformed. - * @private - * @type {PIXI.DisplayObject[]} - */ - _elements = []; - /** * A flag to indicate that the wireframe needs to be redrawn. * @private @@ -86,7 +81,12 @@ export default class Transformer extends Container { */ _viewport = null; - _selection = null; + /** + * Manages the state of the currently selected elements. + * @private + * @type {SelectionModel} + */ + _selection; /** * @param {TransformerOptions} [opts] - The options for the transformer. @@ -109,6 +109,13 @@ export default class Transformer extends Container { } } + /** + * @event Transformer#update_elements + * @type {object} + * @property {PIXI.DisplayObject[]} current - The current array of selected elements. + * @property {PIXI.DisplayObject[]} added - The elements that were added in this update. + * @property {PIXI.DisplayObject[]} removed - The elements that were removed in this update. + */ this._selection.on('update', ({ current, added, removed }) => { this.update(); this.emit('update_elements', { current, added, removed }); @@ -123,8 +130,9 @@ export default class Transformer extends Container { } /** - * The wireframe graphics instance. - * @returns {Wireframe} + * The wireframe graphics instance used for drawing bounds. + * @type {Wireframe} + * @readonly */ get wireframe() { return this.#wireframe; @@ -132,33 +140,43 @@ export default class Transformer extends Container { /** * The current bounds display mode. - * @returns {BoundsDisplayMode} + * @type {BoundsDisplayMode} */ get boundsDisplayMode() { return this._boundsDisplayMode; } - /** - * @param {BoundsDisplayMode} value - */ set boundsDisplayMode(value) { this._boundsDisplayMode = value; this.update(); } + /** + * The selection model instance that manages the selected elements. + * Use this to programmatically add, remove, or set the selection. + * @type {SelectionModel} + * @readonly + * @example + * transformer.selection.add(newElement); + * transformer.selection.remove(oldElement); + * transformer.selection.set([element1, element2]); + */ get selection() { return this._selection; } /** - * The array of elements to be transformed. - * @returns {PIXI.DisplayObject[]} + * The array of elements currently being transformed. + * This is a convenient getter for `selection.elements`. + * @type {PIXI.DisplayObject[]} */ get elements() { return this._selection.elements; } /** + * Sets the elements to be transformed, replacing any existing selection. + * This is a convenient setter for `selection.set()`. * @param {PIXI.DisplayObject | PIXI.DisplayObject[]} value */ set elements(value) { @@ -167,15 +185,12 @@ export default class Transformer extends Container { /** * The style of the wireframe. - * @returns {WireframeStyle} + * @type {WireframeStyle} */ get wireframeStyle() { return this._wireframeStyle; } - /** - * @param {Partial} value - */ set wireframeStyle(value) { this._wireframeStyle = Object.assign(this._wireframeStyle, value); this.wireframe.setStrokeStyle(this.wireframeStyle); From 02e9c3ee2aadd3f2fa9e652d04aa12877897d4b4 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 12:04:19 +0900 Subject: [PATCH 04/16] fix --- src/patchmap.js | 4 ++-- src/transformer/SelectionModel.js | 10 +++++++--- src/transformer/Transformer.js | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/patchmap.js b/src/patchmap.js index 05b9c408..8ef0af2e 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -189,7 +189,7 @@ class Patchmap extends EventEmitter { ); this.app.start(); - this.emit('draw', { data: validatedData }); + this.emit('draw', validatedData, this); return validatedData; function processData(data) { @@ -205,7 +205,7 @@ class Patchmap extends EventEmitter { update(opts) { const updatedElements = update(this.viewport, opts); - this.emit('updated', updatedElements); + this.emit('updated', updatedElements, this); } focus(ids) { diff --git a/src/transformer/SelectionModel.js b/src/transformer/SelectionModel.js index ebaecffe..f019f94d 100644 --- a/src/transformer/SelectionModel.js +++ b/src/transformer/SelectionModel.js @@ -15,7 +15,7 @@ export default class SelectionModel extends EventEmitter { const added = newElements.filter((el) => !oldElements.includes(el)); const removed = oldElements.filter((el) => !newElements.includes(el)); - this.emit('update', { current: this.#elements, added, removed }); + this.emit('update', { current: this.#elements, added, removed }, this); } add(elementsToAdd) { @@ -24,7 +24,7 @@ export default class SelectionModel extends EventEmitter { ); if (added.length > 0) { this.#elements.push(...added); - this.emit('update', { current: this.#elements, added, removed: [] }); + this.emit('update', { current: this.#elements, added, removed: [] }.this); } } @@ -41,7 +41,11 @@ export default class SelectionModel extends EventEmitter { if (removed.length > 0) { this.#elements = newElements; - this.emit('update', { current: this.#elements, added: [], removed }); + this.emit( + 'update', + { current: this.#elements, added: [], removed }, + this, + ); } } } diff --git a/src/transformer/Transformer.js b/src/transformer/Transformer.js index 24b30544..b48bd086 100644 --- a/src/transformer/Transformer.js +++ b/src/transformer/Transformer.js @@ -118,7 +118,7 @@ export default class Transformer extends Container { */ this._selection.on('update', ({ current, added, removed }) => { this.update(); - this.emit('update_elements', { current, added, removed }); + this.emit('update_elements', { current, added, removed }, this); }); this.on('added', () => { From 3e8376f4b2671888834e6f055a7671f4c2296218 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 12:35:51 +0900 Subject: [PATCH 05/16] fix undoredomanager eventEmitter --- src/command/UndoRedoManager.js | 200 ++++++++++++++++++ ...anager.test.js => UndoRedoManager.test.js} | 18 +- src/command/undo-redo-manager.js | 166 --------------- src/patch-map.ts | 2 +- src/patchmap.js | 8 +- src/transformer/SelectionModel.js | 25 ++- src/transformer/Transformer.js | 2 +- 7 files changed, 232 insertions(+), 189 deletions(-) create mode 100644 src/command/UndoRedoManager.js rename src/command/{undo-redo-manager.test.js => UndoRedoManager.test.js} (95%) delete mode 100644 src/command/undo-redo-manager.js diff --git a/src/command/UndoRedoManager.js b/src/command/UndoRedoManager.js new file mode 100644 index 00000000..6f363b65 --- /dev/null +++ b/src/command/UndoRedoManager.js @@ -0,0 +1,200 @@ +import { EventEmitter } from 'pixi.js'; +import { BundleCommand } from './commands'; +import { isInput } from './utils'; + +/** + * Manages the command history for undo and redo operations. + * It records executed commands, allowing for sequential undoing and redoing. + * This class extends PIXI.EventEmitter to notify about state changes. + * + * @extends PIEI.EventEmitter + * @fires UndoRedoManager#executed + * @fires UndoRedoManager#undone + * @fires UndoRedoManager#redone + * @fires UndoRedoManager#cleared + * @fires UndoRedoManager#change + * @fires UndoRedoManager#destroyed + */ +export class UndoRedoManager extends EventEmitter { + /** + * @private + * @type {import('./commands/base').Command[]} + */ + #commands = []; + + /** + * @private + * @type {number} + */ + #index = -1; + + /** + * @private + * @type {number} + */ + #maxCommands; + + /** + * @private + * @type {((e: KeyboardEvent) => void) | null} + */ + #hotkeyListener; + + /** + * @param {number} [maxCommands=50] - The maximum number of commands to store in history. + */ + constructor(maxCommands = 50) { + super(); + this.#maxCommands = maxCommands; + } + + /** + * The list of commands in the history stack. + * @returns {import('./commands/base').Command[]} + * @readonly + */ + get commands() { + return this.#commands; + } + + /** + * Executes a command, adds it to the history, and clears the redo stack. + * If a `historyId` is provided, it may bundle the command with the previous one. + * @param {import('./commands/base').Command} command - The command to execute. + * @param {object} [options] - Options for execution. + * @param {string} [options.historyId] - An ID to bundle commands together into a single undo/redo step. + */ + execute(command, options = {}) { + command.execute(); + this.#commands = this.#commands.slice(0, this.#index + 1); + + const historyId = options.historyId; + let commandToPush = command; + let isBundled = false; + + if (historyId) { + const lastCommand = this.#commands[this.#commands.length - 1]; + if (lastCommand && lastCommand.id === historyId) { + if (lastCommand instanceof BundleCommand) { + lastCommand.addCommand(command); + } else { + this.#commands[this.#commands.length - 1] = new BundleCommand( + historyId, + [lastCommand, command], + ); + } + commandToPush = this.#commands[this.#commands.length - 1]; + isBundled = true; + } else { + commandToPush = new BundleCommand(historyId, [command]); + } + } + + if (!isBundled) { + this.#commands.push(commandToPush); + this.#index++; + if (this.#commands.length > this.#maxCommands) { + this.#commands.shift(); + this.#index--; + } + } + + this.emit('executed', { command: commandToPush, target: this }); + this.emit('change', { target: this }); + } + + /** + * Undoes the last executed command. + */ + undo() { + if (this.canUndo()) { + const command = this.#commands[this.#index]; + command.undo(); + this.#index--; + + this.emit('undone', { command, target: this }); + this.emit('change', { target: this }); + } + } + + /** + * Redoes the last undone command. + */ + redo() { + if (this.canRedo()) { + this.#index++; + const command = this.#commands[this.#index]; + command.execute(); + + this.emit('redone', { command, target: this }); + this.emit('change', { target: this }); + } + } + + /** + * Checks if there are any commands to undo. + * @returns {boolean} True if an undo operation is possible. + */ + canUndo() { + return this.#index >= 0; + } + + /** + * Checks if there are any commands to redo. + * @returns {boolean} True if a redo operation is possible. + */ + canRedo() { + return this.#index < this.#commands.length - 1; + } + + /** + * Clears the entire command history. + */ + clear() { + this.#commands = []; + this.#index = -1; + + this.emit('cleared', { target: this }); + this.emit('change', { target: this }); + } + + /** + * Sets up hotkeys for undo/redo functionality (Ctrl+Z, Ctrl+Y/Cmd+Shift+Z). + * @private + */ + _setHotkeys() { + this.#hotkeyListener = (e) => { + const key = (e.key || '').toLowerCase(); + if (isInput(e.target)) return; + + if (key === 'z' && (e.ctrlKey || e.metaKey)) { + if (e.shiftKey) { + this.redo(); + } else { + this.undo(); + } + e.preventDefault(); + } + if (key === 'y' && (e.ctrlKey || e.metaKey)) { + this.redo(); + e.preventDefault(); + } + }; + + document.addEventListener('keydown', this.#hotkeyListener, false); + } + + /** + * Removes event listeners and clears all internal states to prevent memory leaks. + */ + destroy() { + if (this.#hotkeyListener) { + document.removeEventListener('keydown', this.#hotkeyListener, false); + this.#hotkeyListener = null; + } + this.clear(); + + this.emit('destroyed', { target: this }); + this.removeAllListeners(); + } +} diff --git a/src/command/undo-redo-manager.test.js b/src/command/UndoRedoManager.test.js similarity index 95% rename from src/command/undo-redo-manager.test.js rename to src/command/UndoRedoManager.test.js index 9c73e2a3..11c94388 100644 --- a/src/command/undo-redo-manager.test.js +++ b/src/command/UndoRedoManager.test.js @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; +import { UndoRedoManager } from './UndoRedoManager'; import { Command } from './commands/base'; import { BundleCommand } from './commands/bundle'; -import { UndoRedoManager } from './undo-redo-manager'; /** * A mock command for testing purposes. @@ -149,24 +149,22 @@ describe('UndoRedoManager', () => { it('should correctly notify subscribers on state changes', () => { const manager = new UndoRedoManager(); const listener = vi.fn(); - const unsubscribe = manager.subscribe(listener); - - expect(listener).toHaveBeenCalledTimes(1); + manager.on('change', listener); manager.execute(new MockCommand()); - expect(listener).toHaveBeenCalledTimes(2); + expect(listener).toHaveBeenCalledTimes(1); manager.undo(); - expect(listener).toHaveBeenCalledTimes(3); + expect(listener).toHaveBeenCalledTimes(2); manager.redo(); - expect(listener).toHaveBeenCalledTimes(4); + expect(listener).toHaveBeenCalledTimes(3); manager.clear(); - expect(listener).toHaveBeenCalledTimes(5); + expect(listener).toHaveBeenCalledTimes(4); - unsubscribe(); + manager.off('change', listener); manager.execute(new MockCommand()); - expect(listener).toHaveBeenCalledTimes(5); + expect(listener).toHaveBeenCalledTimes(4); }); }); diff --git a/src/command/undo-redo-manager.js b/src/command/undo-redo-manager.js deleted file mode 100644 index ea71449d..00000000 --- a/src/command/undo-redo-manager.js +++ /dev/null @@ -1,166 +0,0 @@ -import { BundleCommand } from './commands'; -import { isInput } from './utils'; - -export class UndoRedoManager { - constructor(maxCommands = 50) { - this._commands = []; - this._index = -1; - this._listeners = new Set(); - this._maxCommands = maxCommands; - this._hotkeyListener = null; - } - - /** - * Returns the list of commands in the history. - * @returns {Array} - */ - get commands() { - return this._commands; - } - - execute(command, options = {}) { - command.execute(); - this._commands = this._commands.slice(0, this._index + 1); - - const historyId = options.historyId; - let shouldPush = false; - let commandToPush = command; - - if (historyId) { - const lastCommand = this._commands[this._commands.length - 1]; - if (lastCommand && lastCommand.id === historyId) { - if (lastCommand instanceof BundleCommand) { - lastCommand.addCommand(command); - } else { - this._commands[this._commands.length - 1] = new BundleCommand( - historyId, - [lastCommand, command], - ); - } - } else { - shouldPush = true; - commandToPush = new BundleCommand(historyId, [command]); - } - } else { - shouldPush = true; - } - - if (shouldPush) { - this._commands.push(commandToPush); - this._index++; - if (this._commands.length > this._maxCommands) { - this._commands.shift(); - this._index--; - } - } - this._emitChange(); - } - - /** - * Undoes the last executed command. - */ - undo() { - if (this.canUndo()) { - const command = this._commands[this._index]; - command.undo(); - this._index--; - this._emitChange(); - } - } - - /** - * Redoes the last undone command. - */ - redo() { - if (this.canRedo()) { - this._index++; - const command = this._commands[this._index]; - command.execute(); - this._emitChange(); - } - } - - /** - * Checks if there are any commands to undo. - * @returns {boolean} - */ - canUndo() { - return this._index >= 0; - } - - /** - * Checks if there are any commands to redo. - * @returns {boolean} - */ - canRedo() { - return this._index < this._commands.length - 1; - } - - /** - * Clears the command history. - */ - clear() { - this._commands = []; - this._index = -1; - this._emitChange(); - } - - /** - * Subscribes a listener to be called when the command history changes. - * @param {Function} listener - The listener function to call. - * @returns {Function} - A function to unsubscribe the listener. - */ - subscribe(listener) { - this._listeners.add(listener); - listener(this); - return () => { - this._listeners.delete(listener); - }; - } - - /** - * Emits a change event to all listeners. - * @private - */ - _emitChange() { - this._listeners.forEach((listener) => listener(this)); - } - - /** - * Sets up hotkeys for undo/redo functionality (Ctrl+Z, Ctrl+Y). - * @private - */ - _setHotkeys() { - this._hotkeyListener = (e) => { - const key = (e.key || '').toLowerCase(); - if (isInput(e.target)) return; - - if (key === 'z' && (e.ctrlKey || e.metaKey)) { - if (e.shiftKey) { - this.redo(); - } else { - this.undo(); - } - e.preventDefault(); - } - if (key === 'y' && (e.ctrlKey || e.metaKey)) { - this.redo(); - e.preventDefault(); - } - }; - - document.addEventListener('keydown', this._hotkeyListener, false); - } - - /** - * Removes event listeners and clears all internal states to prevent memory leaks. - */ - destroy() { - if (this._hotkeyListener) { - document.removeEventListener('keydown', this._hotkeyListener, false); - this._hotkeyListener = null; - } - this.clear(); - this._listeners.clear(); - } -} diff --git a/src/patch-map.ts b/src/patch-map.ts index f04787a0..a566d4e9 100644 --- a/src/patch-map.ts +++ b/src/patch-map.ts @@ -1,5 +1,5 @@ export { Patchmap } from './patchmap'; -export { UndoRedoManager } from './command/undo-redo-manager'; +export { UndoRedoManager } from './command/UndoRedoManager'; export { Command } from './command/commands/base'; export { default as Transformer } from './transformer/Transformer'; export { default as State, PROPAGATE_EVENT } from './events/states/State'; diff --git a/src/patchmap.js b/src/patchmap.js index 8ef0af2e..1bc331d5 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -1,7 +1,7 @@ import gsap from 'gsap'; import { Application, EventEmitter, UPDATE_PRIORITY } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; -import { UndoRedoManager } from './command/undo-redo-manager'; +import { UndoRedoManager } from './command/UndoRedoManager'; import { draw } from './display/draw'; import { update } from './display/update'; import { fit, focus } from './events/focus-fit'; @@ -131,7 +131,7 @@ class Patchmap extends EventEmitter { this._stateManager.register('selection', SelectionState, true); this.transformer = transformer; this.isInit = true; - this.emit('initialized', this); + this.emit('initialized', { target: this }); } destroy() { @@ -189,7 +189,7 @@ class Patchmap extends EventEmitter { ); this.app.start(); - this.emit('draw', validatedData, this); + this.emit('draw', { data: validatedData, target: this }); return validatedData; function processData(data) { @@ -205,7 +205,7 @@ class Patchmap extends EventEmitter { update(opts) { const updatedElements = update(this.viewport, opts); - this.emit('updated', updatedElements, this); + this.emit('updated', { elements: updatedElements, target: this }); } focus(ids) { diff --git a/src/transformer/SelectionModel.js b/src/transformer/SelectionModel.js index f019f94d..38e9310d 100644 --- a/src/transformer/SelectionModel.js +++ b/src/transformer/SelectionModel.js @@ -15,7 +15,12 @@ export default class SelectionModel extends EventEmitter { const added = newElements.filter((el) => !oldElements.includes(el)); const removed = oldElements.filter((el) => !newElements.includes(el)); - this.emit('update', { current: this.#elements, added, removed }, this); + this.emit('update', { + target: this, + current: this.#elements, + added, + removed, + }); } add(elementsToAdd) { @@ -24,7 +29,12 @@ export default class SelectionModel extends EventEmitter { ); if (added.length > 0) { this.#elements.push(...added); - this.emit('update', { current: this.#elements, added, removed: [] }.this); + this.emit('update', { + target: this, + current: this.#elements, + added, + removed: [], + }); } } @@ -41,11 +51,12 @@ export default class SelectionModel extends EventEmitter { if (removed.length > 0) { this.#elements = newElements; - this.emit( - 'update', - { current: this.#elements, added: [], removed }, - this, - ); + this.emit('update', { + target: this, + current: this.#elements, + added: [], + removed, + }); } } } diff --git a/src/transformer/Transformer.js b/src/transformer/Transformer.js index b48bd086..262a047a 100644 --- a/src/transformer/Transformer.js +++ b/src/transformer/Transformer.js @@ -118,7 +118,7 @@ export default class Transformer extends Container { */ this._selection.on('update', ({ current, added, removed }) => { this.update(); - this.emit('update_elements', { current, added, removed }, this); + this.emit('update_elements', { target: this, current, added, removed }); }); this.on('added', () => { From 353b34677c51c312598603869bde7e2c5454daf2 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 13:57:50 +0900 Subject: [PATCH 06/16] add WildcardEventEmitter --- src/utils/event/WildcardEventEmitter.js | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/utils/event/WildcardEventEmitter.js diff --git a/src/utils/event/WildcardEventEmitter.js b/src/utils/event/WildcardEventEmitter.js new file mode 100644 index 00000000..78efcb2d --- /dev/null +++ b/src/utils/event/WildcardEventEmitter.js @@ -0,0 +1,55 @@ +import { EventEmitter } from 'pixi.js'; + +/** + * Extends PIXI.EventEmitter to add support for namespace-based wildcard events. + * When an event with a namespace (e.g., 'namespace:event') is emitted, + * it automatically triggers a corresponding wildcard event (e.g., 'namespace:*'). + * This allows for listening to all events within a specific category. + * + * @extends PIXI.EventEmitter + * @example + * const emitter = new WildcardEventEmitter(); + * + * // Listen for a specific event + * emitter.on('history:undone', ({ command }) => { + * console.log(`Command undone: ${command.id}`); + * }); + * + * // Listen for all events in the 'history' namespace + * emitter.on('history:*', ({ command }) => { + * console.log('A history event occurred.'); + * }); + * + * emitter.emit('history:undone', { command: { id: 'cmd-1' } }); + * // CONSOLE LOG: Command undone: cmd-1 + * // CONSOLE LOG: A history event occurred. + */ +export class WildcardEventEmitter extends EventEmitter { + /** + * Emits an event to listeners. + * If the event name contains a colon (`:`), it also emits a wildcard event + * for the corresponding namespace (e.g., emitting 'state:pushed' will also emit 'state:*'). + * @override + * @param {string} eventName - The name of the event to emit. + * @param {...*} args - The arguments to pass to the listeners. + * @returns {boolean} `true` if the event had listeners (either specific or wildcard), else `false`. + */ + emit(eventName, ...args) { + // 1. Emit the original, specific event first. + const specificResult = super.emit(eventName, ...args); + + // 2. Check for a namespace separator. + const separatorIndex = eventName.indexOf(':'); + if (separatorIndex > 0) { + // 3. Construct and emit the wildcard event. + const namespace = eventName.substring(0, separatorIndex); + const wildcardEvent = `${namespace}:*`; + const wildcardResult = super.emit(wildcardEvent, ...args); + + // Return true if either the specific or wildcard listener was found. + return specificResult || wildcardResult; + } + + return specificResult; + } +} From f3ab345bc775c72567c2e38317309e1c8e5e3827 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 14:02:03 +0900 Subject: [PATCH 07/16] add StateManager eventEmitter --- src/events/StateManager.js | 64 +++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/events/StateManager.js b/src/events/StateManager.js index 9863542f..976191d4 100644 --- a/src/events/StateManager.js +++ b/src/events/StateManager.js @@ -1,10 +1,20 @@ +import { WildcardEventEmitter } from '../utils/event/WildcardEventEmitter'; import { PROPAGATE_EVENT } from './states/State'; /** * Manages the state of the application, including the registration, transition, and management of states. * This class implements a stack-based state machine, allowing for nested states and complex interaction flows. + * + * @extends PIXI.EventEmitter + * @fires StateManager#state:pushed + * @fires StateManager#state:popped + * @fires StateManager#state:set + * @fires StateManager#state:reset + * @fires StateManager#modifier:activated + * @fires StateManager#modifier:deactivated + * @fires StateManager#destroyed */ -export default class StateManager { +export default class StateManager extends WildcardEventEmitter { /** @private */ #context; /** @private */ @@ -23,6 +33,7 @@ export default class StateManager { * @param {object} context - The context in which the StateManager operates, typically containing the viewport and other global instances. */ constructor(context) { + super(); this.#context = context; } @@ -77,7 +88,11 @@ export default class StateManager { */ setState(name, ...args) { this.resetState(); - this.pushState(name, ...args); + const newState = this.pushState(name, ...args); + + if (newState) { + this.emit('state:set', { state: newState, target: this }); + } } /** @@ -86,16 +101,19 @@ export default class StateManager { resetState() { this.exitAll(); this.#stateStack.length = 0; + + this.emit('state:reset', { target: this }); } /** * Pushes a new state onto the stack, pausing the previous state. * @param {string} name - The name of the state to push. * @param {...*} args - Additional arguments to pass to the new state's `enter` method. + * @returns {import('./states/State').default | undefined} The new state instance that was pushed, or undefined if not found. */ pushState(name, ...args) { - const currentState = this.getCurrentState(); - currentState?.pause?.(); + const pausedState = this.getCurrentState(); + pausedState?.pause?.(); const stateDef = this.#stateRegistry.get(name); if (!stateDef) { @@ -114,6 +132,13 @@ export default class StateManager { this.#stateStack.push(instance); instance.enter?.(this.#context, ...args); + + this.emit('state:pushed', { + pushedState: instance, + pausedState, + target: this, + }); + return instance; } /** @@ -124,12 +149,14 @@ export default class StateManager { popState(payload) { if (this.#stateStack.length === 0) return null; - const currentState = this.#stateStack.pop(); - currentState?.exit?.(); + const poppedState = this.#stateStack.pop(); + poppedState?.exit?.(); + + const resumedState = this.getCurrentState(); + resumedState?.resume?.(payload); - const previousState = this.getCurrentState(); - previousState?.resume?.(payload); - return currentState; + this.emit('state:popped', { poppedState, resumedState, target: this }); + return poppedState; } /** @@ -199,14 +226,26 @@ export default class StateManager { this.#modifierState = instance; this.#modifierState.enter?.(this.#context, ...args); + + this.emit('modifier:activated', { + modifierState: this.#modifierState, + target: this, + }); } /** * Deactivates the current modifier state, restoring event handling to the main state stack. */ deactivateModifier() { - this.#modifierState?.exit?.(); - this.#modifierState = null; + const deactivatedModifier = this.#modifierState; + if (deactivatedModifier) { + deactivatedModifier.exit?.(); + this.#modifierState = null; + this.emit('modifier:deactivated', { + modifierState: deactivatedModifier, + target: this, + }); + } } /** @@ -280,5 +319,8 @@ export default class StateManager { this.#modifierState = null; this.#boundEvents.clear(); this.#eventListeners = {}; + + this.emit('destroyed', { target: this }); + this.removeAllListeners(); } } From b69adb3fe8902789c5ac61029e669ba614e5bda1 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 14:44:47 +0900 Subject: [PATCH 08/16] fix --- src/command/UndoRedoManager.js | 33 +++++++++++-------------- src/events/StateManager.js | 6 ++--- src/patchmap.js | 13 ++++++---- src/transformer/SelectionModel.js | 8 ++++-- src/transformer/Transformer.js | 2 +- src/utils/event/WildcardEventEmitter.js | 16 +++++++++--- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/command/UndoRedoManager.js b/src/command/UndoRedoManager.js index 6f363b65..2385ea7c 100644 --- a/src/command/UndoRedoManager.js +++ b/src/command/UndoRedoManager.js @@ -1,21 +1,20 @@ -import { EventEmitter } from 'pixi.js'; +import { WildcardEventEmitter } from '../utils/event/WildcardEventEmitter'; import { BundleCommand } from './commands'; import { isInput } from './utils'; /** * Manages the command history for undo and redo operations. * It records executed commands, allowing for sequential undoing and redoing. - * This class extends PIXI.EventEmitter to notify about state changes. + * This class extends WildcardEventEmitter to notify about state changes. * - * @extends PIEI.EventEmitter - * @fires UndoRedoManager#executed - * @fires UndoRedoManager#undone - * @fires UndoRedoManager#redone - * @fires UndoRedoManager#cleared - * @fires UndoRedoManager#change - * @fires UndoRedoManager#destroyed + * @extends WildcardEventEmitter + * @fires UndoRedoManager#history:executed + * @fires UndoRedoManager#history:undone + * @fires UndoRedoManager#history:redone + * @fires UndoRedoManager#history:cleared + * @fires UndoRedoManager#history:destroyed */ -export class UndoRedoManager extends EventEmitter { +export class UndoRedoManager extends WildcardEventEmitter { /** * @private * @type {import('./commands/base').Command[]} @@ -99,8 +98,7 @@ export class UndoRedoManager extends EventEmitter { } } - this.emit('executed', { command: commandToPush, target: this }); - this.emit('change', { target: this }); + this.emit('history:executed', { command: commandToPush, target: this }); } /** @@ -112,8 +110,7 @@ export class UndoRedoManager extends EventEmitter { command.undo(); this.#index--; - this.emit('undone', { command, target: this }); - this.emit('change', { target: this }); + this.emit('history:undone', { command, target: this }); } } @@ -126,8 +123,7 @@ export class UndoRedoManager extends EventEmitter { const command = this.#commands[this.#index]; command.execute(); - this.emit('redone', { command, target: this }); - this.emit('change', { target: this }); + this.emit('history:redone', { command, target: this }); } } @@ -154,8 +150,7 @@ export class UndoRedoManager extends EventEmitter { this.#commands = []; this.#index = -1; - this.emit('cleared', { target: this }); - this.emit('change', { target: this }); + this.emit('history:cleared', { target: this }); } /** @@ -194,7 +189,7 @@ export class UndoRedoManager extends EventEmitter { } this.clear(); - this.emit('destroyed', { target: this }); + this.emit('history:destroyed', { target: this }); this.removeAllListeners(); } } diff --git a/src/events/StateManager.js b/src/events/StateManager.js index 976191d4..5db5d5bd 100644 --- a/src/events/StateManager.js +++ b/src/events/StateManager.js @@ -5,14 +5,14 @@ import { PROPAGATE_EVENT } from './states/State'; * Manages the state of the application, including the registration, transition, and management of states. * This class implements a stack-based state machine, allowing for nested states and complex interaction flows. * - * @extends PIXI.EventEmitter + * @extends WildcardEventEmitter * @fires StateManager#state:pushed * @fires StateManager#state:popped * @fires StateManager#state:set * @fires StateManager#state:reset + * @fires StateManager#state:destroyed * @fires StateManager#modifier:activated * @fires StateManager#modifier:deactivated - * @fires StateManager#destroyed */ export default class StateManager extends WildcardEventEmitter { /** @private */ @@ -320,7 +320,7 @@ export default class StateManager extends WildcardEventEmitter { this.#boundEvents.clear(); this.#eventListeners = {}; - this.emit('destroyed', { target: this }); + this.emit('state:destroyed', { target: this }); this.removeAllListeners(); } } diff --git a/src/patchmap.js b/src/patchmap.js index 1bc331d5..0a56425e 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { Application, EventEmitter, UPDATE_PRIORITY } from 'pixi.js'; +import { Application, UPDATE_PRIORITY } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; import { UndoRedoManager } from './command/UndoRedoManager'; import { draw } from './display/draw'; @@ -22,8 +22,9 @@ import './display/components/registry'; import StateManager from './events/StateManager'; import SelectionState from './events/states/SelectionState'; import Transformer from './transformer/Transformer'; +import { WildcardEventEmitter } from './utils/event/WildcardEventEmitter'; -class Patchmap extends EventEmitter { +class Patchmap extends WildcardEventEmitter { _app = null; _viewport = null; _resizeObserver = null; @@ -131,7 +132,7 @@ class Patchmap extends EventEmitter { this._stateManager.register('selection', SelectionState, true); this.transformer = transformer; this.isInit = true; - this.emit('initialized', { target: this }); + this.emit('patchmap:initialized', { target: this }); } destroy() { @@ -155,6 +156,8 @@ class Patchmap extends EventEmitter { this._undoRedoManager = new UndoRedoManager(); this._animationContext = gsap.context(() => {}); this._transformer = null; + this.emit('patchmap:destroyed', { target: this }); + this.removeAllListeners(); } draw(data) { @@ -189,7 +192,7 @@ class Patchmap extends EventEmitter { ); this.app.start(); - this.emit('draw', { data: validatedData, target: this }); + this.emit('patchmap:draw', { data: validatedData, target: this }); return validatedData; function processData(data) { @@ -205,7 +208,7 @@ class Patchmap extends EventEmitter { update(opts) { const updatedElements = update(this.viewport, opts); - this.emit('updated', { elements: updatedElements, target: this }); + this.emit('patchmap:updated', { elements: updatedElements, target: this }); } focus(ids) { diff --git a/src/transformer/SelectionModel.js b/src/transformer/SelectionModel.js index 38e9310d..509c8c4f 100644 --- a/src/transformer/SelectionModel.js +++ b/src/transformer/SelectionModel.js @@ -1,7 +1,7 @@ -import { EventEmitter } from 'pixi.js'; import { convertArray } from '../utils/convert'; +import { WildcardEventEmitter } from '../utils/event/WildcardEventEmitter'; -export default class SelectionModel extends EventEmitter { +export default class SelectionModel extends WildcardEventEmitter { #elements = []; get elements() { @@ -59,4 +59,8 @@ export default class SelectionModel extends EventEmitter { }); } } + + destroy() { + this.removeAllListeners(); + } } diff --git a/src/transformer/Transformer.js b/src/transformer/Transformer.js index 262a047a..70c2da4b 100644 --- a/src/transformer/Transformer.js +++ b/src/transformer/Transformer.js @@ -207,7 +207,7 @@ export default class Transformer extends Container { if (this._viewport) { this._viewport.off('zoomed', this.update); } - this.selection.removeAllListeners(); + this.selection.destroy(); super.destroy(options); } diff --git a/src/utils/event/WildcardEventEmitter.js b/src/utils/event/WildcardEventEmitter.js index 78efcb2d..4a9f832b 100644 --- a/src/utils/event/WildcardEventEmitter.js +++ b/src/utils/event/WildcardEventEmitter.js @@ -41,12 +41,22 @@ export class WildcardEventEmitter extends EventEmitter { // 2. Check for a namespace separator. const separatorIndex = eventName.indexOf(':'); if (separatorIndex > 0) { - // 3. Construct and emit the wildcard event. const namespace = eventName.substring(0, separatorIndex); const wildcardEvent = `${namespace}:*`; - const wildcardResult = super.emit(wildcardEvent, ...args); - // Return true if either the specific or wildcard listener was found. + const originalPayload = args[0]; + let wildcardArgs = args; + + if ( + originalPayload && + typeof originalPayload === 'object' && + !Array.isArray(originalPayload) + ) { + const wildcardPayload = { ...originalPayload, type: eventName }; + wildcardArgs = [wildcardPayload, ...args.slice(1)]; + } + + const wildcardResult = super.emit(wildcardEvent, ...wildcardArgs); return specificResult || wildcardResult; } From ff61f9dcee0391c76726bb897180b0dab9f6e318 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 14:49:50 +0900 Subject: [PATCH 09/16] fix --- src/utils/event/WildcardEventEmitter.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/utils/event/WildcardEventEmitter.js b/src/utils/event/WildcardEventEmitter.js index 4a9f832b..0142f388 100644 --- a/src/utils/event/WildcardEventEmitter.js +++ b/src/utils/event/WildcardEventEmitter.js @@ -2,9 +2,8 @@ import { EventEmitter } from 'pixi.js'; /** * Extends PIXI.EventEmitter to add support for namespace-based wildcard events. - * When an event with a namespace (e.g., 'namespace:event') is emitted, - * it automatically triggers a corresponding wildcard event (e.g., 'namespace:*'). - * This allows for listening to all events within a specific category. + * It enriches the wildcard event payload with structured data, including the + * original namespace and event type. * * @extends PIXI.EventEmitter * @example @@ -27,21 +26,23 @@ import { EventEmitter } from 'pixi.js'; export class WildcardEventEmitter extends EventEmitter { /** * Emits an event to listeners. - * If the event name contains a colon (`:`), it also emits a wildcard event - * for the corresponding namespace (e.g., emitting 'state:pushed' will also emit 'state:*'). + * If the event name has a namespace (e.g., 'namespace:event'), this method + * will first emit the specific event. Then, it will emit a corresponding + * wildcard event ('namespace:*') with a structured payload that includes + * the `namespace` and `type` of the original event. + * * @override * @param {string} eventName - The name of the event to emit. * @param {...*} args - The arguments to pass to the listeners. - * @returns {boolean} `true` if the event had listeners (either specific or wildcard), else `false`. + * @returns {boolean} `true` if the event had listeners, else `false`. */ emit(eventName, ...args) { - // 1. Emit the original, specific event first. const specificResult = super.emit(eventName, ...args); - // 2. Check for a namespace separator. const separatorIndex = eventName.indexOf(':'); if (separatorIndex > 0) { const namespace = eventName.substring(0, separatorIndex); + const type = eventName.substring(separatorIndex + 1); const wildcardEvent = `${namespace}:*`; const originalPayload = args[0]; @@ -52,7 +53,7 @@ export class WildcardEventEmitter extends EventEmitter { typeof originalPayload === 'object' && !Array.isArray(originalPayload) ) { - const wildcardPayload = { ...originalPayload, type: eventName }; + const wildcardPayload = { ...originalPayload, namespace, type }; wildcardArgs = [wildcardPayload, ...args.slice(1)]; } From a4ce958b3e86a792074e3ffe77c7e99287156c0a Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:21:18 +0900 Subject: [PATCH 10/16] fix docs --- README.md | 13 ------------- README_KR.md | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/README.md b/README.md index ad0e40ba..405515e9 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ Therefore, to use this, an understanding of the following two libraries is essen - [canUndo()](#canundo) - [canRedo()](#canredo) - [clear()](#clear) - - [subscribe(listener)](#subscribelistener) - [πŸ§‘β€πŸ’» Development](#-development) - [Setting up the development environment](#setting-up-the-development-environment) - [VSCode Integration](#vscode-integration) @@ -591,18 +590,6 @@ Returns whether redo is possible. #### `clear()` Clears all command history. -#### `subscribe(listener)` -Subscribes a listener that will be called when command-related changes occur. You can call the returned function to unsubscribe. -```js -let canUndo = false; -let canRedo = false; - -const unsubscribe = undoRedoManager.subscribe((manager) => { - canUndo = manager.canUndo(); - canRedo = manager.canRedo(); -}); -``` -
## πŸ§‘β€πŸ’» Development diff --git a/README_KR.md b/README_KR.md index a784830a..581c760f 100644 --- a/README_KR.md +++ b/README_KR.md @@ -37,7 +37,6 @@ PATCH MAP은 PATCH μ„œλΉ„μŠ€μ˜ μš”κ΅¬ 사항을 μΆ©μ‘±μ‹œν‚€κΈ° μœ„ν•΄ `pixi.js - [canUndo()](#canundo) - [canRedo()](#canredo) - [clear()](#clear) - - [subscribe(listener)](#subscribelistener) - [πŸ§‘β€πŸ’» 개발](#-개발) - [개발 ν™˜κ²½ μ„ΈνŒ…](#개발-ν™˜κ²½-μ„ΈνŒ…) - [VSCode 톡합](#vscode-톡합) @@ -590,18 +589,6 @@ undoRedoManager.redo(); #### `clear()` λͺ¨λ“  λͺ…λ Ή 기둝을 μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€. -#### `subscribe(listener)` -λ¦¬μŠ€λ„ˆλ₯Ό κ΅¬λ…ν•˜μ—¬ λͺ…λ Ή κ΄€λ ¨ λ³€κ²½ 사항이 μ΄λ£¨μ–΄μ‘Œμ„ λ•Œ, ν•΄λ‹Ή λ¦¬μŠ€λ„ˆκ°€ ν˜ΈμΆœλ©λ‹ˆλ‹€. λ°˜ν™˜λœ ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜μ—¬ ꡬ독을 μ·¨μ†Œν•  수 μžˆμŠ΅λ‹ˆλ‹€. -```js -let canUndo = false; -let canRedo = false; - -const unsubscribe = undoRedoManager.subscribe((manager) => { - canUndo = manager.canUndo(); - canRedo = manager.canRedo(); -}); -``` -
## πŸ§‘β€πŸ’» 개발 From ed17814b54afc56ee890c0b45d1a0525d8065d31 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:33:27 +0900 Subject: [PATCH 11/16] fix doc --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++ README_KR.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 405515e9..f3d462cb 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Therefore, to use this, an understanding of the following two libraries is essen - [canUndo()](#canundo) - [canRedo()](#canredo) - [clear()](#clear) +- [πŸ“’ Full List of Available Events](#-full-list-of-available-events) - [πŸ§‘β€πŸ’» Development](#-development) - [Setting up the development environment](#setting-up-the-development-environment) - [VSCode Integration](#vscode-integration) @@ -559,6 +560,22 @@ patchmap.transformer.elements = [selectedObject]; patchmap.transformer.elements = []; ``` +#### transformer.selection + +An instance of `SelectionModel` for dedicated management of the `Transformer`'s selection state. This allows you to programmatically control the selected elements. + +```js +// Add, remove, and replace selected elements +transformer.selection.add(item1); +transformer.selection.remove(item1); +transformer.selection.set([item2]); + +// Subscribe to selection change events +transformer.on('update_elements', ({ current, added, removed }) => { + console.log('Current selection:', current); +}); +``` +
## undoRedoManager @@ -590,6 +607,46 @@ Returns whether redo is possible. #### `clear()` Clears all command history. +Of course. Here is the English translation of the event list: + +
+ +## πŸ“’ Full List of Available Events + +This is the list of events that can be subscribed to with this update. You can subscribe using `.on(eventName, callback)`. + +#### `Patchmap` + + * `patchmap:initialized`: Fired when `patchmap.init()` completes successfully. + * `patchmap:draw`: Fired when new data is rendered via `patchmap.draw()`. + * `patchmap:updated`: Fired when elements are updated via `patchmap.update()`. + * `patchmap:destroyed`: Fired when the instance is destroyed by calling `patchmap.destroy()`. + +#### `UndoRedoManager` + + * `history:executed`: Fired when a new command is added to the execution stack. + * `history:undone`: Fired when `undo()` is executed. + * `history:redone`: Fired when `redo()` is executed. + * `history:cleared`: Fired when all history is deleted with `clear()`. + * `history:destroyed`: Fired when `destroy()` is called. + * `history:*`: Subscribes to all of the above `history:` namespace events. + +#### `StateManager` + + * `state:pushed`: Fired when a new state is added to the stack. + * `state:popped`: Fired when the current state is removed from the stack. + * `state:set`: Fired when the state stack is reset and a new state is set via `setState()`. + * `state:reset`: Fired when all states are removed with `resetState()`. + * `state:destroyed`: Fired when `destroy()` is called. + * `modifier:activated`: Fired when a modifier state is activated. + * `modifier:deactivated`: Fired when a modifier state is deactivated. + * `state:*`: Subscribes to all of the above `state:` namespace events. + * `modifier:*`: Subscribes to all of the above `modifier:` namespace events. + +#### `Transformer` + + * `update_elements`: Fired when the content of `transformer.elements` or `transformer.selection` changes. +
## πŸ§‘β€πŸ’» Development diff --git a/README_KR.md b/README_KR.md index 581c760f..18867613 100644 --- a/README_KR.md +++ b/README_KR.md @@ -37,6 +37,7 @@ PATCH MAP은 PATCH μ„œλΉ„μŠ€μ˜ μš”κ΅¬ 사항을 μΆ©μ‘±μ‹œν‚€κΈ° μœ„ν•΄ `pixi.js - [canUndo()](#canundo) - [canRedo()](#canredo) - [clear()](#clear) +- [πŸ“’ μ‚¬μš© κ°€λŠ₯ν•œ 전체 이벀트 λͺ©λ‘](#-μ‚¬μš©-κ°€λŠ₯ν•œ-전체-이벀트-λͺ©λ‘) - [πŸ§‘β€πŸ’» 개발](#-개발) - [개발 ν™˜κ²½ μ„ΈνŒ…](#개발-ν™˜κ²½-μ„ΈνŒ…) - [VSCode 톡합](#vscode-톡합) @@ -47,19 +48,24 @@ PATCH MAP은 PATCH μ„œλΉ„μŠ€μ˜ μš”κ΅¬ 사항을 μΆ©μ‘±μ‹œν‚€κΈ° μœ„ν•΄ `pixi.js ## πŸš€ μ‹œμž‘ν•˜κΈ° ### μ„€μΉ˜ + #### NPM + ```sh npm install @conalog/patch-map ``` #### CDN + ```html ``` ### κΈ°λ³Έ 예제 + μ‹œμž‘ν•˜λŠ” 데 도움이 λ˜λŠ” κ°„λ‹¨ν•œ μ˜ˆμ œμž…λ‹ˆλ‹€: [예제](https://codesandbox.io/p/sandbox/yvjrpx) + ```js import { Patchmap } from '@conalog/patch-map'; @@ -108,6 +114,7 @@ patchmap.draw(data); ## Patchmap ### `init(el, options)` + PATCH MAP을 μ΄ˆκΈ°ν™”ν•˜λŠ” κ²ƒμœΌλ‘œ, 1번만 μ‹€ν–‰λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. ```js @@ -123,6 +130,7 @@ await patchmap.init(el, { ``` #### **Options** + λ Œλ”λ§ λ™μž‘μ„ μ‚¬μš©μž μ •μ˜ν•˜λ €λ©΄ λ‹€μŒ μ˜΅μ…˜μ„ μ‚¬μš©ν•˜μ„Έμš”: - `app` @@ -394,6 +402,7 @@ patchmap.fit(['item-1', 'item-2'])
### `selector(path)` + [jsonpath](https://github.com/JSONPath-Plus/JSONPath) 문법에 λ”°λ₯Έ 객체 νƒμƒ‰κΈ°μž…λ‹ˆλ‹€. ```js @@ -558,6 +567,22 @@ patchmap.transformer.elements = [selectedObject]; patchmap.transformer.elements = []; ``` +#### transformer.selection + +`Transformer`의 선택 μƒνƒœλ₯Ό μ „λ¬Έμ μœΌλ‘œ κ΄€λ¦¬ν•˜λŠ” `SelectionModel` μΈμŠ€ν„΄μŠ€μž…λ‹ˆλ‹€. 이λ₯Ό 톡해 μ„ νƒλœ μš”μ†Œλ₯Ό ν”„λ‘œκ·Έλž˜λ° λ°©μ‹μœΌλ‘œ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +```js +// 선택 μš”μ†Œ μΆ”κ°€, 제거, ꡐ체 +transformer.selection.add(item1); +transformer.selection.remove(item1); +transformer.selection.set([item2]); + +// 선택 λ³€κ²½ 이벀트 ꡬ독 +transformer.on('update_elements', ({ current, added, removed }) => { + console.log('ν˜„μž¬ 선택:', current); +}); +``` +
## undoRedoManager @@ -581,19 +606,61 @@ undoRedoManager.redo(); ``` #### `canUndo()` + μ‹€ν–‰ μ·¨μ†Œκ°€ κ°€λŠ₯ν•œμ§€ μ—¬λΆ€λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. #### `canRedo()` + μž¬μ‹€ν–‰μ΄ κ°€λŠ₯ν•œμ§€ μ—¬λΆ€λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. #### `clear()` + λͺ¨λ“  λͺ…λ Ή 기둝을 μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€.
+## πŸ“’ μ‚¬μš© κ°€λŠ₯ν•œ 전체 이벀트 λͺ©λ‘ + +이번 μ—…λ°μ΄νŠΈλ‘œ 인해 ꡬ독 κ°€λŠ₯ν•œ 이벀트 λͺ©λ‘μž…λ‹ˆλ‹€. `.on(eventName, callback)`을 μ‚¬μš©ν•˜μ—¬ ꡬ독할 수 μžˆμŠ΅λ‹ˆλ‹€. + +#### `Patchmap` + + * `patchmap:initialized`: `patchmap.init()`이 μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `patchmap:draw`: `patchmap.draw()`λ₯Ό 톡해 μƒˆλ‘œμš΄ 데이터가 λ Œλ”λ§λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `patchmap:updated`: `patchmap.update()`λ₯Ό 톡해 μš”μ†Œκ°€ μ—…λ°μ΄νŠΈλ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `patchmap:destroyed`: `patchmap.destroy()`κ°€ ν˜ΈμΆœλ˜μ–΄ μΈμŠ€ν„΄μŠ€κ°€ 파괴될 λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + +#### `UndoRedoManager` + + * `history:executed`: μƒˆλ‘œμš΄ μ»€λ§¨λ“œκ°€ μ‹€ν–‰ μŠ€νƒμ— μΆ”κ°€λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `history:undone`: `undo()`κ°€ μ‹€ν–‰λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `history:redone`: `redo()`κ°€ μ‹€ν–‰λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `history:cleared`: `clear()`둜 λͺ¨λ“  νžˆμŠ€ν† λ¦¬κ°€ μ‚­μ œλ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `history:destroyed`: `destroy()`κ°€ ν˜ΈμΆœλ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `history:*`: μœ„μ˜ λͺ¨λ“  `history:` λ„€μž„μŠ€νŽ˜μ΄μŠ€ 이벀트λ₯Ό κ΅¬λ…ν•©λ‹ˆλ‹€. + +#### `StateManager` + + * `state:pushed`: μƒˆλ‘œμš΄ μƒνƒœκ°€ μŠ€νƒμ— μΆ”κ°€λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `state:popped`: ν˜„μž¬ μƒνƒœκ°€ μŠ€νƒμ—μ„œ μ œκ±°λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `state:set`: `setState()`λ₯Ό 톡해 μƒνƒœ μŠ€νƒμ΄ λ¦¬μ…‹λ˜κ³  μƒˆλ‘œμš΄ μƒνƒœκ°€ μ„€μ •λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `state:reset`: `resetState()`둜 λͺ¨λ“  μƒνƒœκ°€ μ œκ±°λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `state:destroyed`: `destroy()`κ°€ ν˜ΈμΆœλ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `modifier:activated`: μˆ˜μ •μž(Modifier) μƒνƒœκ°€ ν™œμ„±ν™”λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `modifier:deactivated`: μˆ˜μ •μž(Modifier) μƒνƒœκ°€ λΉ„ν™œμ„±ν™”λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + * `state:*`: μœ„μ˜ λͺ¨λ“  `state:` λ„€μž„μŠ€νŽ˜μ΄μŠ€ 이벀트λ₯Ό κ΅¬λ…ν•©λ‹ˆλ‹€. + * `modifier:*`: μœ„μ˜ λͺ¨λ“  `modifier:` λ„€μž„μŠ€νŽ˜μ΄μŠ€ 이벀트λ₯Ό κ΅¬λ…ν•©λ‹ˆλ‹€. + +#### `Transformer` + + * `update_elements`: `transformer.elements` λ˜λŠ” `transformer.selection`의 λ‚΄μš©μ΄ 변경될 λ•Œ λ°œμƒν•©λ‹ˆλ‹€. + +
+ ## πŸ§‘β€πŸ’» 개발 ### 개발 ν™˜κ²½ μ„ΈνŒ… + ```sh npm install # μ˜μ‘΄μ„± μ„€μΉ˜ npm run dev # 개발 μ„œλ²„ μ‹œμž‘ @@ -602,9 +669,12 @@ npm run lint:fix # μ½”λ“œ ν¬λ§·νŒ… μˆ˜μ • ``` ### VSCode 톡합 + μΌκ΄€λœ μ½”λ“œ ν¬λ§·νŒ…μ„ μœ„ν•΄ Biome을 μ„€μ •ν•˜μ„Έμš”. -1. [Biome ν™•μž₯](https://biomejs.dev/reference/vscode/)을 μ„€μΉ˜ν•˜μ„Έμš”. -2. VSCode 섀정을 μ—…λ°μ΄νŠΈν•˜μ„Έμš”: + +1. [Biome ν™•μž₯](https://biomejs.dev/reference/vscode/)을 μ„€μΉ˜ν•˜μ„Έμš”. +2. VSCode 섀정을 μ—…λ°μ΄νŠΈν•˜μ„Έμš”: + ```json { "editor.formatOnSave": true, From 9657b05f7e68c954a64e59a2e3c1d1c6a8fe7e2a Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:36:41 +0900 Subject: [PATCH 12/16] fix --- src/command/UndoRedoManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command/UndoRedoManager.js b/src/command/UndoRedoManager.js index 2385ea7c..f5ee1670 100644 --- a/src/command/UndoRedoManager.js +++ b/src/command/UndoRedoManager.js @@ -53,7 +53,7 @@ export class UndoRedoManager extends WildcardEventEmitter { * @readonly */ get commands() { - return this.#commands; + return [...this.#commands]; } /** From 6b200b6991fdb590bd6be33289028912cddba155 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:37:11 +0900 Subject: [PATCH 13/16] fix --- src/command/UndoRedoManager.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/command/UndoRedoManager.test.js b/src/command/UndoRedoManager.test.js index 11c94388..2faf974e 100644 --- a/src/command/UndoRedoManager.test.js +++ b/src/command/UndoRedoManager.test.js @@ -149,7 +149,7 @@ describe('UndoRedoManager', () => { it('should correctly notify subscribers on state changes', () => { const manager = new UndoRedoManager(); const listener = vi.fn(); - manager.on('change', listener); + manager.on('history:*', listener); manager.execute(new MockCommand()); expect(listener).toHaveBeenCalledTimes(1); @@ -163,7 +163,7 @@ describe('UndoRedoManager', () => { manager.clear(); expect(listener).toHaveBeenCalledTimes(4); - manager.off('change', listener); + manager.off('history:*', listener); manager.execute(new MockCommand()); expect(listener).toHaveBeenCalledTimes(4); }); From 97511323b5ed9535e329d961298d166c42d2734e Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:37:38 +0900 Subject: [PATCH 14/16] fix --- src/transformer/SelectionModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformer/SelectionModel.js b/src/transformer/SelectionModel.js index 509c8c4f..1aaa9fd5 100644 --- a/src/transformer/SelectionModel.js +++ b/src/transformer/SelectionModel.js @@ -5,7 +5,7 @@ export default class SelectionModel extends WildcardEventEmitter { #elements = []; get elements() { - return this.#elements; + return [...this.#elements]; } set(elements) { From bfe05c7c9b811241780a276da0d451f3a6603b94 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:38:11 +0900 Subject: [PATCH 15/16] fix --- src/transformer/Transformer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/transformer/Transformer.js b/src/transformer/Transformer.js index 70c2da4b..ab72f978 100644 --- a/src/transformer/Transformer.js +++ b/src/transformer/Transformer.js @@ -146,6 +146,9 @@ export default class Transformer extends Container { return this._boundsDisplayMode; } + /** + * @param {BoundsDisplayMode} value + */ set boundsDisplayMode(value) { this._boundsDisplayMode = value; this.update(); From 9d6ed32d083fe82c8900a53a0b244768d393f15a Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 21 Aug 2025 15:42:11 +0900 Subject: [PATCH 16/16] fix --- README.md | 2 -- src/patchmap.js | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3d462cb..148e6424 100644 --- a/README.md +++ b/README.md @@ -607,8 +607,6 @@ Returns whether redo is possible. #### `clear()` Clears all command history. -Of course. Here is the English translation of the event list: -
## πŸ“’ Full List of Available Events diff --git a/src/patchmap.js b/src/patchmap.js index 0a56425e..5f2c4b1a 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -141,6 +141,7 @@ class Patchmap extends WildcardEventEmitter { this.undoRedoManager.destroy(); this.animationContext.revert(); this.stateManager.resetState(); + this.stateManager.destroy(); event.removeAllEvent(this.viewport); this.viewport.destroy({ children: true, context: true, style: true }); const parentElement = this.app.canvas.parentElement; @@ -156,6 +157,7 @@ class Patchmap extends WildcardEventEmitter { this._undoRedoManager = new UndoRedoManager(); this._animationContext = gsap.context(() => {}); this._transformer = null; + this._stateManager = null; this.emit('patchmap:destroyed', { target: this }); this.removeAllListeners(); }