packages/core/src/lib/manipulation/frame.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.