@scenoco-three/editor 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -93
- package/dist/cli/index.js +49 -115
- package/dist-app/assets/EffectComposer-lekj9UUq.js +1 -0
- package/dist-app/assets/GLTFLoader-C2lUhzim.js +1 -0
- package/dist-app/assets/OutputPass-DZtd342R.js +72 -0
- package/dist-app/assets/Pass-BP3b3hlf.js +1 -0
- package/dist-app/assets/RenderPass-BjVchlYg.js +1 -0
- package/dist-app/assets/ShaderPass-DMXLcV9X.js +24 -0
- package/dist-app/assets/SkeletonUtils-Cxy_owx9.js +1 -0
- package/dist-app/assets/index-AxOpglZ-.css +1 -0
- package/dist-app/assets/index-D-Iq7tix.js +4135 -0
- package/dist-app/assets/inter-CaiW00F8.js +1 -0
- package/dist-app/assets/registry-BOM35tSr.js +1433 -0
- package/dist-app/assets/worker-_Lh8Vt-i.js +1270 -0
- package/dist-app/examples/breakout/assets/geometry/ball.geometry.xml +1 -0
- package/dist-app/examples/breakout/assets/geometry/brick.geometry.xml +1 -0
- package/dist-app/examples/breakout/assets/geometry/floor-sensor.geometry.xml +1 -0
- package/dist-app/examples/breakout/assets/geometry/paddle.geometry.xml +1 -0
- package/dist-app/examples/breakout/assets/geometry/wall-side.geometry.xml +1 -0
- package/dist-app/examples/breakout/assets/geometry/wall-top.geometry.xml +1 -0
- package/dist-app/examples/breakout/assets/materials/ball.material.xml +1 -0
- package/dist-app/examples/breakout/assets/materials/brick-firm.material.xml +1 -0
- package/dist-app/examples/breakout/assets/materials/brick-hard.material.xml +1 -0
- package/dist-app/examples/breakout/assets/materials/brick-soft.material.xml +1 -0
- package/dist-app/examples/breakout/assets/materials/paddle.material.xml +10 -0
- package/dist-app/examples/breakout/assets/materials/wall.material.xml +1 -0
- package/dist-app/examples/breakout/assets/render/neon.render.xml +14 -0
- package/dist-app/examples/breakout/assets/scenes/arena.prefab.xml +68 -0
- package/dist-app/examples/breakout/assets/scenes/brick.prefab.xml +15 -0
- package/dist-app/examples/breakout/assets/scenes/hud.scene.xml +56 -0
- package/dist-app/examples/breakout/assets/scenes/level-1.scene.xml +36 -0
- package/dist-app/examples/breakout/assets/scenes/level-2.scene.xml +36 -0
- package/dist-app/examples/breakout/assets/scenes/level-3.scene.xml +36 -0
- package/dist-app/examples/castle/assets/geometry/capsule.geometry.xml +3 -0
- package/dist-app/examples/castle/assets/geometry/cone.geometry.xml +2 -0
- package/dist-app/examples/castle/assets/geometry/cube.geometry.xml +3 -0
- package/dist-app/examples/castle/assets/geometry/cylinder.geometry.xml +3 -0
- package/dist-app/examples/castle/assets/geometry/plane.geometry.xml +4 -0
- package/dist-app/examples/castle/assets/geometry/quad.geometry.xml +3 -0
- package/dist-app/examples/castle/assets/geometry/sphere.geometry.xml +2 -0
- package/dist-app/examples/castle/assets/materials/grass.material.xml +1 -0
- package/dist-app/examples/castle/assets/materials/roof.material.xml +1 -0
- package/dist-app/examples/castle/assets/materials/stone.material.xml +1 -0
- package/dist-app/examples/castle/assets/materials/wood.material.xml +1 -0
- package/dist-app/examples/castle/assets/scenes/main.scene.xml +25 -0
- package/dist-app/examples/collector/assets/geometry/capsule.geometry.xml +3 -0
- package/dist-app/examples/collector/assets/geometry/cone.geometry.xml +2 -0
- package/dist-app/examples/collector/assets/geometry/cube.geometry.xml +3 -0
- package/dist-app/examples/collector/assets/geometry/cylinder.geometry.xml +3 -0
- package/dist-app/examples/collector/assets/geometry/plane.geometry.xml +4 -0
- package/dist-app/examples/collector/assets/geometry/quad.geometry.xml +3 -0
- package/dist-app/examples/collector/assets/geometry/sphere.geometry.xml +2 -0
- package/dist-app/examples/collector/assets/materials/crate.material.xml +1 -0
- package/dist-app/examples/collector/assets/materials/depot.material.xml +1 -0
- package/dist-app/examples/collector/assets/materials/ground.material.xml +1 -0
- package/dist-app/examples/collector/assets/materials/player.material.xml +1 -0
- package/dist-app/examples/collector/assets/scenes/crate.prefab.xml +9 -0
- package/dist-app/examples/collector/assets/scenes/main.scene.xml +83 -0
- package/dist-app/examples/playable/assets/geometry/capsule.geometry.xml +3 -0
- package/dist-app/examples/playable/assets/geometry/cone.geometry.xml +2 -0
- package/dist-app/examples/playable/assets/geometry/cube.geometry.xml +3 -0
- package/dist-app/examples/playable/assets/geometry/cylinder.geometry.xml +3 -0
- package/dist-app/examples/playable/assets/geometry/plane.geometry.xml +4 -0
- package/dist-app/examples/playable/assets/geometry/quad.geometry.xml +3 -0
- package/dist-app/examples/playable/assets/geometry/sphere.geometry.xml +2 -0
- package/dist-app/examples/playable/assets/materials/ground.material.xml +1 -0
- package/dist-app/examples/playable/assets/materials/target.material.xml +2 -0
- package/dist-app/examples/playable/assets/scenes/main.scene.xml +53 -0
- package/dist-app/examples/playable/assets/scenes/target.prefab.xml +9 -0
- package/dist-app/examples.json +68 -0
- package/{app → dist-app}/index.html +2 -2
- package/dist-app/model.json +1 -0
- package/package.json +26 -30
- package/app/cameraRig.ts +0 -118
- package/app/commands.ts +0 -78
- package/app/desktopControls.ts +0 -47
- package/app/document.ts +0 -547
- package/app/editLog.ts +0 -127
- package/app/editor.css +0 -51
- package/app/gizmo.ts +0 -169
- package/app/main.ts +0 -543
- package/app/meta.ts +0 -24
- package/app/picking.ts +0 -47
- package/app/session.ts +0 -572
- package/app/touchControls.ts +0 -127
- package/app/ui-react/AddMemberPopup.tsx +0 -114
- package/app/ui-react/BottomLauncher.tsx +0 -15
- package/app/ui-react/EditorUI.tsx +0 -22
- package/app/ui-react/FilePicker.tsx +0 -80
- package/app/ui-react/HeadBtn.tsx +0 -21
- package/app/ui-react/InspectorSheet.tsx +0 -475
- package/app/ui-react/NodeListSheet.tsx +0 -56
- package/app/ui-react/TopBar.tsx +0 -137
- package/app/ui-react/Tree.tsx +0 -106
- package/app/ui-react/cx.ts +0 -4
- package/app/ui-react/fields.tsx +0 -407
- package/app/ui-react/format.ts +0 -30
- package/app/ui-react/mount.tsx +0 -31
- package/app/ui-react/store.ts +0 -80
- package/app/ui-react/types.ts +0 -88
- package/app/ui-react/ui.module.css +0 -250
- package/app/ui-react/useSize.ts +0 -20
- package/app/values.ts +0 -137
- package/app/vite-env.d.ts +0 -7
- package/dist/index.d.ts +0 -10
- package/dist/index.js +0 -10
- package/dist/mcp.d.ts +0 -32
- package/dist/mcp.js +0 -191
- package/dist/protocol.d.ts +0 -170
- package/dist/protocol.js +0 -22
- package/dist/server.d.ts +0 -124
- package/dist/server.js +0 -437
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import { useEffect, useReducer, useRef, useState, type ReactNode } from 'react';
|
|
2
|
-
import { ChevronDown, ChevronRight, Plus, Trash2, X } from 'lucide-react';
|
|
3
|
-
import type { Object3D } from 'three';
|
|
4
|
-
import type { PropInfo } from '@scenoco-three/compiler';
|
|
5
|
-
import type { SceneDocument } from '../document.js';
|
|
6
|
-
import { shownValue, widgetFor, type Scalar } from '../values.js';
|
|
7
|
-
import { getDeps, useUI } from './store.js';
|
|
8
|
-
import { cx } from './cx.js';
|
|
9
|
-
import { BoolRow, ColorRow, EnumRow, EulerRow, LockRow, NumberRow, TextRow, VecRow } from './fields.js';
|
|
10
|
-
import { HeadBtn } from './HeadBtn.js';
|
|
11
|
-
import styles from './ui.module.css';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The inspector.
|
|
15
|
-
*
|
|
16
|
-
* ### What it shows
|
|
17
|
-
*
|
|
18
|
-
* Everything the open document **can** write, with what it *does* write marked and clearable. A
|
|
19
|
-
* field left unwritten inherits whatever the class decides later; one written with today's default
|
|
20
|
-
* is pinned to today's value — so the distinction is worth showing, and it is why `PropInfo`
|
|
21
|
-
* carries `default` separately from the current value.
|
|
22
|
-
*
|
|
23
|
-
* ### Where the schema comes from
|
|
24
|
-
*
|
|
25
|
-
* `ProjectModel`, read from the project's own TypeScript: field names, types, defaults, doc
|
|
26
|
-
* comments. v1 read a runtime registry that decorators populated, so the inspector could only
|
|
27
|
-
* show what had been restated in a `@property`. Nothing is restated now, and nothing can drift.
|
|
28
|
-
*
|
|
29
|
-
* Attachments are gone with `<Attachments>` — a geometry and a material are assets a mesh points
|
|
30
|
-
* at, so they show as ordinary attributes.
|
|
31
|
-
*/
|
|
32
|
-
const PREFAB_REASON = 'From another document — open it to edit this.';
|
|
33
|
-
const TYPE_REASON = 'No attribute can express this type.';
|
|
34
|
-
|
|
35
|
-
/** Sections the user has folded away, by title. Module state: one inspector per page. */
|
|
36
|
-
const closedSections = new Set<string>();
|
|
37
|
-
/** Sections already shown once, so `startClosed` folds them the first time and never again. */
|
|
38
|
-
const seenSections = new Set<string>();
|
|
39
|
-
|
|
40
|
-
export function InspectorSheet(): React.JSX.Element | null {
|
|
41
|
-
const open = useUI((s) => s.inspectorOpen);
|
|
42
|
-
useUI((s) => s.rev); // re-read live values on refresh
|
|
43
|
-
const selected = useUI((s) => s.selected);
|
|
44
|
-
const closeInspector = useUI((s) => s.closeInspector);
|
|
45
|
-
if (!open) return null;
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div className={cx(styles.sheet, styles.sheetInspector)}>
|
|
49
|
-
<div className={styles.head}>
|
|
50
|
-
<span className={styles.title}>Inspector</span>
|
|
51
|
-
<div className={styles.actions}>
|
|
52
|
-
<HeadBtn label="Close" onClick={closeInspector}>
|
|
53
|
-
<X />
|
|
54
|
-
</HeadBtn>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<div className={styles.body}>
|
|
58
|
-
{selected ? (
|
|
59
|
-
<InspectorBody node={selected} />
|
|
60
|
-
) : (
|
|
61
|
-
<div className={styles.inspector}>
|
|
62
|
-
<div className={styles.empty}>Select a node — tap it in the viewport or the node list.</div>
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function InspectorBody({ node }: { node: Object3D }): React.JSX.Element {
|
|
71
|
-
const { getDoc } = getDeps();
|
|
72
|
-
const doc = getDoc();
|
|
73
|
-
const foreign = doc?.foreignSource(node);
|
|
74
|
-
const mine = doc?.canEditTransform(node) ?? false;
|
|
75
|
-
const sections: ReactNode[] = [];
|
|
76
|
-
|
|
77
|
-
const tag = doc?.tagOf(node);
|
|
78
|
-
const tagName = doc?.tagNameOf(node) ?? node.type;
|
|
79
|
-
|
|
80
|
-
const renderNodeProp = (prop: PropInfo): React.JSX.Element => (
|
|
81
|
-
<PropRow
|
|
82
|
-
key={prop.name}
|
|
83
|
-
prop={prop}
|
|
84
|
-
written={doc?.nodeAttr(node, prop.name)}
|
|
85
|
-
editable={mine}
|
|
86
|
-
commit={(v, c) => doc?.setNodeValue(node, prop, v, c)}
|
|
87
|
-
clear={() => doc?.clearNodeAttr(node, prop.name)}
|
|
88
|
-
/>
|
|
89
|
-
);
|
|
90
|
-
const tagProps = split([...(tag?.parameters ?? []), ...(tag?.properties ?? [])], (p) =>
|
|
91
|
-
doc?.nodeAttr(node, p.name) !== undefined,
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Placement, which every node takes. Written from the live object, so a gizmo drag and a typed
|
|
95
|
-
// number are the same edit — in the document's units, which for rotation means degrees.
|
|
96
|
-
const placement = (
|
|
97
|
-
<Section key="placement" title="Placement">
|
|
98
|
-
{mine ? (
|
|
99
|
-
<>
|
|
100
|
-
<VecRow
|
|
101
|
-
label="position"
|
|
102
|
-
value={node.position.toArray()}
|
|
103
|
-
commit={(v, c) => commitVector(doc, node, 'position', v, c)}
|
|
104
|
-
description="Local position"
|
|
105
|
-
/>
|
|
106
|
-
<EulerRow
|
|
107
|
-
label="eulerAngles"
|
|
108
|
-
value={node.eulerAngles.toArray()}
|
|
109
|
-
commit={(v, c) => commitVector(doc, node, 'eulerAngles', v, c)}
|
|
110
|
-
description="Local rotation, in degrees"
|
|
111
|
-
/>
|
|
112
|
-
<VecRow
|
|
113
|
-
label="scale"
|
|
114
|
-
value={node.scale.toArray()}
|
|
115
|
-
commit={(v, c) => commitVector(doc, node, 'scale', v, c)}
|
|
116
|
-
description="Local scale"
|
|
117
|
-
/>
|
|
118
|
-
</>
|
|
119
|
-
) : (
|
|
120
|
-
<>
|
|
121
|
-
<LockRow label="position" value={fmtVec(node.position.toArray())} reason={PREFAB_REASON} />
|
|
122
|
-
<LockRow label="eulerAngles" value={fmtVec(node.eulerAngles.toArray())} reason={PREFAB_REASON} />
|
|
123
|
-
<LockRow label="scale" value={fmtVec(node.scale.toArray())} reason={PREFAB_REASON} />
|
|
124
|
-
</>
|
|
125
|
-
)}
|
|
126
|
-
</Section>
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// The components on this node, each with what its class declares.
|
|
130
|
-
for (const [i, { component, info }] of (doc?.componentsOf(node) ?? []).entries()) {
|
|
131
|
-
const name = component.constructor.name;
|
|
132
|
-
// Per component, not per node: a prefab instance sits in this document while its components
|
|
133
|
-
// are declared in the prefab's, and only the second question decides whether a field writes.
|
|
134
|
-
const editableHere = doc?.canEditComponent(component) ?? false;
|
|
135
|
-
sections.push(
|
|
136
|
-
<Section
|
|
137
|
-
key={`comp-${i}`}
|
|
138
|
-
title={name}
|
|
139
|
-
{...(info?.description ? { description: info.description } : {})}
|
|
140
|
-
actions={
|
|
141
|
-
editableHere ? <MemberActions onDelete={() => doc?.removeComponent(component)} /> : undefined
|
|
142
|
-
}
|
|
143
|
-
>
|
|
144
|
-
{(() => {
|
|
145
|
-
const render = (prop: PropInfo): React.JSX.Element => (
|
|
146
|
-
<PropRow
|
|
147
|
-
key={prop.name}
|
|
148
|
-
prop={prop}
|
|
149
|
-
written={doc?.componentProp(component, prop.name)}
|
|
150
|
-
editable={editableHere}
|
|
151
|
-
{...(editableHere ? {} : { live: liveText(component, prop.name) })}
|
|
152
|
-
commit={(v, c) => doc?.setComponentValue(component, prop, v, c)}
|
|
153
|
-
clear={() => doc?.clearComponentProp(component, prop.name)}
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
const fields = split(info?.fields ?? [], (p) => doc?.componentProp(component, p.name) !== undefined);
|
|
157
|
-
return (
|
|
158
|
-
<>
|
|
159
|
-
{fields.written.map(render)}
|
|
160
|
-
{fields.rest.length > 0 ? (
|
|
161
|
-
<Section title={`${fields.rest.length} more on ${name}`} startClosed>
|
|
162
|
-
{fields.rest.map(render)}
|
|
163
|
-
</Section>
|
|
164
|
-
) : null}
|
|
165
|
-
</>
|
|
166
|
-
);
|
|
167
|
-
})()}
|
|
168
|
-
</Section>,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
if (doc?.canEditComponents(node)) sections.push(<AddRow key="add-comp" node={node} />);
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className={styles.inspector}>
|
|
175
|
-
<div className={cx(styles.secHead, styles.secHeadPlain)}>
|
|
176
|
-
{tagName}
|
|
177
|
-
{foreign ? <span className={styles.secDesc}>from {foreign}</span> : null}
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
{/* Unity's order, and for Unity's reason: what a node *is* — its name and whether it is
|
|
181
|
-
on — then where it is, then what it does. Identity and placement are what you reach for
|
|
182
|
-
on almost every selection; a tag's own attributes and its components are what you came
|
|
183
|
-
for on some of them. */}
|
|
184
|
-
{mine ? (
|
|
185
|
-
<IdentityRow
|
|
186
|
-
id={doc?.nodeAttr(node, 'id') ?? ''}
|
|
187
|
-
active={doc?.nodeAttr(node, 'active') !== 'false'}
|
|
188
|
-
setId={(v) => doc?.setNodeAttr(node, 'id', v === '' ? null : v)}
|
|
189
|
-
setActive={(v) => doc?.setNodeAttr(node, 'active', v ? null : 'false')}
|
|
190
|
-
/>
|
|
191
|
-
) : null}
|
|
192
|
-
{mine ? (
|
|
193
|
-
<BoolRow
|
|
194
|
-
label="visible"
|
|
195
|
-
value={node.visible}
|
|
196
|
-
commit={(v) => doc?.setVisible(node, v)}
|
|
197
|
-
unset={doc?.nodeAttr(node, 'visible') === undefined}
|
|
198
|
-
description="Hides the node while it keeps updating. `active` turns the whole subtree off."
|
|
199
|
-
{...(doc?.nodeAttr(node, 'visible') !== undefined
|
|
200
|
-
? { onClear: () => doc?.clearNodeAttr(node, 'visible') }
|
|
201
|
-
: {})}
|
|
202
|
-
/>
|
|
203
|
-
) : null}
|
|
204
|
-
|
|
205
|
-
{placement}
|
|
206
|
-
|
|
207
|
-
{/* The tag's own attributes — a camera's `fov`, a light's `intensity` — read from
|
|
208
|
-
three.js's declarations rather than from a table in this repo.
|
|
209
|
-
|
|
210
|
-
What the document *writes* stays in front; the rest folds away. `Mesh` alone declares
|
|
211
|
-
some thirty settable properties and a package that augments `Object3D` adds its own to
|
|
212
|
-
every tag, so showing all of them inline buries the three that were authored. */}
|
|
213
|
-
{tagProps.written.length > 0 || tagProps.rest.length > 0 ? (
|
|
214
|
-
<Section title={`<${tagName}>`}>
|
|
215
|
-
{tagProps.written.map(renderNodeProp)}
|
|
216
|
-
{tagProps.rest.length > 0 ? (
|
|
217
|
-
<Section title={`${tagProps.rest.length} more on <${tagName}>`} startClosed>
|
|
218
|
-
{tagProps.rest.map(renderNodeProp)}
|
|
219
|
-
</Section>
|
|
220
|
-
) : null}
|
|
221
|
-
</Section>
|
|
222
|
-
) : null}
|
|
223
|
-
|
|
224
|
-
{sections}
|
|
225
|
-
</div>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* One attribute, dispatched by its **shape** — the single question the whole DSL rests on, and
|
|
231
|
-
* the one the compiler already answered by reading the field's type.
|
|
232
|
-
*
|
|
233
|
-
* v1 dispatched on a `ValueType` the author restated in a decorator. There is no decorator now,
|
|
234
|
-
* so a field is a colour because it is declared `Color`.
|
|
235
|
-
*/
|
|
236
|
-
function PropRow({
|
|
237
|
-
prop,
|
|
238
|
-
written,
|
|
239
|
-
editable,
|
|
240
|
-
live,
|
|
241
|
-
commit,
|
|
242
|
-
clear,
|
|
243
|
-
}: {
|
|
244
|
-
prop: PropInfo;
|
|
245
|
-
written: string | undefined;
|
|
246
|
-
editable: boolean;
|
|
247
|
-
/** What the object actually holds — shown on a locked row, where `written` says nothing. */
|
|
248
|
-
live?: string | undefined;
|
|
249
|
-
commit: (value: Scalar, opts?: { coalesce?: boolean }) => void;
|
|
250
|
-
clear: () => void;
|
|
251
|
-
}): React.JSX.Element | null {
|
|
252
|
-
const shape = widgetFor(prop);
|
|
253
|
-
const value = shownValue(prop, written);
|
|
254
|
-
const unset = written === undefined;
|
|
255
|
-
const description = `${prop.description ?? prop.type}${unset ? '\n\nDefault — not written in the file.' : ''}`;
|
|
256
|
-
const onClear = !unset && editable ? clear : undefined;
|
|
257
|
-
|
|
258
|
-
if (shape === null) return <LockRow label={prop.name} value={String(value)} reason={TYPE_REASON} description={prop.description} />;
|
|
259
|
-
// A locked row prefers the live value. The document has nothing written for something another
|
|
260
|
-
// file declares, and falling back to the class default *looks* like a fact: a crate whose
|
|
261
|
-
// prefab sets `gravityScale` to 1 would read 0 here, which is worse than saying nothing.
|
|
262
|
-
if (!editable) {
|
|
263
|
-
return (
|
|
264
|
-
<LockRow
|
|
265
|
-
label={prop.name}
|
|
266
|
-
// Blank rather than the class default when the live value is not something a row can
|
|
267
|
-
// show: the document says nothing about this field, so neither should the inspector.
|
|
268
|
-
value={live ?? ''}
|
|
269
|
-
reason={PREFAB_REASON}
|
|
270
|
-
description={prop.description}
|
|
271
|
-
/>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const common = { label: prop.name, description, unset, ...(onClear ? { onClear } : {}) };
|
|
276
|
-
switch (shape) {
|
|
277
|
-
case 'number':
|
|
278
|
-
return <NumberRow {...common} value={value as number} commit={(v, c) => commit(v, c)} />;
|
|
279
|
-
case 'boolean':
|
|
280
|
-
return <BoolRow {...common} value={value as boolean} commit={(v) => commit(v)} />;
|
|
281
|
-
case 'color':
|
|
282
|
-
return <ColorRow {...common} value={value as number} commit={(v) => commit(v)} />;
|
|
283
|
-
case 'vector2':
|
|
284
|
-
return <VecRow {...common} value={value as number[]} labels={['x', 'y']} commit={(v, c) => commit(v, c)} />;
|
|
285
|
-
case 'vector3':
|
|
286
|
-
case 'vector4':
|
|
287
|
-
return <VecRow {...common} value={value as number[]} commit={(v, c) => commit(v, c)} />;
|
|
288
|
-
case 'euler':
|
|
289
|
-
return <EulerRow {...common} value={value as number[]} commit={(v, c) => commit(v, c)} />;
|
|
290
|
-
case 'enum':
|
|
291
|
-
return (
|
|
292
|
-
<EnumRow
|
|
293
|
-
{...common}
|
|
294
|
-
value={String(value)}
|
|
295
|
-
options={(prop.options ?? []).map((o) => o.replace(/^['"]|['"]$/g, ''))}
|
|
296
|
-
commit={(v) => commit(v)}
|
|
297
|
-
/>
|
|
298
|
-
);
|
|
299
|
-
default:
|
|
300
|
-
// `string`, and the two reference shapes — a `#id` and an asset path are both text a
|
|
301
|
-
// person types, and the compiler is what checks them.
|
|
302
|
-
return <TextRow {...common} value={String(value)} commit={(v) => commit(v)} />;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* The node's identity: whether it is on, and what it is called — one row, as Unity has it.
|
|
308
|
-
*
|
|
309
|
-
* `active` is written **only when false**. A document that says nothing is active, so writing
|
|
310
|
-
* `active="true"` would pin today's default into the file for no gain; unticking and re-ticking
|
|
311
|
-
* therefore leaves the document exactly as it was found.
|
|
312
|
-
*/
|
|
313
|
-
function IdentityRow({
|
|
314
|
-
id,
|
|
315
|
-
active,
|
|
316
|
-
setId,
|
|
317
|
-
setActive,
|
|
318
|
-
}: {
|
|
319
|
-
id: string;
|
|
320
|
-
active: boolean;
|
|
321
|
-
setId: (value: string) => void;
|
|
322
|
-
setActive: (value: boolean) => void;
|
|
323
|
-
}): React.JSX.Element {
|
|
324
|
-
const [text, setText] = useState(id);
|
|
325
|
-
const focused = useRef(false);
|
|
326
|
-
useEffect(() => {
|
|
327
|
-
if (!focused.current) setText(id);
|
|
328
|
-
}, [id]);
|
|
329
|
-
return (
|
|
330
|
-
<div className={styles.identity}>
|
|
331
|
-
<input
|
|
332
|
-
type="checkbox"
|
|
333
|
-
className={styles.toggle}
|
|
334
|
-
checked={active}
|
|
335
|
-
title="Active — off turns the whole subtree off"
|
|
336
|
-
onChange={(e) => setActive(e.target.checked)}
|
|
337
|
-
/>
|
|
338
|
-
<input
|
|
339
|
-
type="text"
|
|
340
|
-
className={cx(styles.input, styles.identityId)}
|
|
341
|
-
value={text}
|
|
342
|
-
placeholder="id"
|
|
343
|
-
title="A typed field on the generated class, and what #id refers to"
|
|
344
|
-
onChange={(e) => setText(e.target.value)}
|
|
345
|
-
onFocus={() => (focused.current = true)}
|
|
346
|
-
onBlur={() => {
|
|
347
|
-
focused.current = false;
|
|
348
|
-
if (text !== id) setId(text.trim());
|
|
349
|
-
}}
|
|
350
|
-
onKeyDown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()}
|
|
351
|
-
/>
|
|
352
|
-
</div>
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** A gizmo drag and a typed number take the same path: read the live object, write the document. */
|
|
357
|
-
function commitVector(
|
|
358
|
-
doc: SceneDocument | undefined,
|
|
359
|
-
node: Object3D,
|
|
360
|
-
channel: 'position' | 'eulerAngles' | 'scale',
|
|
361
|
-
value: number[],
|
|
362
|
-
opts?: { coalesce?: boolean },
|
|
363
|
-
): void {
|
|
364
|
-
const target = channel === 'eulerAngles' ? node.eulerAngles : node[channel];
|
|
365
|
-
target.set(value[0] ?? 0, value[1] ?? 0, value[2] ?? 0);
|
|
366
|
-
if (channel === 'eulerAngles') node.eulerAngles = target;
|
|
367
|
-
doc?.setTransform(node, channel, opts);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function fmtVec(values: number[]): string {
|
|
371
|
-
return values.map((n) => Math.round(n * 1000) / 1000).join(', ');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** The "+ Add component" row, which opens the picker. */
|
|
375
|
-
function AddRow({ node }: { node: Object3D }): React.JSX.Element {
|
|
376
|
-
const openAdd = useUI((s) => s.openAdd);
|
|
377
|
-
return (
|
|
378
|
-
<button type="button" className={styles.addRow} onClick={() => openAdd(node)}>
|
|
379
|
-
<Plus />
|
|
380
|
-
Add component
|
|
381
|
-
</button>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Per-member actions.
|
|
387
|
-
*
|
|
388
|
-
* Only delete, for now: the compiler can reorder elements, but a component's order in a
|
|
389
|
-
* `<Components>` block does not change what it does, so there is nothing to reorder *for*.
|
|
390
|
-
*/
|
|
391
|
-
function MemberActions({ onDelete }: { onDelete: () => void }): React.JSX.Element {
|
|
392
|
-
const stop = (fn: () => void) => (e: React.MouseEvent) => {
|
|
393
|
-
e.stopPropagation(); // the header itself toggles the fold
|
|
394
|
-
fn();
|
|
395
|
-
};
|
|
396
|
-
return (
|
|
397
|
-
<button
|
|
398
|
-
type="button"
|
|
399
|
-
className={cx(styles.memberBtn, styles.memberDelete)}
|
|
400
|
-
title="Delete"
|
|
401
|
-
aria-label="Delete"
|
|
402
|
-
onClick={stop(onDelete)}
|
|
403
|
-
>
|
|
404
|
-
<Trash2 />
|
|
405
|
-
</button>
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/** What the document writes, and everything else — the inspector's ordering everywhere. */
|
|
410
|
-
function split<T>(items: readonly T[], written: (item: T) => boolean): { written: T[]; rest: T[] } {
|
|
411
|
-
return { written: items.filter(written), rest: items.filter((i) => !written(i)) };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function Section({
|
|
415
|
-
title,
|
|
416
|
-
description,
|
|
417
|
-
actions,
|
|
418
|
-
startClosed,
|
|
419
|
-
children,
|
|
420
|
-
}: {
|
|
421
|
-
title: string;
|
|
422
|
-
description?: string;
|
|
423
|
-
actions?: ReactNode;
|
|
424
|
-
/** Folded on first sight — for the "…more" groups nobody opened deliberately. */
|
|
425
|
-
startClosed?: boolean;
|
|
426
|
-
children: ReactNode;
|
|
427
|
-
}): React.JSX.Element {
|
|
428
|
-
const [, force] = useReducer((x: number) => x + 1, 0);
|
|
429
|
-
if (startClosed && !seenSections.has(title)) {
|
|
430
|
-
seenSections.add(title);
|
|
431
|
-
closedSections.add(title);
|
|
432
|
-
}
|
|
433
|
-
const closed = closedSections.has(title);
|
|
434
|
-
const toggle = (): void => {
|
|
435
|
-
if (closed) closedSections.delete(title);
|
|
436
|
-
else closedSections.add(title);
|
|
437
|
-
force();
|
|
438
|
-
};
|
|
439
|
-
return (
|
|
440
|
-
<div className={styles.section}>
|
|
441
|
-
<div className={styles.secHead} onClick={toggle}>
|
|
442
|
-
<span className={styles.secCaret}>{closed ? <ChevronRight /> : <ChevronDown />}</span>
|
|
443
|
-
{title}
|
|
444
|
-
{actions ? <span className={styles.secActions}>{actions}</span> : null}
|
|
445
|
-
{description ? <span className={styles.secDesc}>{description}</span> : null}
|
|
446
|
-
</div>
|
|
447
|
-
{!closed && <div className={styles.secBody}>{children}</div>}
|
|
448
|
-
</div>
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* What a component actually holds for one field, as text a locked row can show.
|
|
454
|
-
*
|
|
455
|
-
* Undefined for anything an attribute could not have written in the first place — a live physics
|
|
456
|
-
* body, a texture — where the honest row is the name and the lock, not `[object Object]`.
|
|
457
|
-
*/
|
|
458
|
-
function liveText(target: object, name: string): string | undefined {
|
|
459
|
-
const value = (target as Record<string, unknown>)[name];
|
|
460
|
-
if (value === null || value === undefined) return undefined;
|
|
461
|
-
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return String(value);
|
|
462
|
-
const v = value as {
|
|
463
|
-
isColor?: boolean;
|
|
464
|
-
isVector2?: boolean;
|
|
465
|
-
isVector3?: boolean;
|
|
466
|
-
x?: number;
|
|
467
|
-
y?: number;
|
|
468
|
-
z?: number;
|
|
469
|
-
getHexString?: () => string;
|
|
470
|
-
};
|
|
471
|
-
if (v.isColor) return `#${v.getHexString?.() ?? ''}`;
|
|
472
|
-
if (v.isVector3) return `x: ${v.x}; y: ${v.y}; z: ${v.z}`;
|
|
473
|
-
if (v.isVector2) return `x: ${v.x}; y: ${v.y}`;
|
|
474
|
-
return undefined;
|
|
475
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { ClipboardPaste, Copy, Focus, Scissors, SlidersHorizontal, Trash2, X } from 'lucide-react';
|
|
2
|
-
import { getDeps, useUI } from './store.js';
|
|
3
|
-
import { HeadBtn } from './HeadBtn.js';
|
|
4
|
-
import { Tree } from './Tree.js';
|
|
5
|
-
import { cx } from './cx.js';
|
|
6
|
-
import styles from './ui.module.css';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The node list: a bottom sheet with the scene tree and an actions row —
|
|
10
|
-
* **Focus** / **Open inspector** on the selection, the tree-edit actions
|
|
11
|
-
* **Copy / Cut / Paste / Delete**, and close. Paste keys off the clipboard (read
|
|
12
|
-
* on each refresh via `rev`); the rest need a selection.
|
|
13
|
-
*/
|
|
14
|
-
export function NodeListSheet(): React.JSX.Element | null {
|
|
15
|
-
const open = useUI((s) => s.nodesOpen);
|
|
16
|
-
const selected = useUI((s) => s.selected);
|
|
17
|
-
useUI((s) => s.rev); // re-read clipboard state (Paste enablement) on refresh
|
|
18
|
-
const openInspector = useUI((s) => s.openInspector);
|
|
19
|
-
const closeNodes = useUI((s) => s.closeNodes);
|
|
20
|
-
const { focusSelected, copy, cut, paste, remove, hasClipboard } = getDeps();
|
|
21
|
-
if (!open) return null;
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className={cx(styles.sheet, styles.sheetNodes)}>
|
|
25
|
-
<div className={styles.head}>
|
|
26
|
-
<span className={styles.title}>Nodes</span>
|
|
27
|
-
<div className={styles.actions}>
|
|
28
|
-
<HeadBtn label="Focus selected" disabled={!selected} onClick={() => selected && focusSelected(selected)}>
|
|
29
|
-
<Focus />
|
|
30
|
-
</HeadBtn>
|
|
31
|
-
<HeadBtn label="Open inspector" disabled={!selected} onClick={openInspector}>
|
|
32
|
-
<SlidersHorizontal />
|
|
33
|
-
</HeadBtn>
|
|
34
|
-
<HeadBtn label="Copy" disabled={!selected} onClick={() => selected && copy(selected)}>
|
|
35
|
-
<Copy />
|
|
36
|
-
</HeadBtn>
|
|
37
|
-
<HeadBtn label="Cut" disabled={!selected} onClick={() => selected && cut(selected)}>
|
|
38
|
-
<Scissors />
|
|
39
|
-
</HeadBtn>
|
|
40
|
-
<HeadBtn label="Paste" disabled={!hasClipboard()} onClick={() => paste(selected)}>
|
|
41
|
-
<ClipboardPaste />
|
|
42
|
-
</HeadBtn>
|
|
43
|
-
<HeadBtn label="Delete" disabled={!selected} onClick={() => selected && remove(selected)}>
|
|
44
|
-
<Trash2 />
|
|
45
|
-
</HeadBtn>
|
|
46
|
-
<HeadBtn label="Close" onClick={closeNodes}>
|
|
47
|
-
<X />
|
|
48
|
-
</HeadBtn>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
<div className={styles.body}>
|
|
52
|
-
<Tree />
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
}
|
package/app/ui-react/TopBar.tsx
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Axis3d,
|
|
4
|
-
Globe,
|
|
5
|
-
Move,
|
|
6
|
-
PanelTop,
|
|
7
|
-
Pause,
|
|
8
|
-
Play,
|
|
9
|
-
Redo2,
|
|
10
|
-
Rotate3d,
|
|
11
|
-
Save,
|
|
12
|
-
Scaling,
|
|
13
|
-
Square,
|
|
14
|
-
StepForward,
|
|
15
|
-
Undo2,
|
|
16
|
-
} from 'lucide-react';
|
|
17
|
-
import { getDeps, useUI } from './store.js';
|
|
18
|
-
import { cx } from './cx.js';
|
|
19
|
-
import styles from './ui.module.css';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* One responsive toolbar: transport, gizmo mode (move/rotate/scale), the two view switches,
|
|
23
|
-
* undo/redo, and save. Fits any width — it caps at the viewport and scrolls horizontally rather
|
|
24
|
-
* than overflowing (see `.topbar`).
|
|
25
|
-
*
|
|
26
|
-
* Each mode shows only what it can do. Editing offers Play and nothing else from the transport,
|
|
27
|
-
* because Pause, Step and Stop have no meaning until something is running; playing offers the
|
|
28
|
-
* transport and nothing else, because there is no editing while the sim runs. A permanently
|
|
29
|
-
* greyed row of buttons is a worse answer than a row that is the length of what is possible.
|
|
30
|
-
*/
|
|
31
|
-
export function TopBar(): React.JSX.Element {
|
|
32
|
-
useUI((s) => s.rev); // re-read handler state on every refresh
|
|
33
|
-
const { handlers } = getDeps();
|
|
34
|
-
const s = handlers.state();
|
|
35
|
-
const editing = s.mode === 'edit';
|
|
36
|
-
const paused = s.mode === 'play' && !s.playing;
|
|
37
|
-
const mode = handlers.getMode();
|
|
38
|
-
const space = handlers.getSpace();
|
|
39
|
-
const showUi = handlers.getShowUi();
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className={styles.topbar}>
|
|
43
|
-
<TBtn label="Play" onClick={() => handlers.play()} disabled={s.playing} active={s.playing}>
|
|
44
|
-
<Play />
|
|
45
|
-
</TBtn>
|
|
46
|
-
{!editing && (
|
|
47
|
-
<>
|
|
48
|
-
<TBtn label="Pause" onClick={() => handlers.pause()} disabled={!s.playing}>
|
|
49
|
-
<Pause />
|
|
50
|
-
</TBtn>
|
|
51
|
-
<TBtn label="Step one frame" onClick={() => handlers.step()} disabled={!paused}>
|
|
52
|
-
<StepForward />
|
|
53
|
-
</TBtn>
|
|
54
|
-
<TBtn label="Stop" onClick={() => handlers.stop()}>
|
|
55
|
-
<Square />
|
|
56
|
-
</TBtn>
|
|
57
|
-
</>
|
|
58
|
-
)}
|
|
59
|
-
|
|
60
|
-
{editing && (
|
|
61
|
-
<>
|
|
62
|
-
<div className={styles.sep} />
|
|
63
|
-
{/* Always live. The mode is a property of the editor, not of the selection: choosing
|
|
64
|
-
Rotate before picking what to rotate is the ordinary way round, and a mode you can
|
|
65
|
-
only set once something is selected makes the first click of every session fail. */}
|
|
66
|
-
<TBtn label="Move (G)" onClick={() => handlers.setMode('translate')} active={mode === 'translate'}>
|
|
67
|
-
<Move />
|
|
68
|
-
</TBtn>
|
|
69
|
-
<TBtn label="Rotate (R)" onClick={() => handlers.setMode('rotate')} active={mode === 'rotate'}>
|
|
70
|
-
<Rotate3d />
|
|
71
|
-
</TBtn>
|
|
72
|
-
<TBtn label="Scale (Y)" onClick={() => handlers.setMode('scale')} active={mode === 'scale'}>
|
|
73
|
-
<Scaling />
|
|
74
|
-
</TBtn>
|
|
75
|
-
<div className={styles.sep} />
|
|
76
|
-
<TBtn
|
|
77
|
-
label={space === 'world' ? 'Handles: World — switch to Local' : 'Handles: Local — switch to World'}
|
|
78
|
-
onClick={() => handlers.setSpace(space === 'world' ? 'local' : 'world')}
|
|
79
|
-
>
|
|
80
|
-
{space === 'world' ? <Globe /> : <Axis3d />}
|
|
81
|
-
</TBtn>
|
|
82
|
-
<TBtn
|
|
83
|
-
label={showUi ? 'Hide the scene’s UI' : 'Show the scene’s UI'}
|
|
84
|
-
onClick={() => handlers.setShowUi(!showUi)}
|
|
85
|
-
active={showUi}
|
|
86
|
-
>
|
|
87
|
-
<PanelTop />
|
|
88
|
-
</TBtn>
|
|
89
|
-
<div className={styles.sep} />
|
|
90
|
-
<TBtn label="Undo" onClick={() => handlers.undo()} disabled={!s.canUndo}>
|
|
91
|
-
<Undo2 />
|
|
92
|
-
</TBtn>
|
|
93
|
-
<TBtn label="Redo" onClick={() => handlers.redo()} disabled={!s.canRedo}>
|
|
94
|
-
<Redo2 />
|
|
95
|
-
</TBtn>
|
|
96
|
-
<div className={styles.sep} />
|
|
97
|
-
<TBtn
|
|
98
|
-
label="Save"
|
|
99
|
-
onClick={() => handlers.save()}
|
|
100
|
-
disabled={!s.dirty || s.saving}
|
|
101
|
-
className={cx(styles.save, s.dirty && styles.dirty, s.saving && styles.saving)}
|
|
102
|
-
>
|
|
103
|
-
<Save />
|
|
104
|
-
</TBtn>
|
|
105
|
-
</>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function TBtn({
|
|
112
|
-
label,
|
|
113
|
-
onClick,
|
|
114
|
-
disabled,
|
|
115
|
-
active,
|
|
116
|
-
className,
|
|
117
|
-
children,
|
|
118
|
-
}: {
|
|
119
|
-
label: string;
|
|
120
|
-
onClick: () => void;
|
|
121
|
-
disabled?: boolean;
|
|
122
|
-
active?: boolean;
|
|
123
|
-
className?: string;
|
|
124
|
-
children: ReactNode;
|
|
125
|
-
}): React.JSX.Element {
|
|
126
|
-
return (
|
|
127
|
-
<button
|
|
128
|
-
className={cx(styles.tbtn, active && styles.active, className)}
|
|
129
|
-
title={label}
|
|
130
|
-
aria-label={label}
|
|
131
|
-
onClick={onClick}
|
|
132
|
-
disabled={disabled}
|
|
133
|
-
>
|
|
134
|
-
{children}
|
|
135
|
-
</button>
|
|
136
|
-
);
|
|
137
|
-
}
|