onejs-react 0.1.1 → 0.1.3
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/README.md +182 -1
- package/package.json +4 -3
- package/src/host-config.ts +165 -7
- package/src/index.ts +11 -0
- package/src/style-parser.ts +129 -9
- package/src/types.ts +130 -3
- package/src/vector.ts +312 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ React 19 reconciler for Unity's UI Toolkit.
|
|
|
10
10
|
| `src/renderer.ts` | Entry point: `render(element, container)` |
|
|
11
11
|
| `src/components.tsx` | Component wrappers: View, Text, Label, Button, TextField, etc. |
|
|
12
12
|
| `src/screen.tsx` | Responsive design: ScreenProvider, useBreakpoint, useScreenSize, useResponsive |
|
|
13
|
-
| `src/types.ts` | TypeScript type definitions |
|
|
13
|
+
| `src/types.ts` | TypeScript type definitions (includes Vector Drawing types) |
|
|
14
14
|
| `src/index.ts` | Package exports |
|
|
15
15
|
|
|
16
16
|
## Components
|
|
@@ -48,6 +48,70 @@ function App() {
|
|
|
48
48
|
render(<App />, __root);
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Type Usage Guide
|
|
52
|
+
|
|
53
|
+
OneJS has multiple type sources. Here's when to use each:
|
|
54
|
+
|
|
55
|
+
### React Components (Most Common)
|
|
56
|
+
|
|
57
|
+
Import types from `onejs-react` for refs and component props:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { View, Button, VisualElement, ButtonElement } from "onejs-react"
|
|
61
|
+
|
|
62
|
+
function MyComponent() {
|
|
63
|
+
const viewRef = useRef<VisualElement>(null)
|
|
64
|
+
const buttonRef = useRef<ButtonElement>(null)
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
buttonRef.current?.Focus()
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View ref={viewRef}>
|
|
72
|
+
<Button ref={buttonRef} text="Click me" />
|
|
73
|
+
</View>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Imperative Element Creation
|
|
79
|
+
|
|
80
|
+
For creating elements outside React, use `unity-types`:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { Button } from "UnityEngine.UIElements"
|
|
84
|
+
|
|
85
|
+
const btn = new Button()
|
|
86
|
+
btn.text = "Dynamic Button"
|
|
87
|
+
__root.Add(btn)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### render() Container
|
|
91
|
+
|
|
92
|
+
The `render()` function accepts any `RenderContainer`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { render, RenderContainer } from "onejs-react"
|
|
96
|
+
|
|
97
|
+
// __root is provided by the runtime
|
|
98
|
+
render(<App />, __root)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Type Hierarchy
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
RenderContainer (minimal: __csHandle, __csType)
|
|
105
|
+
└── VisualElement (full API: style, hierarchy, events)
|
|
106
|
+
├── TextElement (+ text property)
|
|
107
|
+
│ ├── LabelElement
|
|
108
|
+
│ └── ButtonElement
|
|
109
|
+
├── TextFieldElement (+ value, isPasswordField, etc.)
|
|
110
|
+
├── ToggleElement (+ value: boolean)
|
|
111
|
+
├── SliderElement (+ value, lowValue, highValue)
|
|
112
|
+
└── ScrollViewElement (+ scrollOffset, ScrollTo)
|
|
113
|
+
```
|
|
114
|
+
|
|
51
115
|
## Key Concepts
|
|
52
116
|
|
|
53
117
|
- **Element types**: Use `ojs-` prefix internally (e.g., `ojs-view`, `ojs-button`) to avoid conflicts with HTML types
|
|
@@ -77,6 +141,123 @@ Test suite uses Vitest with mocked Unity CS globals. Tests are in `src/__tests__
|
|
|
77
141
|
| `mocks.ts` | Mock implementations of Unity UI Toolkit classes |
|
|
78
142
|
| `setup.ts` | Global test setup for CS, __eventAPI |
|
|
79
143
|
|
|
144
|
+
## Vector Drawing
|
|
145
|
+
|
|
146
|
+
OneJS exposes Unity's `Painter2D` API for GPU-accelerated vector graphics. Any element can render custom vector content via `onGenerateVisualContent`.
|
|
147
|
+
|
|
148
|
+
### Basic Usage
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { View, render } from "onejs-react"
|
|
152
|
+
|
|
153
|
+
function Circle() {
|
|
154
|
+
return (
|
|
155
|
+
<View
|
|
156
|
+
style={{ width: 200, height: 200, backgroundColor: "#333" }}
|
|
157
|
+
onGenerateVisualContent={(mgc) => {
|
|
158
|
+
const p = mgc.painter2D
|
|
159
|
+
|
|
160
|
+
// Draw a filled circle
|
|
161
|
+
p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1) // Red
|
|
162
|
+
p.BeginPath()
|
|
163
|
+
p.Arc(
|
|
164
|
+
new CS.UnityEngine.Vector2(100, 100), // center
|
|
165
|
+
80, // radius
|
|
166
|
+
CS.UnityEngine.UIElements.Angle.Degrees(0),
|
|
167
|
+
CS.UnityEngine.UIElements.Angle.Degrees(360),
|
|
168
|
+
CS.UnityEngine.UIElements.ArcDirection.Clockwise
|
|
169
|
+
)
|
|
170
|
+
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Painter2D Methods
|
|
178
|
+
|
|
179
|
+
Path operations:
|
|
180
|
+
- `BeginPath()` - Start a new path
|
|
181
|
+
- `ClosePath()` - Close the current subpath
|
|
182
|
+
- `MoveTo(point)` - Move to point without drawing
|
|
183
|
+
- `LineTo(point)` - Draw line to point
|
|
184
|
+
- `Arc(center, radius, startAngle, endAngle, direction)` - Draw arc
|
|
185
|
+
- `ArcTo(p1, p2, radius)` - Draw arc tangent to two lines
|
|
186
|
+
- `BezierCurveTo(cp1, cp2, end)` - Cubic bezier curve
|
|
187
|
+
- `QuadraticCurveTo(cp, end)` - Quadratic bezier curve
|
|
188
|
+
|
|
189
|
+
Rendering:
|
|
190
|
+
- `Fill(fillRule)` - Fill the current path
|
|
191
|
+
- `Stroke()` - Stroke the current path
|
|
192
|
+
|
|
193
|
+
Properties:
|
|
194
|
+
- `fillColor` - Fill color (Unity Color)
|
|
195
|
+
- `strokeColor` - Stroke color (Unity Color)
|
|
196
|
+
- `lineWidth` - Stroke width in pixels
|
|
197
|
+
- `lineCap` - Line cap style (Butt, Round, Square)
|
|
198
|
+
- `lineJoin` - Line join style (Miter, Round, Bevel)
|
|
199
|
+
|
|
200
|
+
### Triggering Repaints
|
|
201
|
+
|
|
202
|
+
Use `MarkDirtyRepaint()` to trigger a repaint when drawing state changes:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
function AnimatedCircle() {
|
|
206
|
+
const ref = useRef<VisualElement>(null)
|
|
207
|
+
const [radius, setRadius] = useState(50)
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
// Trigger repaint when radius changes
|
|
211
|
+
ref.current?.MarkDirtyRepaint()
|
|
212
|
+
}, [radius])
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<View
|
|
216
|
+
ref={ref}
|
|
217
|
+
style={{ width: 200, height: 200 }}
|
|
218
|
+
onGenerateVisualContent={(mgc) => {
|
|
219
|
+
const p = mgc.painter2D
|
|
220
|
+
p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
|
|
221
|
+
p.BeginPath()
|
|
222
|
+
p.Arc(
|
|
223
|
+
new CS.UnityEngine.Vector2(100, 100),
|
|
224
|
+
radius,
|
|
225
|
+
CS.UnityEngine.UIElements.Angle.Degrees(0),
|
|
226
|
+
CS.UnityEngine.UIElements.Angle.Degrees(360),
|
|
227
|
+
CS.UnityEngine.UIElements.ArcDirection.Clockwise
|
|
228
|
+
)
|
|
229
|
+
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Differences from HTML5 Canvas
|
|
237
|
+
|
|
238
|
+
| Feature | Unity Painter2D | HTML5 Canvas |
|
|
239
|
+
|---------|-----------------|--------------|
|
|
240
|
+
| Transforms | Manual point calculation | Built-in translate/rotate/scale |
|
|
241
|
+
| Gradients | Limited (strokeGradient) | Full linear/radial/conic |
|
|
242
|
+
| State Stack | Not built-in | save()/restore() |
|
|
243
|
+
| Text | Via MeshGenerationContext.DrawText() | fillText/strokeText |
|
|
244
|
+
| Shadows | Not available | shadowBlur, shadowColor |
|
|
245
|
+
| Clipping | Via nested VisualElements | clip() path-based |
|
|
246
|
+
|
|
247
|
+
### Types
|
|
248
|
+
|
|
249
|
+
The following types are re-exported from `unity-types`:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
type Vector2 = CS.UnityEngine.Vector2
|
|
253
|
+
type Color = CS.UnityEngine.Color
|
|
254
|
+
type Angle = CS.UnityEngine.UIElements.Angle
|
|
255
|
+
type ArcDirection = CS.UnityEngine.UIElements.ArcDirection
|
|
256
|
+
type Painter2D = CS.UnityEngine.UIElements.Painter2D
|
|
257
|
+
type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContext
|
|
258
|
+
type GenerateVisualContentCallback = (context: MeshGenerationContext) => void
|
|
259
|
+
```
|
|
260
|
+
|
|
80
261
|
## Dependencies
|
|
81
262
|
|
|
82
263
|
- `react-reconciler@0.31.x` (React 19 compatible)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "React 19 renderer for OneJS (Unity UI Toolkit)",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
],
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/
|
|
20
|
+
"url": "https://github.com/Singtaa/onejs-react"
|
|
21
21
|
},
|
|
22
|
-
"author": "
|
|
22
|
+
"author": "Singtaa",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
25
|
"test": "vitest run",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@types/react-reconciler": "^0.28.9",
|
|
39
39
|
"react": "^19.0.0",
|
|
40
40
|
"typescript": "^5.7.0",
|
|
41
|
+
"unity-types": "file:../unity-types",
|
|
41
42
|
"vitest": "^2.1.0"
|
|
42
43
|
},
|
|
43
44
|
"license": "MIT"
|
package/src/host-config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {HostConfig} from 'react-reconciler';
|
|
2
|
-
import type {BaseProps, ViewStyle, VisualElement} from './types';
|
|
3
|
-
import {parseStyleValue} from './style-parser';
|
|
2
|
+
import type {BaseProps, ViewStyle, VisualElement, GenerateVisualContentCallback} from './types';
|
|
3
|
+
import {parseStyleValue, parseColor} from './style-parser';
|
|
4
4
|
|
|
5
5
|
// CSObject is an alias for VisualElement - they represent the same C# objects
|
|
6
6
|
type CSObject = VisualElement;
|
|
@@ -53,6 +53,7 @@ declare const CS: {
|
|
|
53
53
|
ListViewReorderMode: CSEnum;
|
|
54
54
|
AlternatingRowBackground: CSEnum;
|
|
55
55
|
CollectionVirtualizationMethod: CSEnum;
|
|
56
|
+
DisplayStyle: CSEnum;
|
|
56
57
|
};
|
|
57
58
|
};
|
|
58
59
|
OneJS: {
|
|
@@ -143,6 +144,8 @@ export interface Instance {
|
|
|
143
144
|
mergedInto?: Instance;
|
|
144
145
|
// Set to true when a non-text child is added, disabling further text merging
|
|
145
146
|
hasMixedContent?: boolean;
|
|
147
|
+
// For vector drawing: track the current generateVisualContent callback
|
|
148
|
+
visualContentCallback?: GenerateVisualContentCallback;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
export type TextInstance = Instance; // For Label elements with text content
|
|
@@ -432,6 +435,38 @@ function applyEvents(instance: Instance, props: BaseProps) {
|
|
|
432
435
|
}
|
|
433
436
|
}
|
|
434
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Apply generateVisualContent callback for vector drawing.
|
|
440
|
+
* Uses Unity's generateVisualContent delegate on VisualElement.
|
|
441
|
+
*
|
|
442
|
+
* This follows the same pattern as ListView's makeItem/bindItem callbacks -
|
|
443
|
+
* we assign JS functions directly to C# delegate properties via the interop layer.
|
|
444
|
+
*/
|
|
445
|
+
function applyVisualContentCallback(instance: Instance, props: BaseProps) {
|
|
446
|
+
const callback = props.onGenerateVisualContent;
|
|
447
|
+
const existingCallback = instance.visualContentCallback;
|
|
448
|
+
|
|
449
|
+
if (callback !== existingCallback) {
|
|
450
|
+
const element = instance.element as unknown as { generateVisualContent: GenerateVisualContentCallback | null };
|
|
451
|
+
|
|
452
|
+
// Remove old callback if exists
|
|
453
|
+
if (existingCallback) {
|
|
454
|
+
// Clear the delegate via C# interop
|
|
455
|
+
element.generateVisualContent = null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Add new callback if provided
|
|
459
|
+
if (callback) {
|
|
460
|
+
// Assign callback to generateVisualContent property
|
|
461
|
+
// The C# interop layer handles the delegate conversion
|
|
462
|
+
element.generateVisualContent = callback;
|
|
463
|
+
instance.visualContentCallback = callback;
|
|
464
|
+
} else {
|
|
465
|
+
instance.visualContentCallback = undefined;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
435
470
|
// MARK: Text Merging
|
|
436
471
|
// Rebuild concatenated text from merged text children
|
|
437
472
|
function rebuildMergedText(instance: Instance) {
|
|
@@ -541,6 +576,111 @@ function setValueProp<T>(target: T, key: keyof T, props: Record<string, unknown>
|
|
|
541
576
|
}
|
|
542
577
|
}
|
|
543
578
|
|
|
579
|
+
// Apply TextField-specific properties
|
|
580
|
+
function applyTextFieldProps(element: CSObject, props: Record<string, unknown>) {
|
|
581
|
+
// Map readOnly prop to isReadOnly property
|
|
582
|
+
if (props.readOnly !== undefined) {
|
|
583
|
+
(element as { isReadOnly: boolean }).isReadOnly = props.readOnly as boolean;
|
|
584
|
+
}
|
|
585
|
+
if (props.multiline !== undefined) {
|
|
586
|
+
(element as { multiline: boolean }).multiline = props.multiline as boolean;
|
|
587
|
+
}
|
|
588
|
+
if (props.maxLength !== undefined) {
|
|
589
|
+
(element as { maxLength: number }).maxLength = props.maxLength as number;
|
|
590
|
+
}
|
|
591
|
+
if (props.isPasswordField !== undefined) {
|
|
592
|
+
(element as { isPasswordField: boolean }).isPasswordField = props.isPasswordField as boolean;
|
|
593
|
+
}
|
|
594
|
+
if (props.maskChar !== undefined) {
|
|
595
|
+
(element as { maskChar: string }).maskChar = (props.maskChar as string).charAt(0);
|
|
596
|
+
}
|
|
597
|
+
if (props.isDelayed !== undefined) {
|
|
598
|
+
(element as { isDelayed: boolean }).isDelayed = props.isDelayed as boolean;
|
|
599
|
+
}
|
|
600
|
+
if (props.selectAllOnFocus !== undefined) {
|
|
601
|
+
(element as { selectAllOnFocus: boolean }).selectAllOnFocus = props.selectAllOnFocus as boolean;
|
|
602
|
+
}
|
|
603
|
+
if (props.selectAllOnMouseUp !== undefined) {
|
|
604
|
+
(element as { selectAllOnMouseUp: boolean }).selectAllOnMouseUp = props.selectAllOnMouseUp as boolean;
|
|
605
|
+
}
|
|
606
|
+
if (props.hideMobileInput !== undefined) {
|
|
607
|
+
(element as { hideMobileInput: boolean }).hideMobileInput = props.hideMobileInput as boolean;
|
|
608
|
+
}
|
|
609
|
+
if (props.autoCorrection !== undefined) {
|
|
610
|
+
(element as { autoCorrection: boolean }).autoCorrection = props.autoCorrection as boolean;
|
|
611
|
+
}
|
|
612
|
+
// Note: placeholder is handled differently in Unity - it's set via the textEdition interface
|
|
613
|
+
// For now we skip it as it requires more complex handling
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Apply Slider-specific properties
|
|
617
|
+
function applySliderProps(element: CSObject, props: Record<string, unknown>) {
|
|
618
|
+
if (props.lowValue !== undefined) {
|
|
619
|
+
(element as { lowValue: number }).lowValue = props.lowValue as number;
|
|
620
|
+
}
|
|
621
|
+
if (props.highValue !== undefined) {
|
|
622
|
+
(element as { highValue: number }).highValue = props.highValue as number;
|
|
623
|
+
}
|
|
624
|
+
if (props.showInputField !== undefined) {
|
|
625
|
+
(element as { showInputField: boolean }).showInputField = props.showInputField as boolean;
|
|
626
|
+
}
|
|
627
|
+
if (props.inverted !== undefined) {
|
|
628
|
+
(element as { inverted: boolean }).inverted = props.inverted as boolean;
|
|
629
|
+
}
|
|
630
|
+
if (props.pageSize !== undefined) {
|
|
631
|
+
(element as { pageSize: number }).pageSize = props.pageSize as number;
|
|
632
|
+
}
|
|
633
|
+
if (props.fill !== undefined) {
|
|
634
|
+
(element as { fill: boolean }).fill = props.fill as boolean;
|
|
635
|
+
}
|
|
636
|
+
if (props.direction !== undefined) {
|
|
637
|
+
const UIE = CS.UnityEngine.UIElements;
|
|
638
|
+
(element as { direction: unknown }).direction = UIE.SliderDirection[props.direction as string];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Apply Toggle-specific properties
|
|
643
|
+
function applyToggleProps(element: CSObject, props: Record<string, unknown>) {
|
|
644
|
+
if (props.text !== undefined) {
|
|
645
|
+
(element as { text: string }).text = props.text as string;
|
|
646
|
+
}
|
|
647
|
+
if (props.toggleOnLabelClick !== undefined) {
|
|
648
|
+
(element as { toggleOnLabelClick: boolean }).toggleOnLabelClick = props.toggleOnLabelClick as boolean;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Apply Image-specific properties
|
|
653
|
+
function applyImageProps(element: CSObject, props: Record<string, unknown>) {
|
|
654
|
+
if (props.image !== undefined) {
|
|
655
|
+
(element as { image: unknown }).image = props.image;
|
|
656
|
+
}
|
|
657
|
+
if (props.sprite !== undefined) {
|
|
658
|
+
(element as { sprite: unknown }).sprite = props.sprite;
|
|
659
|
+
}
|
|
660
|
+
if (props.vectorImage !== undefined) {
|
|
661
|
+
(element as { vectorImage: unknown }).vectorImage = props.vectorImage;
|
|
662
|
+
}
|
|
663
|
+
if (props.scaleMode !== undefined) {
|
|
664
|
+
const scaleMode = CS.UnityEngine.ScaleMode[props.scaleMode as string];
|
|
665
|
+
(element as { scaleMode: unknown }).scaleMode = scaleMode;
|
|
666
|
+
}
|
|
667
|
+
if (props.tintColor !== undefined) {
|
|
668
|
+
// Parse color string to Unity Color
|
|
669
|
+
const color = parseColor(props.tintColor as string);
|
|
670
|
+
if (color) {
|
|
671
|
+
(element as { tintColor: unknown }).tintColor = color;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (props.sourceRect !== undefined) {
|
|
675
|
+
const rect = props.sourceRect as { x: number; y: number; width: number; height: number };
|
|
676
|
+
(element as { sourceRect: unknown }).sourceRect = new CS.UnityEngine.Rect(rect.x, rect.y, rect.width, rect.height);
|
|
677
|
+
}
|
|
678
|
+
if (props.uv !== undefined) {
|
|
679
|
+
const rect = props.uv as { x: number; y: number; width: number; height: number };
|
|
680
|
+
(element as { uv: unknown }).uv = new CS.UnityEngine.Rect(rect.x, rect.y, rect.width, rect.height);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
544
684
|
// Apply ScrollView-specific properties
|
|
545
685
|
function applyScrollViewProps(element: CSScrollView, props: Record<string, unknown>) {
|
|
546
686
|
const UIE = CS.UnityEngine.UIElements;
|
|
@@ -593,9 +733,23 @@ function applyListViewProps(element: CSListView, props: Record<string, unknown>)
|
|
|
593
733
|
|
|
594
734
|
// Apply component-specific props based on element type
|
|
595
735
|
function applyComponentProps(element: CSObject, type: string, props: Record<string, unknown>) {
|
|
736
|
+
// For Slider, apply range props (lowValue/highValue) BEFORE value
|
|
737
|
+
// Unity's Slider clamps value to [lowValue, highValue], so range must be set first
|
|
738
|
+
if (type === 'ojs-slider') {
|
|
739
|
+
applySliderProps(element, props);
|
|
740
|
+
applyCommonProps(element, props);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
596
744
|
applyCommonProps(element, props);
|
|
597
745
|
|
|
598
|
-
if (type === 'ojs-
|
|
746
|
+
if (type === 'ojs-textfield') {
|
|
747
|
+
applyTextFieldProps(element, props);
|
|
748
|
+
} else if (type === 'ojs-toggle') {
|
|
749
|
+
applyToggleProps(element, props);
|
|
750
|
+
} else if (type === 'ojs-image') {
|
|
751
|
+
applyImageProps(element, props);
|
|
752
|
+
} else if (type === 'ojs-scrollview') {
|
|
599
753
|
applyScrollViewProps(element as CSScrollView, props);
|
|
600
754
|
} else if (type === 'ojs-listview') {
|
|
601
755
|
applyListViewProps(element as CSListView, props);
|
|
@@ -621,6 +775,7 @@ function createInstance(type: string, props: BaseProps): Instance {
|
|
|
621
775
|
|
|
622
776
|
applyClassName(element, props.className);
|
|
623
777
|
applyEvents(instance, props);
|
|
778
|
+
applyVisualContentCallback(instance, props);
|
|
624
779
|
applyComponentProps(element, type, props as Record<string, unknown>);
|
|
625
780
|
|
|
626
781
|
return instance;
|
|
@@ -645,6 +800,9 @@ function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseP
|
|
|
645
800
|
// Update events
|
|
646
801
|
applyEvents(instance, newProps);
|
|
647
802
|
|
|
803
|
+
// Update vector drawing callback
|
|
804
|
+
applyVisualContentCallback(instance, newProps);
|
|
805
|
+
|
|
648
806
|
// Update component-specific props
|
|
649
807
|
applyComponentProps(element, instance.type, newProps as Record<string, unknown>);
|
|
650
808
|
|
|
@@ -887,16 +1045,16 @@ export const hostConfig = {
|
|
|
887
1045
|
|
|
888
1046
|
// Visibility support
|
|
889
1047
|
hideInstance(instance: Instance) {
|
|
890
|
-
instance.element.style.display =
|
|
1048
|
+
instance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.None;
|
|
891
1049
|
},
|
|
892
1050
|
hideTextInstance(textInstance: TextInstance) {
|
|
893
|
-
textInstance.element.style.display =
|
|
1051
|
+
textInstance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.None;
|
|
894
1052
|
},
|
|
895
1053
|
unhideInstance(instance: Instance, _props: BaseProps) {
|
|
896
|
-
instance.element.style.display =
|
|
1054
|
+
instance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.Flex;
|
|
897
1055
|
},
|
|
898
1056
|
unhideTextInstance(textInstance: TextInstance, _text: string) {
|
|
899
|
-
textInstance.element.style.display =
|
|
1057
|
+
textInstance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.Flex;
|
|
900
1058
|
},
|
|
901
1059
|
|
|
902
1060
|
// Text content
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,9 @@ export type {
|
|
|
35
35
|
BreakpointName,
|
|
36
36
|
} from './screen';
|
|
37
37
|
|
|
38
|
+
// Vector Drawing
|
|
39
|
+
export { Transform2D, useVectorContent } from './vector';
|
|
40
|
+
|
|
38
41
|
// Types
|
|
39
42
|
export type {
|
|
40
43
|
ViewStyle,
|
|
@@ -84,4 +87,12 @@ export type {
|
|
|
84
87
|
SliderElement,
|
|
85
88
|
ScrollViewElement,
|
|
86
89
|
ImageElement,
|
|
90
|
+
// Vector drawing types
|
|
91
|
+
Vector2,
|
|
92
|
+
Color,
|
|
93
|
+
Angle,
|
|
94
|
+
ArcDirection,
|
|
95
|
+
Painter2D,
|
|
96
|
+
MeshGenerationContext,
|
|
97
|
+
GenerateVisualContentCallback,
|
|
87
98
|
} from './types';
|
package/src/style-parser.ts
CHANGED
|
@@ -13,6 +13,17 @@ declare const CS: {
|
|
|
13
13
|
Length: new (value: number, unit?: number) => CSLength;
|
|
14
14
|
LengthUnit: { Pixel: number; Percent: number };
|
|
15
15
|
StyleKeyword: { Auto: number; None: number; Initial: number };
|
|
16
|
+
// Enums for style properties
|
|
17
|
+
FlexDirection: Record<string, number>;
|
|
18
|
+
Wrap: Record<string, number>;
|
|
19
|
+
Align: Record<string, number>;
|
|
20
|
+
Justify: Record<string, number>;
|
|
21
|
+
Position: Record<string, number>;
|
|
22
|
+
Overflow: Record<string, number>;
|
|
23
|
+
DisplayStyle: Record<string, number>;
|
|
24
|
+
Visibility: Record<string, number>;
|
|
25
|
+
WhiteSpace: Record<string, number>;
|
|
26
|
+
TextAnchor: Record<string, number>;
|
|
16
27
|
};
|
|
17
28
|
};
|
|
18
29
|
};
|
|
@@ -80,12 +91,119 @@ const NUMBER_PROPERTIES = new Set([
|
|
|
80
91
|
"flexGrow", "flexShrink", "opacity",
|
|
81
92
|
])
|
|
82
93
|
|
|
83
|
-
// Enum
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
// Enum property mappings: React style value -> Unity enum value
|
|
95
|
+
// Keys are camelCase (React/CSS style), values map to Unity enum member names
|
|
96
|
+
const ENUM_MAPPINGS: Record<string, { enum: () => Record<string, number>, values: Record<string, string> }> = {
|
|
97
|
+
flexDirection: {
|
|
98
|
+
enum: () => CS.UnityEngine.UIElements.FlexDirection,
|
|
99
|
+
values: {
|
|
100
|
+
"row": "Row",
|
|
101
|
+
"row-reverse": "RowReverse",
|
|
102
|
+
"column": "Column",
|
|
103
|
+
"column-reverse": "ColumnReverse",
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
flexWrap: {
|
|
107
|
+
enum: () => CS.UnityEngine.UIElements.Wrap,
|
|
108
|
+
values: {
|
|
109
|
+
"nowrap": "NoWrap",
|
|
110
|
+
"wrap": "Wrap",
|
|
111
|
+
"wrap-reverse": "WrapReverse",
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
alignItems: {
|
|
115
|
+
enum: () => CS.UnityEngine.UIElements.Align,
|
|
116
|
+
values: {
|
|
117
|
+
"auto": "Auto",
|
|
118
|
+
"flex-start": "FlexStart",
|
|
119
|
+
"flex-end": "FlexEnd",
|
|
120
|
+
"center": "Center",
|
|
121
|
+
"stretch": "Stretch",
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
alignSelf: {
|
|
125
|
+
enum: () => CS.UnityEngine.UIElements.Align,
|
|
126
|
+
values: {
|
|
127
|
+
"auto": "Auto",
|
|
128
|
+
"flex-start": "FlexStart",
|
|
129
|
+
"flex-end": "FlexEnd",
|
|
130
|
+
"center": "Center",
|
|
131
|
+
"stretch": "Stretch",
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
alignContent: {
|
|
135
|
+
enum: () => CS.UnityEngine.UIElements.Align,
|
|
136
|
+
values: {
|
|
137
|
+
"auto": "Auto",
|
|
138
|
+
"flex-start": "FlexStart",
|
|
139
|
+
"flex-end": "FlexEnd",
|
|
140
|
+
"center": "Center",
|
|
141
|
+
"stretch": "Stretch",
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
justifyContent: {
|
|
145
|
+
enum: () => CS.UnityEngine.UIElements.Justify,
|
|
146
|
+
values: {
|
|
147
|
+
"flex-start": "FlexStart",
|
|
148
|
+
"flex-end": "FlexEnd",
|
|
149
|
+
"center": "Center",
|
|
150
|
+
"space-between": "SpaceBetween",
|
|
151
|
+
"space-around": "SpaceAround",
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
position: {
|
|
155
|
+
enum: () => CS.UnityEngine.UIElements.Position,
|
|
156
|
+
values: {
|
|
157
|
+
"relative": "Relative",
|
|
158
|
+
"absolute": "Absolute",
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
overflow: {
|
|
162
|
+
enum: () => CS.UnityEngine.UIElements.Overflow,
|
|
163
|
+
values: {
|
|
164
|
+
"visible": "Visible",
|
|
165
|
+
"hidden": "Hidden",
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
display: {
|
|
169
|
+
enum: () => CS.UnityEngine.UIElements.DisplayStyle,
|
|
170
|
+
values: {
|
|
171
|
+
"flex": "Flex",
|
|
172
|
+
"none": "None",
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
visibility: {
|
|
176
|
+
enum: () => CS.UnityEngine.UIElements.Visibility,
|
|
177
|
+
values: {
|
|
178
|
+
"visible": "Visible",
|
|
179
|
+
"hidden": "Hidden",
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
whiteSpace: {
|
|
183
|
+
enum: () => CS.UnityEngine.UIElements.WhiteSpace,
|
|
184
|
+
values: {
|
|
185
|
+
"normal": "Normal",
|
|
186
|
+
"nowrap": "NoWrap",
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse an enum style value
|
|
193
|
+
* @param key - Style property name
|
|
194
|
+
* @param value - String value from React style (e.g., "row", "flex-start")
|
|
195
|
+
* @returns Unity enum value or null if not found
|
|
196
|
+
*/
|
|
197
|
+
function parseEnumValue(key: string, value: string): number | null {
|
|
198
|
+
const mapping = ENUM_MAPPINGS[key]
|
|
199
|
+
if (!mapping) return null
|
|
200
|
+
|
|
201
|
+
const unityEnumName = mapping.values[value]
|
|
202
|
+
if (!unityEnumName) return null
|
|
203
|
+
|
|
204
|
+
const enumType = mapping.enum()
|
|
205
|
+
return enumType[unityEnumName] ?? null
|
|
206
|
+
}
|
|
89
207
|
|
|
90
208
|
/**
|
|
91
209
|
* Parse a length value from various formats
|
|
@@ -236,9 +354,11 @@ export function parseStyleValue(key: string, value: unknown): unknown {
|
|
|
236
354
|
return value
|
|
237
355
|
}
|
|
238
356
|
|
|
239
|
-
// Enum properties -
|
|
240
|
-
if (
|
|
241
|
-
|
|
357
|
+
// Enum properties - convert string to Unity enum value
|
|
358
|
+
if (key in ENUM_MAPPINGS && typeof value === "string") {
|
|
359
|
+
const parsed = parseEnumValue(key, value)
|
|
360
|
+
if (parsed !== null) return parsed
|
|
361
|
+
// Fall through if parsing failed
|
|
242
362
|
}
|
|
243
363
|
|
|
244
364
|
// Unknown property - pass through unchanged
|
package/src/types.ts
CHANGED
|
@@ -124,7 +124,7 @@ export interface ViewStyle {
|
|
|
124
124
|
color?: StyleColor;
|
|
125
125
|
/** Font size in pixels. Examples: 16, "16px" */
|
|
126
126
|
fontSize?: StyleLength;
|
|
127
|
-
|
|
127
|
+
/** Text alignment. Note: Use USS class or stylesheet for -unity-font-style (italic/bold) */
|
|
128
128
|
unityTextAlign?: 'upper-left' | 'upper-center' | 'upper-right' | 'middle-left' | 'middle-center' | 'middle-right' | 'lower-left' | 'lower-center' | 'lower-right';
|
|
129
129
|
whiteSpace?: 'normal' | 'nowrap';
|
|
130
130
|
}
|
|
@@ -213,6 +213,55 @@ export type GeometryEventHandler = (event: GeometryEventData) => void;
|
|
|
213
213
|
export type NavigationEventHandler = (event: NavigationEventData) => void;
|
|
214
214
|
export type TransitionEventHandler = (event: TransitionEventData) => void;
|
|
215
215
|
|
|
216
|
+
// Vector Drawing Types - Re-export from unity-types (CS.* namespace)
|
|
217
|
+
// These types are provided by the unity-types package and represent Unity's actual API
|
|
218
|
+
|
|
219
|
+
/** Unity Vector2 - 2D point/vector. Use CS.UnityEngine.Vector2 at runtime. */
|
|
220
|
+
export type Vector2 = CS.UnityEngine.Vector2;
|
|
221
|
+
|
|
222
|
+
/** Unity Color - RGBA color. Use CS.UnityEngine.Color at runtime. */
|
|
223
|
+
export type Color = CS.UnityEngine.Color;
|
|
224
|
+
|
|
225
|
+
/** Unity Angle - Represents an angle with unit. Use CS.UnityEngine.UIElements.Angle at runtime. */
|
|
226
|
+
export type Angle = CS.UnityEngine.UIElements.Angle;
|
|
227
|
+
|
|
228
|
+
/** Unity ArcDirection - Direction for arc drawing. Use CS.UnityEngine.UIElements.ArcDirection at runtime. */
|
|
229
|
+
export type ArcDirection = CS.UnityEngine.UIElements.ArcDirection;
|
|
230
|
+
|
|
231
|
+
/** Unity Painter2D - Vector drawing API. Accessed via mgc.painter2D in generateVisualContent. */
|
|
232
|
+
export type Painter2D = CS.UnityEngine.UIElements.Painter2D;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Unity MeshGenerationContext - Provides rendering context within generateVisualContent callback.
|
|
236
|
+
*
|
|
237
|
+
* Access painter2D for vector drawing, or use DrawText/DrawVectorImage for other content.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* <View
|
|
241
|
+
* style={{ width: 200, height: 200 }}
|
|
242
|
+
* onGenerateVisualContent={(mgc) => {
|
|
243
|
+
* const p = mgc.painter2D
|
|
244
|
+
*
|
|
245
|
+
* p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
|
|
246
|
+
* p.BeginPath()
|
|
247
|
+
* p.Arc(
|
|
248
|
+
* new CS.UnityEngine.Vector2(100, 100),
|
|
249
|
+
* 80,
|
|
250
|
+
* CS.UnityEngine.UIElements.Angle.Degrees(0),
|
|
251
|
+
* CS.UnityEngine.UIElements.Angle.Degrees(360),
|
|
252
|
+
* CS.UnityEngine.UIElements.ArcDirection.Clockwise
|
|
253
|
+
* )
|
|
254
|
+
* p.Fill()
|
|
255
|
+
* }}
|
|
256
|
+
* />
|
|
257
|
+
*/
|
|
258
|
+
export type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContext;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Callback type for generateVisualContent
|
|
262
|
+
*/
|
|
263
|
+
export type GenerateVisualContentCallback = (context: MeshGenerationContext) => void;
|
|
264
|
+
|
|
216
265
|
// Base props for all components
|
|
217
266
|
export interface BaseProps {
|
|
218
267
|
key?: string | number;
|
|
@@ -281,6 +330,35 @@ export interface BaseProps {
|
|
|
281
330
|
onTransitionStart?: TransitionEventHandler;
|
|
282
331
|
onTransitionEnd?: TransitionEventHandler;
|
|
283
332
|
onTransitionCancel?: TransitionEventHandler;
|
|
333
|
+
|
|
334
|
+
// Vector drawing
|
|
335
|
+
/**
|
|
336
|
+
* Callback for custom vector drawing via Unity's generateVisualContent.
|
|
337
|
+
* Called when the element needs to repaint its visual content.
|
|
338
|
+
*
|
|
339
|
+
* Use element.MarkDirtyRepaint() to trigger a repaint when your drawing state changes.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* <View
|
|
343
|
+
* style={{ width: 200, height: 200 }}
|
|
344
|
+
* onGenerateVisualContent={(mgc) => {
|
|
345
|
+
* const p = mgc.painter2D
|
|
346
|
+
* const Angle = CS.UnityEngine.UIElements.Angle
|
|
347
|
+
*
|
|
348
|
+
* p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
|
|
349
|
+
* p.BeginPath()
|
|
350
|
+
* p.Arc(
|
|
351
|
+
* new CS.UnityEngine.Vector2(100, 100),
|
|
352
|
+
* 80,
|
|
353
|
+
* Angle.Degrees(0),
|
|
354
|
+
* Angle.Degrees(360),
|
|
355
|
+
* CS.UnityEngine.UIElements.ArcDirection.Clockwise
|
|
356
|
+
* )
|
|
357
|
+
* p.Fill()
|
|
358
|
+
* }}
|
|
359
|
+
* />
|
|
360
|
+
*/
|
|
361
|
+
onGenerateVisualContent?: GenerateVisualContentCallback;
|
|
284
362
|
}
|
|
285
363
|
|
|
286
364
|
// Component-specific props
|
|
@@ -300,23 +378,39 @@ export interface ButtonProps extends BaseProps {
|
|
|
300
378
|
|
|
301
379
|
export interface TextFieldProps extends BaseProps {
|
|
302
380
|
value?: string;
|
|
381
|
+
label?: string;
|
|
303
382
|
placeholder?: string;
|
|
304
383
|
multiline?: boolean;
|
|
305
384
|
readOnly?: boolean;
|
|
306
385
|
maxLength?: number;
|
|
386
|
+
isPasswordField?: boolean;
|
|
387
|
+
maskChar?: string;
|
|
388
|
+
isDelayed?: boolean;
|
|
389
|
+
selectAllOnFocus?: boolean;
|
|
390
|
+
selectAllOnMouseUp?: boolean;
|
|
391
|
+
hideMobileInput?: boolean;
|
|
392
|
+
autoCorrection?: boolean;
|
|
307
393
|
onChange?: ChangeEventHandler<string>;
|
|
308
394
|
}
|
|
309
395
|
|
|
310
396
|
export interface ToggleProps extends BaseProps {
|
|
311
397
|
value?: boolean;
|
|
312
398
|
label?: string;
|
|
399
|
+
text?: string;
|
|
400
|
+
toggleOnLabelClick?: boolean;
|
|
313
401
|
onChange?: ChangeEventHandler<boolean>;
|
|
314
402
|
}
|
|
315
403
|
|
|
316
404
|
export interface SliderProps extends BaseProps {
|
|
317
405
|
value?: number;
|
|
406
|
+
label?: string;
|
|
318
407
|
lowValue?: number;
|
|
319
408
|
highValue?: number;
|
|
409
|
+
direction?: 'Horizontal' | 'Vertical';
|
|
410
|
+
pageSize?: number;
|
|
411
|
+
showInputField?: boolean;
|
|
412
|
+
inverted?: boolean;
|
|
413
|
+
fill?: boolean;
|
|
320
414
|
onChange?: ChangeEventHandler<number>;
|
|
321
415
|
}
|
|
322
416
|
|
|
@@ -344,8 +438,20 @@ export interface ScrollViewProps extends BaseProps {
|
|
|
344
438
|
}
|
|
345
439
|
|
|
346
440
|
export interface ImageProps extends BaseProps {
|
|
347
|
-
|
|
348
|
-
|
|
441
|
+
/** Image source - can be a Texture2D, Sprite, or path string */
|
|
442
|
+
image?: object;
|
|
443
|
+
/** Sprite to display (alternative to image) */
|
|
444
|
+
sprite?: object;
|
|
445
|
+
/** Vector image to display */
|
|
446
|
+
vectorImage?: object;
|
|
447
|
+
/** How the image scales to fit the element */
|
|
448
|
+
scaleMode?: 'StretchToFill' | 'ScaleAndCrop' | 'ScaleToFit';
|
|
449
|
+
/** Tint color applied to the image */
|
|
450
|
+
tintColor?: string;
|
|
451
|
+
/** Source rectangle within the texture (normalized 0-1 coordinates) */
|
|
452
|
+
sourceRect?: { x: number; y: number; width: number; height: number };
|
|
453
|
+
/** UV coordinates for the image */
|
|
454
|
+
uv?: { x: number; y: number; width: number; height: number };
|
|
349
455
|
}
|
|
350
456
|
|
|
351
457
|
/**
|
|
@@ -400,6 +506,27 @@ export interface VisualElement extends RenderContainer {
|
|
|
400
506
|
|
|
401
507
|
// Layout
|
|
402
508
|
MarkDirtyRepaint: () => void;
|
|
509
|
+
|
|
510
|
+
// Vector drawing
|
|
511
|
+
/**
|
|
512
|
+
* Callback delegate for custom visual content generation.
|
|
513
|
+
* Can be used via ref for raw access to Unity's generateVisualContent.
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* useEffect(() => {
|
|
517
|
+
* const ve = ref.current
|
|
518
|
+
* const draw = (mgc) => { ... }
|
|
519
|
+
* ve.generateVisualContent += draw
|
|
520
|
+
* return () => { ve.generateVisualContent -= draw }
|
|
521
|
+
* }, [])
|
|
522
|
+
*/
|
|
523
|
+
generateVisualContent: {
|
|
524
|
+
(callback: GenerateVisualContentCallback): void;
|
|
525
|
+
} & {
|
|
526
|
+
// Delegate operators (C# interop)
|
|
527
|
+
'+='?: (callback: GenerateVisualContentCallback) => void;
|
|
528
|
+
'-='?: (callback: GenerateVisualContentCallback) => void;
|
|
529
|
+
};
|
|
403
530
|
}
|
|
404
531
|
|
|
405
532
|
// Specific element types for better ref typing
|
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
|
+
}
|