Skip to content

Commit 5449e5a

Browse files
authored
Fix TypeScript errors, get test suite running, and fix scene graph ordering [next]
Fix TypeScript errors, get test suite running, and fix scene graph ordering [next]
2 parents 3e6a661 + e8312ac commit 5449e5a

18 files changed

Lines changed: 185 additions & 99 deletions

File tree

.github/workflows/pkg-pr-new.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ jobs:
2626
- name: Install dependencies
2727
run: pnpm install
2828

29+
- name: Typecheck
30+
run: pnpm lint:types
31+
32+
- name: Test
33+
run: pnpm test --run
34+
2935
- name: Build
3036
run: pnpm build
3137

playground/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="vite/client" />
12
import { A, Route, Router } from "@solidjs/router"
23
import { createSignal, For, lazy, type ParentProps } from "solid-js"
34
import * as THREE from "three"
@@ -171,6 +172,5 @@ export function App() {
171172
/>
172173
</Router>
173174
)
174-
console.log(router.toArray())
175175
return router
176176
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { splitProps } from "solid-js"
2-
import { defaultProps } from "./default-props.ts"
1+
import { mergeProps, type MergeProps, splitProps } from "solid-js"
32
import type { KeyOfOptionals } from "./type-utils.ts"
43

54
export function processProps<
65
const TProps,
7-
const TDefaults extends Required<Pick<TProps, KeyOfOptionals<TProps>>>,
8-
const TSplit extends readonly (keyof TProps)[],
6+
const TDefaults extends Partial<Pick<TProps, KeyOfOptionals<TProps>>>,
7+
const TSplit extends readonly (keyof MergeProps<[TDefaults, TProps]>)[],
98
>(props: TProps, defaults: TDefaults, split?: TSplit) {
10-
return splitProps(defaultProps(props, defaults), split ?? [])
9+
const merged = mergeProps(defaults, props)
10+
return splitProps(merged, (split ?? []) as readonly (keyof typeof merged)[])
1111
}

playground/src/api/canvas/usage.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,20 @@ export default function () {
6363
fov: 75,
6464
}}
6565
fallback={<div style={{ color: "white", padding: "20px" }}>Loading Canvas...</div>}
66-
gl={{
67-
antialias: true,
68-
alpha: true,
69-
powerPreference: "high-performance",
70-
}}
66+
gl={canvas =>
67+
new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" })
68+
}
7169
scene={{
7270
background: new THREE.Color(0x202020),
7371
fog: new THREE.Fog(0x202020, 10, 50),
7472
}}
7573
defaultRaycaster={{
7674
params: {
75+
Mesh: {},
7776
Line: { threshold: 0.1 },
77+
LOD: {},
7878
Points: { threshold: 0.1 },
79+
Sprite: {},
7980
},
8081
}}
8182
shadows={shadows()}

playground/src/api/use-loader/single-texture.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,7 @@ function SkyboxSphere() {
1515
"https://threejs.org/examples/textures/cube/SwedishRoyalCastle/ny.jpg", // negative y
1616
"https://threejs.org/examples/textures/cube/SwedishRoyalCastle/pz.jpg", // positive z
1717
"https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nz.jpg", // negative z
18-
],
19-
// CubeTextureLoader properties
20-
{
21-
mapping: THREE.CubeReflectionMapping,
22-
wrapS: THREE.ClampToEdgeWrapping,
23-
wrapT: THREE.ClampToEdgeWrapping,
24-
magFilter: THREE.LinearFilter,
25-
minFilter: THREE.LinearMipmapLinearFilter,
26-
},
18+
] as string[],
2719
)
2820

2921
return (

src/components.tsx

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { whenMemo } from "@bigmistqke/solid-whenever"
21
import {
32
Show,
4-
createEffect,
53
createMemo,
64
mergeProps,
75
splitProps,
@@ -94,22 +92,18 @@ type EntityProps<T extends object | Constructor<object>> = Overwrite<
9492
*/
9593
export function Entity<T extends object | Constructor<object>>(props: EntityProps<T>) {
9694
const [config, rest] = splitProps(props, ["from", "args"])
97-
const memo = whenMemo(
98-
() => config.from,
99-
from => {
100-
// listen to key changes
101-
props.key
102-
const instance = meta(
103-
isConstructor(from) ? autodispose(new from(...(config.args ?? []))) : from,
104-
{
105-
props,
106-
},
107-
) as Meta<T>
108-
useProps(instance, rest)
109-
return instance
110-
},
111-
)
112-
return memo as unknown as JSX.Element
95+
const instance = createMemo(() => {
96+
const from = config.from
97+
if (!from) return undefined
98+
// track key changes to force reconstruction
99+
props.key
100+
return meta(
101+
isConstructor(from) ? autodispose(new from(...(config.args ?? []))) : from,
102+
{ props },
103+
) as Meta<T>
104+
})
105+
useProps(instance, rest)
106+
return instance as unknown as JSX.Element
113107
}
114108

115109
/**********************************************************************************/
@@ -194,12 +188,10 @@ export function Resource<const TLoader extends Loader<object, any>>(props: Resou
194188
options,
195189
)
196190

197-
createEffect(() => console.log("resource", resource()))
198-
199191
useProps(resource, rest)
200192

201193
return (
202-
<Show when={"children" in config && resource()} fallback={resource()}>
194+
<Show when={"children" in config && resource()} fallback={resource() as JSX.Element}>
203195
{resource => props.children?.(resource)}
204196
</Show>
205197
)

src/create-events.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const isEventType = (type: string): type is EventName =>
5959
function createThreeEvent<
6060
TEvent extends Event,
6161
TConfig extends { stoppable?: boolean; intersections?: Array<Intersection> },
62-
>(nativeEvent: TEvent, { stoppable = true, intersections }: TConfig = {}) {
62+
>(nativeEvent: TEvent, { stoppable = true, intersections }: TConfig = {} as TConfig) {
6363
const event: Record<string, any> = stoppable
6464
? {
6565
nativeEvent,
@@ -128,7 +128,7 @@ function raycast<TNativeEvent extends MouseEvent | WheelEvent>(
128128
stack.push(...object.children)
129129
}
130130

131-
return context.raycaster.intersectObjects(nodeSet.values().toArray(), false)
131+
return context.raycaster.intersectObjects(Array.from(nodeSet), false)
132132
}
133133

134134
/**********************************************************************************/
@@ -323,10 +323,11 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
323323

324324
// Handle leave-event
325325
const leaveEvent = createThreeEvent(nativeEvent, { intersections, stoppable: false })
326-
const leaveSet = hoveredSet.difference(enterSet)
326+
const prevHoveredSet = hoveredSet
327327
hoveredSet = enterSet
328328

329-
for (const object of leaveSet.values()) {
329+
for (const object of prevHoveredSet) {
330+
if (enterSet.has(object)) continue
330331
getMeta(object)?.props[`on${type}Leave`]?.(
331332
// @ts-expect-error TODO: fix type-error
332333
leaveEvent,
@@ -388,7 +389,7 @@ function createDefaultEventRegistry(
388389
let node: Object3D | null = intersection.object
389390

390391
while (node && !event.stopped) {
391-
getMeta(intersection.object)?.props[type]?.(
392+
getMeta(node)?.props[type]?.(
392393
// @ts-expect-error TODO: fix type-error
393394
event,
394395
)

src/data-structure/stack.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ export class Stack<T = any> {
3838
array.push(value)
3939
return array
4040
})
41-
// @ts-expect-error TODO: fix type-error
42-
if (import.meta.env?.MODE === "development") {
41+
if (process.env.NODE_ENV === "development") {
4342
const array = untrack(this.#array.bind(this))
4443
if (array.length > 2) {
4544
// TODO: write better warning message

src/hooks.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export function useLoader<
216216
input: TInput,
217217
): PromiseMaybe<LoadOutput<TLoader, TInput>> {
218218
if (isRecord(input)) {
219-
return awaitMapObject(input, value => getOrInsert(registry, loader, value)) as Promise<
219+
return awaitMapObject(input, async value => getOrInsert(registry, loader, value)) as PromiseMaybe<
220220
LoadOutput<TLoader, TInput>
221221
>
222222
} else {
@@ -225,19 +225,19 @@ export function useLoader<
225225
const cachedPromise = registry.get(loader, _input, false)
226226

227227
if (cachedPromise) {
228-
return cachedPromise
228+
return cachedPromise as PromiseMaybe<LoadOutput<TLoader, TInput>>
229229
}
230230

231-
const promise = load(loader, input)
232-
registry.set(loader, input, promise)
231+
const promise = load(loader, _input)
232+
registry.set(loader, _input, promise)
233233

234234
return promise as Promise<LoadOutput<TLoader, TInput>>
235235
}
236236
}
237237

238238
function loadUrl<TInput extends LoadInput<TLoader>>(
239239
url: TInput,
240-
): Promise<LoadOutput<TLoader, TInput>> {
240+
): PromiseMaybe<LoadOutput<TLoader, TInput>> {
241241
if (config.cache === true) {
242242
if (!useLoader.cache) {
243243
return load(loader(), url)

src/props.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,30 @@ function isWritable(object: object, propertyName: string) {
3232
function applySceneGraph(parent: object, child: object) {
3333
const parentMeta = getMeta(parent)
3434
if (parentMeta) {
35-
// Update parent's augmented children-property.
3635
parentMeta.children.add(child)
3736
onCleanup(() => parentMeta.children.delete(child))
3837
}
3938

4039
const childMeta = getMeta(child)
4140
if (childMeta) {
42-
// Update parent's augmented children-property.
4341
childMeta.parent = parent
4442
onCleanup(() => (childMeta.parent = undefined))
4543
}
4644

4745
let attachProp = childMeta?.props.attach
4846

49-
// Attach-prop can be a callback. It returns a cleanup-function.
5047
if (typeof attachProp === "function") {
51-
const cleanup = attachProp(parent, child as Meta)
48+
const cleanup = attachProp(parent, child as Meta<object>)
5249
onCleanup(cleanup)
5350
return
5451
}
5552

56-
// Defaults for Material, BufferGeometry and Fog.
5753
if (!attachProp) {
5854
if (child instanceof Material) attachProp = "material"
5955
else if (child instanceof BufferGeometry) attachProp = "geometry"
6056
else if (child instanceof Fog) attachProp = "fog"
6157
}
6258

63-
// If an attachProp is defined, attach the child to the parent.
6459
if (attachProp) {
6560
let target = parent
6661
let property: string | undefined
@@ -83,12 +78,8 @@ function applySceneGraph(parent: object, child: object) {
8378
return
8479
}
8580

86-
// If no attach-prop is defined, add the child to the parent.
87-
if (child instanceof Object3D && parent instanceof Object3D && !parent.children.includes(child)) {
88-
parent.add(child)
89-
onCleanup(() => parent.remove(child))
90-
return child
91-
}
81+
// Object3D children are managed by the ordering loop in useSceneGraph
82+
if (child instanceof Object3D && parent instanceof Object3D) return
9283

9384
console.error(
9485
"Error while connecting/attaching child: child does not have attach-props defined and is not an Object3D",
@@ -119,6 +110,8 @@ export const useSceneGraph = <T extends object>(
119110
props: { children?: JSXElement | JSXElement[]; onUpdate?(event: T): void },
120111
) => {
121112
const c = children(() => props.children)
113+
114+
// Per-item: metadata, attach props, events
122115
createComputed(
123116
mapArray(
124117
() => c.toArray() as unknown as (Meta<object> | undefined)[],
@@ -133,6 +126,48 @@ export const useSceneGraph = <T extends object>(
133126
}),
134127
),
135128
)
129+
130+
// Object3D scene graph sync: add, remove, reorder
131+
createComputed((previousManagedChildren: Set<Object3D>) => {
132+
const parent = resolve(_parent)
133+
if (!(parent instanceof Object3D)) {
134+
return previousManagedChildren
135+
}
136+
137+
const childArray = c.toArray() as unknown as Array<object | undefined>
138+
const managedChildren = new Set<Object3D>()
139+
140+
for (const child of childArray) {
141+
if (!(child instanceof Object3D) || getMeta(child)?.props.attach) continue
142+
managedChildren.add(child)
143+
if (child.parent !== parent) {
144+
parent.add(child)
145+
}
146+
}
147+
148+
for (const child of previousManagedChildren) {
149+
if (!managedChildren.has(child)) {
150+
parent.remove(child)
151+
}
152+
}
153+
154+
// Reorder: walk parent.children, assign desired order at managed slots
155+
let childArrayIndex = 0
156+
for (let i = 0; i < parent.children.length; i++) {
157+
if (!managedChildren.has(parent.children[i]!)) {
158+
continue
159+
}
160+
while (childArrayIndex < childArray.length) {
161+
const child = childArray[childArrayIndex++]
162+
if (child instanceof Object3D && !getMeta(child)?.props.attach) {
163+
parent.children[i] = child
164+
break
165+
}
166+
}
167+
}
168+
169+
return managedChildren
170+
}, new Set<Object3D>())
136171
}
137172

138173
/**********************************************************************************/

0 commit comments

Comments
 (0)