onejs-react 0.1.10 → 0.1.12
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/package.json +1 -1
- package/src/__tests__/components.test.tsx +79 -1
- package/src/__tests__/mocks.ts +46 -0
- package/src/__tests__/pre-setup.ts +15 -0
- package/src/__tests__/setup.ts +2 -0
- package/src/components.tsx +69 -3
- package/src/host-config.ts +10 -1
- package/src/index.ts +1 -0
- package/src/style-parser.ts +140 -0
- package/src/types.ts +32 -8
package/package.json
CHANGED
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
Slider,
|
|
21
21
|
ScrollView,
|
|
22
22
|
Image,
|
|
23
|
+
clearImageCache,
|
|
23
24
|
} from '../components';
|
|
24
|
-
import { MockVisualElement, MockLength, MockColor, createMockContainer, flushMicrotasks, getEventAPI } from './mocks';
|
|
25
|
+
import { MockVisualElement, MockTexture2D, MockLength, MockColor, createMockContainer, flushMicrotasks, getEventAPI, mockFileSystem } from './mocks';
|
|
25
26
|
|
|
26
27
|
// Helper to extract value from style (handles both raw values and MockLength/MockColor)
|
|
27
28
|
function getStyleValue(style: unknown): unknown {
|
|
@@ -365,6 +366,83 @@ describe('components', () => {
|
|
|
365
366
|
expect(getStyleValue(el.style.width)).toBe(100);
|
|
366
367
|
expect(getStyleValue(el.style.height)).toBe(100);
|
|
367
368
|
});
|
|
369
|
+
|
|
370
|
+
it('loads texture when src is provided', async () => {
|
|
371
|
+
mockFileSystem.set("/project/App/assets/images/logo.png", [0x89, 0x50, 0x4e, 0x47]);
|
|
372
|
+
(globalThis as any).__workingDir = "/project/App";
|
|
373
|
+
|
|
374
|
+
const container = createMockContainer();
|
|
375
|
+
render(<Image src="images/logo.png" style={{ width: 64, height: 64 }} />, container as any);
|
|
376
|
+
await flushMicrotasks();
|
|
377
|
+
|
|
378
|
+
const el = container.children[0] as any;
|
|
379
|
+
expect(el.image).toBeInstanceOf(MockTexture2D);
|
|
380
|
+
expect((el.image as MockTexture2D)._loaded).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('returns null for missing file with console.error', async () => {
|
|
384
|
+
(globalThis as any).__workingDir = "/project/App";
|
|
385
|
+
|
|
386
|
+
const container = createMockContainer();
|
|
387
|
+
render(<Image src="images/missing.png" />, container as any);
|
|
388
|
+
await flushMicrotasks();
|
|
389
|
+
|
|
390
|
+
const el = container.children[0] as any;
|
|
391
|
+
expect(el.image).toBeNull();
|
|
392
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
393
|
+
expect.stringContaining("Image src not found: images/missing.png")
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('caches textures across renders', async () => {
|
|
398
|
+
mockFileSystem.set("/project/App/assets/photos/hero.png", [0x89, 0x50]);
|
|
399
|
+
(globalThis as any).__workingDir = "/project/App";
|
|
400
|
+
const cs = (globalThis as any).CS;
|
|
401
|
+
const readSpy = vi.spyOn(cs.System.IO.File, 'ReadAllBytes');
|
|
402
|
+
|
|
403
|
+
const container = createMockContainer();
|
|
404
|
+
render(<Image src="photos/hero.png" />, container as any);
|
|
405
|
+
await flushMicrotasks();
|
|
406
|
+
|
|
407
|
+
// Re-render with same src
|
|
408
|
+
render(<Image src="photos/hero.png" />, container as any);
|
|
409
|
+
await flushMicrotasks();
|
|
410
|
+
|
|
411
|
+
expect(readSpy).toHaveBeenCalledTimes(1);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('src takes precedence over image prop', async () => {
|
|
415
|
+
mockFileSystem.set("/project/App/assets/a.png", [0x89]);
|
|
416
|
+
(globalThis as any).__workingDir = "/project/App";
|
|
417
|
+
const manualTex = { manual: true };
|
|
418
|
+
|
|
419
|
+
const container = createMockContainer();
|
|
420
|
+
render(<Image src="a.png" image={manualTex} />, container as any);
|
|
421
|
+
await flushMicrotasks();
|
|
422
|
+
|
|
423
|
+
const el = container.children[0] as any;
|
|
424
|
+
expect(el.image).toBeInstanceOf(MockTexture2D);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('clearImageCache resets the cache', async () => {
|
|
428
|
+
mockFileSystem.set("/project/App/assets/b.png", [0x89]);
|
|
429
|
+
(globalThis as any).__workingDir = "/project/App";
|
|
430
|
+
const cs = (globalThis as any).CS;
|
|
431
|
+
const readSpy = vi.spyOn(cs.System.IO.File, 'ReadAllBytes');
|
|
432
|
+
|
|
433
|
+
const container1 = createMockContainer();
|
|
434
|
+
render(<Image src="b.png" />, container1 as any);
|
|
435
|
+
await flushMicrotasks();
|
|
436
|
+
|
|
437
|
+
clearImageCache();
|
|
438
|
+
|
|
439
|
+
// Render in a new container to force a fresh component mount
|
|
440
|
+
const container2 = createMockContainer();
|
|
441
|
+
render(<Image src="b.png" />, container2 as any);
|
|
442
|
+
await flushMicrotasks();
|
|
443
|
+
|
|
444
|
+
expect(readSpy).toHaveBeenCalledTimes(2);
|
|
445
|
+
});
|
|
368
446
|
});
|
|
369
447
|
|
|
370
448
|
describe('event handler mapping', () => {
|
package/src/__tests__/mocks.ts
CHANGED
|
@@ -223,6 +223,32 @@ export class MockImage extends MockVisualElement {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Mock Texture2D for image loading tests
|
|
228
|
+
*/
|
|
229
|
+
export class MockTexture2D {
|
|
230
|
+
width: number
|
|
231
|
+
height: number
|
|
232
|
+
filterMode: number = 0
|
|
233
|
+
_loaded = false
|
|
234
|
+
|
|
235
|
+
constructor(w: number, h: number) {
|
|
236
|
+
this.width = w
|
|
237
|
+
this.height = h
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
LoadImage(_bytes: any): boolean {
|
|
241
|
+
this._loaded = true
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Mock file system for image loading tests.
|
|
248
|
+
* Tests can add entries to control which files "exist" and what bytes they contain.
|
|
249
|
+
*/
|
|
250
|
+
export const mockFileSystem = new Map<string, number[]>()
|
|
251
|
+
|
|
226
252
|
/**
|
|
227
253
|
* Create the mock CS global object that mirrors QuickJSBootstrap.js proxy
|
|
228
254
|
*
|
|
@@ -231,11 +257,30 @@ export class MockImage extends MockVisualElement {
|
|
|
231
257
|
*/
|
|
232
258
|
export function createMockCS() {
|
|
233
259
|
return {
|
|
260
|
+
System: {
|
|
261
|
+
IO: {
|
|
262
|
+
Path: {
|
|
263
|
+
Combine: (...parts: string[]) => parts.join("/"),
|
|
264
|
+
GetDirectoryName: (p: string) => p.substring(0, p.lastIndexOf("/")),
|
|
265
|
+
},
|
|
266
|
+
File: {
|
|
267
|
+
Exists: (path: string) => mockFileSystem.has(path),
|
|
268
|
+
ReadAllBytes: (path: string) => mockFileSystem.get(path) || [],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
234
272
|
UnityEngine: {
|
|
235
273
|
// Core types
|
|
236
274
|
Color: MockColor,
|
|
237
275
|
Rect: class { constructor(public x: number, public y: number, public width: number, public height: number) {} },
|
|
238
276
|
ScaleMode: { StretchToFill: 0, ScaleAndCrop: 1, ScaleToFit: 2 },
|
|
277
|
+
Application: {
|
|
278
|
+
isEditor: true,
|
|
279
|
+
dataPath: "/project/Assets",
|
|
280
|
+
streamingAssetsPath: "/project/Assets/StreamingAssets",
|
|
281
|
+
},
|
|
282
|
+
Texture2D: MockTexture2D,
|
|
283
|
+
FilterMode: { Point: 0, Bilinear: 1, Trilinear: 2 },
|
|
239
284
|
// UI Elements
|
|
240
285
|
UIElements: {
|
|
241
286
|
VisualElement: MockVisualElement,
|
|
@@ -308,6 +353,7 @@ export function findElementByHandle(handle: number): MockVisualElement | undefin
|
|
|
308
353
|
*/
|
|
309
354
|
export function resetAllMocks(): void {
|
|
310
355
|
createdElements = [];
|
|
356
|
+
mockFileSystem.clear();
|
|
311
357
|
}
|
|
312
358
|
|
|
313
359
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-setup - Defines globals that must exist before any module imports.
|
|
3
|
+
*
|
|
4
|
+
* This runs before setup.ts to ensure useExtensions and CS are available when
|
|
5
|
+
* components.tsx is first imported (it calls useExtensions at module level).
|
|
6
|
+
* setup.ts replaces CS with a full mock in beforeEach.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createMockCS } from "./mocks";
|
|
10
|
+
|
|
11
|
+
// No-op useExtensions for test environment (extension methods are mocked directly on types)
|
|
12
|
+
(globalThis as any).useExtensions = () => {};
|
|
13
|
+
|
|
14
|
+
// Minimal CS mock so module-level code can reference CS.UnityEngine.ImageConversion
|
|
15
|
+
(globalThis as any).CS = createMockCS();
|
package/src/__tests__/setup.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { vi, beforeEach, afterEach } from "vitest";
|
|
12
12
|
import { createMockCS, resetAllMocks } from "./mocks";
|
|
13
|
+
import { clearImageCache } from "../components";
|
|
13
14
|
|
|
14
15
|
// Extend globalThis type for our mocks
|
|
15
16
|
declare global {
|
|
@@ -31,6 +32,7 @@ const originalQueueMicrotask = global.queueMicrotask;
|
|
|
31
32
|
// Set up globals before each test
|
|
32
33
|
beforeEach(() => {
|
|
33
34
|
resetAllMocks();
|
|
35
|
+
clearImageCache();
|
|
34
36
|
|
|
35
37
|
// Create fresh mock CS global
|
|
36
38
|
(globalThis as any).CS = createMockCS();
|
package/src/components.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, type ReactElement, type Ref } from 'react';
|
|
1
|
+
import { forwardRef, useMemo, type ReactElement, type Ref } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
ViewProps,
|
|
4
4
|
TextProps,
|
|
@@ -21,6 +21,64 @@ import type {
|
|
|
21
21
|
ImageElement,
|
|
22
22
|
} from './types';
|
|
23
23
|
|
|
24
|
+
declare const CS: any
|
|
25
|
+
declare function useExtensions(typeRef: any): void
|
|
26
|
+
|
|
27
|
+
// Register ImageConversion extension methods so tex.LoadImage(bytes) works
|
|
28
|
+
useExtensions(CS.UnityEngine.ImageConversion)
|
|
29
|
+
|
|
30
|
+
// Module-level image cache shared across all Image instances
|
|
31
|
+
const _imageCache = new Map<string, any>()
|
|
32
|
+
|
|
33
|
+
function _resolveAssetPath(src: string): string {
|
|
34
|
+
const Path = CS.System.IO.Path
|
|
35
|
+
if (CS.UnityEngine.Application.isEditor) {
|
|
36
|
+
const workingDir = typeof (globalThis as any).__workingDir === "string"
|
|
37
|
+
? (globalThis as any).__workingDir
|
|
38
|
+
: Path.Combine(Path.GetDirectoryName(CS.UnityEngine.Application.dataPath), "App")
|
|
39
|
+
return Path.Combine(workingDir, "assets", src)
|
|
40
|
+
}
|
|
41
|
+
return Path.Combine(CS.UnityEngine.Application.streamingAssetsPath, "onejs", "assets", src)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _loadImageAsset(src: string): any | null {
|
|
45
|
+
const cached = _imageCache.get(src)
|
|
46
|
+
if (cached) return cached
|
|
47
|
+
|
|
48
|
+
const fullPath = _resolveAssetPath(src)
|
|
49
|
+
if (!CS.System.IO.File.Exists(fullPath)) {
|
|
50
|
+
console.error(`Image src not found: ${src} (resolved to ${fullPath})`)
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let result: any
|
|
55
|
+
if (src.toLowerCase().endsWith(".svg")) {
|
|
56
|
+
const svgText = CS.System.IO.File.ReadAllText(fullPath)
|
|
57
|
+
result = CS.OneJS.SVGUtils.LoadFromString(svgText)
|
|
58
|
+
} else {
|
|
59
|
+
const bytes = CS.System.IO.File.ReadAllBytes(fullPath)
|
|
60
|
+
const tex = new CS.UnityEngine.Texture2D(2, 2)
|
|
61
|
+
tex.LoadImage(bytes)
|
|
62
|
+
tex.filterMode = CS.UnityEngine.FilterMode.Bilinear
|
|
63
|
+
result = tex
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_imageCache.set(src, result)
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _isVectorImage(obj: any): boolean {
|
|
71
|
+
return obj != null && obj.GetType?.().Name === "VectorImage"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear the Image component's image cache.
|
|
76
|
+
* Call this if you need to force-reload images (e.g., after replacing files on disk).
|
|
77
|
+
*/
|
|
78
|
+
export function clearImageCache(): void {
|
|
79
|
+
_imageCache.clear()
|
|
80
|
+
}
|
|
81
|
+
|
|
24
82
|
// Props with ref support for intrinsic elements
|
|
25
83
|
type WithRef<Props, Element> = Props & { ref?: Ref<Element> };
|
|
26
84
|
|
|
@@ -105,8 +163,16 @@ export const ScrollView = forwardRef<ScrollViewElement, ScrollViewProps>((props,
|
|
|
105
163
|
});
|
|
106
164
|
ScrollView.displayName = 'ScrollView';
|
|
107
165
|
|
|
108
|
-
export const Image = forwardRef<ImageElement, ImageProps>((
|
|
109
|
-
|
|
166
|
+
export const Image = forwardRef<ImageElement, ImageProps>(({ src, image, ...rest }, ref) => {
|
|
167
|
+
const resolved = useMemo(() => {
|
|
168
|
+
if (src) return _loadImageAsset(src)
|
|
169
|
+
return image
|
|
170
|
+
}, [src, image])
|
|
171
|
+
const isVector = useMemo(() => _isVectorImage(resolved), [resolved])
|
|
172
|
+
if (isVector) {
|
|
173
|
+
return <ojs-image ref={ref} vectorImage={resolved} {...rest} />;
|
|
174
|
+
}
|
|
175
|
+
return <ojs-image ref={ref} image={resolved} {...rest} />;
|
|
110
176
|
});
|
|
111
177
|
Image.displayName = 'Image';
|
|
112
178
|
|
package/src/host-config.ts
CHANGED
|
@@ -642,7 +642,16 @@ function applyToggleProps(element: CSObject, props: Record<string, unknown>) {
|
|
|
642
642
|
// Apply Image-specific properties
|
|
643
643
|
function applyImageProps(element: CSObject, props: Record<string, unknown>) {
|
|
644
644
|
const el = element as any;
|
|
645
|
-
if (props.image !== undefined)
|
|
645
|
+
if (props.image !== undefined) {
|
|
646
|
+
const img = props.image;
|
|
647
|
+
if (img != null && (img as any).GetType?.().Name === "VectorImage") {
|
|
648
|
+
el.image = null;
|
|
649
|
+
el.vectorImage = img;
|
|
650
|
+
} else {
|
|
651
|
+
el.vectorImage = null;
|
|
652
|
+
el.image = img;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
646
655
|
if (props.sprite !== undefined) el.sprite = props.sprite;
|
|
647
656
|
if (props.vectorImage !== undefined) el.vectorImage = props.vectorImage;
|
|
648
657
|
if (props.scaleMode !== undefined) {
|
package/src/index.ts
CHANGED
package/src/style-parser.ts
CHANGED
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
declare const CS: {
|
|
10
10
|
UnityEngine: {
|
|
11
11
|
Color: new (r: number, g: number, b: number, a: number) => CSColor;
|
|
12
|
+
Vector3: new (x: number, y: number, z: number) => any;
|
|
12
13
|
FontStyle: Record<string, number>;
|
|
13
14
|
UIElements: {
|
|
14
15
|
Length: new (value: number, unit?: number) => CSLength;
|
|
15
16
|
LengthUnit: { Pixel: number; Percent: number };
|
|
16
17
|
StyleKeyword: { Auto: number; None: number; Initial: number };
|
|
18
|
+
Angle: { Degrees: (v: number) => any; Radians: (v: number) => any; Turns: (v: number) => any; Gradians: (v: number) => any };
|
|
19
|
+
Translate: new (x: any, y: any) => any;
|
|
20
|
+
Rotate: new (angle: any) => any;
|
|
21
|
+
Scale: new (v: any) => any;
|
|
22
|
+
TransformOrigin: new (x: any, y: any) => any;
|
|
23
|
+
StyleTranslate: new (v: any) => any;
|
|
24
|
+
StyleRotate: new (v: any) => any;
|
|
25
|
+
StyleScale: new (v: any) => any;
|
|
26
|
+
StyleTransformOrigin: new (v: any) => any;
|
|
17
27
|
// Enums for style properties
|
|
18
28
|
FlexDirection: Record<string, number>;
|
|
19
29
|
Wrap: Record<string, number>;
|
|
@@ -357,6 +367,130 @@ export function parseColor(value: string): CSColor | null {
|
|
|
357
367
|
return null
|
|
358
368
|
}
|
|
359
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Parse a length value for transform properties (no keyword support)
|
|
372
|
+
* number → Length(n, Pixel), "50%" → Length(50, Percent), "10px" → Length(10, Pixel)
|
|
373
|
+
*/
|
|
374
|
+
function parseLengthForTransform(value: number | string): CSLength | null {
|
|
375
|
+
if (typeof value === "number") {
|
|
376
|
+
return new CS.UnityEngine.UIElements.Length(value, CS.UnityEngine.UIElements.LengthUnit.Pixel)
|
|
377
|
+
}
|
|
378
|
+
if (typeof value === "string") {
|
|
379
|
+
const match = value.trim().match(/^(-?[\d.]+)(px|%)?$/)
|
|
380
|
+
if (match) {
|
|
381
|
+
const num = parseFloat(match[1])
|
|
382
|
+
if (isNaN(num)) return null
|
|
383
|
+
const unit = match[2] === "%"
|
|
384
|
+
? CS.UnityEngine.UIElements.LengthUnit.Percent
|
|
385
|
+
: CS.UnityEngine.UIElements.LengthUnit.Pixel
|
|
386
|
+
return new CS.UnityEngine.UIElements.Length(num, unit)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Parse an angle value
|
|
394
|
+
* number → Angle.Degrees(n), string → parse unit suffix (deg|rad|turn|grad)
|
|
395
|
+
*/
|
|
396
|
+
function parseAngle(value: number | string): any | null {
|
|
397
|
+
const Angle = CS.UnityEngine.UIElements.Angle
|
|
398
|
+
if (typeof value === "number") {
|
|
399
|
+
return Angle.Degrees(value)
|
|
400
|
+
}
|
|
401
|
+
if (typeof value === "string") {
|
|
402
|
+
const match = value.trim().match(/^(-?[\d.]+)(deg|rad|turn|grad)?$/)
|
|
403
|
+
if (match) {
|
|
404
|
+
const num = parseFloat(match[1])
|
|
405
|
+
if (isNaN(num)) return null
|
|
406
|
+
const unit = match[2] || "deg"
|
|
407
|
+
if (unit === "rad") return Angle.Radians(num)
|
|
408
|
+
if (unit === "turn") return Angle.Turns(num)
|
|
409
|
+
if (unit === "grad") return Angle.Gradians(num)
|
|
410
|
+
return Angle.Degrees(num)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Parse translate style: [x, y] array or C# Translate pass-through
|
|
418
|
+
*/
|
|
419
|
+
function parseTranslateStyle(value: unknown): unknown {
|
|
420
|
+
if (Array.isArray(value)) {
|
|
421
|
+
const x = parseLengthForTransform(value[0])
|
|
422
|
+
const y = parseLengthForTransform(value[1])
|
|
423
|
+
if (x !== null && y !== null) {
|
|
424
|
+
return new CS.UnityEngine.UIElements.StyleTranslate(
|
|
425
|
+
new CS.UnityEngine.UIElements.Translate(x, y)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (typeof value === "object" && value !== null) {
|
|
430
|
+
try { return new CS.UnityEngine.UIElements.StyleTranslate(value) } catch { return value }
|
|
431
|
+
}
|
|
432
|
+
return value
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Parse rotate style: number (degrees), string with unit, or C# Rotate pass-through
|
|
437
|
+
*/
|
|
438
|
+
function parseRotateStyle(value: unknown): unknown {
|
|
439
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
440
|
+
const angle = parseAngle(value)
|
|
441
|
+
if (angle !== null) {
|
|
442
|
+
return new CS.UnityEngine.UIElements.StyleRotate(
|
|
443
|
+
new CS.UnityEngine.UIElements.Rotate(angle)
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (typeof value === "object" && value !== null) {
|
|
448
|
+
try { return new CS.UnityEngine.UIElements.StyleRotate(value) } catch { return value }
|
|
449
|
+
}
|
|
450
|
+
return value
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Parse scale style: number (uniform), [x, y] array, or C# Scale pass-through
|
|
455
|
+
*/
|
|
456
|
+
function parseScaleStyle(value: unknown): unknown {
|
|
457
|
+
if (typeof value === "number") {
|
|
458
|
+
return new CS.UnityEngine.UIElements.StyleScale(
|
|
459
|
+
new CS.UnityEngine.UIElements.Scale(new CS.UnityEngine.Vector3(value, value, 1))
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
if (Array.isArray(value)) {
|
|
463
|
+
const x = typeof value[0] === "number" ? value[0] : 1
|
|
464
|
+
const y = typeof value[1] === "number" ? value[1] : 1
|
|
465
|
+
return new CS.UnityEngine.UIElements.StyleScale(
|
|
466
|
+
new CS.UnityEngine.UIElements.Scale(new CS.UnityEngine.Vector3(x, y, 1))
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
if (typeof value === "object" && value !== null) {
|
|
470
|
+
try { return new CS.UnityEngine.UIElements.StyleScale(value) } catch { return value }
|
|
471
|
+
}
|
|
472
|
+
return value
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Parse transformOrigin style: [x, y] array or C# TransformOrigin pass-through
|
|
477
|
+
*/
|
|
478
|
+
function parseTransformOriginStyle(value: unknown): unknown {
|
|
479
|
+
if (Array.isArray(value)) {
|
|
480
|
+
const x = parseLengthForTransform(value[0])
|
|
481
|
+
const y = parseLengthForTransform(value[1])
|
|
482
|
+
if (x !== null && y !== null) {
|
|
483
|
+
return new CS.UnityEngine.UIElements.StyleTransformOrigin(
|
|
484
|
+
new CS.UnityEngine.UIElements.TransformOrigin(x, y)
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (typeof value === "object" && value !== null) {
|
|
489
|
+
try { return new CS.UnityEngine.UIElements.StyleTransformOrigin(value) } catch { return value }
|
|
490
|
+
}
|
|
491
|
+
return value
|
|
492
|
+
}
|
|
493
|
+
|
|
360
494
|
/**
|
|
361
495
|
* Parse a style value based on the property name
|
|
362
496
|
* @param key - Style property name (e.g., "width", "backgroundColor")
|
|
@@ -399,6 +533,12 @@ export function parseStyleValue(key: string, value: unknown): unknown {
|
|
|
399
533
|
// Fall through if parsing failed
|
|
400
534
|
}
|
|
401
535
|
|
|
536
|
+
// Transform properties
|
|
537
|
+
if (key === "translate") return parseTranslateStyle(value)
|
|
538
|
+
if (key === "rotate") return parseRotateStyle(value)
|
|
539
|
+
if (key === "scale") return parseScaleStyle(value)
|
|
540
|
+
if (key === "transformOrigin") return parseTransformOriginStyle(value)
|
|
541
|
+
|
|
402
542
|
// Unknown property - pass through unchanged
|
|
403
543
|
return value
|
|
404
544
|
}
|
package/src/types.ts
CHANGED
|
@@ -167,13 +167,31 @@ export interface ViewStyle {
|
|
|
167
167
|
unitySliceScale?: number;
|
|
168
168
|
|
|
169
169
|
// Transform
|
|
170
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Rotation transform.
|
|
172
|
+
* - number: degrees (e.g., 45)
|
|
173
|
+
* - string: with unit (e.g., "0.5turn", "1.57rad", "45deg", "50grad")
|
|
174
|
+
* - C# Rotate struct: pass-through (auto-wrapped in StyleRotate)
|
|
175
|
+
*/
|
|
171
176
|
rotate?: any;
|
|
172
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Scale transform.
|
|
179
|
+
* - number: uniform scale (e.g., 1.5)
|
|
180
|
+
* - [x, y]: non-uniform scale (e.g., [1.5, 2])
|
|
181
|
+
* - C# Scale struct: pass-through (auto-wrapped in StyleScale)
|
|
182
|
+
*/
|
|
173
183
|
scale?: any;
|
|
174
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Translation transform.
|
|
186
|
+
* - [x, y]: numbers are px, strings parsed (e.g., [10, 20] or ["50%", 10])
|
|
187
|
+
* - C# Translate struct: pass-through (auto-wrapped in StyleTranslate)
|
|
188
|
+
*/
|
|
175
189
|
translate?: any;
|
|
176
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Transform origin point.
|
|
192
|
+
* - [x, y]: numbers are px, strings parsed (e.g., ["50%", "50%"])
|
|
193
|
+
* - C# TransformOrigin struct: pass-through (auto-wrapped in StyleTransformOrigin)
|
|
194
|
+
*/
|
|
177
195
|
transformOrigin?: any;
|
|
178
196
|
|
|
179
197
|
// Transition
|
|
@@ -510,12 +528,12 @@ export interface ScrollViewProps extends BaseProps {
|
|
|
510
528
|
}
|
|
511
529
|
|
|
512
530
|
export interface ImageProps extends BaseProps {
|
|
513
|
-
/**
|
|
531
|
+
/** Path to image asset relative to the assets/ folder. Supports PNG, JPG, and SVG files. */
|
|
532
|
+
src?: string;
|
|
533
|
+
/** Pre-loaded Texture2D or VectorImage object. Type is auto-detected at runtime. */
|
|
514
534
|
image?: object;
|
|
515
535
|
/** Sprite to display (alternative to image) */
|
|
516
536
|
sprite?: object;
|
|
517
|
-
/** Vector image to display */
|
|
518
|
-
vectorImage?: object;
|
|
519
537
|
/** How the image scales to fit the element */
|
|
520
538
|
scaleMode?: 'StretchToFill' | 'ScaleAndCrop' | 'ScaleToFit';
|
|
521
539
|
/** Tint color applied to the image */
|
|
@@ -636,7 +654,13 @@ export interface ScrollViewElement extends VisualElement {
|
|
|
636
654
|
}
|
|
637
655
|
|
|
638
656
|
export interface ImageElement extends VisualElement {
|
|
639
|
-
|
|
657
|
+
image: any;
|
|
658
|
+
sprite: any;
|
|
659
|
+
vectorImage: any;
|
|
660
|
+
scaleMode: number;
|
|
661
|
+
tintColor: any;
|
|
662
|
+
sourceRect: any;
|
|
663
|
+
uv: any;
|
|
640
664
|
}
|
|
641
665
|
|
|
642
666
|
// ListView uses Unity's virtualization callbacks directly
|