diff --git a/src/extensions/core/extensions/cloudBadges/comfy.ext.config.ts b/src/extensions/core/extensions/cloudBadges/comfy.ext.config.ts index e6cde655b..7cc79f9f6 100644 --- a/src/extensions/core/extensions/cloudBadges/comfy.ext.config.ts +++ b/src/extensions/core/extensions/cloudBadges/comfy.ext.config.ts @@ -1,5 +1,7 @@ import { defineComfyExtConfig } from '@/extensions/utils' export default defineComfyExtConfig({ + name: 'Comfy.Cloud.Badges', + activationEvents: ['*'], comfyCloud: true, }) diff --git a/src/extensions/core/extensions/cloudFeedbackTopbarButton/comfy.ext.config.ts b/src/extensions/core/extensions/cloudFeedbackTopbarButton/comfy.ext.config.ts index e6cde655b..6a7164e70 100644 --- a/src/extensions/core/extensions/cloudFeedbackTopbarButton/comfy.ext.config.ts +++ b/src/extensions/core/extensions/cloudFeedbackTopbarButton/comfy.ext.config.ts @@ -1,5 +1,7 @@ import { defineComfyExtConfig } from '@/extensions/utils' export default defineComfyExtConfig({ + name: 'Comfy.Cloud.FeedbackButton', + activationEvents: ['*'], comfyCloud: true, }) diff --git a/src/extensions/core/extensions/cloudRemoteConfig/comfy.ext.config.ts b/src/extensions/core/extensions/cloudRemoteConfig/comfy.ext.config.ts index e6cde655b..393068a6f 100644 --- a/src/extensions/core/extensions/cloudRemoteConfig/comfy.ext.config.ts +++ b/src/extensions/core/extensions/cloudRemoteConfig/comfy.ext.config.ts @@ -1,5 +1,7 @@ import { defineComfyExtConfig } from '@/extensions/utils' export default defineComfyExtConfig({ + name: 'Comfy.Cloud.RemoteConfig', + activationEvents: ['*'], comfyCloud: true, }) diff --git a/src/extensions/core/extensions/cloudSessionCookie/comfy.ext.config.ts b/src/extensions/core/extensions/cloudSessionCookie/comfy.ext.config.ts index e6cde655b..8356d90d6 100644 --- a/src/extensions/core/extensions/cloudSessionCookie/comfy.ext.config.ts +++ b/src/extensions/core/extensions/cloudSessionCookie/comfy.ext.config.ts @@ -1,5 +1,7 @@ import { defineComfyExtConfig } from '@/extensions/utils' export default defineComfyExtConfig({ + name: 'Comfy.Cloud.SessionCookie', + activationEvents: ['*'], comfyCloud: true, }) diff --git a/src/extensions/core/extensions/cloudSubscription/comfy.ext.config.ts b/src/extensions/core/extensions/cloudSubscription/comfy.ext.config.ts index 059f2284f..7e5205812 100644 --- a/src/extensions/core/extensions/cloudSubscription/comfy.ext.config.ts +++ b/src/extensions/core/extensions/cloudSubscription/comfy.ext.config.ts @@ -1,6 +1,8 @@ import { defineComfyExtConfig } from '@/extensions/utils' export default defineComfyExtConfig({ + name: 'Comfy.Cloud.Subscription', + activationEvents: ['*'], comfyCloud: { subscriptionRequired: true, }, diff --git a/src/extensions/core/extensions/load3d/comfy.ext.config.ts b/src/extensions/core/extensions/load3d/comfy.ext.config.ts new file mode 100644 index 000000000..426a973f7 --- /dev/null +++ b/src/extensions/core/extensions/load3d/comfy.ext.config.ts @@ -0,0 +1,111 @@ +import { defineComfyExtConfig } from '@/extensions/utils' + +export default defineComfyExtConfig({ + name: 'Comfy.Load3D', + activationEvents: ['onWidgets:contributes', 'onCommands:contributes', 'onSettings:contributes'], + contributes: [ + { + name: 'Comfy.Preview3D', + widgets: ['PREVIEW_3D'], + }, + { + name: 'Comfy.Load3D', + widgets: ['LOAD_3D'], + settings: [ + { + id: 'Comfy.Load3D.ShowGrid', + category: ['3D', 'Scene', 'Initial Grid Visibility'], + name: 'Initial Grid Visibility', + tooltip: + 'Controls whether the grid is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.', + type: 'boolean', + defaultValue: true, + experimental: true + }, + { + id: 'Comfy.Load3D.BackgroundColor', + category: ['3D', 'Scene', 'Initial Background Color'], + name: 'Initial Background Color', + tooltip: + 'Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation.', + type: 'color', + defaultValue: '282828', + experimental: true + }, + { + id: 'Comfy.Load3D.CameraType', + category: ['3D', 'Camera', 'Initial Camera Type'], + name: 'Initial Camera Type', + tooltip: + 'Controls whether the camera is perspective or orthographic by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.', + type: 'combo', + options: ['perspective', 'orthographic'], + defaultValue: 'perspective', + experimental: true + }, + { + id: 'Comfy.Load3D.LightIntensity', + category: ['3D', 'Light', 'Initial Light Intensity'], + name: 'Initial Light Intensity', + tooltip: + 'Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation.', + type: 'number', + defaultValue: 3, + experimental: true + }, + { + id: 'Comfy.Load3D.LightIntensityMaximum', + category: ['3D', 'Light', 'Light Intensity Maximum'], + name: 'Light Intensity Maximum', + tooltip: + 'Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget.', + type: 'number', + defaultValue: 10, + experimental: true + }, + { + id: 'Comfy.Load3D.LightIntensityMinimum', + category: ['3D', 'Light', 'Light Intensity Minimum'], + name: 'Light Intensity Minimum', + tooltip: + 'Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget.', + type: 'number', + defaultValue: 1, + experimental: true + }, + { + id: 'Comfy.Load3D.LightAdjustmentIncrement', + category: ['3D', 'Light', 'Light Adjustment Increment'], + name: 'Light Adjustment Increment', + tooltip: + 'Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment.', + type: 'slider', + attrs: { + min: 0.1, + max: 1, + step: 0.1 + }, + defaultValue: 0.5, + experimental: true + }, + { + id: 'Comfy.Load3D.3DViewerEnable', + category: ['3D', '3DViewer', 'Enable'], + name: 'Enable 3D Viewer (Beta)', + tooltip: + 'Enables the 3D Viewer (Beta) for selected nodes. This feature allows you to visualize and interact with 3D models directly within the full size 3d viewer.', + type: 'boolean', + defaultValue: false, + experimental: true + } + ], + commands: [ + { + id: 'Comfy.3DViewer.Open3DViewer', + icon: 'pi pi-pencil', + label: 'Open 3D Viewer (Beta) for Selected Node', + } + ], + }, + ], +}) diff --git a/src/extensions/core/extensions/saveMesh/comfy.ext.config.ts b/src/extensions/core/extensions/saveMesh/comfy.ext.config.ts new file mode 100644 index 000000000..aea78abfc --- /dev/null +++ b/src/extensions/core/extensions/saveMesh/comfy.ext.config.ts @@ -0,0 +1,12 @@ +import { defineComfyExtConfig } from '@/extensions/utils' + +export default defineComfyExtConfig({ + name: 'Comfy.SaveGLB', + activationEvents: ['onWidgets:contributes', 'onCommands:contributes', 'onSettings:contributes'], + contributes: [ + { + name: 'Comfy.SaveGLB', + widgets: ['SAVE_GLB'], + }, + ], +}) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 54d02399f..c00d6a21d 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -1,14 +1,15 @@ import { dispatchComfyExtensions } from '../dispatch' +import type { ComfyExtensionConfigs, ComfyExtensionEntrance } from '../types' -export async function importExtensions() { +export async function registerExtensions() { console.log('importExtensions running...') const extConfigs = import.meta.glob(`./extensions/*/comfy.ext.config.ts`, { // Since each config is small, we only import the default export and use eager mode for better tree-shaking and performance. import: 'default', eager: true, - }) - const extensionEntrance = import.meta.glob(`./extensions/*/index.ts`) + }) as ComfyExtensionConfigs + const extensionEntrance = import.meta.glob(`./extensions/*/index.ts`) as ComfyExtensionEntrance dispatchComfyExtensions({ configs: extConfigs, entrance: extensionEntrance }) } diff --git a/src/extensions/dispatch.ts b/src/extensions/dispatch.ts index 3b81e5848..6a94dd92a 100644 --- a/src/extensions/dispatch.ts +++ b/src/extensions/dispatch.ts @@ -4,7 +4,7 @@ import type { ComfyExtensionEntrance, ComfyExtensionLoadContext } from './types' -import { formatExtensions, shouldLoadExtension } from './utils' +import { formatExtensions, normalizationActivationEvents } from './utils' const extLoadContext: ComfyExtensionLoadContext = { get isCloud() { @@ -22,11 +22,61 @@ export async function dispatchComfyExtensions(options: { const { configs, entrance } = options const extensions = formatExtensions(entrance, configs) for (const extension of Object.values(extensions)) { - if (shouldLoadExtension(extLoadContext, extension.config)) { - const module = await extension.entry() - console.log('✅ extension', extension.name, 'loaded', extension, module) + // if (shouldLoadExtension(extLoadContext, extension.config)) { + // const module = await extension.entry() + // console.log('✅ extension', extension.name, 'loaded', extension, module) + // } else { + // console.log('❌ extension', extension.name, 'disabled', extension.config) + // } + const activationEvents = normalizationActivationEvents( + extLoadContext, + extension.config + ) + if (!activationEvents.length) { + console.log( + '❌ extension', + extension.name, + 'has no activation events', + extension.config + ) } else { - console.log('❌ extension', extension.name, 'disabled', extension.config) + console.log( + '🧶 extension', + extension.name, + 'has activation events:', + activationEvents + ) } + activationEvents.forEach((event) => + onceExtImportEvent(event, async ({ event }) => { + console.log( + '✅ extension', + extension.name, + 'loaded by', + event, + extension + ) + await extension.entry() + }) + ) + } +} + +type EventCallback = (ctx: { event: string }) => void | Promise + +const eventMap = new Map>() + +export async function importExtensionsByEvent(event: string) { + const callbacks = eventMap.get(event) + if (!callbacks) return + eventMap.delete(event) + await Promise.all([...callbacks].map((cb) => cb({ event }))) +} + +function onceExtImportEvent(event: string, callback: EventCallback) { + if (eventMap.has(event)) { + eventMap.get(event)!.add(callback) + } else { + eventMap.set(event, new Set([callback])) } } diff --git a/src/extensions/types.ts b/src/extensions/types.ts index aa12c3415..fa788933c 100644 --- a/src/extensions/types.ts +++ b/src/extensions/types.ts @@ -1,4 +1,47 @@ +import type { ComfyExtension } from '@/types' + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +type ExcludeFunctions = T extends Function ? never : T +type StaticOnly = { + // eslint-disable-next-line prettier/prettier + [K in keyof T as ExcludeFunctions extends never ? never : K]: ExcludeFunctions +} + +type StaticComfyCommand = StaticOnly< + NonNullable[number] +> +type StaticComfySettingParams = StaticOnly< + NonNullable[number] +> +type StaticComfyKeybinding = StaticOnly< + NonNullable[number] +> +type StaticComfyMenuCommandGroup = StaticOnly< + NonNullable[number] +> + +export type ComfyExtensionActivationEvent = + | '*' + | 'onWidgets:contributes' + | 'onCommands:contributes' + | 'onSettings:contributes' + +type ComfyExtensionContributes = { + name: string + widgets?: string[] + commands?: StaticComfyCommand[] + settings?: StaticComfySettingParams[] + keybindings?: StaticComfyKeybinding[] +} + export interface ComfyExtensionConfig { + name?: string + + activationEvents: ComfyExtensionActivationEvent[] + + contributes?: ComfyExtensionContributes | ComfyExtensionContributes[] + menuCommands?: StaticComfyMenuCommandGroup[] + comfyCloud?: | boolean | { diff --git a/src/extensions/utils.ts b/src/extensions/utils.ts index cdad268e8..3d773da53 100644 --- a/src/extensions/utils.ts +++ b/src/extensions/utils.ts @@ -39,13 +39,64 @@ export function formatExtensions( return pkgs } -export function shouldLoadExtension( +export function normalizationActivationEvents( ctx: ComfyExtensionLoadContext, - extConfig: ComfyExtensionConfig | undefined -): boolean { - // No Config -> Load Extension - if (!extConfig) return true + config: ComfyExtensionConfig | undefined +): string[] { + if (!config) return ['*'] + if (!checkAboutCloud(ctx, config)) return [] + + const { activationEvents, contributes: _contributes } = config + + if (activationEvents.includes('*')) return ['*'] + + const contributes = _contributes + ? Array.isArray(_contributes) + ? _contributes + : [_contributes] + : [] + + const events: string[] = [] + + if (activationEvents.includes('onCommands:contributes')) { + for (const contribute of contributes) { + if (contribute.commands) { + for (const command of contribute.commands) { + events.push(`onCommands:${command}`) + } + } + } + } + + if (activationEvents.includes('onSettings:contributes')) { + for (const contribute of contributes) { + if (contribute.settings) { + for (const setting of contribute.settings) { + events.push(`onSettings:${setting.id}`) + } + } + } + } + + if (activationEvents.includes('onWidgets:contributes')) { + for (const contribute of contributes) { + events.push(`onWidgets:${contribute.name}`) + if (contribute.widgets) { + for (const widget of contribute.widgets) { + events.push(`onWidgets:${widget}`) + } + } + } + } + + return events +} + +function checkAboutCloud( + ctx: ComfyExtensionLoadContext, + extConfig: ComfyExtensionConfig +): boolean { // Cloud Only Extension const { comfyCloud } = extConfig if (comfyCloud) { diff --git a/src/services/extensionService.ts b/src/services/extensionService.ts index 8e15ad137..ed705ecf9 100644 --- a/src/services/extensionService.ts +++ b/src/services/extensionService.ts @@ -12,6 +12,7 @@ import { useWidgetStore } from '@/stores/widgetStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import type { ComfyExtension } from '@/types/comfy' import type { AuthUserInfo } from '@/types/authTypes' +import { importExtensionsByEvent } from '@/extensions/dispatch' export const useExtensionService = () => { const extensionStore = useExtensionStore() @@ -35,8 +36,12 @@ export const useExtensionService = () => { // Need to load core extensions first as some custom extensions // may depend on them. - const { importExtensions } = await import('../extensions/core/index') - await importExtensions() + const { registerExtensions } = await import('../extensions/core/index') + await registerExtensions() + + // Import Immediately Loaded Extensions + await importExtensionsByEvent('*') + extensionStore.captureCoreExtensions() await Promise.all( extensions