@volter/editor-blender 0.1.8 → 0.1.9

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.
Files changed (33) hide show
  1. package/contributions/blender-palette.source.mjs +15 -5
  2. package/contributions/blender-properties-view.tsx +28 -9
  3. package/contributions/blender-runtime.document.tsx +6 -1
  4. package/contributions/blender.command.ts +5 -1
  5. package/contributions/blender.environment.ts +32 -0
  6. package/contributions/blender.palette.json +8 -0
  7. package/contributions/blender.status.tsx +2 -6
  8. package/contributions/blender.style.ts +30 -89
  9. package/contributions/material-preview.view.ts +35 -0
  10. package/contributions/model.layout.ts +7 -7
  11. package/contributions/models.finder.ts +16 -0
  12. package/contributions/sculpt.layout.ts +3 -0
  13. package/contributions/shading.layout.ts +3 -0
  14. package/contributions/studiolights/world/city.exr +0 -0
  15. package/contributions/studiolights/world/courtyard.exr +0 -0
  16. package/contributions/studiolights/world/forest.exr +0 -0
  17. package/contributions/studiolights/world/interior.exr +0 -0
  18. package/contributions/studiolights/world/license.txt +15 -0
  19. package/contributions/studiolights/world/night.exr +0 -0
  20. package/contributions/studiolights/world/studio.exr +0 -0
  21. package/contributions/studiolights/world/sunrise.exr +0 -0
  22. package/contributions/studiolights/world/sunset.exr +0 -0
  23. package/contributions/uv-editing.layout.ts +3 -0
  24. package/dist-node/serving.mjs +624 -0
  25. package/host/blender-runtime-host.ts +11 -1
  26. package/node/blender-mcp.ts +2 -2
  27. package/package.json +21 -10
  28. package/serving/blender-routes.ts +427 -0
  29. package/serving/blender-wasm-artifact.ts +449 -0
  30. package/serving/index.ts +10 -0
  31. package/src/layouts.tsx +4 -5
  32. package/src/presentation.ts +32 -0
  33. package/src/regions.ts +59 -0
@@ -187,14 +187,24 @@ const MAPPING = {
187
187
  '#69aa15',
188
188
  'the same blend over `.tui.yaxis` #8bdc00 — measured, see axisX.',
189
189
  ),
190
- // There is no `viewport.axisZ`: `EditorTheme.color.viewport` declares X and
191
- // Y only (`packages/editor/src/theme.ts`), because the editor's grid plane
192
- // draws two axis lines. `.tui.zaxis` #2890ff is the member a third would
193
- // cite when one exists — a palette key no reader reads is a name with no
194
- // caller.
190
+ // There is no `viewport.axisZ`: the grid plane draws two axis lines. The
191
+ // gizmos carry all three (`gizmo.*`).
195
192
  'viewport.selection': rgb('space_view3d.select'),
196
193
  'viewport.active': rgb('space_view3d.active'),
197
194
 
195
+ // The transform gizmo draws the theme's axis colours as they are
196
+ // (`transform_gizmo_3d.cc`, `gizmo_get_axis_color`).
197
+ 'gizmo.x': rgb('tui.xaxis'),
198
+ 'gizmo.y': rgb('tui.yaxis'),
199
+ 'gizmo.z': rgb('tui.zaxis'),
200
+ 'gizmo.navigationX': held(
201
+ '#f53651',
202
+ "the navigation gizmo's X ball as Blender draws it, read off `modeling-object-none.png` " +
203
+ '(245,54,81): drawn from `.tui.xaxis` but not equal to it — measured.',
204
+ ),
205
+ 'gizmo.navigationY': held('#6fa41b', 'the same ball for Y (111,164,27) — measured, see navigationX.'),
206
+ 'gizmo.navigationZ': held('#2e83e3', 'the same ball for Z (46,131,227) — measured, see navigationX.'),
207
+
198
208
  'region.outliner': rgb('space_outliner.back'),
199
209
  'region.properties': rgb('space_properties.back'),
200
210
 
@@ -383,9 +383,27 @@ function Numeric({
383
383
  );
384
384
  }
385
385
 
386
- /** X/Y/Z/W — `RNA_property_array_item_char`'s own letters for a vector or
387
- * colour; anything longer is numbered, as Blender numbers it. */
388
- const ARRAY_LETTERS = ['X', 'Y', 'Z', 'W'];
386
+ /** An array component's letter, as `RNA_property_array_item_char` gives it:
387
+ * W/X/Y/Z for a quaternion or axis-angle (the angle first), X/Y/Z/W for the
388
+ * vector subtypes, R/G/B/A for a colour, and none otherwise, where the
389
+ * component is numbered. */
390
+ const VECTOR_SUBTYPES = new Set([
391
+ 'TRANSLATION',
392
+ 'DIRECTION',
393
+ 'XYZ',
394
+ 'XYZ_LENGTH',
395
+ 'EULER',
396
+ 'VELOCITY',
397
+ 'ACCELERATION',
398
+ 'COORDS',
399
+ ]);
400
+ function arrayItemChar(subtype: string, index: number): string | undefined {
401
+ if (subtype === 'QUATERNION' || subtype === 'AXISANGLE') return 'WXYZ'[index];
402
+ if (index >= 4) return undefined;
403
+ if (VECTOR_SUBTYPES.has(subtype)) return 'XYZW'[index];
404
+ if (subtype === 'COLOR' || subtype === 'COLOR_GAMMA') return 'RGBA'[index];
405
+ return undefined;
406
+ }
389
407
 
390
408
  /** A field's WHOLE name, for the control's `aria-label` — the property's UI
391
409
  * name (its RNA identifier when it has none), plus the array component's
@@ -393,7 +411,7 @@ const ARRAY_LETTERS = ['X', 'Y', 'Z', 'W'];
393
411
  * component after the first. */
394
412
  function fieldName(row: BlenderRnaRow, index?: number): string {
395
413
  const name = row.name || row.identifier;
396
- return index === undefined ? name : `${name} ${ARRAY_LETTERS[index] ?? index}`;
414
+ return index === undefined ? name : `${name} ${arrayItemChar(row.subtype, index) ?? index}`;
397
415
  }
398
416
 
399
417
  function colourOf(values: readonly number[]): string {
@@ -499,8 +517,8 @@ function ArrayRows({ row, label, title, write }: WidgetProps & { label: string;
499
517
  <>
500
518
  {values.map((value, index) => (
501
519
  <Row
502
- key={`${row.identifier}-${ARRAY_LETTERS[index] ?? index}`}
503
- label={componentLabel(label, index, values.length)}
520
+ key={`${row.identifier}-${index}`}
521
+ label={componentLabel(label, index, row.subtype)}
504
522
  title={title}
505
523
  readonly={row.readonly}
506
524
  identifier={`${row.identifier}[${index}]`}
@@ -526,9 +544,10 @@ function ArrayRows({ row, label, title, write }: WidgetProps & { label: string;
526
544
  );
527
545
  }
528
546
 
529
- function componentLabel(label: string, index: number, length: number): string {
530
- if (length > ARRAY_LETTERS.length) return index === 0 ? label : `${label} ${index}`;
531
- return `${index === 0 ? `${label} ` : ''}${ARRAY_LETTERS[index]}`;
547
+ function componentLabel(label: string, index: number, subtype: string): string {
548
+ const letter = arrayItemChar(subtype, index);
549
+ if (letter === undefined) return index === 0 ? label : `${label} ${index}`;
550
+ return `${index === 0 ? `${label} ` : ''}${letter}`;
532
551
  }
533
552
 
534
553
  function widgetFor(props: WidgetProps): React.ReactNode {
@@ -20,10 +20,12 @@
20
20
  *
21
21
  * WITHOUT AN ENTRY it is still the document `blender-start` presents into: a
22
22
  * project that lists no `.blend` of its own gets one Model document at the
23
- * standing `blender:runtime` address (`project-adapter.ts` injects it), and
23
+ * standing `blender:runtime` address (`models.finder.ts` lists it), and
24
24
  * the session opens its default `models/model.blend`.
25
25
  */
26
26
 
27
+ // How the `model` stage this document builds behaves (its starting presentation).
28
+ import '../src/presentation';
27
29
  import { blenderModelView } from '@volter/blender-engine/browser/three/blender-runtime-view';
28
30
  import type { ToolContributionProps, ToolDocumentToolbar } from '@volter/editor-sdk/contributions';
29
31
  import { editorHost } from '@volter/editor-sdk/host';
@@ -236,6 +238,9 @@ function BlenderModelViewport({
236
238
  // `--factory-startup`), about a third of the size a bare fit gives, which
237
239
  // is the number `openingFit` was written for and nothing was passing.
238
240
  openingFit={3}
241
+ // Every entry this document opens is a `model` stage, the standing `blender:runtime`
242
+ // address included (its id carries no `model:` prefix).
243
+ stageKind={documentKind}
239
244
  // SOLID SHADING IS BLENDER'S, and Blender's Solid mode has NO world
240
245
  // light: `light_ambient` is (0,0,0), there is no IBL, and the model is
241
246
  // lit by four studio lights stated in VIEW space. So this document
@@ -60,7 +60,11 @@ export const commands: CommandContribution['commands'] = {
60
60
  'blender-screenshot-view': verb('always', 60_000),
61
61
  'blender-read-file': verb('none', 60_000),
62
62
  'blender-write-file': verb('none', 60_000),
63
- 'blender-list-files': verb('none'),
63
+ // Listing waits on the worker like a read: after an execute the MCP mirror lists
64
+ // while the page is still presenting the frame and refreshing its RNA and
65
+ // outliner reads, which on a large document outlasts the generic 5s default and
66
+ // turned a successful execute into a timeout.
67
+ 'blender-list-files': verb('none', 60_000),
64
68
  // THE RNA DOOR (WORK.md §Blender in the tab is Blender, "Inspection
65
69
  // parity", I1). Two READS — a datablock's `bl_rna.properties` and the
66
70
  // Properties context — and one WRITE. The reads present nothing, so they
@@ -0,0 +1,32 @@
1
+ /**
2
+ * BLENDER'S WORLD STUDIO LIGHTS: the eight HDRIs Blender ships for Material Preview
3
+ * (`datafiles/studiolights/world`, copied from Blender 5.2), registered as environment images
4
+ * (`@volter/editor-sdk/kit/environment-images`) so a view can light by one and draw it behind
5
+ * the scene. Material Preview opens on Forest. All CC0, by Greg Zaal (Poly Haven); the terms
6
+ * and each original are in `studiolights/world/license.txt`.
7
+ */
8
+ import type { EnvironmentImageSet } from '@volter/editor-sdk/kit/environment-images';
9
+ import city from './studiolights/world/city.exr?url';
10
+ import courtyard from './studiolights/world/courtyard.exr?url';
11
+ import forest from './studiolights/world/forest.exr?url';
12
+ import interior from './studiolights/world/interior.exr?url';
13
+ import night from './studiolights/world/night.exr?url';
14
+ import studio from './studiolights/world/studio.exr?url';
15
+ import sunrise from './studiolights/world/sunrise.exr?url';
16
+ import sunset from './studiolights/world/sunset.exr?url';
17
+
18
+ export const point = 'workspace.environment';
19
+ export const environment: EnvironmentImageSet = {
20
+ id: 'blender-studiolights-world',
21
+ source: "Blender's world studio lights, CC0 (Poly Haven); studiolights/world/license.txt",
22
+ images: [
23
+ { id: 'blender:forest', title: 'Forest', url: forest, format: 'exr' },
24
+ { id: 'blender:city', title: 'City', url: city, format: 'exr' },
25
+ { id: 'blender:courtyard', title: 'Courtyard', url: courtyard, format: 'exr' },
26
+ { id: 'blender:interior', title: 'Interior', url: interior, format: 'exr' },
27
+ { id: 'blender:night', title: 'Night', url: night, format: 'exr' },
28
+ { id: 'blender:studio', title: 'Studio', url: studio, format: 'exr' },
29
+ { id: 'blender:sunrise', title: 'Sunrise', url: sunrise, format: 'exr' },
30
+ { id: 'blender:sunset', title: 'Sunset', url: sunset, format: 'exr' },
31
+ ],
32
+ };
@@ -53,6 +53,14 @@
53
53
  "selection": "#ed5700",
54
54
  "active": "#ffa028"
55
55
  },
56
+ "gizmo": {
57
+ "x": "#ff3352",
58
+ "y": "#8bdc00",
59
+ "z": "#2890ff",
60
+ "navigationX": "#f53651",
61
+ "navigationY": "#6fa41b",
62
+ "navigationZ": "#2e83e3"
63
+ },
56
64
  "region": {
57
65
  "outliner": "#282828",
58
66
  "properties": "#303030"
@@ -4,9 +4,7 @@
4
4
  * Blender's bottom row opens with three tiny mouse glyphs, each with one
5
5
  * button lit and the action it performs beside it — `Select`, `Rotate View`,
6
6
  * `Options` in the reference (`modeling.png`). It is the one piece of chrome
7
- * that tells a newcomer what the three buttons DO without a menu, and the
8
- * gap a human build session measured here was exactly that question ("how do
9
- * I move the camera", six times — `viewport-controls-hint.ts`).
7
+ * that tells a newcomer what the three buttons DO without a menu.
10
8
  *
11
9
  * THEY ARE HOVER-DRIVEN THERE, AND ALWAYS-ON HERE. Measured across the whole
12
10
  * reference set: `modeling.png` is the ONE frame with any ink left of centre
@@ -33,9 +31,7 @@
33
31
  * `editor-viewport.ts:682-686` sets `orbitControls.mouseButtons` to
34
32
  * `{ LEFT: -1, MIDDLE: PAN, RIGHT: ROTATE }`, and the left button is left
35
33
  * free for the selection raycast (`editor-viewport.ts:3782`,
36
- * `if (e.button === 0 && !e.altKey)`). The editor's own prose says the
37
- * same thing (`ViewportControlsHint.tsx:22`: "Orbit: right-drag · Pan:
38
- * middle-drag").
34
+ * `if (e.button === 0 && !e.altKey)`).
39
35
  *
40
36
  * That contract is a constant of the host, not of any document, so the table
41
37
  * below is a constant too — with the citations above as its provenance. When
@@ -175,97 +175,38 @@ export const style: StyleContribution = {
175
175
  // applied TWICE; see `theme.ts`'s `areaEmbossValue`.
176
176
  areaEmboss: 8.2,
177
177
  },
178
- viewport: {
179
- // BLENDER'S GIZMO IS A CONSTANT SCREEN SIZE (owner, 2026-09-21: "is
180
- // gizmo perhaps the wrong size?"). `U.gizmo_size` is 75
181
- // (`DNA_userdef_types.h:1095`; Preferences > Viewport > Gizmos > Size,
182
- // `rna_userdef.cc:5415-5419`, range 10..200), and it is px PER GIZMO
183
- // UNIT: `wm_gizmo.cc:450-474` sets `scale_final = scale_basis *
184
- // UI_SCALE_FAC * U.gizmo_size * ED_view3d_pixel_size_no_ui_scale(...)`,
185
- // whose last term is world units per DEVICE px against a
186
- // `UI_SCALE_FAC` of device px per UI px — so one gizmo unit is 75 CSS
187
- // px whatever the display scale (read back from the engine's own pin
188
- // on this box: `ui_scale=2.0 pixel_size=2.0 gizmo_size=75`).
189
- gizmoSize: 75,
190
- // BLENDER'S SHELF OPENS ON SELECT BOX, so a selected object carries no
191
- // transform gizmo until one of the four transform tools is armed
192
- // (`space_toolsystem_toolbar.py`: `_defs_view3d_generic.select_box` is
193
- // the first entry of the Object Mode `_tools_default` tuple).
194
- // Photographed at the engine's pin rather than remembered:
195
- // `gizmo-select-box.png` has Select Box lit in the shelf, the default
196
- // cube selected and outlined orange, and nothing at its origin but the
197
- // 3D cursor and the object dot; `gizmo-move.png` has Move lit in the
198
- // same frame. Ours opened on the COMBINED gizmo — Unity's shape, three
199
- // tools' handles at once, on something merely selected.
200
- shelfTool: 'select',
201
- // BLENDER'S WORLD IS Z-UP, and the stage presents it through the exact
202
- // signed permutation `(x, y, z) → (x, z, −y)` the presented root
203
- // carries (`blender-runtime-view.ts`'s constructor). So three's Y is
204
- // Blender's Z and three's Z is Blender's −Y, and the transform gizmo's
205
- // colours, the side each arm is drawn on, the plane squares and the
206
- // navigation gizmo's six labels all say so: the arm that points up is
207
- // Z and it is blue, Y is green and points away.
208
- upAxis: 'z',
209
- // BLENDER'S SELECT BOX IS A TOUCH TEST — its object-mode box select
210
- // reads the object-id buffer under the rectangle, so any drawn part of
211
- // an object inside it selects that object. The editor's own answer is
212
- // containment, for its own measured reason; see the field.
213
- boxSelect: 'touch',
214
- },
178
+ },
179
+ stage: {
180
+ // BLENDER'S GIZMO IS A CONSTANT SCREEN SIZE (owner, 2026-09-21: "is
181
+ // gizmo perhaps the wrong size?"). `U.gizmo_size` is 75
182
+ // (`DNA_userdef_types.h:1095`; Preferences > Viewport > Gizmos > Size,
183
+ // `rna_userdef.cc:5415-5419`, range 10..200), and it is px PER GIZMO
184
+ // UNIT: `wm_gizmo.cc:450-474` sets `scale_final = scale_basis *
185
+ // UI_SCALE_FAC * U.gizmo_size * ED_view3d_pixel_size_no_ui_scale(...)`,
186
+ // whose last term is world units per DEVICE px against a
187
+ // `UI_SCALE_FAC` of device px per UI px — so one gizmo unit is 75 CSS
188
+ // px whatever the display scale (read back from the engine's own pin
189
+ // on this box: `ui_scale=2.0 pixel_size=2.0 gizmo_size=75`).
190
+ gizmoSize: 75,
191
+ // A resting handle at 0.6 opacity, a highlighted one opaque in its own colour
192
+ // (`transform_gizmo_3d.cc`, `gizmo_get_axis_color`).
193
+ gizmoOpacity: 0.6,
194
+ // BLENDER'S OUTLINE IS A HARD LINE, and hidden parts are not drawn: four device pixels
195
+ // of orange around the cube in `modeling-object-selected.png` (at 2x), no halo.
196
+ outlineStyle: 'crisp',
197
+ outlineWidth: 4,
198
+ outlineHidden: false,
199
+ // BLENDER'S FLOOR LINES, in device pixels, measured across one scanline of
200
+ // `modeling-object-none.png` at device resolution: the 1 m line is 4 px at half rise and
201
+ // plateaus at 83, the 10 m line is 6 px and plateaus at 101 — so the major line is wider
202
+ // (2.25 against 1.5) and carried (102 − 63) / (84 − 63) past the minor's colour from the
203
+ // background they share. Stated here, the look's, since the editor's own floor is a
204
+ // single hairline level.
205
+ gridLineWidth: 1.5,
206
+ gridMajorWidth: 2.25,
207
+ gridMajorContrast: 39 / 21,
215
208
  },
216
209
  },
217
210
  icons,
218
- regions: {
219
- workspaceTabs: 'shown',
220
- // Blender has no document tab strip over its areas (the host's own note
221
- // in `workspace-presets.ts` says so; the bundle contradicted it).
222
- header: 'shown',
223
- shelf: 'shown',
224
- inspector: 'properties',
225
- // Measured on `modeling.png`: Blender's top-right corner carries two quiet
226
- // wells (Scene, ViewLayer) and nothing brighter than the 216 its labels
227
- // ink at. Ours drew a yellow FPS/ms readout, an orange sparkline and a lit
228
- // audio meter there — the brightest, highest-chroma thing in the frame and
229
- // the first place the eye lands. The numbers keep their door (the Profiler
230
- // utility, which the readout's own tooltip names).
231
- telemetry: 'hidden',
232
- // Measured on `outliner.png`: Scene Collection, Collection, Camera, Cube
233
- // and Light each carry their NAME and nothing after it — no type text
234
- // anywhere in the frame. The type is the GLYPH, which is why the glyph is
235
- // coloured by category in the first place; a row that also spelled the
236
- // type would say it twice. Ours printed `Hero Box ·HeroBox` at
237
- // `content.dim` in a 10px face, one `space-2` past the name.
238
- hierarchyTypeSuffix: 'hidden',
239
- // Measured across all three Outliner frames — `outliner.png` (edit mode),
240
- // `modeling-object-none.png` (object mode, nothing selected) and
241
- // `modeling-object-selected.png` (object mode, everything selected):
242
- // Blender draws NO rule, dotted or solid, under any name in that panel, in
243
- // any selection state. Ours drew an orange dotted rule under every
244
- // component-instance name — six of this scaffold's eleven rows — which is a
245
- // mark with no counterpart in the frame at all. The instance fact keeps its
246
- // doors (the label's tooltip, Go to Callsite / Open Component Source, the
247
- // inspector's component sections); only the paint yields.
248
- hierarchyInstanceRule: 'hidden',
249
- // Measured on `outliner.png`: every object row carries an EYE (CSS ink
250
- // 257.5-270.5, 32 px in from the area's right edge) and a render CAMERA
251
- // (277.5-290.5, 12 px in) on a 20 px column pitch, and no lock glyph
252
- // anywhere — Blender's Disable Selection column is off by default. Ours
253
- // drew a lock 29-41 px in and the eye 6-18 px in, so the LOCK was sitting
254
- // in Blender's eye column and the eye outboard of its camera column. The
255
- // lock goes, and the render column is reserved blank so the eye lands on
256
- // Blender's own; `workspace-regions.ts` carries why it stays blank.
257
- hierarchyRestrictions: 'viewport+render',
258
- // Measured at native 2x on `outliner.png` AND on the Properties area of
259
- // `modeling-edit-none.png` — device x relative to each area's own left
260
- // edge, and the two agree to half a pixel: every Blender area begins with
261
- // an EDITOR-TYPE WELL at x 17..80, i.e. 8.5 CSS in and 32 CSS wide, 20
262
- // tall, its glyph and a chevron inside a 1 px `#3c3c3c` outline over a
263
- // `#272727` inner. It is there whether the area holds one editor or a
264
- // dozen, and its dropdown is how an area changes editor. Ours began with
265
- // a 203 px strip of WORDS, which is what squeezed the Outliner's search
266
- // to 64 px against Blender's 115, truncated the third tab to `Libr`, and
267
- // left the Properties area — a lone panel declaring no header controls —
268
- // with no header band at all.
269
- },
270
211
  palette,
271
212
  };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * BLENDER'S MATERIAL PREVIEW: the scene lit by a world studio light alone, Forest at strength 1
3
+ * and rotation 0, fixed in the world, drawn over the viewport's own colour, in AgX. Read from
4
+ * Blender 5.2's factory View3DShading: `studio_light` Default (forest.exr),
5
+ * `studiolight_intensity` 1, `studiolight_rotate_z` 0, `studiolight_background_alpha` 0,
6
+ * `use_studiolight_view_rotation` (World Space Lighting) on, `use_scene_lights` off.
7
+ * Measured against Blender 5.2's own EEVEE render of the default cube under the same world
8
+ * with its lamp removed.
9
+ * A named view (`@volter/editor-sdk/kit/viewport-presentation` `ViewPreset`); the images are
10
+ * `blender.environment.ts`'s.
11
+ */
12
+ import type { ViewPreset } from '@volter/editor-sdk/kit/viewport-presentation';
13
+
14
+ export const point = 'workspace.view';
15
+ export const view: ViewPreset = {
16
+ id: 'blender-material-preview',
17
+ title: 'Material Preview',
18
+ layer: {
19
+ all: {
20
+ lighting: {
21
+ source: 'preview',
22
+ // Blender's viewport never changes its shading on its own: a lamp in the scene does not
23
+ // take Material Preview over.
24
+ auto: null,
25
+ preview: {
26
+ sceneLights: false,
27
+ sun: { enabled: false },
28
+ environment: { enabled: true, image: 'blender:forest', energy: 1, rotation: 0 },
29
+ },
30
+ tone: { mapper: 'agx', exposure: 1 },
31
+ },
32
+ backdrop: { source: 'fill' },
33
+ },
34
+ },
35
+ };
@@ -7,20 +7,20 @@
7
7
  */
8
8
 
9
9
  import type { WorkspaceLayoutContribution } from '@volter/editor-sdk/looks';
10
+ import { BLENDER_REGIONS } from '../src/regions';
10
11
 
11
12
  export const point = 'workspace.layout';
12
13
  export const layout: WorkspaceLayoutContribution = {
13
14
  id: 'model',
14
15
  title: 'Model',
15
16
  description: 'Blender-shaped modeling: a dominant document, outliner above properties.',
16
- // No `tabs` here ON PURPOSE. The host's default is already `shown`, so
17
- // stating it changed nothing for a bare host — but the WORKSPACE layer wins
18
- // over the style bundle (`workspace-regions.ts`), so restating the default
19
- // silently shadowed `blender.style.ts`'s `tabs: 'hidden'` and the Model
20
- // document still painted a 25px tab row (measured against Blender 5.2, which
21
- // has none). Leave a region unstated unless this workspace genuinely needs
22
- // it different from the host default.
17
+ // No `tabs` here: the workspace layer sits above the person's own choice
18
+ // (`workspace-regions.ts`), so a stated region shadows it. Leave a region
19
+ // unstated unless this workspace genuinely needs it different from the host
20
+ // default.
21
+ // Blender's areas beneath this workspace's own (`../src/regions.ts`).
23
22
  regions: {
23
+ ...BLENDER_REGIONS,
24
24
  header: 'shown',
25
25
  shelf: 'shown',
26
26
  inspector: 'properties',
@@ -16,6 +16,12 @@
16
16
  *
17
17
  * documents: { find: [{ finder: 'modelsFromBlendFiles', include: ['src/models/**\/*.blend'] }] }
18
18
  *
19
+ * THE SESSION'S OWN MODEL. A project that holds no `.blend` of its own still
20
+ * needs ONE model — it is what `blender-start` presents into and what a first
21
+ * bpy call models in, saved to the session's default `models/model.blend` — so
22
+ * when the selection finds no file, the finder lists the standing
23
+ * `blender:runtime` entry instead. Never beside a real model.
24
+ *
19
25
  * This module is a plain object the HOST registers; it mounts no UI. It reads
20
26
  * the project only through `input.files` — the PATHS matching those globs.
21
27
  * Deliberately not `input.sources`: a `.blend` is binary, there is nothing in
@@ -54,6 +60,16 @@ export const finder = {
54
60
  source: { path },
55
61
  });
56
62
  }
63
+ if (entries.length === 0) {
64
+ entries.push({
65
+ id: 'blender:runtime',
66
+ label: 'Model',
67
+ kind: 'model',
68
+ region: null,
69
+ authorable: true,
70
+ reach: { kind: 'root-mount' as const },
71
+ });
72
+ }
57
73
  return { entries, notes };
58
74
  },
59
75
  } as const;
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { WorkspaceLayoutContribution } from '@volter/editor-sdk/looks';
9
+ import { BLENDER_REGIONS } from '../src/regions';
9
10
 
10
11
  export const point = 'workspace.layout';
11
12
  export const layout: WorkspaceLayoutContribution = {
@@ -16,7 +17,9 @@ export const layout: WorkspaceLayoutContribution = {
16
17
  requires: { documentKind: 'model' },
17
18
  // `tabs` unstated — see `model.layout.ts`: the workspace layer wins over the
18
19
  // style bundle, so restating the host default shadows it.
20
+ // Blender's areas beneath this workspace's own (`../src/regions.ts`).
19
21
  regions: {
22
+ ...BLENDER_REGIONS,
20
23
  header: 'shown',
21
24
  shelf: 'shown',
22
25
  inspector: 'properties',
@@ -53,6 +53,7 @@
53
53
  */
54
54
 
55
55
  import type { WorkspaceLayoutContribution } from '@volter/editor-sdk/looks';
56
+ import { BLENDER_REGIONS } from '../src/regions';
56
57
 
57
58
  export const point = 'workspace.layout';
58
59
  export const layout: WorkspaceLayoutContribution = {
@@ -64,7 +65,9 @@ export const layout: WorkspaceLayoutContribution = {
64
65
  // layer wins over the style bundle, so restating the host default would
65
66
  // silently shadow `blender.style.ts`'s `tabs: 'hidden'` and paint a tab row
66
67
  // Blender does not have.
68
+ // Blender's areas beneath this workspace's own (`../src/regions.ts`).
67
69
  regions: {
70
+ ...BLENDER_REGIONS,
68
71
  header: 'shown',
69
72
  shelf: 'shown',
70
73
  inspector: 'properties',
@@ -0,0 +1,15 @@
1
+ All HDRIs are licensed as CC0.
2
+
3
+ These were created by Greg Zaal (Poly Haven https://polyhaven.com).
4
+ Originals used for each HDRI:
5
+ - City: https://polyhaven.com/a/portland_landing_pad
6
+ - Courtyard: https://polyhaven.com/a/courtyard
7
+ - Forest: https://polyhaven.com/a/ninomaru_teien
8
+ - Interior: https://polyhaven.com/a/hotel_room
9
+ - Night: Probably https://polyhaven.com/a/moonless_golf
10
+ - Studio: Probably https://polyhaven.com/a/studio_small_01
11
+ - Sunrise: https://polyhaven.com/a/spruit_sunrise
12
+ - Sunset: https://polyhaven.com/a/venice_sunset
13
+
14
+ 1K resolution of each was taken, and compressed with oiiotool:
15
+ oiiotool input.exr --ch R,G,B -d float --compression dwab:300 --clamp:min=0.0:max=32000.0 -o output.exr
@@ -46,6 +46,7 @@
46
46
  */
47
47
 
48
48
  import type { WorkspaceLayoutContribution } from '@volter/editor-sdk/looks';
49
+ import { BLENDER_REGIONS } from '../src/regions';
49
50
 
50
51
  export const point = 'workspace.layout';
51
52
  export const layout: WorkspaceLayoutContribution = {
@@ -57,7 +58,9 @@ export const layout: WorkspaceLayoutContribution = {
57
58
  // layer wins over the style bundle, so restating the host default would
58
59
  // silently shadow `blender.style.ts`'s `tabs: 'hidden'` and paint a tab row
59
60
  // Blender does not have.
61
+ // Blender's areas beneath this workspace's own (`../src/regions.ts`).
60
62
  regions: {
63
+ ...BLENDER_REGIONS,
61
64
  header: 'shown',
62
65
  shelf: 'shown',
63
66
  inspector: 'properties',