@octanejs/three 0.1.0 → 0.1.2

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/src/index.ts CHANGED
@@ -26,22 +26,39 @@ export {
26
26
  export type {
27
27
  Args,
28
28
  Attach,
29
+ AttachFnType,
30
+ AttachType,
29
31
  Catalogue,
32
+ Color,
30
33
  ConstructorRepresentation,
34
+ Disposable,
35
+ ElementProps,
36
+ Euler,
31
37
  EventProps,
32
38
  Instance,
33
39
  InstanceProps,
40
+ Layers,
34
41
  MathRepresentation,
42
+ MathProps,
35
43
  MathType,
36
44
  MathTypes,
45
+ Matrix3,
46
+ Matrix4,
37
47
  PrimitiveProps,
48
+ Quaternion,
38
49
  RaycastableRepresentation,
50
+ ReactProps,
51
+ ReconcilerRoot,
39
52
  ThreeElement,
40
53
  ThreeElements,
41
54
  ThreeInstanceProps,
42
55
  ThreeKey,
43
56
  ThreeRef,
44
57
  ThreeToJSXElements,
58
+ Vector2,
59
+ Vector3,
60
+ Vector4,
61
+ VectorRepresentation,
45
62
  } from './core/index.js';
46
63
  export type {
47
64
  Camera,
@@ -81,6 +98,14 @@ export type {
81
98
  Viewport,
82
99
  XRManager,
83
100
  } from './core/index.js';
101
+ export { act, flushSync } from './scheduling.js';
102
+ export type { Act } from './scheduling.js';
84
103
  export { Canvas } from './web/Canvas.tsrx';
85
104
  export type { CanvasProps, CanvasRef, CanvasStyle } from './web/Canvas.tsrx';
105
+ export { DOMRegion } from './web/DOMRegion.js';
106
+ export type { DOMRegionProps, DOMRegionTarget } from './web/DOMRegion.js';
86
107
  export { createPointerEvents as events } from './web/events.js';
108
+ /** Canonical namespace for renderer-local Three element and math types. */
109
+ export * as OctaneThree from './intrinsics.js';
110
+ /** R3F's historical namespace name retained for source compatibility. */
111
+ export * as ReactThreeFiber from './intrinsics.js';
package/src/intrinsics.ts CHANGED
@@ -3,13 +3,33 @@ import type { ThreeElements as ThreeIntrinsicElements } from './core/catalogue.j
3
3
  export type {
4
4
  Args,
5
5
  Attach,
6
+ AttachFnType,
7
+ AttachType,
8
+ Color,
6
9
  ConstructorRepresentation,
10
+ ElementProps,
11
+ Euler,
12
+ EventProps,
13
+ Layers,
14
+ MathProps,
15
+ MathRepresentation,
16
+ MathType,
17
+ MathTypes,
18
+ Matrix3,
19
+ Matrix4,
7
20
  PrimitiveProps,
21
+ Quaternion,
22
+ RaycastableRepresentation,
23
+ ReactProps,
8
24
  ThreeElement,
9
25
  ThreeElements,
10
26
  ThreeKey,
11
27
  ThreeRef,
12
28
  ThreeToJSXElements,
29
+ Vector2,
30
+ Vector3,
31
+ Vector4,
32
+ VectorRepresentation,
13
33
  } from './core/catalogue.js';
14
34
 
15
35
  /** TypeScript resolves this namespace through the intrinsic jsx-runtime export. */
@@ -0,0 +1,83 @@
1
+ import * as Octane from 'octane';
2
+ import { flushUniversalAct, flushUniversalSync, type UniversalSyncFlusher } from 'octane/universal';
3
+
4
+ export type Act = typeof import('octane').act;
5
+ type FlushSync = typeof import('octane').flushSync;
6
+ const NO_ACT_ERROR = Symbol('octane.three.no-act-error');
7
+
8
+ function isThenable<T>(value: T | Promise<T>): value is Promise<T> {
9
+ return (
10
+ value !== null &&
11
+ (typeof value === 'object' || typeof value === 'function') &&
12
+ typeof (value as PromiseLike<T>).then === 'function'
13
+ );
14
+ }
15
+
16
+ function runActCallback<T>(
17
+ callback: () => T | Promise<T>,
18
+ flushOwner?: UniversalSyncFlusher,
19
+ ): T | Promise<T> {
20
+ const result = flushUniversalAct(callback, flushOwner);
21
+ if (!isThenable(result)) return result;
22
+ return Promise.resolve(result).then((value) => flushUniversalAct(() => value, flushOwner));
23
+ }
24
+
25
+ function runClientAct<T>(callback: () => T | Promise<T>, clientAct: Act): Promise<T> {
26
+ const completions: Promise<unknown>[] = [];
27
+ const flushOwner: UniversalSyncFlusher = <Value>(run: () => Value): Value => {
28
+ let value!: Value;
29
+ let error: unknown = NO_ACT_ERROR;
30
+ const completion = clientAct(() => {
31
+ try {
32
+ value = run();
33
+ } catch (caught) {
34
+ error = caught;
35
+ }
36
+ });
37
+ completions.push(completion);
38
+ if (error !== NO_ACT_ERROR) throw error;
39
+ return value;
40
+ };
41
+
42
+ let result: T | Promise<T>;
43
+ try {
44
+ // Each universal wave executes inside a full DOM act phase, including DOM
45
+ // passive effects. This closes DOMRegion cascades in both directions.
46
+ result = flushUniversalAct(callback, flushOwner);
47
+ } catch (error) {
48
+ return Promise.allSettled(completions.splice(0)).then(() => {
49
+ throw error;
50
+ });
51
+ }
52
+
53
+ return (async () => {
54
+ try {
55
+ const value = await result;
56
+ await Promise.all(completions.splice(0));
57
+ const settled = flushUniversalAct(() => value, flushOwner);
58
+ await Promise.all(completions.splice(0));
59
+ return settled;
60
+ } catch (error) {
61
+ await Promise.allSettled(completions.splice(0));
62
+ throw error;
63
+ }
64
+ })();
65
+ }
66
+
67
+ /** Flushes both the DOM scheduler and mounted universal Three roots. */
68
+ export const flushSync: FlushSync = (callback) => {
69
+ const clientFlushSync = Reflect.get(Octane, 'flushSync') as FlushSync | undefined;
70
+ return flushUniversalSync(callback, clientFlushSync);
71
+ };
72
+
73
+ /** R3F-compatible test helper backed by Octane's client scheduler when available. */
74
+ export const act: Act = (callback) => {
75
+ const clientAct = Reflect.get(Octane, 'act') as Act | undefined;
76
+ const clientFlushSync = Reflect.get(Octane, 'flushSync') as FlushSync | undefined;
77
+ if (typeof clientAct === 'function') return runClientAct(callback, clientAct);
78
+ try {
79
+ return Promise.resolve(runActCallback(callback, clientFlushSync));
80
+ } catch (error) {
81
+ return Promise.reject(error);
82
+ }
83
+ };
@@ -0,0 +1,58 @@
1
+ import {
2
+ defineUniversalComponent,
3
+ universalPlan,
4
+ universalProps,
5
+ universalValue,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ type RendererRegion,
9
+ } from 'octane/universal';
10
+ import { Object3D } from 'three';
11
+ import { createDOMRegionBinding, type DOMRegionTarget } from './dom-region.js';
12
+
13
+ export type { DOMRegionTarget } from './dom-region.js';
14
+
15
+ export interface DOMRegionProps {
16
+ /** Explicit DOM parent for the region's one owned child container. */
17
+ target: DOMRegionTarget;
18
+ /** Compiler-owned DOM renderer region. Application JSX is lowered into this payload. */
19
+ children?: RendererRegion;
20
+ }
21
+
22
+ class DOMRegionSentinel extends Object3D {
23
+ declare region?: RendererRegion;
24
+ }
25
+
26
+ const DOM_REGION_PLAN = universalPlan('three', {
27
+ kind: 'host',
28
+ type: 'primitive',
29
+ propsSlot: 0,
30
+ });
31
+ const DOM_REGION_BINDING = Symbol('octane.three.dom-region.binding');
32
+ const DOM_REGION_SENTINEL = Symbol('octane.three.dom-region.sentinel');
33
+ const DOM_REGION_LIFETIME = Symbol('octane.three.dom-region.lifetime');
34
+ const DOM_REGION_COMMIT = Symbol('octane.three.dom-region.commit');
35
+
36
+ /**
37
+ * Low-level Three-to-DOM renderer boundary.
38
+ *
39
+ * DOMRegion deliberately provides no positioning, transforms, occlusion, or styling.
40
+ */
41
+ export const DOMRegion = defineUniversalComponent<DOMRegionProps>('three', (props) => {
42
+ const binding = useMemo(() => createDOMRegionBinding(), [], DOM_REGION_BINDING);
43
+ const sentinel = useMemo(() => new DOMRegionSentinel(), [], DOM_REGION_SENTINEL);
44
+ useLayoutEffect(() => binding.attach(), [binding], DOM_REGION_LIFETIME);
45
+ useLayoutEffect(
46
+ () => binding.commit(props.target, props.children),
47
+ [binding, props.target, props.children],
48
+ DOM_REGION_COMMIT,
49
+ );
50
+
51
+ return universalValue(DOM_REGION_PLAN, [
52
+ universalProps([
53
+ ['set', 'object', sentinel],
54
+ ['set', 'dispose', null],
55
+ ['set', 'region', props.children],
56
+ ]),
57
+ ]);
58
+ });
@@ -0,0 +1,104 @@
1
+ import * as Octane from 'octane';
2
+ import type { ComponentBody, Root } from 'octane';
3
+ import { isRendererRegion, type RendererRegion } from 'octane/universal';
4
+
5
+ export type DOMRegionTarget = HTMLElement | { current: HTMLElement | null };
6
+
7
+ function isHTMLElement(value: unknown): value is HTMLElement {
8
+ if (value === null || typeof value !== 'object') return false;
9
+ const view = (value as Node).ownerDocument?.defaultView;
10
+ return view !== null && view !== undefined && value instanceof view.HTMLElement;
11
+ }
12
+
13
+ function resolveTarget(target: DOMRegionTarget): HTMLElement | null {
14
+ if (target === null || typeof target !== 'object') {
15
+ throw new TypeError('@octanejs/three: DOMRegion target must be an HTMLElement or ref.');
16
+ }
17
+ const resolved = 'current' in target ? target.current : target;
18
+ if (resolved !== null && !isHTMLElement(resolved)) {
19
+ throw new TypeError('@octanejs/three: DOMRegion target ref must contain an HTMLElement.');
20
+ }
21
+ return resolved;
22
+ }
23
+
24
+ /** Package-private owner for the one DOM root materialized by a DOMRegion. */
25
+ export interface DOMRegionBinding {
26
+ attach(): () => void;
27
+ commit(target: DOMRegionTarget, region: RendererRegion | undefined): void;
28
+ }
29
+
30
+ export function createDOMRegionBinding(): DOMRegionBinding {
31
+ const createRoot = Reflect.get(Octane, 'createRoot') as
32
+ | typeof import('octane').createRoot
33
+ | undefined;
34
+ if (typeof createRoot !== 'function' || typeof document === 'undefined') {
35
+ throw new Error('@octanejs/three: DOMRegion is client-only and requires a DOM root.');
36
+ }
37
+ const host = document.createElement('div');
38
+ let attached = false;
39
+ let root: Root | null = null;
40
+
41
+ const unmount = () => {
42
+ const current = root;
43
+ try {
44
+ if (current !== null) current.unmount();
45
+ } finally {
46
+ host.remove();
47
+ }
48
+ };
49
+
50
+ const createTrackedRoot = (): Root => {
51
+ const next = createRoot(host);
52
+ const rootUnmount = next.unmount.bind(next);
53
+ let live = true;
54
+ // The reverse-owner bridge may unmount this root before the Three sentinel
55
+ // itself is deleted. Track that public teardown so the next accepted commit
56
+ // can create a fresh root instead of updating an already-unmounted one.
57
+ next.unmount = () => {
58
+ if (!live) return;
59
+ live = false;
60
+ const wasCurrent = root === next;
61
+ if (wasCurrent) root = null;
62
+ try {
63
+ rootUnmount();
64
+ } finally {
65
+ if (wasCurrent) host.remove();
66
+ }
67
+ };
68
+ return next;
69
+ };
70
+
71
+ return {
72
+ attach() {
73
+ attached = true;
74
+ let active = true;
75
+ return () => {
76
+ if (!active) return;
77
+ active = false;
78
+ attached = false;
79
+ unmount();
80
+ };
81
+ },
82
+ commit(target, region) {
83
+ if (!attached) return;
84
+ const resolvedTarget = resolveTarget(target);
85
+ if (resolvedTarget === null || region === undefined) {
86
+ unmount();
87
+ return;
88
+ }
89
+ if (
90
+ !isRendererRegion(region) ||
91
+ region.ownerRenderer !== 'three' ||
92
+ region.childRenderer !== 'dom'
93
+ ) {
94
+ throw new TypeError(
95
+ '@octanejs/three: DOMRegion children must be a compiler-owned Three-to-DOM region.',
96
+ );
97
+ }
98
+
99
+ if (root === null) root = createTrackedRoot();
100
+ if (host.parentElement !== resolvedTarget) resolvedTarget.append(host);
101
+ root.render(region.component as ComponentBody, region.props);
102
+ },
103
+ };
104
+ }