From 1471b9a008c14fb437ef5aff3279e63dbb440295 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Tue, 10 Jun 2025 19:05:51 +0900 Subject: [PATCH 01/27] fix init asset --- src/init.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/init.js b/src/init.js index 2451cf58..d67702c2 100644 --- a/src/init.js +++ b/src/init.js @@ -75,14 +75,22 @@ export const initViewport = (app, opts = {}) => { export const initAsset = async (opts = {}) => { const options = deepMerge(DEFAULT_INIT_OPTIONS.assets, opts); - const manifest = transformManifest(options); - await PIXI.Assets.init({ manifest }); - const fontBundle = Object.entries(firaCode).map(([key, font]) => ({ - alias: `firaCode-${key}`, - src: font, - data: { family: `FiraCode ${key}` }, - })); - PIXI.Assets.addBundle('fonts', fontBundle); + + if (!PIXI.Assets._initialized) { + const manifest = transformManifest(options); + await PIXI.Assets.init({ manifest }); + } + + const fontsBundle = await PIXI.Assets.loadBundle(['fonts']); + if (!('fonts' in fontsBundle)) { + const fontBundle = Object.entries(firaCode).map(([key, font]) => ({ + alias: `firaCode-${key}`, + src: font, + data: { family: `FiraCode ${key}` }, + })); + PIXI.Assets.addBundle('fonts', fontBundle); + } + await PIXI.Assets.loadBundle([...Object.keys(options), 'fonts']); }; From 81fd91b8c7ddff66d607ae5a233d02546fb5af99 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 11:34:25 +0900 Subject: [PATCH 02/27] fix export undoredomanger --- src/command/index.js | 2 -- src/patch-map.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/command/index.js b/src/command/index.js index f07b568e..53afc78d 100644 --- a/src/command/index.js +++ b/src/command/index.js @@ -1,5 +1,3 @@ import * as commands from './commands'; -import { UndoRedoManager } from './undo-redo-manager'; export const Commands = commands; -export const undoRedoManager = new UndoRedoManager(); diff --git a/src/patch-map.ts b/src/patch-map.ts index 7a184528..74c3c9e6 100644 --- a/src/patch-map.ts +++ b/src/patch-map.ts @@ -1,4 +1,4 @@ export { Patchmap } from './patchmap'; -export { undoRedoManager } from './command'; +export { UndoRedoManager } from './command/undo-redo-manager'; export { Command } from './command/commands/base'; export * as change from './display/change'; From f7145bd0a16625c3a46b2ab0613dc9e7a84980c9 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 11:35:28 +0900 Subject: [PATCH 03/27] fix pipeline name --- src/display/change/pipeline/base.js | 2 +- src/display/change/pipeline/component.js | 4 ++-- src/display/change/pipeline/element.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/display/change/pipeline/base.js b/src/display/change/pipeline/base.js index 7b08d630..06212bda 100644 --- a/src/display/change/pipeline/base.js +++ b/src/display/change/pipeline/base.js @@ -2,7 +2,7 @@ import * as change from '..'; import { Commands } from '../../../command'; import { createCommandHandler } from './utils'; -export const pipeline = { +export const basePipeline = { show: { keys: ['show'], handler: createCommandHandler(Commands.ShowCommand, change.changeShow), diff --git a/src/display/change/pipeline/component.js b/src/display/change/pipeline/component.js index 1bc21f44..950a7638 100644 --- a/src/display/change/pipeline/component.js +++ b/src/display/change/pipeline/component.js @@ -1,10 +1,10 @@ import * as change from '..'; import { Commands } from '../../../command'; -import { pipeline } from './base'; +import { basePipeline } from './base'; import { createCommandHandler } from './utils'; export const componentPipeline = { - ...pipeline, + ...basePipeline, tint: { keys: ['color', 'tint'], handler: createCommandHandler(Commands.TintCommand, change.changeTint), diff --git a/src/display/change/pipeline/element.js b/src/display/change/pipeline/element.js index 2d495a9e..41a0954d 100644 --- a/src/display/change/pipeline/element.js +++ b/src/display/change/pipeline/element.js @@ -1,11 +1,11 @@ import * as change from '..'; import { Commands } from '../../../command'; import { updateComponents } from '../../update/update-components'; -import { pipeline } from './base'; +import { basePipeline } from './base'; import { createCommandHandler } from './utils'; export const elementPipeline = { - ...pipeline, + ...basePipeline, position: { keys: ['position'], handler: createCommandHandler( From 51b8323585a30bd8994aeaef845773a6aee9637e Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 11:36:28 +0900 Subject: [PATCH 04/27] fix undoredomanager instance --- src/display/change/pipeline/utils.js | 4 +--- src/display/draw.js | 10 +++++----- src/display/elements/grid.js | 15 +++++++++++---- src/display/elements/group.js | 15 +++++++++++---- src/display/elements/item.js | 15 +++++++++++---- src/display/elements/relations.js | 15 +++++++++++---- src/display/update/update-object.js | 9 +++++---- src/display/update/update.js | 18 +++++++++++++----- src/patchmap.js | 15 ++++++++++----- 9 files changed, 78 insertions(+), 38 deletions(-) diff --git a/src/display/change/pipeline/utils.js b/src/display/change/pipeline/utils.js index aced9917..594a783a 100644 --- a/src/display/change/pipeline/utils.js +++ b/src/display/change/pipeline/utils.js @@ -1,7 +1,5 @@ -import { undoRedoManager } from '../../../command'; - export const createCommandHandler = (Command, changeFn) => { - return (object, config, options) => { + return (object, config, undoRedoManager, options) => { if (options?.historyId) { undoRedoManager.execute(new Command(object, config), { historyId: options.historyId, diff --git a/src/display/draw.js b/src/display/draw.js index 43713cdd..cc8c79c7 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -5,7 +5,7 @@ import { createItem } from './elements/item'; import { createRelations } from './elements/relations'; import { update } from './update/update'; -export const draw = (viewport, data) => { +export const draw = (viewport, data, undoRedoManager) => { gsap.globalTimeline.clear(); destroyChildren(viewport); render(viewport, data); @@ -16,7 +16,7 @@ export const draw = (viewport, data) => { case 'group': { const element = createGroup(config); element.viewport = viewport; - update(null, { + update(null, undoRedoManager, { elements: element, changes: config, }); @@ -27,7 +27,7 @@ export const draw = (viewport, data) => { case 'grid': { const element = createGrid(config); element.viewport = viewport; - update(null, { + update(null, undoRedoManager, { elements: element, changes: config, }); @@ -37,7 +37,7 @@ export const draw = (viewport, data) => { case 'item': { const element = createItem(config); element.viewport = viewport; - update(null, { + update(null, undoRedoManager, { elements: element, changes: config, }); @@ -47,7 +47,7 @@ export const draw = (viewport, data) => { case 'relations': { const element = createRelations({ viewport, ...config }); element.viewport = viewport; - update(null, { + update(null, undoRedoManager, { elements: element, changes: config, }); diff --git a/src/display/elements/grid.js b/src/display/elements/grid.js index 03622887..e762bd23 100644 --- a/src/display/elements/grid.js +++ b/src/display/elements/grid.js @@ -24,10 +24,17 @@ export const createGrid = (config) => { }; const pipelineKeys = ['show', 'position', 'gridComponents']; -export const updateGrid = (element, config, options) => { - const validateConfig = validate(config, deepGridObject); - if (isValidationError(validateConfig)) throw validateConfig; - updateObject(element, config, elementPipeline, pipelineKeys, options); +export const updateGrid = (element, changes, undoRedoManager, options) => { + const validated = validate(changes, deepGridObject); + if (isValidationError(validated)) throw validated; + updateObject( + element, + changes, + elementPipeline, + pipelineKeys, + undoRedoManager, + options, + ); }; const addItemElements = (container, cells, cellSize) => { diff --git a/src/display/elements/group.js b/src/display/elements/group.js index 7f579183..00b63a69 100644 --- a/src/display/elements/group.js +++ b/src/display/elements/group.js @@ -11,8 +11,15 @@ export const createGroup = (config) => { }; const pipelineKeys = ['show', 'position']; -export const updateGroup = (element, config, options) => { - const validateConfig = validate(config, deepGroupObject); - if (isValidationError(validateConfig)) throw validateConfig; - updateObject(element, config, elementPipeline, pipelineKeys, options); +export const updateGroup = (element, changes, undoRedoManager, options) => { + const validated = validate(changes, deepGroupObject); + if (isValidationError(validated)) throw validated; + updateObject( + element, + changes, + elementPipeline, + pipelineKeys, + undoRedoManager, + options, + ); }; diff --git a/src/display/elements/item.js b/src/display/elements/item.js index 4c68671f..71c25565 100644 --- a/src/display/elements/item.js +++ b/src/display/elements/item.js @@ -18,8 +18,15 @@ export const createItem = (config) => { }; const pipelineKeys = ['show', 'position', 'components']; -export const updateItem = (element, config, options) => { - const validateConfig = validate(config, deepSingleObject); - if (isValidationError(validateConfig)) throw validateConfig; - updateObject(element, config, elementPipeline, pipelineKeys, options); +export const updateItem = (element, changes, undoRedoManager, options) => { + const validated = validate(changes, deepSingleObject); + if (isValidationError(validated)) throw validated; + updateObject( + element, + changes, + elementPipeline, + pipelineKeys, + undoRedoManager, + options, + ); }; diff --git a/src/display/elements/relations.js b/src/display/elements/relations.js index c563bbc1..d536d6ed 100644 --- a/src/display/elements/relations.js +++ b/src/display/elements/relations.js @@ -14,10 +14,17 @@ export const createRelations = (config) => { }; const pipelineKeys = ['show', 'strokeStyle', 'links']; -export const updateRelations = (element, config, options) => { - const validateConfig = validate(config, deepRelationGroupObject); - if (isValidationError(validateConfig)) throw validateConfig; - updateObject(element, config, elementPipeline, pipelineKeys, options); +export const updateRelations = (element, changes, undoRedoManager, options) => { + const validated = validate(changes, deepRelationGroupObject); + if (isValidationError(validated)) throw validated; + updateObject( + element, + changes, + elementPipeline, + pipelineKeys, + undoRedoManager, + options, + ); }; const createPath = () => { diff --git a/src/display/update/update-object.js b/src/display/update/update-object.js index 5be85a30..380bc1f0 100644 --- a/src/display/update/update-object.js +++ b/src/display/update/update-object.js @@ -4,23 +4,24 @@ const DEFAULT_EXCEPTION_KEYS = new Set(['position']); export const updateObject = ( object, - config, + changes, pipeline, pipelineKeys, + undoRedoManager, options, ) => { if (!object) return; const pipelines = pipelineKeys.map((key) => pipeline[key]).filter(Boolean); for (const { keys, handler } of pipelines) { - const hasMatch = keys.some((key) => key in config); + const hasMatch = keys.some((key) => key in changes); if (hasMatch) { - handler(object, config, options); + handler(object, changes, undoRedoManager, options); } } const matchedKeys = new Set(pipelines.flatMap((item) => item.keys)); - for (const [key, value] of Object.entries(config)) { + for (const [key, value] of Object.entries(changes)) { if (!matchedKeys.has(key) && !DEFAULT_EXCEPTION_KEYS.has(key)) { changeProperty(object, key, value); } diff --git a/src/display/update/update.js b/src/display/update/update.js index 40fb3b2c..5f36f867 100644 --- a/src/display/update/update.js +++ b/src/display/update/update.js @@ -16,7 +16,7 @@ const updateSchema = z.object({ relativeTransform: z.boolean().default(false), }); -export const update = (parent, opts) => { +export const update = (parent, undoRedoManager, opts) => { const config = validate(opts, updateSchema.passthrough()); if (isValidationError(config)) throw config; @@ -35,16 +35,24 @@ export const update = (parent, opts) => { switch (element.type) { case 'group': - updateGroup(element, elConfig.changes, { historyId }); + updateGroup(element, elConfig.changes, undoRedoManager, { + historyId, + }); break; case 'grid': - updateGrid(element, elConfig.changes, { historyId }); + updateGrid(element, elConfig.changes, undoRedoManager, { + historyId, + }); break; case 'item': - updateItem(element, elConfig.changes, { historyId }); + updateItem(element, elConfig.changes, undoRedoManager, { + historyId, + }); break; case 'relations': - updateRelations(element, elConfig.changes, { historyId }); + updateRelations(element, elConfig.changes, undoRedoManager, { + historyId, + }); break; } } diff --git a/src/patchmap.js b/src/patchmap.js index 406cf93a..8b08f072 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -1,7 +1,7 @@ import gsap from 'gsap'; import { Application, Assets } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; -import { undoRedoManager } from './command'; +import { UndoRedoManager } from './command/undo-redo-manager'; import { draw } from './display/draw'; import { update } from './display/update/update'; import { dragSelect } from './events/drag-select'; @@ -27,6 +27,7 @@ class Patchmap { this._viewport = null; this._resizeObserver = null; this._isInit = false; + this._undoRedoManager = new UndoRedoManager(); } get app() { @@ -45,6 +46,10 @@ class Patchmap { return this._isInit; } + get undoRedoManager() { + return this._undoRedoManager; + } + get event() { return { add: (opts) => { @@ -70,7 +75,7 @@ class Patchmap { asset: assetOptions = {}, } = opts; - undoRedoManager._setHotkeys(); + this.undoRedoManager._setHotkeys(); theme.set(themeOptions); this._app = new Application(); await initApp(this.app, { resizeTo: element, ...appOptions }); @@ -106,9 +111,9 @@ class Patchmap { if (isValidationError(validatedData)) throw validatedData; this.app.stop(); - draw(this.viewport, validatedData); + draw(this.viewport, validatedData, this.undoRedoManager); this.app.start(); - undoRedoManager.clear(); + this.undoRedoManager.clear(); return validatedData; function preprocessData(data) { @@ -131,7 +136,7 @@ class Patchmap { } update(opts) { - update(this.viewport, opts); + update(this.viewport, this.undoRedoManager, opts); } focus(ids) { From 710f24a32d7b9ed6dcd90b48ba487caccbf88169 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 14:12:13 +0900 Subject: [PATCH 05/27] fix local renderer --- src/assets/textures/rect.js | 6 +++--- src/assets/textures/texture.js | 16 ++++++++-------- src/assets/textures/utils.js | 14 +++++++------- src/display/change/texture.js | 3 +++ src/init.js | 3 +++ src/patchmap.js | 2 -- src/utils/canvas.js | 5 ++--- src/utils/renderer.js | 13 ------------- 8 files changed, 26 insertions(+), 36 deletions(-) delete mode 100644 src/utils/renderer.js diff --git a/src/assets/textures/rect.js b/src/assets/textures/rect.js index 72827e01..66f23a62 100644 --- a/src/assets/textures/rect.js +++ b/src/assets/textures/rect.js @@ -2,7 +2,7 @@ import { Graphics } from 'pixi.js'; import { getColor } from '../../utils/get'; import { cacheKey, generateTexture } from './utils'; -export const createRectTexture = (rectOpts) => { +export const createRectTexture = (renderer, rectOpts) => { const { fill = null, borderWidth = null, @@ -10,9 +10,9 @@ export const createRectTexture = (rectOpts) => { radius = null, } = rectOpts; const rect = createRect({ fill, borderWidth, borderColor, radius }); - const texture = generateTexture(rect); + const texture = generateTexture(rect, renderer); - texture.id = cacheKey(rectOpts); + texture.id = cacheKey(renderer, rectOpts); texture.metadata = { slice: { topHeight: borderWidth + 4, diff --git a/src/assets/textures/texture.js b/src/assets/textures/texture.js index cf506ba2..29f6a43e 100644 --- a/src/assets/textures/texture.js +++ b/src/assets/textures/texture.js @@ -1,26 +1,26 @@ -import { Assets, Cache } from 'pixi.js'; +import { Assets } from 'pixi.js'; import { createRectTexture } from './rect'; import { cacheKey } from './utils'; -export const getTexture = (config) => { +export const getTexture = (renderer, config) => { let texture = null; if (typeof config === 'string') { texture = Assets.get(config); } else { - texture = Cache.has(cacheKey(config)) - ? Assets.get(cacheKey(config)) - : createTexture(config); + texture = Assets.cache.has(cacheKey(renderer, config)) + ? Assets.cache.get(cacheKey(renderer, config)) + : createTexture(renderer, config); } return texture; }; -export const createTexture = (config) => { +export const createTexture = (renderer, config) => { let texture = null; switch (config.type) { case 'rect': - texture = createRectTexture(config); + texture = createRectTexture(renderer, config); break; } - Cache.set(cacheKey(config), texture); + Assets.cache.set(cacheKey(renderer, config), texture); return texture; }; diff --git a/src/assets/textures/utils.js b/src/assets/textures/utils.js index f660fc44..7f66cb8d 100644 --- a/src/assets/textures/utils.js +++ b/src/assets/textures/utils.js @@ -1,22 +1,22 @@ import { TextureStyle } from '../../display/data-schema/component-schema'; import { deepMerge } from '../../utils/deepmerge/deepmerge'; -import { renderer } from '../../utils/renderer'; const RESOLUTION = 5; -export const generateTexture = (target = null, opts = {}) => { +export const generateTexture = (target, renderer, opts = {}) => { const options = deepMerge({ resolution: RESOLUTION }, opts); if (!target) return; - const texture = renderer.get().generateTexture({ + const texture = renderer.generateTexture({ target, resolution: options.resolution, }); return texture; }; -export const cacheKey = (config) => { - return TextureStyle.keyof() - .options.map((key) => config[key]) - .join('-'); +export const cacheKey = (renderer, config) => { + return [ + renderer.uid, + ...TextureStyle.keyof().options.map((key) => config[key]), + ].join('-'); }; diff --git a/src/display/change/texture.js b/src/display/change/texture.js index 8ce99627..9eccf427 100644 --- a/src/display/change/texture.js +++ b/src/display/change/texture.js @@ -1,5 +1,6 @@ import { getTexture } from '../../assets/textures/texture'; import { deepMerge } from '../../utils/deepmerge/deepmerge'; +import { getViewport } from '../../utils/get'; import { isConfigMatch, updateConfig } from './utils'; export const changeTexture = (object, { texture: textureConfig }) => { @@ -7,7 +8,9 @@ export const changeTexture = (object, { texture: textureConfig }) => { return; } + const renderer = getViewport(object).app.renderer; const texture = getTexture( + renderer, deepMerge(object.texture?.metadata?.config, textureConfig), ); object.texture = texture ?? null; diff --git a/src/init.js b/src/init.js index d67702c2..ffb21e98 100644 --- a/src/init.js +++ b/src/init.js @@ -7,6 +7,7 @@ import { icons } from './assets/icons'; import { transformManifest } from './assets/utils'; import { deepMerge } from './utils/deepmerge/deepmerge'; import { plugin } from './utils/event/viewport'; +import { uid } from './utils/uuid'; gsap.registerPlugin(PixiPlugin); PixiPlugin.registerPIXI(PIXI); @@ -47,6 +48,7 @@ const DEFAULT_INIT_OPTIONS = { export const initApp = async (app, opts = {}) => { const options = deepMerge(DEFAULT_INIT_OPTIONS.app, opts); await app.init(options); + app.renderer.uid = uid(); }; export const initViewport = (app, opts = {}) => { @@ -60,6 +62,7 @@ export const initViewport = (app, opts = {}) => { opts, ); const viewport = new Viewport(options); + viewport.app = app; viewport.type = 'canvas'; viewport.events = {}; viewport.plugin = { diff --git a/src/patchmap.js b/src/patchmap.js index 8b08f072..e403fa12 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -16,7 +16,6 @@ import { } from './init'; import { convertLegacyData } from './utils/convert'; import { event } from './utils/event/canvas'; -import { renderer } from './utils/renderer'; import { selector } from './utils/selector/selector'; import { theme } from './utils/theme'; import { validateMapData } from './utils/validator'; @@ -81,7 +80,6 @@ class Patchmap { await initApp(this.app, { resizeTo: element, ...appOptions }); this._viewport = initViewport(this.app, viewportOptions); await initAsset(assetOptions); - renderer.set(this.app.renderer); initCanvas(element, this.app); this._resizeObserver = initResizeObserver(element, this.app, this.viewport); diff --git a/src/utils/canvas.js b/src/utils/canvas.js index 6ffff8e9..27672ea6 100644 --- a/src/utils/canvas.js +++ b/src/utils/canvas.js @@ -1,5 +1,3 @@ -import { renderer } from './renderer'; - export const getScaleBounds = (viewport, object) => { const bounds = object.getBounds(); return { @@ -11,6 +9,7 @@ export const getScaleBounds = (viewport, object) => { }; export const getPointerPosition = (viewport) => { - const global = renderer.get().events.pointer.global; + const renderer = viewport.app.renderer; + const global = renderer.events.pointer.global; return viewport ? viewport.toWorld(global.x, global.y) : global; }; diff --git a/src/utils/renderer.js b/src/utils/renderer.js deleted file mode 100644 index e2ea9a9f..00000000 --- a/src/utils/renderer.js +++ /dev/null @@ -1,13 +0,0 @@ -const rendererStore = () => { - let _renderer = null; - - const set = (renderer) => { - _renderer = renderer; - }; - - const get = () => _renderer; - - return { set, get }; -}; - -export const renderer = rendererStore(); From a529ab457f890ba6546c47047bfffa3d224f65f4 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 14:42:47 +0900 Subject: [PATCH 06/27] apply context object pattern --- src/display/draw.js | 3 ++- src/display/update/update.js | 7 ++++--- src/patchmap.js | 19 ++++++++++++++----- src/utils/theme.js | 4 +--- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/display/draw.js b/src/display/draw.js index cc8c79c7..55fc3a07 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -5,7 +5,8 @@ import { createItem } from './elements/item'; import { createRelations } from './elements/relations'; import { update } from './update/update'; -export const draw = (viewport, data, undoRedoManager) => { +export const draw = (context, data) => { + const { viewport, undoRedoManager } = context; gsap.globalTimeline.clear(); destroyChildren(viewport); render(viewport, data); diff --git a/src/display/update/update.js b/src/display/update/update.js index 5f36f867..3ff9255f 100644 --- a/src/display/update/update.js +++ b/src/display/update/update.js @@ -16,14 +16,15 @@ const updateSchema = z.object({ relativeTransform: z.boolean().default(false), }); -export const update = (parent, undoRedoManager, opts) => { +export const update = (context, opts) => { const config = validate(opts, updateSchema.passthrough()); if (isValidationError(config)) throw config; + const { viewport, undoRedoManager } = context; const historyId = createHistoryId(config.saveToHistory); const elements = 'elements' in config ? convertArray(config.elements) : []; - if (parent && config.path) { - elements.push(...selector(parent, config.path)); + if (viewport && config.path) { + elements.push(...selector(viewport, config.path)); } for (const element of elements) { diff --git a/src/patchmap.js b/src/patchmap.js index e403fa12..1a2df1c0 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -17,7 +17,7 @@ import { import { convertLegacyData } from './utils/convert'; import { event } from './utils/event/canvas'; import { selector } from './utils/selector/selector'; -import { theme } from './utils/theme'; +import { themeStore } from './utils/theme'; import { validateMapData } from './utils/validator'; class Patchmap { @@ -26,6 +26,7 @@ class Patchmap { this._viewport = null; this._resizeObserver = null; this._isInit = false; + this._theme = themeStore(); this._undoRedoManager = new UndoRedoManager(); } @@ -38,7 +39,7 @@ class Patchmap { } get theme() { - return theme.get(); + return this._theme.get(); } get isInit() { @@ -75,7 +76,7 @@ class Patchmap { } = opts; this.undoRedoManager._setHotkeys(); - theme.set(themeOptions); + this._theme.set(themeOptions); this._app = new Application(); await initApp(this.app, { resizeTo: element, ...appOptions }); this._viewport = initViewport(this.app, viewportOptions); @@ -109,7 +110,11 @@ class Patchmap { if (isValidationError(validatedData)) throw validatedData; this.app.stop(); - draw(this.viewport, validatedData, this.undoRedoManager); + const context = { + viewport: this.viewport, + undoRedoManager: this.undoRedoManager, + }; + draw(context, validatedData); this.app.start(); this.undoRedoManager.clear(); return validatedData; @@ -134,7 +139,11 @@ class Patchmap { } update(opts) { - update(this.viewport, this.undoRedoManager, opts); + const context = { + viewport: this.viewport, + undoRedoManager: this.undoRedoManager, + }; + update(context, opts); } focus(ids) { diff --git a/src/utils/theme.js b/src/utils/theme.js index e46a56aa..c02ab183 100644 --- a/src/utils/theme.js +++ b/src/utils/theme.js @@ -1,6 +1,6 @@ import { deepMerge } from './deepmerge/deepmerge'; -const themeStore = () => { +export const themeStore = () => { let _theme = { primary: { default: '#0C73BF', @@ -24,5 +24,3 @@ const themeStore = () => { return { set, get }; }; - -export const theme = themeStore(); From 1e43adc8949165dbd699f787ac42a615430919a9 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 14:43:02 +0900 Subject: [PATCH 07/27] fix getColor --- src/utils/get.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/get.js b/src/utils/get.js index e3384b4c..0e2513ab 100644 --- a/src/utils/get.js +++ b/src/utils/get.js @@ -1,5 +1,3 @@ -import { theme } from './theme'; - export const getNestedValue = (object, path = null) => { if (!path) return null; return path @@ -7,11 +5,11 @@ export const getNestedValue = (object, path = null) => { .reduce((acc, key) => (acc && acc[key] != null ? acc[key] : null), object); }; -export const getColor = (color) => { +export const getColor = (theme, color) => { return ( (typeof color === 'string' && color.startsWith('#') ? color - : getNestedValue(theme.get(), color)) ?? '#000' + : getNestedValue(theme, color)) ?? '#000' ); }; From ffef39d77f73d13afff64a8e5e06a7101911c240 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 15:41:23 +0900 Subject: [PATCH 08/27] fix theme --- src/assets/textures/rect.js | 10 +++++----- src/assets/textures/texture.js | 8 ++++---- src/command/commands/tint.js | 11 ++++++++--- src/display/change/asset.js | 5 +++-- src/display/change/pipeline/component.js | 8 ++++---- src/display/change/pipeline/utils.js | 7 ++++--- src/display/change/stroke-style.js | 8 ++++++-- src/display/change/text-style.js | 3 ++- src/display/change/texture.js | 7 ++++++- src/display/change/tint.js | 4 ++-- src/display/draw.js | 10 +++++----- src/display/elements/grid.js | 11 ++--------- src/display/elements/group.js | 11 ++--------- src/display/elements/item.js | 11 ++--------- src/display/elements/relations.js | 11 ++--------- src/display/update/update-object.js | 3 +-- src/display/update/update.js | 18 +++++++++++++----- src/patchmap.js | 2 ++ 18 files changed, 73 insertions(+), 75 deletions(-) diff --git a/src/assets/textures/rect.js b/src/assets/textures/rect.js index 66f23a62..b5efc0a1 100644 --- a/src/assets/textures/rect.js +++ b/src/assets/textures/rect.js @@ -2,14 +2,14 @@ import { Graphics } from 'pixi.js'; import { getColor } from '../../utils/get'; import { cacheKey, generateTexture } from './utils'; -export const createRectTexture = (renderer, rectOpts) => { +export const createRectTexture = (renderer, theme, rectOpts) => { const { fill = null, borderWidth = null, borderColor = null, radius = null, } = rectOpts; - const rect = createRect({ fill, borderWidth, borderColor, radius }); + const rect = createRect(theme, { fill, borderWidth, borderColor, radius }); const texture = generateTexture(rect, renderer); texture.id = cacheKey(renderer, rectOpts); @@ -26,7 +26,7 @@ export const createRectTexture = (renderer, rectOpts) => { return texture; }; -const createRect = ({ fill, borderWidth, borderColor, radius }) => { +const createRect = (theme, { fill, borderWidth, borderColor, radius }) => { const graphics = new Graphics(); const size = 20 + borderWidth; @@ -37,11 +37,11 @@ const createRect = ({ fill, borderWidth, borderColor, radius }) => { graphics.rect(...xywh); } - if (fill) graphics.fill(getColor(fill)); + if (fill) graphics.fill(getColor(theme, fill)); if (borderWidth) { graphics.stroke({ width: borderWidth, - color: getColor(borderColor), + color: getColor(theme, borderColor), }); } return graphics; diff --git a/src/assets/textures/texture.js b/src/assets/textures/texture.js index 29f6a43e..1e56de75 100644 --- a/src/assets/textures/texture.js +++ b/src/assets/textures/texture.js @@ -2,23 +2,23 @@ import { Assets } from 'pixi.js'; import { createRectTexture } from './rect'; import { cacheKey } from './utils'; -export const getTexture = (renderer, config) => { +export const getTexture = (renderer, theme, config) => { let texture = null; if (typeof config === 'string') { texture = Assets.get(config); } else { texture = Assets.cache.has(cacheKey(renderer, config)) ? Assets.cache.get(cacheKey(renderer, config)) - : createTexture(renderer, config); + : createTexture(renderer, theme, config); } return texture; }; -export const createTexture = (renderer, config) => { +export const createTexture = (renderer, theme, config) => { let texture = null; switch (config.type) { case 'rect': - texture = createRectTexture(renderer, config); + texture = createRectTexture(renderer, theme, config); break; } Assets.cache.set(cacheKey(renderer, config), texture); diff --git a/src/command/commands/tint.js b/src/command/commands/tint.js index 427944d0..5c913bd7 100644 --- a/src/command/commands/tint.js +++ b/src/command/commands/tint.js @@ -18,11 +18,12 @@ export class TintCommand extends Command { * @param {Object} object - The Pixi.js display object whose tint will be changed. * @param {Object} config - The new configuration for the object's tint. */ - constructor(object, config) { + constructor(object, config, options) { super('tint_object'); this.object = object; this._config = parsePick(config, optionKeys); this._prevConfig = parsePick(object.config, optionKeys); + this._options = options; } get config() { @@ -33,17 +34,21 @@ export class TintCommand extends Command { return this._prevConfig; } + get options() { + return this._options; + } + /** * Executes the command to change the object's tint. */ execute() { - changeTint(this.object, this.config); + changeTint(this.object, this.config, this.options); } /** * Undoes the command, reverting the object's tint to its previous state. */ undo() { - changeTint(this.object, this.prevConfig); + changeTint(this.object, this.prevConfig, this.options); } } diff --git a/src/display/change/asset.js b/src/display/change/asset.js index 9fac2b69..0b272b5d 100644 --- a/src/display/change/asset.js +++ b/src/display/change/asset.js @@ -1,12 +1,13 @@ import { getTexture } from '../../assets/textures/texture'; import { isConfigMatch, updateConfig } from './utils'; -export const changeAsset = (object, { asset: assetConfig }) => { +export const changeAsset = (object, { asset: assetConfig }, { theme }) => { if (isConfigMatch(object, 'asset', assetConfig)) { return; } - const asset = getTexture(assetConfig); + const renderer = getViewport(object).app.renderer; + const asset = getTexture(renderer, theme, assetConfig); if (!asset) { console.warn(`Asset not found for config: ${JSON.stringify(assetConfig)}`); } diff --git a/src/display/change/pipeline/component.js b/src/display/change/pipeline/component.js index 950a7638..b42b7ae4 100644 --- a/src/display/change/pipeline/component.js +++ b/src/display/change/pipeline/component.js @@ -11,8 +11,8 @@ export const componentPipeline = { }, texture: { keys: ['texture'], - handler: (component, config) => { - change.changeTexture(component, config); + handler: (component, config, options) => { + change.changeTexture(component, config, options); }, }, asset: { @@ -60,8 +60,8 @@ export const componentPipeline = { }, textStyle: { keys: ['style', 'margin'], - handler: (component, config) => { - change.changeTextStyle(component, config); + handler: (component, config, options) => { + change.changeTextStyle(component, config, options); change.changePlacement(component, config); // Ensure placement is updated after style change }, }, diff --git a/src/display/change/pipeline/utils.js b/src/display/change/pipeline/utils.js index 594a783a..6a860a03 100644 --- a/src/display/change/pipeline/utils.js +++ b/src/display/change/pipeline/utils.js @@ -1,11 +1,12 @@ export const createCommandHandler = (Command, changeFn) => { - return (object, config, undoRedoManager, options) => { + return (object, config, options) => { + const { undoRedoManager } = options; if (options?.historyId) { - undoRedoManager.execute(new Command(object, config), { + undoRedoManager.execute(new Command(object, config, options), { historyId: options.historyId, }); } else { - changeFn(object, config); + changeFn(object, config, options); } }; }; diff --git a/src/display/change/stroke-style.js b/src/display/change/stroke-style.js index 6a5d8df5..e3e72627 100644 --- a/src/display/change/stroke-style.js +++ b/src/display/change/stroke-style.js @@ -2,12 +2,16 @@ import { getColor } from '../../utils/get'; import { selector } from '../../utils/selector/selector'; import { updateConfig } from './utils'; -export const changeStrokeStyle = (object, { strokeStyle, links }) => { +export const changeStrokeStyle = ( + object, + { strokeStyle, links }, + { theme }, +) => { const path = selector(object, '$.children[?(@.type==="path")]')[0]; if (!path) return; if ('color' in strokeStyle) { - strokeStyle.color = getColor(strokeStyle.color); + strokeStyle.color = getColor(theme, strokeStyle.color); } path.setStrokeStyle({ ...path.strokeStyle, ...strokeStyle }); diff --git a/src/display/change/text-style.js b/src/display/change/text-style.js index 6498d8bb..f262d63a 100644 --- a/src/display/change/text-style.js +++ b/src/display/change/text-style.js @@ -6,6 +6,7 @@ import { isConfigMatch, updateConfig } from './utils'; export const changeTextStyle = ( object, { style = object.config.style, margin = object.config.margin }, + { theme }, ) => { if ( isConfigMatch(object, 'style', style) && @@ -18,7 +19,7 @@ export const changeTextStyle = ( if (key === 'fontFamily' || key === 'fontWeight') { object.style.fontFamily = `${style.fontFamily ?? object.style.fontFamily.split(' ')[0]} ${FONT_WEIGHT[style.fontWeight ?? object.style.fontWeight]}`; } else if (key === 'fill') { - object.style[key] = getColor(style.fill); + object.style[key] = getColor(theme, style.fill); } else if (key === 'fontSize' && style[key] === 'auto') { const marginObj = parseMargin(margin); setAutoFontSize(object, marginObj); diff --git a/src/display/change/texture.js b/src/display/change/texture.js index 9eccf427..485a57c4 100644 --- a/src/display/change/texture.js +++ b/src/display/change/texture.js @@ -3,7 +3,11 @@ import { deepMerge } from '../../utils/deepmerge/deepmerge'; import { getViewport } from '../../utils/get'; import { isConfigMatch, updateConfig } from './utils'; -export const changeTexture = (object, { texture: textureConfig }) => { +export const changeTexture = ( + object, + { texture: textureConfig }, + { theme }, +) => { if (isConfigMatch(object, 'texture', textureConfig)) { return; } @@ -11,6 +15,7 @@ export const changeTexture = (object, { texture: textureConfig }) => { const renderer = getViewport(object).app.renderer; const texture = getTexture( renderer, + theme, deepMerge(object.texture?.metadata?.config, textureConfig), ); object.texture = texture ?? null; diff --git a/src/display/change/tint.js b/src/display/change/tint.js index fbd00206..1cba08b4 100644 --- a/src/display/change/tint.js +++ b/src/display/change/tint.js @@ -1,8 +1,8 @@ import { getColor } from '../../utils/get'; import { updateConfig } from './utils'; -export const changeTint = (object, { color, tint }) => { - const hexColor = getColor(tint ?? color); +export const changeTint = (object, { color, tint }, { theme }) => { + const hexColor = getColor(theme, tint ?? color); object.tint = hexColor; updateConfig(object, { tint }); }; diff --git a/src/display/draw.js b/src/display/draw.js index 55fc3a07..a42fe351 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -6,7 +6,7 @@ import { createRelations } from './elements/relations'; import { update } from './update/update'; export const draw = (context, data) => { - const { viewport, undoRedoManager } = context; + const { viewport } = context; gsap.globalTimeline.clear(); destroyChildren(viewport); render(viewport, data); @@ -17,7 +17,7 @@ export const draw = (context, data) => { case 'group': { const element = createGroup(config); element.viewport = viewport; - update(null, undoRedoManager, { + update(context, { elements: element, changes: config, }); @@ -28,7 +28,7 @@ export const draw = (context, data) => { case 'grid': { const element = createGrid(config); element.viewport = viewport; - update(null, undoRedoManager, { + update(context, { elements: element, changes: config, }); @@ -38,7 +38,7 @@ export const draw = (context, data) => { case 'item': { const element = createItem(config); element.viewport = viewport; - update(null, undoRedoManager, { + update(context, { elements: element, changes: config, }); @@ -48,7 +48,7 @@ export const draw = (context, data) => { case 'relations': { const element = createRelations({ viewport, ...config }); element.viewport = viewport; - update(null, undoRedoManager, { + update(context, { elements: element, changes: config, }); diff --git a/src/display/elements/grid.js b/src/display/elements/grid.js index e762bd23..b9a0bcab 100644 --- a/src/display/elements/grid.js +++ b/src/display/elements/grid.js @@ -24,17 +24,10 @@ export const createGrid = (config) => { }; const pipelineKeys = ['show', 'position', 'gridComponents']; -export const updateGrid = (element, changes, undoRedoManager, options) => { +export const updateGrid = (element, changes, options) => { const validated = validate(changes, deepGridObject); if (isValidationError(validated)) throw validated; - updateObject( - element, - changes, - elementPipeline, - pipelineKeys, - undoRedoManager, - options, - ); + updateObject(element, changes, elementPipeline, pipelineKeys, options); }; const addItemElements = (container, cells, cellSize) => { diff --git a/src/display/elements/group.js b/src/display/elements/group.js index 00b63a69..bcb772db 100644 --- a/src/display/elements/group.js +++ b/src/display/elements/group.js @@ -11,15 +11,8 @@ export const createGroup = (config) => { }; const pipelineKeys = ['show', 'position']; -export const updateGroup = (element, changes, undoRedoManager, options) => { +export const updateGroup = (element, changes, options) => { const validated = validate(changes, deepGroupObject); if (isValidationError(validated)) throw validated; - updateObject( - element, - changes, - elementPipeline, - pipelineKeys, - undoRedoManager, - options, - ); + updateObject(element, changes, elementPipeline, pipelineKeys, options); }; diff --git a/src/display/elements/item.js b/src/display/elements/item.js index 71c25565..75d8bbd1 100644 --- a/src/display/elements/item.js +++ b/src/display/elements/item.js @@ -18,15 +18,8 @@ export const createItem = (config) => { }; const pipelineKeys = ['show', 'position', 'components']; -export const updateItem = (element, changes, undoRedoManager, options) => { +export const updateItem = (element, changes, options) => { const validated = validate(changes, deepSingleObject); if (isValidationError(validated)) throw validated; - updateObject( - element, - changes, - elementPipeline, - pipelineKeys, - undoRedoManager, - options, - ); + updateObject(element, changes, elementPipeline, pipelineKeys, options); }; diff --git a/src/display/elements/relations.js b/src/display/elements/relations.js index d536d6ed..5023af84 100644 --- a/src/display/elements/relations.js +++ b/src/display/elements/relations.js @@ -14,17 +14,10 @@ export const createRelations = (config) => { }; const pipelineKeys = ['show', 'strokeStyle', 'links']; -export const updateRelations = (element, changes, undoRedoManager, options) => { +export const updateRelations = (element, changes, options) => { const validated = validate(changes, deepRelationGroupObject); if (isValidationError(validated)) throw validated; - updateObject( - element, - changes, - elementPipeline, - pipelineKeys, - undoRedoManager, - options, - ); + updateObject(element, changes, elementPipeline, pipelineKeys, options); }; const createPath = () => { diff --git a/src/display/update/update-object.js b/src/display/update/update-object.js index 380bc1f0..40f3470a 100644 --- a/src/display/update/update-object.js +++ b/src/display/update/update-object.js @@ -7,7 +7,6 @@ export const updateObject = ( changes, pipeline, pipelineKeys, - undoRedoManager, options, ) => { if (!object) return; @@ -16,7 +15,7 @@ export const updateObject = ( for (const { keys, handler } of pipelines) { const hasMatch = keys.some((key) => key in changes); if (hasMatch) { - handler(object, changes, undoRedoManager, options); + handler(object, changes, options); } } diff --git a/src/display/update/update.js b/src/display/update/update.js index 3ff9255f..718fafd6 100644 --- a/src/display/update/update.js +++ b/src/display/update/update.js @@ -20,7 +20,7 @@ export const update = (context, opts) => { const config = validate(opts, updateSchema.passthrough()); if (isValidationError(config)) throw config; - const { viewport, undoRedoManager } = context; + const { viewport = null, undoRedoManager, theme } = context; const historyId = createHistoryId(config.saveToHistory); const elements = 'elements' in config ? convertArray(config.elements) : []; if (viewport && config.path) { @@ -36,23 +36,31 @@ export const update = (context, opts) => { switch (element.type) { case 'group': - updateGroup(element, elConfig.changes, undoRedoManager, { + updateGroup(element, elConfig.changes, { historyId, + theme, + undoRedoManager, }); break; case 'grid': - updateGrid(element, elConfig.changes, undoRedoManager, { + updateGrid(element, elConfig.changes, { historyId, + theme, + undoRedoManager, }); break; case 'item': - updateItem(element, elConfig.changes, undoRedoManager, { + updateItem(element, elConfig.changes, { historyId, + theme, + undoRedoManager, }); break; case 'relations': - updateRelations(element, elConfig.changes, undoRedoManager, { + updateRelations(element, elConfig.changes, { historyId, + theme, + undoRedoManager, }); break; } diff --git a/src/patchmap.js b/src/patchmap.js index 1a2df1c0..2f5f44c1 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -113,6 +113,7 @@ class Patchmap { const context = { viewport: this.viewport, undoRedoManager: this.undoRedoManager, + theme: this.theme, }; draw(context, validatedData); this.app.start(); @@ -142,6 +143,7 @@ class Patchmap { const context = { viewport: this.viewport, undoRedoManager: this.undoRedoManager, + theme: this.theme, }; update(context, opts); } From 1eb1e6dcddf2dec33eeb5189ce3aab0a08a4be4d Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 15:47:36 +0900 Subject: [PATCH 09/27] fix --- src/display/change/asset.js | 1 + src/display/change/pipeline/component.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/display/change/asset.js b/src/display/change/asset.js index 0b272b5d..3472a384 100644 --- a/src/display/change/asset.js +++ b/src/display/change/asset.js @@ -1,4 +1,5 @@ import { getTexture } from '../../assets/textures/texture'; +import { getViewport } from '../../utils/get'; import { isConfigMatch, updateConfig } from './utils'; export const changeAsset = (object, { asset: assetConfig }, { theme }) => { diff --git a/src/display/change/pipeline/component.js b/src/display/change/pipeline/component.js index b42b7ae4..fd0c75ea 100644 --- a/src/display/change/pipeline/component.js +++ b/src/display/change/pipeline/component.js @@ -17,8 +17,8 @@ export const componentPipeline = { }, asset: { keys: ['asset'], - handler: (component, config) => { - change.changeAsset(component, config); + handler: (component, config, options) => { + change.changeAsset(component, config, options); }, }, textureTransform: { From c0df91a746410064195596960543846b1db92662 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 16:21:55 +0900 Subject: [PATCH 10/27] refactor draw & update --- src/display/draw.js | 55 ++++++++++-------------------------- src/display/update/update.js | 43 +++++++++------------------- 2 files changed, 29 insertions(+), 69 deletions(-) diff --git a/src/display/draw.js b/src/display/draw.js index a42fe351..9fc8e5ee 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -5,6 +5,13 @@ import { createItem } from './elements/item'; import { createRelations } from './elements/relations'; import { update } from './update/update'; +const elementcreators = { + group: createGroup, + grid: createGrid, + item: createItem, + relations: createRelations, +}; + export const draw = (context, data) => { const { viewport } = context; gsap.globalTimeline.clear(); @@ -13,47 +20,15 @@ export const draw = (context, data) => { function render(parent, data) { for (const config of data) { - switch (config.type) { - case 'group': { - const element = createGroup(config); - element.viewport = viewport; - update(context, { - elements: element, - changes: config, - }); - parent.addChild(element); + const creator = elementcreators[config.type]; + if (creator) { + const element = creator(config); + element.viewport = viewport; + update(context, { elements: element, changes: config }); + parent.addChild(element); + + if (config.type === 'group') { render(element, config.items); - break; - } - case 'grid': { - const element = createGrid(config); - element.viewport = viewport; - update(context, { - elements: element, - changes: config, - }); - parent.addChild(element); - break; - } - case 'item': { - const element = createItem(config); - element.viewport = viewport; - update(context, { - elements: element, - changes: config, - }); - parent.addChild(element); - break; - } - case 'relations': { - const element = createRelations({ viewport, ...config }); - element.viewport = viewport; - update(context, { - elements: element, - changes: config, - }); - parent.addChild(element); - break; } } } diff --git a/src/display/update/update.js b/src/display/update/update.js index 718fafd6..e2344a0e 100644 --- a/src/display/update/update.js +++ b/src/display/update/update.js @@ -16,6 +16,13 @@ const updateSchema = z.object({ relativeTransform: z.boolean().default(false), }); +const elementUpdaters = { + group: updateGroup, + grid: updateGrid, + item: updateItem, + relations: updateRelations, +}; + export const update = (context, opts) => { const config = validate(opts, updateSchema.passthrough()); if (isValidationError(config)) throw config; @@ -34,35 +41,13 @@ export const update = (context, opts) => { elConfig.changes = applyRelativeTransform(element, elConfig.changes); } - switch (element.type) { - case 'group': - updateGroup(element, elConfig.changes, { - historyId, - theme, - undoRedoManager, - }); - break; - case 'grid': - updateGrid(element, elConfig.changes, { - historyId, - theme, - undoRedoManager, - }); - break; - case 'item': - updateItem(element, elConfig.changes, { - historyId, - theme, - undoRedoManager, - }); - break; - case 'relations': - updateRelations(element, elConfig.changes, { - historyId, - theme, - undoRedoManager, - }); - break; + const updater = elementUpdaters[element.type]; + if (updater) { + updater(element, elConfig.changes, { + historyId, + theme, + undoRedoManager, + }); } } }; From 3f63231164e80681b7a5ae52216a8d7ca73b20a4 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 16:29:41 +0900 Subject: [PATCH 11/27] fix gsap context --- src/display/change/percent-size.js | 15 +++++++++------ src/display/change/pipeline/component.js | 4 ++-- src/display/draw.js | 5 ++--- src/display/update/update.js | 8 ++------ src/patchmap.js | 9 ++++++++- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/display/change/percent-size.js b/src/display/change/percent-size.js index e3a823af..6db1cbc5 100644 --- a/src/display/change/percent-size.js +++ b/src/display/change/percent-size.js @@ -11,6 +11,7 @@ export const changePercentSize = ( margin = object.config.margin, animationDuration = object.config.animationDuration, }, + { animationContext }, ) => { if ( isConfigMatch(object, 'percentWidth', percentWidth) && @@ -41,12 +42,14 @@ export const changePercentSize = ( component.parent.size.height - (marginObj.top + marginObj.bottom); if (object.config.animation) { - killTweensOf(component); - gsap.to(component, { - pixi: { height: maxHeight * percentHeight }, - duration: animationDuration / 1000, - ease: 'power2.inOut', - onUpdate: () => changePlacement(component, {}), + animationContext.add(() => { + killTweensOf(component); + gsap.to(component, { + pixi: { height: maxHeight * percentHeight }, + duration: animationDuration / 1000, + ease: 'power2.inOut', + onUpdate: () => changePlacement(component, {}), + }); }); } else { component.height = maxHeight * percentHeight; diff --git a/src/display/change/pipeline/component.js b/src/display/change/pipeline/component.js index fd0c75ea..f9d86159 100644 --- a/src/display/change/pipeline/component.js +++ b/src/display/change/pipeline/component.js @@ -35,8 +35,8 @@ export const componentPipeline = { }, percentSize: { keys: ['percentWidth', 'percentHeight', 'margin'], - handler: (component, config) => { - change.changePercentSize(component, config); + handler: (component, config, options) => { + change.changePercentSize(component, config, options); change.changePlacement(component, {}); }, }, diff --git a/src/display/draw.js b/src/display/draw.js index 9fc8e5ee..bf486548 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -1,4 +1,3 @@ -import gsap from 'gsap'; import { createGrid } from './elements/grid'; import { createGroup } from './elements/group'; import { createItem } from './elements/item'; @@ -13,8 +12,8 @@ const elementcreators = { }; export const draw = (context, data) => { - const { viewport } = context; - gsap.globalTimeline.clear(); + const { viewport, animationContext } = context; + animationContext.revert(); destroyChildren(viewport); render(viewport, data); diff --git a/src/display/update/update.js b/src/display/update/update.js index e2344a0e..c8e93f85 100644 --- a/src/display/update/update.js +++ b/src/display/update/update.js @@ -27,7 +27,7 @@ export const update = (context, opts) => { const config = validate(opts, updateSchema.passthrough()); if (isValidationError(config)) throw config; - const { viewport = null, undoRedoManager, theme } = context; + const { viewport = null, ...otherContext } = context; const historyId = createHistoryId(config.saveToHistory); const elements = 'elements' in config ? convertArray(config.elements) : []; if (viewport && config.path) { @@ -43,11 +43,7 @@ export const update = (context, opts) => { const updater = elementUpdaters[element.type]; if (updater) { - updater(element, elConfig.changes, { - historyId, - theme, - undoRedoManager, - }); + updater(element, elConfig.changes, { historyId, ...otherContext }); } } }; diff --git a/src/patchmap.js b/src/patchmap.js index 2f5f44c1..fcd6331a 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -28,6 +28,7 @@ class Patchmap { this._isInit = false; this._theme = themeStore(); this._undoRedoManager = new UndoRedoManager(); + this._animationContext = gsap.context(() => {}); } get app() { @@ -50,6 +51,10 @@ class Patchmap { return this._undoRedoManager; } + get animationContext() { + return this._animationContext; + } + get event() { return { add: (opts) => { @@ -88,7 +93,7 @@ class Patchmap { } destroy() { - gsap.globalTimeline.clear(); + this.animationContext.revert(); Assets.reset(); const parentElement = this.app.canvas.parentElement; this.viewport.destroy(true); @@ -114,6 +119,7 @@ class Patchmap { viewport: this.viewport, undoRedoManager: this.undoRedoManager, theme: this.theme, + animationContext: this.animationContext, }; draw(context, validatedData); this.app.start(); @@ -144,6 +150,7 @@ class Patchmap { viewport: this.viewport, undoRedoManager: this.undoRedoManager, theme: this.theme, + animationContext: this.animationContext, }; update(context, opts); } From ed3ca4afb1c2e833ed549dd3596aa67936558f81 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 16:59:53 +0900 Subject: [PATCH 12/27] fix percentHeight condition --- src/display/change/percent-size.js | 8 ++++++-- src/display/update/update-components.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/display/change/percent-size.js b/src/display/change/percent-size.js index 6db1cbc5..23b4cc99 100644 --- a/src/display/change/percent-size.js +++ b/src/display/change/percent-size.js @@ -22,8 +22,12 @@ export const changePercentSize = ( } const marginObj = parseMargin(margin); - if (percentWidth) changeWidth(object, percentWidth, marginObj); - if (percentHeight) changeHeight(object, percentHeight, marginObj); + if (!Number.isNaN(percentWidth)) { + changeWidth(object, percentWidth, marginObj); + } + if (!Number.isNaN(percentHeight)) { + changeHeight(object, percentHeight, marginObj); + } updateConfig(object, { percentWidth, percentHeight, diff --git a/src/display/update/update-components.js b/src/display/update/update-components.js index 3dac1c87..7c7f0fce 100644 --- a/src/display/update/update-components.js +++ b/src/display/update/update-components.js @@ -53,7 +53,7 @@ export const updateComponents = ( item.addChild(component); } - componentFn[component.type].update(component, { ...config }, options); + componentFn[component.type].update(component, config, options); } }; From 9ccc148a93eaba452200e437694b6a73e67253c0 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 17:03:27 +0900 Subject: [PATCH 13/27] fix --- src/init.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/init.js b/src/init.js index ffb21e98..1ded0813 100644 --- a/src/init.js +++ b/src/init.js @@ -84,8 +84,7 @@ export const initAsset = async (opts = {}) => { await PIXI.Assets.init({ manifest }); } - const fontsBundle = await PIXI.Assets.loadBundle(['fonts']); - if (!('fonts' in fontsBundle)) { + if (!PIXI.Assets.resolver.hasBundle('fonts')) { const fontBundle = Object.entries(firaCode).map(([key, font]) => ({ alias: `firaCode-${key}`, src: font, From ad6a95be37e3ced32480e659feb3b96bc15b236c Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 11 Jun 2025 19:15:20 +0900 Subject: [PATCH 14/27] fix select event --- src/events/drag-select.js | 102 +++++++++++++++++------------------- src/events/single-select.js | 32 +++++------ src/patchmap.js | 18 +++++-- 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/src/events/drag-select.js b/src/events/drag-select.js index 916a86de..912d068a 100644 --- a/src/events/drag-select.js +++ b/src/events/drag-select.js @@ -1,4 +1,3 @@ -import { Graphics } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; import { getPointerPosition } from '../utils/canvas'; import { deepMerge } from '../utils/deepmerge/deepmerge'; @@ -11,33 +10,27 @@ import { checkEvents, isMoved } from './utils'; const DRAG_SELECT_EVENT_ID = 'drag-select-down drag-select-move drag-select-up'; const DEBOUNCE_FN_INTERVAL = 25; // ms -let config = {}; -let lastMoveTime = 0; -const state = { - isDragging: false, - startPoint: null, - endPoint: null, - movePoint: null, - box: new Graphics(), -}; - -export const dragSelect = (viewport, opts) => { - const options = validate(deepMerge(config, opts), dragSelectEventSchema); +export const dragSelect = (viewport, state, opts) => { + const options = validate( + deepMerge(state.config, opts), + dragSelectEventSchema, + ); if (isValidationError(options)) throw options; if (!checkEvents(viewport, DRAG_SELECT_EVENT_ID)) { - addEvents(viewport); + addEvents(viewport, state); } changeDraggableState( viewport, - config.enabled && config.draggable, + state, + state.config.enabled && state.config.draggable, options.enabled && options.draggable, ); - config = options; + state.config = options; }; -const addEvents = (viewport) => { +const addEvents = (viewport, state) => { event.removeEvent(viewport, DRAG_SELECT_EVENT_ID); registerDownEvent(); registerMoveEvent(); @@ -48,13 +41,13 @@ const addEvents = (viewport) => { id: 'drag-select-down', action: 'mousedown touchstart', fn: () => { - resetState(); + resetState(state); const point = getPointerPosition(viewport); state.isDragging = true; state.box.renderable = true; - state.startPoint = { ...point }; - state.movePoint = { ...point }; + state.point.start = { ...point }; + state.point.move = { ...point }; }, }); } @@ -66,13 +59,13 @@ const addEvents = (viewport) => { fn: (e) => { if (!state.isDragging) return; - state.endPoint = { ...getPointerPosition(viewport) }; - drawSelectionBox(); + state.point.end = { ...getPointerPosition(viewport) }; + drawSelectionBox(state); - if (isMoved(viewport, state.movePoint, state.endPoint)) { + if (isMoved(viewport, state.point.move, state.point.end)) { viewport.plugin.start('mouse-edges'); - triggerFn(viewport, e); - state.movePoint = JSON.parse(JSON.stringify(state.endPoint)); + triggerFn(viewport, e, state); + state.point.move = JSON.parse(JSON.stringify(state.point.end)); } }, }); @@ -84,56 +77,59 @@ const addEvents = (viewport) => { action: 'mouseup touchend mouseleave', fn: (e) => { if ( - state.startPoint && - state.endPoint && - isMoved(viewport, state.startPoint, state.endPoint) + state.point.start && + state.point.end && + isMoved(viewport, state.point.start, state.point.end) ) { - triggerFn(viewport, e); + triggerFn(viewport, e, state); viewport.plugin.stop('mouse-edges'); } - resetState(); + resetState(state); }, }); } }; -const drawSelectionBox = () => { - const { box, startPoint, endPoint } = state; - if (!startPoint || !endPoint) return; +const drawSelectionBox = (state) => { + const { box, point } = state; + if (!point.start || !point.end) return; box.clear(); box.position.set( - Math.min(startPoint.x, endPoint.x), - Math.min(startPoint.y, endPoint.y), + Math.min(point.start.x, point.end.x), + Math.min(point.start.y, point.end.y), ); box .rect( 0, 0, - Math.abs(startPoint.x - endPoint.x), - Math.abs(startPoint.y - endPoint.y), + Math.abs(point.start.x - point.end.x), + Math.abs(point.start.y - point.end.y), ) .fill({ color: '#9FD6FF', alpha: 0.2 }) .stroke({ width: 2, color: '#1099FF', pixelLine: true }); }; -const triggerFn = (viewport, e) => { +const triggerFn = (viewport, e, state) => { const now = performance.now(); - if (e.type === 'pointermove' && now - lastMoveTime < DEBOUNCE_FN_INTERVAL) { + if ( + e.type === 'pointermove' && + now - state.lastMoveTime < DEBOUNCE_FN_INTERVAL + ) { return; } - lastMoveTime = now; + state.lastMoveTime = now; const intersectObjs = - state.startPoint && state.endPoint - ? findIntersectObjects(viewport, state, config) + state.point.start && state.point.end + ? findIntersectObjects(viewport, state, state.config) : []; - if ('onDragSelect' in config) { - config.onDragSelect(intersectObjs, e); + if ('onDragSelect' in state.config) { + state.config.onDragSelect(intersectObjs, e); } }; -const changeDraggableState = (viewport, wasDraggable, isDraggable) => { +const changeDraggableState = (viewport, state, wasDraggable, isDraggable) => { if (wasDraggable === isDraggable) return; if (isDraggable) { @@ -142,31 +138,29 @@ const changeDraggableState = (viewport, wasDraggable, isDraggable) => { }); viewport.plugin.stop('mouse-edges'); event.onEvent(viewport, DRAG_SELECT_EVENT_ID); - addChildBox(viewport); + addChildBox(viewport, state); } else { viewport.plugin.remove('mouse-edges'); event.offEvent(viewport, DRAG_SELECT_EVENT_ID); - resetState(); - removeChildBox(viewport); + resetState(state); + removeChildBox(viewport, state); } }; -const resetState = () => { +const resetState = (state) => { state.isDragging = false; - state.startPoint = null; - state.endPoint = null; - state.movePoint = null; + state.point = { start: null, end: null, move: null }; state.box.clear(); state.box.renderable = false; }; -const addChildBox = (viewport) => { +const addChildBox = (viewport, state) => { if (!state.box.parent) { viewport.addChild(state.box); } }; -const removeChildBox = (viewport) => { +const removeChildBox = (viewport, state) => { if (state.box.parent) { viewport.removeChild(state.box); } diff --git a/src/events/single-select.js b/src/events/single-select.js index 3c758229..8871dca4 100644 --- a/src/events/single-select.js +++ b/src/events/single-select.js @@ -9,22 +9,19 @@ import { checkEvents, isMoved } from './utils'; const SELECT_EVENT_ID = 'select-down select-up select-over'; -let config = {}; -let state = { startPosition: null, endPosition: null }; - -export const select = (viewport, opts) => { - const options = validate(deepMerge(config, opts), selectEventSchema); +export const select = (viewport, state, opts) => { + const options = validate(deepMerge(state.config, opts), selectEventSchema); if (isValidationError(options)) throw options; if (!checkEvents(viewport, SELECT_EVENT_ID)) { - addEvents(viewport); + addEvents(viewport, state); } - changeEnableState(viewport, config.enabled, options.enabled); - config = options; + changeEnableState(viewport, state.config.enabled, options.enabled); + state.config = options; }; -const addEvents = (viewport) => { +const addEvents = (viewport, state) => { event.removeEvent(viewport, SELECT_EVENT_ID); registerDownEvent(); registerUpEvent(); @@ -35,7 +32,7 @@ const addEvents = (viewport) => { id: 'select-down', action: 'mousedown touchstart', fn: () => { - state.startPosition = { + state.position.start = { x: viewport.position.x, y: viewport.position.y, }; @@ -48,19 +45,19 @@ const addEvents = (viewport) => { id: 'select-up', action: 'mouseup touchend', fn: (e) => { - state.endPosition = { + state.position.end = { x: viewport.position.x, y: viewport.position.y, }; if ( - state.startPosition && - !isMoved(viewport, state.startPosition, state.endPosition) + state.position.start && + !isMoved(viewport, state.position.start, state.position.end) ) { executeFn('onSelect', e); } - state = { startPosition: null, endPosition: null }; + state.position = { start: null, end: null }; executeFn('onOver', e); }, }); @@ -78,8 +75,11 @@ const addEvents = (viewport) => { function executeFn(fnName, e) { const point = getPointerPosition(viewport); - if (fnName in config) { - config[fnName](findIntersectObject(viewport, { point }, config), e); + if (fnName in state.config) { + state.config[fnName]( + findIntersectObject(viewport, { point }, state.config), + e, + ); } } }; diff --git a/src/patchmap.js b/src/patchmap.js index fcd6331a..c8706825 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { Application, Assets } from 'pixi.js'; +import { Application, Assets, Graphics } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; import { UndoRedoManager } from './command/undo-redo-manager'; import { draw } from './display/draw'; @@ -29,6 +29,18 @@ class Patchmap { this._theme = themeStore(); this._undoRedoManager = new UndoRedoManager(); this._animationContext = gsap.context(() => {}); + + this._singleSelectState = { + config: {}, + position: { start: null, end: null }, + }; + this._dragSelectState = { + config: {}, + lastMoveTime: 0, + isDragging: false, + point: { start: null, end: null, move: null }, + box: new Graphics(), + }; } get app() { @@ -168,8 +180,8 @@ class Patchmap { } select(opts) { - select(this.viewport, opts); - dragSelect(this.viewport, opts); + select(this.viewport, this._singleSelectState, opts); + dragSelect(this.viewport, this._dragSelectState, opts); } } From 5e8f19491adf6b57e18d9acff6a5012734687d99 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 10:52:03 +0900 Subject: [PATCH 15/27] fix --- src/command/commands/tint.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/command/commands/tint.js b/src/command/commands/tint.js index 5c913bd7..14beb7dd 100644 --- a/src/command/commands/tint.js +++ b/src/command/commands/tint.js @@ -17,6 +17,7 @@ export class TintCommand extends Command { * Creates an instance of TintCommand. * @param {Object} object - The Pixi.js display object whose tint will be changed. * @param {Object} config - The new configuration for the object's tint. + * @param {object} options - Options for command execution. */ constructor(object, config, options) { super('tint_object'); From fa350ff99de42084a94071409090df6cbd10a5e6 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 15:59:52 +0900 Subject: [PATCH 16/27] fix deepmerge --- src/utils/deepmerge/deepmerge.js | 3 ++- src/utils/findIndexByPriority.js | 36 +++++++++++++++++++------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/utils/deepmerge/deepmerge.js b/src/utils/deepmerge/deepmerge.js index 4ebb6778..5019c40f 100644 --- a/src/utils/deepmerge/deepmerge.js +++ b/src/utils/deepmerge/deepmerge.js @@ -56,12 +56,13 @@ const _deepMerge = (target, source, options, visited) => { }; const mergeArray = (target, source, options, visited) => { + const { mergeBy } = options; const merged = [...target]; const used = new Set(); source.forEach((item, i) => { if (item && typeof item === 'object' && !Array.isArray(item)) { - const idx = findIndexByPriority(merged, item, used); + const idx = findIndexByPriority(merged, item, used, mergeBy); if (idx !== -1) { merged[idx] = _deepMerge(merged[idx], item, options, visited); used.add(idx); diff --git a/src/utils/findIndexByPriority.js b/src/utils/findIndexByPriority.js index 4b605766..02525e86 100644 --- a/src/utils/findIndexByPriority.js +++ b/src/utils/findIndexByPriority.js @@ -5,26 +5,32 @@ const schema = z.object({ criteria: z.object({}).passthrough(), }); -export const findIndexByPriority = (arr, criteria, usedIndexes = new Set()) => { +/** + * Finds the index of the first item in an array that matches the criteria, based on a priority list of keys. + * @param {Array} arr - The array to search. + * @param {Object} criteria - The criteria object to match. + * @param {Array} [priorityKeys=['id', 'label', 'type']] - The list of keys to check, in order of priority. + * @param {Set} [usedIndexes=new Set()] - A set of indices to exclude from the search. + * @returns {number} - The index of the first matching item, or -1 if no match is found. + */ +export const findIndexByPriority = ( + arr, + criteria, + usedIndexes = new Set(), + priorityKeys = ['id', 'label', 'type'], +) => { const validation = schema.safeParse({ arr, criteria }); if (!validation.success) { throw new TypeError(validation.error.message); } - if ('id' in criteria) { - return arr.findIndex( - (item, idx) => !usedIndexes.has(idx) && item?.id === criteria.id, - ); - } - if ('label' in criteria) { - return arr.findIndex( - (item, idx) => !usedIndexes.has(idx) && item?.label === criteria.label, - ); - } - if ('type' in criteria) { - return arr.findIndex( - (item, idx) => !usedIndexes.has(idx) && item?.type === criteria.type, - ); + for (const key of priorityKeys) { + if (key in criteria) { + return arr.findIndex( + (item, idx) => !usedIndexes.has(idx) && item[key] === criteria[key], + ); + } } + return -1; }; From 538805cb6f96343c19d8738a862c474c54fa57d9 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 16:00:02 +0900 Subject: [PATCH 17/27] delete Assets.reset --- src/patchmap.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/patchmap.js b/src/patchmap.js index c8706825..73e787a2 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { Application, Assets, Graphics } from 'pixi.js'; +import { Application, Graphics } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; import { UndoRedoManager } from './command/undo-redo-manager'; import { draw } from './display/draw'; @@ -89,7 +89,7 @@ class Patchmap { app: appOptions = {}, viewport: viewportOptions = {}, theme: themeOptions = {}, - asset: assetOptions = {}, + assets: assetOptions = [], } = opts; this.undoRedoManager._setHotkeys(); @@ -106,7 +106,6 @@ class Patchmap { destroy() { this.animationContext.revert(); - Assets.reset(); const parentElement = this.app.canvas.parentElement; this.viewport.destroy(true); this.app.destroy(true); From bd57c8bbee35e0f6ecb86c6ccf14ce231639e03b Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 16:10:39 +0900 Subject: [PATCH 18/27] fix destroy --- src/patchmap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patchmap.js b/src/patchmap.js index 73e787a2..c3a6d590 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -107,7 +107,7 @@ class Patchmap { destroy() { this.animationContext.revert(); const parentElement = this.app.canvas.parentElement; - this.viewport.destroy(true); + this.viewport.destroy({ children: true, context: true, style: true }); this.app.destroy(true); parentElement.remove(); if (this._resizeObserver) this._resizeObserver.disconnect(); From 480525fc8e82f241fa1f0b8afc39c64bc841363e Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 16:10:56 +0900 Subject: [PATCH 19/27] fix init asset --- src/init.js | 69 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/init.js b/src/init.js index 1ded0813..b5e82b9f 100644 --- a/src/init.js +++ b/src/init.js @@ -4,7 +4,6 @@ import { Viewport } from 'pixi-viewport'; import * as PIXI from 'pixi.js'; import { firaCode } from './assets/fonts'; import { icons } from './assets/icons'; -import { transformManifest } from './assets/utils'; import { deepMerge } from './utils/deepmerge/deepmerge'; import { plugin } from './utils/event/viewport'; import { uid } from './utils/uuid'; @@ -31,18 +30,24 @@ const DEFAULT_INIT_OPTIONS = { decelerate: {}, }, }, - assets: { - icons: { - object: { src: icons.object }, - inverter: { src: icons.inverter }, - combiner: { src: icons.combiner }, - edge: { src: icons.edge }, - device: { src: icons.device }, - loading: { src: icons.loading }, - warning: { src: icons.warning }, - wifi: { src: icons.wifi }, + assets: [ + { + name: 'icons', + items: Object.entries(icons).map(([alias, src]) => ({ + alias, + src, + data: { resolution: 3 }, + })), }, - }, + { + name: 'fonts', + items: Object.entries(firaCode).map(([key, font]) => ({ + alias: `firaCode-${key}`, + src: font, + data: { family: `FiraCode ${key}` }, + })), + }, + ], }; export const initApp = async (app, opts = {}) => { @@ -77,23 +82,33 @@ export const initViewport = (app, opts = {}) => { }; export const initAsset = async (opts = {}) => { - const options = deepMerge(DEFAULT_INIT_OPTIONS.assets, opts); - - if (!PIXI.Assets._initialized) { - const manifest = transformManifest(options); - await PIXI.Assets.init({ manifest }); - } + const assets = deepMerge(DEFAULT_INIT_OPTIONS.assets, opts, { + mergeBy: ['name', 'alias'], + }); - if (!PIXI.Assets.resolver.hasBundle('fonts')) { - const fontBundle = Object.entries(firaCode).map(([key, font]) => ({ - alias: `firaCode-${key}`, - src: font, - data: { family: `FiraCode ${key}` }, - })); - PIXI.Assets.addBundle('fonts', fontBundle); + const bundlesToLoad = []; + const assetsToLoad = []; + for (const asset of assets) { + if (asset.name && Array.isArray(asset.items)) { + if (!PIXI.Assets.resolver.hasBundle(asset.name)) { + PIXI.Assets.addBundle(asset.name, asset.items); + bundlesToLoad.push(asset.name); + } + } else if (asset.alias && asset.src) { + if (!PIXI.Assets.cache.has(asset.alias)) { + PIXI.Assets.add(asset); + assetsToLoad.push(asset.alias); + } + } } - - await PIXI.Assets.loadBundle([...Object.keys(options), 'fonts']); + await Promise.all([ + bundlesToLoad.length > 0 + ? PIXI.Assets.loadBundle(bundlesToLoad) + : Promise.resolve(), + assetsToLoad.length > 0 + ? PIXI.Assets.load(assetsToLoad) + : Promise.resolve(), + ]); }; export const initResizeObserver = (el, app, viewport) => { From bc887603e3f4e559f26ec8e0c093bab715870da0 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 16:24:03 +0900 Subject: [PATCH 20/27] init select event --- src/display/draw.js | 2 +- src/patchmap.js | 33 +++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/display/draw.js b/src/display/draw.js index bf486548..2d1af237 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -37,6 +37,6 @@ export const draw = (context, data) => { const destroyChildren = (parent) => { const children = [...parent.children]; for (const child of children) { - child.destroy({ children: true }); + child.destroy({ children: true, context: true, style: true }); } }; diff --git a/src/patchmap.js b/src/patchmap.js index c3a6d590..fcf0410c 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -30,17 +30,8 @@ class Patchmap { this._undoRedoManager = new UndoRedoManager(); this._animationContext = gsap.context(() => {}); - this._singleSelectState = { - config: {}, - position: { start: null, end: null }, - }; - this._dragSelectState = { - config: {}, - lastMoveTime: 0, - isDragging: false, - point: { start: null, end: null, move: null }, - box: new Graphics(), - }; + this._singleSelectState = null; + this._dragSelectState = null; } get app() { @@ -126,6 +117,10 @@ class Patchmap { if (isValidationError(validatedData)) throw validatedData; this.app.stop(); + Object.keys(event.getAllEvent(this.viewport)).forEach((id) => + event.removeEvent(this.viewport, id), + ); + this.initSelectState(); const context = { viewport: this.viewport, undoRedoManager: this.undoRedoManager, @@ -133,8 +128,8 @@ class Patchmap { animationContext: this.animationContext, }; draw(context, validatedData); - this.app.start(); this.undoRedoManager.clear(); + this.app.start(); return validatedData; function preprocessData(data) { @@ -182,6 +177,20 @@ class Patchmap { select(this.viewport, this._singleSelectState, opts); dragSelect(this.viewport, this._dragSelectState, opts); } + + initSelectState() { + this._singleSelectState = { + config: {}, + position: { start: null, end: null }, + }; + this._dragSelectState = { + config: {}, + lastMoveTime: 0, + isDragging: false, + point: { start: null, end: null, move: null }, + box: new Graphics(), + }; + } } export { Patchmap }; From 9a3d8a3e984273ce7c5fd5632396b7fa1277ee54 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 16:26:08 +0900 Subject: [PATCH 21/27] fix --- src/display/draw.js | 3 +-- src/patchmap.js | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/display/draw.js b/src/display/draw.js index 2d1af237..6110d85b 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -12,8 +12,7 @@ const elementcreators = { }; export const draw = (context, data) => { - const { viewport, animationContext } = context; - animationContext.revert(); + const { viewport } = context; destroyChildren(viewport); render(viewport, data); diff --git a/src/patchmap.js b/src/patchmap.js index fcf0410c..55bac470 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -117,18 +117,18 @@ class Patchmap { if (isValidationError(validatedData)) throw validatedData; this.app.stop(); - Object.keys(event.getAllEvent(this.viewport)).forEach((id) => - event.removeEvent(this.viewport, id), - ); + this.undoRedoManager.clear(); + this.animationContext.revert(); + Object.keys(event.getAllEvent(this.viewport)).forEach((id) => { + event.removeEvent(this.viewport, id); + }); this.initSelectState(); const context = { viewport: this.viewport, undoRedoManager: this.undoRedoManager, theme: this.theme, - animationContext: this.animationContext, }; draw(context, validatedData); - this.undoRedoManager.clear(); this.app.start(); return validatedData; From aa1cdb6e146bbbc6c1b00e2b852dc6949b769991 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 16:29:06 +0900 Subject: [PATCH 22/27] fix --- src/display/change/percent-size.js | 4 ++-- src/utils/canvas.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/display/change/percent-size.js b/src/display/change/percent-size.js index 23b4cc99..d1f1e3b4 100644 --- a/src/display/change/percent-size.js +++ b/src/display/change/percent-size.js @@ -22,10 +22,10 @@ export const changePercentSize = ( } const marginObj = parseMargin(margin); - if (!Number.isNaN(percentWidth)) { + if (!Number.isFinite(percentWidth)) { changeWidth(object, percentWidth, marginObj); } - if (!Number.isNaN(percentHeight)) { + if (!Number.isFinite(percentHeight)) { changeHeight(object, percentHeight, marginObj); } updateConfig(object, { diff --git a/src/utils/canvas.js b/src/utils/canvas.js index 27672ea6..bb1a921c 100644 --- a/src/utils/canvas.js +++ b/src/utils/canvas.js @@ -9,7 +9,7 @@ export const getScaleBounds = (viewport, object) => { }; export const getPointerPosition = (viewport) => { - const renderer = viewport.app.renderer; - const global = renderer.events.pointer.global; + const renderer = viewport?.app?.renderer; + const global = renderer?.events.pointer.global; return viewport ? viewport.toWorld(global.x, global.y) : global; }; From 38888ff7159aec54ceefac00d39358d2d1830e8b Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 17:02:07 +0900 Subject: [PATCH 23/27] fix details --- src/display/change/percent-size.js | 4 ++-- src/display/draw.js | 2 +- src/patchmap.js | 6 +++--- src/utils/event/canvas.js | 5 +++++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/display/change/percent-size.js b/src/display/change/percent-size.js index d1f1e3b4..3a6e401e 100644 --- a/src/display/change/percent-size.js +++ b/src/display/change/percent-size.js @@ -22,10 +22,10 @@ export const changePercentSize = ( } const marginObj = parseMargin(margin); - if (!Number.isFinite(percentWidth)) { + if (Number.isFinite(percentWidth)) { changeWidth(object, percentWidth, marginObj); } - if (!Number.isFinite(percentHeight)) { + if (Number.isFinite(percentHeight)) { changeHeight(object, percentHeight, marginObj); } updateConfig(object, { diff --git a/src/display/draw.js b/src/display/draw.js index 6110d85b..599a58c5 100644 --- a/src/display/draw.js +++ b/src/display/draw.js @@ -36,6 +36,6 @@ export const draw = (context, data) => { const destroyChildren = (parent) => { const children = [...parent.children]; for (const child of children) { - child.destroy({ children: true, context: true, style: true }); + child.destroy({ children: true }); } }; diff --git a/src/patchmap.js b/src/patchmap.js index 55bac470..69260638 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -98,6 +98,7 @@ class Patchmap { destroy() { this.animationContext.revert(); const parentElement = this.app.canvas.parentElement; + event.removeAllEvent(this.viewport); this.viewport.destroy({ children: true, context: true, style: true }); this.app.destroy(true); parentElement.remove(); @@ -119,14 +120,13 @@ class Patchmap { this.app.stop(); this.undoRedoManager.clear(); this.animationContext.revert(); - Object.keys(event.getAllEvent(this.viewport)).forEach((id) => { - event.removeEvent(this.viewport, id); - }); + event.removeAllEvent(this.viewport); this.initSelectState(); const context = { viewport: this.viewport, undoRedoManager: this.undoRedoManager, theme: this.theme, + animationContext: this.animationContext, }; draw(context, validatedData); this.app.start(); diff --git a/src/utils/event/canvas.js b/src/utils/event/canvas.js index 62e5be1a..d78193ec 100644 --- a/src/utils/event/canvas.js +++ b/src/utils/event/canvas.js @@ -41,6 +41,10 @@ export const removeEvent = (viewport, id) => { } }; +export const removeAllEvent = (viewport) => { + removeEvent(viewport, Object.keys(getAllEvent(viewport)).join(' ')); +}; + export const onEvent = (viewport, id) => { const eventIds = splitByWhitespace(id); if (!eventIds.length) return; @@ -101,6 +105,7 @@ export const getAllEvent = (viewport) => viewport.events; export const event = { addEvent, removeEvent, + removeAllEvent, onEvent, offEvent, getEvent, From ccdd7cc540ede16fa70358952f013d54ab67aa86 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 17:04:09 +0900 Subject: [PATCH 24/27] add undoredoManager destroy method --- src/command/undo-redo-manager.js | 49 +++++++++++++++++++------------- src/patchmap.js | 1 + 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/command/undo-redo-manager.js b/src/command/undo-redo-manager.js index 0425090c..50f12a22 100644 --- a/src/command/undo-redo-manager.js +++ b/src/command/undo-redo-manager.js @@ -7,6 +7,7 @@ export class UndoRedoManager { this._index = -1; this._listeners = new Set(); this._maxCommands = maxCommands; + this._hotkeyListener = null; } /** @@ -133,26 +134,36 @@ export class UndoRedoManager { * @private */ _setHotkeys() { - document.addEventListener( - 'keydown', - (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._hotkeyListener = (e) => { + const key = (e.key || '').toLowerCase(); + if (isInput(e.target)) return; + + if (key === 'z' && (e.ctrlKey || e.metaKey)) { + if (e.shiftKey) { this.redo(); - e.preventDefault(); + } else { + this.undo(); } - }, - false, - ); + 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/patchmap.js b/src/patchmap.js index 69260638..6ad3c912 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -96,6 +96,7 @@ class Patchmap { } destroy() { + this.undoRedoManager.destroy(); this.animationContext.revert(); const parentElement = this.app.canvas.parentElement; event.removeAllEvent(this.viewport); From d29d975a57ee41404f784bd5f59ee377cacc1051 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 17:08:53 +0900 Subject: [PATCH 25/27] fix --- src/patchmap.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/patchmap.js b/src/patchmap.js index 6ad3c912..464f6ed9 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -98,9 +98,9 @@ class Patchmap { destroy() { this.undoRedoManager.destroy(); this.animationContext.revert(); - const parentElement = this.app.canvas.parentElement; event.removeAllEvent(this.viewport); this.viewport.destroy({ children: true, context: true, style: true }); + const parentElement = this.app.canvas.parentElement; this.app.destroy(true); parentElement.remove(); if (this._resizeObserver) this._resizeObserver.disconnect(); @@ -109,6 +109,11 @@ class Patchmap { this._viewport = null; this._resizeObserver = null; this._isInit = false; + this._theme = themeStore(); + this._undoRedoManager = new UndoRedoManager(); + this._animationContext = gsap.context(() => {}); + this._singleSelectState = null; + this._dragSelectState = null; } draw(data) { From 79a5b15503c063ff9ed22ed76afb9b469eb70266 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 17:16:55 +0900 Subject: [PATCH 26/27] fix naming --- src/patchmap.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/patchmap.js b/src/patchmap.js index 464f6ed9..a2156ca6 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -80,7 +80,7 @@ class Patchmap { app: appOptions = {}, viewport: viewportOptions = {}, theme: themeOptions = {}, - assets: assetOptions = [], + assets: assetsOptions = [], } = opts; this.undoRedoManager._setHotkeys(); @@ -88,7 +88,7 @@ class Patchmap { this._app = new Application(); await initApp(this.app, { resizeTo: element, ...appOptions }); this._viewport = initViewport(this.app, viewportOptions); - await initAsset(assetOptions); + await initAsset(assetsOptions); initCanvas(element, this.app); this._resizeObserver = initResizeObserver(element, this.app, this.viewport); From fc0fea2897ce158e07e5b92dca88afa033a9d2de Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 12 Jun 2025 17:22:41 +0900 Subject: [PATCH 27/27] fix --- src/patchmap.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/patchmap.js b/src/patchmap.js index a2156ca6..ef65011b 100644 --- a/src/patchmap.js +++ b/src/patchmap.js @@ -66,6 +66,7 @@ class Patchmap { return id; }, remove: (id) => event.removeEvent(this.viewport, id), + removeAll: () => event.removeAllEvent(this.viewport), on: (id) => event.onEvent(this.viewport, id), off: (id) => event.offEvent(this.viewport, id), get: (id) => event.getEvent(this.viewport, id),