+ {#each plans as p (p.uuid)}
+ {#if plans.length > 1}{p.name}{/if}
+ {#if !p.clips.length}No clips — apply some with Character ▸ Apply clip…{/if}
+ {#each p.clips as c (c.name)}
+
+ {/each}
+ {/each}
+
+ {/if}
+ {:else}
+ Clip
+
+ Format
+
+ {#each CLIP_FORMATS as f (f.id)}
+ {@const off = f.needs === 'humanoid' && !single.humanoid}
+
+ {/each}
+ {#each CONVERT_FORMATS as f (f.id)}
+
+ {/each}
+
+ {human?.ok
+ ? 'A full humanoid'
+ : 'Missing: ' + (human?.missing ?? REQUIRED_BONES).map(boneLabel).join(', ')} — {Object.keys(
+ human?.map ?? {}
+ ).length} of 55 bones mapped ({human?.via === 'file'
+ ? 'from the file'
+ : human?.via === 'structure'
+ ? 'from its shape — check it'
+ : 'from bone names'}). A fix here changes no file.
+
+
+ {#each rows as b (b.name)}
+ {@const cur = human?.map[b.name] ?? ''}
+ {@const fixed = human ? b.name in human.overrides : false}
+
+ {/each}
+
+
+
+
+
+ {:else}
+
+ {#each info.bones as b (b.name)}
+
+ {/each}
+
+ {#if !info.editable}
+
+ This model is a {info.kind.toUpperCase()} file: rig fix-ups edit GLB / VRM files. Download
+ it as GLB and import that.
+
+ {:else if tab === 'bones'}
+ {#if $pickedBone}
+
+
+
+
+
+ Rest
+ {#each [0, 1, 2] as k (k)}
+
+ (restT = restT.map((v, i) => (i === k ? +e.currentTarget.value : v)))}
+ />
+ {/each}
+
+
+
+
+
+
+
+ {:else}
+
Pick a bone in the list.
+ {/if}
+
+
+
+
+ {:else}
+
+
+
+
+
+
+ {#if brushOn}
+
+ Radius
+
+
+ Strength
+
+
+ {/if}
+
+ Test pose
+
+ {bend}°
+
+
+ Bends the picked bone here only, to see the weights work. Each stroke is saved and sent
+ when you lift.
+
+ {/if}
+ {/if}
+
+ {/if}
+
+
+
diff --git a/src/components/editors/AnimationWindow.svelte b/src/components/editors/AnimationWindow.svelte
index cfa1efd65..52e18eec0 100644
--- a/src/components/editors/AnimationWindow.svelte
+++ b/src/components/editors/AnimationWindow.svelte
@@ -33,7 +33,8 @@
// posed from the synced clock) — the window used to ignore them entirely, so
// a rigged model showed "no movements yet" and its own animations were
// reachable only from the Inspector.
- import { animatedObjects, setAnimationState, clipInfo, behaviorOf } from '$lib/animatedImports';
+ import { animatedObjects, setAnimationState, clipInfo, behaviorOf, restRowsOf } from '$lib/animatedImports';
+ import { openApplyClip } from '$lib/characterUi.js';
import Icon from '../ui/Icon.svelte';
import { minimalScroll } from '$lib/ui/minimalScroll.js';
import ContextMenu from '../ContextMenu.svelte';
@@ -2037,6 +2038,15 @@
- {#if row.kind === 'models'}
+ {#if row.kind === 'avatars'}
+ placeAvatar(row)}
+ >{placing[key] ? 'Placing…' : 'Add to scene'}
+ {:else if row.kind === 'models'}
{#if item}
- place(row)}>Add to scene
- show(item)}>Library
+ place(row)}>Add to scene
+ show(item)}>Library
{:else}
- bring(row)}>Add to Library
+ bring(row)}>Add to Library
{/if}
{:else if row.kind === 'hdris'}
- useSky(row)}>Use as sky
+ useSky(row)}>Use as sky
{:else}
{#if !preset}
- bring(row)}>Add
+ bring(row)}>Add
{/if}
1 ? 's' : '') : 'Select an object first'}
+ title={selectionCount
+ ? 'Put it on the selected object' + (selectionCount > 1 ? 's' : '')
+ : 'Select an object first'}
onclick={() => applyMaterial(row, preset)}>Apply
{/if}
@@ -380,7 +619,8 @@
variant="ghost"
size="sm"
disabled={loading}
- onclick={() => (kind.source === 'polyhaven' ? (shown += PAGE) : loadMoreAcg())}>Show more (kind.source === 'polyhaven' ? (shown += PAGE) : loadMoreAcg())}
+ >Show more
{/if}
@@ -388,8 +628,15 @@
Every asset here is CC0 (public domain) —
- Poly Haven and
- ambientCG. Credits stay with each file.
+ Poly Haven
+ and
+ ambientCG{kind.source === 'osa' ? '' : '.'} Credits stay with each file.
+ {#if kind.source === 'osa'}Avatars: the Open Source Avatars registry — CC0 collections, CC-BY ones on request (their credit is kept).{/if}
{/if}
@@ -402,6 +649,13 @@
gap: 6px;
padding: 6px 0 4px;
}
+ .ab-ccby {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--text-muted);
+ }
.ab-filters :global(.ab-cat) {
min-width: 120px;
flex: 1 1 140px;
diff --git a/src/components/editors/Explorer.svelte b/src/components/editors/Explorer.svelte
index 9826739b1..c347d0b7f 100644
--- a/src/components/editors/Explorer.svelte
+++ b/src/components/editors/Explorer.svelte
@@ -1072,7 +1072,8 @@
object: 'box',
prefab: 'boxes',
'prefab-folder': 'folder', // 37 R4: a folder inside the Prefabs view
- scene: 'map' // 21-F4: a level (.tpscene)
+ scene: 'map', // 21-F4: a level (.tpscene)
+ anim: 'person-standing' // 57 M1: an animation clip (.tpanim)
};
// semantic icon colors (ui.css classes over the --icon-* theme tokens)
/** R22-R7: what a kind is CALLED in the filter. The store values are internal
@@ -1084,11 +1085,12 @@
text: 'Text and config',
object: '3D models',
prefab: 'Prefabs',
- scene: 'Scenes'
+ scene: 'Scenes',
+ anim: 'Animation clips'
};
/** R22-R7: the filter's fixed order. Every kind the Explorer can hold, so the menu is
* a statement about the app rather than about this library's current contents. */
- const FILTER_KINDS = ['image', 'object', 'audio', 'video', 'text', 'scene', 'prefab'];
+ const FILTER_KINDS = ['image', 'object', 'anim', 'audio', 'video', 'text', 'scene', 'prefab'];
const KIND_COLORS: Record = {
image: 'ico-image',
audio: 'ico-audio',
@@ -1097,7 +1099,8 @@
object: 'ico-object',
prefab: 'ico-prefab',
'prefab-folder': 'ico-folder',
- scene: 'ico-prefab' // 21-F4: levels share the prefab tint
+ scene: 'ico-prefab', // 21-F4: levels share the prefab tint
+ anim: 'ico-object' // 57 M1: a clip belongs with the models it moves
};
// ---- R22 round 13 P3: WHERE AM I, when it is not the library ----------------------
@@ -6528,6 +6531,24 @@
url: URL.createObjectURL(blob),
onClose: () => gridEl?.focus()
});
+ } else if (item.kind === 'anim') {
+ // 57 M1: a clip PLAYS on the selected character (the drop's assign, without the drag)
+ const pick = selectedSet()[0];
+ if (!pick) {
+ showToast('Select a character, then double-click the clip — or drag it onto one');
+ return;
+ }
+ const { assignClipAsset } = await import('$lib/tpanim/clipAssets.js');
+ const { objectsGroup } = await import('../../stores/sceneStore.js');
+ const { hasAnimatedImport } = await import('$lib/animatedImports');
+ const group: any = get(objectsGroup);
+ let root = group?.getObjectByProperty('uuid', pick);
+ // the rig the selection belongs to; else (a parts model) its top-level object
+ let rig = root;
+ while (rig && !hasAnimatedImport(rig.uuid)) rig = rig.parent;
+ while (!rig && root && root.parent && root.parent !== group) root = root.parent;
+ const target = rig ?? root;
+ if (target) assignClipAsset(target.uuid, { hash: item.hash, name: item.name }, { play: true });
} else if (item.kind === 'object' && !item.packEntry) {
// R22 round 12 (user): "double click on 3d objects should open same preview as
// when opening image". ONE window for every kind, so the arrows walk from a
diff --git a/src/components/editors/Nodes.svelte b/src/components/editors/Nodes.svelte
index dcdabb388..f7b64402a 100644
--- a/src/components/editors/Nodes.svelte
+++ b/src/components/editors/Nodes.svelte
@@ -267,6 +267,7 @@
animstate: AnimStateNode, // 17-E F3: the readable half of it
playseq: TimelineSeqNode, // 48 S2: Play Sequence
onseqevent: TimelineSeqNode, // 48 S2: On Sequence Event
+ setexpression: AnimationNode, // 57 M4: spec-driven (expression text + weight)
// A3: ONE generic card for the whole HUD group (the ShaderNode precedent)
hudscreen: HudNode,
hudtext: HudNode,
diff --git a/src/components/menu/Inspector.svelte b/src/components/menu/Inspector.svelte
index e37d9de07..45831c0b5 100644
--- a/src/components/menu/Inspector.svelte
+++ b/src/components/menu/Inspector.svelte
@@ -92,7 +92,8 @@
import { nameOf } from '$lib/lockControl';
import { geometrySpec } from '$lib/geometryParams';
import { LIGHT_PARAMS, SHADOW_TYPES, SHADOW_SIZES, setShadowMapSize, cappedShadowSize } from '$lib/lightParams';
- import { animatedObjects, setAnimationState } from '$lib/animatedImports';
+ import { animatedObjects, setAnimationState, restRowsOf } from '$lib/animatedImports';
+ import CharacterSection from './inspector/CharacterSection.svelte';
import { captureAutoKey, playheadOf } from '$lib/animationPreview';
import { moveObjectToGroup, selectObject, flyTo, setPickThrough } from '$lib/objectActions';
import { listPhysicsObjects, enablePhysicsOnSelection, setPhysicsFor, PHYSICS_MATERIALS } from '$lib/physics';
@@ -3188,7 +3189,7 @@