@volter/editor-blender 0.1.149 → 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,36 +1,8 @@
1
- /**
2
- * THE UV EDITOR — Blender's UV editor over the engine's own mesh data, in a UV
3
- * Editing workspace (WORK.md §Blender in the tab is Blender, "Inspection
4
- * parity", I5; ARCHITECTURE-CORE §Blender north star).
5
- *
6
- * Every constant and colour it draws with is `./blender-uv-geometry.ts`, read
7
- * from Blender's source at the engine's pin and cited there; every number it
8
- * draws comes from `session.py`'s `rna_uv_layout` door. Blender's UI layer is
9
- * never run, ported or recorded: this is OUR panel over RNA.
10
- *
11
- * ## SVG, not canvas 2D
12
- *
13
- * The same reason the node view gives, measured for this data: a UV layout is
14
- * one `<path>` per island wash, one per island outline and one `<circle>` per
15
- * drawn corner, and the browser's own renderer does the transform, the
16
- * antialiasing and the hit testing. MEASURED on
17
- * `examples/first-person/src/models/arena-weapons.blend`'s largest UV mesh
18
- * (`LauncherMuzzleCollar`, 1,120 corners / 280 faces): 280 face paths, 280
19
- * outline paths and 1,120 dots. The DOT CAP below is what keeps that honest on
20
- * a mesh an order of magnitude larger, and the geometry lives in its own
21
- * module precisely so a canvas can take over behind it when a measurement says
22
- * so.
23
- *
24
- * ## It never edits, and every gesture that would is refused BY NAME
25
- *
26
- * Pinning, unwrapping, selecting and moving a UV all write the mesh, and
27
- * editing parity is not the program. The refusals are the view's own, reached
28
- * from the verb table and from the pointer handlers through one function
29
- * (`refuseUvViewGesture`), so the two cannot drift into two vocabularies.
30
- */
31
1
 
2
+
3
+ import { bytesFromBase64 } from '@volter/blender-engine/browser/three/blender-base64';
32
4
  import type { BlenderUvLayout } from '@volter/blender-engine/browser/rna';
33
- import { registerViewVerbs } from '@volter/sdk/views';
5
+ import type { ViewVerb } from '@volter/sdk/commands';
34
6
  import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
35
7
  import {
36
8
  blenderRnaVersion,
@@ -59,24 +31,16 @@ import {
59
31
  UV_TILE_GRID,
60
32
  UV_VERT_DOT_SIZE,
61
33
  UV_VERT_OUTLINE_WIDTH,
62
- uvBytes,
63
34
  uvFloat32,
64
35
  uvRgba,
65
36
  uvUint32,
66
37
  } from './blender-uv-geometry';
67
38
 
68
39
  export const point = 'workspace.document';
69
- /** Blender's own name for this editor — `rna_space.cc`'s `SPACE_IMAGE` item
70
- * reads "UV Editor" when `SpaceImage.mode` is `UV`, which is the mode
71
- * Blender's UV Editing workspace opens it in (measured on the engine: that
72
- * workspace's `IMAGE_EDITOR` area answers `mode: 'UV'`). */
40
+ /** Blender’s SPACE_IMAGE title is UV Editor when SpaceImage.mode is UV. */
73
41
  export const title = 'UV Editor';
74
42
 
75
- /** How many corner dots the view will draw before it stands them down and
76
- * says so. A dot is an element; 20,000 of them is a DOM the browser spends
77
- * more time laying out than the read took, and a silent thinning is exactly
78
- * the "named grade of acceptable partiality" the anti-shim rule forbids — so
79
- * the cap is a WARNING with a number in it, never a quiet decimation. */
43
+ /** Warn rather than silently thin the UV corner dots beyond this DOM drawing limit. */
80
44
  const DOT_CAP = 8000;
81
45
 
82
46
  const ZOOM_MIN = 16;
@@ -92,60 +56,48 @@ const REFUSALS = {
92
56
  'Unwrapping runs `bpy.ops.uv.unwrap`, which rewrites every UV on the mesh. Editing parity is not the program.',
93
57
  } as const;
94
58
 
95
- /** THE VIEW'S VERBS, published once, here — U8's ruling 1 (2026-09-19). Under
96
- * the Code-OSS frame each becomes a `volter.blender-uv-view.<verb>` command the
97
- * bridge dispatches into the view, and standalone `cyclotron edit` reaches the
98
- * SAME table through the session. **This view adds no `blender-*` session
99
- * verb at all**, which is exactly what the ruling asked the remaining I5
100
- * views to stop paying for. */
101
- registerViewVerbs({
102
- view: 'blender-uv-view',
103
- title,
104
- verbs: [
105
- { id: 'state', run: () => uvViewState() },
106
- {
107
- id: 'view-all',
108
- title: 'UV Editor: View All',
109
- run: () => {
110
- requestUvViewAll();
111
- return uvViewState();
112
- },
59
+ /** The page half registers each UV action as a volter.blender-uv-view.<verb> command. */
60
+ export const UV_VIEW_VERBS: readonly ViewVerb[] = [
61
+ { id: 'state', run: () => uvViewState() },
62
+ {
63
+ id: 'view-all',
64
+ run: () => {
65
+ requestUvViewAll();
66
+ return uvViewState();
113
67
  },
114
- {
115
- id: 'zoom',
116
- title: 'UV Editor: Zoom',
117
- run: (args) => {
118
- const to = Number(args?.['to'] ?? args?.['zoom']);
119
- if (!Number.isFinite(to))
120
- throw new Error('zoom needs a numeric `to` — CSS pixels per UV unit, 16 … 8192.');
121
- const { transform } = uvViewState();
122
- setUvViewState({
123
- transform: { ...transform, zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, to)) },
124
- });
125
- return uvViewState();
126
- },
68
+ },
69
+ {
70
+ id: 'zoom',
71
+ run: (args) => {
72
+ const to = Number(args?.['to'] ?? args?.['zoom']);
73
+ if (!Number.isFinite(to))
74
+ throw new Error('zoom needs a numeric `to` — CSS pixels per UV unit, 16 … 8192.');
75
+ const { transform } = uvViewState();
76
+ setUvViewState({
77
+ transform: { ...transform, zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, to)) },
78
+ });
79
+ return uvViewState();
127
80
  },
128
- {
129
- id: 'pan',
130
- title: 'UV Editor: Pan',
131
- run: (args) => {
132
- const du = Number(args?.['u'] ?? 0);
133
- const dv = Number(args?.['v'] ?? 0);
134
- if (!Number.isFinite(du) || !Number.isFinite(dv))
135
- throw new Error('pan needs numeric `u` and `v`, in UV units.');
136
- const { transform } = uvViewState();
137
- setUvViewState({
138
- transform: { ...transform, cx: transform.cx + du, cy: transform.cy + dv },
139
- });
140
- return uvViewState();
141
- },
81
+ },
82
+ {
83
+ id: 'pan',
84
+ run: (args) => {
85
+ const du = Number(args?.['u'] ?? 0);
86
+ const dv = Number(args?.['v'] ?? 0);
87
+ if (!Number.isFinite(du) || !Number.isFinite(dv))
88
+ throw new Error('pan needs numeric `u` and `v`, in UV units.');
89
+ const { transform } = uvViewState();
90
+ setUvViewState({
91
+ transform: { ...transform, cx: transform.cx + du, cy: transform.cy + dv },
92
+ });
93
+ return uvViewState();
142
94
  },
143
- { id: 'move-uv', run: () => refuse(REFUSALS.move('the active UV map')) },
144
- { id: 'select', run: () => refuse(REFUSALS.select) },
145
- { id: 'pin', run: () => refuse(REFUSALS.pin) },
146
- { id: 'unwrap', run: () => refuse(REFUSALS.unwrap) },
147
- ],
148
- });
95
+ },
96
+ { id: 'move-uv', run: () => refuse(REFUSALS.move('the active UV map')) },
97
+ { id: 'select', run: () => refuse(REFUSALS.select) },
98
+ { id: 'pin', run: () => refuse(REFUSALS.pin) },
99
+ { id: 'unwrap', run: () => refuse(REFUSALS.unwrap) },
100
+ ];
149
101
 
150
102
  function refuse(text: string): unknown {
151
103
  refuseUvViewGesture(text);
@@ -170,14 +122,12 @@ function prepare(layout: BlenderUvLayout | null): Prepared | null {
170
122
  // A PIN IS ONE BYTE, not a float: the door ships `array("b")` bytes
171
123
  // straight off `MeshUVLoopLayer.pin` and answers null when nothing is
172
124
  // pinned, so an absent payload is "no pins" rather than "unknown".
173
- pins: layout.pinBase64 ? uvBytes(layout.pinBase64) : null,
125
+ pins: layout.pinBase64 ? bytesFromBase64(layout.pinBase64) : null,
174
126
  };
175
127
  }
176
128
 
177
129
  export default function BlenderUvEditor() {
178
- // THE TREE'S FRESHNESS IS THE TREE'S (ruling 3, 2026-09-19): the RNA door's
179
- // own version, bumped by every write and by a presented frame, rather than
180
- // the properties model's — which moves only when a frame ships.
130
+ // Use the RNA door’s version, bumped by each write and presented frame.
181
131
  const version = useSyncExternalStore(subscribeBlenderRna, blenderRnaVersion, blenderRnaVersion);
182
132
  useSyncExternalStore(subscribeUvView, uvViewVersion, uvViewVersion);
183
133
  const framed = useRef(0);
@@ -377,18 +327,7 @@ export default function BlenderUvEditor() {
377
327
  style={{
378
328
  display: 'flex',
379
329
  flexDirection: 'column',
380
- // FILLED FROM THE HOST BOX, NOT `height: 100%`, and that is the
381
- // MEASURED difference between a drawer panel and an EDITOR GROUP. In
382
- // the drawer this view's `height: '100%'` worked, because the drawer
383
- // group imposes a height on its panel. As a `workspace.document` in
384
- // `volter:area:uv` it resolved to ZERO against an auto-height content
385
- // box — measured live 2026-09-19, `volter.blender-uv-view.state`
386
- // answered `size: {w: 782, h: 0}` with a fully READ subject beneath
387
- // it (24 corners, 6 faces, the ProbeChecker backdrop found), which is
388
- // a view that has everything to draw and nowhere to draw it. The node
389
- // view already had `position: absolute; inset: 0` for its own reason
390
- // and was `{w: 1294, h: 467}` in the same layout, which is what named
391
- // the difference.
330
+ // Fill the host box with absolute positioning so the editor area imposes its height.
392
331
  position: 'absolute',
393
332
  inset: 0,
394
333
  overflow: 'hidden',
@@ -397,10 +336,7 @@ export default function BlenderUvEditor() {
397
336
  font: '11px Inter, system-ui, sans-serif',
398
337
  }}
399
338
  >
400
- {/* A HEIGHT IMPOSED FROM ABOVE. The node view's walk measured what the
401
- other way costs: an SVG at `height: 100%` inside an auto-height parent
402
- settles at ~30 px, because each side's height is a function of the
403
- other's. */}
339
+ {/* Impose SVG height from the parent box rather than percentage sizing against auto-height content. */}
404
340
  <div ref={canvas} style={{ flex: 1, minHeight: 0, position: 'relative' }}>
405
341
  <svg
406
342
  width={size.w}
@@ -1,120 +1,29 @@
1
- /**
2
- * THE UV EDITOR'S GEOMETRY AND COLOUR, every constant read from Blender's own
3
- * source at the engine's pin (5.2.0, `fbe6228777e7`) — WORK.md §Blender in the
4
- * tab is Blender, "Inspection parity", I5; ARCHITECTURE-CORE §Blender north
5
- * star ("the reference is Blender's SOURCE as well as its frames").
6
- *
7
- * The drawing this describes is `MeshUVs` — the overlay class that REPLACED
8
- * the 5.2-era `overlay_edit_uv.cc`, which is why that filename does not exist
9
- * in the checkout — at `draw/engines/overlay/overlay_mesh.hh:483-760`, plus
10
- * the four shaders it binds
11
- * (`shaders/overlay_edit_uv_{edges_frag,verts_vert,verts_frag,faces_vert,face_dots_vert}.glsl`),
12
- * `space_image.cc`'s `image_create` for the space's own defaults and
13
- * `userdef_default_theme.c`'s `.space_image` for the colours.
14
- *
15
- * ## What is NOT mode-gated, and why every flag below reads "unselected"
16
- *
17
- * Blender draws UVs only when `show_uv_edit_ = space_mode_is_uv &&
18
- * object_mode_is_edit` (`overlay_mesh.hh:593`). Both halves belong to its UI
19
- * layer, which this editor never runs (orchestrator ruling 1, 2026-09-19:
20
- * "inspection is not mode-gated"). So the view reads the DATA in whatever mode
21
- * the engine is in and names the mode in its status line.
22
- *
23
- * The same ruling's second half decides the selection colours: "the view reads
24
- * the flags the data carries and draws everything unselected when the flags
25
- * are absent". MEASURED at this pin: `MeshUVLoopLayer`
26
- * (`makesrna/intern/rna_mesh.cc:2380-2456`) declares `uv`, `pin`, `name`,
27
- * `active`, `active_render` and `active_clone` — and NO vertex or edge
28
- * selection at all, because UV selection lives in the BMesh an edit-mode
29
- * session holds. `pin` IS declared, so pins are drawn; everything else takes
30
- * its unselected branch, and the SELECTED constants below are recorded beside
31
- * them rather than dropped, because a colour that is never reached is still
32
- * the specification of the one that is.
33
- */
1
+ import { bytesFromBase64 } from '@volter/blender-engine/browser/three/blender-base64';
2
+
34
3
 
35
4
  // ------------------------------------------------------------------- colours
36
5
 
37
- /**
38
- * `.space_image` (`release/datafiles/userdef/userdef_default_theme.c:578-608`),
39
- * whole. Alpha is carried where Blender carries it: `face` is `0x0a` and
40
- * `face_select` `0x3c` out of 255, which is what makes a UV island read as a
41
- * wash rather than a fill.
42
- */
6
+ /** Values used by the read-only UV view from Blender's space_image theme. */
43
7
  export const UV_THEME = {
44
- /** `.back` `RGBA(0x30303000)` (`:580`) — alpha ZERO, so the space paints no
45
- * backdrop of its own and the region's own surface shows. The node editor's
46
- * `.back` is the same shape (`:661`), and U8's ruling 2 is what gives this
47
- * view a surface colour of its own instead. */
48
8
  back: '#303030',
49
- backAlpha: 0,
50
- /** `.grid` `RGBA(0x303030ff)` (`:586`) — FULL alpha, unlike the node
51
- * editor's. See `UV_GRID_OPEN` below for what that measures to. */
52
9
  grid: '#303030',
53
- /** `.wire_edit` (`:587`) — the unselected edge under the OUTLINE line style,
54
- * halved by the shader when `use_edge_select` is on
55
- * (`overlay_edit_uv_edges_frag.glsl:27`). */
56
- wireEdit: '#c0c0c0',
57
- /** `.vertex_select` (`:588`) — `theme.colors.vert_select`, the fill of a
58
- * selected UV vertex (`overlay_edit_uv_verts_vert.glsl:20`). */
59
- vertexSelect: '#ff8500',
60
- /** `.edge_select` (`:589`) — mixed into every line style by
61
- * `selection_fac` (`overlay_edit_uv_edges_frag.glsl:31,41,46,50`). */
62
- edgeSelect: '#ff8500',
63
- /** `.face` `RGBA(0xffffff0a)` (`:590`) — the unselected island wash. */
64
10
  face: '#ffffff',
65
11
  faceAlpha: 0x0a / 255,
66
- /** `.face_select` `RGBA(0xff85003c)` (`:591`). */
67
- faceSelect: '#ff8500',
68
- faceSelectAlpha: 0x3c / 255,
69
- /** `.editmesh_active` `RGBA(0xffffff40)` (`:596`) — the ACTIVE face, which
70
- * `overlay_edit_uv_faces_vert.glsl:25` puts over the selected colour. */
71
- editmeshActive: '#ffffff',
72
- editmeshActiveAlpha: 0x40 / 255,
73
- /** `.uv_shadow` (`:606`) — the SHADOW line style, which is what the
74
- * wireframe pass draws outside UV edit mode
75
- * (`edit_uv_line_style_from_space_image`, `overlay_mesh.hh:1091-1111`).
76
- * **That is this view's line style**, because this view is never in UV edit
77
- * mode: `sima->mode == SI_MODE_UV` is a space flag, and there is no space. */
78
12
  uvShadow: '#707070',
79
- /** `.preview_back` (`:601`). Recorded whole because the theme block is, and
80
- * named as NOT USED here: it is `TH_PREVIEW_BACK`, the backdrop behind a
81
- * node/texture preview, not the UV tile's. */
82
- previewBack: '#727272',
83
- /** `TH_VERTEX` and `TH_FACEDOT` and `TH_WIRE` are NOT DECLARED in
84
- * `.space_image` — checked key by key against `:578-608`. `resources.cc`
85
- * resolves each straight off the active space's `ThemeSpace`
86
- * (`:407-408`, `:361-362`, and the facedot case beside them), so the C
87
- * struct's zero stands and all three are BLACK — the same reading I4
88
- * recorded for `TH_VERTEX_UNREFERENCED`. Both shaders force alpha to 1 over
89
- * that zero (`…verts_vert.glsl:19`: `float4(color.rgb, 1.0f)`;
90
- * `…face_dots_vert.glsl:18`: `float4(theme.colors.wire.rgb, 1.0f)`), so an
91
- * unselected UV vertex and an unselected face dot are opaque black. */
92
13
  vertex: '#000000',
93
14
  faceDot: '#000000',
94
- wire: '#000000',
95
15
  } as const;
96
16
 
97
- /** `.space_image`'s SIZES, the same block (`:592-595`). */
17
+ /** space_image sizes used by the vertex, edge and face-dot drawing. */
98
18
  export const UV_SIZES = {
99
- /** `.vertex_size = 3` (`:592`). */
100
19
  vertexSize: 3,
101
- /** `.edge_width = 1` (`:593`). */
102
20
  edgeWidth: 1,
103
- /** `.outline_width = 1` (`:594`). */
104
- outlineWidth: 1,
105
- /** `.facedot_size = 3` (`:595`). */
106
21
  facedotSize: 3,
107
22
  } as const;
108
23
 
109
24
  // ------------------------------------------------------------------ geometry
110
25
 
111
- /**
112
- * `UI_SCALE_FAC` is 1 here, for the reason I5's node work already measured:
113
- * `U.widget_unit` is `int(roundf(18 * scale_factor)) + 2 * pixelsize` = 20 at
114
- * scale 1 (`windowmanager/intern/wm_window.cc:779`), which is the value every
115
- * node constant divides by — so the session's scale factor is 1 and every
116
- * `* UI_SCALE_FAC` below is the identity.
117
- */
26
+ /** At scale 1, U.widget_unit is 20; UI_SCALE_FAC is one for these normalized geometry constants. */
118
27
  export const UI_SCALE_FAC = 1;
119
28
 
120
29
  /**
@@ -140,11 +49,6 @@ export const UV_VERT_OUTLINE_WIDTH = 0.75;
140
49
  * (`overlay_mesh.hh:727-729`) — 3, with no `√2` and no outline. */
141
50
  export const UV_FACEDOT_SIZE = UV_SIZES.facedotSize * UI_SCALE_FAC;
142
51
 
143
- /** A DASHED EDGE'S PERIOD: `4.0f * UI_SCALE_FAC` (`overlay_mesh.hh:670`) — 4
144
- * pixels, of which the shader inks the first HALF
145
- * (`…edges_frag.glsl:38`: `fract(line_distance / dash_length) < 0.5`). */
146
- export const UV_DASH_LENGTH = 4 * UI_SCALE_FAC;
147
-
148
52
  /**
149
53
  * THE THREE OPACITIES, and all three default to 1.0 —
150
54
  * `image_create` (`editors/space_image/space_image.cc:116-118`) sets
@@ -164,8 +68,6 @@ export const UV_DASH_LENGTH = 4 * UI_SCALE_FAC;
164
68
  * says so.
165
69
  */
166
70
  export const UV_OPACITY = 1;
167
- export const UV_FACE_OPACITY = 1;
168
- export const UV_EDGE_OPACITY = 1;
169
71
 
170
72
  /**
171
73
  * THE TILE GRID. `image_create` (`space_image.cc:129-133`) sets
@@ -177,63 +79,15 @@ export const UV_EDGE_OPACITY = 1;
177
79
  export const UV_TILE_GRID: readonly [number, number] = [1, 1];
178
80
  export const UV_GRID_SUBDIV: readonly [number, number] = [10, 10];
179
81
 
180
- /**
181
- * THE GRID'S VISIBILITY IS THE ONE THING THE SOURCE HERE CANNOT SETTLE, and it
182
- * is named rather than guessed at.
183
- *
184
- * `.space_image.grid` is `#303030` at FULL alpha (`:586`) — unlike the node
185
- * editor's, whose alpha is zero, which is why that view draws no grid at all.
186
- * But the image region is cleared by `DRW_draw_view`
187
- * (`space_image.cc:758`), the overlay ENGINE's own clear, and
188
- * `draw/engines/overlay`'s grid pass is not in this checkout's sparse set. So
189
- * the source states the grid's colour and not the value it sits on, and
190
- * `#303030` on this view's own `#303030` surface is INVISIBLE — measured, not
191
- * assumed. The view draws the grid at the theme's colour anyway, because
192
- * drawing a different one would be inventing a mark; the contrast is the
193
- * first thing the owed sighted frame-beside-frame read settles.
194
- */
82
+ /** Equal grid and surface colours leave this view's tile grid without contrast. */
195
83
  export const UV_GRID_OPEN =
196
- "Blender's `.space_image.grid` is #303030 at full alpha " +
197
- "(userdef_default_theme.c:586) and this view's surface is #303030, so the tile " +
198
- "grid draws at the theme's colour and is invisible against it. The image " +
199
- "region's own clear is the overlay engine's (DRW_draw_view, space_image.cc:758), " +
200
- 'which is not in the source checkout — the sighted read settles the contrast.';
84
+ "Blender's space_image grid and this view's surface are both #303030, so the tile grid is invisible against it.";
201
85
 
202
- /**
203
- * THE LINE STYLE THIS VIEW DRAWS, and why it is not the default one.
204
- *
205
- * `edit_uv_line_style_from_space_image` (`overlay_mesh.hh:1091-1111`) returns
206
- * one of five: with `sima->mode == SI_MODE_UV` it maps `dt_uv` — whose enum is
207
- * `OUTLINE, DASH, BLACK, WHITE` in that order (`rna_space.cc:4010-4016`) and
208
- * whose `image_create` leaves it at 0, so Blender's stock UV editor draws
209
- * **OUTLINE** — and OTHERWISE it returns `SHADOW`.
210
- *
211
- * There is no `SpaceImage` here and therefore no `mode`, so the honest branch
212
- * is the else: **SHADOW**, `theme.colors.uv_shadow` `#707070`
213
- * (`…edges_frag.glsl:48-50`). The OUTLINE constants are carried beside it
214
- * because that is what the frame-beside-frame read will be against once a
215
- * mode-carrying surface exists.
216
- */
86
+ /** Without a SpaceImage edit-mode context, draw the SHADOW UV line style. */
217
87
  export const UV_LINE_STYLES = {
218
- outline: {
219
- /** `mix(wire_edit, edge_select, selection_fac)` with `use_edge_select`
220
- * off (`…edges_frag.glsl:34`), over a black outer line (`:36`). */
221
- inner: UV_THEME.wireEdit,
222
- outer: '#000000',
223
- },
224
- dash: { inner: '#595959', outer: null, period: UV_DASH_LENGTH },
225
- black: { inner: '#000000', outer: null },
226
- white: { inner: '#ffffff', outer: null },
227
- shadow: { inner: UV_THEME.uvShadow, outer: null },
88
+ shadow: { inner: UV_THEME.uvShadow },
228
89
  } as const;
229
90
 
230
- /** `0.35` in the DASH style's `float4(float3(0.35f), 1.0f)`
231
- * (`…edges_frag.glsl:39`) is a LINEAR value, and every other colour in that
232
- * shader comes from the theme as sRGB — so the dash's grey is `0.35`
233
- * linear = **#595959** sRGB, which is what `UV_LINE_STYLES.dash.inner`
234
- * carries. (`0.35^(1/2.4)` with the sRGB knee: 0.3487 → 0.6236 → 159.) */
235
- export const UV_DASH_LINEAR_GREY = 0.35;
236
-
237
91
  /**
238
92
  * THE EDGE'S DRAWN WIDTH. `…edges_frag.glsl:53-54` inks out to
239
93
  * `max(theme.sizes.edge - 0.5, 0)` and fades over the next
@@ -251,23 +105,13 @@ export const UV_PIN_COLOR = '#ff0000';
251
105
 
252
106
  // -------------------------------------------------------------- the decoding
253
107
 
254
- /** One base64 payload as bytes. The doors ship typed-array bytes because a UV
255
- * layout is `loops` corners and JSON numbers would be four times the wire for
256
- * the same values — the shape I4's weights already established. */
257
- export function uvBytes(base64: string): Uint8Array {
258
- const binary = atob(base64);
259
- const bytes = new Uint8Array(binary.length);
260
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
261
- return bytes;
262
- }
263
-
264
108
  export function uvFloat32(base64: string): Float32Array {
265
- const bytes = uvBytes(base64);
109
+ const bytes = bytesFromBase64(base64);
266
110
  return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 2);
267
111
  }
268
112
 
269
113
  export function uvUint32(base64: string): Uint32Array {
270
- const bytes = uvBytes(base64);
114
+ const bytes = bytesFromBase64(base64);
271
115
  return new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 2);
272
116
  }
273
117
 
@@ -279,15 +123,7 @@ export function uvRgba(hex: string, alpha: number): string {
279
123
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${Math.round(alpha * 1000) / 1000})`;
280
124
  }
281
125
 
282
- /** THE VIEW'S OWN CHROME, as named constants rather than inline literals —
283
- * `.space_image.text` `#e6e6e6` (`userdef_default_theme.c:582`) for the
284
- * status line, and Blender's selection orange `#ed5700` (the palette's
285
- * `viewport.selection`, itself derived from `.space_view3d.select`) for a
286
- * refusal, which is the same hue I2's rail already wears for "this is the
287
- * one you are looking at". They live here because a `@volter/editor-blender` view may
288
- * not import the editor's theme (the package's host-import pin is ZERO) and
289
- * because the style-token ratchet counts LITERALS in a `.tsx`, which is
290
- * exactly the right pressure: a colour with a citation is a constant. */
126
+ /** UV chrome constants name the source theme text and selection colours. The view imports no host theme module. */
291
127
  export const UV_CHROME = {
292
128
  text: '#e6e6e6',
293
129
  refusal: '#ed5700',
@@ -297,9 +133,8 @@ export const UV_CHROME = {
297
133
  rule: '#232323',
298
134
  /** The status line's own box. Blender's editor footers are one text row in
299
135
  * a `widget_unit`-tall strip; at `U.widget_unit` 20 (`wm_window.cc:779`)
300
- * and 11-px text that is 3 px of padding either side of the row, and the
301
- * 12-px gap is the same `NODE_MARGIN_X/3` rhythm the node view's labels
302
- * already use. Named here for the reason the colours are. */
136
+ * and 11-px text, the strip uses 3 px of vertical padding and a 12-px gap
137
+ * between status items. */
303
138
  statusPadding: '3px 8px',
304
139
  statusGap: 12,
305
140
  } as const;
@@ -16,12 +16,18 @@
16
16
  * It is read through the SDK's host door and NOT through `@editor/*`, and
17
17
  * its two inline styles spell the theme tokens directly
18
18
  * (`var(--volter-space-2)`, `var(--volter-font-md)`) rather than importing the
19
- * host's `fontSizeVar`/`spaceVar` handles: `@volter/editor-blender` is pinned at ZERO
20
- * host imports (`validate-editor-closure.mjs`), the first package to reach
21
- * that destination, and a token NAME is the published contract while the
22
- * handle object is the host's own sugar. The widget kit's move to
23
- * `@volter/sdk/widgets` is deferred (WORK.md §Skews as packages, item 5);
24
- * until it lands, a package styling inline writes the `var()`.
19
+ * host's `fontSizeVar`/`spaceVar` handles: `@volter/editor-blender` imports
20
+ * nothing from the host, and a token NAME is the published contract while the
21
+ * handle object is the host's own sugar. The inline `var()` values read the
22
+ * same published theme tokens as `@volter/sdk/widgets`.
23
+ *
24
+ * After it, where the editor runs from a checkout, `build <commit>` (`*`
25
+ * with uncommitted changes), the session's own answer of which code it serves
26
+ * (`@volter/sdk/serving-engine`), apart from the project's pin: one version is
27
+ * many builds of a checkout, and the commit tells them apart. Its tooltip names
28
+ * the branch and how far it is behind the last-fetched `origin/main`, as when
29
+ * the editor started. An installed release has no checkout, and shows the
30
+ * version alone.
25
31
  *
26
32
  * Nothing before a project is open (there is no version to tell the truth
27
33
  * about yet), per the registry contract's "an item that is currently
@@ -34,7 +40,8 @@
34
40
  */
35
41
 
36
42
  import { editorHost } from '@volter/sdk/host';
37
- import { useSyncExternalStore } from 'react';
43
+ import { servingEngine, type ServingEngine } from '@volter/sdk/serving-engine';
44
+ import { useEffect, useState, useSyncExternalStore } from 'react';
38
45
 
39
46
  export const point = 'workspace.status';
40
47
  export const title = 'Version';
@@ -50,35 +57,39 @@ export default function BlenderVersionStatus() {
50
57
  project.engineVersion,
51
58
  project.engineVersion,
52
59
  );
60
+ // (the build beside the pin: from a checkout, its commit, so two builds of one version are told apart)
61
+ const [engine, setEngine] = useState<ServingEngine | null>(null);
62
+ useEffect(() => {
63
+ let live = true;
64
+ void servingEngine().then((answer) => {
65
+ if (live) setEngine(answer);
66
+ });
67
+ return () => {
68
+ live = false;
69
+ };
70
+ }, []);
53
71
  if (version === null) return null;
72
+ const commit = engine?.commit ?? null;
73
+ const build = commit === null ? null : `build ${commit}${engine?.dirty === true ? '*' : ''}`;
74
+ const from = commit === null ? '' : `\nThis editor runs from a checkout, as it was when the editor started: ` +
75
+ `${engine?.branch ?? 'a detached HEAD'} at ${commit}${engine?.dirty === true ? ', with uncommitted changes' : ''}` +
76
+ `${typeof engine?.behindOriginMain === 'number' ? `, ${String(engine.behindOriginMain)} commit(s) behind the last-fetched origin/main` : ''}`;
54
77
  return (
55
78
  <span
56
79
  className="volter-status-copy"
57
80
  data-testid="status-blender-version"
58
81
  data-engine-version={version}
59
- // NO COLOUR AND NO `sm`. Measured on the native-2x frames, Blender's
60
- // `5.2.0` is `#878787` (135, a flat plateau — 53 px at 134 and 33 at 135
61
- // across the digits) with a 9.0 CSS cap height (18 device). 135 is the
62
- // DIMMEST text in Blender's window and the same ink its mouse hints take
63
- // at the other end of the band, so the band is one voice and this
64
- // inherits it rather than pinning `content.muted` (194) and becoming the
65
- // loudest thing in the quietest strip. The cap puts the size on the
66
- // `md`/`base` rung (11px -> 8.8), not `sm` (10px -> 8.0).
82
+ // Inherit the status band’s colour and base font size.
67
83
  style={{
68
84
  display: 'inline-flex',
69
85
  alignItems: 'center',
70
86
  gap: 'var(--volter-space-2)',
71
87
  fontSize: 'var(--volter-font-md)',
72
88
  }}
73
- title={`This project is built against engine ${version}`}
89
+ data-engine-commit={commit ?? undefined}
90
+ title={`This project is built against engine ${version}${from}`}
74
91
  >
75
- {/* Blender's thin rule between the status glyph and the number, measured
76
- on `modeling.png` at device x 3369..3371, y 2069..2097: 1.5 CSS wide,
77
- 14.5 CSS tall, and inked 134 — the TEXT's own colour, not a boundary.
78
- It was drawn here at `boundary.strong` (#3c3c3c, 60), which against a
79
- #171717 band is all but invisible; `currentColor` is both the measured
80
- colour and the one that cannot drift from the number beside it. The
81
- height rung closest to 14.5 is `icon-md` (14). */}
92
+ {/* The thin divider inherits the number’s ink and uses the icon-md height. */}
82
93
  <span
83
94
  aria-hidden="true"
84
95
  style={{
@@ -88,6 +99,15 @@ export default function BlenderVersionStatus() {
88
99
  }}
89
100
  />
90
101
  {version}
102
+ {build !== null && (
103
+ <>
104
+ <span
105
+ aria-hidden="true"
106
+ style={{ width: 'var(--volter-stroke-resting)', height: 'var(--volter-icon-md)', background: 'currentColor' }}
107
+ />
108
+ {build}
109
+ </>
110
+ )}
91
111
  </span>
92
112
  );
93
113
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * BLENDER'S WORLD STUDIO LIGHTS: the eight HDRIs Blender ships for Material Preview
3
3
  * (`datafiles/studiolights/world`, copied from Blender 5.2), registered as environment images
4
- * (`@volter/sdk/kit/environment-images`) so a view can light by one and draw it behind
4
+ * (`@volter/sdk/environment-images`) so a view can light by one and draw it behind
5
5
  * the scene. Material Preview opens on Forest. All CC0, by Greg Zaal (Poly Haven); the terms
6
6
  * and each original are in `studiolights/world/license.txt`.
7
7
  */
8
- import type { EnvironmentImageSet } from '@volter/sdk/kit/environment-images';
8
+ import type { EnvironmentImageSet } from '@volter/sdk/environment-images';
9
9
  import city from './studiolights/world/city.exr?url';
10
10
  import courtyard from './studiolights/world/courtyard.exr?url';
11
11
  import forest from './studiolights/world/forest.exr?url';
@@ -970,9 +970,6 @@
970
970
  "object-group": {
971
971
  "path": "M2.45 5.59L13.55 5.59A1.43 1.43 0 0 0 13.55 2.72L2.45 2.72A1.43 1.43 0 0 0 2.45 5.59ZM12.12 4.16L12.12 11.84A1.43 1.43 0 0 0 14.99 11.84L14.99 4.16A1.43 1.43 0 0 0 12.12 4.16ZM13.55 10.41L2.45 10.41A1.43 1.43 0 0 0 2.45 13.28L13.55 13.28A1.43 1.43 0 0 0 13.55 10.41ZM3.88 11.84L3.88 4.16A1.43 1.43 0 0 0 1.01 4.16L1.01 11.84A1.43 1.43 0 0 0 3.88 11.84ZM-0.66 0.68A0.37 0.37 0 0 0 -1.03 1.05L-1.03 4.1A0.37 0.37 0 0 0 -0.66 4.46L2.39 4.46A0.37 0.37 0 0 0 2.75 4.1L2.75 1.05A0.37 0.37 0 0 0 2.39 0.68ZM13.61 0.68A0.37 0.37 0 0 0 13.25 1.05L13.25 4.1A0.37 0.37 0 0 0 13.61 4.46L16.66 4.46A0.37 0.37 0 0 0 17.03 4.1L17.03 1.05A0.37 0.37 0 0 0 16.66 0.68ZM-0.66 11.54A0.37 0.37 0 0 0 -1.03 11.9L-1.03 14.95A0.37 0.37 0 0 0 -0.66 15.32L2.39 15.32A0.37 0.37 0 0 0 2.75 14.95L2.75 11.9A0.37 0.37 0 0 0 2.39 11.54ZM13.61 11.54A0.37 0.37 0 0 0 13.25 11.9L13.25 14.95A0.37 0.37 0 0 0 13.61 15.32L16.66 15.32A0.37 0.37 0 0 0 17.03 14.95L17.03 11.9A0.37 0.37 0 0 0 16.66 11.54Z"
972
972
  },
973
- "outliner": {
974
- "path": "M-0.23 3.29L5.65 3.29L5.65 -0.23L-0.23 -0.23ZM2.12 15.06L3.29 15.06L3.29 4.47L2.12 4.47ZM2.12 9.17L6.83 9.17L6.83 8L2.12 8ZM2.12 15.05L6.83 15.05L6.83 13.88L2.12 13.88ZM8 10.35L16.24 10.35L16.24 6.83L8 6.83ZM8 16.24L16.24 16.24L16.24 12.71L8 12.71Z"
975
- },
976
973
  "outliner-camera": {
977
974
  "path": "M1.53 4.58A1.1 1.1 0 0 0 0.44 5.68L0.44 11.54A1.1 1.1 0 0 0 1.53 12.64L9.34 12.64A1.1 1.1 0 0 0 10.44 11.54L10.44 5.68A1.1 1.1 0 0 0 9.34 4.58ZM15.56 12.39L15.56 4.83L10.93 8.61ZM1.47 2.14A1.53 1.53 0 1 0 4.52 2.14A1.53 1.53 0 1 0 1.47 2.14ZM5.62 2.14A1.53 1.53 0 1 0 8.67 2.14A1.53 1.53 0 1 0 5.62 2.14Z",
978
975
  "tone": "object"
@@ -1030,9 +1027,6 @@
1030
1027
  "plus": {
1031
1028
  "path": "M7.11 1.9L7.11 14.1A0.89 0.89 0 0 0 8.89 14.1L8.89 1.9A0.89 0.89 0 0 0 7.11 1.9ZM1.9 8.89L14.1 8.89A0.89 0.89 0 0 0 14.1 7.11L1.9 7.11A0.89 0.89 0 0 0 1.9 8.89Z"
1032
1029
  },
1033
- "properties": {
1034
- "path": "M2.71 -0.23A2.94 2.94 0 0 0 -0.23 2.71L-0.23 3.89A2.94 2.94 0 0 0 2.71 6.83L13.29 6.83A2.94 2.94 0 0 0 16.24 3.89L16.24 2.71A2.94 2.94 0 0 0 13.29 -0.23ZM6.83 0.94L12.71 0.94L13.32 1.02L13.89 1.25L14.37 1.63L14.75 2.11L14.98 2.68L15.06 3.29L14.98 3.9L14.75 4.47L14.37 4.96L13.89 5.33L13.32 5.57L12.71 5.65L6.83 5.65ZM2.71 9.17A2.94 2.94 0 0 0 -0.23 12.11L-0.23 13.29A2.94 2.94 0 0 0 2.71 16.24L13.29 16.24A2.94 2.94 0 0 0 16.24 13.29L16.24 12.11A2.94 2.94 0 0 0 13.29 9.17ZM10.35 10.34L12.71 10.34L13.32 10.42L13.89 10.66L14.37 11.03L14.75 11.52L14.98 12.09L15.06 12.7L14.98 13.31L14.75 13.87L14.37 14.36L13.89 14.74L13.32 14.97L12.71 15.05L10.35 15.05Z"
1035
- },
1036
1030
  "properties-bone": {
1037
1031
  "path": "M5.5 1C4.68 1 4 1.68 4 2.5L4 3.5L4 4L3.5 4L2.5 4C1.68 4 1 4.68 1 5.5C1 6.32 1.68 7 2.5 7L3.5 7C3.93 7 4.27 6.79 4.54 6.5L9.5 11.46C9.21 11.73 9 12.07 9 12.5L9 13.5C9 14.32 9.67 15 10.5 15C11.32 15 12 14.32 12 13.5L12 12.5L12 12L12.5 12L13.5 12C14.32 12 15 11.32 15 10.5C15 9.68 14.32 9 13.5 9L12.5 9C12.07 9 11.73 9.21 11.46 9.5L6.5 4.54C6.79 4.27 7 3.93 7 3.5L7 2.5C7 1.68 6.33 1 5.5 1Z",
1038
1032
  "tone": "data"
@@ -1227,9 +1221,6 @@
1227
1221
  "up-right-and-down-left-from-center": {
1228
1222
  "path": "M2.75 14.46L14.46 2.75A0.85 0.85 0 0 0 13.25 1.54L1.54 13.25A0.85 0.85 0 0 0 2.75 14.46ZM9.22 2.27L14.59 2.27A0.85 0.85 0 0 0 14.59 0.56L9.22 0.56A0.85 0.85 0 0 0 9.22 2.27ZM13.73 1.41L13.73 6.78A0.85 0.85 0 0 0 15.44 6.78L15.44 1.41A0.85 0.85 0 0 0 13.73 1.41ZM6.78 13.73L1.41 13.73A0.85 0.85 0 0 0 1.41 15.44L6.78 15.44A0.85 0.85 0 0 0 6.78 13.73ZM2.27 14.59L2.27 9.22A0.85 0.85 0 0 0 0.56 9.22L0.56 14.59A0.85 0.85 0 0 0 2.27 14.59Z"
1229
1223
  },
1230
- "users": {
1231
- "path": "M1.78 4.34A3.05 3.05 0 1 0 7.88 4.34A3.05 3.05 0 1 0 1.78 4.34ZM0.31 13.61L0.37 12.91L0.53 12.22L0.81 11.56L1.18 10.96L1.64 10.42L2.17 9.96L2.78 9.59L3.43 9.32L4.12 9.15L4.83 9.1L5.53 9.15L6.22 9.32L6.88 9.59L7.48 9.96L8.02 10.42L8.48 10.96L8.85 11.56L9.12 12.22L9.29 12.91L9.34 13.61L11.05 13.61L10.97 12.64L10.75 11.69L10.37 10.79L9.86 9.95L9.23 9.21L8.49 8.58L7.65 8.07L6.75 7.69L5.8 7.47L4.83 7.39L3.85 7.47L2.91 7.69L2 8.07L1.17 8.58L0.43 9.21L-0.21 9.95L-0.72 10.79L-1.09 11.69L-1.32 12.64L-1.39 13.61ZM-1.39 13.61A0.85 0.85 0 1 0 0.31 13.61A0.85 0.85 0 1 0 -1.39 13.61ZM9.34 13.61A0.85 0.85 0 1 0 11.05 13.61A0.85 0.85 0 1 0 9.34 13.61ZM11.54 3.16L11.8 3.09L12.07 3.05L12.35 3.06L12.61 3.11L12.87 3.21L13.11 3.33L13.33 3.5L13.52 3.7L13.67 3.92L13.79 4.16L13.88 4.42L13.92 4.69L13.92 4.96L13.88 5.23L13.79 5.49L13.67 5.74L13.52 5.96L13.33 6.16L13.11 6.32L12.87 6.45L12.61 6.54L12.35 6.59L12.07 6.6L11.8 6.57L11.54 6.5L11 7.97L11.5 8.11L12.01 8.17L12.52 8.15L13.03 8.06L13.51 7.89L13.96 7.64L14.37 7.33L14.73 6.96L15.02 6.54L15.25 6.08L15.41 5.59L15.49 5.08L15.49 4.57L15.41 4.06L15.25 3.57L15.02 3.11L14.73 2.69L14.37 2.32L13.96 2.01L13.51 1.77L13.03 1.6L12.52 1.5L12.01 1.48L11.5 1.54L11 1.68ZM10.49 2.42A0.79 0.79 0 1 0 12.06 2.42A0.79 0.79 0 1 0 10.49 2.42ZM10.49 7.24A0.79 0.79 0 1 0 12.06 7.24A0.79 0.79 0 1 0 10.49 7.24ZM9.04 11.66L9.26 11.16L9.56 10.71L9.92 10.3L10.34 9.95L10.81 9.68L11.32 9.47L11.85 9.35L12.39 9.31L12.94 9.35L13.47 9.47L13.97 9.68L14.44 9.95L14.86 10.3L15.23 10.71L15.52 11.16L15.75 11.66L17.29 11.1L16.96 10.37L16.53 9.71L16 9.12L15.38 8.61L14.7 8.21L13.96 7.91L13.18 7.73L12.39 7.67L11.6 7.73L10.82 7.91L10.09 8.21L9.4 8.61L8.79 9.12L8.26 9.71L7.82 10.37L7.49 11.1ZM7.45 11.38A0.82 0.82 0 1 0 9.08 11.38A0.82 0.82 0 1 0 7.45 11.38ZM15.7 11.38A0.82 0.82 0 1 0 17.34 11.38A0.82 0.82 0 1 0 15.7 11.38Z"
1232
- },
1233
1224
  "viewport-camera": {
1234
1225
  "path": "M4 12L4 10.5C4 10.22 4.22 10 4.5 10L10.5 10C10.78 10 11 10.22 11 10.5L11 15.5C11 15.78 10.78 16 10.5 16L5.71 16C5.57 16 5.45 15.95 5.35 15.85L4.15 14.65C4.05 14.55 4 14.43 4 14.29L4 13L3 13L3 13.5C3 13.78 2.78 14 2.5 14C2.22 14 2 13.78 2 13.5L2 11.5C2 11.22 2.22 11 2.5 11C2.78 11 3 11.22 3 11.5L3 12ZM5.91 15L5 14.09L5 11L10 11L10 15ZM13.22 13.5L15 14.59L15 12.41ZM15.15 15.85C15.24 15.94 15.36 16 15.5 16C15.78 16 16 15.78 16 15.5L16 11.5C16 11.22 15.78 11 15.5 11C15.36 11 15.24 11.06 15.15 11.15L12.04 13.04C11.87 13.12 11.75 13.3 11.75 13.5C11.75 13.7 11.87 13.88 12.04 13.96ZM6.38 4.22C6.02 4.3 5.65 4.17 5.41 3.89C4.95 3.34 4.27 3 3.5 3C2.12 3 1 4.12 1 5.5C1 6.88 2.12 8 3.5 8L10.5 8C12.43 8 14 6.43 14 4.5C14 2.57 12.43 1 10.5 1C8.91 1 7.56 2.06 7.14 3.52C7.04 3.88 6.75 4.15 6.38 4.22ZM6.61 2.24C6.42 2.56 6.28 2.89 6.18 3.25C5.94 2.97 5.67 2.73 5.36 2.54C4.82 2.2 4.18 2 3.5 2C1.57 2 0 3.57 0 5.5C0 7.43 1.57 9 3.5 9L10.5 9C12.99 9 15 6.99 15 4.5C15 2.01 12.99 0 10.5 0C8.84 0 7.38 0.9 6.61 2.24ZM10.5 5C10.78 5 11 4.78 11 4.5C11 4.22 10.78 4 10.5 4C10.22 4 10 4.22 10 4.5C10 4.78 10.22 5 10.5 5ZM10.5 6C11.33 6 12 5.33 12 4.5C12 3.67 11.33 3 10.5 3C9.67 3 9 3.67 9 4.5C9 5.33 9.67 6 10.5 6ZM4.5 5.5C4.5 6.05 4.05 6.5 3.5 6.5C2.95 6.5 2.5 6.05 2.5 5.5C2.5 4.95 2.95 4.5 3.5 4.5C4.05 4.5 4.5 4.95 4.5 5.5Z"
1235
1226
  },