Skip to content

Conversation

bigmistqke
Copy link
Contributor

@bigmistqke bigmistqke commented Aug 24, 2025

Initial implementation of a plugin-system, continuing on #32

Plugin API

Vibe coded with claude on the plugin API choices. It's a bit much with the overloads, but it feels fun to use and the api allows for nice type inference.

/** Without setup */

// Plugin for all:
plugin((element) => ({ onMouseDown(cb: (event: MouseEvent) => void): ... }))

// Conditional plugins:

// Filter all elements extending from Mesh / Camera
plugin(
    [Mesh, Camera], 
    (meshOrCamera) => ({ onMouseDown(cb: (event: MouseEvent) => void): ... })
)

// Filter all elements with a type guard
plugin(
    (element): element is Mesh => element instanceof Mesh, 
    (mesh) => ({ onMouseDown(cb: (event: MouseEvent) => void): ... })
)

/** With setup */

// Plugin for all:
plugin
    .setup((store) => ({ ... })
    .then((element, context) => ({ onMouseDown(cb: (event: MouseEvent) => void): ... }))

// Conditional plugins:

// Filter all elements extending from Mesh / Camera
plugin
    .setup((store) => ({ ... })
    .then(
        [Mesh, Camera], 
        (meshOrCamera, context) => ({ onMouseDown(cb: (event: MouseEvent) => void): ... })
    )

// Filter all elements with a type guard
plugin
    .setup((store) => ({ ... })
    .then(
        (element): element is Mesh => element instanceof Mesh, 
        (mesh, context) => ({ onMouseDown(cb: (event: MouseEvent) => void): ... })
    )

Usage

via createT

const ShakePlugin = plugin([THREE.Camera, THREE.Mesh], element => ({
  shake: (intensity = 0.1) => {
    const originalPosition = element.position.clone()
    useFrame(() => {
      element.position.x = originalPosition.x + (Math.random() - 0.5) * intensity
      element.position.y = originalPosition.y + (Math.random() - 0.5) * intensity
      element.position.z = originalPosition.z + (Math.random() - 0.5) * intensity
    })
  },
}))

const { T, Canvas } = createT(THREE, [ShakePlugin])

return <T.Mesh shake={0.1} />

via prop

return <Entity from={Mesh} plugins={[Shakeplugin]} shake={0.1} />

Event Plugin

Abstracted the event handling that we inherited from react-three-fiber out of core and into a plugin: EventPlugin

bigmistqke and others added 11 commits August 24, 2025 17:41
Add createPlugin() builder API that enables type-safe conditional plugins:
- createPlugin().extends(Constructor).provide() for filtered plugins
- createPlugin().provide() for global plugins
- createPlugin(setup).provide() for plugins requiring initialization
- Proper context passing from setup to provide functions

Refactor EventPlugin to use new builder pattern, eliminating interface
duplication and improving developer experience. The new API provides
better type inference and cleaner syntax while maintaining full
type safety for conditional plugin methods.
- Replace separate .extends() and .filter() methods with unified .prop()
- Support three filtering patterns:
  - Single constructor: .prop(THREE.Mesh, element => ...)
  - Multiple constructors: .prop([THREE.Camera, THREE.Light], element => ...)
  - Type guards: .prop((element): element is T => condition, element => ...)
- Add generic context typing to PluginBuilder
- Update examples to demonstrate all three patterns
- Maintain full TypeScript inference and runtime type checking

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Remove separate .provide() method in favor of unified .prop()
- Support single argument for global plugins: .prop((element, context) => methods)
- Support two arguments for filtered plugins: .prop(filter, methods)
- Add proper TypeScript interface with method overloads
- Update EventPlugin to use new unified API
- Add GlobalPlugin example demonstrating single-argument usage
- Fix type inference issues with generic context typing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Replace createPlugin().prop() with direct plugin() calls
- Add plugin.setup().then() chain for plugins needing context
- Support all filtering patterns directly:
  - Global: plugin(element => methods)
  - Constructor: plugin(THREE.Mesh, element => methods)
  - Array: plugin([THREE.Camera, THREE.Light], element => methods)
  - Type guard: plugin(typeGuard, element => methods)
- Update EventPlugin to use setup chain pattern
- Add ContextPlugin example demonstrating setup usage
- Maintain backward compatibility with plugin().prop()

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Restructure plugin system with simplified type resolution
- Consolidate event types and handlers into event-plugin.ts
- Reorganize type definitions for better maintainability
- Clean up imports and remove unused type utilities
- Update PluginExample with Entity component usage
- Rename PluginInterface to PluginFn for clarity
- Improve plugin prop type resolution with PluginPropsOf
- Move event-related types to their proper location
@bigmistqke
Copy link
Contributor Author

bigmistqke commented Aug 24, 2025

In #32 I discussed having <Canvas /> as the base of where the plugins cascade, via context, but this I have not currently implemented as I am not sure it is necessary. The methods currently (declare it in createT or pass it via props) might be sufficient and provide a type-safety that context can not provide.

The setup functions currently do rely on context so they only set once (via a ReactiveMap<Plugin, ReturnType<Plugin>>). This way if I have a canvas that is set with createT(..., [EventPlugin]) and then I have another entity that also uses the same plugin <Entity from={Mesh} plugins={[EventPlugin]} />, they both share the same setup function/context.

But I would personally prefer if nothing relied on where it is mount. Maybe we can follow the approach of Web Components and walk up the tree when it is attached to the scene via useSceneGraph? Something to explore...

@bigmistqke bigmistqke changed the title add plugin system feat: add plugin system Aug 25, 2025
@bigmistqke bigmistqke added enhancement New feature or request next Issues related to next pre-release labels Aug 25, 2025
- Remove complex overload resolution system in favor of direct
inheritance checking.
- Remove (element: any): {} fallback overloads from PluginFn
signatures
- Replace complex ResolvePluginReturn with simple TKind extends P
inheritance check
- Simplify PluginPropsOf type with inline constraint
- Fix JSDoc comment for CameraKind type

This eliminates the need for R1-R5 overload complexity while
maintaining full functionality through direct type inheritance
matching.
- instead of mergeProps, let's just merge it together, this prevents the need for proxies. plugins are not going to be hotly updated, but could be hotly accessed, so it's better to optimize for access.
@bigmistqke
Copy link
Contributor Author

bigmistqke commented Aug 26, 2025

Maybe the setup function should be global instead, and not connected to any root. That way they can be initialized from the start and are not context dependent.

The EventPlugin needs some additional consideration and is a good usecase: Make your core features plugins, to validate that you have a powerful and well designed plugin API.!

  1. How could EventPlugin work with multiple canvases and a global setup function? How can it know which objects to intersect and which not?
  2. EventPlugin adds event listeners to the canvas, so we kind of do need to access the canvas somehow.
    • Do we have a bind-method: plugin.bind(dom => dom.addEventListener(...)).then(...)?
    • If plugins can be passed to any object, how can we know it is connected to the canvas?

Solution

One solution would be for the EventPlugin to do rely on context, but to be very explicit about it:

const { T, Canvas } = createT(THREE, [EventPlugin])

return <Canvas>
    <EventPlugin.Provider>
        ...
    </EventPlugin.Provider>
</Canvas>

Then at least the API communicates what it can and cannot do.
All other plugins will work wether you use declared the component with the plugin outside a <Canvas/>, but the EventPlugin is context bound.

Then maybe we can remove plugin.setup(...) all together: if you want a setup, simply add a context!

We could add API to prevent deep nesting of contexts:

const { T, Canvas } = createT(THREE, [EventPlugin])

return <Canvas contexts={[EventPlugin.Provider]}>
   ...
</Canvas>

- simplifies signature/implementation significantly
- removes the question of where/who/what/when the setup should happen
- instead of setup-method in plugin, if you want to do setup and context: use context. e.g. attaching a .Provider to your plugin and passing the plugin to Canvas' context-prop: <Canvas contexts={[EventPlugin]} />
    -> you could also inject the provider like <Canvas><Plugin.Provider>..., but for plugins that want to modify the canvas-props, you will have to pass it to canvas' context-prop.
include @react-three/fiber's license, as we do in solid-drei
@bigmistqke
Copy link
Contributor Author

bigmistqke commented Aug 26, 2025

Currently i am resolving the methods for each entity to a unique object. This is extra memory overhead per entity.

A solution could be to change the API and resolve the prop dynamically

createEffect(() => {
    for(const key of props){
        // plugin.run returns true if the plugin contained the method
        if(plugins.find(plugin => plugin.run(element, props, key))) return 
        ...
    }
})

If the selector is a createSelector the overhead of doing it during prop resolution would be pretty minimal, most likely.

The API of the plugin should change then, to something like:

const Plugin = plugin([Mesh], 
    { 
        onMouseDown(element, value){ 
            ... 
        }
    }
)
// or
const Plugin = plugin([Mesh], 
    (element, key, value) => {
       ...
    }
)

@bigmistqke bigmistqke mentioned this pull request Oct 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request next Issues related to next pre-release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant