@volter/blender-engine 0.1.61 → 0.1.62
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/browser/pull-job.mts +3 -1
- package/browser/runtime.ts +25 -11
- package/browser/session.py +9 -2
- package/browser/worker.ts +17 -12
- package/package.json +1 -1
package/browser/pull-job.mts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** A cooperative job advances only when its owner asks for its next unit.
|
|
2
2
|
* Checkpoints park the producer; they do not renew a timer while work runs. */
|
|
3
3
|
export type PullResult<T> = { load: 'continue'; token: string; phase: string } | { load: 'done'; value: T };
|
|
4
|
+
let nextJob = 0;
|
|
4
5
|
export class PullJob<T> {
|
|
6
|
+
private readonly job = ++nextJob;
|
|
5
7
|
private started = false;
|
|
6
8
|
private finished = false;
|
|
7
9
|
private sequence = 0;
|
|
@@ -17,7 +19,7 @@ export class PullJob<T> {
|
|
|
17
19
|
this.started = true;
|
|
18
20
|
void Promise.resolve().then(() => this.work(phase => new Promise<void>(resolve => {
|
|
19
21
|
this.resume = resolve;
|
|
20
|
-
this.expected = `load:${++this.sequence}`;
|
|
22
|
+
this.expected = `load:${this.job}:${++this.sequence}`;
|
|
21
23
|
const waiter = this.waiter!; this.waiter = null;
|
|
22
24
|
waiter.resolve({ load: 'continue', token: this.expected, phase });
|
|
23
25
|
}))).then(value => {
|
package/browser/runtime.ts
CHANGED
|
@@ -159,7 +159,7 @@ export class BlenderRuntime {
|
|
|
159
159
|
#terminated = false;
|
|
160
160
|
#dirty = false;
|
|
161
161
|
readonly #beforeUnload = (event: BeforeUnloadEvent): void => {
|
|
162
|
-
if (!this.#dirty && this.#
|
|
162
|
+
if (!this.#dirty && this.#acceptedRequests === 0) return;
|
|
163
163
|
event.preventDefault();
|
|
164
164
|
event.returnValue = '';
|
|
165
165
|
};
|
|
@@ -259,7 +259,7 @@ export class BlenderRuntime {
|
|
|
259
259
|
);
|
|
260
260
|
this.#project = project;
|
|
261
261
|
this.#document ??= document ?? 'models/model.blend';
|
|
262
|
-
this.#started ??= this.#
|
|
262
|
+
this.#started ??= this.#request({
|
|
263
263
|
op: 'start',
|
|
264
264
|
project,
|
|
265
265
|
...(document === undefined ? {} : { document }),
|
|
@@ -267,15 +267,6 @@ export class BlenderRuntime {
|
|
|
267
267
|
return this.#started;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
async #load(request: Extract<Request, { op: 'start' }>): Promise<RuntimeStart> {
|
|
271
|
-
let result = await this.#request(request) as PullResult<RuntimeStart>;
|
|
272
|
-
while (result?.load === 'continue') {
|
|
273
|
-
this.#loadBoundary = result.phase;
|
|
274
|
-
result = await this.#request({ op: 'load-next', token: result.token }) as PullResult<RuntimeStart>;
|
|
275
|
-
}
|
|
276
|
-
return result?.load === 'done' ? result.value : result as unknown as RuntimeStart;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
270
|
#ready(): Promise<RuntimeStart> {
|
|
280
271
|
if (this.#project === null)
|
|
281
272
|
return Promise.reject(
|
|
@@ -566,9 +557,32 @@ export class BlenderRuntime {
|
|
|
566
557
|
this.#lastCallWindow = { start: started, end: started + elapsed };
|
|
567
558
|
}
|
|
568
559
|
|
|
560
|
+
#acceptedRequests = 0;
|
|
561
|
+
#requestTail: Promise<void> = Promise.resolve();
|
|
569
562
|
#request(request: Request, shutdown = false): Promise<unknown> {
|
|
570
563
|
if (this.#terminated || (this.#stopping && !shutdown))
|
|
571
564
|
return Promise.reject(new Error('The Blender session is stopping or terminated'));
|
|
565
|
+
const began = performance.now();
|
|
566
|
+
this.#acceptedRequests++;
|
|
567
|
+
const answer = this.#requestTail.then(async () => {
|
|
568
|
+
this.#loadBoundary = request.op;
|
|
569
|
+
let result = await this.#wireRequest(request) as PullResult<unknown>;
|
|
570
|
+
while (result?.load === 'continue') {
|
|
571
|
+
this.#loadBoundary = result.phase;
|
|
572
|
+
result = await this.#wireRequest({ op: 'load-next', token: result.token }) as PullResult<unknown>;
|
|
573
|
+
}
|
|
574
|
+
return result?.load === 'done' ? result.value : result;
|
|
575
|
+
}).finally(() => {
|
|
576
|
+
this.#acceptedRequests--;
|
|
577
|
+
const totalMs = Math.round(performance.now() - began);
|
|
578
|
+
try { if (totalMs >= 1000) this.#options.log?.('log', `@@VOLTER-WORK op=${request.op} totalMs=${totalMs}`); } catch { /* diagnostics cannot poison the queue */ }
|
|
579
|
+
});
|
|
580
|
+
this.#requestTail = answer.then(() => {}, () => {});
|
|
581
|
+
return answer;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
#wireRequest(request: Request): Promise<unknown> {
|
|
585
|
+
if (this.#terminated) return Promise.reject(new Error('The Blender session is terminated'));
|
|
572
586
|
const id = ++this.#nextId;
|
|
573
587
|
return new Promise((resolve, reject) => {
|
|
574
588
|
this.#pending.set(id, { resolve, reject });
|
package/browser/session.py
CHANGED
|
@@ -4421,8 +4421,11 @@ def rna_rig(object_name=None):
|
|
|
4421
4421
|
subjects = [obj for obj in bpy.context.view_layer.objects
|
|
4422
4422
|
if obj.type == "MESH" and obj.data is not None
|
|
4423
4423
|
and obj.find_armature() is not None]
|
|
4424
|
-
|
|
4425
|
-
|
|
4424
|
+
rigs = []
|
|
4425
|
+
for obj in subjects:
|
|
4426
|
+
_asked({"checkpoint": "rig-object"})
|
|
4427
|
+
rigs.append(_rig_of(obj))
|
|
4428
|
+
return {"frame": scene_frame, "named": object_name, "rigs": rigs}
|
|
4426
4429
|
|
|
4427
4430
|
|
|
4428
4431
|
def _rig_of(obj):
|
|
@@ -4506,7 +4509,11 @@ def _rig_of(obj):
|
|
|
4506
4509
|
truncated = 0
|
|
4507
4510
|
unweighted = 0
|
|
4508
4511
|
most = 0
|
|
4512
|
+
checkpoint_at = time.monotonic()
|
|
4509
4513
|
for i, vertex in enumerate(mesh.vertices):
|
|
4514
|
+
if i % 1024 == 0 and time.monotonic() - checkpoint_at >= 0.05:
|
|
4515
|
+
_asked({"checkpoint": "rig-weights"})
|
|
4516
|
+
checkpoint_at = time.monotonic()
|
|
4510
4517
|
pairs = []
|
|
4511
4518
|
for element in vertex.groups:
|
|
4512
4519
|
bone = group_to_bone.get(int(element.group))
|
package/browser/worker.ts
CHANGED
|
@@ -573,17 +573,22 @@ async function listSessionFiles(files: BlenderFiles, root: string): Promise<File
|
|
|
573
573
|
return out;
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
function pullWork(label: string, work: () => Promise<unknown>): Promise<unknown> {
|
|
577
|
+
if (loadCheckpoint) throw new Error('The current Blender operation must finish first');
|
|
578
|
+
loadJob = new PullJob(async checkpoint => {
|
|
579
|
+
loadCheckpoint = checkpoint;
|
|
580
|
+
const began = performance.now();
|
|
581
|
+
try { return await work(); }
|
|
582
|
+
finally { loadCheckpoint = null; log('log', `@@VOLTER-WORK op=${label} totalMs=${Math.round(performance.now() - began)}`); }
|
|
583
|
+
});
|
|
584
|
+
return loadJob.step();
|
|
585
|
+
}
|
|
586
|
+
|
|
576
587
|
async function handle(request: WorkerRequest): Promise<unknown> {
|
|
577
588
|
switch (request.op) {
|
|
578
589
|
case 'start': {
|
|
579
590
|
if (loadJob) throw new Error('The Blender load has already started');
|
|
580
|
-
|
|
581
|
-
loadCheckpoint = checkpoint;
|
|
582
|
-
const began = performance.now();
|
|
583
|
-
try { return await start(request.project, request.document); }
|
|
584
|
-
finally { loadCheckpoint = null; log('log', `@@VOLTER-LOAD totalMs=${Math.round(performance.now() - began)}`); }
|
|
585
|
-
});
|
|
586
|
-
return loadJob.step();
|
|
591
|
+
return pullWork('start', () => start(request.project, request.document));
|
|
587
592
|
}
|
|
588
593
|
case 'load-next':
|
|
589
594
|
if (!loadJob) throw new Error('No Blender load to continue');
|
|
@@ -632,7 +637,7 @@ async function handle(request: WorkerRequest): Promise<unknown> {
|
|
|
632
637
|
case 'present':
|
|
633
638
|
// Straight through to `session.py`'s own `present` op — the worker adds
|
|
634
639
|
// nothing, and a capture-less present answers `{ presented, revision }`.
|
|
635
|
-
return ask({ op: 'present' });
|
|
640
|
+
return pullWork('present', () => ask({ op: 'present' }));
|
|
636
641
|
case 'scene-info':
|
|
637
642
|
return session.sceneInfo();
|
|
638
643
|
case 'object-info':
|
|
@@ -698,10 +703,10 @@ async function handle(request: WorkerRequest): Promise<unknown> {
|
|
|
698
703
|
// because `collection` crossed `runtime.ts` and `session.py` and never
|
|
699
704
|
// this switch, with no error anywhere.
|
|
700
705
|
case 'rig':
|
|
701
|
-
return ask({
|
|
706
|
+
return pullWork('rig', () => ask({
|
|
702
707
|
op: 'rig',
|
|
703
708
|
...(request.object === undefined ? {} : { object: request.object }),
|
|
704
|
-
});
|
|
709
|
+
}));
|
|
705
710
|
case 'action-clip':
|
|
706
711
|
return ask({
|
|
707
712
|
op: 'action-clip',
|
|
@@ -791,7 +796,7 @@ self.onmessage = (event: MessageEvent<WorkerRequest>) => {
|
|
|
791
796
|
async function answerRequest(request: WorkerRequest): Promise<void> {
|
|
792
797
|
try {
|
|
793
798
|
const result = await handle(request);
|
|
794
|
-
if (documentDirty) await saveDocument();
|
|
799
|
+
if (!loadCheckpoint && documentDirty) await saveDocument();
|
|
795
800
|
await reportHistory();
|
|
796
801
|
post({ id: request.id, result });
|
|
797
802
|
} catch (error) {
|
|
@@ -811,7 +816,7 @@ async function answerRequest(request: WorkerRequest): Promise<void> {
|
|
|
811
816
|
}
|
|
812
817
|
|
|
813
818
|
async function reportHistory(): Promise<void> {
|
|
814
|
-
if (!engine || !session) return;
|
|
819
|
+
if (loadCheckpoint || !engine || !session) return;
|
|
815
820
|
const entries = await engine.request({ op: 'history-events' }) as import('./protocol').NativeHistoryEntry[];
|
|
816
821
|
if (entries.length) post({ op: 'history', entries });
|
|
817
822
|
}
|