@canvas-commons/2d 0.2.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.
Files changed (183) hide show
  1. package/LICENSE +22 -0
  2. package/editor/index.css +40 -0
  3. package/editor/index.js +525 -0
  4. package/editor/index.js.map +1 -0
  5. package/lib/index-OvpZ-GsA.d.ts +5673 -0
  6. package/lib/index-OvpZ-GsA.d.ts.map +1 -0
  7. package/lib/index.d.ts +2 -0
  8. package/lib/index.js +10326 -0
  9. package/lib/index.js.map +1 -0
  10. package/lib/jsx-dev-runtime.d.ts +2 -0
  11. package/lib/jsx-dev-runtime.js +2 -0
  12. package/lib/jsx-runtime.d.ts +2 -0
  13. package/lib/jsx-runtime.js +28 -0
  14. package/lib/jsx-runtime.js.map +1 -0
  15. package/package.json +79 -0
  16. package/src/editor/NodeInspectorConfig.tsx +76 -0
  17. package/src/editor/PreviewOverlayConfig.tsx +65 -0
  18. package/src/editor/Provider.tsx +109 -0
  19. package/src/editor/SceneGraphTabConfig.tsx +87 -0
  20. package/src/editor/icons/CircleIcon.tsx +7 -0
  21. package/src/editor/icons/CodeIcon.tsx +8 -0
  22. package/src/editor/icons/CurveIcon.tsx +7 -0
  23. package/src/editor/icons/GridIcon.tsx +7 -0
  24. package/src/editor/icons/IconMap.ts +35 -0
  25. package/src/editor/icons/ImgIcon.tsx +8 -0
  26. package/src/editor/icons/LayoutIcon.tsx +9 -0
  27. package/src/editor/icons/LineIcon.tsx +7 -0
  28. package/src/editor/icons/NodeIcon.tsx +7 -0
  29. package/src/editor/icons/RayIcon.tsx +7 -0
  30. package/src/editor/icons/RectIcon.tsx +7 -0
  31. package/src/editor/icons/ShapeIcon.tsx +7 -0
  32. package/src/editor/icons/TxtIcon.tsx +8 -0
  33. package/src/editor/icons/VideoIcon.tsx +7 -0
  34. package/src/editor/icons/View2DIcon.tsx +10 -0
  35. package/src/editor/index.css +0 -0
  36. package/src/editor/index.ts +19 -0
  37. package/src/editor/shortcuts.ts +27 -0
  38. package/src/editor/tree/DetachedRoot.tsx +27 -0
  39. package/src/editor/tree/NodeElement.tsx +72 -0
  40. package/src/editor/tree/TreeElement.tsx +70 -0
  41. package/src/editor/tree/TreeRoot.tsx +10 -0
  42. package/src/editor/tree/ViewRoot.tsx +20 -0
  43. package/src/editor/tree/index.module.scss +45 -0
  44. package/src/editor/tree/index.ts +4 -0
  45. package/src/editor/tree/navigation.ts +145 -0
  46. package/src/editor/tsconfig.build.json +5 -0
  47. package/src/editor/tsconfig.json +12 -0
  48. package/src/editor/tsdoc.json +4 -0
  49. package/src/editor/utils/SignalSet.ts +37 -0
  50. package/src/editor/utils/index.ts +1 -0
  51. package/src/editor/vite-env.d.ts +1 -0
  52. package/src/lib/code/CodeCursor.ts +468 -0
  53. package/src/lib/code/CodeDiffer.ts +77 -0
  54. package/src/lib/code/CodeFragment.ts +96 -0
  55. package/src/lib/code/CodeHighlighter.ts +73 -0
  56. package/src/lib/code/CodeMetrics.ts +47 -0
  57. package/src/lib/code/CodeRange.test.ts +113 -0
  58. package/src/lib/code/CodeRange.ts +222 -0
  59. package/src/lib/code/CodeScope.ts +100 -0
  60. package/src/lib/code/CodeSelection.ts +28 -0
  61. package/src/lib/code/CodeSignal.ts +348 -0
  62. package/src/lib/code/CodeTokenizer.ts +54 -0
  63. package/src/lib/code/DefaultHighlightStyle.ts +98 -0
  64. package/src/lib/code/LezerHighlighter.ts +113 -0
  65. package/src/lib/code/diff.test.ts +311 -0
  66. package/src/lib/code/diff.ts +319 -0
  67. package/src/lib/code/extractRange.ts +125 -0
  68. package/src/lib/code/index.ts +13 -0
  69. package/src/lib/components/Bezier.ts +103 -0
  70. package/src/lib/components/Camera.ts +401 -0
  71. package/src/lib/components/Circle.ts +310 -0
  72. package/src/lib/components/Code.ts +532 -0
  73. package/src/lib/components/CubicBezier.ts +115 -0
  74. package/src/lib/components/Curve.ts +460 -0
  75. package/src/lib/components/Grid.ts +134 -0
  76. package/src/lib/components/Icon.ts +153 -0
  77. package/src/lib/components/Img.ts +328 -0
  78. package/src/lib/components/Knot.ts +156 -0
  79. package/src/lib/components/Latex.ts +537 -0
  80. package/src/lib/components/Layout.ts +1101 -0
  81. package/src/lib/components/Line.ts +394 -0
  82. package/src/lib/components/Node.ts +1941 -0
  83. package/src/lib/components/Path.ts +132 -0
  84. package/src/lib/components/Polygon.ts +266 -0
  85. package/src/lib/components/QuadBezier.ts +103 -0
  86. package/src/lib/components/Ray.ts +126 -0
  87. package/src/lib/components/Rect.ts +219 -0
  88. package/src/lib/components/SVG.ts +853 -0
  89. package/src/lib/components/Shape.ts +322 -0
  90. package/src/lib/components/Spline.ts +318 -0
  91. package/src/lib/components/Txt.test.tsx +81 -0
  92. package/src/lib/components/Txt.ts +204 -0
  93. package/src/lib/components/TxtLeaf.ts +210 -0
  94. package/src/lib/components/Video.ts +368 -0
  95. package/src/lib/components/View2D.ts +85 -0
  96. package/src/lib/components/__logs__/image-without-source.ts +18 -0
  97. package/src/lib/components/__logs__/line-without-points.ts +31 -0
  98. package/src/lib/components/__logs__/reactive-playback-rate.ts +22 -0
  99. package/src/lib/components/__logs__/spline-with-insufficient-knots.ts +25 -0
  100. package/src/lib/components/__tests__/Camera.test.tsx +73 -0
  101. package/src/lib/components/__tests__/children.test.tsx +142 -0
  102. package/src/lib/components/__tests__/clone.test.tsx +126 -0
  103. package/src/lib/components/__tests__/fontInheritance.test.tsx +102 -0
  104. package/src/lib/components/__tests__/generatorTest.ts +27 -0
  105. package/src/lib/components/__tests__/mockScene2D.ts +50 -0
  106. package/src/lib/components/__tests__/query.test.tsx +122 -0
  107. package/src/lib/components/__tests__/state.test.tsx +60 -0
  108. package/src/lib/components/index.ts +26 -0
  109. package/src/lib/components/types.ts +35 -0
  110. package/src/lib/curves/ArcSegment.ts +169 -0
  111. package/src/lib/curves/CircleSegment.ts +99 -0
  112. package/src/lib/curves/CubicBezierSegment.ts +80 -0
  113. package/src/lib/curves/CurveDrawingInfo.ts +11 -0
  114. package/src/lib/curves/CurvePoint.ts +15 -0
  115. package/src/lib/curves/CurveProfile.ts +28 -0
  116. package/src/lib/curves/KnotInfo.ts +10 -0
  117. package/src/lib/curves/LineSegment.ts +75 -0
  118. package/src/lib/curves/Polynomial.ts +355 -0
  119. package/src/lib/curves/Polynomial2D.ts +62 -0
  120. package/src/lib/curves/PolynomialSegment.ts +151 -0
  121. package/src/lib/curves/QuadBezierSegment.ts +66 -0
  122. package/src/lib/curves/Segment.ts +31 -0
  123. package/src/lib/curves/UniformPolynomialCurveSampler.ts +93 -0
  124. package/src/lib/curves/createCurveProfileLerp.ts +471 -0
  125. package/src/lib/curves/getBezierSplineProfile.ts +227 -0
  126. package/src/lib/curves/getCircleProfile.ts +86 -0
  127. package/src/lib/curves/getPathProfile.ts +177 -0
  128. package/src/lib/curves/getPointAtDistance.ts +21 -0
  129. package/src/lib/curves/getPolylineProfile.test.ts +21 -0
  130. package/src/lib/curves/getPolylineProfile.ts +88 -0
  131. package/src/lib/curves/getRectProfile.ts +138 -0
  132. package/src/lib/curves/index.ts +16 -0
  133. package/src/lib/decorators/canvasStyleSignal.ts +15 -0
  134. package/src/lib/decorators/colorSignal.ts +9 -0
  135. package/src/lib/decorators/compound.ts +85 -0
  136. package/src/lib/decorators/computed.ts +18 -0
  137. package/src/lib/decorators/defaultStyle.ts +15 -0
  138. package/src/lib/decorators/filtersSignal.ts +133 -0
  139. package/src/lib/decorators/index.ts +10 -0
  140. package/src/lib/decorators/initializers.ts +32 -0
  141. package/src/lib/decorators/nodeName.ts +13 -0
  142. package/src/lib/decorators/signal.test.ts +89 -0
  143. package/src/lib/decorators/signal.ts +348 -0
  144. package/src/lib/decorators/spacingSignal.ts +15 -0
  145. package/src/lib/decorators/transformSignals.test.ts +858 -0
  146. package/src/lib/decorators/transformSignals.ts +1633 -0
  147. package/src/lib/decorators/vector2Signal.ts +35 -0
  148. package/src/lib/globals.d.ts +2 -0
  149. package/src/lib/index.ts +9 -0
  150. package/src/lib/jsx-dev-runtime.ts +2 -0
  151. package/src/lib/jsx-runtime.ts +45 -0
  152. package/src/lib/morphers/PathMorpher.ts +8 -0
  153. package/src/lib/morphers/defaultMorpher.ts +43 -0
  154. package/src/lib/morphers/index.ts +4 -0
  155. package/src/lib/morphers/manimMorpher.ts +658 -0
  156. package/src/lib/parse-svg-path.d.ts +14 -0
  157. package/src/lib/partials/Filter.ts +185 -0
  158. package/src/lib/partials/Gradient.ts +103 -0
  159. package/src/lib/partials/Pattern.ts +35 -0
  160. package/src/lib/partials/RoughConfig.ts +180 -0
  161. package/src/lib/partials/ShaderConfig.ts +122 -0
  162. package/src/lib/partials/index.ts +5 -0
  163. package/src/lib/partials/types.ts +58 -0
  164. package/src/lib/scenes/Scene2D.ts +167 -0
  165. package/src/lib/scenes/index.ts +3 -0
  166. package/src/lib/scenes/makeScene2D.ts +19 -0
  167. package/src/lib/scenes/useScene2D.ts +6 -0
  168. package/src/lib/tsconfig.build.json +5 -0
  169. package/src/lib/tsconfig.json +11 -0
  170. package/src/lib/tsdoc.json +4 -0
  171. package/src/lib/utils/CanvasUtils.ts +304 -0
  172. package/src/lib/utils/PathDataBuilder.ts +320 -0
  173. package/src/lib/utils/diff.test.ts +453 -0
  174. package/src/lib/utils/diff.ts +148 -0
  175. package/src/lib/utils/index.ts +5 -0
  176. package/src/lib/utils/is.ts +11 -0
  177. package/src/lib/utils/makeSignalExtensions.ts +29 -0
  178. package/src/lib/utils/rough.ts +513 -0
  179. package/src/lib/utils/withDefaults.tsx +26 -0
  180. package/src/tsconfig.base.json +18 -0
  181. package/src/tsconfig.build.json +8 -0
  182. package/src/tsconfig.json +5 -0
  183. package/tsconfig.project.json +7 -0
@@ -0,0 +1,1633 @@
1
+ import {
2
+ DEFAULT,
3
+ InterpolationFunction,
4
+ PossibleVector2,
5
+ Signal,
6
+ SignalContext,
7
+ SignalExtensions,
8
+ SignalGenerator,
9
+ SignalValue,
10
+ SimpleVector2Signal,
11
+ ThreadGenerator,
12
+ TimingFunction,
13
+ Vector2,
14
+ Vector2Signal,
15
+ Vector2SignalContext,
16
+ deepLerp,
17
+ unwrap,
18
+ } from '@canvas-commons/core';
19
+ import {Node} from '../components/Node';
20
+ import {makeSignalExtensions} from '../utils/makeSignalExtensions';
21
+ import {compound} from './compound';
22
+ import {addInitializer} from './initializers';
23
+ import {getPropertyMetaOrCreate, wrapper} from './signal';
24
+
25
+ /**
26
+ * Utility class for handling coordinate space transformations.
27
+ *
28
+ * Provides shared logic for converting between different coordinate spaces:
29
+ * - **Local**: The node's own coordinate system relative to its parent
30
+ * - **Absolute**: The global coordinate system of the scene
31
+ * - **View**: The coordinate system of the view/camera
32
+ * - **Relative**: Coordinate system relative to another specific node
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Convert an absolute position to local coordinates
37
+ * const localPos = TransformConverter.absoluteToLocalPosition(node, [100, 200]);
38
+ *
39
+ * // Convert a view space scale to local coordinates
40
+ * const localScale = TransformConverter.viewToLocalScale(node, [2, 2]);
41
+ * ```
42
+ */
43
+ class TransformConverter {
44
+ private static wrapVectorSignalTransform(
45
+ value: SignalValue<PossibleVector2>,
46
+ transform: (val: Vector2) => Vector2,
47
+ ): SignalValue<PossibleVector2> {
48
+ if (typeof value === 'function') {
49
+ return () => transform(new Vector2(value()));
50
+ }
51
+ return transform(new Vector2(value));
52
+ }
53
+
54
+ private static wrapScalarSignalTransform(
55
+ value: SignalValue<number>,
56
+ transform: (val: number) => number,
57
+ ): SignalValue<number> {
58
+ if (typeof value === 'function') {
59
+ return () => transform(value());
60
+ }
61
+ return transform(value);
62
+ }
63
+
64
+ public static absoluteToLocalPosition(
65
+ owner: Node,
66
+ absoluteValue: SignalValue<PossibleVector2>,
67
+ ): SignalValue<PossibleVector2> {
68
+ return this.wrapVectorSignalTransform(absoluteValue, val =>
69
+ val.transformAsPoint(owner.worldToParent()),
70
+ );
71
+ }
72
+
73
+ public static absoluteToLocalScale(
74
+ owner: Node,
75
+ absoluteValue: SignalValue<PossibleVector2>,
76
+ ): SignalValue<PossibleVector2> {
77
+ return this.wrapVectorSignalTransform(absoluteValue, val => {
78
+ const parentAbsScale = owner.parent()?.absoluteScale() ?? Vector2.one;
79
+ return val.div(parentAbsScale);
80
+ });
81
+ }
82
+
83
+ public static absoluteToLocalRotation(
84
+ owner: Node,
85
+ absoluteValue: SignalValue<number>,
86
+ ): SignalValue<number> {
87
+ return this.wrapScalarSignalTransform(absoluteValue, val => {
88
+ const parentAbsRotation = owner.parent()?.absoluteRotation() ?? 0;
89
+ return val - parentAbsRotation;
90
+ });
91
+ }
92
+
93
+ public static relativeToAbsolutePosition(
94
+ targetNode: Node,
95
+ relativeValue: SignalValue<PossibleVector2>,
96
+ ): SignalValue<PossibleVector2> {
97
+ return this.wrapVectorSignalTransform(relativeValue, val =>
98
+ val.add(targetNode.absolutePosition()),
99
+ );
100
+ }
101
+
102
+ public static relativeToAbsoluteScale(
103
+ targetNode: Node,
104
+ relativeValue: SignalValue<PossibleVector2>,
105
+ ): SignalValue<PossibleVector2> {
106
+ return this.wrapVectorSignalTransform(relativeValue, val =>
107
+ val.mul(targetNode.absoluteScale()),
108
+ );
109
+ }
110
+
111
+ public static relativeToAbsoluteRotation(
112
+ targetNode: Node,
113
+ relativeValue: SignalValue<number>,
114
+ ): SignalValue<number> {
115
+ return this.wrapScalarSignalTransform(
116
+ relativeValue,
117
+ val => val + targetNode.absoluteRotation(),
118
+ );
119
+ }
120
+
121
+ public static viewToLocalPosition(
122
+ owner: Node,
123
+ viewValue: SignalValue<PossibleVector2>,
124
+ ): SignalValue<PossibleVector2> {
125
+ return this.wrapVectorSignalTransform(viewValue, val => {
126
+ const worldPos = val.transformAsPoint(owner.view().localToWorld());
127
+ return worldPos.transformAsPoint(owner.worldToParent());
128
+ });
129
+ }
130
+
131
+ public static viewToLocalScale(
132
+ owner: Node,
133
+ viewValue: SignalValue<PossibleVector2>,
134
+ ): SignalValue<PossibleVector2> {
135
+ return this.wrapVectorSignalTransform(viewValue, val => {
136
+ const viewMatrix = owner.view().localToWorld();
137
+ const [xMagnitude, yMagnitude] = this.getViewScaleMagnitudes(viewMatrix);
138
+ return new Vector2(val.x / xMagnitude, val.y / yMagnitude);
139
+ });
140
+ }
141
+
142
+ public static viewToLocalRotation(
143
+ owner: Node,
144
+ viewValue: SignalValue<number>,
145
+ ): SignalValue<number> {
146
+ return this.wrapScalarSignalTransform(
147
+ viewValue,
148
+ val => val - this.getViewRotation(owner),
149
+ );
150
+ }
151
+
152
+ public static getViewRotation(owner: Node): number {
153
+ const viewMatrix = owner.view().localToWorld();
154
+ return Vector2.degrees(viewMatrix.m11, viewMatrix.m12);
155
+ }
156
+
157
+ public static calculateViewSpaceScale(
158
+ owner: Node,
159
+ localScale: Vector2,
160
+ ): Vector2 {
161
+ const viewMatrix = owner.view().localToWorld();
162
+ return new Vector2(
163
+ Vector2.magnitude(
164
+ viewMatrix.m11 * localScale.x,
165
+ viewMatrix.m12 * localScale.x,
166
+ ),
167
+ Vector2.magnitude(
168
+ viewMatrix.m21 * localScale.y,
169
+ viewMatrix.m22 * localScale.y,
170
+ ),
171
+ );
172
+ }
173
+
174
+ private static getViewScaleMagnitudes(
175
+ viewMatrix: DOMMatrix,
176
+ ): [number, number] {
177
+ return [
178
+ Vector2.magnitude(viewMatrix.m11, viewMatrix.m12),
179
+ Vector2.magnitude(viewMatrix.m21, viewMatrix.m22),
180
+ ];
181
+ }
182
+
183
+ public static absoluteToLocalLayoutPosition(
184
+ owner: Node,
185
+ absoluteValue: SignalValue<PossibleVector2>,
186
+ ): SignalValue<PossibleVector2> {
187
+ return this.wrapVectorSignalTransform(absoluteValue, val =>
188
+ val.transformAsPoint(owner.worldToLocal()),
189
+ );
190
+ }
191
+ }
192
+
193
+ interface ComponentTransformMethod<TOwner> {
194
+ (): number;
195
+ (value: SignalValue<number>): TOwner;
196
+ (
197
+ value: SignalValue<number>,
198
+ duration: number,
199
+ timingFunction?: TimingFunction,
200
+ interpolationFunction?: InterpolationFunction<number>,
201
+ ): ThreadGenerator;
202
+ }
203
+
204
+ const GETTER_ARGS = 0;
205
+ const SETTER_ARGS = 1;
206
+
207
+ /**
208
+ * Enhanced transform method interface that provides both vector and component access.
209
+ *
210
+ * This interface allows you to work with 2D transforms in multiple ways:
211
+ * - Get/set the entire vector: `position()` or `position([x, y])`
212
+ * - Animate the vector: `position([x, y], duration)`
213
+ * - Access individual components: `position.x()` or `position.y()`
214
+ * - Animate components: `position.x(value, duration)`
215
+ *
216
+ * @typeParam TOwner - The type of the object that owns this transform method
217
+ */
218
+ interface EnhancedTransformMethod<TOwner> {
219
+ /** Get the current transform value */
220
+ (): Vector2;
221
+ /** Set the transform value immediately */
222
+ (value: SignalValue<PossibleVector2>): TOwner;
223
+ /** Animate the transform to a new value */
224
+ (
225
+ value: SignalValue<PossibleVector2>,
226
+ duration: number,
227
+ timingFunction?: TimingFunction,
228
+ interpolationFunction?: InterpolationFunction<Vector2>,
229
+ ): SignalGenerator<PossibleVector2, Vector2>;
230
+ /** Access to the X component of the transform */
231
+ x: ComponentTransformMethod<TOwner>;
232
+ /** Access to the Y component of the transform */
233
+ y: ComponentTransformMethod<TOwner>;
234
+ }
235
+
236
+ // Node-based transform method interface (takes a node parameter)
237
+ interface NodeTransformMethod<TOwner extends Node> {
238
+ (node: Node): CurriedRelativeTransformSignal<TOwner>;
239
+ }
240
+
241
+ // Transform-specific signal helpers with enhanced component-wise support
242
+ interface PositionSignalHelpers<TOwner extends Node> {
243
+ abs: EnhancedTransformMethod<TOwner>;
244
+ relativeTo: NodeTransformMethod<TOwner>;
245
+ view: EnhancedTransformMethod<TOwner>;
246
+ local: EnhancedTransformMethod<TOwner>;
247
+ }
248
+
249
+ interface ScaleSignalHelpers<TOwner extends Node> {
250
+ abs: EnhancedTransformMethod<TOwner>;
251
+ relativeTo: NodeScaleTransformMethod<TOwner>;
252
+ view: EnhancedTransformMethod<TOwner>;
253
+ local: EnhancedTransformMethod<TOwner>;
254
+ }
255
+
256
+ // Node-based scale transform method interface (similar to position but returns scale signal)
257
+ interface NodeScaleTransformMethod<TOwner extends Node> {
258
+ (node: Node): CurriedRelativeScaleSignal<TOwner>;
259
+ }
260
+
261
+ // Rotation-specific transform method interface (scalar)
262
+ interface EnhancedRotationMethod<TOwner extends Node> {
263
+ (): number;
264
+ (value: SignalValue<number>): TOwner;
265
+ (
266
+ value: SignalValue<number>,
267
+ duration: number,
268
+ timingFunction?: TimingFunction,
269
+ interpolationFunction?: InterpolationFunction<number>,
270
+ ): SignalGenerator<number, number>;
271
+ }
272
+
273
+ // Node-based rotation transform method interface
274
+ interface NodeRotationMethod<TOwner extends Node> {
275
+ (node: Node): CurriedRelativeRotationSignal<TOwner>;
276
+ }
277
+
278
+ interface RotationSignalHelpers<TOwner extends Node> {
279
+ abs: EnhancedRotationMethod<TOwner>;
280
+ relativeTo: NodeRotationMethod<TOwner>;
281
+ view: EnhancedRotationMethod<TOwner>;
282
+ local: EnhancedRotationMethod<TOwner>;
283
+ }
284
+
285
+ /**
286
+ * Enhanced position signal that provides coordinate space transformation methods.
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * // Get/set position in different coordinate spaces
291
+ * node.position([100, 200]); // Set local position
292
+ * node.position.abs([300, 400]); // Set absolute position
293
+ * node.position.relativeTo(other, [50, 0]); // Set position relative to another node
294
+ * node.position.view([0, 0]); // Set position in view space
295
+ *
296
+ * // Component access
297
+ * node.position.x(150); // Set only X coordinate
298
+ * node.position.abs.y(250); // Set only absolute Y coordinate
299
+ * ```
300
+ */
301
+ export type PositionSignal<TOwner extends Node = Node> = Vector2Signal<TOwner> &
302
+ PositionSignalHelpers<TOwner>;
303
+
304
+ /**
305
+ * Enhanced scale signal that provides coordinate space transformation methods.
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * // Scale operations in different coordinate spaces
310
+ * node.scale([2, 1.5]); // Set local scale
311
+ * node.scale.abs([4, 3]); // Set absolute scale
312
+ * node.scale.relativeTo(parent, [0.5, 0.5]); // Scale relative to parent
313
+ * ```
314
+ */
315
+ export type ScaleSignal<TOwner extends Node = Node> = Vector2Signal<TOwner> &
316
+ ScaleSignalHelpers<TOwner>;
317
+
318
+ /**
319
+ * Enhanced rotation signal that provides coordinate space transformation methods.
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * // Rotation operations in different coordinate spaces
324
+ * node.rotation(45); // Set local rotation (degrees)
325
+ * node.rotation.abs(90); // Set absolute rotation
326
+ * node.rotation.relativeTo(other, 180); // Set rotation relative to another node
327
+ * ```
328
+ */
329
+ export type RotationSignal<TOwner extends Node = Node> = Signal<
330
+ number,
331
+ number,
332
+ TOwner
333
+ > &
334
+ RotationSignalHelpers<TOwner>;
335
+
336
+ /**
337
+ * Layout position signal for computed position properties like `top`, `left`, etc.
338
+ *
339
+ * These signals compute their values dynamically based on the node's size and origin,
340
+ * and delegate setting operations to the main position signal.
341
+ */
342
+ export type LayoutPositionSignal<TOwner extends Node = Node> =
343
+ SimpleVector2Signal<TOwner> & PositionSignalHelpers<TOwner>;
344
+
345
+ // Shared helper functions for creating enhanced transform methods
346
+
347
+ /**
348
+ * UNIFIED TRANSFORM SIGNAL ARCHITECTURE
349
+ *
350
+ * Every signal in the system is a coordinate space transformation:
351
+ * - Forward transform: base space → target space
352
+ * - Inverse transform: target space → base space
353
+ * - Component access and tweening built on top
354
+ */
355
+
356
+ // Type for component configuration
357
+ interface ComponentConfig<TTarget> {
358
+ getComponent: (target: TTarget, component: 'x' | 'y') => number;
359
+ setComponent: (
360
+ target: TTarget,
361
+ component: 'x' | 'y',
362
+ value: number,
363
+ ) => TTarget;
364
+ }
365
+
366
+ // Standard Vector2 component configuration
367
+ const VECTOR2_COMPONENT_CONFIG: ComponentConfig<Vector2> = {
368
+ getComponent: (vec: Vector2, component: 'x' | 'y') => vec[component],
369
+ setComponent: (vec: Vector2, component: 'x' | 'y', value: number) =>
370
+ new Vector2(component === 'x' ? [value, vec.y] : [vec.x, value]),
371
+ };
372
+
373
+ /**
374
+ * Creates a unified transform signal that handles coordinate space transformations.
375
+ * This is the foundation for all signals (abs, view, local, relativeTo, origin signals).
376
+ */
377
+ function createTransformSignal<TBase, TTarget, TOwner extends Node>(
378
+ baseGetter: () => TBase,
379
+ baseSetter: (value: SignalValue<TBase>) => TOwner,
380
+ baseTweener: (
381
+ value: SignalValue<TBase>,
382
+ duration: number,
383
+ timingFunction?: TimingFunction,
384
+ interpolationFunction?: InterpolationFunction<TTarget>,
385
+ ) => any,
386
+ forwardTransform: (base: TBase) => TTarget,
387
+ inverseTransform: (target: TTarget) => SignalValue<TBase>,
388
+ componentConfig?: ComponentConfig<TTarget>,
389
+ ) {
390
+ // Main signal function with proper overloads
391
+ function signal(): TTarget;
392
+ function signal(value: SignalValue<TTarget>): TOwner;
393
+ function signal(
394
+ value: SignalValue<TTarget>,
395
+ duration: number,
396
+ timingFunction?: TimingFunction,
397
+ interpolationFunction?: InterpolationFunction<TTarget>,
398
+ ): any;
399
+ function signal(
400
+ value?: SignalValue<TTarget>,
401
+ duration?: number,
402
+ timingFunction?: TimingFunction,
403
+ interpolationFunction?: InterpolationFunction<TTarget>,
404
+ ): TTarget | TOwner | any {
405
+ if (arguments.length === 0) {
406
+ // Getter: apply forward transform
407
+ return forwardTransform(baseGetter());
408
+ }
409
+
410
+ if (arguments.length === 1) {
411
+ // Setter: apply inverse transform and store
412
+ const baseValue = inverseTransform(unwrap(value!));
413
+ return baseSetter(baseValue);
414
+ }
415
+
416
+ // Tweener: apply inverse transform and animate
417
+ const baseValue = inverseTransform(unwrap(value!));
418
+ return baseTweener(
419
+ baseValue,
420
+ duration!,
421
+ timingFunction,
422
+ interpolationFunction,
423
+ );
424
+ }
425
+
426
+ // Add component accessors for Vector2-like targets
427
+ if (componentConfig) {
428
+ const {getComponent, setComponent} = componentConfig;
429
+
430
+ (signal as any).x = function (
431
+ value?: SignalValue<number>,
432
+ duration?: number,
433
+ timingFunction?: TimingFunction,
434
+ interpolationFunction?: InterpolationFunction<TTarget>,
435
+ ): number | TOwner | any {
436
+ if (arguments.length === 0) {
437
+ return getComponent(signal(), 'x');
438
+ }
439
+
440
+ const currentTarget = signal();
441
+ const newTarget = setComponent(currentTarget, 'x', unwrap(value!));
442
+
443
+ if (arguments.length === 1) {
444
+ return signal(newTarget);
445
+ }
446
+
447
+ // Component tweening
448
+ return signal(
449
+ newTarget,
450
+ duration!,
451
+ timingFunction,
452
+ interpolationFunction as InterpolationFunction<TTarget>,
453
+ );
454
+ };
455
+
456
+ (signal as any).y = function (
457
+ value?: SignalValue<number>,
458
+ duration?: number,
459
+ timingFunction?: TimingFunction,
460
+ interpolationFunction?: InterpolationFunction<TTarget>,
461
+ ): number | TOwner | any {
462
+ if (arguments.length === 0) {
463
+ return getComponent(signal(), 'y');
464
+ }
465
+
466
+ const currentTarget = signal();
467
+ const newTarget = setComponent(currentTarget, 'y', unwrap(value!));
468
+
469
+ if (arguments.length === 1) {
470
+ return signal(newTarget);
471
+ }
472
+
473
+ // Component tweening
474
+ return signal(
475
+ newTarget,
476
+ duration!,
477
+ timingFunction,
478
+ interpolationFunction as InterpolationFunction<TTarget>,
479
+ );
480
+ };
481
+ }
482
+
483
+ return signal;
484
+ }
485
+
486
+ /**
487
+ * Create a curried relative transform signal.
488
+ */
489
+ function createCurriedRelativeSignal<TOwner extends Node = Node>(
490
+ baseContext: PositionSignalContext<TOwner>,
491
+ targetNode: Node,
492
+ ): CurriedRelativeTransformSignal<TOwner> {
493
+ // Create the main function
494
+ const curriedSignal = function (
495
+ ...args: any[]
496
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
497
+ if (args.length === 0) {
498
+ // Getter: compute relative position using internal method
499
+ return baseContext['relativeToImpl'](targetNode);
500
+ }
501
+
502
+ // Setter/Tweener: delegate to internal method
503
+ const [value, duration, timingFunction, interpolationFunction] = args;
504
+ return baseContext['relativeToImpl'](
505
+ targetNode,
506
+ value,
507
+ duration,
508
+ timingFunction,
509
+ interpolationFunction,
510
+ );
511
+ };
512
+
513
+ // Add component accessors
514
+ Object.defineProperty(curriedSignal, 'x', {
515
+ get() {
516
+ return function (...args: any[]): number | TOwner | ThreadGenerator {
517
+ if (args.length === 0) {
518
+ return (curriedSignal() as Vector2).x;
519
+ }
520
+ // Component setter/tweener - build new vector and set/animate it
521
+ const current = curriedSignal() as Vector2;
522
+ if (args.length === 1) {
523
+ return curriedSignal(
524
+ new Vector2([unwrap(args[0]), current.y]),
525
+ ) as TOwner;
526
+ }
527
+ // Component tweener
528
+ const [value, duration, timingFunction, interpolationFunction] = args;
529
+ return curriedSignal(
530
+ new Vector2([unwrap(value), current.y]),
531
+ duration,
532
+ timingFunction,
533
+ interpolationFunction as any,
534
+ ) as ThreadGenerator;
535
+ };
536
+ },
537
+ });
538
+
539
+ Object.defineProperty(curriedSignal, 'y', {
540
+ get() {
541
+ return function (...args: any[]): number | TOwner | ThreadGenerator {
542
+ if (args.length === 0) {
543
+ return (curriedSignal() as Vector2).y;
544
+ }
545
+ // Component setter/tweener - build new vector and set/animate it
546
+ const current = curriedSignal() as Vector2;
547
+ if (args.length === 1) {
548
+ return curriedSignal(
549
+ new Vector2([current.x, unwrap(args[0])]),
550
+ ) as TOwner;
551
+ }
552
+ // Component tweener
553
+ const [value, duration, timingFunction, interpolationFunction] = args;
554
+ return curriedSignal(
555
+ new Vector2([current.x, unwrap(value)]),
556
+ duration,
557
+ timingFunction,
558
+ interpolationFunction as any,
559
+ ) as ThreadGenerator;
560
+ };
561
+ },
562
+ });
563
+
564
+ return curriedSignal as CurriedRelativeTransformSignal<TOwner>;
565
+ }
566
+
567
+ // Properly typed curried signal interface for position
568
+ interface CurriedRelativeTransformSignal<TOwner extends Node> {
569
+ (): Vector2;
570
+ (value: SignalValue<PossibleVector2>): TOwner;
571
+ (
572
+ value: SignalValue<PossibleVector2>,
573
+ duration: number,
574
+ timingFunction?: TimingFunction,
575
+ interpolationFunction?: InterpolationFunction<Vector2>,
576
+ ): SignalGenerator<PossibleVector2, Vector2>;
577
+
578
+ x: ComponentTransformMethod<TOwner>;
579
+ y: ComponentTransformMethod<TOwner>;
580
+ }
581
+
582
+ /**
583
+ * Create a curried relative transform signal for rotation.
584
+ */
585
+ function createCurriedRelativeRotationSignal<TOwner extends Node = Node>(
586
+ baseContext: RotationSignalContext<TOwner>,
587
+ targetNode: Node,
588
+ ): CurriedRelativeRotationSignal<TOwner> {
589
+ // Create the main function
590
+ const curriedSignal = function (
591
+ ...args: any[]
592
+ ): number | TOwner | SignalGenerator<number, number> {
593
+ if (args.length === 0) {
594
+ // Getter: compute relative rotation using internal method
595
+ return baseContext['relativeToImpl'](targetNode);
596
+ }
597
+
598
+ // Setter/Tweener: delegate to internal method
599
+ const [value, duration, timingFunction, interpolationFunction] = args;
600
+ return baseContext['relativeToImpl'](
601
+ targetNode,
602
+ value,
603
+ duration,
604
+ timingFunction,
605
+ interpolationFunction,
606
+ );
607
+ };
608
+
609
+ return curriedSignal as CurriedRelativeRotationSignal<TOwner>;
610
+ }
611
+
612
+ /**
613
+ * Create a curried relative transform signal for scale.
614
+ */
615
+ function createCurriedRelativeScaleSignal<TOwner extends Node = Node>(
616
+ baseContext: ScaleSignalContext<TOwner>,
617
+ targetNode: Node,
618
+ ): CurriedRelativeScaleSignal<TOwner> {
619
+ // Create the main function
620
+ const curriedSignal = function (
621
+ ...args: any[]
622
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
623
+ if (args.length === 0) {
624
+ // Getter: compute relative scale
625
+ const absScale = (baseContext as any).owner.absoluteScale();
626
+ const targetAbsScale = targetNode.absoluteScale();
627
+ return absScale.div(targetAbsScale);
628
+ }
629
+
630
+ // Setter/Tweener: convert relative scale to local scale and set it
631
+ const [value, duration, timingFunction, interpolationFunction] = args;
632
+ const absoluteValue = TransformConverter.relativeToAbsoluteScale(
633
+ targetNode,
634
+ value,
635
+ );
636
+ const localValue = TransformConverter.absoluteToLocalScale(
637
+ (baseContext as any).owner,
638
+ absoluteValue,
639
+ );
640
+ return (baseContext as any).invoke(
641
+ localValue,
642
+ duration,
643
+ timingFunction,
644
+ interpolationFunction,
645
+ ) as TOwner | SignalGenerator<PossibleVector2, Vector2>;
646
+ };
647
+
648
+ // Add component accessors
649
+ Object.defineProperty(curriedSignal, 'x', {
650
+ get() {
651
+ return function (...args: any[]): number | TOwner | ThreadGenerator {
652
+ if (args.length === 0) {
653
+ return (curriedSignal() as Vector2).x;
654
+ }
655
+ // Component setter/tweener - build new vector and set/animate it
656
+ const current = curriedSignal() as Vector2;
657
+ if (args.length === 1) {
658
+ return curriedSignal(
659
+ new Vector2([unwrap(args[0]), current.y]),
660
+ ) as TOwner;
661
+ }
662
+ // Component tweener
663
+ const [value, duration, timingFunction, interpolationFunction] = args;
664
+ return curriedSignal(
665
+ new Vector2([unwrap(value), current.y]),
666
+ duration,
667
+ timingFunction,
668
+ interpolationFunction as any,
669
+ ) as ThreadGenerator;
670
+ };
671
+ },
672
+ });
673
+
674
+ Object.defineProperty(curriedSignal, 'y', {
675
+ get() {
676
+ return function (...args: any[]): number | TOwner | ThreadGenerator {
677
+ if (args.length === 0) {
678
+ return (curriedSignal() as Vector2).y;
679
+ }
680
+ // Component setter/tweener - build new vector and set/animate it
681
+ const current = curriedSignal() as Vector2;
682
+ if (args.length === 1) {
683
+ return curriedSignal(
684
+ new Vector2([current.x, unwrap(args[0])]),
685
+ ) as TOwner;
686
+ }
687
+ // Component tweener
688
+ const [value, duration, timingFunction, interpolationFunction] = args;
689
+ return curriedSignal(
690
+ new Vector2([current.x, unwrap(value)]),
691
+ duration,
692
+ timingFunction,
693
+ interpolationFunction as any,
694
+ ) as ThreadGenerator;
695
+ };
696
+ },
697
+ });
698
+
699
+ return curriedSignal as CurriedRelativeScaleSignal<TOwner>;
700
+ }
701
+
702
+ // Properly typed curried signal interface for rotation
703
+ interface CurriedRelativeRotationSignal<TOwner extends Node> {
704
+ (): number;
705
+ (value: SignalValue<number>): TOwner;
706
+ (
707
+ value: SignalValue<number>,
708
+ duration: number,
709
+ timingFunction?: TimingFunction,
710
+ interpolationFunction?: InterpolationFunction<number>,
711
+ ): SignalGenerator<number, number>;
712
+ }
713
+
714
+ // Properly typed curried signal interface for scale
715
+ interface CurriedRelativeScaleSignal<TOwner extends Node> {
716
+ (): Vector2;
717
+ (value: SignalValue<PossibleVector2>): TOwner;
718
+ (
719
+ value: SignalValue<PossibleVector2>,
720
+ duration: number,
721
+ timingFunction?: TimingFunction,
722
+ interpolationFunction?: InterpolationFunction<Vector2>,
723
+ ): SignalGenerator<PossibleVector2, Vector2>;
724
+
725
+ x: ComponentTransformMethod<TOwner>;
726
+ y: ComponentTransformMethod<TOwner>;
727
+ }
728
+
729
+ // Position-aware Vector2 signal context
730
+ export class PositionSignalContext<
731
+ TOwner extends Node = Node,
732
+ > extends Vector2SignalContext<TOwner> {
733
+ public constructor(
734
+ entries: ('x' | 'y' | [keyof Vector2, Signal<number, number, TOwner>])[],
735
+ parser: (value: PossibleVector2) => Vector2,
736
+ initial: SignalValue<PossibleVector2>,
737
+ interpolation: InterpolationFunction<Vector2>,
738
+ owner: TOwner,
739
+ extensions: Partial<SignalExtensions<PossibleVector2, Vector2>> = {},
740
+ ) {
741
+ super(entries, parser, initial, interpolation, owner, extensions);
742
+
743
+ // Create enhanced transform methods using unified approach
744
+ Object.defineProperty(this.invokable, 'abs', {
745
+ value: createTransformSignal<PossibleVector2, Vector2, TOwner>(
746
+ () => this.get(),
747
+ value => this.invoke(value) as TOwner,
748
+ (value, duration, timingFunction, interpolationFunction) =>
749
+ this.invoke(value, duration, timingFunction, interpolationFunction),
750
+ local =>
751
+ new Vector2(local).transformAsPoint(this.owner.parentToWorld()),
752
+ absolute =>
753
+ TransformConverter.absoluteToLocalPosition(
754
+ this.owner,
755
+ unwrap(absolute!) as PossibleVector2,
756
+ ),
757
+ VECTOR2_COMPONENT_CONFIG,
758
+ ),
759
+ enumerable: false,
760
+ });
761
+
762
+ Object.defineProperty(this.invokable, 'view', {
763
+ value: createTransformSignal<PossibleVector2, Vector2, TOwner>(
764
+ () => this.get(),
765
+ value => this.invoke(value) as TOwner,
766
+ (value, duration, timingFunction, interpolationFunction) =>
767
+ this.invoke(value, duration, timingFunction, interpolationFunction),
768
+ local => {
769
+ // Transform local position to world, then world to view space
770
+ const worldPos = new Vector2(local).transformAsPoint(
771
+ this.owner.parentToWorld(),
772
+ );
773
+ return worldPos.transformAsPoint(this.owner.view().worldToLocal());
774
+ },
775
+ view =>
776
+ TransformConverter.viewToLocalPosition(
777
+ this.owner,
778
+ unwrap(view!) as PossibleVector2,
779
+ ),
780
+ VECTOR2_COMPONENT_CONFIG,
781
+ ),
782
+ enumerable: false,
783
+ });
784
+
785
+ Object.defineProperty(this.invokable, 'local', {
786
+ value: createTransformSignal<PossibleVector2, Vector2, TOwner>(
787
+ () => this.get(),
788
+ value => this.invoke(value) as TOwner,
789
+ (value, duration, timingFunction, interpolationFunction) =>
790
+ this.invoke(value, duration, timingFunction, interpolationFunction),
791
+ local => new Vector2(local),
792
+ local => local,
793
+ VECTOR2_COMPONENT_CONFIG,
794
+ ),
795
+ enumerable: false,
796
+ });
797
+
798
+ Object.defineProperty(this.invokable, 'relativeTo', {
799
+ value: this.relativeTo.bind(this),
800
+ enumerable: false,
801
+ });
802
+ }
803
+
804
+ public override toSignal(): PositionSignal<TOwner> {
805
+ return this.invokable as PositionSignal<TOwner>;
806
+ }
807
+
808
+ // Internal method for the actual relativeTo implementation
809
+ private relativeToImpl(
810
+ node: Node,
811
+ value?: SignalValue<PossibleVector2>,
812
+ duration?: number,
813
+ timingFunction?: TimingFunction,
814
+ interpolationFunction?: InterpolationFunction<Vector2>,
815
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
816
+ if (arguments.length === 1) {
817
+ const absPosition = this.owner.absolutePosition();
818
+ const targetAbsPosition = node.absolutePosition();
819
+ return absPosition.sub(targetAbsPosition);
820
+ }
821
+
822
+ // Convert relative value to local value and set it
823
+ const absoluteValue = TransformConverter.relativeToAbsolutePosition(
824
+ node,
825
+ value!,
826
+ );
827
+ const localValue = TransformConverter.absoluteToLocalPosition(
828
+ this.owner,
829
+ absoluteValue,
830
+ );
831
+ return this.invoke(
832
+ localValue,
833
+ duration,
834
+ timingFunction,
835
+ interpolationFunction,
836
+ ) as TOwner | SignalGenerator<PossibleVector2, Vector2>;
837
+ }
838
+
839
+ public relativeTo(node: Node): CurriedRelativeTransformSignal<TOwner> {
840
+ return createCurriedRelativeSignal(this, node);
841
+ }
842
+ }
843
+
844
+ // Scale-aware Vector2 signal context
845
+ export class ScaleSignalContext<
846
+ TOwner extends Node = Node,
847
+ > extends Vector2SignalContext<TOwner> {
848
+ public constructor(
849
+ entries: ('x' | 'y' | [keyof Vector2, Signal<number, number, TOwner>])[],
850
+ parser: (value: PossibleVector2) => Vector2,
851
+ initial: SignalValue<PossibleVector2>,
852
+ interpolation: InterpolationFunction<Vector2>,
853
+ owner: TOwner,
854
+ extensions: Partial<SignalExtensions<PossibleVector2, Vector2>> = {},
855
+ ) {
856
+ super(entries, parser, initial, interpolation, owner, extensions);
857
+
858
+ // Create enhanced transform methods using unified approach
859
+ Object.defineProperty(this.invokable, 'abs', {
860
+ value: createTransformSignal<PossibleVector2, Vector2, TOwner>(
861
+ () => this.get(),
862
+ value => this.invoke(value) as TOwner,
863
+ (value, duration, timingFunction, interpolationFunction) =>
864
+ this.invoke(value, duration, timingFunction, interpolationFunction),
865
+ () => {
866
+ const matrix = this.owner.localToWorld();
867
+ return new Vector2(
868
+ Vector2.magnitude(matrix.m11, matrix.m12),
869
+ Vector2.magnitude(matrix.m21, matrix.m22),
870
+ );
871
+ },
872
+ absolute =>
873
+ TransformConverter.absoluteToLocalScale(
874
+ this.owner,
875
+ unwrap(absolute!) as PossibleVector2,
876
+ ),
877
+ VECTOR2_COMPONENT_CONFIG,
878
+ ),
879
+ enumerable: false,
880
+ });
881
+
882
+ Object.defineProperty(this.invokable, 'view', {
883
+ value: createTransformSignal<PossibleVector2, Vector2, TOwner>(
884
+ () => this.get(),
885
+ value => this.invoke(value) as TOwner,
886
+ (value, duration, timingFunction, interpolationFunction) =>
887
+ this.invoke(value, duration, timingFunction, interpolationFunction),
888
+ local =>
889
+ TransformConverter.calculateViewSpaceScale(
890
+ this.owner,
891
+ new Vector2(local),
892
+ ),
893
+ view =>
894
+ TransformConverter.viewToLocalScale(
895
+ this.owner,
896
+ unwrap(view!) as PossibleVector2,
897
+ ),
898
+ VECTOR2_COMPONENT_CONFIG,
899
+ ),
900
+ enumerable: false,
901
+ });
902
+
903
+ Object.defineProperty(this.invokable, 'local', {
904
+ value: createTransformSignal<PossibleVector2, Vector2, TOwner>(
905
+ () => this.get(),
906
+ value => this.invoke(value) as TOwner,
907
+ (value, duration, timingFunction, interpolationFunction) =>
908
+ this.invoke(value, duration, timingFunction, interpolationFunction),
909
+ local => new Vector2(local),
910
+ local => local,
911
+ VECTOR2_COMPONENT_CONFIG,
912
+ ),
913
+ enumerable: false,
914
+ });
915
+
916
+ Object.defineProperty(this.invokable, 'relativeTo', {
917
+ value: this.relativeTo.bind(this),
918
+ enumerable: false,
919
+ });
920
+ }
921
+
922
+ public override toSignal(): ScaleSignal<TOwner> {
923
+ return this.invokable as ScaleSignal<TOwner>;
924
+ }
925
+
926
+ public relativeTo(node: Node): CurriedRelativeScaleSignal<TOwner> {
927
+ return createCurriedRelativeScaleSignal(this, node);
928
+ }
929
+ }
930
+
931
+ // Rotation-aware signal context
932
+ export class RotationSignalContext<
933
+ TOwner extends Node = Node,
934
+ > extends SignalContext<number, number, TOwner> {
935
+ public constructor(
936
+ initial: SignalValue<number> | undefined,
937
+ interpolation: InterpolationFunction<number>,
938
+ owner: TOwner,
939
+ parser: (value: number) => number = value => value,
940
+ extensions: Partial<SignalExtensions<number, number>> = {},
941
+ ) {
942
+ super(initial, interpolation, owner, parser, extensions);
943
+
944
+ Object.defineProperty(this.invokable, 'abs', {
945
+ value: this.abs.bind(this),
946
+ enumerable: false,
947
+ });
948
+
949
+ Object.defineProperty(this.invokable, 'relativeTo', {
950
+ value: this.relativeTo.bind(this),
951
+ enumerable: false,
952
+ });
953
+
954
+ Object.defineProperty(this.invokable, 'view', {
955
+ value: this.view.bind(this),
956
+ enumerable: false,
957
+ });
958
+
959
+ Object.defineProperty(this.invokable, 'local', {
960
+ value: this.local.bind(this),
961
+ enumerable: false,
962
+ });
963
+ }
964
+
965
+ public override toSignal(): RotationSignal<TOwner> {
966
+ return this.invokable as RotationSignal<TOwner>;
967
+ }
968
+
969
+ public abs(): number;
970
+ public abs(value: SignalValue<number>): TOwner;
971
+ public abs(
972
+ value: SignalValue<number>,
973
+ duration: number,
974
+ timingFunction?: TimingFunction,
975
+ interpolationFunction?: InterpolationFunction<number>,
976
+ ): SignalGenerator<number, number>;
977
+ public abs(
978
+ value?: SignalValue<number>,
979
+ duration?: number,
980
+ timingFunction?: TimingFunction,
981
+ interpolationFunction?: InterpolationFunction<number>,
982
+ ): number | TOwner | SignalGenerator<number, number> {
983
+ if (arguments.length === 0) {
984
+ // Get absolute rotation by extracting it from localToWorld matrix
985
+ const matrix = this.owner.localToWorld();
986
+ return Vector2.degrees(matrix.m11, matrix.m12);
987
+ }
988
+
989
+ // Convert absolute rotation to local rotation and set it
990
+ const localValue = TransformConverter.absoluteToLocalRotation(
991
+ this.owner,
992
+ value!,
993
+ );
994
+ return this.invoke(
995
+ localValue,
996
+ duration,
997
+ timingFunction,
998
+ interpolationFunction,
999
+ ) as TOwner | SignalGenerator<number, number>;
1000
+ }
1001
+
1002
+ // Internal method for the actual relativeTo implementation
1003
+ private relativeToImpl(
1004
+ node: Node,
1005
+ value?: SignalValue<number>,
1006
+ duration?: number,
1007
+ timingFunction?: TimingFunction,
1008
+ interpolationFunction?: InterpolationFunction<number>,
1009
+ ): number | TOwner | SignalGenerator<number, number> {
1010
+ if (arguments.length === 1) {
1011
+ const absRotation = this.owner.absoluteRotation();
1012
+ const targetAbsRotation = node.absoluteRotation();
1013
+ return absRotation - targetAbsRotation;
1014
+ }
1015
+
1016
+ // Convert relative rotation to local rotation and set it
1017
+ const absoluteValue = TransformConverter.relativeToAbsoluteRotation(
1018
+ node,
1019
+ value!,
1020
+ );
1021
+ const localValue = TransformConverter.absoluteToLocalRotation(
1022
+ this.owner,
1023
+ absoluteValue,
1024
+ );
1025
+ return this.invoke(
1026
+ localValue,
1027
+ duration,
1028
+ timingFunction,
1029
+ interpolationFunction,
1030
+ ) as TOwner | SignalGenerator<number, number>;
1031
+ }
1032
+
1033
+ public relativeTo(node: Node): CurriedRelativeRotationSignal<TOwner> {
1034
+ return createCurriedRelativeRotationSignal(this, node);
1035
+ }
1036
+
1037
+ public view(): number;
1038
+ public view(value: SignalValue<number>): TOwner;
1039
+ public view(
1040
+ value: SignalValue<number>,
1041
+ duration: number,
1042
+ timingFunction?: TimingFunction,
1043
+ interpolationFunction?: InterpolationFunction<number>,
1044
+ ): SignalGenerator<number, number>;
1045
+ public view(
1046
+ value?: SignalValue<number>,
1047
+ duration?: number,
1048
+ timingFunction?: TimingFunction,
1049
+ interpolationFunction?: InterpolationFunction<number>,
1050
+ ): number | TOwner | SignalGenerator<number, number> {
1051
+ if (arguments.length === 0) {
1052
+ // For rotation in view space, we need to get the rotation relative to view
1053
+ const currentRotation = this.get();
1054
+ const viewRotation = TransformConverter.getViewRotation(this.owner);
1055
+ return currentRotation + viewRotation;
1056
+ }
1057
+
1058
+ // Convert view space rotation to local rotation and set it
1059
+ const localValue = TransformConverter.viewToLocalRotation(
1060
+ this.owner,
1061
+ value!,
1062
+ );
1063
+ return this.invoke(
1064
+ localValue,
1065
+ duration,
1066
+ timingFunction,
1067
+ interpolationFunction,
1068
+ ) as TOwner | SignalGenerator<number, number>;
1069
+ }
1070
+
1071
+ public local(): number;
1072
+ public local(value: SignalValue<number>): TOwner;
1073
+ public local(
1074
+ value: SignalValue<number>,
1075
+ duration: number,
1076
+ timingFunction?: TimingFunction,
1077
+ interpolationFunction?: InterpolationFunction<number>,
1078
+ ): SignalGenerator<number, number>;
1079
+ public local(
1080
+ value?: SignalValue<number>,
1081
+ duration?: number,
1082
+ timingFunction?: TimingFunction,
1083
+ interpolationFunction?: InterpolationFunction<number>,
1084
+ ): number | TOwner | SignalGenerator<number, number> {
1085
+ if (arguments.length === 0) {
1086
+ return this.get();
1087
+ }
1088
+
1089
+ // Local value is just the raw value
1090
+ return this.invoke(
1091
+ value!,
1092
+ duration,
1093
+ timingFunction,
1094
+ interpolationFunction,
1095
+ ) as TOwner | SignalGenerator<number, number>;
1096
+ }
1097
+ }
1098
+
1099
+ // Custom setter function type for layout positions
1100
+ type LayoutPositionSetter<TOwner> = (
1101
+ value: SignalValue<PossibleVector2> | typeof DEFAULT,
1102
+ ) => TOwner;
1103
+
1104
+ // Layout position signal context (for computed position signals like top, left, etc.)
1105
+ export class LayoutPositionSignalContext<
1106
+ TOwner extends Node = Node,
1107
+ > extends SignalContext<PossibleVector2, Vector2, TOwner> {
1108
+ private readonly hasCustomDelegate: boolean;
1109
+
1110
+ public constructor(
1111
+ initial: SignalValue<PossibleVector2> | undefined,
1112
+ interpolation: InterpolationFunction<Vector2>,
1113
+ owner: TOwner,
1114
+ parser: (value: PossibleVector2) => Vector2 = value => new Vector2(value),
1115
+ extensions: Partial<SignalExtensions<PossibleVector2, Vector2>> = {},
1116
+ ) {
1117
+ super(initial, interpolation, owner, parser, extensions);
1118
+
1119
+ // Track if we have custom delegation behavior
1120
+ this.hasCustomDelegate = Boolean(extensions.getter || extensions.setter);
1121
+
1122
+ // Create enhanced transform methods with component access using shared helpers
1123
+ const absMethod = this.abs.bind(this);
1124
+
1125
+ // Add component methods to abs
1126
+ (absMethod as any).x = function (
1127
+ value?: SignalValue<number>,
1128
+ duration?: number,
1129
+ timingFunction?: TimingFunction,
1130
+ interpolationFunction?: InterpolationFunction<number>,
1131
+ ): number | TOwner | ThreadGenerator {
1132
+ if (arguments.length === 0) {
1133
+ return absMethod().x;
1134
+ }
1135
+ // Get the current absolute position once and preserve the y component
1136
+ const currentAbsPos = absMethod();
1137
+ const newAbsPos = new Vector2([unwrap(value!), currentAbsPos.y]);
1138
+ if (arguments.length === 1) {
1139
+ return absMethod(newAbsPos);
1140
+ }
1141
+ return absMethod(
1142
+ newAbsPos,
1143
+ duration!,
1144
+ timingFunction,
1145
+ interpolationFunction as any,
1146
+ ) as ThreadGenerator;
1147
+ };
1148
+
1149
+ (absMethod as any).y = function (
1150
+ value?: SignalValue<number>,
1151
+ duration?: number,
1152
+ timingFunction?: TimingFunction,
1153
+ interpolationFunction?: InterpolationFunction<number>,
1154
+ ): number | TOwner | ThreadGenerator {
1155
+ if (arguments.length === 0) {
1156
+ return absMethod().y;
1157
+ }
1158
+ // Get the current absolute position once and preserve the x component
1159
+ const currentAbsPos = absMethod();
1160
+ const newAbsPos = new Vector2([currentAbsPos.x, unwrap(value!)]);
1161
+ if (arguments.length === 1) {
1162
+ return absMethod(newAbsPos);
1163
+ }
1164
+ return absMethod(
1165
+ newAbsPos,
1166
+ duration!,
1167
+ timingFunction,
1168
+ interpolationFunction as any,
1169
+ ) as ThreadGenerator;
1170
+ };
1171
+
1172
+ Object.defineProperty(this.invokable, 'abs', {
1173
+ value: absMethod,
1174
+ enumerable: false,
1175
+ });
1176
+
1177
+ Object.defineProperty(this.invokable, 'relativeTo', {
1178
+ value: this.relativeTo.bind(this),
1179
+ enumerable: false,
1180
+ });
1181
+
1182
+ const viewMethod = this.view.bind(this);
1183
+
1184
+ // Add component methods to view
1185
+ (viewMethod as any).x = function (
1186
+ this: LayoutPositionSignalContext<TOwner>,
1187
+ value?: SignalValue<number>,
1188
+ duration?: number,
1189
+ timingFunction?: TimingFunction,
1190
+ interpolationFunction?: InterpolationFunction<number>,
1191
+ ): number | TOwner | ThreadGenerator {
1192
+ if (arguments.length === 0) {
1193
+ return viewMethod().x;
1194
+ }
1195
+
1196
+ // For view space component setting, we need to be more precise about preserving the other component
1197
+ // Get the current LOCAL position and current VIEW position
1198
+ const currentLocal = this.get();
1199
+ const currentView = currentLocal
1200
+ .transformAsPoint(this.owner.localToWorld())
1201
+ .transformAsPoint(this.owner.view().worldToLocal());
1202
+
1203
+ // Create target view position with only the x component changed
1204
+ const targetView = new Vector2([unwrap(value!), currentView.y]);
1205
+
1206
+ // Transform to local and set
1207
+ const targetLocal = new Vector2(
1208
+ unwrap(TransformConverter.viewToLocalPosition(this.owner, targetView)),
1209
+ );
1210
+
1211
+ if (arguments.length === 1) {
1212
+ return this.invoke(targetLocal) as TOwner;
1213
+ }
1214
+ return this.invoke(
1215
+ targetLocal,
1216
+ duration!,
1217
+ timingFunction,
1218
+ interpolationFunction as any,
1219
+ ) as ThreadGenerator;
1220
+ }.bind(this);
1221
+
1222
+ (viewMethod as any).y = function (
1223
+ this: LayoutPositionSignalContext<TOwner>,
1224
+ value?: SignalValue<number>,
1225
+ duration?: number,
1226
+ timingFunction?: TimingFunction,
1227
+ interpolationFunction?: InterpolationFunction<number>,
1228
+ ): number | TOwner | ThreadGenerator {
1229
+ if (arguments.length === 0) {
1230
+ return viewMethod().y;
1231
+ }
1232
+
1233
+ // Get the current LOCAL position and current VIEW position
1234
+ const currentLocal = this.get();
1235
+ const currentView = currentLocal
1236
+ .transformAsPoint(this.owner.localToWorld())
1237
+ .transformAsPoint(this.owner.view().worldToLocal());
1238
+
1239
+ // Create target view position with only the y component changed
1240
+ const targetView = new Vector2([currentView.x, unwrap(value!)]);
1241
+
1242
+ // Transform to local and set
1243
+ const targetLocal = new Vector2(
1244
+ unwrap(TransformConverter.viewToLocalPosition(this.owner, targetView)),
1245
+ );
1246
+
1247
+ if (arguments.length === 1) {
1248
+ return this.invoke(targetLocal) as TOwner;
1249
+ }
1250
+ return this.invoke(
1251
+ targetLocal,
1252
+ duration!,
1253
+ timingFunction,
1254
+ interpolationFunction as any,
1255
+ ) as ThreadGenerator;
1256
+ }.bind(this);
1257
+
1258
+ Object.defineProperty(this.invokable, 'view', {
1259
+ value: viewMethod,
1260
+ enumerable: false,
1261
+ });
1262
+
1263
+ const localMethod = this.local.bind(this);
1264
+
1265
+ // Add component methods to local
1266
+ (localMethod as any).x = function (
1267
+ value?: SignalValue<number>,
1268
+ duration?: number,
1269
+ timingFunction?: TimingFunction,
1270
+ interpolationFunction?: InterpolationFunction<number>,
1271
+ ): number | TOwner | ThreadGenerator {
1272
+ if (arguments.length === 0) {
1273
+ return localMethod().x;
1274
+ }
1275
+ // Get the current local position once and preserve the y component
1276
+ const currentLocalPos = localMethod();
1277
+ const newLocalPos = new Vector2([unwrap(value!), currentLocalPos.y]);
1278
+ if (arguments.length === 1) {
1279
+ return localMethod(newLocalPos);
1280
+ }
1281
+ return localMethod(
1282
+ newLocalPos,
1283
+ duration!,
1284
+ timingFunction,
1285
+ interpolationFunction as any,
1286
+ ) as ThreadGenerator;
1287
+ };
1288
+
1289
+ (localMethod as any).y = function (
1290
+ value?: SignalValue<number>,
1291
+ duration?: number,
1292
+ timingFunction?: TimingFunction,
1293
+ interpolationFunction?: InterpolationFunction<number>,
1294
+ ): number | TOwner | ThreadGenerator {
1295
+ if (arguments.length === 0) {
1296
+ return localMethod().y;
1297
+ }
1298
+ // Get the current local position once and preserve the x component
1299
+ const currentLocalPos = localMethod();
1300
+ const newLocalPos = new Vector2([currentLocalPos.x, unwrap(value!)]);
1301
+ if (arguments.length === 1) {
1302
+ return localMethod(newLocalPos);
1303
+ }
1304
+ return localMethod(
1305
+ newLocalPos,
1306
+ duration!,
1307
+ timingFunction,
1308
+ interpolationFunction as any,
1309
+ ) as ThreadGenerator;
1310
+ };
1311
+
1312
+ Object.defineProperty(this.invokable, 'local', {
1313
+ value: localMethod,
1314
+ enumerable: false,
1315
+ });
1316
+ }
1317
+
1318
+ /**
1319
+ * Override get to use custom getter if available.
1320
+ * This enables computed layout positions (e.g., top, left) to calculate their values dynamically.
1321
+ */
1322
+ public override get(): Vector2 {
1323
+ if (this.extensions.getter) {
1324
+ return this.extensions.getter();
1325
+ }
1326
+ return super.get();
1327
+ }
1328
+
1329
+ /**
1330
+ * Set a custom setter function for layout position delegation.
1331
+ * This allows layout signals to delegate setting behavior to position updates.
1332
+ */
1333
+ public setCustomSetter(setterFunc: LayoutPositionSetter<TOwner>): void {
1334
+ this.extensions.setter = setterFunc;
1335
+ }
1336
+
1337
+ /**
1338
+ * Override invoke to handle layout-specific delegation patterns.
1339
+ * Uses custom setter for simple value assignments when available.
1340
+ */
1341
+ public override invoke(
1342
+ value?: SignalValue<PossibleVector2> | typeof DEFAULT,
1343
+ duration?: number,
1344
+ timingFunction?: TimingFunction,
1345
+ interpolationFunction?: InterpolationFunction<Vector2>,
1346
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
1347
+ // Getter: no arguments
1348
+ if (arguments.length === GETTER_ARGS) {
1349
+ return this.get();
1350
+ }
1351
+
1352
+ // Setter: single argument with custom delegate
1353
+ if (
1354
+ arguments.length === SETTER_ARGS &&
1355
+ this.hasCustomDelegate &&
1356
+ this.extensions.setter
1357
+ ) {
1358
+ const result = this.extensions.setter(value!);
1359
+ return result !== undefined ? result : this.owner;
1360
+ }
1361
+
1362
+ // Animation or fallback to default behavior
1363
+ return super.invoke(value, duration, timingFunction, interpolationFunction);
1364
+ }
1365
+
1366
+ public override toSignal(): LayoutPositionSignal<TOwner> {
1367
+ return this.invokable as LayoutPositionSignal<TOwner>;
1368
+ }
1369
+
1370
+ public abs(): Vector2;
1371
+ public abs(value: SignalValue<PossibleVector2>): TOwner;
1372
+ public abs(
1373
+ value: SignalValue<PossibleVector2>,
1374
+ duration: number,
1375
+ timingFunction?: TimingFunction,
1376
+ interpolationFunction?: InterpolationFunction<Vector2>,
1377
+ ): SignalGenerator<PossibleVector2, Vector2>;
1378
+ public abs(
1379
+ value?: SignalValue<PossibleVector2>,
1380
+ duration?: number,
1381
+ timingFunction?: TimingFunction,
1382
+ interpolationFunction?: InterpolationFunction<Vector2>,
1383
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
1384
+ if (arguments.length === 0) {
1385
+ // For layout position signals, get the current computed value
1386
+ const currentValue = this.get();
1387
+ return currentValue.transformAsPoint(this.owner.localToWorld());
1388
+ }
1389
+
1390
+ // Convert absolute value to local value for this layout position and set it
1391
+ const localValue = TransformConverter.absoluteToLocalLayoutPosition(
1392
+ this.owner,
1393
+ value!,
1394
+ );
1395
+ return this.invoke(
1396
+ localValue,
1397
+ duration,
1398
+ timingFunction,
1399
+ interpolationFunction,
1400
+ ) as TOwner | SignalGenerator<PossibleVector2, Vector2>;
1401
+ }
1402
+
1403
+ public relativeTo(node: Node): Vector2;
1404
+ public relativeTo(node: Node, value: SignalValue<PossibleVector2>): TOwner;
1405
+ public relativeTo(
1406
+ node: Node,
1407
+ value: SignalValue<PossibleVector2>,
1408
+ duration: number,
1409
+ timingFunction?: TimingFunction,
1410
+ interpolationFunction?: InterpolationFunction<Vector2>,
1411
+ ): SignalGenerator<PossibleVector2, Vector2>;
1412
+ public relativeTo(
1413
+ node: Node,
1414
+ value?: SignalValue<PossibleVector2>,
1415
+ duration?: number,
1416
+ timingFunction?: TimingFunction,
1417
+ interpolationFunction?: InterpolationFunction<Vector2>,
1418
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
1419
+ if (arguments.length === 1) {
1420
+ // For layout positions, calculate relative position using absolute coordinates
1421
+ const currentValue = this.get();
1422
+ const absPosition = currentValue.transformAsPoint(
1423
+ this.owner.localToWorld(),
1424
+ );
1425
+ const targetAbsPosition = node.absolutePosition();
1426
+ return absPosition.sub(targetAbsPosition);
1427
+ }
1428
+
1429
+ // Convert relative value to local value and set it
1430
+ const absoluteValue = TransformConverter.relativeToAbsolutePosition(
1431
+ node,
1432
+ value!,
1433
+ );
1434
+ const localValue = TransformConverter.absoluteToLocalLayoutPosition(
1435
+ this.owner,
1436
+ absoluteValue,
1437
+ );
1438
+ return this.invoke(
1439
+ localValue,
1440
+ duration,
1441
+ timingFunction,
1442
+ interpolationFunction,
1443
+ ) as TOwner | SignalGenerator<PossibleVector2, Vector2>;
1444
+ }
1445
+
1446
+ public view(): Vector2;
1447
+ public view(value: SignalValue<PossibleVector2>): TOwner;
1448
+ public view(
1449
+ value: SignalValue<PossibleVector2>,
1450
+ duration: number,
1451
+ timingFunction?: TimingFunction,
1452
+ interpolationFunction?: InterpolationFunction<Vector2>,
1453
+ ): SignalGenerator<PossibleVector2, Vector2>;
1454
+ public view(
1455
+ value?: SignalValue<PossibleVector2>,
1456
+ duration?: number,
1457
+ timingFunction?: TimingFunction,
1458
+ interpolationFunction?: InterpolationFunction<Vector2>,
1459
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
1460
+ if (arguments.length === 0) {
1461
+ const currentValue = this.get();
1462
+ return currentValue.transformAsPoint(this.owner.view().localToWorld());
1463
+ }
1464
+
1465
+ // Convert view space value to local value and set it
1466
+ const localValue = TransformConverter.viewToLocalPosition(
1467
+ this.owner,
1468
+ value!,
1469
+ );
1470
+ return this.invoke(
1471
+ localValue,
1472
+ duration,
1473
+ timingFunction,
1474
+ interpolationFunction,
1475
+ ) as TOwner | SignalGenerator<PossibleVector2, Vector2>;
1476
+ }
1477
+
1478
+ public local(): Vector2;
1479
+ public local(value: SignalValue<PossibleVector2>): TOwner;
1480
+ public local(
1481
+ value: SignalValue<PossibleVector2>,
1482
+ duration: number,
1483
+ timingFunction?: TimingFunction,
1484
+ interpolationFunction?: InterpolationFunction<Vector2>,
1485
+ ): SignalGenerator<PossibleVector2, Vector2>;
1486
+ public local(
1487
+ value?: SignalValue<PossibleVector2>,
1488
+ duration?: number,
1489
+ timingFunction?: TimingFunction,
1490
+ interpolationFunction?: InterpolationFunction<Vector2>,
1491
+ ): Vector2 | TOwner | SignalGenerator<PossibleVector2, Vector2> {
1492
+ if (arguments.length === 0) {
1493
+ return this.get();
1494
+ }
1495
+
1496
+ // Local value is just the raw value
1497
+ return this.invoke(
1498
+ value!,
1499
+ duration,
1500
+ timingFunction,
1501
+ interpolationFunction,
1502
+ ) as TOwner | SignalGenerator<PossibleVector2, Vector2>;
1503
+ }
1504
+ }
1505
+
1506
+ /**
1507
+ * Creates an enhanced position signal with coordinate space transformation methods.
1508
+ *
1509
+ * This decorator creates a position signal that supports operations in multiple coordinate spaces:
1510
+ * - **Local**: Relative to the node's parent
1511
+ * - **Absolute**: In the global scene coordinates
1512
+ * - **View**: In the camera/view coordinate system
1513
+ * - **Relative**: Relative to another specific node
1514
+ *
1515
+ * @param prefix - Optional prefix for the underlying X/Y property names.
1516
+ * Can be a string (e.g., 'scale' creates 'scaleX'/'scaleY') or
1517
+ * an object mapping \{x: 'propX', y: 'propY'\}
1518
+ *
1519
+ * @example
1520
+ * ```typescript
1521
+ * class MyNode extends Node {
1522
+ * \@positionSignal()
1523
+ * declare readonly position: PositionSignal<this>;
1524
+ *
1525
+ * \@positionSignal('anchor')
1526
+ * declare readonly anchor: PositionSignal<this>; // Uses anchorX/anchorY
1527
+ * }
1528
+ * ```
1529
+ */
1530
+ export function positionSignal(
1531
+ prefix?: string | Record<string, string>,
1532
+ ): PropertyDecorator {
1533
+ return (target, key) => {
1534
+ compound(
1535
+ typeof prefix === 'object'
1536
+ ? prefix
1537
+ : {
1538
+ x: prefix ? `${prefix}X` : 'x',
1539
+ y: prefix ? `${prefix}Y` : 'y',
1540
+ },
1541
+ PositionSignalContext,
1542
+ )(target, key);
1543
+ wrapper(Vector2)(target, key);
1544
+ };
1545
+ }
1546
+
1547
+ /**
1548
+ * Creates an enhanced scale signal with coordinate space transformation methods.
1549
+ *
1550
+ * Scale signals support operations in multiple coordinate spaces:
1551
+ * - **Local**: Scale relative to the node's parent
1552
+ * - **Absolute**: Scale in global scene coordinates
1553
+ * - **View**: Scale in camera/view coordinate system
1554
+ * - **Relative**: Scale relative to another node's scale
1555
+ *
1556
+ * @param prefix - Optional prefix for the underlying X/Y property names
1557
+ *
1558
+ * @example
1559
+ * ```typescript
1560
+ * class MyNode extends Node {
1561
+ * \@scaleSignal('scale')
1562
+ * declare readonly scale: ScaleSignal<this>;
1563
+ * }
1564
+ *
1565
+ * // Usage:
1566
+ * node.scale([2, 1.5]); // Local scale
1567
+ * node.scale.abs([4, 3]); // Absolute scale
1568
+ * node.scale.relativeTo(parent, [0.5, 0.5]); // Scale relative to parent
1569
+ * ```
1570
+ */
1571
+ export function scaleSignal(
1572
+ prefix?: string | Record<string, string>,
1573
+ ): PropertyDecorator {
1574
+ return (target, key) => {
1575
+ compound(
1576
+ typeof prefix === 'object'
1577
+ ? prefix
1578
+ : {
1579
+ x: prefix ? `${prefix}X` : 'x',
1580
+ y: prefix ? `${prefix}Y` : 'y',
1581
+ },
1582
+ ScaleSignalContext,
1583
+ )(target, key);
1584
+ wrapper(Vector2)(target, key);
1585
+ };
1586
+ }
1587
+
1588
+ /**
1589
+ * Creates an enhanced rotation signal with coordinate space transformation methods.
1590
+ *
1591
+ * Rotation signals work with degrees and support operations in multiple coordinate spaces:
1592
+ * - **Local**: Rotation relative to the node's parent
1593
+ * - **Absolute**: Rotation in global scene coordinates
1594
+ * - **View**: Rotation in camera/view coordinate system
1595
+ * - **Relative**: Rotation relative to another node's rotation
1596
+ *
1597
+ * @example
1598
+ * ```typescript
1599
+ * class MyNode extends Node {
1600
+ * \@rotationSignal()
1601
+ * declare readonly rotation: RotationSignal<this>;
1602
+ * }
1603
+ *
1604
+ * // Usage:
1605
+ * node.rotation(45); // Local rotation (45 degrees)
1606
+ * node.rotation.abs(90); // Absolute rotation (90 degrees)
1607
+ * node.rotation.relativeTo(other, 180); // 180 degrees relative to other node
1608
+ * ```
1609
+ */
1610
+ export function rotationSignal(): PropertyDecorator {
1611
+ return (target, key) => {
1612
+ const meta = getPropertyMetaOrCreate<number>(target, key);
1613
+ addInitializer(target, (instance: Node) => {
1614
+ const initial = meta.default;
1615
+ const parser = meta.parser?.bind(instance) ?? ((value: number) => value);
1616
+ const signalContext = new RotationSignalContext(
1617
+ initial,
1618
+ meta.interpolationFunction ?? deepLerp,
1619
+ instance,
1620
+ parser,
1621
+ makeSignalExtensions(meta, instance, key as string),
1622
+ );
1623
+
1624
+ // Use Object.defineProperty to avoid casting
1625
+ Object.defineProperty(instance, key, {
1626
+ value: signalContext.toSignal(),
1627
+ writable: false,
1628
+ enumerable: true,
1629
+ configurable: false,
1630
+ });
1631
+ });
1632
+ };
1633
+ }