@volter/blender-engine 0.1.95 → 0.1.96

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.
@@ -806,6 +806,8 @@ def _saved_view():
806
806
  "distance": float(region.view_distance),
807
807
  "perspective": region.view_perspective,
808
808
  "shading": space.shading.type,
809
+ "scene_world": bool(space.shading.use_scene_world),
810
+ "scene_lights": bool(space.shading.use_scene_lights),
809
811
  }
810
812
  return None
811
813
 
@@ -4901,13 +4903,12 @@ def rna_outliner(selected=None):
4901
4903
  # `add_layer_collections_recursive`: an EXCLUDED collection's objects
4902
4904
  # are not added at all -- it is not in the view layer.
4903
4905
  if not lc.exclude:
4904
- page, more = _outliner_page(collection.objects)
4905
- for obj in page:
4906
+ # Fold parenting before paging: a parent may follow its children in
4907
+ # collection order, and must not disappear behind the flat cap.
4908
+ for obj in collection.objects:
4906
4909
  object_row = _outliner_object(obj, view_layer, seen, chosen, active)
4907
4910
  place(obj, object_row, row)
4908
4911
  children.append(object_row)
4909
- if more:
4910
- row["more"] = more
4911
4912
  row["children"] = children
4912
4913
  return row
4913
4914
 
@@ -4920,13 +4921,10 @@ def rna_outliner(selected=None):
4920
4921
  _outliner_key("%s.layer_collection.children" % view_layer_path,
4921
4922
  child.collection.name))
4922
4923
  for child in root.children]
4923
- page, more = _outliner_page(root.collection.objects)
4924
- for obj in page:
4924
+ for obj in root.collection.objects:
4925
4925
  object_row = _outliner_object(obj, view_layer, seen, chosen, active)
4926
4926
  place(obj, object_row, scene_collection)
4927
4927
  children.append(object_row)
4928
- if more:
4929
- scene_collection["more"] = more
4930
4928
  scene_collection["children"] = children
4931
4929
 
4932
4930
  # `ObjectsChildrenBuilder::make_object_parent_hierarchy_collections`, parents
@@ -4974,6 +4972,18 @@ def rna_outliner(selected=None):
4974
4972
  parent_row["children"].append(duplicate)
4975
4973
  mine.append([duplicate, parent_row, parent_collection])
4976
4974
 
4975
+ # Cap the hierarchy's visible child lists, not the collection's flat input.
4976
+ # Every parent participates in folding even when its child list needs a page.
4977
+ def page_tree(row):
4978
+ children, more = _outliner_page(row.get("children", []))
4979
+ row["children"] = children
4980
+ if more:
4981
+ row["more"] = more
4982
+ for child in children:
4983
+ page_tree(child)
4984
+
4985
+ page_tree(scene_collection)
4986
+
4977
4987
  return {
4978
4988
  "scene": scene_path,
4979
4989
  "viewLayer": view_layer_path,
@@ -576,6 +576,8 @@ export const frameSchema = z
576
576
  perspective: z.enum(['PERSP', 'ORTHO', 'CAMERA']),
577
577
  lens: z.number().finite().positive().default(50),
578
578
  shading: z.enum(['WIREFRAME', 'SOLID', 'MATERIAL', 'RENDERED']).optional(),
579
+ scene_world: z.boolean().optional(),
580
+ scene_lights: z.boolean().optional(),
579
581
  })
580
582
  .nullable()
581
583
  .optional(),
@@ -693,9 +695,18 @@ export class BlenderRuntimeView {
693
695
  private readonly lightExtrasRoot = new THREE.Group();
694
696
  private readonly emptyExtrasRoot = new THREE.Group();
695
697
  private rendered = false;
698
+ private sceneLights = false;
699
+ private shadowFit: {
700
+ camera: THREE.Camera | undefined;
701
+ cameraMatrix: THREE.Matrix4 | undefined;
702
+ projection: THREE.Matrix4 | undefined;
703
+ objects: Map<THREE.Object3D, { matrix: THREE.Matrix4; visible: boolean }>;
704
+ } | null = null;
696
705
  /** The camera the VIEWPORT is held in render lighting through (Blender's Rendered shading),
697
706
  * or null when the viewport shows modeling lighting. See {@link holdRendered}. */
698
707
  private heldRendered: (() => THREE.Camera) | null = null;
708
+ /** Material Preview can use scene lighting while retaining viewport visibility. */
709
+ private heldVisibility: 'viewport' | 'render' = 'render';
699
710
  /** What the World was last composed for ({@link worldKeyFor}), or null when it is cleared:
700
711
  * composing rebuilds its textures, so it happens only when the key changes. */
701
712
  private worldApplied: string | null = null;
@@ -881,6 +892,8 @@ export class BlenderRuntimeView {
881
892
  readonly projection: 'perspective' | 'orthographic';
882
893
  readonly lens: number;
883
894
  readonly drawMode: 'wireframe' | 'solid' | 'preview' | 'rendered' | null;
895
+ readonly sceneWorld: boolean;
896
+ readonly sceneLights: boolean;
884
897
  } | null {
885
898
  const saved = this.frame?.view;
886
899
  if (!saved) return null;
@@ -896,6 +909,8 @@ export class BlenderRuntimeView {
896
909
  projection: saved.perspective === 'ORTHO' ? 'orthographic' : 'perspective',
897
910
  lens: saved.lens,
898
911
  drawMode: saved.shading ? SAVED_SHADING[saved.shading] : null,
912
+ sceneWorld: saved.scene_world ?? false,
913
+ sceneLights: saved.scene_lights ?? false,
899
914
  };
900
915
  }
901
916
 
@@ -1005,7 +1020,16 @@ export class BlenderRuntimeView {
1005
1020
  }
1006
1021
 
1007
1022
  stageMode(): string | null {
1008
- return this.mode;
1023
+ return this.playing ? 'PLAY' : this.mode;
1024
+ }
1025
+
1026
+ /** PLAY IS A MODE OF THE STAGE: while the document plays, the keys are the player's, and the
1027
+ * mode this context publishes is what the frame's key rules stand down on
1028
+ * (`scripts/workbench/generate-keymaps.mjs`, `whenFor`). Blender's own mode returns on stop. */
1029
+ private playing = false;
1030
+
1031
+ setPlaying(playing: boolean): void {
1032
+ this.playing = playing;
1009
1033
  }
1010
1034
 
1011
1035
  /** The inverse: the presented object for a Blender datablock NAME, or null
@@ -1153,15 +1177,26 @@ export class BlenderRuntimeView {
1153
1177
  * ({@link setRendered}) — the scene's own lights, its World behind and around the model,
1154
1178
  * `hide_render` visibility and shadows — seen through `drawCamera()`, the camera the stage
1155
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.
1156
1181
  * A render taken meanwhile ends back in this state rather than in modeling.
1157
1182
  */
1158
- holdRendered(drawCamera: (() => THREE.Camera) | null): void {
1159
- if (drawCamera === this.heldRendered) return;
1183
+ private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
1184
+ holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = 'render'): void {
1185
+ if (drawCamera === this.heldRendered && sceneLighting?.world === this.heldSceneLighting?.world && sceneLighting?.lights === this.heldSceneLighting?.lights && visibility === this.heldVisibility) return;
1160
1186
  this.heldRendered = drawCamera;
1187
+ this.heldSceneLighting = sceneLighting;
1188
+ this.heldVisibility = visibility;
1161
1189
  if (this.capturing) return;
1162
1190
  this.report(drawCamera === null ? this.applyRendered(false) : this.applyRendered(true, drawCamera()));
1163
1191
  }
1164
1192
 
1193
+ /** Prepare a rendered viewport before revealing its first frame, using the
1194
+ * same lighting/image/World readiness as a photograph. Call after the stage
1195
+ * has attached the root to its scene. */
1196
+ async prepareRendered(camera: THREE.Camera): Promise<void> {
1197
+ await this.applyRendered(true, camera);
1198
+ }
1199
+
1165
1200
  /**
1166
1201
  * Re-apply the held render lighting if the draw camera or the World changed since it was
1167
1202
  * last applied. Cheap when nothing did; the stage calls it every frame it draws.
@@ -1170,7 +1205,8 @@ export class BlenderRuntimeView {
1170
1205
  // A photograph in progress keeps its own state; it returns to this one when it ends.
1171
1206
  if (this.heldRendered === null || this.capturing) return;
1172
1207
  const camera = this.heldRendered();
1173
- if (this.worldKeyFor(camera) === this.worldApplied) return;
1208
+ const worldKey = this.heldSceneLighting?.world === false ? null : this.worldKeyFor(camera);
1209
+ if (worldKey === this.worldApplied) return;
1174
1210
  this.report(this.applyRendered(true, camera));
1175
1211
  }
1176
1212
 
@@ -1183,7 +1219,12 @@ export class BlenderRuntimeView {
1183
1219
  const ortho = (camera as THREE.OrthographicCamera).isOrthographicCamera;
1184
1220
  // Coarse, so an orbit recomposes a handful of times rather than every frame.
1185
1221
  const turn = ortho ? camera.getWorldDirection(new THREE.Vector3()).toArray().map((v) => v.toFixed(1)).join(',') : '';
1186
- return `${camera.uuid}:${turn}:${this.worldKey}`;
1222
+ // A stage can announce itself before parenting the model into its Scene.
1223
+ // Retry after that attachment; the first World apply otherwise finds no Scene
1224
+ // and its cached key leaves the viewport with no sky or ambient light.
1225
+ let owner: THREE.Object3D = this.root;
1226
+ while (owner.parent) owner = owner.parent;
1227
+ return `${owner.uuid}:${camera.uuid}:${turn}:${this.worldKey}`;
1187
1228
  }
1188
1229
 
1189
1230
  private report(work: Promise<void>): void {
@@ -1258,6 +1299,10 @@ export class BlenderRuntimeView {
1258
1299
  const start = performance.now();
1259
1300
  this.instances.prepareDraw(camera);
1260
1301
  this.root.updateMatrixWorld(true);
1302
+ // The script/navigation has just posed the camera and the detached
1303
+ // objects. Fit shadows for that draw, rather than the camera's pose
1304
+ // when Rendered shading was first entered. Captures use this path too.
1305
+ if (this.rendered) this.applyShadows(true, camera);
1261
1306
  const opaqueDone = performance.now();
1262
1307
  const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
1263
1308
  options?.interactive === true, options?.height ?? 0);
@@ -1321,7 +1366,11 @@ export class BlenderRuntimeView {
1321
1366
  }
1322
1367
 
1323
1368
  private async applyRendered(rendered: boolean, camera?: THREE.Camera): Promise<void> {
1324
- const extinction = (rendered ? worldMedium(this.frame?.world)?.extinction : null) ?? new THREE.Vector3();
1369
+ // A frame or shading change can replace geometry, visibility and lights.
1370
+ this.shadowFit = null;
1371
+ const sceneWorld = rendered && (this.capturing || this.heldSceneLighting?.world !== false);
1372
+ const sceneLights = rendered && (this.capturing || this.heldSceneLighting?.lights !== false);
1373
+ const extinction = (sceneWorld ? worldMedium(this.frame?.world)?.extinction : null) ?? new THREE.Vector3();
1325
1374
  for (const material of [...this.materials.values(), this.fallback]) applyWorldExtinction(material, extinction);
1326
1375
  // An area light cannot be DRAWN until its lookup tables are uploaded, and a
1327
1376
  // render is one photograph with no second chance at it.
@@ -1329,12 +1378,15 @@ export class BlenderRuntimeView {
1329
1378
  // same way it waits for area-light tables and image decodes: one
1330
1379
  // photograph, no second chance at it.
1331
1380
  this.rendered = rendered;
1381
+ this.sceneLights = sceneLights;
1382
+ // This is the studio rig, not the authored lights. A scene-lit preview
1383
+ // never adds the Solid studio, including World-only Material Preview.
1332
1384
  this.lighting.setRendered(rendered);
1333
1385
  // The scene's world is what a render sees past the geometry AND its
1334
1386
  // ambient light; modeling keeps the document's own backdrop and fill.
1335
1387
  // A photograph always composes its own; the viewport recomposes only when the key changed.
1336
- const worldKey = rendered && camera ? this.worldKeyFor(camera) : null;
1337
- if (!rendered) this.world.clear();
1388
+ const worldKey = sceneWorld && camera ? this.worldKeyFor(camera) : null;
1389
+ if (!sceneWorld) this.world.clear();
1338
1390
  else if (this.capturing || worldKey !== this.worldApplied) this.world.apply(this.root, this.frame?.world ?? null, camera);
1339
1391
  this.worldApplied = this.capturing ? null : worldKey;
1340
1392
  this.applyVisibility();
@@ -1372,6 +1424,21 @@ export class BlenderRuntimeView {
1372
1424
  */
1373
1425
  private applyShadows(rendered: boolean, camera?: THREE.Camera): void {
1374
1426
  this.root.updateMatrixWorld(true);
1427
+ const fitted = this.shadowFit;
1428
+ if (rendered && fitted && fitted.camera === camera &&
1429
+ (!camera || (fitted.cameraMatrix?.equals(camera.matrixWorld) && fitted.projection?.equals(camera.projectionMatrix))) &&
1430
+ fitted.objects.size === this.objects.size &&
1431
+ [...this.objects.values()].every(object => {
1432
+ const previous = fitted.objects.get(object);
1433
+ return previous?.visible === object.visible && previous.matrix.equals(object.matrixWorld);
1434
+ })) return;
1435
+ this.shadowFit = rendered ? {
1436
+ camera,
1437
+ cameraMatrix: camera?.matrixWorld.clone(),
1438
+ projection: camera?.projectionMatrix.clone(),
1439
+ objects: new Map([...this.objects.values()].map(object =>
1440
+ [object, { matrix: object.matrixWorld.clone(), visible: object.visible }])),
1441
+ } : null;
1375
1442
  const boxes: THREE.Box3[] = [];
1376
1443
  for (const object of this.objects.values()) {
1377
1444
  const mesh = object as THREE.Mesh;
@@ -1497,12 +1564,14 @@ export class BlenderRuntimeView {
1497
1564
  }
1498
1565
 
1499
1566
  private applyVisibility(): void {
1567
+ const renderVisibility = this.rendered && (this.capturing || this.heldVisibility === 'render');
1500
1568
  for (const obj of this.frame?.objects ?? []) {
1501
1569
  const object = this.objects.get(obj.id);
1502
- if (object) object.visible = this.rendered ? obj.render_visible : obj.visible;
1570
+ const visible = renderVisibility ? obj.render_visible : obj.visible;
1571
+ if (object) object.visible = visible;
1503
1572
  if (!obj.light) continue;
1504
1573
  const light = this.lights.get(obj.light);
1505
- if (light) light.visible = this.rendered && obj.render_visible;
1574
+ if (light) light.visible = this.sceneLights && visible;
1506
1575
  }
1507
1576
  // AN OVERLAY IS MODELING CHROME AND IS NEVER PHOTOGRAPHED. The same
1508
1577
  // distinction the loop above draws between what the viewport shows and
@@ -2224,6 +2293,26 @@ export class BlenderRuntimeView {
2224
2293
  };
2225
2294
  }
2226
2295
 
2296
+ /**
2297
+ * A COPY OF THE MODEL THAT NO LONGER FOLLOWS IT, for Play: the graph `follow` builds, from the
2298
+ * frame this view holds now, and handed nothing afterwards. What moves its objects is the
2299
+ * caller's; Blender's data and this view never see it, and disposing it is the whole undo.
2300
+ */
2301
+ detach(): { readonly view: BlenderRuntimeView; dispose(): void } {
2302
+ const detached = new BlenderRuntimeView();
2303
+ if (this.frame) detached.applyFrame(this.fullFrame());
2304
+ let disposed = false;
2305
+ return {
2306
+ view: detached,
2307
+ dispose: () => {
2308
+ if (disposed) return;
2309
+ disposed = true;
2310
+ detached.dispose();
2311
+ detached.root.removeFromParent();
2312
+ },
2313
+ };
2314
+ }
2315
+
2227
2316
  captureSnapshot() {
2228
2317
  const frame = this.fullFrame();
2229
2318
  const source = this.frame!;
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.95",
5
+ "version": "0.1.96",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },