babylon.quarks-editor 0.17.7 → 0.17.8
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/dist/core/timeline.d.ts +1 -1
- package/dist/core/timeline.js +3 -2
- package/dist/react/EffectEditor.d.ts +3 -0
- package/dist/react/EffectEditor.js +6 -1
- package/dist/react/EffectEditorHost.js +72 -11
- package/dist/react/ObjectModule.d.ts +6 -0
- package/dist/react/ObjectModule.js +28 -0
- package/dist/react/TimelinePanel.d.ts +4 -3
- package/dist/react/TimelinePanel.js +60 -10
- package/dist/react/TimelineTrackRow.d.ts +2 -2
- package/dist/react/TimelineTrackRow.js +16 -12
- package/dist/react/theme.js +44 -21
- package/package.json +68 -63
- package/src/core/binding.ts +105 -105
- package/src/core/collision.ts +40 -40
- package/src/core/groups.ts +29 -29
- package/src/core/loadEffect.ts +102 -102
- package/src/core/scrub.ts +28 -28
- package/src/core/shapes.ts +133 -133
- package/src/core/systems.ts +98 -98
- package/src/core/timeline.ts +95 -94
- package/src/core/values.ts +113 -113
- package/src/index.ts +32 -32
- package/src/react/CurveEditor.tsx +168 -168
- package/src/react/EffectEditor.tsx +125 -111
- package/src/react/EffectEditorHost.tsx +466 -377
- package/src/react/GradientEditor.tsx +127 -127
- package/src/react/ModuleSection.tsx +54 -54
- package/src/react/ObjectModule.tsx +47 -0
- package/src/react/PromptDialog.tsx +85 -85
- package/src/react/TimelinePanel.tsx +412 -354
- package/src/react/TimelineTrackRow.tsx +291 -267
- package/src/react/behaviorModules.tsx +658 -658
- package/src/react/fields.tsx +76 -76
- package/src/react/index.ts +43 -43
- package/src/react/modules.tsx +1034 -1034
- package/src/react/theme.ts +108 -85
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
import React, {useRef, useState} from 'react';
|
|
2
|
-
import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
|
|
3
|
-
import {NumberField, Row} from './fields';
|
|
4
|
-
import {buttonStyle, theme} from './theme';
|
|
5
|
-
|
|
6
|
-
const BAR_WIDTH = 218;
|
|
7
|
-
|
|
8
|
-
/** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
|
|
9
|
-
export function GradientEditor(props: {stops: GradientStop[]; onChange: (stops: GradientStop[]) => void}) {
|
|
10
|
-
const [selected, setSelected] = useState(0);
|
|
11
|
-
const barRef = useRef<HTMLDivElement>(null);
|
|
12
|
-
const dragging = useRef(false);
|
|
13
|
-
|
|
14
|
-
const stops = props.stops;
|
|
15
|
-
const current = stops[Math.min(selected, stops.length - 1)];
|
|
16
|
-
|
|
17
|
-
const posFromEvent = (clientX: number) => {
|
|
18
|
-
const rect = barRef.current!.getBoundingClientRect();
|
|
19
|
-
return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const updateStop = (index: number, patch: Partial<GradientStop>) => {
|
|
23
|
-
const next = stops.map((s, i) => (i === index ? {...s, ...patch} : s));
|
|
24
|
-
props.onChange(next);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<div>
|
|
29
|
-
<div
|
|
30
|
-
ref={barRef}
|
|
31
|
-
className="qe-hover"
|
|
32
|
-
style={{
|
|
33
|
-
position: 'relative',
|
|
34
|
-
width: BAR_WIDTH,
|
|
35
|
-
height: 20,
|
|
36
|
-
borderRadius: 6,
|
|
37
|
-
border: `1px solid ${theme.border}`,
|
|
38
|
-
background: `${stopsToCssGradient(stops)}, repeating-conic-gradient(#333 0% 25%, #555 0% 50%) 0 0 / 10px 10px`,
|
|
39
|
-
cursor: 'copy',
|
|
40
|
-
}}
|
|
41
|
-
onPointerDown={(e) => {
|
|
42
|
-
if (e.target !== barRef.current) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const pos = posFromEvent(e.clientX);
|
|
46
|
-
const base = current ?? {r: 1, g: 1, b: 1, a: 1, pos: 0};
|
|
47
|
-
const next = [...stops, {...base, pos}];
|
|
48
|
-
props.onChange(next);
|
|
49
|
-
setSelected(next.length - 1);
|
|
50
|
-
}}
|
|
51
|
-
onPointerMove={(e) => {
|
|
52
|
-
if (dragging.current) {
|
|
53
|
-
updateStop(selected, {pos: posFromEvent(e.clientX)});
|
|
54
|
-
}
|
|
55
|
-
}}
|
|
56
|
-
onPointerUp={() => {
|
|
57
|
-
dragging.current = false;
|
|
58
|
-
}}
|
|
59
|
-
>
|
|
60
|
-
{stops.map((stop, i) => (
|
|
61
|
-
<div
|
|
62
|
-
key={i}
|
|
63
|
-
className="qe-hover"
|
|
64
|
-
style={{
|
|
65
|
-
position: 'absolute',
|
|
66
|
-
left: `calc(${stop.pos * 100}% - 6px)`,
|
|
67
|
-
top: -4,
|
|
68
|
-
width: 12,
|
|
69
|
-
height: 26,
|
|
70
|
-
borderRadius: 4,
|
|
71
|
-
background: rgbToHex(stop.r, stop.g, stop.b),
|
|
72
|
-
border: `2px solid ${i === selected ? theme.accent : '#0a1020'}`,
|
|
73
|
-
cursor: 'ew-resize',
|
|
74
|
-
boxSizing: 'border-box',
|
|
75
|
-
}}
|
|
76
|
-
onPointerDown={(e) => {
|
|
77
|
-
e.stopPropagation();
|
|
78
|
-
setSelected(i);
|
|
79
|
-
dragging.current = true;
|
|
80
|
-
(e.currentTarget as Element).setPointerCapture(e.pointerId);
|
|
81
|
-
}}
|
|
82
|
-
onPointerMove={(e) => {
|
|
83
|
-
if (dragging.current && i === selected) {
|
|
84
|
-
updateStop(i, {pos: posFromEvent(e.clientX)});
|
|
85
|
-
}
|
|
86
|
-
}}
|
|
87
|
-
onPointerUp={(e) => {
|
|
88
|
-
dragging.current = false;
|
|
89
|
-
(e.currentTarget as Element).releasePointerCapture(e.pointerId);
|
|
90
|
-
}}
|
|
91
|
-
/>
|
|
92
|
-
))}
|
|
93
|
-
</div>
|
|
94
|
-
{current && (
|
|
95
|
-
<>
|
|
96
|
-
<Row label="Stop color">
|
|
97
|
-
<input
|
|
98
|
-
type="color"
|
|
99
|
-
className="qe-hover"
|
|
100
|
-
value={rgbToHex(current.r, current.g, current.b)}
|
|
101
|
-
onChange={(e) => updateStop(selected, hexToRgb(e.target.value))}
|
|
102
|
-
style={{width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer'}}
|
|
103
|
-
/>
|
|
104
|
-
</Row>
|
|
105
|
-
<Row label="Stop alpha">
|
|
106
|
-
<NumberField value={current.a} min={0} step={0.05} onChange={(a) => updateStop(selected, {a: Math.min(1, a)})} />
|
|
107
|
-
</Row>
|
|
108
|
-
<Row label="Stop position">
|
|
109
|
-
<NumberField value={current.pos} min={0} step={0.05} onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})} />
|
|
110
|
-
</Row>
|
|
111
|
-
{stops.length > 2 && (
|
|
112
|
-
<button
|
|
113
|
-
className="qe-hover"
|
|
114
|
-
style={{...buttonStyle, marginTop: 6}}
|
|
115
|
-
onClick={() => {
|
|
116
|
-
props.onChange(stops.filter((_, i) => i !== selected));
|
|
117
|
-
setSelected(0);
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
120
|
-
Remove stop
|
|
121
|
-
</button>
|
|
122
|
-
)}
|
|
123
|
-
</>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
1
|
+
import React, {useRef, useState} from 'react';
|
|
2
|
+
import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
|
|
3
|
+
import {NumberField, Row} from './fields';
|
|
4
|
+
import {buttonStyle, theme} from './theme';
|
|
5
|
+
|
|
6
|
+
const BAR_WIDTH = 218;
|
|
7
|
+
|
|
8
|
+
/** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
|
|
9
|
+
export function GradientEditor(props: {stops: GradientStop[]; onChange: (stops: GradientStop[]) => void}) {
|
|
10
|
+
const [selected, setSelected] = useState(0);
|
|
11
|
+
const barRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
const dragging = useRef(false);
|
|
13
|
+
|
|
14
|
+
const stops = props.stops;
|
|
15
|
+
const current = stops[Math.min(selected, stops.length - 1)];
|
|
16
|
+
|
|
17
|
+
const posFromEvent = (clientX: number) => {
|
|
18
|
+
const rect = barRef.current!.getBoundingClientRect();
|
|
19
|
+
return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const updateStop = (index: number, patch: Partial<GradientStop>) => {
|
|
23
|
+
const next = stops.map((s, i) => (i === index ? {...s, ...patch} : s));
|
|
24
|
+
props.onChange(next);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<div
|
|
30
|
+
ref={barRef}
|
|
31
|
+
className="qe-hover"
|
|
32
|
+
style={{
|
|
33
|
+
position: 'relative',
|
|
34
|
+
width: BAR_WIDTH,
|
|
35
|
+
height: 20,
|
|
36
|
+
borderRadius: 6,
|
|
37
|
+
border: `1px solid ${theme.border}`,
|
|
38
|
+
background: `${stopsToCssGradient(stops)}, repeating-conic-gradient(#333 0% 25%, #555 0% 50%) 0 0 / 10px 10px`,
|
|
39
|
+
cursor: 'copy',
|
|
40
|
+
}}
|
|
41
|
+
onPointerDown={(e) => {
|
|
42
|
+
if (e.target !== barRef.current) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const pos = posFromEvent(e.clientX);
|
|
46
|
+
const base = current ?? {r: 1, g: 1, b: 1, a: 1, pos: 0};
|
|
47
|
+
const next = [...stops, {...base, pos}];
|
|
48
|
+
props.onChange(next);
|
|
49
|
+
setSelected(next.length - 1);
|
|
50
|
+
}}
|
|
51
|
+
onPointerMove={(e) => {
|
|
52
|
+
if (dragging.current) {
|
|
53
|
+
updateStop(selected, {pos: posFromEvent(e.clientX)});
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
onPointerUp={() => {
|
|
57
|
+
dragging.current = false;
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{stops.map((stop, i) => (
|
|
61
|
+
<div
|
|
62
|
+
key={i}
|
|
63
|
+
className="qe-hover"
|
|
64
|
+
style={{
|
|
65
|
+
position: 'absolute',
|
|
66
|
+
left: `calc(${stop.pos * 100}% - 6px)`,
|
|
67
|
+
top: -4,
|
|
68
|
+
width: 12,
|
|
69
|
+
height: 26,
|
|
70
|
+
borderRadius: 4,
|
|
71
|
+
background: rgbToHex(stop.r, stop.g, stop.b),
|
|
72
|
+
border: `2px solid ${i === selected ? theme.accent : '#0a1020'}`,
|
|
73
|
+
cursor: 'ew-resize',
|
|
74
|
+
boxSizing: 'border-box',
|
|
75
|
+
}}
|
|
76
|
+
onPointerDown={(e) => {
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
setSelected(i);
|
|
79
|
+
dragging.current = true;
|
|
80
|
+
(e.currentTarget as Element).setPointerCapture(e.pointerId);
|
|
81
|
+
}}
|
|
82
|
+
onPointerMove={(e) => {
|
|
83
|
+
if (dragging.current && i === selected) {
|
|
84
|
+
updateStop(i, {pos: posFromEvent(e.clientX)});
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
onPointerUp={(e) => {
|
|
88
|
+
dragging.current = false;
|
|
89
|
+
(e.currentTarget as Element).releasePointerCapture(e.pointerId);
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
{current && (
|
|
95
|
+
<>
|
|
96
|
+
<Row label="Stop color">
|
|
97
|
+
<input
|
|
98
|
+
type="color"
|
|
99
|
+
className="qe-hover"
|
|
100
|
+
value={rgbToHex(current.r, current.g, current.b)}
|
|
101
|
+
onChange={(e) => updateStop(selected, hexToRgb(e.target.value))}
|
|
102
|
+
style={{width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer'}}
|
|
103
|
+
/>
|
|
104
|
+
</Row>
|
|
105
|
+
<Row label="Stop alpha">
|
|
106
|
+
<NumberField value={current.a} min={0} step={0.05} onChange={(a) => updateStop(selected, {a: Math.min(1, a)})} />
|
|
107
|
+
</Row>
|
|
108
|
+
<Row label="Stop position">
|
|
109
|
+
<NumberField value={current.pos} min={0} step={0.05} onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})} />
|
|
110
|
+
</Row>
|
|
111
|
+
{stops.length > 2 && (
|
|
112
|
+
<button
|
|
113
|
+
className="qe-hover"
|
|
114
|
+
style={{...buttonStyle, marginTop: 6}}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
props.onChange(stops.filter((_, i) => i !== selected));
|
|
117
|
+
setSelected(0);
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
Remove stop
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import React, {useState} from 'react';
|
|
2
|
-
import {theme} from './theme';
|
|
3
|
-
|
|
4
|
-
/** Unity-style module foldout with an optional enable checkbox. */
|
|
5
|
-
export function ModuleSection(props: {
|
|
6
|
-
title: string;
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
enabled?: boolean;
|
|
9
|
-
onToggle?: (enabled: boolean) => void;
|
|
10
|
-
defaultOpen?: boolean;
|
|
11
|
-
}) {
|
|
12
|
-
const [open, setOpen] = useState(props.defaultOpen ?? true);
|
|
13
|
-
const toggleable = props.onToggle !== undefined;
|
|
14
|
-
const dimmed = toggleable && props.enabled === false;
|
|
15
|
-
return (
|
|
16
|
-
<section
|
|
17
|
-
style={{
|
|
18
|
-
border: `1px solid ${theme.border}`,
|
|
19
|
-
borderRadius: 10,
|
|
20
|
-
background: theme.sectionBg,
|
|
21
|
-
marginTop: 8,
|
|
22
|
-
overflow: 'hidden',
|
|
23
|
-
}}
|
|
24
|
-
>
|
|
25
|
-
<header
|
|
26
|
-
className="qe-hover"
|
|
27
|
-
style={{
|
|
28
|
-
display: 'flex',
|
|
29
|
-
alignItems: 'center',
|
|
30
|
-
gap: 8,
|
|
31
|
-
padding: '7px 10px',
|
|
32
|
-
cursor: 'pointer',
|
|
33
|
-
userSelect: 'none',
|
|
34
|
-
background: 'rgba(20, 30, 60, 0.5)',
|
|
35
|
-
}}
|
|
36
|
-
onClick={() => setOpen(!open)}
|
|
37
|
-
>
|
|
38
|
-
<span style={{color: theme.accent, fontSize: 10, width: 10}}>{open ? '▼' : '►'}</span>
|
|
39
|
-
{toggleable && (
|
|
40
|
-
<input
|
|
41
|
-
type="checkbox"
|
|
42
|
-
className="qe-hover"
|
|
43
|
-
checked={props.enabled}
|
|
44
|
-
onClick={(e) => e.stopPropagation()}
|
|
45
|
-
onChange={(e) => props.onToggle?.(e.target.checked)}
|
|
46
|
-
style={{width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer'}}
|
|
47
|
-
/>
|
|
48
|
-
)}
|
|
49
|
-
<span style={{color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600}}>{props.title}</span>
|
|
50
|
-
</header>
|
|
51
|
-
{open && !dimmed && <div style={{padding: '4px 10px 10px'}}>{props.children}</div>}
|
|
52
|
-
</section>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
1
|
+
import React, {useState} from 'react';
|
|
2
|
+
import {theme} from './theme';
|
|
3
|
+
|
|
4
|
+
/** Unity-style module foldout with an optional enable checkbox. */
|
|
5
|
+
export function ModuleSection(props: {
|
|
6
|
+
title: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
onToggle?: (enabled: boolean) => void;
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
}) {
|
|
12
|
+
const [open, setOpen] = useState(props.defaultOpen ?? true);
|
|
13
|
+
const toggleable = props.onToggle !== undefined;
|
|
14
|
+
const dimmed = toggleable && props.enabled === false;
|
|
15
|
+
return (
|
|
16
|
+
<section
|
|
17
|
+
style={{
|
|
18
|
+
border: `1px solid ${theme.border}`,
|
|
19
|
+
borderRadius: 10,
|
|
20
|
+
background: theme.sectionBg,
|
|
21
|
+
marginTop: 8,
|
|
22
|
+
overflow: 'hidden',
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<header
|
|
26
|
+
className="qe-hover"
|
|
27
|
+
style={{
|
|
28
|
+
display: 'flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
gap: 8,
|
|
31
|
+
padding: '7px 10px',
|
|
32
|
+
cursor: 'pointer',
|
|
33
|
+
userSelect: 'none',
|
|
34
|
+
background: 'rgba(20, 30, 60, 0.5)',
|
|
35
|
+
}}
|
|
36
|
+
onClick={() => setOpen(!open)}
|
|
37
|
+
>
|
|
38
|
+
<span style={{color: theme.accent, fontSize: 10, width: 10}}>{open ? '▼' : '►'}</span>
|
|
39
|
+
{toggleable && (
|
|
40
|
+
<input
|
|
41
|
+
type="checkbox"
|
|
42
|
+
className="qe-hover"
|
|
43
|
+
checked={props.enabled}
|
|
44
|
+
onClick={(e) => e.stopPropagation()}
|
|
45
|
+
onChange={(e) => props.onToggle?.(e.target.checked)}
|
|
46
|
+
style={{width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer'}}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
<span style={{color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600}}>{props.title}</span>
|
|
50
|
+
</header>
|
|
51
|
+
{open && !dimmed && <div style={{padding: '4px 10px 10px'}}>{props.children}</div>}
|
|
52
|
+
</section>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import {Quaternion} from '@babylonjs/core/Maths/math.vector';
|
|
4
|
+
import {ModuleSection} from './ModuleSection';
|
|
5
|
+
import {NumberField, Row} from './fields';
|
|
6
|
+
|
|
7
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
8
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
9
|
+
|
|
10
|
+
function getEulerDegrees(node: TransformNode): {x: number; y: number; z: number} {
|
|
11
|
+
const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation;
|
|
12
|
+
return {x: euler.x * RAD_TO_DEG, y: euler.y * RAD_TO_DEG, z: euler.z * RAD_TO_DEG};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setEulerDegreeAxis(node: TransformNode, axis: 'x' | 'y' | 'z', degrees: number): void {
|
|
16
|
+
const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation.clone();
|
|
17
|
+
euler[axis] = degrees * DEG_TO_RAD;
|
|
18
|
+
if (node.rotationQuaternion) {
|
|
19
|
+
node.rotationQuaternion = Quaternion.FromEulerVector(euler);
|
|
20
|
+
} else {
|
|
21
|
+
node.rotation = euler;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function AxisRow(props: {label: string; values: {x: number; y: number; z: number}; step: number; onChange: (axis: 'x' | 'y' | 'z', value: number) => void}) {
|
|
26
|
+
return (
|
|
27
|
+
<Row label={props.label}>
|
|
28
|
+
<div style={{display: 'flex', gap: 4}}>
|
|
29
|
+
<NumberField value={props.values.x} step={props.step} onChange={(v) => props.onChange('x', v)} />
|
|
30
|
+
<NumberField value={props.values.y} step={props.step} onChange={(v) => props.onChange('y', v)} />
|
|
31
|
+
<NumberField value={props.values.z} step={props.step} onChange={(v) => props.onChange('z', v)} />
|
|
32
|
+
</div>
|
|
33
|
+
</Row>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ObjectModule(props: {node: TransformNode; label: string}) {
|
|
38
|
+
const {node} = props;
|
|
39
|
+
const euler = getEulerDegrees(node);
|
|
40
|
+
return (
|
|
41
|
+
<ModuleSection title={`Object — ${props.label}`}>
|
|
42
|
+
<AxisRow label="Position" values={{x: node.position.x, y: node.position.y, z: node.position.z}} step={0.1} onChange={(axis, v) => (node.position[axis] = v)} />
|
|
43
|
+
<AxisRow label="Rotation" values={euler} step={1} onChange={(axis, v) => setEulerDegreeAxis(node, axis, v)} />
|
|
44
|
+
<AxisRow label="Scale" values={{x: node.scaling.x, y: node.scaling.y, z: node.scaling.z}} step={0.1} onChange={(axis, v) => (node.scaling[axis] = v)} />
|
|
45
|
+
</ModuleSection>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
-
import {buttonStyle, inputStyle, theme} from './theme';
|
|
3
|
-
|
|
4
|
-
export interface PromptDialogProps {
|
|
5
|
-
title: string;
|
|
6
|
-
defaultValue?: string;
|
|
7
|
-
placeholder?: string;
|
|
8
|
-
confirmLabel?: string;
|
|
9
|
-
/** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
|
|
10
|
-
allowEmpty?: boolean;
|
|
11
|
-
onSubmit: (value: string) => void;
|
|
12
|
-
onCancel: () => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
|
|
16
|
-
* Enter, cancels on Escape or backdrop click. */
|
|
17
|
-
export function PromptDialog(props: PromptDialogProps): React.ReactElement {
|
|
18
|
-
const {title, defaultValue = '', placeholder, confirmLabel = 'OK', allowEmpty = false, onSubmit, onCancel} = props;
|
|
19
|
-
const [value, setValue] = useState(defaultValue);
|
|
20
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
inputRef.current?.focus();
|
|
24
|
-
inputRef.current?.select();
|
|
25
|
-
}, []);
|
|
26
|
-
|
|
27
|
-
const submit = () => {
|
|
28
|
-
const trimmed = value.trim();
|
|
29
|
-
if (trimmed || allowEmpty) {
|
|
30
|
-
onSubmit(trimmed);
|
|
31
|
-
} else {
|
|
32
|
-
onCancel();
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
style={{
|
|
39
|
-
position: 'fixed',
|
|
40
|
-
inset: 0,
|
|
41
|
-
background: 'rgba(3, 5, 12, 0.6)',
|
|
42
|
-
display: 'flex',
|
|
43
|
-
alignItems: 'center',
|
|
44
|
-
justifyContent: 'center',
|
|
45
|
-
zIndex: 1000,
|
|
46
|
-
fontFamily: theme.font,
|
|
47
|
-
}}
|
|
48
|
-
onClick={onCancel}
|
|
49
|
-
>
|
|
50
|
-
<div
|
|
51
|
-
style={{
|
|
52
|
-
background: theme.panelBg,
|
|
53
|
-
border: `1px solid ${theme.border}`,
|
|
54
|
-
borderRadius: 10,
|
|
55
|
-
padding: 16,
|
|
56
|
-
minWidth: 300,
|
|
57
|
-
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
|
|
58
|
-
}}
|
|
59
|
-
onClick={(e) => e.stopPropagation()}
|
|
60
|
-
>
|
|
61
|
-
<div style={{fontSize: 13, color: theme.text, marginBottom: 10}}>{title}</div>
|
|
62
|
-
<input
|
|
63
|
-
ref={inputRef}
|
|
64
|
-
className="qe-hover"
|
|
65
|
-
style={{...inputStyle, marginBottom: 14}}
|
|
66
|
-
value={value}
|
|
67
|
-
placeholder={placeholder}
|
|
68
|
-
onChange={(e) => setValue(e.target.value)}
|
|
69
|
-
onKeyDown={(e) => {
|
|
70
|
-
if (e.key === 'Enter') submit();
|
|
71
|
-
if (e.key === 'Escape') onCancel();
|
|
72
|
-
}}
|
|
73
|
-
/>
|
|
74
|
-
<div style={{display: 'flex', justifyContent: 'flex-end', gap: 8}}>
|
|
75
|
-
<button className="qe-hover" style={buttonStyle} onClick={onCancel}>
|
|
76
|
-
Cancel
|
|
77
|
-
</button>
|
|
78
|
-
<button className="qe-hover" style={{...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent}} onClick={submit}>
|
|
79
|
-
{confirmLabel}
|
|
80
|
-
</button>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
1
|
+
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {buttonStyle, inputStyle, theme} from './theme';
|
|
3
|
+
|
|
4
|
+
export interface PromptDialogProps {
|
|
5
|
+
title: string;
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
confirmLabel?: string;
|
|
9
|
+
/** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
|
|
10
|
+
allowEmpty?: boolean;
|
|
11
|
+
onSubmit: (value: string) => void;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
|
|
16
|
+
* Enter, cancels on Escape or backdrop click. */
|
|
17
|
+
export function PromptDialog(props: PromptDialogProps): React.ReactElement {
|
|
18
|
+
const {title, defaultValue = '', placeholder, confirmLabel = 'OK', allowEmpty = false, onSubmit, onCancel} = props;
|
|
19
|
+
const [value, setValue] = useState(defaultValue);
|
|
20
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
inputRef.current?.focus();
|
|
24
|
+
inputRef.current?.select();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const submit = () => {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (trimmed || allowEmpty) {
|
|
30
|
+
onSubmit(trimmed);
|
|
31
|
+
} else {
|
|
32
|
+
onCancel();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
position: 'fixed',
|
|
40
|
+
inset: 0,
|
|
41
|
+
background: 'rgba(3, 5, 12, 0.6)',
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
zIndex: 1000,
|
|
46
|
+
fontFamily: theme.font,
|
|
47
|
+
}}
|
|
48
|
+
onClick={onCancel}
|
|
49
|
+
>
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
background: theme.panelBg,
|
|
53
|
+
border: `1px solid ${theme.border}`,
|
|
54
|
+
borderRadius: 10,
|
|
55
|
+
padding: 16,
|
|
56
|
+
minWidth: 300,
|
|
57
|
+
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
|
|
58
|
+
}}
|
|
59
|
+
onClick={(e) => e.stopPropagation()}
|
|
60
|
+
>
|
|
61
|
+
<div style={{fontSize: 13, color: theme.text, marginBottom: 10}}>{title}</div>
|
|
62
|
+
<input
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
className="qe-hover"
|
|
65
|
+
style={{...inputStyle, marginBottom: 14}}
|
|
66
|
+
value={value}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
onChange={(e) => setValue(e.target.value)}
|
|
69
|
+
onKeyDown={(e) => {
|
|
70
|
+
if (e.key === 'Enter') submit();
|
|
71
|
+
if (e.key === 'Escape') onCancel();
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
<div style={{display: 'flex', justifyContent: 'flex-end', gap: 8}}>
|
|
75
|
+
<button className="qe-hover" style={buttonStyle} onClick={onCancel}>
|
|
76
|
+
Cancel
|
|
77
|
+
</button>
|
|
78
|
+
<button className="qe-hover" style={{...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent}} onClick={submit}>
|
|
79
|
+
{confirmLabel}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|