Skip to content

Commit d0402c9

Browse files
committed
refactor: simplify plugin type system by removing fallback overloads
- 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.
1 parent 57f2d1b commit d0402c9

File tree

5 files changed

+122
-115
lines changed

5 files changed

+122
-115
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"Bash(ls:*)",
88
"Bash(npm run typecheck:*)",
99
"Bash(npm run:*)",
10-
"Bash(npx tsc:*)"
10+
"Bash(npx tsc:*)",
11+
"Bash(grep:*)"
1112
],
1213
"deny": []
1314
}

playground/examples/PluginExample.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as THREE from "three"
2-
import type { Meta } from "types.ts"
2+
import type { Meta, Plugin } from "types.ts"
33
import {
44
createT,
55
Entity,
@@ -53,8 +53,11 @@ const MaterialPlugin = plugin(
5353
)
5454

5555
// Global plugin - applies to all elements using single argument
56-
const GlobalPlugin = plugin(element => ({
57-
log: (message: string) => {
56+
const GlobalPlugin: Plugin<{
57+
(element: THREE.Material): { log(message: number): void }
58+
(element: THREE.Mesh): { log(message: string): void }
59+
}> = plugin(element => ({
60+
log: (message: string | number) => {
5861
console.info(`[${element.constructor.name}] ${message}`)
5962
},
6063
}))
@@ -103,7 +106,7 @@ export function PluginExample() {
103106
onClick={event => event.stopPropagation()}
104107
>
105108
<T.TorusKnotGeometry args={[1, 0.5, 128, 32]} />
106-
<T.MeshStandardMaterial metalness={1} roughness={0} color="white">
109+
<T.MeshStandardMaterial metalness={1} roughness={0} color="white" log={0}>
107110
<Resource
108111
loader={THREE.CubeTextureLoader}
109112
attach="envMap"

src/components.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function Entity<
9090
props:
9191
| Props<T>
9292
| { from: T; children?: JSXElement; plugins?: TPlugins }
93-
| InferPluginProps<TPlugins>,
93+
| InferPluginProps<T, TPlugins>,
9494
) {
9595
const [config, rest] = splitProps(props, ["from", "args"])
9696
const memo = whenMemo(

src/create-canvas.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "three"
1111
import { createThree } from "./create-three.tsx"
1212
import type { EventRaycaster } from "./raycasters.tsx"
13-
import type { Context, Plugin, PluginPropsOf, Props } from "./types.ts"
13+
import type { Context, InferPluginProps, Plugin, Props } from "./types.ts"
1414

1515
/**
1616
* Props for the Canvas component, which initializes the Three.js rendering context and acts as the root for your 3D scene.
@@ -56,7 +56,7 @@ export interface CanvasProps extends ParentProps {
5656
* @returns A div element containing the WebGL canvas configured to occupy the full available space.
5757
*/
5858
export function createCanvas<TPlugins extends Plugin[] = Plugin[]>(plugins: TPlugins) {
59-
return function (props: ParentProps<CanvasProps> & Partial<PluginPropsOf<Scene, TPlugins>>) {
59+
return function (props: ParentProps<CanvasProps> & Partial<InferPluginProps<Scene, TPlugins>>) {
6060
let canvas: HTMLCanvasElement = null!
6161
let container: HTMLDivElement = null!
6262

src/types.ts

Lines changed: 110 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface Viewport {
114114
aspect: number
115115
}
116116

117-
/** Possible camera types. */
117+
/** Possible camera kinds. */
118118
export type CameraKind = PerspectiveCamera | OrthographicCamera
119119

120120
export type Loader<TSource, TResult extends object> = {
@@ -196,7 +196,7 @@ export type Props<T, TPlugins extends Plugin[] | undefined = Plugin[]> = Partial
196196
raycastable: boolean
197197
plugins: TPlugins
198198
},
199-
TPlugins extends Plugin[] ? PluginPropsOf<InstanceOf<T>, TPlugins> : {},
199+
TPlugins extends Plugin[] ? InferPluginProps<InstanceOf<T>, TPlugins> : {},
200200
]
201201
>
202202
>
@@ -306,145 +306,147 @@ export interface Plugin<TFn = (element: any) => any> {
306306
(context: Context): TFn
307307
}
308308

309+
/**
310+
* Plugin function interface that defines all possible plugin creation patterns.
311+
*
312+
* Plugins extend solid-three components with additional functionality and can be:
313+
* - Global: apply to all elements
314+
* - Filtered: apply only to specific element types (via constructor array or type guard)
315+
* - With setup: access to the Three.js context during initialization
316+
*
317+
* @example
318+
* // Global plugin
319+
* const LogPlugin = plugin(element => ({
320+
* log: (message: string) => console.log(`[${element.type}] ${message}`)
321+
* }))
322+
*
323+
* @example
324+
* // Filtered plugin with constructor array
325+
* const ShakePlugin = plugin([THREE.Camera, THREE.Mesh], element => ({
326+
* shake: (intensity = 0.1) => {
327+
* useFrame(() => {
328+
* element.position.x += (Math.random() - 0.5) * intensity
329+
* })
330+
* }
331+
* }))
332+
*
333+
* @example
334+
* // Filtered plugin with type guard
335+
* const MaterialPlugin = plugin(
336+
* (element): element is THREE.Mesh => element instanceof THREE.Mesh,
337+
* element => ({
338+
* setColor: (color: string) => element.material.color.set(color)
339+
* })
340+
* )
341+
*
342+
* @example
343+
* // Plugin with setup context
344+
* const ContextPlugin = plugin
345+
* .setup((context) => ({ scene: context.scene }))
346+
* .then([THREE.Object3D], (element, context) => ({
347+
* addToScene: () => context.scene.add(element)
348+
* }))
349+
*/
309350
export interface PluginFn {
310-
// No setup - direct usage with one argument (global)
311-
<Methods extends Record<string, any>>(methods: (element: any) => Methods): Plugin<
351+
/**
352+
* Creates a global plugin that applies to all elements.
353+
*
354+
* @param methods - Function that receives an element and returns plugin methods
355+
* @returns Plugin that applies to all elements
356+
*/
357+
<const Methods extends Record<string, any>>(methods: (element: any) => Methods): Plugin<
312358
(element: any) => Methods
313359
>
314360

315-
// No setup - direct usage with two arguments (array of constructors)
316-
<T extends readonly Constructor[], Methods extends Record<string, any>>(
361+
/**
362+
* Creates a filtered plugin that applies only to specific constructor types.
363+
*
364+
* @param Constructors - Array of constructor functions to filter by
365+
* @param methods - Function that receives a filtered element and returns plugin methods
366+
* @returns Plugin that applies only to matching constructor types
367+
*/
368+
<const T extends readonly Constructor[], const Methods extends Record<string, any>>(
317369
Constructors: T,
318370
methods: (element: T extends readonly Constructor<infer U>[] ? U : never) => Methods,
319371
): Plugin<{
320372
(element: T extends readonly Constructor<infer U>[] ? U : never): Methods
321-
(element: any): {}
322373
}>
323374

324-
// No setup - direct usage with two arguments (type guard)
325-
<T, Methods extends Record<string, any>>(
375+
/**
376+
* Creates a filtered plugin that applies only to elements matching a type guard.
377+
*
378+
* @param condition - Type guard function that determines if plugin applies
379+
* @param methods - Function that receives a filtered element and returns plugin methods
380+
* @returns Plugin that applies only to elements matching the type guard
381+
*/
382+
<const T, const Methods extends Record<string, any>>(
326383
condition: (element: unknown) => element is T,
327384
methods: (element: T) => Methods,
328385
): Plugin<{
329386
(element: T): Methods
330-
(element: any): {}
331387
}>
332388

333-
// Setup function
334-
setup<TSetupContext extends object>(
389+
/**
390+
* Creates a plugin with access to setup context.
391+
*
392+
* The setup function runs once when the plugin is initialized and receives
393+
* the Three.js context. The returned data is passed to all plugin methods.
394+
*
395+
* @param setupFn - Function that receives the Three.js context and returns setup data
396+
* @returns Object with 'then' method to define the plugin behavior
397+
*/
398+
setup<const TSetupContext extends object>(
335399
setupFn: (context: Context) => TSetupContext,
336400
): {
337401
then: {
338-
// With setup - one argument (global)
339-
<Methods extends Record<string, any>>(
402+
/**
403+
* Creates a global plugin with setup context.
404+
*
405+
* @param methods - Function that receives element and setup context, returns plugin methods
406+
* @returns Plugin that applies to all elements with setup context
407+
*/
408+
<const Methods extends Record<string, any>>(
340409
methods: (element: any, context: TSetupContext) => Methods,
341410
): Plugin<(element: any) => Methods>
342411

343-
// With setup - two arguments (array of constructors)
344-
<T extends readonly Constructor[], Methods extends Record<string, any>>(
412+
/**
413+
* Creates a filtered plugin with setup context using constructor array.
414+
*
415+
* @param Constructors - Array of constructor functions to filter by
416+
* @param methods - Function that receives filtered element and setup context, returns plugin methods
417+
* @returns Plugin that applies only to matching constructor types with setup context
418+
*/
419+
<const T extends readonly Constructor[], const Methods extends Record<string, any>>(
345420
Constructors: T,
346421
methods: (
347422
element: T extends readonly Constructor<infer U>[] ? U : never,
348423
context: TSetupContext,
349424
) => Methods,
350425
): Plugin<{
351426
(element: T extends readonly Constructor<infer U>[] ? U : never): Methods
352-
(element: any): {}
353427
}>
354428

355-
// With setup - two arguments (type guard)
356-
<T, Methods extends Record<string, any>>(
429+
/**
430+
* Creates a filtered plugin with setup context using type guard.
431+
*
432+
* @param condition - Type guard function that determines if plugin applies
433+
* @param methods - Function that receives filtered element and setup context, returns plugin methods
434+
* @returns Plugin that applies only to elements matching the type guard with setup context
435+
*/
436+
<const T, const Methods extends Record<string, any>>(
357437
condition: (element: unknown) => element is T,
358438
methods: (element: T, context: TSetupContext) => Methods,
359439
): Plugin<{
360440
(element: T): Methods
361-
(element: any): {}
362441
}>
363442
}
364443
}
365444
}
366445

367-
export type InferPluginProps<TPlugins extends Plugin[]> = Merge<{
368-
[TKey in keyof TPlugins]: TPlugins[TKey] extends () => (element: any) => infer U
369-
? { [TKey in keyof U]: U[TKey] extends (callback: infer V) => any ? V : never }
370-
: never
371-
}>
372-
373-
/**
374-
* Helper type to resolve overloaded function returns
375-
* Matches overloads from most specific to least specific
376-
*/
377-
type ResolvePluginReturn<TFn, TTarget> = TFn extends {
378-
(element: infer P1): infer R1
379-
(element: infer P2): infer R2
380-
(element: infer P3): infer R3
381-
(element: infer P4): infer R4
382-
(element: infer P5): infer R5
383-
}
384-
? TTarget extends P1
385-
? R1
386-
: TTarget extends P2
387-
? R2
388-
: TTarget extends P3
389-
? R3
390-
: TTarget extends P4
391-
? R4
392-
: TTarget extends P5
393-
? R5
394-
: never
395-
: TFn extends {
396-
(element: infer P1): infer R1
397-
(element: infer P2): infer R2
398-
(element: infer P3): infer R3
399-
(element: infer P4): infer R4
400-
}
401-
? TTarget extends P1
402-
? R1
403-
: TTarget extends P2
404-
? R2
405-
: TTarget extends P3
406-
? R3
407-
: TTarget extends P4
408-
? R4
409-
: never
410-
: TFn extends {
411-
(element: infer P1): infer R1
412-
(element: infer P2): infer R2
413-
(element: infer P3): infer R3
414-
}
415-
? TTarget extends P1
416-
? R1
417-
: TTarget extends P2
418-
? R2
419-
: TTarget extends P3
420-
? R3
421-
: never
422-
: TFn extends { (element: infer P1): infer R1; (element: infer P2): infer R2 }
423-
? TTarget extends P1
424-
? R1
425-
: TTarget extends P2
426-
? R2
427-
: never
428-
: TFn extends { (element: infer P): infer R }
429-
? TTarget extends P
430-
? R
431-
: never
432-
: never
433-
434-
/**
435-
* Resolves what a plugin returns for a specific element type T
436-
* Handles both simple functions and overloaded functions
437-
*/
438446
type PluginReturn<TPlugin, TKind> = TPlugin extends Plugin<infer TFn>
439-
? TFn extends (...args: any[]) => any
440-
? ResolvePluginReturn<TFn, TKind> extends infer TResult
441-
? TResult extends never
442-
? TFn extends (element: TKind) => infer R
443-
? R
444-
: TFn extends (element: any) => infer R
445-
? R
446-
: {}
447-
: TResult
447+
? TFn extends { (element: infer P): infer R }
448+
? TKind extends P
449+
? R
448450
: {}
449451
: {}
450452
: {}
@@ -453,12 +455,13 @@ type PluginReturn<TPlugin, TKind> = TPlugin extends Plugin<infer TFn>
453455
* Resolves plugin props for a specific element type T
454456
* This allows plugins to provide conditional methods based on the actual element type
455457
*/
456-
export type PluginPropsOf<T, TPlugins extends Plugin[]> = Merge<{
457-
[K in keyof TPlugins]: PluginReturn<TPlugins[K], T> extends infer Methods
458-
? Methods extends Record<string, any>
459-
? {
460-
[M in keyof Methods]: Methods[M] extends (value: infer V) => any ? V : never
461-
}
462-
: {}
458+
export type InferPluginProps<T, TPlugins extends Plugin[]> = Merge<{
459+
[K in keyof TPlugins]: PluginReturn<TPlugins[K], T> extends infer Methods extends Record<
460+
string,
461+
any
462+
>
463+
? {
464+
[M in keyof Methods]: Methods[M] extends (value: infer V) => any ? V : never
465+
}
463466
: {}
464467
}>

0 commit comments

Comments
 (0)