Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fdb3b23
feat: add initial plugin system
bigmistqke Aug 24, 2025
cb83bc2
feat: add EventPlugin
bigmistqke Aug 24, 2025
268cbee
feat: conditional plugins
bigmistqke Aug 24, 2025
604f610
feat: implement plugin builder API with conditional type support
bigmistqke Aug 24, 2025
713e226
feat: add multiple classes to createFilter().extends(...)
bigmistqke Aug 24, 2025
f804a12
feat: unify plugin API with .prop() method
bigmistqke Aug 24, 2025
ca52caf
feat: consolidate plugin API to single .prop() method
bigmistqke Aug 24, 2025
2714041
chore: cleanup
bigmistqke Aug 24, 2025
d6d2c6e
feat: implement direct plugin() API with setup chain
bigmistqke Aug 24, 2025
00d93e5
cleanup: move PluginInterface to types.ts
bigmistqke Aug 24, 2025
05400ed
refactor: major internal restructuring and API improvements
bigmistqke Aug 24, 2025
a97e40e
cleanup: remove legacy code
bigmistqke Aug 24, 2025
6798b71
feat: simplify plugin signature by removing single class-filter
bigmistqke Aug 24, 2025
f740cb2
feat: pass context as argument to setup function of PluginFn
bigmistqke Aug 24, 2025
99f8ddc
cleanup: cleanup EventPlugin, remove Canvas-export
bigmistqke Aug 24, 2025
57f2d1b
feat: pass PluginProps to <Canvas/>
bigmistqke Aug 25, 2025
d0402c9
refactor: simplify plugin type system by removing fallback overloads
bigmistqke Aug 25, 2025
cc062ab
refactor: consolidate types and remove separate canvas module
bigmistqke Aug 25, 2025
c779d94
feat: add createPluginMethods
bigmistqke Aug 25, 2025
9cd9fda
refactor: pass pluginMethods to applyProps
bigmistqke Aug 25, 2025
63dfc0e
feat: remove setup-function of plugin
bigmistqke Aug 26, 2025
a537e9a
chore: update LICENSE
bigmistqke Aug 26, 2025
286f002
feat: prevent default canvas prop resolution if plugin overrides method
bigmistqke Aug 27, 2025
15f3b42
chore: update gitignore
bigmistqke Aug 27, 2025
e4572d3
demo: fix SolarExample
bigmistqke Sep 20, 2025
9d8b987
demo: add vanilla example
bigmistqke Sep 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.claude
.scratch
dist
types
node_modules
packed/
packed/
27 changes: 27 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,30 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================================================

This project is based on @react-three/fiber (https://github.com/pmndrs/react-three-fiber)
Original

MIT License

Copyright (c) 2019-2025 Poimandres

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@
}
},
"dependencies": {
"@solid-primitives/map": "^0.7.2",
"@solid-primitives/resize-observer": "^2.0.25",
"@solid-primitives/set": "^0.7.2",
"debounce": "^2.1.0"
},
"devDependencies": {
Expand Down
32 changes: 29 additions & 3 deletions playground/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { A, Route, Router } from "@solidjs/router"
import type { ParentProps } from "solid-js"
import * as THREE from "three"
import { Canvas, createT, Entity } from "../src/index.ts"
import { createT, Entity } from "../src/index.ts"
import { EnvironmentExample } from "./examples/EnvironmentExample.tsx"
import { PluginExample } from "./examples/PluginExample.tsx"
import { PortalExample } from "./examples/PortalExample.tsx"
import { SolarExample } from "./examples/SolarExample.tsx"
import { VanillaExample } from "./examples/VanillaExample.tsx"
import "./index.css"

const T = createT(THREE)
const { T, Canvas } = createT({ ...THREE, Entity })

function Layout(props: ParentProps) {
return (
Expand Down Expand Up @@ -67,6 +69,28 @@ function Layout(props: ParentProps) {
>
Environment
</A>
<A
href="/plugin"
style={{
color: "white",
"text-decoration": "none",
padding: "5px 10px",
display: "block",
}}
>
Plugins
</A>
<A
href="/vanilla"
style={{
color: "white",
"text-decoration": "none",
padding: "5px 10px",
display: "block",
}}
>
Vanilla
</A>
</nav>
{props.children}
</>
Expand All @@ -79,6 +103,8 @@ export function App() {
<Route path="/simple-solar" component={SolarExample} />
<Route path="/portal" component={PortalExample} />
<Route path="/environment" component={EnvironmentExample} />
<Route path="/plugin" component={PluginExample} />
<Route path="/vanilla" component={VanillaExample} />
<Route
path="/"
component={() => (
Expand All @@ -87,7 +113,7 @@ export function App() {
scene={{ background: [1, 0, 0] }}
style={{ width: "100vw", height: "100vh" }}
>
<Entity from={THREE.Group}>
<Entity from={THREE.Group} position={[0, 0, 0]}>
<T.Mesh>
<T.BoxGeometry />
<T.MeshBasicMaterial color="gray" />
Expand Down
3 changes: 1 addition & 2 deletions playground/controls/OrbitControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEffect, createMemo, onCleanup, type Ref } from "solid-js"
import { createEffect, createMemo, onCleanup } from "solid-js"
import type { Event } from "three"
import { OrbitControls as ThreeOrbitControls } from "three-stdlib"
import { useFrame, useThree, type S3 } from "../../src/index.ts"
Expand All @@ -7,7 +7,6 @@ import { whenEffect } from "../../src/utils/conditionals.ts"
import { processProps } from "./process-props.ts"

export interface OrbitControlsProps extends S3.Props<typeof ThreeOrbitControls> {
ref?: Ref<ThreeOrbitControls>
camera?: S3.CameraKind
domElement?: HTMLElement
enableDamping?: boolean
Expand Down
14 changes: 11 additions & 3 deletions playground/examples/EnvironmentExample.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { createSignal } from "solid-js"
import * as THREE from "three"
import { Resource } from "../../src/components.tsx"
import { Canvas, createT } from "../../src/index.ts"
import { createT, EventPlugin, Resource } from "../../src/index.ts"
import { OrbitControls } from "../controls/OrbitControls.tsx"

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

export function EnvironmentExample() {
const [position, setPosition] = createSignal(0)

setInterval(() => setPosition(position => position + 1), 500)

return (
<Canvas
style={{ width: "100vw", height: "100vh" }}
Expand All @@ -16,6 +20,10 @@ export function EnvironmentExample() {
onPointerEnter={event => console.debug("canvas pointer enter", event)}
>
<OrbitControls />
<T.Mesh position={new THREE.Vector3(position(), 0, 0)}>
<T.BoxGeometry args={[1, 0.5, 128, 32]} />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
<T.Mesh>
<T.TorusKnotGeometry args={[1, 0.5, 128, 32]} />
<T.MeshStandardMaterial metalness={1} roughness={0} color="white">
Expand Down
Empty file removed playground/examples/Gltf.tsx
Empty file.
121 changes: 121 additions & 0 deletions playground/examples/PluginExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as THREE from "three"
import type { Meta } from "types.ts"
import {
createT,
Entity,
EventPlugin,
plugin,
Resource,
useFrame,
useThree,
} from "../../src/index.ts"
import { OrbitControls } from "../controls/OrbitControls.tsx"

// LookAt plugin - works for all Object3D elements
const LookAtPlugin = plugin([THREE.Object3D], element => {
return {
lookAt: (target: THREE.Object3D | [number, number, number]) => {
useFrame(() => {
if (Array.isArray(target)) {
element.lookAt(...target)
} else {
element.lookAt(target.position)
}
})
},
}
})

// Shake plugin - works for both Camera and Light elements using array syntax
const ShakePlugin = plugin([THREE.Camera, THREE.DirectionalLight, 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
})
},
}))

// Custom filter plugin - works for objects with a 'material' property using type guard
const MaterialPlugin = plugin(
(element: any): element is THREE.Mesh =>
element instanceof THREE.Mesh && element.material !== undefined,
element => ({
highlight: (color: string = "yellow") => {
const material = element.material as THREE.MeshBasicMaterial
material.color.set(color)
},
setColor: (color: string) => {
const material = element.material as THREE.MeshBasicMaterial
material.color.setHex(parseInt(color.replace("#", ""), 16))
},
}),
)

// Global plugin - applies to all elements using single argument
const GlobalPlugin: {
(element: THREE.Material): { log(message: number): void }
(element: THREE.Mesh): { log(message: string): void }
} = plugin(element => ({
log: (message: string | number) => {
console.info(`[${element.constructor.name}] ${message}`)
},
}))

const { T, Canvas } = createT(THREE, [
LookAtPlugin,
ShakePlugin,
EventPlugin,
MaterialPlugin,
GlobalPlugin,
])

export function PluginExample() {
let cubeRef: Meta<THREE.Mesh>
let cameraRef: Meta<THREE.PerspectiveCamera>

return (
<Canvas
style={{ width: "100vw", height: "100vh" }}
defaultCamera={{ position: new THREE.Vector3(0, 0, 5) }}
contexts={[EventPlugin]}
onClickMissed={() => console.info("missed!")}
>
<OrbitControls />
<Entity from={THREE.Mesh} />
{/* Mesh with lookAt (from LookAtPlugin) and material methods (from MaterialPlugin) */}
<T.Mesh
ref={cubeRef!}
position={[0, 0, 0]}
highlight="red"
lookAt={useThree().currentCamera}
log="Mesh rendered!"
shake={0.1}
onClick={event => console.info("clicked mesh!")}
>
<T.TorusKnotGeometry args={[1, 0.5, 128, 32]} />
<T.MeshStandardMaterial metalness={1} roughness={0} color="white">
<Resource
loader={THREE.CubeTextureLoader}
attach="envMap"
path="https://rawcdn.githack.com/mrdoob/three.js/54ac263593c81b669ca9a089491ddd9e240427d2/examples/textures/cube/Bridge2/"
url={["posx.jpg", "negx.jpg", "posy.jpg", "negy.jpg", "posz.jpg", "negz.jpg"]}
/>
</T.MeshStandardMaterial>
</T.Mesh>
<Entity
from={THREE.Mesh}
highlight="red"
position={new THREE.Vector3()}
plugins={[MaterialPlugin] as const}
/>
{/* Camera with shake (from ShakePlugin) */}
<T.PerspectiveCamera ref={cameraRef!} position={[10, 10, 10]} shake={0.05} />

<T.DirectionalLight position={[5, 5, 5]} intensity={1} />
<T.AmbientLight intensity={0.5} />
</Canvas>
)
}
4 changes: 2 additions & 2 deletions playground/examples/PortalExample.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as THREE from "three"
import { Canvas, createT, Entity, Portal } from "../../src/index.ts"
import { createT, Entity, EventPlugin, Portal } from "../../src/index.ts"

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

export function PortalExample() {
const group = new THREE.Group()
Expand Down
Loading