onejs-react 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/vector.ts ADDED
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Vector drawing utilities for OneJS.
3
+ *
4
+ * Provides Transform2D for applying 2D transformations to drawing coordinates,
5
+ * since Unity's Painter2D doesn't have built-in transform support.
6
+ *
7
+ * Also provides useVectorContent hook for automatic repaint on dependency changes.
8
+ */
9
+
10
+ import { useRef, useEffect, useCallback, type DependencyList, type RefObject } from 'react'
11
+ import type { Vector2, VisualElement, MeshGenerationContext, GenerateVisualContentCallback } from './types'
12
+
13
+ // Global declarations for Unity interop
14
+ declare const CS: {
15
+ UnityEngine: {
16
+ Vector2: new (x: number, y: number) => Vector2;
17
+ };
18
+ };
19
+
20
+ /**
21
+ * 2D transformation helper for vector drawing.
22
+ *
23
+ * Unity's Painter2D doesn't support transforms (translate, rotate, scale).
24
+ * This class provides client-side matrix math to transform coordinates
25
+ * before passing them to Painter2D methods.
26
+ *
27
+ * @example
28
+ * import { Transform2D } from "onejs-react"
29
+ *
30
+ * <View
31
+ * style={{ width: 200, height: 200 }}
32
+ * onGenerateVisualContent={(mgc) => {
33
+ * const p = mgc.painter2D
34
+ * const t = new Transform2D()
35
+ *
36
+ * // Center and rotate 45 degrees
37
+ * t.translate(100, 100)
38
+ * t.rotate(Math.PI / 4)
39
+ *
40
+ * // Draw a square using transformed coordinates
41
+ * p.BeginPath()
42
+ * p.MoveTo(t.point(-40, -40))
43
+ * p.LineTo(t.point(40, -40))
44
+ * p.LineTo(t.point(40, 40))
45
+ * p.LineTo(t.point(-40, 40))
46
+ * p.ClosePath()
47
+ * p.Fill()
48
+ * }}
49
+ * />
50
+ */
51
+ export class Transform2D {
52
+ // Current transformation matrix (a, b, c, d, e, f)
53
+ // [ a c e ] [ x ] [ a*x + c*y + e ]
54
+ // [ b d f ] * [ y ] = [ b*x + d*y + f ]
55
+ // [ 0 0 1 ] [ 1 ] [ 1 ]
56
+ private _a: number = 1
57
+ private _b: number = 0
58
+ private _c: number = 0
59
+ private _d: number = 1
60
+ private _e: number = 0
61
+ private _f: number = 0
62
+
63
+ // State stack for save/restore
64
+ private _stack: Array<[number, number, number, number, number, number]> = []
65
+
66
+ /**
67
+ * Save the current transformation state to the stack.
68
+ * Use restore() to return to this state later.
69
+ */
70
+ save(): void {
71
+ this._stack.push([this._a, this._b, this._c, this._d, this._e, this._f])
72
+ }
73
+
74
+ /**
75
+ * Restore the most recently saved transformation state.
76
+ * If the stack is empty, resets to identity.
77
+ */
78
+ restore(): void {
79
+ const state = this._stack.pop()
80
+ if (state) {
81
+ [this._a, this._b, this._c, this._d, this._e, this._f] = state
82
+ } else {
83
+ this.reset()
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Reset the transformation to identity (no transformation).
89
+ */
90
+ reset(): void {
91
+ this._a = 1
92
+ this._b = 0
93
+ this._c = 0
94
+ this._d = 1
95
+ this._e = 0
96
+ this._f = 0
97
+ }
98
+
99
+ /**
100
+ * Apply a translation (move the origin).
101
+ * @param x - Horizontal translation
102
+ * @param y - Vertical translation
103
+ */
104
+ translate(x: number, y: number): void {
105
+ // new_e = a*x + c*y + e
106
+ // new_f = b*x + d*y + f
107
+ this._e += this._a * x + this._c * y
108
+ this._f += this._b * x + this._d * y
109
+ }
110
+
111
+ /**
112
+ * Apply a rotation around the current origin.
113
+ * @param angle - Rotation angle in radians (clockwise)
114
+ */
115
+ rotate(angle: number): void {
116
+ const cos = Math.cos(angle)
117
+ const sin = Math.sin(angle)
118
+
119
+ // Multiply current matrix by rotation matrix
120
+ const a = this._a * cos + this._c * sin
121
+ const b = this._b * cos + this._d * sin
122
+ const c = this._a * -sin + this._c * cos
123
+ const d = this._b * -sin + this._d * cos
124
+
125
+ this._a = a
126
+ this._b = b
127
+ this._c = c
128
+ this._d = d
129
+ }
130
+
131
+ /**
132
+ * Apply a scale transformation.
133
+ * @param x - Horizontal scale factor
134
+ * @param y - Vertical scale factor (defaults to x for uniform scale)
135
+ */
136
+ scale(x: number, y?: number): void {
137
+ const sy = y ?? x
138
+
139
+ this._a *= x
140
+ this._b *= x
141
+ this._c *= sy
142
+ this._d *= sy
143
+ }
144
+
145
+ /**
146
+ * Transform a point using the current transformation matrix.
147
+ * @param x - X coordinate in local space
148
+ * @param y - Y coordinate in local space
149
+ * @returns Transformed point as Unity Vector2
150
+ */
151
+ point(x: number, y: number): Vector2 {
152
+ const tx = this._a * x + this._c * y + this._e
153
+ const ty = this._b * x + this._d * y + this._f
154
+ return new CS.UnityEngine.Vector2(tx, ty)
155
+ }
156
+
157
+ /**
158
+ * Transform multiple points at once.
159
+ * @param coords - Array of [x, y] coordinate pairs
160
+ * @returns Array of transformed Vector2 points
161
+ *
162
+ * @example
163
+ * const corners = t.points([-40, -40], [40, -40], [40, 40], [-40, 40])
164
+ * p.MoveTo(corners[0])
165
+ * for (let i = 1; i < corners.length; i++) p.LineTo(corners[i])
166
+ */
167
+ points(...coords: [number, number][]): Vector2[] {
168
+ return coords.map(([x, y]) => this.point(x, y))
169
+ }
170
+
171
+ /**
172
+ * Get the raw transformation values.
173
+ * Useful for debugging or advanced matrix operations.
174
+ *
175
+ * Returns [a, b, c, d, e, f] where:
176
+ * - a, d: scale
177
+ * - b, c: rotation/skew
178
+ * - e, f: translation
179
+ */
180
+ get values(): [number, number, number, number, number, number] {
181
+ return [this._a, this._b, this._c, this._d, this._e, this._f]
182
+ }
183
+
184
+ /**
185
+ * Set the transformation matrix directly.
186
+ * @param a - Horizontal scale (1 = no scale)
187
+ * @param b - Vertical skew
188
+ * @param c - Horizontal skew
189
+ * @param d - Vertical scale (1 = no scale)
190
+ * @param e - Horizontal translation
191
+ * @param f - Vertical translation
192
+ */
193
+ setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void {
194
+ this._a = a
195
+ this._b = b
196
+ this._c = c
197
+ this._d = d
198
+ this._e = e
199
+ this._f = f
200
+ }
201
+
202
+ /**
203
+ * Multiply the current matrix by another matrix.
204
+ * @param a - Horizontal scale
205
+ * @param b - Vertical skew
206
+ * @param c - Horizontal skew
207
+ * @param d - Vertical scale
208
+ * @param e - Horizontal translation
209
+ * @param f - Vertical translation
210
+ */
211
+ transform(a: number, b: number, c: number, d: number, e: number, f: number): void {
212
+ const a_ = this._a * a + this._c * b
213
+ const b_ = this._b * a + this._d * b
214
+ const c_ = this._a * c + this._c * d
215
+ const d_ = this._b * c + this._d * d
216
+ const e_ = this._a * e + this._c * f + this._e
217
+ const f_ = this._b * e + this._d * f + this._f
218
+
219
+ this._a = a_
220
+ this._b = b_
221
+ this._c = c_
222
+ this._d = d_
223
+ this._e = e_
224
+ this._f = f_
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Hook for vector drawing with automatic repaint on dependency changes.
230
+ *
231
+ * Returns a ref to attach to a VisualElement. When dependencies change,
232
+ * automatically calls MarkDirtyRepaint() to trigger a redraw.
233
+ *
234
+ * @param draw - Drawing callback that receives MeshGenerationContext
235
+ * @param deps - Dependency array (like useEffect) - repaint when these change
236
+ * @returns Ref to attach to the element
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * function AnimatedCircle() {
241
+ * const [radius, setRadius] = useState(50)
242
+ *
243
+ * const ref = useVectorContent((mgc) => {
244
+ * const p = mgc.painter2D
245
+ * const Angle = CS.UnityEngine.UIElements.Angle
246
+ *
247
+ * p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1)
248
+ * p.BeginPath()
249
+ * p.Arc(
250
+ * new CS.UnityEngine.Vector2(100, 100),
251
+ * radius,
252
+ * Angle.Degrees(0),
253
+ * Angle.Degrees(360),
254
+ * CS.UnityEngine.UIElements.ArcDirection.Clockwise
255
+ * )
256
+ * p.Fill()
257
+ * }, [radius]) // Auto-repaints when radius changes
258
+ *
259
+ * return <View ref={ref} style={{ width: 200, height: 200 }} />
260
+ * }
261
+ * ```
262
+ */
263
+ export function useVectorContent(
264
+ draw: GenerateVisualContentCallback,
265
+ deps: DependencyList = []
266
+ ): RefObject<VisualElement | null> {
267
+ const ref = useRef<VisualElement | null>(null)
268
+ const drawRef = useRef(draw)
269
+
270
+ // Keep drawRef current
271
+ drawRef.current = draw
272
+
273
+ // Register callback and handle updates
274
+ useEffect(() => {
275
+ const element = ref.current
276
+ if (!element) return
277
+
278
+ // Create a stable wrapper that always calls the latest draw function
279
+ const callback: GenerateVisualContentCallback = (mgc) => {
280
+ drawRef.current(mgc)
281
+ }
282
+
283
+ // Assign the callback to generateVisualContent
284
+ // Use unknown cast because VisualElement interface doesn't expose this property directly
285
+ const el = element as unknown as { generateVisualContent: GenerateVisualContentCallback | null }
286
+ el.generateVisualContent = callback
287
+
288
+ // Initial repaint to render content
289
+ element.MarkDirtyRepaint()
290
+
291
+ return () => {
292
+ // Clear callback on cleanup
293
+ el.generateVisualContent = null
294
+ }
295
+ }, []) // Only run once on mount
296
+
297
+ // Trigger repaint when dependencies change (but not on first render)
298
+ const isFirstRender = useRef(true)
299
+ useEffect(() => {
300
+ if (isFirstRender.current) {
301
+ isFirstRender.current = false
302
+ return
303
+ }
304
+
305
+ const element = ref.current
306
+ if (element) {
307
+ element.MarkDirtyRepaint()
308
+ }
309
+ }, deps)
310
+
311
+ return ref
312
+ }