@octanejs/three 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +49 -0
- package/README.md +215 -0
- package/UPSTREAM.md +58 -0
- package/package.json +77 -0
- package/src/config.ts +50 -0
- package/src/core/attach.ts +117 -0
- package/src/core/catalogue.ts +245 -0
- package/src/core/driver.ts +1609 -0
- package/src/core/events.ts +539 -0
- package/src/core/hooks.ts +203 -0
- package/src/core/index.ts +86 -0
- package/src/core/loader.ts +210 -0
- package/src/core/loop.ts +210 -0
- package/src/core/portal.ts +236 -0
- package/src/core/props.ts +645 -0
- package/src/core/root.ts +652 -0
- package/src/core/store.ts +741 -0
- package/src/index.ts +86 -0
- package/src/intrinsics.ts +18 -0
- package/src/renderer.ts +12 -0
- package/src/testing.ts +259 -0
- package/src/web/Canvas.tsrx +295 -0
- package/src/web/Canvas.tsrx.d.ts +23 -0
- package/src/web/events.ts +79 -0
- package/src/web/measure.ts +96 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { flushSync } from 'octane';
|
|
2
|
+
import type { RootState, RootStore } from '../core/store.js';
|
|
3
|
+
import {
|
|
4
|
+
createEvents,
|
|
5
|
+
getThreeEventPriority,
|
|
6
|
+
type DomEvent,
|
|
7
|
+
type EventManager,
|
|
8
|
+
type Events,
|
|
9
|
+
} from '../core/events.js';
|
|
10
|
+
|
|
11
|
+
const DOM_EVENTS = {
|
|
12
|
+
onClick: ['click', false],
|
|
13
|
+
onContextMenu: ['contextmenu', false],
|
|
14
|
+
onDoubleClick: ['dblclick', false],
|
|
15
|
+
onWheel: ['wheel', true],
|
|
16
|
+
onPointerDown: ['pointerdown', true],
|
|
17
|
+
onPointerUp: ['pointerup', true],
|
|
18
|
+
onPointerLeave: ['pointerleave', true],
|
|
19
|
+
onPointerMove: ['pointermove', true],
|
|
20
|
+
onPointerCancel: ['pointercancel', true],
|
|
21
|
+
onLostPointerCapture: ['lostpointercapture', true],
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/** Default R3F-compatible event manager for web canvases. */
|
|
25
|
+
export function createPointerEvents(store: RootStore): EventManager<HTMLElement> {
|
|
26
|
+
const { handlePointer } = createEvents(store);
|
|
27
|
+
const handlers = Object.fromEntries(
|
|
28
|
+
(Object.keys(DOM_EVENTS) as Array<keyof typeof DOM_EVENTS>).map((name) => {
|
|
29
|
+
const handleEvent = handlePointer(name);
|
|
30
|
+
return [
|
|
31
|
+
name,
|
|
32
|
+
getThreeEventPriority(name) === 'discrete'
|
|
33
|
+
? (event: Event) => flushSync(() => handleEvent(event))
|
|
34
|
+
: handleEvent,
|
|
35
|
+
];
|
|
36
|
+
}),
|
|
37
|
+
) as unknown as Events;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
priority: 1,
|
|
41
|
+
enabled: true,
|
|
42
|
+
compute(event: DomEvent, state: RootState) {
|
|
43
|
+
state.pointer.set(
|
|
44
|
+
(event.offsetX / state.size.width) * 2 - 1,
|
|
45
|
+
-(event.offsetY / state.size.height) * 2 + 1,
|
|
46
|
+
);
|
|
47
|
+
state.raycaster.setFromCamera(state.pointer, state.camera);
|
|
48
|
+
},
|
|
49
|
+
connected: undefined,
|
|
50
|
+
handlers,
|
|
51
|
+
update() {
|
|
52
|
+
const { events, internal } = store.getState();
|
|
53
|
+
if (internal.lastEvent.current !== null && events.handlers !== undefined) {
|
|
54
|
+
events.handlers.onPointerMove(internal.lastEvent.current);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
connect(target) {
|
|
58
|
+
const { events, set } = store.getState();
|
|
59
|
+
events.disconnect?.();
|
|
60
|
+
set((state) => ({ events: { ...state.events, connected: target } }));
|
|
61
|
+
if (events.handlers === undefined) return;
|
|
62
|
+
for (const name of Object.keys(DOM_EVENTS) as Array<keyof typeof DOM_EVENTS>) {
|
|
63
|
+
const [eventName, passive] = DOM_EVENTS[name];
|
|
64
|
+
target.addEventListener(eventName, events.handlers[name], { passive });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
disconnect() {
|
|
68
|
+
const { events, set } = store.getState();
|
|
69
|
+
if (!events.connected) return;
|
|
70
|
+
if (events.handlers !== undefined) {
|
|
71
|
+
for (const name of Object.keys(DOM_EVENTS) as Array<keyof typeof DOM_EVENTS>) {
|
|
72
|
+
const [eventName] = DOM_EVENTS[name];
|
|
73
|
+
events.connected.removeEventListener(eventName, events.handlers[name]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
set((state) => ({ events: { ...state.events, connected: undefined } }));
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Size } from '../core/store.js';
|
|
2
|
+
|
|
3
|
+
export interface ResizeOptions {
|
|
4
|
+
debounce?: number | { resize: number; scroll: number };
|
|
5
|
+
scroll?: boolean;
|
|
6
|
+
polyfill?: ResizeObserverConstructor;
|
|
7
|
+
offsetSize?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResizeObserverConstructor {
|
|
11
|
+
new (callback: ResizeObserverCallback): ResizeObserver;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function debounceDelay(options: ResizeOptions, kind: 'resize' | 'scroll'): number {
|
|
15
|
+
const configured = options.debounce;
|
|
16
|
+
if (typeof configured === 'number') return configured;
|
|
17
|
+
return configured?.[kind] ?? 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function measuredSize(
|
|
21
|
+
element: HTMLElement,
|
|
22
|
+
entry: ResizeObserverEntry | undefined,
|
|
23
|
+
offsetSize: boolean,
|
|
24
|
+
): Size {
|
|
25
|
+
const bounds = element.getBoundingClientRect();
|
|
26
|
+
let width = bounds.width;
|
|
27
|
+
let height = bounds.height;
|
|
28
|
+
if (offsetSize) {
|
|
29
|
+
width = element.offsetWidth;
|
|
30
|
+
height = element.offsetHeight;
|
|
31
|
+
} else if (entry !== undefined) {
|
|
32
|
+
// ResizeObserver callbacks can arrive before layout APIs expose their new
|
|
33
|
+
// values (and controlled test/browser shims commonly behave this way).
|
|
34
|
+
// Prefer the element rect, but retain the observer's positive dimensions.
|
|
35
|
+
if (width <= 0 && entry.contentRect.width > 0) width = entry.contentRect.width;
|
|
36
|
+
if (height <= 0 && entry.contentRect.height > 0) height = entry.contentRect.height;
|
|
37
|
+
}
|
|
38
|
+
return { width, height, top: bounds.top, left: bounds.left };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Observe the DOM box used to configure a Three root.
|
|
43
|
+
*
|
|
44
|
+
* The first rect is delivered synchronously; a zero-sized shell remains
|
|
45
|
+
* dormant until ResizeObserver reports a positive layout.
|
|
46
|
+
*/
|
|
47
|
+
export function observeCanvasSize(
|
|
48
|
+
element: HTMLElement,
|
|
49
|
+
options: ResizeOptions,
|
|
50
|
+
onSize: (size: Size) => void,
|
|
51
|
+
): () => void {
|
|
52
|
+
let disposed = false;
|
|
53
|
+
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
let scrollTimer: ReturnType<typeof setTimeout> | undefined;
|
|
55
|
+
const deliver = (entry?: ResizeObserverEntry) => {
|
|
56
|
+
if (!disposed) onSize(measuredSize(element, entry, options.offsetSize === true));
|
|
57
|
+
};
|
|
58
|
+
const schedule = (kind: 'resize' | 'scroll', entry?: ResizeObserverEntry) => {
|
|
59
|
+
const delay = debounceDelay(options, kind);
|
|
60
|
+
if (delay <= 0) {
|
|
61
|
+
deliver(entry);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (kind === 'resize') {
|
|
65
|
+
if (resizeTimer !== undefined) clearTimeout(resizeTimer);
|
|
66
|
+
resizeTimer = setTimeout(() => deliver(entry), delay);
|
|
67
|
+
} else {
|
|
68
|
+
if (scrollTimer !== undefined) clearTimeout(scrollTimer);
|
|
69
|
+
scrollTimer = setTimeout(() => deliver(entry), delay);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const Observer = options.polyfill ?? globalThis.ResizeObserver;
|
|
74
|
+
const observer =
|
|
75
|
+
typeof Observer === 'function'
|
|
76
|
+
? new Observer((entries) => schedule('resize', entries[0]))
|
|
77
|
+
: undefined;
|
|
78
|
+
observer?.observe(element);
|
|
79
|
+
|
|
80
|
+
const handleScroll = () => schedule('scroll');
|
|
81
|
+
if (options.scroll === true && typeof window !== 'undefined') {
|
|
82
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
83
|
+
}
|
|
84
|
+
deliver();
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
if (disposed) return;
|
|
88
|
+
disposed = true;
|
|
89
|
+
if (resizeTimer !== undefined) clearTimeout(resizeTimer);
|
|
90
|
+
if (scrollTimer !== undefined) clearTimeout(scrollTimer);
|
|
91
|
+
observer?.disconnect();
|
|
92
|
+
if (options.scroll === true && typeof window !== 'undefined') {
|
|
93
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|