@volter/blender-engine 0.1.125 → 0.1.127
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/session.py
CHANGED
|
@@ -1405,10 +1405,10 @@ class Session:
|
|
|
1405
1405
|
if hasattr(_blender_web, "memory_reset_peak"):
|
|
1406
1406
|
_blender_web.memory_reset_peak()
|
|
1407
1407
|
if hasattr(_blender_web, "export_frame_chunked"):
|
|
1408
|
-
exported = _blender_web.export_frame_chunked(json.dumps(options),
|
|
1408
|
+
exported = _blender_web.export_frame_chunked(json.dumps(options, ensure_ascii=False),
|
|
1409
1409
|
lambda: _asked({"checkpoint": "native-export"}))
|
|
1410
1410
|
else:
|
|
1411
|
-
exported = _blender_web.export_frame(json.dumps(options))
|
|
1411
|
+
exported = _blender_web.export_frame(json.dumps(options, ensure_ascii=False))
|
|
1412
1412
|
_mark("export:door")
|
|
1413
1413
|
frame = json.loads(exported)
|
|
1414
1414
|
del exported
|
|
@@ -1627,7 +1627,9 @@ class Session:
|
|
|
1627
1627
|
if kind == "image" and self._send_encoded_image(key, frame["images"][key]):
|
|
1628
1628
|
continue
|
|
1629
1629
|
options["key"] = key
|
|
1630
|
-
|
|
1630
|
+
# The native door reads UTF-8 keys. Preserve Unicode datablock
|
|
1631
|
+
# names rather than turning them into JSON \u escape sequences.
|
|
1632
|
+
piece = json.loads(door(json.dumps(options, ensure_ascii=False)))
|
|
1631
1633
|
if piece.get("error"):
|
|
1632
1634
|
raise RuntimeError("Blender export door: %s" % piece["error"])
|
|
1633
1635
|
frame["warnings"].extend(piece.get("warnings", []))
|
|
@@ -5060,11 +5062,14 @@ def dispatch(request):
|
|
|
5060
5062
|
op = request.get("op")
|
|
5061
5063
|
if op not in _WATCHED_OPS:
|
|
5062
5064
|
return _dispatch_request(request)
|
|
5063
|
-
|
|
5065
|
+
# ID.session_uid survives renames, reallocations and file reloads.
|
|
5066
|
+
# Names alone falsely reported an ordinary rename as a deleted object.
|
|
5067
|
+
before = {o.session_uid: o.name for o in bpy.data.objects}
|
|
5064
5068
|
try:
|
|
5065
5069
|
return _dispatch_request(request)
|
|
5066
5070
|
finally:
|
|
5067
|
-
|
|
5071
|
+
remaining = {o.session_uid for o in bpy.data.objects}
|
|
5072
|
+
removed = [name for uid, name in before.items() if uid not in remaining]
|
|
5068
5073
|
if removed:
|
|
5069
5074
|
cause = {k: request[k] for k in ("label", "property", "column", "direction", "token", "path")
|
|
5070
5075
|
if k in request}
|
|
@@ -159,6 +159,16 @@ export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.
|
|
|
159
159
|
return copy;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** A copy of a presented material that ONE object can wear and a script can recolour (Model
|
|
163
|
+
* Play's `tint`/`setOpacity`): the same hooks, physical values, maps and private uniforms,
|
|
164
|
+
* drawn from the material's constants -- `color`, `opacity`, `map`, `emissive` -- with no
|
|
165
|
+
* node graph, since a graph's Base Color and Alpha replace those constants in the shader.
|
|
166
|
+
* Null for a material that is not one of the presenter's. The caller owns and disposes it. */
|
|
167
|
+
export function ownedSurfaceMaterial(source: THREE.Material): THREE.MeshPhysicalMaterial | null {
|
|
168
|
+
if (!(source instanceof THREE.MeshPhysicalMaterial) || !uniforms.has(source) || !materialDrawHooksSupported(source)) return null;
|
|
169
|
+
return graphShadow(source);
|
|
170
|
+
}
|
|
171
|
+
|
|
162
172
|
export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
|
|
163
173
|
clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
|
|
164
174
|
const data = input ?? defaults;
|
|
@@ -705,8 +705,8 @@ export class BlenderRuntimeView {
|
|
|
705
705
|
/** The camera the VIEWPORT is held in render lighting through (Blender's Rendered shading),
|
|
706
706
|
* or null when the viewport shows modeling lighting. See {@link holdRendered}. */
|
|
707
707
|
private heldRendered: (() => THREE.Camera) | null = null;
|
|
708
|
-
/**
|
|
709
|
-
private heldVisibility: 'viewport' | 'render' = '
|
|
708
|
+
/** Editing retains viewport visibility in every shading mode; Play selects render visibility. */
|
|
709
|
+
private heldVisibility: 'viewport' | 'render' = 'viewport';
|
|
710
710
|
/** What the World was last composed for ({@link worldKeyFor}), or null when it is cleared:
|
|
711
711
|
* composing rebuilds its textures, so it happens only when the key changes. */
|
|
712
712
|
private worldApplied: string | null = null;
|
|
@@ -1175,13 +1175,14 @@ export class BlenderRuntimeView {
|
|
|
1175
1175
|
/**
|
|
1176
1176
|
* BLENDER'S RENDERED SHADING: the viewport held in the lighting a render photographs with
|
|
1177
1177
|
* ({@link setRendered}) — the scene's own lights, its World behind and around the model,
|
|
1178
|
-
*
|
|
1178
|
+
* viewport visibility and shadows — seen through `drawCamera()`, the camera the stage
|
|
1179
1179
|
* draws with (orthographic in an orthographic view). `null` returns the viewport to modeling.
|
|
1180
|
-
*
|
|
1180
|
+
* Render visibility is explicitly selected for a detached Play view; editing
|
|
1181
|
+
* keeps viewport visibility independently of its shading and lighting choices.
|
|
1181
1182
|
* A render taken meanwhile ends back in this state rather than in modeling.
|
|
1182
1183
|
*/
|
|
1183
1184
|
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1184
|
-
holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = '
|
|
1185
|
+
holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = 'viewport'): void {
|
|
1185
1186
|
if (drawCamera === this.heldRendered && sceneLighting?.world === this.heldSceneLighting?.world && sceneLighting?.lights === this.heldSceneLighting?.lights && visibility === this.heldVisibility) return;
|
|
1186
1187
|
this.heldRendered = drawCamera;
|
|
1187
1188
|
this.heldSceneLighting = sceneLighting;
|
|
@@ -1193,7 +1194,8 @@ export class BlenderRuntimeView {
|
|
|
1193
1194
|
/** Prepare a rendered viewport before revealing its first frame, using the
|
|
1194
1195
|
* same lighting/image/World readiness as a photograph. Call after the stage
|
|
1195
1196
|
* has attached the root to its scene. */
|
|
1196
|
-
async prepareRendered(camera: THREE.Camera): Promise<void> {
|
|
1197
|
+
async prepareRendered(camera: THREE.Camera, visibility: 'viewport' | 'render' = 'viewport'): Promise<void> {
|
|
1198
|
+
this.heldVisibility = visibility;
|
|
1197
1199
|
await this.applyRendered(true, camera);
|
|
1198
1200
|
}
|
|
1199
1201
|
|
|
@@ -1276,7 +1278,9 @@ export class BlenderRuntimeView {
|
|
|
1276
1278
|
this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
|
|
1277
1279
|
if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
1278
1280
|
this.motionGeometry.setObjects(this.objects.values());
|
|
1279
|
-
|
|
1281
|
+
// Opaque material ranges also preserve Rendered's materials and shadows.
|
|
1282
|
+
// The range planner itself excludes transparency and custom shaders.
|
|
1283
|
+
this.materialRanges.setObjects(this.drawBatching ? this.objects.values() : []);
|
|
1280
1284
|
}
|
|
1281
1285
|
|
|
1282
1286
|
/** Presentation-only comparison door; never changes Blender or its data. */
|
|
@@ -3,6 +3,16 @@ import {presenterChanged} from './blender-presenter-change';
|
|
|
3
3
|
|
|
4
4
|
export type TextureExtension = 'REPEAT' | 'EXTEND' | 'MIRROR' | 'CLIP';
|
|
5
5
|
|
|
6
|
+
/** Texture.copy requests an upload even when an encoded image has not yet
|
|
7
|
+
* decoded. Keep the sampler object (and its shared Source) stable, but defer
|
|
8
|
+
* that request until the owner actually supplies image data. Graph sampler
|
|
9
|
+
* filter changes go through this same setter while decoding is pending. */
|
|
10
|
+
class PendingImageSampler extends THREE.Texture {
|
|
11
|
+
override set needsUpdate(value: boolean) {
|
|
12
|
+
if (value && this.image) super.needsUpdate = value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
/** A shared material may draw meshes whose named layers have different
|
|
7
17
|
* channel positions. Resolve at Three's per-draw hook, before program choice. */
|
|
8
18
|
export function bindNamedUvChannels(material: THREE.MeshPhysicalMaterial, geometry: THREE.BufferGeometry): void {
|
|
@@ -40,13 +50,17 @@ export class BlenderTextureSamplers {
|
|
|
40
50
|
let binding = this.bindings.get(key);
|
|
41
51
|
if (!binding || binding.source !== source) {
|
|
42
52
|
this.delete(key);
|
|
43
|
-
|
|
53
|
+
// Ready raster/DataTextures keep their own subtype's clone behavior.
|
|
54
|
+
// Only an empty encoded-image Texture with an owner decode promise
|
|
55
|
+
// needs the gate; an unexplained missing image keeps its diagnostics.
|
|
56
|
+
const texture = !source.image && ready ? new PendingImageSampler().copy(source) : source.clone();
|
|
57
|
+
binding = {source, texture, version: -1, disposed: false};
|
|
44
58
|
this.bindings.set(key, binding);
|
|
45
59
|
const held = binding;
|
|
46
60
|
// The owner awaits and reports decode failures. Avoid an unhandled
|
|
47
61
|
// rejection here, and never upload a clone removed during the decode.
|
|
48
62
|
if (ready) void ready.then(() => {
|
|
49
|
-
if (!held.disposed) {
|
|
63
|
+
if (!held.disposed && source.image) {
|
|
50
64
|
held.texture.needsUpdate = true;
|
|
51
65
|
held.version = source.version;
|
|
52
66
|
presenterChanged();
|
package/browser/worker.ts
CHANGED
|
@@ -541,10 +541,15 @@ async function stageProjectFiles(files_: BlenderFiles, project: string): Promise
|
|
|
541
541
|
say(`${file.path}: HTTP ${answer.status}`);
|
|
542
542
|
continue;
|
|
543
543
|
}
|
|
544
|
+
// The index can precede an append or rewrite. Stream the response's own size,
|
|
545
|
+
// not an earlier index entry's size; keep the old stamp so the next sync rechecks.
|
|
546
|
+
const declaredSize = Number(answer.headers.get('content-length'));
|
|
547
|
+
const responseSize = answer.headers.has('content-length') && Number.isSafeInteger(declaredSize) && declaredSize >= 0
|
|
548
|
+
? declaredSize : file.size;
|
|
544
549
|
const dir = path.slice(0, path.lastIndexOf('/'));
|
|
545
550
|
if (dir) await files_.mkdirTree(dir);
|
|
546
551
|
if (files_.writeFileStream && answer.body) {
|
|
547
|
-
await files_.writeFileStream(path, loadCheckpoint ? checkpointStream(answer.body, () => loadCheckpoint!("file-import")) : answer.body,
|
|
552
|
+
await files_.writeFileStream(path, loadCheckpoint ? checkpointStream(answer.body, () => loadCheckpoint!("file-import")) : answer.body, responseSize);
|
|
548
553
|
} else {
|
|
549
554
|
await files_.writeFile(path, new Uint8Array(await answer.arrayBuffer()));
|
|
550
555
|
}
|