Skip to content

packages/core/src/lib/manipulation/frame.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import type { PibblManipulationFrameProps } from './controller-types.js';
2 import { createElement } from "../element/create-element.js";
3 import { Group } from "../components/g.js";
4 import { definePrimitive } from "../define-primitive.js";
5 import { GLOBAL_STATE } from "../global-state.js";
6 import { useHookSlot, requireHookContext } from "../hooks/hook-slot.js";
7 import { pushLayoutBox } from "../layout/context.js";
8 import { getCurrentWorldTransform } from "../render-target.js";
9 import { currentRootCoordinateMapping } from "./coordinates.js";
10 import type { PibblNode } from "../element/types.js";
11 import type { PibblComponentRefs } from "../types.js";
12 import type { SystemStyle } from "../types.js";
13 import type { PibblManipulationBox } from "./types.js";
14 import type { PibblManipulationController } from "./controller-types.js";
15 import {
16   manipulationState,
17   detachManipulationFrame,
18   type ManipulationState,
19 } from "./controller.js";
20 
21 const frames: ManipulationState[] = [];
22 export function requireManipulationFrame(
23   controller: PibblManipulationController,
24 ): ManipulationState {
25   const state = manipulationState(controller);
26   if (frames.at(-1) !== state)
27     throw new Error(
28       "Pibbl manipulation handles must be descendants of their controller frame.",
29     );
30   return state;
31 }
32 interface FrameProps {
33   readonly controller: PibblManipulationController;
34   readonly children?: PibblNode;
35 }
36 interface FrameStyle extends SystemStyle, PibblManipulationBox {}
37 interface FrameAttachment {
38   state: ManipulationState;
39 }
40 
41 function attachManipulationFrame(
42   state: ManipulationState,
43   componentRefs: PibblComponentRefs<any>,
44 ): void {
45   if (state.frame && state.frame !== componentRefs) {
46     if (GLOBAL_STATE.seenIds?.has(state.frame.id))
47       throw new Error(
48         "A manipulation controller can attach to only one frame.",
49       );
50     detachManipulationFrame(state);
51   }
52   state.frame = componentRefs;
53 }
54 
55 function releaseManipulationFrame(
56   state: ManipulationState,
57   componentRefs: PibblComponentRefs<any>,
58 ): void {
59   if (state.frame !== componentRefs) return;
60   state.frame = undefined;
61   detachManipulationFrame(state);
62 }
63 
64 /**
65  * Provides a manipulation controller and its presentation frame to descendant handles.
66  *
67  * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
68  * props and style types.
69  * @throws When called directly; Pibbl mounts this component through JSX or createElement.
70  *
71  * @see {@link PibblManipulationFrameProps}
72  */
73 export const ManipulationFrame = definePrimitive<FrameProps, FrameStyle>(
74   function ManipulationFrame(props, style) {
75     const state = manipulationState(props.controller);
76     const { componentRefs, pibblInstance } = requireHookContext();
77     if (state.root.eventRoot !== pibblInstance.eventRoot)
78       throw new Error(
79         "A manipulation controller cannot attach across Pibbl roots.",
80       );
81     const slot = useHookSlot<FrameAttachment>("manipulationFrame", (teardowns) => {
82       const attachment: FrameAttachment = { state };
83       attachManipulationFrame(state, componentRefs);
84       teardowns.add(() => releaseManipulationFrame(attachment.state, componentRefs));
85       return attachment;
86     });
87     const attachment = slot.value;
88     if (attachment.state !== state) {
89       releaseManipulationFrame(attachment.state, componentRefs);
90       attachManipulationFrame(state, componentRefs);
91       attachment.state = state;
92     } else {
93       attachManipulationFrame(state, componentRefs);
94     }
95     const box = state.options.box.get();
96     if (
97       ![box.left, box.top, style.width, style.height].every(Number.isFinite) ||
98       style.width <= 0 ||
99       style.height <= 0
100     )
101       throw new Error(
102         "ManipulationFrame requires finite position and positive dimensions.",
103       );
104     const parent = getCurrentWorldTransform().translate(-box.left, -box.top);
105     const transaction = pibblInstance.eventRoot.renderTransaction;
106     if (!transaction)
107       throw new Error(
108         "ManipulationFrame requires an active render transaction.",
109       );
110     transaction.onFinish((success) => {
111       if (success && state.alive && state.frame === componentRefs) {
112         state.parent = parent;
113         state.coordinateMapping = currentRootCoordinateMapping(state.root);
114       }
115     });
116     const releaseLayout = pushLayoutBox(
117       Object.freeze({ x: 0, y: 0, width: style.width, height: style.height }),
118     );
119     frames.push(state);
120     GLOBAL_STATE.componentRefs!.onAfterRender = () => {
121       frames.pop();
122       releaseLayout();
123     };
124     const rotation = state.options.rotation?.get() ?? 0;
125     if (!Number.isFinite(rotation))
126       throw new Error("ManipulationFrame rotation must be finite radians.");
127     return createElement(Group, {
128       style: {
129         rotationDegrees: (rotation * 180) / Math.PI,
130         rotationOrigin: [style.width / 2, style.height / 2],
131       },
132       children: props.children,
133     });
134   },
135   { childInput: "structural" },
136 );
137 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.