@almadar/ui 5.142.0 → 5.144.0

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.
@@ -1,7 +1,7 @@
1
- export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-rwc3svaX.cjs';
1
+ export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-BdGFhYe3.cjs';
2
2
  import { FieldValue, EntityRow, EventPayload } from '@almadar/core';
3
3
  export { AssetLoadStatus, BridgeHealth, CheckStatus, EffectTrace, EventLogEntry, OrbitalVerificationAPI, ServerResponseTrace, TraitStateSnapshot, TransitionTrace, VerificationCheck, VerificationSnapshot, VerificationSummary } from '@almadar/core';
4
- import '../paintDispatch-BQn5Lyx1.cjs';
4
+ import '../paintDispatch-Cb_hQj4Y.cjs';
5
5
  import 'clsx';
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
- export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-BDwC4QpH.js';
1
+ export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-DNkqAWZK.js';
2
2
  import { FieldValue, EntityRow, EventPayload } from '@almadar/core';
3
3
  export { AssetLoadStatus, BridgeHealth, CheckStatus, EffectTrace, EventLogEntry, OrbitalVerificationAPI, ServerResponseTrace, TraitStateSnapshot, TransitionTrace, VerificationCheck, VerificationSnapshot, VerificationSummary } from '@almadar/core';
4
- import '../paintDispatch-BQn5Lyx1.js';
4
+ import '../paintDispatch-Cb_hQj4Y.js';
5
5
  import 'clsx';
6
6
 
7
7
  /**
@@ -3343,6 +3343,10 @@ var atlasCache = /* @__PURE__ */ new Map();
3343
3343
  function isTilesheet(a) {
3344
3344
  return typeof a.tileWidth === "number";
3345
3345
  }
3346
+ function isSpriteSheetAtlas(a) {
3347
+ const s = a;
3348
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
3349
+ }
3346
3350
  function getAtlas(url, onReady) {
3347
3351
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3348
3352
  atlasCache.set(url, void 0);
@@ -3379,6 +3383,7 @@ function subRectFor(atlas, sprite) {
3379
3383
  sh: atlas.tileHeight
3380
3384
  };
3381
3385
  }
3386
+ if (isSpriteSheetAtlas(atlas)) return null;
3382
3387
  const st = atlas.subTextures[sprite];
3383
3388
  if (!st) return null;
3384
3389
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -3612,7 +3617,7 @@ var Button = React12__namespace.default.forwardRef(
3612
3617
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
3613
3618
  children: [
3614
3619
  isLoading ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons2.Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
3615
- children || label,
3620
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
3616
3621
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
3617
3622
  ]
3618
3623
  }
@@ -3322,6 +3322,10 @@ var atlasCache = /* @__PURE__ */ new Map();
3322
3322
  function isTilesheet(a) {
3323
3323
  return typeof a.tileWidth === "number";
3324
3324
  }
3325
+ function isSpriteSheetAtlas(a) {
3326
+ const s = a;
3327
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
3328
+ }
3325
3329
  function getAtlas(url, onReady) {
3326
3330
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3327
3331
  atlasCache.set(url, void 0);
@@ -3358,6 +3362,7 @@ function subRectFor(atlas, sprite) {
3358
3362
  sh: atlas.tileHeight
3359
3363
  };
3360
3364
  }
3365
+ if (isSpriteSheetAtlas(atlas)) return null;
3361
3366
  const st = atlas.subTextures[sprite];
3362
3367
  if (!st) return null;
3363
3368
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -3591,7 +3596,7 @@ var Button = React12__default.forwardRef(
3591
3596
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
3592
3597
  children: [
3593
3598
  isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
3594
- children || label,
3599
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
3595
3600
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
3596
3601
  ]
3597
3602
  }
@@ -87,8 +87,10 @@ interface DrawSpriteProps extends DrawableBase {
87
87
  height?: number;
88
88
  /** Explicit atlas sub-rect override (px); omitted → resolved from `asset.atlas`/`asset.sprite`. */
89
89
  frame?: BlitSrc;
90
- /** Named GLB animation clip to play (3D backend only; the 2D painter animates via `frame`). Matched case-insensitively against the model's clips. */
90
+ /** Animation to play. 3D backend: a named GLB clip (matched case-insensitively). 2D painter: when `asset.atlas` resolves to a `SpriteSheetAtlas` manifest, the canonical row of that name (idle/walk/attack/hit/death) plays off the host paint clock; an explicit `frame` rect overrides. */
91
91
  animation?: string;
92
+ /** Override the sheet row's loop flag. The paint clock is wall-time, so one-shot rows hold their final frame — preview pages set `true` to cycle attack/hit/death continuously. */
93
+ loop?: boolean;
92
94
  /** Mirror horizontally (facing). */
93
95
  flipX?: boolean;
94
96
  /** Rotation in radians about the sprite's center. */
@@ -271,42 +273,15 @@ interface DrawTextProps extends DrawableBase {
271
273
  opacity?: number;
272
274
  }
273
275
 
274
- /**
275
- * `draw-group` — the drawable CONTAINER atom (dimension-agnostic).
276
- *
277
- * Multi-shape art (a hero token = cloak + head + sword shapes) travels as ONE
278
- * node: the host paints the group by translating to its projected position,
279
- * applying `scale`/`rotate`/`opacity`, then painting each `items` descriptor
280
- * through the normal dispatch. `items` is a plain data prop (NOT `children`,
281
- * which UISlotRenderer special-cases) — same convention as the `draw-*-layer`
282
- * molecules. The React component renders `null`; it exists so the pattern
283
- * pipeline registers a `draw-group` pattern and standalone pages stay
284
- * inspectable. 2D-canvas only — the 3D backend skips it with a warn.
285
- */
286
-
287
- interface DrawGroupProps extends DrawableBase {
288
- type: 'draw-group';
289
- /** Logical scene position; the projector maps it to pixels / world. */
290
- position: ScenePos;
291
- /** Uniform scale applied to the whole group. */
292
- scale?: number;
293
- /** Rotation in radians (painter units). */
294
- rotate?: number;
295
- /** 0..1 opacity. */
296
- opacity?: number;
297
- /** Clip the group's items to an SVG path (`d` syntax) in world units relative to the group origin — masked reveals, meters, portraits. */
298
- clip?: string;
299
- /** The grouped drawables, painted through the normal dispatch. */
300
- items: DrawableNode[];
301
- }
302
-
303
276
  /**
304
277
  * `draw-mesh` — the volumetric 3D drawable atom (3D-canvas backend).
305
278
  *
306
279
  * ONE descriptor (`DrawMeshProps`, grounded in core `ScenePos`) with the R3F
307
280
  * backend `Mesh3D` in `lib/drawable/three/DrawMesh3D` (kept OUT of this file so
308
281
  * the 2D path never pulls three). A parametric primitive (box, sphere, capsule,
309
- * cylinder, cone, torus, plane, circle) at a scene position `position.z` is
282
+ * cylinder, cone, torus, plane, circle `plane`/`circle` lie flat in the
283
+ * scene ground plane facing up, matching the 2D painter) or a raw-geometry `polyhedron`
284
+ * (`vertices` + `faces` as plain data) at a scene position — `position.z` is
310
285
  * height in cells — with a data-driven material (`standard`/`physical`/`toon`/
311
286
  * `basic`), an optional inverted-hull outline, and keyframe animation over
312
287
  * transform/material tracks. Low-poly is a knob, not a geometry rewrite:
@@ -316,7 +291,7 @@ interface DrawGroupProps extends DrawableBase {
316
291
  * pattern and standalone pages stay inspectable.
317
292
  */
318
293
 
319
- type MeshShapeKind = 'box' | 'sphere' | 'capsule' | 'cylinder' | 'cone' | 'torus' | 'plane' | 'circle';
294
+ type MeshShapeKind = 'box' | 'sphere' | 'capsule' | 'cylinder' | 'cone' | 'torus' | 'plane' | 'circle' | 'polyhedron';
320
295
  type MeshMaterialKind = 'standard' | 'physical' | 'toon' | 'basic';
321
296
  type MeshMaterialSide = 'front' | 'back' | 'double';
322
297
  /**
@@ -346,6 +321,8 @@ interface MeshMaterial {
346
321
  flatShading?: boolean;
347
322
  /** Which faces render (default `front`). */
348
323
  side?: MeshMaterialSide;
324
+ /** Albedo texture: an image URL or `data:` URI, sampled through the polyhedron's `uvs`. Multiplied by `color` — keep `color` white (or unset) to show the texture unmodified. */
325
+ map?: string;
349
326
  }
350
327
  /** Inverted-hull toon outline: a back-face shell of the same geometry, scaled out by `width`. */
351
328
  interface MeshOutline {
@@ -386,6 +363,22 @@ interface DrawMeshAnimation {
386
363
  /** Keyframes ordered by `at` (0..1). */
387
364
  keyframes: DrawMeshKeyframe[];
388
365
  }
366
+ /**
367
+ * Smooth-skinning data for a `polyhedron` — plain declarative data, no runtime
368
+ * objects. Vertices carry blended bone weights (a neck-base vertex can be 60%
369
+ * neck / 40% spine), so the surface stretches continuously across joints
370
+ * instead of cracking at rigid chunk boundaries.
371
+ */
372
+ interface DrawMeshSkin {
373
+ /** Bone names, resolved against sibling `draw-group` `bone:` names in the same canvas. */
374
+ bones: string[];
375
+ /** One 16-float column-major inverse bind matrix per bone (model space → bone bind-local; translations in cells). */
376
+ inverseBindMatrices: number[][];
377
+ /** Per-vertex bone indices into `bones`, up to 4 each (pad with 0). */
378
+ indices: number[][];
379
+ /** Per-vertex blend weights matching `indices`, normalized to sum 1 (pad with 0). */
380
+ weights: number[][];
381
+ }
389
382
  interface DrawMeshProps extends DrawableBase {
390
383
  type: 'draw-mesh';
391
384
  shape: MeshShapeKind;
@@ -407,9 +400,17 @@ interface DrawMeshProps extends DrawableBase {
407
400
  tube?: number;
408
401
  /** Curved-surface tessellation, clamped 3..64 (default 24). 4–8 + `flatShading` = low-poly. */
409
402
  segments?: number;
403
+ /** `polyhedron` only: raw vertices as scene-frame `[x, y, z]` triples in cells, `z` = up. */
404
+ vertices?: number[][];
405
+ /** `polyhedron` only: triangle index triples into `vertices`, wound counter-clockwise seen from OUTSIDE in the scene frame. */
406
+ faces?: number[][];
407
+ /** `polyhedron` only: per-vertex `[u, v]` texture coordinates parallel to `vertices`, glTF convention (`v` grows DOWN from the image's top-left). Pairs with `material.map`. */
408
+ uvs?: number[][];
409
+ /** `polyhedron` only: smooth-skinning weights binding the vertices to named bone groups. Vertices must then be authored in BIND POSE model space; the bones' animated transforms deform them. */
410
+ skin?: DrawMeshSkin;
410
411
  /** Euler rotation `[x, y, z]` in radians. */
411
412
  rotation?: [number, number, number];
412
- /** `bottom` (default) rests the mesh on its position's ground plane; `center` centers it there. */
413
+ /** `bottom` (default) rests the mesh on its position's ground plane; `center` centers it there. For `polyhedron`, `bottom` lifts by −min(z) and `center` applies no offset — the vertices are authoritative. */
413
414
  pivot?: 'bottom' | 'center';
414
415
  material?: MeshMaterial;
415
416
  outline?: MeshOutline;
@@ -421,6 +422,51 @@ interface DrawMeshProps extends DrawableBase {
421
422
  opacity?: number;
422
423
  }
423
424
 
425
+ /**
426
+ * `draw-group` — the drawable CONTAINER atom (dimension-agnostic).
427
+ *
428
+ * Multi-shape art (a hero token = cloak + head + sword shapes) travels as ONE
429
+ * node: the host paints the group by translating to its projected position,
430
+ * applying `scale`/`rotate`/`opacity`, then painting each `items` descriptor
431
+ * through the normal dispatch. `items` is a plain data prop (NOT `children`,
432
+ * which UISlotRenderer special-cases) — same convention as the `draw-*-layer`
433
+ * molecules. The React component renders `null`; it exists so the pattern
434
+ * pipeline registers a `draw-group` pattern and standalone pages stay
435
+ * inspectable. Both backends paint it: 2D as a canvas transform, 3D as a three
436
+ * `<group>` — with `rotation` + `animation` a nested group chain is a skeleton
437
+ * bone chain (rotate the shoulder group and the forearm group follows).
438
+ */
439
+
440
+ interface DrawGroupProps extends DrawableBase {
441
+ type: 'draw-group';
442
+ /** Logical scene position; the projector maps it to pixels / world. */
443
+ position: ScenePos;
444
+ /** Uniform scale applied to the whole group. */
445
+ scale?: number;
446
+ /** Rotation in radians (painter units). */
447
+ rotate?: number;
448
+ /** Euler rotation `[x, y, z]` in radians (world axes) — per-axis joint rotation for the 3D backend; a group placed at a joint with children authored joint-local IS a bone. The 2D painter applies only the in-plane `z` component, after `rotate`. */
449
+ rotation?: [number, number, number];
450
+ /** Skeleton bone name. A named group is addressable by a `draw-mesh` `skin`
451
+ * block (smooth skinning): the skinned mesh deforms with this group's
452
+ * animated transform. Names resolve within the nearest enclosing
453
+ * `skeleton: true` group (falling back to the canvas), so multiple
454
+ * instances of one rig never collide. Distinct from `id` (the hit-test
455
+ * handle). */
456
+ bone?: string;
457
+ /** Marks this group as a skeleton SCOPE: `bone` names and `skin` bindings in
458
+ * its subtree resolve against each other only — one scope per rig instance. */
459
+ skeleton?: boolean;
460
+ /** Keyframe animation over the group TRANSFORM tracks (`offsetX/Y/Z` in cells, `rotateX/Y/Z` in radians, `scale`; the 2D painter also honors `opacity`) — children inherit it, so nested animated groups form a joint chain. Material tracks belong on leaf meshes. */
461
+ animation?: DrawMeshAnimation;
462
+ /** 0..1 opacity. */
463
+ opacity?: number;
464
+ /** Clip the group's items to an SVG path (`d` syntax) in world units relative to the group origin — masked reveals, meters, portraits. */
465
+ clip?: string;
466
+ /** The grouped drawables, painted through the normal dispatch. */
467
+ items: DrawableNode[];
468
+ }
469
+
424
470
  /**
425
471
  * `draw-sprite-layer` — batch multiple sprites in one descriptor for O(layers) rendering.
426
472
  *
@@ -87,8 +87,10 @@ interface DrawSpriteProps extends DrawableBase {
87
87
  height?: number;
88
88
  /** Explicit atlas sub-rect override (px); omitted → resolved from `asset.atlas`/`asset.sprite`. */
89
89
  frame?: BlitSrc;
90
- /** Named GLB animation clip to play (3D backend only; the 2D painter animates via `frame`). Matched case-insensitively against the model's clips. */
90
+ /** Animation to play. 3D backend: a named GLB clip (matched case-insensitively). 2D painter: when `asset.atlas` resolves to a `SpriteSheetAtlas` manifest, the canonical row of that name (idle/walk/attack/hit/death) plays off the host paint clock; an explicit `frame` rect overrides. */
91
91
  animation?: string;
92
+ /** Override the sheet row's loop flag. The paint clock is wall-time, so one-shot rows hold their final frame — preview pages set `true` to cycle attack/hit/death continuously. */
93
+ loop?: boolean;
92
94
  /** Mirror horizontally (facing). */
93
95
  flipX?: boolean;
94
96
  /** Rotation in radians about the sprite's center. */
@@ -271,42 +273,15 @@ interface DrawTextProps extends DrawableBase {
271
273
  opacity?: number;
272
274
  }
273
275
 
274
- /**
275
- * `draw-group` — the drawable CONTAINER atom (dimension-agnostic).
276
- *
277
- * Multi-shape art (a hero token = cloak + head + sword shapes) travels as ONE
278
- * node: the host paints the group by translating to its projected position,
279
- * applying `scale`/`rotate`/`opacity`, then painting each `items` descriptor
280
- * through the normal dispatch. `items` is a plain data prop (NOT `children`,
281
- * which UISlotRenderer special-cases) — same convention as the `draw-*-layer`
282
- * molecules. The React component renders `null`; it exists so the pattern
283
- * pipeline registers a `draw-group` pattern and standalone pages stay
284
- * inspectable. 2D-canvas only — the 3D backend skips it with a warn.
285
- */
286
-
287
- interface DrawGroupProps extends DrawableBase {
288
- type: 'draw-group';
289
- /** Logical scene position; the projector maps it to pixels / world. */
290
- position: ScenePos;
291
- /** Uniform scale applied to the whole group. */
292
- scale?: number;
293
- /** Rotation in radians (painter units). */
294
- rotate?: number;
295
- /** 0..1 opacity. */
296
- opacity?: number;
297
- /** Clip the group's items to an SVG path (`d` syntax) in world units relative to the group origin — masked reveals, meters, portraits. */
298
- clip?: string;
299
- /** The grouped drawables, painted through the normal dispatch. */
300
- items: DrawableNode[];
301
- }
302
-
303
276
  /**
304
277
  * `draw-mesh` — the volumetric 3D drawable atom (3D-canvas backend).
305
278
  *
306
279
  * ONE descriptor (`DrawMeshProps`, grounded in core `ScenePos`) with the R3F
307
280
  * backend `Mesh3D` in `lib/drawable/three/DrawMesh3D` (kept OUT of this file so
308
281
  * the 2D path never pulls three). A parametric primitive (box, sphere, capsule,
309
- * cylinder, cone, torus, plane, circle) at a scene position `position.z` is
282
+ * cylinder, cone, torus, plane, circle `plane`/`circle` lie flat in the
283
+ * scene ground plane facing up, matching the 2D painter) or a raw-geometry `polyhedron`
284
+ * (`vertices` + `faces` as plain data) at a scene position — `position.z` is
310
285
  * height in cells — with a data-driven material (`standard`/`physical`/`toon`/
311
286
  * `basic`), an optional inverted-hull outline, and keyframe animation over
312
287
  * transform/material tracks. Low-poly is a knob, not a geometry rewrite:
@@ -316,7 +291,7 @@ interface DrawGroupProps extends DrawableBase {
316
291
  * pattern and standalone pages stay inspectable.
317
292
  */
318
293
 
319
- type MeshShapeKind = 'box' | 'sphere' | 'capsule' | 'cylinder' | 'cone' | 'torus' | 'plane' | 'circle';
294
+ type MeshShapeKind = 'box' | 'sphere' | 'capsule' | 'cylinder' | 'cone' | 'torus' | 'plane' | 'circle' | 'polyhedron';
320
295
  type MeshMaterialKind = 'standard' | 'physical' | 'toon' | 'basic';
321
296
  type MeshMaterialSide = 'front' | 'back' | 'double';
322
297
  /**
@@ -346,6 +321,8 @@ interface MeshMaterial {
346
321
  flatShading?: boolean;
347
322
  /** Which faces render (default `front`). */
348
323
  side?: MeshMaterialSide;
324
+ /** Albedo texture: an image URL or `data:` URI, sampled through the polyhedron's `uvs`. Multiplied by `color` — keep `color` white (or unset) to show the texture unmodified. */
325
+ map?: string;
349
326
  }
350
327
  /** Inverted-hull toon outline: a back-face shell of the same geometry, scaled out by `width`. */
351
328
  interface MeshOutline {
@@ -386,6 +363,22 @@ interface DrawMeshAnimation {
386
363
  /** Keyframes ordered by `at` (0..1). */
387
364
  keyframes: DrawMeshKeyframe[];
388
365
  }
366
+ /**
367
+ * Smooth-skinning data for a `polyhedron` — plain declarative data, no runtime
368
+ * objects. Vertices carry blended bone weights (a neck-base vertex can be 60%
369
+ * neck / 40% spine), so the surface stretches continuously across joints
370
+ * instead of cracking at rigid chunk boundaries.
371
+ */
372
+ interface DrawMeshSkin {
373
+ /** Bone names, resolved against sibling `draw-group` `bone:` names in the same canvas. */
374
+ bones: string[];
375
+ /** One 16-float column-major inverse bind matrix per bone (model space → bone bind-local; translations in cells). */
376
+ inverseBindMatrices: number[][];
377
+ /** Per-vertex bone indices into `bones`, up to 4 each (pad with 0). */
378
+ indices: number[][];
379
+ /** Per-vertex blend weights matching `indices`, normalized to sum 1 (pad with 0). */
380
+ weights: number[][];
381
+ }
389
382
  interface DrawMeshProps extends DrawableBase {
390
383
  type: 'draw-mesh';
391
384
  shape: MeshShapeKind;
@@ -407,9 +400,17 @@ interface DrawMeshProps extends DrawableBase {
407
400
  tube?: number;
408
401
  /** Curved-surface tessellation, clamped 3..64 (default 24). 4–8 + `flatShading` = low-poly. */
409
402
  segments?: number;
403
+ /** `polyhedron` only: raw vertices as scene-frame `[x, y, z]` triples in cells, `z` = up. */
404
+ vertices?: number[][];
405
+ /** `polyhedron` only: triangle index triples into `vertices`, wound counter-clockwise seen from OUTSIDE in the scene frame. */
406
+ faces?: number[][];
407
+ /** `polyhedron` only: per-vertex `[u, v]` texture coordinates parallel to `vertices`, glTF convention (`v` grows DOWN from the image's top-left). Pairs with `material.map`. */
408
+ uvs?: number[][];
409
+ /** `polyhedron` only: smooth-skinning weights binding the vertices to named bone groups. Vertices must then be authored in BIND POSE model space; the bones' animated transforms deform them. */
410
+ skin?: DrawMeshSkin;
410
411
  /** Euler rotation `[x, y, z]` in radians. */
411
412
  rotation?: [number, number, number];
412
- /** `bottom` (default) rests the mesh on its position's ground plane; `center` centers it there. */
413
+ /** `bottom` (default) rests the mesh on its position's ground plane; `center` centers it there. For `polyhedron`, `bottom` lifts by −min(z) and `center` applies no offset — the vertices are authoritative. */
413
414
  pivot?: 'bottom' | 'center';
414
415
  material?: MeshMaterial;
415
416
  outline?: MeshOutline;
@@ -421,6 +422,51 @@ interface DrawMeshProps extends DrawableBase {
421
422
  opacity?: number;
422
423
  }
423
424
 
425
+ /**
426
+ * `draw-group` — the drawable CONTAINER atom (dimension-agnostic).
427
+ *
428
+ * Multi-shape art (a hero token = cloak + head + sword shapes) travels as ONE
429
+ * node: the host paints the group by translating to its projected position,
430
+ * applying `scale`/`rotate`/`opacity`, then painting each `items` descriptor
431
+ * through the normal dispatch. `items` is a plain data prop (NOT `children`,
432
+ * which UISlotRenderer special-cases) — same convention as the `draw-*-layer`
433
+ * molecules. The React component renders `null`; it exists so the pattern
434
+ * pipeline registers a `draw-group` pattern and standalone pages stay
435
+ * inspectable. Both backends paint it: 2D as a canvas transform, 3D as a three
436
+ * `<group>` — with `rotation` + `animation` a nested group chain is a skeleton
437
+ * bone chain (rotate the shoulder group and the forearm group follows).
438
+ */
439
+
440
+ interface DrawGroupProps extends DrawableBase {
441
+ type: 'draw-group';
442
+ /** Logical scene position; the projector maps it to pixels / world. */
443
+ position: ScenePos;
444
+ /** Uniform scale applied to the whole group. */
445
+ scale?: number;
446
+ /** Rotation in radians (painter units). */
447
+ rotate?: number;
448
+ /** Euler rotation `[x, y, z]` in radians (world axes) — per-axis joint rotation for the 3D backend; a group placed at a joint with children authored joint-local IS a bone. The 2D painter applies only the in-plane `z` component, after `rotate`. */
449
+ rotation?: [number, number, number];
450
+ /** Skeleton bone name. A named group is addressable by a `draw-mesh` `skin`
451
+ * block (smooth skinning): the skinned mesh deforms with this group's
452
+ * animated transform. Names resolve within the nearest enclosing
453
+ * `skeleton: true` group (falling back to the canvas), so multiple
454
+ * instances of one rig never collide. Distinct from `id` (the hit-test
455
+ * handle). */
456
+ bone?: string;
457
+ /** Marks this group as a skeleton SCOPE: `bone` names and `skin` bindings in
458
+ * its subtree resolve against each other only — one scope per rig instance. */
459
+ skeleton?: boolean;
460
+ /** Keyframe animation over the group TRANSFORM tracks (`offsetX/Y/Z` in cells, `rotateX/Y/Z` in radians, `scale`; the 2D painter also honors `opacity`) — children inherit it, so nested animated groups form a joint chain. Material tracks belong on leaf meshes. */
461
+ animation?: DrawMeshAnimation;
462
+ /** 0..1 opacity. */
463
+ opacity?: number;
464
+ /** Clip the group's items to an SVG path (`d` syntax) in world units relative to the group origin — masked reveals, meters, portraits. */
465
+ clip?: string;
466
+ /** The grouped drawables, painted through the normal dispatch. */
467
+ items: DrawableNode[];
468
+ }
469
+
424
470
  /**
425
471
  * `draw-sprite-layer` — batch multiple sprites in one descriptor for O(layers) rendering.
426
472
  *