@volter/editor-blender 0.1.150 → 0.1.151

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 (93) hide show
  1. package/LICENSE +18 -9
  2. package/README.md +5 -5
  3. package/contributions/blender-animation-editors.tsx +17 -69
  4. package/contributions/blender-display-settings.ts +36 -0
  5. package/contributions/blender-export-gltf.ts +228 -0
  6. package/contributions/blender-export.verbs.ts +39 -0
  7. package/contributions/blender-gltf-export.verbs.ts +312 -0
  8. package/contributions/blender-header-menus.tsx +37 -133
  9. package/contributions/blender-icon-trace.mjs +8 -98
  10. package/contributions/blender-icons.source.mjs +96 -860
  11. package/contributions/blender-mixer-pose.ts +369 -0
  12. package/contributions/blender-model-opening.tsx +3 -7
  13. package/contributions/blender-node-editor.document.tsx +28 -180
  14. package/contributions/blender-node-geometry.ts +44 -230
  15. package/contributions/blender-node-panels.source.mjs +7 -121
  16. package/contributions/blender-outliner-authoring.ts +61 -415
  17. package/contributions/blender-outliner-model.ts +15 -112
  18. package/contributions/blender-play-area.tsx +100 -0
  19. package/contributions/blender-playback-movie.ts +409 -0
  20. package/contributions/blender-pose.ts +18 -368
  21. package/contributions/blender-properties-context.ts +1 -1
  22. package/contributions/blender-properties-model.ts +15 -90
  23. package/contributions/blender-properties-tab.tsx +3 -50
  24. package/contributions/blender-properties-view.tsx +15 -147
  25. package/contributions/blender-remedy.ts +14 -0
  26. package/contributions/blender-runtime-skin.ts +103 -99
  27. package/contributions/blender-runtime.document.tsx +217 -339
  28. package/contributions/blender-scene-mixer.ts +51 -0
  29. package/contributions/blender-screenshot.service.ts +28 -0
  30. package/contributions/blender-timeline-geometry.ts +16 -136
  31. package/contributions/blender-timeline.document.tsx +142 -332
  32. package/contributions/blender-uv-editor.document.tsx +48 -112
  33. package/contributions/blender-uv-geometry.ts +14 -179
  34. package/contributions/blender-version.status.tsx +43 -23
  35. package/contributions/blender.environment.ts +2 -2
  36. package/contributions/blender.icons.json +0 -9
  37. package/contributions/blender.keymap.ts +9 -51
  38. package/contributions/blender.node-panels.json +4 -13
  39. package/contributions/blender.page.ts +63 -0
  40. package/contributions/blender.status.tsx +9 -145
  41. package/contributions/blender.style.ts +25 -152
  42. package/contributions/blender.verbs.ts +52 -0
  43. package/contributions/model.layout.ts +2 -34
  44. package/contributions/models.finder.ts +12 -32
  45. package/contributions/properties-bone-constraints.inspector.tsx +1 -16
  46. package/contributions/properties-bone.inspector.tsx +2 -31
  47. package/contributions/properties-collection.inspector.tsx +1 -34
  48. package/contributions/properties-constraints.inspector.tsx +1 -21
  49. package/contributions/properties-data.inspector.tsx +2 -39
  50. package/contributions/properties-material.inspector.tsx +2 -33
  51. package/contributions/properties-modifiers.inspector.tsx +2 -30
  52. package/contributions/properties-object.inspector.tsx +2 -44
  53. package/contributions/properties-output.inspector.tsx +1 -27
  54. package/contributions/properties-particles.inspector.tsx +2 -46
  55. package/contributions/properties-physics.inspector.tsx +2 -41
  56. package/contributions/properties-render.inspector.tsx +1 -45
  57. package/contributions/properties-scene.inspector.tsx +1 -35
  58. package/contributions/properties-texture.inspector.tsx +1 -49
  59. package/contributions/properties-view-layer.inspector.tsx +1 -29
  60. package/contributions/properties-world.inspector.tsx +2 -39
  61. package/contributions/shading.layout.ts +3 -78
  62. package/contributions/uv-editing.layout.ts +3 -70
  63. package/dist-node/serving.mjs +13 -30
  64. package/host/blender-runtime-host.ts +541 -372
  65. package/host/scene-sound.ts +143 -0
  66. package/node/blender-mcp.ts +49 -93
  67. package/package.json +1209 -46
  68. package/producticons/blender-icons.otf +0 -0
  69. package/producticons/blender-product-icon-theme.json +129 -0
  70. package/serving/blender-routes.ts +9 -43
  71. package/serving/blender-wasm-artifact.ts +29 -166
  72. package/serving/index.ts +1 -1
  73. package/src/api.ts +9 -0
  74. package/src/layouts.tsx +1 -17
  75. package/src/looks.ts +1 -12
  76. package/src/model-document-saving.ts +25 -0
  77. package/src/model-play-extension.ts +173 -0
  78. package/src/node-view-state.ts +3 -25
  79. package/src/play-mode.ts +64 -38
  80. package/src/presentation.ts +6 -18
  81. package/src/regions.ts +3 -43
  82. package/src/start-document.ts +15 -0
  83. package/src/timeline-view-state.ts +4 -39
  84. package/src/uv-view-state.ts +4 -20
  85. package/themes/blender.json +299 -0
  86. package/view/blender-view-routes.ts +1 -1
  87. package/contributions/blender-game-panel.tsx +0 -809
  88. package/contributions/blender-palette.source.mjs +0 -350
  89. package/contributions/blender-play-skin.ts +0 -508
  90. package/contributions/blender-screenshot.menu.ts +0 -32
  91. package/contributions/blender.command.ts +0 -109
  92. package/contributions/blender.palette.json +0 -102
  93. package/src/play-live.ts +0 -27
@@ -1,39 +1,4 @@
1
- /**
2
- * ONE PROPERTIES TAB IS ONE `selection.inspector` CONTRIBUTION — the factory
3
- * every `properties-*.inspector.tsx` in this package is built from (WORK.md
4
- * §Blender in the tab is Blender, "Inspection parity", I1, item 3).
5
- *
6
- * WHY A TAB IS A SECTION, and not a rail of our own: under the `properties`
7
- * presentation the inspector's sections ARE the vertical icon rail, 1:1
8
- * (`packages/sdk/src/kit/components/InspectionProjection.tsx`,
9
- * `PropertiesColumn`). So the way to make Blender's tab rail appear is to
10
- * contribute Blender's tabs as sections — which is also exactly the shape I2
11
- * needs, because each of these modules then grows a CURATED body over the same
12
- * door while the generic view stays beneath it.
13
- *
14
- * WHICH TABS STAND is `buttons_context.cc`'s answer, computed in the engine
15
- * (`session.py::rna_context`) and read here. Two kinds of tab:
16
- *
17
- * - `standing: 'any' | 'object'` — a tab `buttons_context.cc` builds a path
18
- * to UNCONDITIONALLY given a scene (`buttons_context_path_scene`,
19
- * `_view_layer`, `_world`) or given an object at all
20
- * (`buttons_context_path_object`, of which only the OBJECT tab is standing
21
- * here — Physics and Constraints share that path but are context-gated,
22
- * because Blender's Properties editor follows the ACTIVE OBJECT rather
23
- * than the row that was clicked, and only the door knows there is one).
24
- * These match from the SELECTION alone, synchronously.
25
- * - everything else — Modifiers (an object type set), Particles (a mesh),
26
- * Object Data (the object has data), Bone (an active bone), Material,
27
- * Collection, Texture — which only the engine can answer, so they match
28
- * against the context the door last returned.
29
- *
30
- * THE STANDING TABS EXIST SO THE RAIL IS NEVER EMPTY: `match()` is
31
- * synchronous and the first composition has read nothing, so if every tab
32
- * waited on the door there would be no rail at all. The READ itself is driven
33
- * from `match` (see the note there), and when its answer lands it calls
34
- * `documents.contextChanged`, which re-derives the whole inspection and brings
35
- * the rest of the rail in.
36
- */
1
+ /** Build one selection.inspector contribution per Blender Properties tab. Standing scene/object tabs match synchronously; context-dependent tabs use the engine’s answer. Matchers drive the shared read, whose publication recomposes the rail; only the active tab mounts its body. */
37
2
 
38
3
  import type {
39
4
  ToolContributionNode,
@@ -52,11 +17,7 @@ export interface BlenderPropertiesTab {
52
17
  /** Whether this tab needs the engine's answer to exist, or stands on the
53
18
  * selection alone — see the header. */
54
19
  readonly standing?: 'any' | 'object';
55
- /** BLENDER'S OWN PANELS for this tab, in Blender's order, transcribed from
56
- * the matching `scripts/startup/bl_ui/properties_*.py`
57
- * ({@link BlenderCuratedPanel}). The generic RNA view stays beneath them
58
- * under "All properties"; a tab that declares none is the generic view
59
- * alone, which is what I1 shipped. */
20
+ /** Curated panels follow Blender’s panel order. The generic RNA view remains beneath them under All properties. */
60
21
  readonly curated?: readonly BlenderCuratedPanel[];
61
22
  }
62
23
 
@@ -64,15 +25,7 @@ export function blenderPropertiesTabMatch(tab: BlenderPropertiesTab) {
64
25
  return (node: ToolContributionNode | null, adapter: unknown): boolean => {
65
26
  const subject = resolveBlenderSubject(node, adapter);
66
27
  if (subject === null) return false;
67
- // THE READ IS DRIVEN FROM `match`, NOT FROM A BODY, and that is a
68
- // correction the live walk forced: under the `properties` presentation
69
- // the host mounts ONLY THE ACTIVE TAB's body, so a read driven from a
70
- // body never happened at all unless the person had already clicked a
71
- // Blender tab — the rail showed the standing tabs and nothing else, for
72
- // good (measured 2026-09-19). A matcher runs on every composition,
73
- // active tab or not, so this is the one place that always sees the
74
- // subject. It is idempotent by construction: `showBlenderSubject`
75
- // returns immediately unless the subject moved.
28
+ // Drive the idempotent subject read from match, which runs for every composition even when this tab’s body is unmounted.
76
29
  showBlenderSubject(subject);
77
30
  if (tab.standing === 'any') return true;
78
31
  if (tab.standing === 'object') return subject.kind === 'object';
@@ -1,50 +1,4 @@
1
- /**
2
- * THE GENERIC PROPERTIES VIEW — every `bl_rna` property of the active
3
- * datablock, drawn in OUR panels (WORK.md §Blender in the tab is Blender,
4
- * "Inspection parity", I1; ARCHITECTURE-CORE §Blender north star, "Inspection
5
- * parity, not editing parity"). Blender's UI layer is never run, ported or
6
- * recorded: what arrives is `session.py`'s RNA rows, and what is drawn here is
7
- * this file's own widgets.
8
- *
9
- * ## The geometry is Blender's SOURCE, not an eyedropper
10
- *
11
- * Every number below cites the file and constant it came from, read at the
12
- * engine's own pin (Blender 5.2.0, `fbe6228777e7`, `~/volter/blender-src`).
13
- * That is the rule as of 2026-09-19: a theme value or a metric traces to
14
- * `userdef_default_theme.c` / `interface_*.cc`, and a frame is what confirms
15
- * the result rather than what supplies the number.
16
- *
17
- * UI_UNIT_Y 20 `UI_interface_c.hh:2346` (`U.widget_unit`, 1x)
18
- * row gap 2 `interface_style.cc:110` (`buttonspacey`)
19
- * label column 40% `interface_layout.cc:79` (UI_ITEM_PROP_SEP_DIVIDE)
20
- * label alignment right `interface_layout.cc:3394` (LayoutAlign::Right)
21
- * panel header 25 `interface_intern.hh:152` (PNL_HEADER = 1.25 UI_UNIT_Y)
22
- * panel label x 20 `interface_panel.cc:1054` (UI_UNIT_X * 1.0)
23
- * sub-panel indent 14 `interface_panel.cc:1058` (+0.7 UI_UNIT_X)
24
- * panel padding 8 `interface_style.cc:111` (`panelspace`)
25
- * panel radius 4 `interface_panel.cc:1103` (panel_roundness 0.4 x unit x 0.5)
26
- * widget radius 0.2 x h `interface_widgets.cc` widget_radius_from_rcti,
27
- * `userdef_default_theme.c:100` wcol_num.roundness
28
- * text margin 8 `interface_intern.hh:1503` (UI_TEXT_MARGIN_X 0.4 x unit)
29
- * font 11 `UI_interface_c.hh:425` (UI_DEFAULT_TEXT_POINTS)
30
- * read-only alpha 0.5 `interface_widgets.cc` widget_alpha_factor /
31
- * widget_color_disabled — EVERY widget colour at
32
- * half alpha, which is why a read-only row here is
33
- * DIMMED rather than a disabled control.
34
- *
35
- * Colours, from `release/datafiles/userdef/userdef_default_theme.c`:
36
- * panel header/back #3d3d3d :281,:282 sub-panel back #0000001f :283
37
- * panel outline #ffffff11 :284 panel title #e6e6e6 :285
38
- * widget inner #545454 :95 widget outline #3d3d3d :93
39
- * widget text #e6e6e6 :98 number slider #4772b3 :97
40
- * checkbox mark #ffffff :77 (wcol_option.item)
41
- *
42
- * The values are literal rather than `var(--volter-…)` because the palette is
43
- * the FRAME's grading (U8/U9) and eyedropped: `blender.palette.json` carries
44
- * `surface.panel` #2f2f2f where the source says `.space_properties.back` is
45
- * #303030, and `content.primary` #e5e5e5 against `.text` #e6e6e6. Where a
46
- * token and the source agree, the token is used.
47
- */
1
+
48
2
 
49
3
  import type {
50
4
  BlenderRnaCollectionValue,
@@ -98,25 +52,7 @@ const TRIANGLE_GAP = 6;
98
52
  * is 0.2 for every widget this view draws. */
99
53
  const widgetRadius = (height: number) => Math.round(height * 0.2);
100
54
 
101
- /**
102
- * WHAT BLENDER DRAWS NOWHERE, WE DRAW NOWHERE — RNA's own `PROP_HIDDEN`
103
- * (`BlenderRnaRow.hidden`, `rna_rna.cc:795-799`). The door reports the whole
104
- * surface and this is the one place it is narrowed, so "hidden" stays
105
- * Blender's answer rather than a list of ours.
106
- *
107
- * AND THE SECOND RULE, ruled on after I2 measured it: UNDRAWN STATE
108
- * (`BlenderRnaRow.undrawn`). The `PROP_HIDDEN` flag above does NOT cover a
109
- * modifier's panel-open booleans (`open_advanced_panel` and its siblings) —
110
- * there is not one `PROP_HIDDEN` in the whole of `rna_modifier.cc`, because
111
- * `rna_def_modifier_panel_open_prop` (`rna_modifier.cc:2694-2705`) sets
112
- * `PROP_NO_DEG_UPDATE` and an sdna bit and no flag. What RNA does say is that
113
- * they carry no `RNA_def_property_ui_text` at all, so
114
- * `rna_define.cc:1311-1312`'s definition-time defaults still stand: `name`
115
- * equal to the identifier, `description` empty. A property Blender never gave
116
- * UI text to is a property Blender never meant to draw, and the generic view
117
- * omits it here for exactly the same reason it omits a hidden one. The
118
- * curated lists need no change: `bl_ui` names none of them.
119
- */
55
+ /** Omit RNA rows marked hidden or undrawn. Undrawn properties have no UI text; curated lists do not name them either. */
120
56
  const shown = (rows: readonly BlenderRnaRow[]): readonly BlenderRnaRow[] =>
121
57
  rows.filter((row) => row.hidden !== true && row.undrawn !== true);
122
58
 
@@ -235,12 +171,7 @@ function Row({
235
171
  }) {
236
172
  return (
237
173
  <div
238
- // WHAT THIS ROW IS, published so it can be READ. The rail is a VS Code
239
- // view (`editor-document-probe.ts` scope `'rail'`), and before these
240
- // three attributes a reading of it could recover a label only as the
241
- // row's text and could not say which RNA property that label belonged
242
- // to or whether the field was writable. They change nothing about what
243
- // is drawn.
174
+ // Publish the RNA property identity and writability for the rail’s document probe.
244
175
  data-rna-field={identifier ?? ''}
245
176
  data-field-label={label}
246
177
  data-field-readonly={readonly ? 'true' : undefined}
@@ -446,7 +377,7 @@ function colourBytes(values: readonly number[], gamma: boolean): [number, number
446
377
  return [byte(0), byte(1), byte(2)];
447
378
  }
448
379
 
449
- function colourOf(values: readonly number[], gamma = false): string {
380
+ function colourOf(values: readonly number[], gamma: boolean): string {
450
381
  const [r, g, b] = colourBytes(values, gamma);
451
382
  return `rgb(${r}, ${g}, ${b})`;
452
383
  }
@@ -523,11 +454,10 @@ function StringWidget({ row, write }: WidgetProps) {
523
454
  );
524
455
  }
525
456
 
526
- /** A COLOR subtype is a swatch, not three numbers — `PropertyRNA.subtype` is
527
- * exactly what says so, and it is why the door carries `subtype` at all. */
528
457
  /** BLENDER'S COLOUR FIELD: the whole-width swatch, and a click opens a picker under it
529
458
  * (`ui_block_colorpicker`); the colour is written when the pick ends, one step, as Blender's
530
- * popup pushes one undo when it closes. */
459
+ * popup pushes one undo when it closes. `PropertyRNA.subtype` selects this swatch
460
+ * presentation for COLOR values instead of separate numeric fields. */
531
461
  function ColourWidget({ row, write }: WidgetProps) {
532
462
  const values = (row.value as readonly number[] | null) ?? [];
533
463
  const gamma = row.subtype === 'COLOR_GAMMA';
@@ -566,23 +496,7 @@ function ColourWidget({ row, write }: WidgetProps) {
566
496
  );
567
497
  }
568
498
 
569
- /** Blender stacks an array's components as their own rows, labelled by
570
- * `RNA_property_array_item_char` (X/Y/Z/W) and numbered past four.
571
- *
572
- * A BOOLEAN ARRAY IS AN ARRAY, and that is why this function dispatches on
573
- * the element type rather than being numeric-only. `Object.lock_location` is
574
- * three booleans, and Blender draws all three: `properties_object.py:49`
575
- * (`OBJECT_PT_transform`) is `row.prop(ob, "lock_location", text="",
576
- * emboss=False, icon='DECORATE_UNLOCKED')` — a whole-array `prop()` beside
577
- * the location vector, which Blender's layout engine expands into one lock
578
- * toggle per component, stacked alongside the three number fields (the same
579
- * shape at `:59`/`:74` for `lock_rotation` and `:83` for `lock_scale`).
580
- * Measured 2026-09-21 in the rail before this: `lock_location` fell to the
581
- * single-checkbox arm of {@link widgetFor}, so ONE unchecked box stood for
582
- * three values — and its write was `setattr(ob, "lock_location", true)`, a
583
- * scalar into a three-element array, which is a refusal rather than the lock
584
- * the person asked for. Each toggle here writes its own index (the door's
585
- * `index` argument, `session.py::rna_set`'s `getattr(target, id)[index]`). */
499
+ /** Draw array elements as separate rows using RNA component labels. Boolean arrays write an individual index through rna_set, just as numeric arrays do. */
586
500
  function ArrayRows({ row, path, label, title, write, locks }: WidgetProps & {
587
501
  label: string; title: string;
588
502
  locks?: readonly { row: BlenderRnaRow; index?: number }[] | undefined;
@@ -793,27 +707,7 @@ function CollectionRow({
793
707
 
794
708
  // ---- the curated body ------------------------------------------------------
795
709
 
796
- /**
797
- * ONE OF BLENDER'S OWN PANELS, as data.
798
- *
799
- * A tab module declares a list of these and the view draws them ABOVE the
800
- * generic RNA view, which stays beneath under "All properties". The list is a
801
- * transcription of the matching `scripts/startup/bl_ui/properties_*.py`:
802
- * `bl_label` is the title, each `layout.prop(ob, "x")` is an entry of
803
- * {@link properties} IN BLENDER'S ORDER, and a `bl_parent_id` panel is a
804
- * {@link sub}. Blender's UI layer is never run, ported or recorded
805
- * (ARCHITECTURE-CORE §Blender north star) — the Python file is read as the
806
- * SPECIFICATION of which properties this tab shows and in what order, and
807
- * what draws them is this file's own widgets over the RNA door's rows.
808
- *
809
- * WHAT IS DELIBERATELY NOT TRANSCRIBED: the `poll`/`if` conditions Blender's
810
- * draw functions carry. A `properties_object.py` panel hides
811
- * `show_all_edges` for a non-mesh by branching in Python; here the property is
812
- * simply NOT ON the datablock RNA answered with, so it does not render and
813
- * the panel header says how many of Blender's it found. That is the same
814
- * outcome by a mechanism that cannot go stale — a transcribed condition is a
815
- * copy of Blender's logic, which is the one thing the ruling forbids.
816
- */
710
+
817
711
  /**
818
712
  * ONE ENTRY OF A PANEL'S PROPERTY LIST.
819
713
  *
@@ -924,19 +818,7 @@ export interface BlenderCuratedPanel {
924
818
  readonly active?: keyof BlenderRnaContext;
925
819
  }
926
820
 
927
- /**
928
- * WHICH MEMBER OF THIS COLLECTION THE CONTEXT CALLS ACTIVE — by address when
929
- * RNA gives the member one, and by INDEX when it does not.
930
- *
931
- * The index half is not a convenience, it is the only answer for two of the
932
- * three lists that need one, and the live walk is what said so (2026-09-19):
933
- * a `VertexGroup` has no `path_from_id()` at all — I1 recorded that — so
934
- * `activeVertexGroup` carries only its index, and `activeMaterial` carries the
935
- * MATERIAL's address while the list being drawn is of material SLOTS, so its
936
- * path never matches a row. Both marked nothing. `rna_context` reports
937
- * `index` for exactly those two (`session.py::rna_context`), which is what
938
- * Blender's own `template_list` highlights by.
939
- */
821
+ /** Resolve active collection members by RNA address or index. Vertex groups have no path_from_id and material context names a material rather than its slot, so both use the context’s index. */
940
822
  function activeMember(panel: BlenderCuratedPanel): { path: string | null; index: number | null } {
941
823
  if (panel.active === undefined) return { path: null, index: null };
942
824
  const handle = blenderPropertiesState().context?.[panel.active];
@@ -1010,12 +892,12 @@ function CuratedCollectionPanel({
1010
892
  function CuratedRows({
1011
893
  path,
1012
894
  properties,
1013
- andDeclared = false,
895
+ andDeclared,
1014
896
  onOpen,
1015
897
  }: {
1016
898
  readonly path: string;
1017
899
  readonly properties: readonly BlenderCuratedProperty[];
1018
- readonly andDeclared?: boolean;
900
+ readonly andDeclared: boolean;
1019
901
  readonly onOpen: (path: string) => void;
1020
902
  }) {
1021
903
  useBlenderProperties();
@@ -1170,12 +1052,7 @@ function CuratedPanel({
1170
1052
  <PropertyRow key={item.identifier} row={item.row} path={item.path} onOpen={onOpen}
1171
1053
  label={item.label} locks={item.locks} />
1172
1054
  ))}
1173
- {/* KEYED BY POSITION, NOT BY TITLE: Blender ships two panels with the
1174
- same `bl_label` under different engines and expects the poll to pick
1175
- one — Render's "Sampling" (EEVEE `:675`, Workbench `:1004`) and
1176
- "Film" (`:757`, `:1025`), View Layer's "Data" (`:111`, `:137`). A
1177
- title key made those React duplicates, which the live walk found by
1178
- name ("Encountered two children with the same key"). */}
1055
+ {/* Key by position: engine-specific panels can share a title. */}
1179
1056
  {sub.map((child, index) => (
1180
1057
  <CuratedPanel key={`${index}:${child.title}`} panel={child} paths={paths} onOpen={onOpen} />
1181
1058
  ))}
@@ -1240,27 +1117,18 @@ function DatablockView({
1240
1117
 
1241
1118
  // ---- the section body ------------------------------------------------------
1242
1119
 
1243
- /**
1244
- * ONE PROPERTIES TAB'S BODY. `tabId` names the tab this section IS; the
1245
- * context door says which datablocks that tab shows and in what order
1246
- * (`session.py::rna_context`, mirrored from `buttons_context.cc`).
1247
- *
1248
- * A pointer row DRILLS: clicking it replaces the shown datablock with the
1249
- * target, and the breadcrumb walks back. That is the "pointer → the target's
1250
- * name as a link that changes the active datablock" half of I1, and it is what
1251
- * makes `object.data.materials[0].node_tree` reachable with no per-tab code.
1252
- */
1120
+ /** One Properties tab body over the datablocks and ordering returned by rna_context. Pointer rows drill into a target datablock and breadcrumbs return to the parent. */
1253
1121
  export function BlenderPropertiesSection({
1254
1122
  tabId,
1255
1123
  subject,
1256
- curated = [],
1124
+ curated,
1257
1125
  }: {
1258
1126
  readonly tabId: string;
1259
1127
  readonly subject: BlenderSubject;
1260
1128
  /** Blender's own panels for this tab, in Blender's order
1261
1129
  * ({@link BlenderCuratedPanel}). Empty — the tab has not been curated yet —
1262
1130
  * and the generic view is the whole body, uncollapsed. */
1263
- readonly curated?: readonly BlenderCuratedPanel[];
1131
+ readonly curated: readonly BlenderCuratedPanel[];
1264
1132
  }) {
1265
1133
  useBlenderProperties();
1266
1134
  useEffect(() => showBlenderSubject(subject), [subject]);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * WHAT TO DO ABOUT WHAT PLAYS ONLY IN BLENDER, said after a warning that names it, by the Timeline
3
+ * (`blender-runtime-skin.ts`). Pure: it reads no Blender host.
4
+ */
5
+ export function remedy(unsupported: readonly string[]): string {
6
+ const out: string[] = [];
7
+ if (unsupported.some((thing) => thing.includes(' constraint ') || thing.includes('drivers')))
8
+ out.push('Bake a constraint or driver into the action (Pose ▸ Animation ▸ Bake Action, Visual Keying), or aim with a Damped Track.');
9
+ if (unsupported.some((thing) => thing.includes('curves on ')))
10
+ out.push("Key the motion on a bone (the root bone) instead of the armature object, or move the object from the game.");
11
+ if (unsupported.some((thing) => thing.includes('interpolation') || thing.includes(' modifier')))
12
+ out.push('Use Bezier, Linear or Constant keys and the Cycles modifier only, or bake the curves (Key ▸ Bake Keyframes).');
13
+ return out.length ? ` ${out.join(' ')}` : '';
14
+ }