@volter/blender-engine 0.1.125 → 0.1.126

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.
@@ -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
- piece = json.loads(door(json.dumps(options)))
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
- before = {o.name for o in bpy.data.objects}
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
- removed = before - {o.name for o in bpy.data.objects}
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}
@@ -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
- /** Material Preview can use scene lighting while retaining viewport visibility. */
709
- private heldVisibility: 'viewport' | 'render' = '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
- * `hide_render` visibility and shadows — seen through `drawCamera()`, the camera the stage
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
- * Material Preview passes viewport visibility independently of its lighting choices.
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' = 'render'): void {
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
- this.materialRanges.setObjects(this.drawBatching && !this.rendered ? this.objects.values() : []);
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
- binding = {source, texture: source.clone(), version: -1, disposed: false};
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, file.size);
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
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/blender-engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "GPL-3.0-or-later",
5
- "version": "0.1.125",
5
+ "version": "0.1.126",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },