@volter/editor-blender 0.1.151 → 0.1.153
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/contributions/blender-model-opening.tsx +25 -51
- package/contributions/blender-node-editor.document.tsx +2 -1
- package/contributions/blender-play-area.tsx +3 -4
- package/contributions/blender-properties-model.ts +44 -1
- package/contributions/blender-properties-view.tsx +24 -34
- package/contributions/blender-runtime.document.tsx +71 -389
- package/contributions/blender-timeline.document.tsx +6 -4
- package/contributions/blender-version.status.tsx +2 -8
- package/contributions/blender.page.ts +12 -11
- package/contributions/blender.status.tsx +1 -7
- package/contributions/models.finder.ts +4 -11
- package/dist-node/serving.mjs +0 -2
- package/host/blender-runtime-host.ts +290 -76
- package/node/blender-mcp.ts +3 -2
- package/package.json +56 -44
- package/serving/blender-routes.ts +0 -5
- package/src/api.ts +2 -2
- package/src/layouts.tsx +2 -2
- package/src/play-mode.ts +10 -65
- package/src/play-tool.ts +44 -0
- package/src/model-play-extension.ts +0 -173
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publisher": "volter",
|
|
4
4
|
"author": "Volter AI, Inc.",
|
|
5
5
|
"license": "AGPL-3.0-only AND GPL-3.0-or-later",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.153",
|
|
7
7
|
"engines": {
|
|
8
8
|
"vscode": "*"
|
|
9
9
|
},
|
|
@@ -95,8 +95,20 @@
|
|
|
95
95
|
],
|
|
96
96
|
"views": {
|
|
97
97
|
"statusbar": [
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
{
|
|
99
|
+
"id": "version",
|
|
100
|
+
"name": "Version",
|
|
101
|
+
"alignment": "right",
|
|
102
|
+
"priority": -5998,
|
|
103
|
+
"module": "./contributions/blender-version.status.tsx"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "mouseHints",
|
|
107
|
+
"name": "Mouse hints",
|
|
108
|
+
"alignment": "left",
|
|
109
|
+
"priority": 5000,
|
|
110
|
+
"module": "./contributions/blender.status.tsx"
|
|
111
|
+
}
|
|
100
112
|
]
|
|
101
113
|
},
|
|
102
114
|
"finders": [
|
|
@@ -535,179 +547,179 @@
|
|
|
535
547
|
{
|
|
536
548
|
"command": "volter.edit.duplicate",
|
|
537
549
|
"key": "shift+D",
|
|
538
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
550
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
539
551
|
},
|
|
540
552
|
{
|
|
541
553
|
"command": "volter.edit.selectAll",
|
|
542
554
|
"key": "A",
|
|
543
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
555
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
544
556
|
},
|
|
545
557
|
{
|
|
546
558
|
"command": "volter.edit.deselectAll",
|
|
547
559
|
"key": "alt+A",
|
|
548
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
560
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
549
561
|
},
|
|
550
562
|
{
|
|
551
563
|
"command": "volter.edit.delete",
|
|
552
564
|
"key": "X",
|
|
553
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
565
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
554
566
|
},
|
|
555
567
|
{
|
|
556
568
|
"command": "volter.transform.select",
|
|
557
569
|
"key": "W",
|
|
558
|
-
"when": "volter.stage.focused && volter.
|
|
570
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
559
571
|
},
|
|
560
572
|
{
|
|
561
573
|
"command": "volter.transform.translate",
|
|
562
574
|
"key": "G",
|
|
563
|
-
"when": "volter.stage.focused && volter.
|
|
575
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
564
576
|
},
|
|
565
577
|
{
|
|
566
578
|
"command": "volter.transform.rotate",
|
|
567
579
|
"key": "R",
|
|
568
|
-
"when": "volter.stage.focused && volter.
|
|
580
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
569
581
|
},
|
|
570
582
|
{
|
|
571
583
|
"command": "volter.transform.scale",
|
|
572
584
|
"key": "S",
|
|
573
|
-
"when": "volter.stage.focused && volter.
|
|
585
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
574
586
|
},
|
|
575
587
|
{
|
|
576
588
|
"command": "volter.viewport.frameSelection",
|
|
577
589
|
"key": "NumPad_Decimal",
|
|
578
|
-
"when": "volter.stage.focused && volter.
|
|
590
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
579
591
|
},
|
|
580
592
|
{
|
|
581
593
|
"command": "volter.viewport.frameSelection",
|
|
582
594
|
"key": ".",
|
|
583
|
-
"when": "volter.stage.focused && volter.
|
|
595
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
584
596
|
},
|
|
585
597
|
{
|
|
586
598
|
"command": "volter.viewport.cyclePivot",
|
|
587
599
|
"key": ",",
|
|
588
|
-
"when": "volter.stage.focused && volter.
|
|
600
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
589
601
|
},
|
|
590
602
|
{
|
|
591
603
|
"command": "volter.view.bottom",
|
|
592
604
|
"key": "ctrl+NumPad7",
|
|
593
|
-
"when": "volter.stage.focused && volter.
|
|
605
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
594
606
|
},
|
|
595
607
|
{
|
|
596
608
|
"command": "volter.view.back",
|
|
597
609
|
"key": "ctrl+NumPad1",
|
|
598
|
-
"when": "volter.stage.focused && volter.
|
|
610
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
599
611
|
},
|
|
600
612
|
{
|
|
601
613
|
"command": "volter.view.left",
|
|
602
614
|
"key": "ctrl+NumPad3",
|
|
603
|
-
"when": "volter.stage.focused && volter.
|
|
615
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
604
616
|
},
|
|
605
617
|
{
|
|
606
618
|
"command": "volter.view.orbitLeft",
|
|
607
619
|
"key": "NumPad4",
|
|
608
|
-
"when": "volter.stage.focused && volter.
|
|
620
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
609
621
|
},
|
|
610
622
|
{
|
|
611
623
|
"command": "volter.view.orbitRight",
|
|
612
624
|
"key": "NumPad6",
|
|
613
|
-
"when": "volter.stage.focused && volter.
|
|
625
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
614
626
|
},
|
|
615
627
|
{
|
|
616
628
|
"command": "volter.view.orbitUp",
|
|
617
629
|
"key": "NumPad8",
|
|
618
|
-
"when": "volter.stage.focused && volter.
|
|
630
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
619
631
|
},
|
|
620
632
|
{
|
|
621
633
|
"command": "volter.view.orbitDown",
|
|
622
634
|
"key": "NumPad2",
|
|
623
|
-
"when": "volter.stage.focused && volter.
|
|
635
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
624
636
|
},
|
|
625
637
|
{
|
|
626
638
|
"command": "volter.view.opposite",
|
|
627
639
|
"key": "NumPad9",
|
|
628
|
-
"when": "volter.stage.focused && volter.
|
|
640
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
629
641
|
},
|
|
630
642
|
{
|
|
631
643
|
"command": "volter.view.rollLeft",
|
|
632
644
|
"key": "shift+NumPad4",
|
|
633
|
-
"when": "volter.stage.focused && volter.
|
|
645
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
634
646
|
},
|
|
635
647
|
{
|
|
636
648
|
"command": "volter.view.rollRight",
|
|
637
649
|
"key": "shift+NumPad6",
|
|
638
|
-
"when": "volter.stage.focused && volter.
|
|
650
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
639
651
|
},
|
|
640
652
|
{
|
|
641
653
|
"command": "volter.view.all",
|
|
642
654
|
"key": "Home",
|
|
643
|
-
"when": "volter.stage.focused && volter.
|
|
655
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
644
656
|
},
|
|
645
657
|
{
|
|
646
658
|
"command": "volter.view.zoomIn",
|
|
647
659
|
"key": "ctrl+=",
|
|
648
|
-
"when": "volter.stage.focused && volter.
|
|
660
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
649
661
|
},
|
|
650
662
|
{
|
|
651
663
|
"command": "volter.view.zoomIn",
|
|
652
664
|
"key": "NumPad_Add",
|
|
653
|
-
"when": "volter.stage.focused && volter.
|
|
665
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
654
666
|
},
|
|
655
667
|
{
|
|
656
668
|
"command": "volter.view.zoomOut",
|
|
657
669
|
"key": "ctrl+-",
|
|
658
|
-
"when": "volter.stage.focused && volter.
|
|
670
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
659
671
|
},
|
|
660
672
|
{
|
|
661
673
|
"command": "volter.view.zoomOut",
|
|
662
674
|
"key": "NumPad_Subtract",
|
|
663
|
-
"when": "volter.stage.focused && volter.
|
|
675
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
664
676
|
},
|
|
665
677
|
{
|
|
666
678
|
"command": "volter.view.projection",
|
|
667
679
|
"key": "NumPad5",
|
|
668
|
-
"when": "volter.stage.focused && volter.
|
|
680
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
669
681
|
},
|
|
670
682
|
{
|
|
671
683
|
"command": "volter.unboundKey",
|
|
672
684
|
"key": "ctrl+D",
|
|
673
685
|
"mac": "cmd+D",
|
|
674
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
686
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
675
687
|
},
|
|
676
688
|
{
|
|
677
689
|
"command": "volter.edit.delete",
|
|
678
690
|
"key": "Delete",
|
|
679
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
691
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
680
692
|
},
|
|
681
693
|
{
|
|
682
694
|
"command": "volter.unboundKey",
|
|
683
695
|
"key": "Backspace",
|
|
684
|
-
"when": "volter.focused && !inputFocus && volter.
|
|
696
|
+
"when": "volter.focused && !inputFocus && volter.keymap == 'blender'"
|
|
685
697
|
},
|
|
686
698
|
{
|
|
687
699
|
"command": "volter.unboundKey",
|
|
688
700
|
"key": "Q",
|
|
689
|
-
"when": "volter.stage.focused && volter.
|
|
701
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
690
702
|
},
|
|
691
703
|
{
|
|
692
704
|
"command": "volter.unboundKey",
|
|
693
705
|
"key": "E",
|
|
694
|
-
"when": "volter.stage.focused && volter.
|
|
706
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
695
707
|
},
|
|
696
708
|
{
|
|
697
709
|
"command": "volter.unboundKey",
|
|
698
710
|
"key": "F",
|
|
699
|
-
"when": "volter.stage.focused && volter.
|
|
711
|
+
"when": "volter.stage.focused && volter.keymap == 'blender'"
|
|
700
712
|
},
|
|
701
713
|
{
|
|
702
714
|
"command": "volter.unboundKey",
|
|
703
715
|
"key": "M",
|
|
704
|
-
"when": "volter.stage.focused && volter.stage.
|
|
716
|
+
"when": "volter.stage.focused && volter.stage.surface == 'canvas' && volter.keymap == 'blender'"
|
|
705
717
|
},
|
|
706
718
|
{
|
|
707
719
|
"command": "volter.unboundKey",
|
|
708
720
|
"key": "ctrl+'",
|
|
709
721
|
"mac": "cmd+'",
|
|
710
|
-
"when": "volter.stage.focused && volter.stage.
|
|
722
|
+
"when": "volter.stage.focused && volter.stage.surface == 'canvas' && volter.keymap == 'blender'"
|
|
711
723
|
}
|
|
712
724
|
],
|
|
713
725
|
"languageModelTools": [
|
|
@@ -1233,17 +1245,17 @@
|
|
|
1233
1245
|
}
|
|
1234
1246
|
},
|
|
1235
1247
|
"dependencies": {
|
|
1236
|
-
"@volter/editor-movie": "0.5.
|
|
1248
|
+
"@volter/editor-movie": "0.5.214",
|
|
1237
1249
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
1238
|
-
"@volter/blender-engine": "0.1.
|
|
1239
|
-
"@volter/editor-threejs": "0.5.
|
|
1250
|
+
"@volter/blender-engine": "0.1.151",
|
|
1251
|
+
"@volter/editor-threejs": "0.5.214",
|
|
1240
1252
|
"zod": "^4.3.6",
|
|
1241
1253
|
"@fortawesome/free-solid-svg-icons": "^7.2.0"
|
|
1242
1254
|
},
|
|
1243
1255
|
"peerDependencies": {
|
|
1244
|
-
"@volter/live": "0.5.
|
|
1245
|
-
"@volter/project": "0.5.
|
|
1246
|
-
"@volter/sdk": "0.5.
|
|
1256
|
+
"@volter/live": "0.5.214",
|
|
1257
|
+
"@volter/project": "0.5.214",
|
|
1258
|
+
"@volter/sdk": "0.5.214",
|
|
1247
1259
|
"react": "^19.0.0",
|
|
1248
1260
|
"react-dom": "^19.0.0",
|
|
1249
1261
|
"three": "^0.186.1",
|
|
@@ -90,7 +90,6 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
90
90
|
method: 'GET',
|
|
91
91
|
match: /^\/__editor\/blender-wasm\/status$/,
|
|
92
92
|
handle: async (_req, res) => {
|
|
93
|
-
services.allowCrossOriginFrameEmbedding(res);
|
|
94
93
|
json(res, await blenderWasmStatus());
|
|
95
94
|
},
|
|
96
95
|
},
|
|
@@ -169,10 +168,6 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
169
168
|
match: /^\/__editor\/blender-wasm\/([^/]+)$/,
|
|
170
169
|
handle: async (req, res, url) => {
|
|
171
170
|
const file = decodeURIComponent(url.pathname.slice('/__editor/blender-wasm/'.length));
|
|
172
|
-
// The frame's page is cross-origin-isolated and this session is not its origin, so every
|
|
173
|
-
// byte of the engine has to say it may be embedded — the glue above all, which Blender's
|
|
174
|
-
// pthreads load with `importScripts`.
|
|
175
|
-
services.allowCrossOriginFrameEmbedding(res);
|
|
176
171
|
const bundled = (BLENDER_WASM_FILES as readonly string[]).includes(file);
|
|
177
172
|
if (!bundled && file !== BLENDER_WALI_ARTIFACT) {
|
|
178
173
|
json(res, { error: `The Blender build has no ${file}.` }, 404);
|
package/src/api.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EDITOR-BLENDER'S API, AS TYPES: what its page half's `activate` returns
|
|
3
|
-
* (`../contributions/blender.page.ts`), the
|
|
3
|
+
* (`../contributions/blender.page.ts`), the Play tool seam's type and a landed save's. A package that reads the
|
|
4
4
|
* API imports these type-only; the values come from `getExtension(...).exports`.
|
|
5
5
|
*/
|
|
6
6
|
export type BlenderPageApi = ReturnType<typeof import('../contributions/blender.page').activate>;
|
|
7
|
-
export type
|
|
7
|
+
export type { PlayTool } from './play-tool';
|
|
8
8
|
export type { ModelPlayMode } from './play-mode';
|
|
9
9
|
export type { ModelDocumentSaved } from '../host/blender-runtime-host';
|
package/src/layouts.tsx
CHANGED
|
@@ -19,12 +19,12 @@ export const ModelArrangement: WorkspaceArrangement = {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
/** Blender-shaped modeling
|
|
22
|
+
/** Blender-shaped modeling. */
|
|
23
23
|
export function ModelLayout() {
|
|
24
24
|
return (
|
|
25
25
|
<EditorFrame>
|
|
26
26
|
<EditorHeader />
|
|
27
|
-
<Workspace arrangement={ModelArrangement}
|
|
27
|
+
<Workspace arrangement={ModelArrangement} />
|
|
28
28
|
<EditorFooter />
|
|
29
29
|
</EditorFrame>
|
|
30
30
|
);
|
package/src/play-mode.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** A model's bottom area shows Blender's animation editors or the Play tool's Game panel; switching leaves a running game running. Remember the person's mode per model for this browser session; otherwise a model opens in Game where the project has a game (`PlayTool.hasGame`). The model on screen announces itself, so focusing the bottom area does not change which model it serves. */
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from './
|
|
3
|
+
playTool,
|
|
4
|
+
subscribePlayTool,
|
|
5
|
+
} from './play-tool';
|
|
6
6
|
|
|
7
7
|
export type ModelPlayMode = 'game' | 'animation';
|
|
8
8
|
|
|
@@ -13,8 +13,6 @@ export function playModeOf(value: unknown): ModelPlayMode | null {
|
|
|
13
13
|
|
|
14
14
|
const STORAGE_PREFIX = 'volter.blender.play-mode:';
|
|
15
15
|
const listeners = new Set<() => void>();
|
|
16
|
-
/** Verbs waiting for a document to be on screen (`documentOnScreen`), told at every change. */
|
|
17
|
-
const waiters = new Set<() => void>();
|
|
18
16
|
/** The person's pick per document; `null` once storage was read and held none. */
|
|
19
17
|
const chosen = new Map<string, ModelPlayMode | null>();
|
|
20
18
|
/** Each model document's `.blend`, as the document reported it. */
|
|
@@ -25,7 +23,6 @@ let version = 0;
|
|
|
25
23
|
function publish(): void {
|
|
26
24
|
version += 1;
|
|
27
25
|
for (const listener of [...listeners]) listener();
|
|
28
|
-
for (const waiter of [...waiters]) waiter();
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
function stored(documentId: string): ModelPlayMode | null {
|
|
@@ -38,7 +35,7 @@ function stored(documentId: string): ModelPlayMode | null {
|
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/** The person's own pick for a document, or `null` when the default stands. */
|
|
41
|
-
|
|
38
|
+
function chosenPlayMode(documentId: string): ModelPlayMode | null {
|
|
42
39
|
let value = chosen.get(documentId);
|
|
43
40
|
if (value === undefined) {
|
|
44
41
|
value = stored(documentId);
|
|
@@ -81,57 +78,6 @@ export function servedModelDocument(): string | null {
|
|
|
81
78
|
return served;
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
/** Model documents on screen whose file is still opening (read and first presented), the newest last. */
|
|
85
|
-
const opening: string[] = [];
|
|
86
|
-
|
|
87
|
-
/** An opening Model document does not serve the bottom area until presentation completes. Return its withdrawal function; documentOnScreen waits for it rather than reporting no model. */
|
|
88
|
-
export function noteModelOpening(documentId: string): () => void {
|
|
89
|
-
opening.push(documentId);
|
|
90
|
-
publish();
|
|
91
|
-
return () => {
|
|
92
|
-
const at = opening.lastIndexOf(documentId);
|
|
93
|
-
if (at < 0) return;
|
|
94
|
-
opening.splice(at, 1);
|
|
95
|
-
publish();
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The model document still opening on screen, or `null` with none. */
|
|
100
|
-
export function openingModelDocument(): string | null {
|
|
101
|
-
return opening.at(-1) ?? null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* THE MODEL DOCUMENT ON SCREEN, once it is: the one served now, else the one opening, waited for at most `waitMs`.
|
|
106
|
-
* Answers `{ served }` once one serves the bottom area, `{ opening }` when one is still opening at the end of the wait
|
|
107
|
-
* (the caller asks again: an open can take minutes), and `null` when none is on screen or opening.
|
|
108
|
-
*/
|
|
109
|
-
export function documentOnScreen(waitMs: number): Promise<{ readonly served: string } | { readonly opening: string } | null> {
|
|
110
|
-
const now = (): { readonly served: string } | { readonly opening: string } | null =>
|
|
111
|
-
served !== null ? { served } : opening.length > 0 ? { opening: opening.at(-1)! } : null;
|
|
112
|
-
const first = now();
|
|
113
|
-
if (first === null || 'served' in first || waitMs <= 0) return Promise.resolve(first);
|
|
114
|
-
return new Promise((resolve) => {
|
|
115
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
-
const settle = (answer: ReturnType<typeof now>): void => {
|
|
117
|
-
waiters.delete(check);
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
clearTimeout(recheck);
|
|
120
|
-
resolve(answer);
|
|
121
|
-
};
|
|
122
|
-
let recheck: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
-
function check(): void {
|
|
124
|
-
const answer = now();
|
|
125
|
-
if (answer !== null && 'served' in answer) { clearTimeout(recheck); settle(answer); return; }
|
|
126
|
-
// (an opening withdrawn is read again a task later: the document's own commit withdraws it before its
|
|
127
|
-
// viewport announces itself, both in one commit, and only an open that ended with nothing on screen is none)
|
|
128
|
-
if (answer === null) { clearTimeout(recheck); recheck = setTimeout(() => { const later = now(); if (later === null || 'served' in later) settle(later); }, 0); }
|
|
129
|
-
}
|
|
130
|
-
waiters.add(check);
|
|
131
|
-
timer = setTimeout(() => settle(now()), waitMs);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
81
|
/** The `.blend` a model document reported, if it has a file of its own. */
|
|
136
82
|
export function modelDocumentSource(documentId: string): string | undefined {
|
|
137
83
|
return sources.get(documentId);
|
|
@@ -139,17 +85,16 @@ export function modelDocumentSource(documentId: string): string | undefined {
|
|
|
139
85
|
|
|
140
86
|
/**
|
|
141
87
|
* THE MODE A DOCUMENT IS IN: Animation whenever no Play tool is installed (there is no game to
|
|
142
|
-
* switch to), else the person's pick, else Game exactly when the
|
|
88
|
+
* switch to), else the person's pick, else Game exactly when the project has a game. A
|
|
143
89
|
* game not yet looked for counts as none, so a document opens on the Timeline and turns to
|
|
144
90
|
* the Game panel when the answer arrives.
|
|
145
91
|
*/
|
|
146
92
|
export function modelPlayMode(documentId: string): ModelPlayMode {
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
93
|
+
const tool = playTool();
|
|
94
|
+
if (tool === null) return 'animation';
|
|
149
95
|
const pick = chosenPlayMode(documentId);
|
|
150
96
|
if (pick !== null) return pick;
|
|
151
|
-
|
|
152
|
-
return blend !== undefined && extension.hasScript?.(blend) === true ? 'game' : 'animation';
|
|
97
|
+
return sources.has(documentId) && tool.hasGame() === true ? 'game' : 'animation';
|
|
153
98
|
}
|
|
154
99
|
|
|
155
100
|
export function playModeVersion(): number {
|
|
@@ -169,7 +114,7 @@ export function playModeVersion(): number {
|
|
|
169
114
|
let stopToolWatch: (() => void) | null = null;
|
|
170
115
|
export function subscribePlayMode(listener: () => void): () => void {
|
|
171
116
|
listeners.add(listener);
|
|
172
|
-
stopToolWatch ??=
|
|
117
|
+
stopToolWatch ??= subscribePlayTool(publish);
|
|
173
118
|
return () => {
|
|
174
119
|
listeners.delete(listener);
|
|
175
120
|
if (listeners.size === 0 && stopToolWatch) {
|
package/src/play-tool.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAY, THE PROJECT'S, as editor-blender's bottom area shows it (`registerPlay` in its API, `docs/adr/plugin-apis.md`):
|
|
3
|
+
* a Play tool (play's) owns its run, its controls and its Game view; the bottom area shows the tool's panel under every
|
|
4
|
+
* document but a model in its Animation mode, and asks the tool whether the project has a game, by which a model opens
|
|
5
|
+
* in Game or Animation (`play-mode.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import type { ComponentType } from 'react';
|
|
8
|
+
|
|
9
|
+
export interface PlayTool {
|
|
10
|
+
/** The run's panel: Play, its transport and its log. */
|
|
11
|
+
readonly Panel: ComponentType;
|
|
12
|
+
/** Whether the project has a game to play: `null` while the tool has not looked yet. */
|
|
13
|
+
hasGame(): boolean | null;
|
|
14
|
+
/** Every change of what the tool answers. */
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let registered: PlayTool | null = null;
|
|
19
|
+
const listeners = new Set<() => void>();
|
|
20
|
+
function publish(): void { for (const listener of listeners) listener(); }
|
|
21
|
+
|
|
22
|
+
/** The registered Play tool, or `null` while none is. */
|
|
23
|
+
export function playTool(): PlayTool | null {
|
|
24
|
+
return registered;
|
|
25
|
+
}
|
|
26
|
+
/** A registration, and every change the registered tool announces. */
|
|
27
|
+
export function subscribePlayTool(listener: () => void): () => void {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => { listeners.delete(listener); };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The API's `registerPlay`: one Play tool at a time. */
|
|
33
|
+
export function registerPlay(tool: PlayTool): () => void {
|
|
34
|
+
if (registered) throw new Error('A Play tool is already registered.');
|
|
35
|
+
registered = tool;
|
|
36
|
+
const stop = tool.subscribe(publish);
|
|
37
|
+
publish();
|
|
38
|
+
return () => {
|
|
39
|
+
stop();
|
|
40
|
+
if (registered !== tool) return;
|
|
41
|
+
registered = null;
|
|
42
|
+
publish();
|
|
43
|
+
};
|
|
44
|
+
}
|