@umicat/three-sdk 0.5.0 → 0.6.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.
@@ -46,4 +46,19 @@ export interface LoadedScene3D {
46
46
  * settled and shipped for 2D, and it is the reason an editor can render a scene
47
47
  * without starting a game.
48
48
  */
49
+ /** One model from the manifest, loaded on its own.
50
+ *
51
+ * `loadScene3D` only fetches what the SCENE references, which is everything a
52
+ * game needs until the first thing that appears at runtime — a weapon picked
53
+ * up, a projectile, a dropped item. Without this, a game reaches for
54
+ * GLTFLoader itself and re-implements asset-base resolution and importScale
55
+ * slightly differently from the loader beside it.
56
+ *
57
+ * Returns a fresh object each call, so two swords do not share one transform. */
58
+ export declare function loadModelAsset(manifest: Manifest3D, modelAssetId: string, opts?: {
59
+ assetBase?: string;
60
+ }): Promise<{
61
+ object: THREE.Object3D;
62
+ clips: THREE.AnimationClip[];
63
+ }>;
49
64
  export declare function loadScene3D(scene3d: Scene3D, manifest: Manifest3D, opts?: LoadSceneOptions): Promise<LoadedScene3D>;
@@ -45,6 +45,28 @@ function makePrimitive(e) {
45
45
  * settled and shipped for 2D, and it is the reason an editor can render a scene
46
46
  * without starting a game.
47
47
  */
48
+ /** One model from the manifest, loaded on its own.
49
+ *
50
+ * `loadScene3D` only fetches what the SCENE references, which is everything a
51
+ * game needs until the first thing that appears at runtime — a weapon picked
52
+ * up, a projectile, a dropped item. Without this, a game reaches for
53
+ * GLTFLoader itself and re-implements asset-base resolution and importScale
54
+ * slightly differently from the loader beside it.
55
+ *
56
+ * Returns a fresh object each call, so two swords do not share one transform. */
57
+ export async function loadModelAsset(manifest, modelAssetId, opts = {}) {
58
+ const asset = (manifest.models ?? []).find((m) => m.id === modelAssetId);
59
+ if (!asset) {
60
+ const have = (manifest.models ?? []).map((m) => m.id).join(', ');
61
+ throw new Error(`[umicat] manifest has no model '${modelAssetId}'. It has: ${have || '(none)'}`);
62
+ }
63
+ const gltf = await new GLTFLoader().loadAsync((opts.assetBase ?? '') + asset.path);
64
+ const object = gltf.scene;
65
+ if (asset.importScale !== undefined)
66
+ object.scale.setScalar(asset.importScale);
67
+ object.userData.modelAssetId = modelAssetId;
68
+ return { object, clips: gltf.animations };
69
+ }
48
70
  export async function loadScene3D(scene3d, manifest, opts = {}) {
49
71
  const problems = validateScene(scene3d);
50
72
  if (problems.length) {
@@ -0,0 +1,38 @@
1
+ import * as THREE from 'three';
2
+ import type { Socket3D } from './scene3d';
3
+ /**
4
+ * Put a thing in a character's hand.
5
+ *
6
+ * A held object is not a child of the character — it is a child of a BONE, so
7
+ * it inherits that bone's animated transform and follows the swing for free.
8
+ * Parenting to the model root instead produces a sword that hovers politely
9
+ * beside a character doing all the work.
10
+ *
11
+ * The catch, and the reason this is a platform primitive rather than four
12
+ * lines in each game: **nothing guarantees a rig has a hand bone.** The
13
+ * character every 3D Umicat game ships with has seven bones — root, two legs,
14
+ * torso, two arms, head — and no wrist at all. So "the hand" is a position
15
+ * along the arm bone, found by tuning, and it is a property of that model.
16
+ * Every game re-deriving it by eye is how you get twelve games with the sword
17
+ * in twelve slightly different places.
18
+ */
19
+ export interface Attachment {
20
+ /** The bone it ended up on — useful when you want to check your work. */
21
+ readonly bone: THREE.Bone;
22
+ /** The wrapper that carries the socket offset; the object is its child. */
23
+ readonly holder: THREE.Object3D;
24
+ detach(): void;
25
+ }
26
+ /** Find a bone by exact name anywhere under `root`. */
27
+ export declare function findBone(root: THREE.Object3D, name: string): THREE.Bone | null;
28
+ /** Every bone name under `root` — what an error message should offer. */
29
+ export declare function boneNames(root: THREE.Object3D): string[];
30
+ /**
31
+ * Attach `object` to a socket on `root`.
32
+ *
33
+ * Throws if the bone is missing, and says which bones DO exist. A silent miss
34
+ * here is the worst outcome available: the weapon stays parented where it was,
35
+ * usually at the world origin, and the character walks around empty-handed
36
+ * while every line of the game's equip logic reports success.
37
+ */
38
+ export declare function attachToSocket(root: THREE.Object3D, socket: Socket3D, object: THREE.Object3D): Attachment;
@@ -0,0 +1,48 @@
1
+ import * as THREE from 'three';
2
+ /** Find a bone by exact name anywhere under `root`. */
3
+ export function findBone(root, name) {
4
+ let found = null;
5
+ root.traverse((o) => { if (!found && o.isBone && o.name === name)
6
+ found = o; });
7
+ return found;
8
+ }
9
+ /** Every bone name under `root` — what an error message should offer. */
10
+ export function boneNames(root) {
11
+ const out = [];
12
+ root.traverse((o) => { if (o.isBone)
13
+ out.push(o.name); });
14
+ return out;
15
+ }
16
+ /**
17
+ * Attach `object` to a socket on `root`.
18
+ *
19
+ * Throws if the bone is missing, and says which bones DO exist. A silent miss
20
+ * here is the worst outcome available: the weapon stays parented where it was,
21
+ * usually at the world origin, and the character walks around empty-handed
22
+ * while every line of the game's equip logic reports success.
23
+ */
24
+ export function attachToSocket(root, socket, object) {
25
+ const bone = findBone(root, socket.bone);
26
+ if (!bone) {
27
+ throw new Error(`[umicat] no bone "${socket.bone}" on this model. It has: ${boneNames(root).join(', ') || '(no bones at all)'}`);
28
+ }
29
+ // A holder, not the object itself: the socket offset and whatever transform
30
+ // the object arrives with are different things, and baking them together
31
+ // means a game cannot move the object within the hand without losing the
32
+ // socket. It also makes detaching exact.
33
+ const holder = new THREE.Object3D();
34
+ holder.name = `socket:${socket.bone}`;
35
+ if (socket.position)
36
+ holder.position.set(socket.position.x, socket.position.y, socket.position.z);
37
+ if (socket.rotation)
38
+ holder.rotation.set(socket.rotation.x, socket.rotation.y, socket.rotation.z);
39
+ if (socket.scale !== undefined)
40
+ holder.scale.setScalar(socket.scale);
41
+ holder.add(object);
42
+ bone.add(holder);
43
+ return {
44
+ bone,
45
+ holder,
46
+ detach() { holder.removeFromParent(); },
47
+ };
48
+ }
package/dist/index.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  export { ThreeUmicat } from './ThreeUmicat.js';
2
2
  export type { UmicatInitOptions } from './ThreeUmicat.js';
3
3
  export * from './scene3d.js';
4
- export { loadScene3D } from './SceneLoader3D.js';
4
+ export { loadScene3D, loadModelAsset } from './SceneLoader3D.js';
5
5
  export { CharacterController3D } from './CharacterController3D.js';
6
6
  export type { CharacterOptions, CharacterState, CharacterInput } from './CharacterController3D.js';
7
7
  export { CharacterAnimator } from './CharacterAnimator.js';
8
8
  export type { ClipMap, CharacterAnimatorOptions } from './CharacterAnimator.js';
9
9
  export { Input3D } from './Input3D.js';
10
- export type { Input3DOptions } from './Input3D.js';
10
+ export type { Input3DOptions, Input3DAction } from './Input3D.js';
11
+ export { attachToSocket, findBone, boneNames } from './Sockets.js';
12
+ export type { Attachment } from './Sockets.js';
11
13
  export type { LoadedScene3D, LoadSceneOptions } from './SceneLoader3D.js';
12
14
  export { ORIENTATION_DIMENSIONS } from '@umicat/platform-sdk/orientation.js';
13
15
  export type { Orientation } from '@umicat/platform-sdk/orientation.js';
package/dist/index.js CHANGED
@@ -5,10 +5,11 @@
5
5
  // that turns it into a three.js scene with physics, and the runtime glue.
6
6
  export { ThreeUmicat } from './ThreeUmicat.js';
7
7
  export * from './scene3d.js';
8
- export { loadScene3D } from './SceneLoader3D.js';
8
+ export { loadScene3D, loadModelAsset } from './SceneLoader3D.js';
9
9
  export { CharacterController3D } from './CharacterController3D.js';
10
10
  export { CharacterAnimator } from './CharacterAnimator.js';
11
11
  export { Input3D } from './Input3D.js';
12
+ export { attachToSocket, findBone, boneNames } from './Sockets.js';
12
13
  // Re-exported so a game imports one package for the common case. A game should
13
14
  // not have to know that identity and saves come from a different package than
14
15
  // the renderer.
package/dist/scene3d.d.ts CHANGED
@@ -92,6 +92,26 @@ export interface Entity3D {
92
92
  * model calls its walk cycle `Walk` fails the same way, only more quietly.
93
93
  */
94
94
  export type AnimationMap = Record<string, string>;
95
+ /**
96
+ * Where a held object sits on a character, named by what it is FOR rather
97
+ * than by which bone it happens to hang off.
98
+ *
99
+ * Rigs do not necessarily have a hand bone — this character's has seven bones
100
+ * and the arm is one of them, so a sword hangs off `arm-right` with an offset
101
+ * that puts it where a hand would be. That offset is a property of the MODEL,
102
+ * not of any game, so it travels in the manifest: tune it once and every game
103
+ * using this character gets a sword in the right place.
104
+ */
105
+ export interface Socket3D {
106
+ /** Bone to hang from. Names are exact; the loader reports what it found. */
107
+ bone: string;
108
+ /** Offset in the bone's local space. */
109
+ position?: Vec3;
110
+ /** Euler angles in radians, applied in the bone's local space. */
111
+ rotation?: Vec3;
112
+ /** Uniform scale for whatever is attached. */
113
+ scale?: number;
114
+ }
95
115
  export interface ModelAsset3D {
96
116
  id: string;
97
117
  /** Resolved by the runtime against the project's asset base url. */
@@ -99,6 +119,8 @@ export interface ModelAsset3D {
99
119
  animations?: AnimationMap;
100
120
  /** Authoring correction applied at load: most models are not born 1 unit tall. */
101
121
  importScale?: number;
122
+ /** Named attachment points — `{ "hand-right": { bone: "arm-right", ... } }`. */
123
+ sockets?: Record<string, Socket3D>;
102
124
  }
103
125
  export interface Light3D {
104
126
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/three-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",