Skip to content

feat: consider plugin-system #32

Open

Description

@bigmistqke

As suggested by @gleamish and continuing on issue #24 maybe it would be interesting to consider a plugin-system to enrich what is contained in useThree and how the props of T are resolved.

API I had in mind:

// solid-three.ts
import { createT, Interactivity } from "solid-three"
import { DreiPlugin } from "solid-drei"
import { BoxGeometry, Mesh, MeshBasicMaterial } from "three"

export const { Canvas, T, extend } = createT({ BoxGeometry, Mesh, MeshBasicMaterial }, [DreiPlugin, Interactivity])

// app.ts
import { Canvas, T } from "./solid-three.ts"
import { render } from "solid-js/web"

render(() => <Canvas><T.Mesh onClick={console.log}><T.BoxGeometry/><T.MeshBasicMaterial/></T.Mesh></Canvas>, document.getElementById('app'))

Activity

  1. added
    enhancementNew feature or request
    nextIssues related to next pre-release
    on Aug 4, 2025
  2. changed the title [-]consider plugin-system[/-] [+]feat: consider plugin-system[/+] on Aug 4, 2025
  3. bigmistqke commented on Aug 4, 2025

    @bigmistqke
    ContributorAuthor
  4. bigmistqke commented on Aug 24, 2025

    @bigmistqke
    ContributorAuthor

    One of the concerns I have for the plugin-system is how it would interact with third party solid-three components:

    • how can you create components if you are not sure what props they will have (or how those props will behave)?
    • if the prop resolution is handled in the root-component <Canvas/>, it could also extend external components, but how can you type them dynamically?

    threlte opts for using context to inject the plugins and augmenting module namespaces to set the prop types. These 2 approaches are fundamentally at odds in my opinion, as context is about scoping and augmenting module namespaces are global. That being said, in 99.9999% of the user-projects there will only be a single threejs context, so it's not bad to have it as an option.

    I am not sure how threlte handles typechecking external components if the props they use are actually valid in the project, but I was thinking of something like this:

    from my-lib

    Library authors would need to make their components generic

    import { Entity, type Plugin } from "solid-three"
    
    export function Box<T extends Plugin<{ onMouseDown(...): ... }>>(props: ...){
        return <Entity<T> from={...} onMouseDown={props.onMouseDown}  />
    }

    The generic extends from Plugin<{ ... }> defining all the non-standard props that the component makes use of.

    user project

    // t.ts
    import { createT, EventPlugin, type Plugin } from "solid-three"
    
    const plugins = [EventPlugin] satisfies Plugin[]
    export type Plugin = typeof plugins
    
    export const { T, Canvas } = createT(THREE, plugins)
    // App.tsx
    import { type Plugin, Canvas } from "./t.ts"
    import { Box } from "my-lib"
    
    return <Canvas>
      <Box<Plugin> onMouseDown={console.log} />
    </Canvas>

    It's a bit of plumbing, but it would give you type-security.

    A QOL idea would be that you can pass your external components in the createT-call, this way they could be typechecked at the declaration and <Box<Plugin> /> can become <T.Box />:

    // t.ts
    import { Box } from "my-lib"
    import { createT, EventPlugin, type Plugin } from "solid-three"
    
    const plugins = [EventPlugin] satisfies Plugin[]
    export type Plugin = typeof plugins
    
    export const { T, Canvas } = createT({ ... THREE, Box }, plugins)
    // App.tsx
    import { T, Canvas } from "./t.ts"
    
    return <Canvas>
      <T.Box onMouseDown={console.log} />
    </Canvas>

    The same can be done for <Entity<Plugin> /> 👉 <T.Entity />

  5. bigmistqke commented on Aug 24, 2025

    @bigmistqke
    ContributorAuthor

    Maybe it could fallback to the namespace if no generic is passed

    my-lib

    import { Entity, type S3 } from "solid-three"
    
    export function Box<T extends S3.Plugin<{ ... }> = $3.DefaultPlugin>(props: ...){
        return <Entity<T> from={...} onMouseDown={props.onMouseDown}  />
    }

    user code

    import { Box } from "my-lib"
    import { Canvas } from "./t.ts"
    
    declare global {
      namespace $3 {
        interface Props {
          onMouseDown(event: MouseEvent): void 
        }
      }
    }
    
    return <Canvas>
      <Box onMouseDown={console.log} />
    </Canvas>
  6. bigmistqke commented on Aug 24, 2025

    @bigmistqke
    ContributorAuthor

    Another option would be to follow in threlte's footsteps and also allow to scope plugins via context-scope:

    import { Entity, type S3, PluginProvider } from "solid-three"
    import { EventPlugin } from "solid-drei"
    
    export function Box(props: ...){
        return <PluginProvider plugins={[EventPlugin]}>
            <Entity from={...} onMouseDown={props.onMouseDown}  />
        </PluginProvider>
    }

    Then you are also assured that the props your component relies upon are being handled as you would expect.
    Or add a plugins-prop for complete granular control?

    import { Entity, type S3, PluginProvider } from "solid-three"
    import { EventPlugin } from "solid-drei"
    
    export function Box(props: ...){
        return <Entity from={...} onMouseDown={props.onMouseDown}  plugins={[EventPlugin]} />
    }

    Question is then: how do these interact with plugins defined in createT(..., [...plugins])?

  7. bigmistqke commented on Aug 24, 2025

    @bigmistqke
    ContributorAuthor

    Question is then: how do these interact with plugins defined in createT(..., [...plugins])?

    Some pseudo code

    my-lib

    import { createT, Entity, type S3 } from "solid-three"
    import { Mesh } from "three"
    
    function MyLibPlugin(element){
        return {
            onMouseDown: (callback) => callback('lib')
        }
    }
    
    const { T, Canvas } = createT(THREE, [MyLibPlugin])
    
    export function Box(props: S3.Props<Mesh>){
        return <T.Mesh onMouseDown={console.log} {...props} />
    }
    
    export function BareBox<T>(props: S3.Props<Mesh>){
        return <Entity<T> from={Mesh} {...props} />
    }

    user code

    import { Box, BareBox } from "my-lib"
    import { createT } from "solid-three"
    
    function Plugin1(element){
        return {
            onMouseDown: (callback) => callback('user 1')
        }
    }
    function Plugin2(element){
        return {
            onMouseDown: (callback) => callback('user 2')
        }
    }
    
    const { T, Canvas } = createT(THREE, [Plugin1])
    
    return <Canvas>
        {/* logs 'user 1' */}
        <T.Mesh onMouseDown={console.log} /> 
        {/* logs 'user 2' */}
        <T.Mesh onMouseDown={console.log} plugins={[Plugin2]} />
        {/* logs 'lib' */}
        <Box onMouseDown={console.log} />
        {/* logs 'user 2' */}
        <Box onMouseDown={console.log} plugins={[Plugin2]} />
        {/* logs 'user 1' */}
        <BareBox<typeof T> onMouseDown={console.log} />
        {/* logs 'user 2' */}
        <BareBox<typeof T> onMouseDown={console.log} plugins={[Plugin2]} />
    </Canvas>

    This is how I imagine it: cascading over multiple levels.

    1. Base layer is the <Canvas/>-context 👉 components mount in <Canvas/> will fall back to resolving their props according to the plugins defined in createT. This includes external components. (?)
      • if in createT(..., [plugin1, plugin2]) plugin1 and plugin2 try to handle the same prop, the last wins (?)
    2. All entities defined with createT and accessed via T. have their props resolved via the plugins defined in createT. This allows for libraries to define their own T for internal use.
    3. Every <Entity /> and <T. /> component has a plugins-prop, in which they can pass plugins too. If a property is defined these will take preference.

    The fact that it is also distributed over context is a bit awkward as

    import { Box } from "my-lib"
    const box = <Box/>
    return <Canvas>{box}</Canvas>

    and

    import { Box } from "my-lib"
    return <Canvas><Box/></Canvas>

    will have different ways of resolving their prop...
    but context is a bit awkward in general, so ye.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestnextIssues related to next pre-release

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions