@volter/editor-blender 0.1.29 → 0.1.31

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.
@@ -76,6 +76,7 @@ import type {
76
76
  } from '@volter/editor-project/adapter';
77
77
  import type * as THREE from 'three';
78
78
  import { threeObject } from '@volter/editor-threejs/adapter/three-contract';
79
+ import { viewportStages } from '@volter/editor-threejs/viewport-door';
79
80
  import { blenderExecute, blenderRnaSet, beginBlenderGesture, endBlenderGesture } from '../host/blender-runtime-host';
80
81
  import {
81
82
  blenderEngineSelection,
@@ -1602,6 +1603,29 @@ export function blenderOutlinerAuthoringFor(
1602
1603
  * callback in Code-OSS. These operators are undone by restoring Blender's
1603
1604
  * state, never by constructing an inverse operator or replaying the script.
1604
1605
  */
1606
+ /**
1607
+ * ADD ▸ CAMERA LOOKS THE WAY THE VIEW LOOKS, AND IS THE SCENE'S CAMERA WHEN THERE IS NONE: what
1608
+ * `object_camera_add_exec` does with the 3D View it runs in (`object_add.cc`: `align` forced to
1609
+ * VIEW; `v3d->scenelock && scene->camera == nullptr` gives `scene->camera = ob`). This engine
1610
+ * runs the operator with no View3D, so both come from here: the area's own view, set on the new
1611
+ * camera the way the gizmo writes a pose (`blenderWorldMatrixRows`) with the operator's
1612
+ * location (the 3D cursor) kept, and the scene camera after it.
1613
+ */
1614
+ const cameraAddCall = (): string => {
1615
+ const areaId = (defaultAdapter as { documentId?: string }).documentId?.replace(/^object3d-document:/, '');
1616
+ const camera = viewportStages().find((one) => one.documentId === areaId)?.rig().drawCamera() ?? null;
1617
+ const view = presented();
1618
+ const lines = ['bpy.ops.object.camera_add()', 'added = bpy.context.view_layer.objects.active'];
1619
+ if (camera !== null && view !== null)
1620
+ lines.push(
1621
+ 'kept = added.location.copy()',
1622
+ `added.matrix_world = ${JSON.stringify(blenderWorldMatrixRows(camera, view.root))}`,
1623
+ 'added.location = kept',
1624
+ );
1625
+ lines.push('if bpy.context.scene.camera is None:', ' bpy.context.scene.camera = added');
1626
+ return lines.join('\n');
1627
+ };
1628
+
1605
1629
  const structure: StructureProvider = {
1606
1630
  /**
1607
1631
  * BLENDER'S ADD MENU, FOR EVERY PARENT — `parentId` is deliberately not
@@ -1633,7 +1657,7 @@ export function blenderOutlinerAuthoringFor(
1633
1657
  // with this same sentence, so this arm is what answers a caller reaching
1634
1658
  // the kind through the door instead of the menu.
1635
1659
  if (entry.refusal !== undefined) return { id: '', ack: refuse(entry.refusal) };
1636
- const call = addCall(entry);
1660
+ const call = entry.call === 'object.camera_add' ? cameraAddCall() : addCall(entry);
1637
1661
  return {
1638
1662
  id: '',
1639
1663
  ack: (async (): Promise<WriteAck> => {
@@ -54,6 +54,7 @@ import type {
54
54
  BlenderRnaView,
55
55
  } from '@volter/blender-engine/browser/rna';
56
56
  import { useEffect, useState, useSyncExternalStore } from 'react';
57
+ import { ColorPicker, hexToRgb, parseAlpha, toHex } from '@volter/editor-sdk/widgets';
57
58
  import {
58
59
  type BlenderSubject,
59
60
  blenderPropertiesState,
@@ -414,10 +415,27 @@ function fieldName(row: BlenderRnaRow, index?: number): string {
414
415
  return index === undefined ? name : `${name} ${arrayItemChar(row.subtype, index) ?? index}`;
415
416
  }
416
417
 
417
- function colourOf(values: readonly number[]): string {
418
+ /** A colour property's display bytes: a `COLOR` is scene-linear and is shown through the display
419
+ * curve, a `COLOR_GAMMA` already holds display values. */
420
+ function colourBytes(values: readonly number[], gamma: boolean): [number, number, number] {
418
421
  const byte = (channel: number) =>
419
- Math.max(0, Math.min(255, Math.round((values[channel] ?? 0) ** (1 / 2.2) * 255)));
420
- return `rgb(${byte(0)}, ${byte(1)}, ${byte(2)})`;
422
+ Math.max(0, Math.min(255, Math.round(Math.max(0, values[channel] ?? 0) ** (gamma ? 1 : 1 / 2.2) * 255)));
423
+ return [byte(0), byte(1), byte(2)];
424
+ }
425
+
426
+ function colourOf(values: readonly number[], gamma = false): string {
427
+ const [r, g, b] = colourBytes(values, gamma);
428
+ return `rgb(${r}, ${g}, ${b})`;
429
+ }
430
+
431
+ /** The picker's colour back as the property's values, alpha kept where the picker has none. */
432
+ function colourValues(color: string, previous: readonly number[], gamma: boolean): number[] {
433
+ const channel = (byte: number) => (gamma ? byte / 255 : (byte / 255) ** 2.2);
434
+ const [r, g, b] = hexToRgb(toHex(color)).map(channel) as [number, number, number];
435
+ const next = [r, g, b];
436
+ const carriesAlpha = /^(rgba|hsla)\(/i.test(color) || /^#[0-9a-f]{8}$/i.test(color);
437
+ if (previous.length > 3) next.push(carriesAlpha ? parseAlpha(color) : (previous[3] ?? 1));
438
+ return next;
421
439
  }
422
440
 
423
441
  /** A property's widget, by RNA TYPE — the dispatcher is one `switch` and each
@@ -484,13 +502,44 @@ function StringWidget({ row, write }: WidgetProps) {
484
502
 
485
503
  /** A COLOR subtype is a swatch, not three numbers — `PropertyRNA.subtype` is
486
504
  * exactly what says so, and it is why the door carries `subtype` at all. */
487
- function ColourWidget({ row }: WidgetProps) {
505
+ /** BLENDER'S COLOUR FIELD: the whole-width swatch, and a click opens a picker under it
506
+ * (`ui_block_colorpicker`); the colour is written when the pick ends, one step, as Blender's
507
+ * popup pushes one undo when it closes. */
508
+ function ColourWidget({ row, write }: WidgetProps) {
488
509
  const values = (row.value as readonly number[] | null) ?? [];
510
+ const gamma = row.subtype === 'COLOR_GAMMA';
511
+ const [open, setOpen] = useState(false);
512
+ const [r, g, b] = colourBytes(values, gamma);
513
+ const alpha = values.length > 3 ? (values[3] ?? 1) : 1;
514
+ const shown = alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})` : colourOf(values, gamma);
489
515
  return (
490
- <span
491
- style={{ ...fieldStyle, background: colourOf(values), display: 'block' }}
492
- title={values.map((value) => value.toFixed(3)).join(', ')}
493
- />
516
+ <span style={{ position: 'relative', display: 'block' }}>
517
+ <button
518
+ type="button"
519
+ aria-label={row.name}
520
+ aria-expanded={open}
521
+ disabled={row.readonly}
522
+ onClick={() => setOpen(!open)}
523
+ style={{
524
+ ...fieldStyle,
525
+ background: colourOf(values, gamma),
526
+ display: 'block',
527
+ width: '100%',
528
+ cursor: row.readonly ? 'default' : 'pointer',
529
+ }}
530
+ title={values.map((value) => value.toFixed(3)).join(', ')}
531
+ />
532
+ {open ? (
533
+ <div style={{ position: 'absolute', zIndex: 'var(--volter-z-dropdown)', top: '100%', right: 0, marginTop: 4 }}>
534
+ <ColorPicker
535
+ value={shown}
536
+ onChange={() => {}}
537
+ onChangeEnd={(color) => write(colourValues(color, values, gamma))}
538
+ onClose={() => setOpen(false)}
539
+ />
540
+ </div>
541
+ ) : null}
542
+ </span>
494
543
  );
495
544
  }
496
545
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/editor-blender",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "AGPL-3.0-only AND GPL-3.0-or-later",
5
- "version": "0.1.29",
5
+ "version": "0.1.31",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -70,14 +70,14 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@modelcontextprotocol/sdk": "1.30.0",
73
- "@volter/blender-engine": "0.1.27",
74
- "@volter/editor-threejs": "0.5.89",
73
+ "@volter/blender-engine": "0.1.29",
74
+ "@volter/editor-threejs": "0.5.91",
75
75
  "zod": "^4.3.6"
76
76
  },
77
77
  "peerDependencies": {
78
- "@volter/editor-live": "0.5.89",
79
- "@volter/editor-project": "0.5.89",
80
- "@volter/editor-sdk": "0.5.89",
78
+ "@volter/editor-live": "0.5.91",
79
+ "@volter/editor-project": "0.5.91",
80
+ "@volter/editor-sdk": "0.5.91",
81
81
  "react": "^19.0.0",
82
82
  "react-dom": "^19.0.0",
83
83
  "three": "^0.180.0",