@threenative/core 0.3.0 → 0.3.1

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 (39) hide show
  1. package/README.md +10 -0
  2. package/capabilities.json +1881 -131
  3. package/dist/assets-kyoF7JlJ.d.ts +103 -0
  4. package/dist/{audio-Dp2mXpD3.d.ts → audio-BFiGneTL.d.ts} +62 -0
  5. package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
  6. package/dist/{game-CYIaKhgl.d.ts → game-XGrTzapq.d.ts} +350 -164
  7. package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
  8. package/dist/hot.d.ts +5 -3
  9. package/dist/hot.js +5 -1
  10. package/dist/index.d.ts +813 -143
  11. package/dist/index.js +4548 -941
  12. package/dist/net.d.ts +65 -0
  13. package/dist/net.js +643 -0
  14. package/dist/playtest.d.ts +29 -5
  15. package/dist/playtest.js +181 -35
  16. package/dist/react.d.ts +4 -2
  17. package/dist/{canvas-layer-CtrZHgIh.d.ts → renderer-C6hqZpoG.d.ts} +237 -75
  18. package/dist/world.d.ts +203 -4
  19. package/dist/world.js +2536 -25
  20. package/gpl/LICENSE.GPL +117 -0
  21. package/gpl/convert.py +192 -0
  22. package/gpl/recipes/_common.py +169 -0
  23. package/gpl/recipes/bake_ao.py +111 -0
  24. package/gpl/recipes/decimate.py +64 -0
  25. package/gpl/recipes/retarget.py +131 -0
  26. package/gpl/recipes/unwrap.py +71 -0
  27. package/mcp/blender-server.mjs +632 -0
  28. package/mcp/blender.mjs +27 -0
  29. package/mcp/engine-server.mjs +271 -23
  30. package/mcp/engine.mjs +15 -7
  31. package/mcp/install.d.mts +37 -0
  32. package/mcp/install.mjs +94 -26
  33. package/mcp/servers.d.mts +34 -0
  34. package/mcp/servers.mjs +110 -9
  35. package/package.json +36 -8
  36. package/patches/three@0.185.1.patch +249 -14
  37. package/scripts/ensure-mcp.mjs +20 -13
  38. package/scripts/bundle-engine-mcp.mjs +0 -15
  39. package/scripts/generate-version.mjs +0 -13
@@ -0,0 +1,131 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ #
3
+ # Copyright (C) 2026 ThreeNative contributors
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify it under the terms of the
6
+ # GNU General Public License as published by the Free Software Foundation; either version 2 of the
7
+ # License, or (at your option) any later version. See ../LICENSE.GPL.
8
+ #
9
+ # Move animation clips from one armature's bone names onto another's, so a clip an artist authored
10
+ # for one rig drives a different character. It renames the tracks; it invents no motion.
11
+ #
12
+ # blender --background --factory-startup --python retarget.py -- \
13
+ # '{"source":"/abs/clips.fbx","target":"/abs/character.glb","out":"/abs/out.glb",
14
+ # "map":{"mixamorig:Hips":"hips","mixamorig:Spine":"spine"}}'
15
+ #
16
+ # Adapt it: a real retarget usually also matches rest poses and bone roll. This one is the honest
17
+ # minimum — name mapping with a fail-closed check that every track found a destination bone — and
18
+ # it is the shape to copy when a game needs the rest-pose correction too.
19
+
20
+ import os
21
+ import sys
22
+
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+
25
+ import bpy # noqa: E402
26
+
27
+ from _common import ( # noqa: E402
28
+ action_fcurves,
29
+ assign_action,
30
+ emit,
31
+ export,
32
+ fail,
33
+ load,
34
+ new_action,
35
+ request,
36
+ )
37
+
38
+ BONE_PATH_PREFIX = 'pose.bones["'
39
+
40
+
41
+ def bone_of(data_path):
42
+ if not data_path.startswith(BONE_PATH_PREFIX):
43
+ return None
44
+ rest = data_path[len(BONE_PATH_PREFIX) :]
45
+ end = rest.find('"]')
46
+ return None if end == -1 else rest[:end]
47
+
48
+
49
+ def rename_bone(data_path, name):
50
+ rest = data_path[len(BONE_PATH_PREFIX) :]
51
+ end = rest.find('"]')
52
+ return BONE_PATH_PREFIX + name + rest[end:]
53
+
54
+
55
+ payload = request()
56
+ source = payload.get("source")
57
+ target = payload.get("target")
58
+ out = payload.get("out")
59
+ mapping = payload.get("map")
60
+
61
+ for key, value in (("source", source), ("target", target), ("out", out)):
62
+ if not isinstance(value, str) or not value:
63
+ fail("retarget requires a '%s' path" % key)
64
+ if not isinstance(mapping, dict) or not mapping:
65
+ fail("retarget requires a non-empty 'map' of source bone name -> destination bone name")
66
+
67
+ # Load the clips first and keep their actions, then open the destination and bring them across.
68
+ load(source)
69
+ clips = {}
70
+ for action in bpy.data.actions:
71
+ clips[action.name] = [
72
+ (
73
+ curve.data_path,
74
+ curve.array_index,
75
+ [(point.co[0], point.co[1]) for point in curve.keyframe_points],
76
+ )
77
+ for curve in action_fcurves(action)
78
+ ]
79
+ if not clips:
80
+ fail("'%s' carries no animation actions to retarget" % source)
81
+
82
+ load(target)
83
+ armatures = [item for item in bpy.data.objects if item.type == "ARMATURE"]
84
+ if not armatures:
85
+ fail("'%s' has no armature to retarget onto" % target)
86
+ rig = armatures[0]
87
+ destination_bones = {bone.name for bone in rig.data.bones}
88
+
89
+ missing = sorted({name for name in mapping.values() if name not in destination_bones})
90
+ if missing:
91
+ fail(
92
+ "destination armature '%s' has no bone(s) %s; it has %s"
93
+ % (rig.name, missing, sorted(destination_bones))
94
+ )
95
+
96
+ rig.animation_data_create()
97
+ retargeted = []
98
+ skipped = []
99
+ for name, curves in sorted(clips.items()):
100
+ action, fcurves, slot = new_action("%s_retargeted" % name, rig)
101
+ written = 0
102
+ for data_path, array_index, points in curves:
103
+ bone = bone_of(data_path)
104
+ if bone is None:
105
+ continue
106
+ destination = mapping.get(bone)
107
+ if destination is None:
108
+ skipped.append(bone)
109
+ continue
110
+ curve = fcurves.new(rename_bone(data_path, destination), index=array_index)
111
+ for frame, value in points:
112
+ curve.keyframe_points.insert(frame, value)
113
+ written += 1
114
+ if written == 0:
115
+ fail("clip '%s' produced no tracks on the destination armature" % name)
116
+ assign_action(rig, action, slot)
117
+ retargeted.append(action.name)
118
+
119
+ export(out)
120
+ emit(
121
+ {
122
+ "clips": sorted(retargeted),
123
+ "destinationBones": sorted(destination_bones),
124
+ "out": out,
125
+ "outBytes": os.path.getsize(out),
126
+ "recipe": "retarget",
127
+ "skippedBones": sorted(set(skipped)),
128
+ "source": source,
129
+ "target": target,
130
+ }
131
+ )
@@ -0,0 +1,71 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ #
3
+ # Copyright (C) 2026 ThreeNative contributors
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify it under the terms of the
6
+ # GNU General Public License as published by the Free Software Foundation; either version 2 of the
7
+ # License, or (at your option) any later version. See ../LICENSE.GPL.
8
+ #
9
+ # Give a mesh a UV layer it does not have, so it can carry a texture or a lightmap at all. It
10
+ # computes a parameterisation from the geometry; it chooses no texture and no material.
11
+ #
12
+ # blender --background --factory-startup --python unwrap.py -- \
13
+ # '{"source":"/abs/in.glb","out":"/abs/out.glb","angleLimit":66,"islandMargin":0.02}'
14
+ #
15
+ # Adapt it: `smart_project` is the general answer. A character usually wants seams an artist placed
16
+ # and `bpy.ops.uv.unwrap(method="ANGLE_BASED")` instead.
17
+
18
+ import math
19
+ import os
20
+ import sys
21
+
22
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
+
24
+ import bpy # noqa: E402
25
+
26
+ from _common import emit, export, fail, load, meshes, request, uv_layers # noqa: E402
27
+
28
+ payload = request()
29
+ source = payload.get("source")
30
+ out = payload.get("out")
31
+ angle_limit = payload.get("angleLimit", 66.0)
32
+ island_margin = payload.get("islandMargin", 0.02)
33
+ only_missing = payload.get("onlyMissing", True)
34
+
35
+ if not isinstance(source, str) or not source:
36
+ fail("unwrap requires a 'source' path")
37
+ if not isinstance(out, str) or not out:
38
+ fail("unwrap requires an 'out' path")
39
+
40
+ load(source)
41
+ before = uv_layers()
42
+ unwrapped = []
43
+ for item in meshes():
44
+ if only_missing and len(item.data.uv_layers) > 0:
45
+ continue
46
+ bpy.context.view_layer.objects.active = item
47
+ for other in bpy.context.selected_objects:
48
+ other.select_set(False)
49
+ item.select_set(True)
50
+ bpy.ops.object.mode_set(mode="EDIT")
51
+ bpy.ops.mesh.select_all(action="SELECT")
52
+ bpy.ops.uv.smart_project(
53
+ angle_limit=math.radians(float(angle_limit)), island_margin=float(island_margin)
54
+ )
55
+ bpy.ops.object.mode_set(mode="OBJECT")
56
+ unwrapped.append(item.name)
57
+
58
+ after = uv_layers()
59
+ export(out)
60
+ emit(
61
+ {
62
+ "meshes": len(meshes()),
63
+ "out": out,
64
+ "outBytes": os.path.getsize(out),
65
+ "recipe": "unwrap",
66
+ "source": source,
67
+ "unwrapped": sorted(unwrapped),
68
+ "uvLayersAfter": after,
69
+ "uvLayersBefore": before,
70
+ }
71
+ )