@scenoco-three/editor 0.5.4 → 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 +25 -29
- 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/globals.d.ts +0 -9
- package/app/main.ts +0 -546
- package/app/meta.ts +0 -24
- package/app/picking.ts +0 -47
- package/app/session.ts +0 -612
- 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 -440
package/app/touchControls.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { Vector2 } from 'three';
|
|
2
|
-
import type { CameraRig } from './cameraRig.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Touch navigation, for editing a scene on a phone or tablet:
|
|
6
|
-
*
|
|
7
|
-
* - **one finger** drags the scene — the camera moves opposite the finger, so whatever is under
|
|
8
|
-
* it stays under it
|
|
9
|
-
* - **two fingers** turn the view by their average movement, and pinch to dolly in and out
|
|
10
|
-
*
|
|
11
|
-
* One finger pans rather than orbits because the other half of the editor's one-finger vocabulary
|
|
12
|
-
* is already spoken for: a tap selects, and a drag on a gizmo handle moves the thing you selected.
|
|
13
|
-
* Turning is the gesture you reach for least often while placing something, so it gets the
|
|
14
|
-
* two-finger slot.
|
|
15
|
-
*
|
|
16
|
-
* Every motion is scaled by `focusDistance`, which is Unity's trick: a drag covers the same
|
|
17
|
-
* *screen* distance whether you're an arm's length from a crate or across the level, so the
|
|
18
|
-
* controls feel the same at every zoom.
|
|
19
|
-
*
|
|
20
|
-
* It shares the camera with the desktop controls by construction — both only ever call `rig`
|
|
21
|
-
* verbs, and the rig is what writes the transform.
|
|
22
|
-
*/
|
|
23
|
-
export class TouchControls {
|
|
24
|
-
private readonly points = new Map<number, Vector2>();
|
|
25
|
-
private previousPinch = 0;
|
|
26
|
-
|
|
27
|
-
/** Per-pixel rates, multiplied by the focus distance (except look, which is angular). */
|
|
28
|
-
private readonly pan = 0.0016;
|
|
29
|
-
private readonly dolly = 0.005;
|
|
30
|
-
private readonly look = 0.005; // radians per pixel
|
|
31
|
-
|
|
32
|
-
private detach: Array<() => void> = [];
|
|
33
|
-
|
|
34
|
-
private readonly rig: CameraRig;
|
|
35
|
-
/** True while a gizmo handle owns the gesture — the camera stays out of it. */
|
|
36
|
-
private readonly isBlocked: () => boolean;
|
|
37
|
-
|
|
38
|
-
constructor(rig: CameraRig, canvas: HTMLElement, isBlocked: () => boolean = () => false) {
|
|
39
|
-
this.rig = rig;
|
|
40
|
-
this.isBlocked = isBlocked;
|
|
41
|
-
const on = (type: string, handler: (event: TouchEvent) => void): void => {
|
|
42
|
-
const listener = handler as EventListener;
|
|
43
|
-
// Not passive: these gestures have to beat the browser's own pan/zoom, and it only
|
|
44
|
-
// honours `preventDefault` from a listener that said up front it might call it.
|
|
45
|
-
canvas.addEventListener(type, listener, { passive: false });
|
|
46
|
-
this.detach.push(() => canvas.removeEventListener(type, listener));
|
|
47
|
-
};
|
|
48
|
-
on('touchstart', this.onStart);
|
|
49
|
-
on('touchmove', this.onMove);
|
|
50
|
-
on('touchend', this.onEnd);
|
|
51
|
-
on('touchcancel', this.onEnd);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private onStart = (event: TouchEvent): void => {
|
|
55
|
-
for (const touch of Array.from(event.changedTouches)) {
|
|
56
|
-
this.points.set(touch.identifier, new Vector2(touch.clientX, touch.clientY));
|
|
57
|
-
}
|
|
58
|
-
this.previousPinch = this.pinchDistance();
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
private onMove = (event: TouchEvent): void => {
|
|
62
|
-
// A finger dragging a gizmo handle emits touch events too — that gesture belongs to the
|
|
63
|
-
// gizmo, and the camera must hold still under it or the drag fights the view.
|
|
64
|
-
if (this.points.size === 1 && this.isBlocked()) return;
|
|
65
|
-
event.preventDefault();
|
|
66
|
-
const scale = this.rig.focusDistance;
|
|
67
|
-
|
|
68
|
-
if (this.points.size === 1) {
|
|
69
|
-
for (const touch of Array.from(event.touches)) {
|
|
70
|
-
const previous = this.points.get(touch.identifier);
|
|
71
|
-
if (!previous) continue;
|
|
72
|
-
this.rig.move(
|
|
73
|
-
-(touch.clientX - previous.x) * this.pan * scale,
|
|
74
|
-
(touch.clientY - previous.y) * this.pan * scale,
|
|
75
|
-
0,
|
|
76
|
-
);
|
|
77
|
-
previous.set(touch.clientX, touch.clientY);
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let dx = 0;
|
|
83
|
-
let dy = 0;
|
|
84
|
-
let moved = 0;
|
|
85
|
-
for (const touch of Array.from(event.touches)) {
|
|
86
|
-
const previous = this.points.get(touch.identifier);
|
|
87
|
-
if (!previous) continue;
|
|
88
|
-
dx += touch.clientX - previous.x;
|
|
89
|
-
dy += touch.clientY - previous.y;
|
|
90
|
-
previous.set(touch.clientX, touch.clientY);
|
|
91
|
-
moved++;
|
|
92
|
-
}
|
|
93
|
-
// The *average* of the two fingers, so a pinch — where they travel opposite ways — cancels
|
|
94
|
-
// out and dollies without also swinging the view around.
|
|
95
|
-
if (moved > 0) this.rig.rotate((-dx / moved) * this.look, (-dy / moved) * this.look);
|
|
96
|
-
|
|
97
|
-
const pinch = this.pinchDistance();
|
|
98
|
-
if (this.previousPinch > 0) {
|
|
99
|
-
const forward = (pinch - this.previousPinch) * this.dolly * scale;
|
|
100
|
-
this.rig.move(0, 0, forward);
|
|
101
|
-
// Closer in means finer control, the same way the wheel tunes fly speed on desktop.
|
|
102
|
-
this.rig.focusDistance = Math.max(0.1, this.rig.focusDistance - forward);
|
|
103
|
-
}
|
|
104
|
-
this.previousPinch = pinch;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
private onEnd = (event: TouchEvent): void => {
|
|
108
|
-
for (const touch of Array.from(event.changedTouches)) this.points.delete(touch.identifier);
|
|
109
|
-
// Lifting one of two fingers must not read as a huge pinch on the next move.
|
|
110
|
-
this.previousPinch = this.pinchDistance();
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
private pinchDistance(): number {
|
|
114
|
-
const [a, b] = [...this.points.values()];
|
|
115
|
-
return a && b ? a.distanceTo(b) : 0;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
dispose(): void {
|
|
119
|
-
for (const off of this.detach) off();
|
|
120
|
-
this.detach = [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** True on a device that can be touched — which is what decides fat gizmo handles. */
|
|
125
|
-
export function isTouchDevice(): boolean {
|
|
126
|
-
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
127
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { X } from 'lucide-react';
|
|
3
|
-
import { getDeps, useUI } from './store.js';
|
|
4
|
-
import { cx } from './cx.js';
|
|
5
|
-
import styles from './ui.module.css';
|
|
6
|
-
|
|
7
|
-
/** One offerable tag with the registry description shown beside it. */
|
|
8
|
-
interface Candidate {
|
|
9
|
-
tag: string;
|
|
10
|
-
description: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The "add component" picker: a centred modal with a case-insensitive filter over name and
|
|
15
|
-
* description.
|
|
16
|
-
*
|
|
17
|
-
* What it offers is every component class the project declares, read from `ProjectModel` — which
|
|
18
|
-
* is read from the source. v1 asked a runtime registry that decorators populated, so a component
|
|
19
|
-
* only appeared here if it had been registered; a class is the registration now.
|
|
20
|
-
*
|
|
21
|
-
* There is no attachment half any more: a geometry and a material are assets a mesh points at,
|
|
22
|
-
* so they are attributes rather than members to add.
|
|
23
|
-
*/
|
|
24
|
-
export function AddMemberPopup(): React.JSX.Element | null {
|
|
25
|
-
const adding = useUI((s) => s.adding);
|
|
26
|
-
const closeAdd = useUI((s) => s.closeAdd);
|
|
27
|
-
useUI((s) => s.rev);
|
|
28
|
-
const [query, setQuery] = useState('');
|
|
29
|
-
const input = useRef<HTMLInputElement>(null);
|
|
30
|
-
|
|
31
|
-
// Reopening for a different target starts from an empty filter.
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (adding) {
|
|
34
|
-
setQuery('');
|
|
35
|
-
input.current?.focus();
|
|
36
|
-
}
|
|
37
|
-
}, [adding]);
|
|
38
|
-
|
|
39
|
-
const candidates = useMemo(() => (adding ? offerings() : []), [adding]);
|
|
40
|
-
if (!adding) return null;
|
|
41
|
-
|
|
42
|
-
const needle = query.trim().toLowerCase();
|
|
43
|
-
const matches = needle
|
|
44
|
-
? candidates.filter((c) => c.tag.toLowerCase().includes(needle) || c.description.toLowerCase().includes(needle))
|
|
45
|
-
: candidates;
|
|
46
|
-
|
|
47
|
-
const { getDoc } = getDeps();
|
|
48
|
-
const add = (tag: string): void => {
|
|
49
|
-
getDoc()?.addComponent(adding, tag);
|
|
50
|
-
closeAdd();
|
|
51
|
-
useUI.getState().bump(); // re-read the inspector against the new member
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const label = 'Add component';
|
|
55
|
-
return (
|
|
56
|
-
// Clicking the backdrop dismisses; clicks inside must not bubble to it.
|
|
57
|
-
<div className={styles.modalBackdrop} onClick={closeAdd}>
|
|
58
|
-
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
|
59
|
-
<div className={styles.modalHead}>
|
|
60
|
-
<span className={styles.title}>{label}</span>
|
|
61
|
-
<button type="button" className={styles.clearBtn} title="Close" aria-label="Close" onClick={closeAdd}>
|
|
62
|
-
<X />
|
|
63
|
-
</button>
|
|
64
|
-
</div>
|
|
65
|
-
<input
|
|
66
|
-
ref={input}
|
|
67
|
-
type="text"
|
|
68
|
-
className={cx(styles.input, styles.modalFilter)}
|
|
69
|
-
placeholder="Filter…"
|
|
70
|
-
value={query}
|
|
71
|
-
autoFocus
|
|
72
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
73
|
-
onKeyDown={(e) => {
|
|
74
|
-
if (e.key === 'Escape') closeAdd();
|
|
75
|
-
// Enter commits the only remaining match — the fast path once you've typed.
|
|
76
|
-
if (e.key === 'Enter' && matches.length === 1) add(matches[0]!.tag);
|
|
77
|
-
}}
|
|
78
|
-
/>
|
|
79
|
-
<div className={styles.modalList}>
|
|
80
|
-
{matches.length === 0 ? (
|
|
81
|
-
<div className={styles.empty}>
|
|
82
|
-
{candidates.length === 0
|
|
83
|
-
? 'This project declares no components yet.'
|
|
84
|
-
: 'Nothing matches that filter.'}
|
|
85
|
-
</div>
|
|
86
|
-
) : (
|
|
87
|
-
matches.map((c) => (
|
|
88
|
-
<button key={c.tag} type="button" className={styles.modalItem} onClick={() => add(c.tag)}>
|
|
89
|
-
<span className={styles.modalItemTag}>{c.tag}</span>
|
|
90
|
-
<span className={styles.modalItemDesc}>{c.description}</span>
|
|
91
|
-
</button>
|
|
92
|
-
))
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** What this node may still receive, straight from the registry. */
|
|
101
|
-
/**
|
|
102
|
-
* Every component the project declares.
|
|
103
|
-
*
|
|
104
|
-
* A component with a `requiresNode` — `class Flicker extends Component<PointLight>` — is only
|
|
105
|
-
* offered on a node it can actually work on. Almost none declare one: like a MonoBehaviour on a
|
|
106
|
-
* GameObject, a component goes on anything, and the constraint exists for the rare one that
|
|
107
|
-
* reads a host-specific member.
|
|
108
|
-
*/
|
|
109
|
-
function offerings(): Candidate[] {
|
|
110
|
-
const model = getDeps().getModel();
|
|
111
|
-
return (model?.components ?? [])
|
|
112
|
-
.map((c) => ({ tag: c.name, description: c.description ?? '' }))
|
|
113
|
-
.sort((a, b) => a.tag.localeCompare(b.tag));
|
|
114
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { ListTree } from 'lucide-react';
|
|
2
|
-
import { useUI } from './store.js';
|
|
3
|
-
import styles from './ui.module.css';
|
|
4
|
-
|
|
5
|
-
/** The small bottom-right button that opens the node list. Hidden while it's open. */
|
|
6
|
-
export function BottomLauncher(): React.JSX.Element | null {
|
|
7
|
-
const nodesOpen = useUI((s) => s.nodesOpen);
|
|
8
|
-
const openNodes = useUI((s) => s.openNodes);
|
|
9
|
-
if (nodesOpen) return null;
|
|
10
|
-
return (
|
|
11
|
-
<button className={styles.launcher} title="Node list" aria-label="Node list" onClick={openNodes}>
|
|
12
|
-
<ListTree />
|
|
13
|
-
</button>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { TopBar } from './TopBar.js';
|
|
2
|
-
import { BottomLauncher } from './BottomLauncher.js';
|
|
3
|
-
import { NodeListSheet } from './NodeListSheet.js';
|
|
4
|
-
import { InspectorSheet } from './InspectorSheet.js';
|
|
5
|
-
import { AddMemberPopup } from './AddMemberPopup.js';
|
|
6
|
-
import { FilePicker } from './FilePicker.js';
|
|
7
|
-
|
|
8
|
-
/** The whole editor chrome: a responsive top bar, a bottom launcher, the two bottom sheets
|
|
9
|
-
* (node list + inspector), and the open dialog that stands in for all of it until a document
|
|
10
|
-
* is open. */
|
|
11
|
-
export function EditorUI(): React.JSX.Element {
|
|
12
|
-
return (
|
|
13
|
-
<>
|
|
14
|
-
<TopBar />
|
|
15
|
-
<BottomLauncher />
|
|
16
|
-
<NodeListSheet />
|
|
17
|
-
<InspectorSheet />
|
|
18
|
-
<AddMemberPopup />
|
|
19
|
-
<FilePicker />
|
|
20
|
-
</>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { getDeps, useUI } from './store.js';
|
|
3
|
-
import styles from './ui.module.css';
|
|
4
|
-
|
|
5
|
-
/** A document the picker offers — the scenes and prefabs the server found in the project. */
|
|
6
|
-
interface Pickable {
|
|
7
|
-
kind: string;
|
|
8
|
-
/** Project-relative path, which is what `open` takes. */
|
|
9
|
-
path: string;
|
|
10
|
-
/** File name without its kind suffix — `main` from `main.scene.xml`. */
|
|
11
|
-
stem: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Only these two are a thing to *open*: the rest are referenced by path from inside one. */
|
|
15
|
-
const OPENABLE = new Set(['scene', 'prefab']);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The start-up open dialog, shown whenever no document is open — which is what a bare
|
|
19
|
-
* `scenoco-editor` means.
|
|
20
|
-
*
|
|
21
|
-
* Opening the first scene it happened to find instead would be a guess, and a wrong one in any
|
|
22
|
-
* project with more than one scene: the editor would come up on something the person has to
|
|
23
|
-
* navigate away from, with no way to see what else there is.
|
|
24
|
-
*/
|
|
25
|
-
export function FilePicker(): React.JSX.Element | null {
|
|
26
|
-
useUI((s) => s.rev); // an open (or a failed one) changes whether this shows at all
|
|
27
|
-
const { getDoc, listDocuments, openDocument } = getDeps();
|
|
28
|
-
const [files, setFiles] = useState<Pickable[] | null>(null);
|
|
29
|
-
const [failed, setFailed] = useState<string | null>(null);
|
|
30
|
-
const open = getDoc() !== undefined;
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (open) return;
|
|
34
|
-
let live = true;
|
|
35
|
-
void listDocuments().then(
|
|
36
|
-
(all) => live && setFiles(all.filter((f) => OPENABLE.has(f.kind))),
|
|
37
|
-
() => live && setFiles([]),
|
|
38
|
-
);
|
|
39
|
-
return () => {
|
|
40
|
-
live = false;
|
|
41
|
-
};
|
|
42
|
-
}, [open, listDocuments]);
|
|
43
|
-
|
|
44
|
-
if (open) return null;
|
|
45
|
-
|
|
46
|
-
const pick = (file: Pickable): void => {
|
|
47
|
-
setFailed(null);
|
|
48
|
-
void openDocument(file.path).catch((error: unknown) =>
|
|
49
|
-
setFailed(error instanceof Error ? error.message : String(error)),
|
|
50
|
-
);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className={styles.modalBackdrop}>
|
|
55
|
-
<div className={styles.modal}>
|
|
56
|
-
<div className={styles.modalHead}>
|
|
57
|
-
<span className={styles.title}>Open a document</span>
|
|
58
|
-
</div>
|
|
59
|
-
<div className={styles.modalList}>
|
|
60
|
-
{files === null ? (
|
|
61
|
-
<div className={styles.empty}>Scanning the project…</div>
|
|
62
|
-
) : files.length === 0 ? (
|
|
63
|
-
<div className={styles.empty}>No .scene.xml or .prefab.xml documents in this project.</div>
|
|
64
|
-
) : (
|
|
65
|
-
files.map((file) => (
|
|
66
|
-
<button key={file.path} type="button" className={styles.modalItem} onClick={() => pick(file)}>
|
|
67
|
-
<span className={styles.modalItemTag}>
|
|
68
|
-
{file.stem}
|
|
69
|
-
<span className={styles.pickKind}>{file.kind}</span>
|
|
70
|
-
</span>
|
|
71
|
-
<span className={styles.modalItemDesc}>{file.path}</span>
|
|
72
|
-
</button>
|
|
73
|
-
))
|
|
74
|
-
)}
|
|
75
|
-
{failed !== null && <div className={styles.empty}>{failed}</div>}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
package/app/ui-react/HeadBtn.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import styles from './ui.module.css';
|
|
3
|
-
|
|
4
|
-
/** An icon button in a sheet header (actions row / close). */
|
|
5
|
-
export function HeadBtn({
|
|
6
|
-
label,
|
|
7
|
-
onClick,
|
|
8
|
-
disabled,
|
|
9
|
-
children,
|
|
10
|
-
}: {
|
|
11
|
-
label: string;
|
|
12
|
-
onClick: () => void;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
children: ReactNode;
|
|
15
|
-
}): React.JSX.Element {
|
|
16
|
-
return (
|
|
17
|
-
<button className={styles.headBtn} title={label} aria-label={label} onClick={onClick} disabled={disabled}>
|
|
18
|
-
{children}
|
|
19
|
-
</button>
|
|
20
|
-
);
|
|
21
|
-
}
|