@jgengine/shell 0.9.0 → 0.11.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.
Files changed (248) hide show
  1. package/CHANGELOG.md +34 -3
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/GamePhaseStamp.d.ts +1 -0
  5. package/dist/GamePhaseStamp.js +9 -0
  6. package/dist/GamePlayerShell.d.ts +4 -31
  7. package/dist/GamePlayerShell.js +162 -932
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.d.ts +8 -0
  10. package/dist/audio/audioEngine.js +63 -8
  11. package/dist/audio/audioWire.d.ts +22 -0
  12. package/dist/audio/audioWire.js +19 -0
  13. package/dist/audio/musicDirector.d.ts +31 -0
  14. package/dist/audio/musicDirector.js +125 -0
  15. package/dist/audio/musicVoices.d.ts +10 -0
  16. package/dist/audio/musicVoices.js +338 -0
  17. package/dist/audio/synthEngine.d.ts +9 -0
  18. package/dist/audio/synthEngine.js +57 -0
  19. package/dist/behaviour.d.ts +2 -1
  20. package/dist/behaviour.js +2 -1
  21. package/dist/behaviourAttach.d.ts +1 -0
  22. package/dist/behaviourAttach.js +1 -0
  23. package/dist/behaviourDriver.d.ts +1 -0
  24. package/dist/behaviourDriver.js +1 -0
  25. package/dist/boundActionDispatch.d.ts +13 -0
  26. package/dist/boundActionDispatch.js +37 -0
  27. package/dist/camera/GameCameraRig.d.ts +5 -2
  28. package/dist/camera/GameCameraRig.js +2 -2
  29. package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
  30. package/dist/camera/GameFirstPersonCamera.js +29 -7
  31. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  32. package/dist/camera/GameInspectionCamera.js +13 -2
  33. package/dist/camera/GameOrbitCamera.js +59 -10
  34. package/dist/camera/cameraBlendMath.d.ts +3 -0
  35. package/dist/camera/cameraBlendMath.js +3 -0
  36. package/dist/camera/fovPreference.d.ts +6 -1
  37. package/dist/camera/fovPreference.js +6 -1
  38. package/dist/camera/index.d.ts +4 -7
  39. package/dist/camera/index.js +4 -7
  40. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  41. package/dist/camera/inspectionCameraMath.js +7 -2
  42. package/dist/camera/orbitCameraMath.d.ts +54 -6
  43. package/dist/camera/orbitCameraMath.js +61 -6
  44. package/dist/camera/rigMath.d.ts +94 -24
  45. package/dist/camera/rigMath.js +100 -24
  46. package/dist/camera/rigResolve.d.ts +4 -2
  47. package/dist/camera/rigResolve.js +4 -2
  48. package/dist/camera/shakeChannel.d.ts +6 -2
  49. package/dist/camera/shakeChannel.js +6 -2
  50. package/dist/camera/shakeChannelMath.d.ts +1 -0
  51. package/dist/camera/shakeChannelMath.js +1 -0
  52. package/dist/cartridge/runtime.d.ts +15 -0
  53. package/dist/cartridge/runtime.js +556 -0
  54. package/dist/cartridge/spec.d.ts +188 -0
  55. package/dist/cartridge/spec.js +19 -0
  56. package/dist/cartridge/validate.d.ts +2 -0
  57. package/dist/cartridge/validate.js +78 -0
  58. package/dist/cartridge.d.ts +1 -1
  59. package/dist/cartridge.js +4 -3
  60. package/dist/commandSink.d.ts +25 -0
  61. package/dist/commandSink.js +32 -0
  62. package/dist/defineGame.js +26 -5
  63. package/dist/devtools/ColPanel.d.ts +1 -0
  64. package/dist/devtools/ColPanel.js +14 -0
  65. package/dist/devtools/CollisionDebugWorld.js +1 -0
  66. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  67. package/dist/devtools/DevtoolsOverlay.js +19 -450
  68. package/dist/devtools/KeysPanel.d.ts +4 -0
  69. package/dist/devtools/KeysPanel.js +31 -0
  70. package/dist/devtools/LogsPanel.d.ts +1 -0
  71. package/dist/devtools/LogsPanel.js +12 -0
  72. package/dist/devtools/NetPanel.d.ts +4 -0
  73. package/dist/devtools/NetPanel.js +10 -0
  74. package/dist/devtools/PerfPanel.d.ts +4 -0
  75. package/dist/devtools/PerfPanel.js +53 -0
  76. package/dist/devtools/TunePanel.d.ts +3 -0
  77. package/dist/devtools/TunePanel.js +309 -0
  78. package/dist/devtools/agentBridge.d.ts +41 -0
  79. package/dist/devtools/agentBridge.js +262 -0
  80. package/dist/devtools/collisionDebug.d.ts +6 -0
  81. package/dist/devtools/collisionDebug.js +6 -0
  82. package/dist/devtools/collisionDebugMath.d.ts +9 -2
  83. package/dist/devtools/collisionDebugMath.js +25 -5
  84. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  85. package/dist/devtools/devtoolsOverrides.js +48 -0
  86. package/dist/devtools/panelAtoms.d.ts +9 -0
  87. package/dist/devtools/panelAtoms.js +10 -0
  88. package/dist/devtools/perfDiagnose.d.ts +2 -0
  89. package/dist/devtools/perfDiagnose.js +43 -0
  90. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  91. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  92. package/dist/drivers/FrameDriver.d.ts +37 -0
  93. package/dist/drivers/FrameDriver.js +232 -0
  94. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  95. package/dist/drivers/HudOnlyDriver.js +60 -0
  96. package/dist/environment/Daylight.d.ts +29 -7
  97. package/dist/environment/Daylight.js +153 -13
  98. package/dist/environment/EnvironmentScene.js +42 -7
  99. package/dist/environment/RoadRibbons.d.ts +7 -0
  100. package/dist/environment/RoadRibbons.js +57 -0
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/hotbarActions.d.ts +26 -0
  112. package/dist/hotbarActions.js +27 -0
  113. package/dist/inputSink.d.ts +30 -0
  114. package/dist/inputSink.js +85 -0
  115. package/dist/materialOverride.d.ts +10 -0
  116. package/dist/materialOverride.js +15 -3
  117. package/dist/multiplayer.js +14 -4
  118. package/dist/postfx/PostProcessing.d.ts +10 -0
  119. package/dist/postfx/PostProcessing.js +120 -0
  120. package/dist/postfx/gradeShader.d.ts +4 -0
  121. package/dist/postfx/gradeShader.js +72 -0
  122. package/dist/registry.d.ts +3 -2
  123. package/dist/render/EnvironmentLighting.d.ts +10 -0
  124. package/dist/render/EnvironmentLighting.js +34 -0
  125. package/dist/render/SceneLighting.d.ts +7 -0
  126. package/dist/render/SceneLighting.js +18 -0
  127. package/dist/render/SceneModels.d.ts +14 -0
  128. package/dist/render/SceneModels.js +337 -0
  129. package/dist/render/defaultSurface.d.ts +29 -0
  130. package/dist/render/defaultSurface.js +101 -0
  131. package/dist/render/modelLoad.d.ts +24 -0
  132. package/dist/render/modelLoad.js +73 -0
  133. package/dist/render/modelRender.d.ts +11 -0
  134. package/dist/render/modelRender.js +15 -0
  135. package/dist/render/resolveModel.d.ts +39 -3
  136. package/dist/render/resolveModel.js +79 -2
  137. package/dist/render/useEntityRenderCues.d.ts +13 -0
  138. package/dist/render/useEntityRenderCues.js +62 -0
  139. package/dist/scatter/InstancedScatter.d.ts +24 -0
  140. package/dist/scatter/InstancedScatter.js +130 -0
  141. package/dist/scatter/scatterModels.d.ts +25 -0
  142. package/dist/scatter/scatterModels.js +61 -0
  143. package/dist/scatter/scatterProxies.d.ts +6 -0
  144. package/dist/scatter/scatterProxies.js +94 -0
  145. package/dist/scatter.d.ts +1 -0
  146. package/dist/scatter.js +1 -0
  147. package/dist/scene/AuthoredScene.d.ts +82 -0
  148. package/dist/scene/AuthoredScene.js +173 -0
  149. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  150. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  151. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  152. package/dist/scene/GrassFieldRenderer.js +25 -0
  153. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  154. package/dist/scene/SoilPatchRenderer.js +21 -0
  155. package/dist/scene/StudioStage.d.ts +37 -0
  156. package/dist/scene/StudioStage.js +42 -0
  157. package/dist/scene/WaterRenderer.d.ts +2 -0
  158. package/dist/scene/WaterRenderer.js +37 -0
  159. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  160. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  161. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  162. package/dist/scene/sceneKindRenderers.js +17 -0
  163. package/dist/scene.d.ts +1 -0
  164. package/dist/scene.js +1 -0
  165. package/dist/settings/appliedSettings.d.ts +5 -0
  166. package/dist/settings/appliedSettings.js +9 -0
  167. package/dist/settings/settingsController.d.ts +2 -0
  168. package/dist/settings/settingsController.js +22 -1
  169. package/dist/shellConstants.d.ts +4 -0
  170. package/dist/shellConstants.js +4 -0
  171. package/dist/shellMovement.d.ts +8 -0
  172. package/dist/shellMovement.js +11 -0
  173. package/dist/shellPointer.d.ts +20 -0
  174. package/dist/shellPointer.js +28 -0
  175. package/dist/structures/PlacementGhost.d.ts +4 -0
  176. package/dist/structures/PlacementGhost.js +4 -0
  177. package/dist/structures/TransformGizmo.d.ts +43 -0
  178. package/dist/structures/TransformGizmo.js +55 -0
  179. package/dist/structures/index.d.ts +1 -0
  180. package/dist/structures/index.js +1 -0
  181. package/dist/terrain/CarvedTerrain.d.ts +4 -1
  182. package/dist/terrain/CarvedTerrain.js +4 -3
  183. package/dist/terrain/GrassField.js +1 -1
  184. package/dist/terrain/TerraformBrushCursor.js +3 -0
  185. package/dist/terrain/grassBudget.d.ts +11 -2
  186. package/dist/terrain/grassBudget.js +20 -4
  187. package/dist/terrain/grassGeometry.d.ts +3 -0
  188. package/dist/terrain/grassGeometry.js +3 -0
  189. package/dist/terrain/grassMaterial.d.ts +2 -0
  190. package/dist/terrain/grassMaterial.js +2 -0
  191. package/dist/terrain/random.d.ts +3 -0
  192. package/dist/terrain/random.js +3 -0
  193. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  194. package/dist/terrain/soilPatchMaterial.js +109 -0
  195. package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
  196. package/dist/terrain/terrainDetailMaterial.js +157 -0
  197. package/dist/terrain/terrainMath.d.ts +8 -1
  198. package/dist/terrain/terrainMath.js +8 -1
  199. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  200. package/dist/touch/TouchControlsOverlay.js +211 -22
  201. package/dist/useShellMultiplayerSync.d.ts +9 -0
  202. package/dist/useShellMultiplayerSync.js +98 -0
  203. package/dist/visibility/CullingProvider.js +1 -1
  204. package/dist/water/OceanConfig.d.ts +2 -0
  205. package/dist/water/OceanConfig.js +2 -0
  206. package/dist/weather/fireSpreadPose.d.ts +1 -0
  207. package/dist/weather/fireSpreadPose.js +1 -0
  208. package/dist/weather/weatherGeometry.d.ts +1 -0
  209. package/dist/weather/weatherGeometry.js +1 -0
  210. package/dist/weather/weatherMath.d.ts +3 -0
  211. package/dist/weather/weatherMath.js +3 -0
  212. package/dist/weather/weatherUniforms.d.ts +3 -0
  213. package/dist/weather/weatherUniforms.js +3 -0
  214. package/dist/world/RetainedVfx.d.ts +10 -0
  215. package/dist/world/RetainedVfx.js +136 -0
  216. package/dist/world/WorldHud.d.ts +52 -3
  217. package/dist/world/WorldHud.js +94 -9
  218. package/dist/world/WorldScene.d.ts +21 -0
  219. package/dist/world/WorldScene.js +159 -0
  220. package/dist/world/WorldVfx.d.ts +9 -0
  221. package/dist/world/WorldVfx.js +184 -0
  222. package/dist/world/entityPose.d.ts +2 -0
  223. package/dist/world/entityPose.js +2 -0
  224. package/dist/world/floatTextStyle.d.ts +1 -0
  225. package/dist/world/floatTextStyle.js +1 -0
  226. package/dist/world/telegraphPulse.d.ts +1 -0
  227. package/dist/world/telegraphPulse.js +1 -0
  228. package/dist/world/worldBarSamples.d.ts +26 -1
  229. package/dist/world/worldBarSamples.js +45 -1
  230. package/dist/worldSky.d.ts +5 -0
  231. package/dist/worldSky.js +11 -0
  232. package/dist/worldSync.d.ts +11 -0
  233. package/dist/worldSync.js +20 -0
  234. package/package.json +6 -6
  235. package/skills/jgengine/SKILL.md +51 -0
  236. package/skills/jgengine/api.md +434 -0
  237. package/skills/jgengine/capabilities.md +29 -0
  238. package/skills/jgengine/reference-cartridge.md +72 -0
  239. package/skills/jgengine-ui/SKILL.md +36 -0
  240. package/skills/jgengine-ui/api.md +1852 -0
  241. package/skills/jgengine-ui/capabilities.md +142 -0
  242. package/skills/jgengine-ui/reference.md +500 -0
  243. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  244. package/skills/jgengine-world/SKILL.md +38 -0
  245. package/skills/jgengine-world/api.md +2652 -0
  246. package/skills/jgengine-world/capabilities.md +370 -0
  247. package/skills/jgengine-world/reference.md +222 -0
  248. package/llms.txt +0 -2643
@@ -0,0 +1,337 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame, useLoader } from "@react-three/fiber";
3
+ import { useTexture } from "@react-three/drei";
4
+ import { Component, Suspense, useEffect, useMemo, useRef } from "react";
5
+ import * as THREE from "three";
6
+ import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
7
+ import { useGameContext } from "@jgengine/react/provider";
8
+ import { sharedGltfLoader } from "./modelLoad.js";
9
+ import { applyMaterialOverride } from "../materialOverride.js";
10
+ import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
11
+ export function EntitySprite({ sprite }) {
12
+ const texture = useLoader(THREE.TextureLoader, sprite.url);
13
+ useEffect(() => {
14
+ texture.colorSpace = THREE.SRGBColorSpace;
15
+ texture.anisotropy = 4;
16
+ texture.needsUpdate = true;
17
+ }, [texture]);
18
+ return (_jsx("sprite", { "position-y": sprite.y, scale: [sprite.width, sprite.height, 1], children: _jsx("spriteMaterial", { map: texture, transparent: true, alphaTest: 0.08, depthWrite: false }) }));
19
+ }
20
+ class ModelFallbackBoundary extends Component {
21
+ state = { failed: false };
22
+ static getDerivedStateFromError() {
23
+ return { failed: true };
24
+ }
25
+ render() {
26
+ return this.state.failed ? this.props.fallback : this.props.children;
27
+ }
28
+ }
29
+ export function IsolatedEntityModel({ model, instanceId, fallback, }) {
30
+ return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
31
+ }
32
+ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
33
+ const gltf = useLoader(sharedGltfLoader, model.url);
34
+ const weaponScene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
35
+ const px = position?.[0] ?? 0;
36
+ const py = position?.[1] ?? 0;
37
+ const pz = position?.[2] ?? 0;
38
+ const rx = rotation?.[0] ?? 0;
39
+ const ry = rotation?.[1] ?? 0;
40
+ const rz = rotation?.[2] ?? 0;
41
+ const s = scale ?? 1;
42
+ useEffect(() => {
43
+ const bone = rig.getObjectByName(slot);
44
+ if (bone === undefined) {
45
+ if (typeof console !== "undefined") {
46
+ console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
47
+ }
48
+ return;
49
+ }
50
+ weaponScene.position.set(px, py, pz);
51
+ weaponScene.rotation.set(rx, ry, rz);
52
+ weaponScene.scale.setScalar(s);
53
+ bone.add(weaponScene);
54
+ return () => {
55
+ bone.remove(weaponScene);
56
+ };
57
+ }, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
58
+ useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
59
+ return null;
60
+ }
61
+ /** Renders a static kit-of-parts child at a fixed local offset under the parent model — no bone/rig lookup, unlike `BoneAttachment`. The nested `EntityModel` still applies its own `dims`/anchor centering, so scale/pivot sanity carries over per part. */
62
+ function ModelPartGroup({ model, position, rotation, scale, }) {
63
+ return (_jsx("group", { position: position ?? [0, 0, 0], rotation: rotation ?? [0, 0, 0], scale: scale ?? 1, children: _jsx(EntityModel, { model: model }) }));
64
+ }
65
+ function ModelMaterialMapsApplier({ scene, maps }) {
66
+ const entries = useMemo(() => {
67
+ const record = {};
68
+ if (maps.color !== undefined)
69
+ record.color = maps.color;
70
+ if (maps.normal !== undefined)
71
+ record.normal = maps.normal;
72
+ if (maps.roughness !== undefined)
73
+ record.roughness = maps.roughness;
74
+ if (maps.ao !== undefined)
75
+ record.ao = maps.ao;
76
+ return record;
77
+ }, [maps.color, maps.normal, maps.roughness, maps.ao]);
78
+ const textures = useTexture(entries);
79
+ useEffect(() => {
80
+ if (textures.color !== undefined)
81
+ textures.color.colorSpace = THREE.SRGBColorSpace;
82
+ applyMaterialOverride(scene, {}, { clone: false, textures });
83
+ }, [scene, textures]);
84
+ return null;
85
+ }
86
+ export function EntityModel({ model, instanceId }) {
87
+ const gltf = useLoader(sharedGltfLoader, model.url);
88
+ const ctx = useGameContext();
89
+ const material = model.material;
90
+ const baseY = model.y ?? 0;
91
+ const dims = model.dims;
92
+ const scene = useMemo(() => {
93
+ const cloned = cloneModelScene(gltf.scene);
94
+ if (material !== undefined)
95
+ applyMaterialOverride(cloned, material, { clone: false });
96
+ return cloned;
97
+ }, [gltf, material]);
98
+ const measured = useMemo(() => {
99
+ if (model.targetHeight === undefined)
100
+ return null;
101
+ const box = new THREE.Box3().setFromObject(scene);
102
+ const height = box.max.y - box.min.y;
103
+ if (!Number.isFinite(height) || height <= 0)
104
+ return null;
105
+ return {
106
+ normalize: model.targetHeight / height,
107
+ minY: box.min.y,
108
+ centerX: (box.min.x + box.max.x) / 2,
109
+ centerZ: (box.min.z + box.max.z) / 2,
110
+ };
111
+ }, [scene, model.targetHeight]);
112
+ const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
113
+ const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
114
+ const position = measured !== null
115
+ ? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
116
+ : centered
117
+ ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
118
+ : [0, baseY, 0];
119
+ useEffect(() => () => {
120
+ disposeClonedMaterials(scene);
121
+ }, [scene]);
122
+ const animation = model.animation;
123
+ const mixerRef = useRef(null);
124
+ const animationPausedRef = useRef(false);
125
+ const stateActionsRef = useRef(null);
126
+ const states = animation?.states;
127
+ const oneShots = animation?.oneShots;
128
+ const oneShotPlayRef = useRef(null);
129
+ const activeOneShotRef = useRef(null);
130
+ useEffect(() => {
131
+ if (animation === undefined || gltf.animations.length === 0) {
132
+ mixerRef.current = null;
133
+ stateActionsRef.current = null;
134
+ return;
135
+ }
136
+ const mixer = new THREE.AnimationMixer(scene);
137
+ if (states !== undefined) {
138
+ const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
139
+ const actions = {
140
+ idle: mixer.clipAction(clipFor(states.idle)),
141
+ walk: mixer.clipAction(clipFor(states.walk)),
142
+ ...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
143
+ };
144
+ for (const action of Object.values(actions)) {
145
+ action.setLoop(THREE.LoopRepeat, Infinity);
146
+ action.timeScale = animation.timeScale ?? 1;
147
+ action.enabled = true;
148
+ }
149
+ actions.idle.play();
150
+ mixer.update(0);
151
+ mixerRef.current = mixer;
152
+ stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
153
+ animationPausedRef.current = false;
154
+ let onOneShotFinished = null;
155
+ if (oneShots !== undefined) {
156
+ const clipNames = new Set();
157
+ for (const spec of Object.values(oneShots)) {
158
+ if (typeof spec === "string")
159
+ clipNames.add(spec);
160
+ else
161
+ for (const name of spec)
162
+ clipNames.add(name);
163
+ }
164
+ const oneShotActions = new Map();
165
+ for (const name of clipNames) {
166
+ const found = THREE.AnimationClip.findByName(gltf.animations, name);
167
+ if (found === null)
168
+ continue;
169
+ const oneShotAction = mixer.clipAction(found);
170
+ oneShotAction.setLoop(THREE.LoopOnce, 1);
171
+ oneShotAction.enabled = true;
172
+ oneShotActions.set(name, oneShotAction);
173
+ }
174
+ onOneShotFinished = ({ action }) => {
175
+ const active = activeOneShotRef.current;
176
+ if (active === null || action !== active.action || active.isDeath)
177
+ return;
178
+ const machine = stateActionsRef.current;
179
+ const back = machine?.actions[machine.active];
180
+ action.fadeOut(0.15);
181
+ if (back !== undefined)
182
+ back.reset().fadeIn(0.15).play();
183
+ activeOneShotRef.current = null;
184
+ };
185
+ mixer.addEventListener("finished", onOneShotFinished);
186
+ oneShotPlayRef.current = (event) => {
187
+ const active = activeOneShotRef.current;
188
+ if (active !== null && active.isDeath)
189
+ return;
190
+ const clipName = resolveOneShotClip(oneShots, event, Math.random());
191
+ if (clipName === null)
192
+ return;
193
+ const oneShotAction = oneShotActions.get(clipName);
194
+ if (oneShotAction === undefined)
195
+ return;
196
+ const machine = stateActionsRef.current;
197
+ machine?.actions[machine.active]?.fadeOut(0.1);
198
+ if (active !== null && active.action !== oneShotAction)
199
+ active.action.stop();
200
+ oneShotAction.clampWhenFinished = event === "death";
201
+ oneShotAction.reset().fadeIn(0.1).play();
202
+ activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
203
+ };
204
+ }
205
+ return () => {
206
+ if (onOneShotFinished !== null)
207
+ mixer.removeEventListener("finished", onOneShotFinished);
208
+ oneShotPlayRef.current = null;
209
+ activeOneShotRef.current = null;
210
+ mixer.stopAllAction();
211
+ mixerRef.current = null;
212
+ stateActionsRef.current = null;
213
+ };
214
+ }
215
+ const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
216
+ gltf.animations[0];
217
+ const action = mixer.clipAction(clip);
218
+ action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
219
+ action.clampWhenFinished = animation.loop === false;
220
+ action.timeScale = animation.timeScale ?? 1;
221
+ action.enabled = true;
222
+ action.paused = animation.paused === true;
223
+ action.play();
224
+ if (animation.time !== undefined)
225
+ action.time = animation.time;
226
+ mixer.update(0);
227
+ mixerRef.current = mixer;
228
+ animationPausedRef.current = animation.paused === true;
229
+ return () => {
230
+ mixer.stopAllAction();
231
+ mixerRef.current = null;
232
+ };
233
+ }, [
234
+ scene,
235
+ gltf,
236
+ animation?.clip,
237
+ animation?.loop,
238
+ animation?.timeScale,
239
+ animation?.paused,
240
+ animation?.time,
241
+ states,
242
+ oneShots,
243
+ ]);
244
+ useEffect(() => {
245
+ if (instanceId === undefined || oneShots === undefined)
246
+ return;
247
+ const fire = (event) => oneShotPlayRef.current?.(event);
248
+ const offAnimation = ctx.game.events.on("entity.animation", (event) => {
249
+ if (event.instanceId === instanceId)
250
+ fire(event.event);
251
+ });
252
+ const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
253
+ if (event.instanceId === instanceId)
254
+ fire("hit");
255
+ });
256
+ const offDied = ctx.game.events.on("entity.died", (event) => {
257
+ if (event.instanceId === instanceId)
258
+ fire("death");
259
+ });
260
+ return () => {
261
+ offAnimation();
262
+ offHit();
263
+ offDied();
264
+ };
265
+ }, [ctx, instanceId, oneShots]);
266
+ const paintCanvasRef = useRef(null);
267
+ const paintDrawnCountRef = useRef(0);
268
+ const paintVersionRef = useRef(-1);
269
+ const materialCacheRef = useRef(null);
270
+ useEffect(() => () => {
271
+ if (paintCanvasRef.current !== null) {
272
+ disposePaintCanvas(paintCanvasRef.current);
273
+ paintCanvasRef.current = null;
274
+ }
275
+ paintDrawnCountRef.current = 0;
276
+ paintVersionRef.current = -1;
277
+ materialCacheRef.current = null;
278
+ }, [scene]);
279
+ useFrame((_state, delta) => {
280
+ const stateMachine = stateActionsRef.current;
281
+ if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
282
+ const entity = ctx.scene.entity.get(instanceId);
283
+ if (entity !== null) {
284
+ const [x, , z] = entity.position;
285
+ if (stateMachine.lastPos !== null) {
286
+ const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
287
+ stateMachine.smoothedSpeed +=
288
+ (instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
289
+ }
290
+ stateMachine.lastPos = [x, entity.position[1], z];
291
+ const walkSpeed = states.walkSpeed ?? 0.5;
292
+ const runSpeed = states.runSpeed ?? 6;
293
+ const next = stateMachine.smoothedSpeed < walkSpeed
294
+ ? "idle"
295
+ : stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
296
+ ? "run"
297
+ : "walk";
298
+ if (next !== stateMachine.active) {
299
+ if (activeOneShotRef.current === null) {
300
+ const fade = states.fadeSec ?? 0.2;
301
+ const from = stateMachine.actions[stateMachine.active];
302
+ const to = stateMachine.actions[next];
303
+ if (from !== undefined && to !== undefined) {
304
+ to.reset().fadeIn(fade).play();
305
+ from.fadeOut(fade);
306
+ }
307
+ }
308
+ stateMachine.active = next;
309
+ }
310
+ }
311
+ }
312
+ if (mixerRef.current !== null && !animationPausedRef.current)
313
+ mixerRef.current.update(delta);
314
+ if (instanceId === undefined)
315
+ return;
316
+ const paint = ctx.scene.entity.paint;
317
+ const version = paint.version(instanceId);
318
+ if (version === paintVersionRef.current)
319
+ return;
320
+ paintVersionRef.current = version;
321
+ const strokes = paint.strokes(instanceId);
322
+ const cache = cacheStandardMaterials(scene, materialCacheRef.current);
323
+ materialCacheRef.current = cache;
324
+ if (paintCanvasRef.current === null) {
325
+ if (strokes.length === 0)
326
+ return;
327
+ const seed = cache.materials[0];
328
+ if (seed === undefined)
329
+ return;
330
+ const paintCanvas = createPaintCanvas(seed);
331
+ paintCanvasRef.current = paintCanvas;
332
+ applyPaintTextureToMaterials(cache.materials, paintCanvas);
333
+ }
334
+ paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
335
+ });
336
+ return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), material?.maps !== undefined ? _jsx(ModelMaterialMapsApplier, { scene: scene, maps: material.maps }) : null, (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`))), (model.parts ?? []).map((part, index) => typeof part.model === "string" ? null : (_jsx(ModelPartGroup, { model: part.model, position: part.position, rotation: part.rotation, scale: part.scale }, index)))] }));
337
+ }
@@ -0,0 +1,29 @@
1
+ import { type ThreeElements } from "@react-three/fiber";
2
+ import * as THREE from "three";
3
+ /**
4
+ * Shared, lazily-built procedural detail maps (a subtle tangent-space normal map plus a
5
+ * roughness-variation map) used by every un-modeled primitive so a flat colored box reads as a
6
+ * surface, not plastic. Pure JS `DataTexture` — no canvas/DOM, so it imports safely under `bun test`.
7
+ * @internal shell-internal default render surface; games never import it.
8
+ */
9
+ export declare function detailMaps(): {
10
+ normal: THREE.DataTexture;
11
+ roughness: THREE.DataTexture;
12
+ };
13
+ /**
14
+ * Props for {@link DefaultSurface}: a required base `color` plus optional PBR overrides.
15
+ * @internal shell-internal default render surface; games never import it.
16
+ */
17
+ export interface DefaultSurfaceProps extends Omit<ThreeElements["meshStandardMaterial"], "args" | "normalMap" | "roughnessMap"> {
18
+ color: THREE.ColorRepresentation;
19
+ roughness?: number;
20
+ metalness?: number;
21
+ detail?: boolean;
22
+ }
23
+ /**
24
+ * The upgraded default material for un-modeled entities/objects: tuned PBR roughness/metalness plus
25
+ * the shared procedural detail maps, so primitive fallbacks catch light and image-based reflections
26
+ * instead of reading as flat plastic. Drop-in replacement for a bare `<meshStandardMaterial color>`.
27
+ * @internal shell-internal default render surface; games never import it.
28
+ */
29
+ export declare function DefaultSurface({ color, roughness, metalness, detail, ...rest }: DefaultSurfaceProps): import("react").JSX.Element;
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import {} from "@react-three/fiber";
3
+ import { useMemo } from "react";
4
+ import * as THREE from "three";
5
+ const DETAIL_SIZE = 64;
6
+ const DETAIL_REPEAT = 3;
7
+ const NORMAL_STRENGTH = 2.2;
8
+ const ROUGHNESS_SPREAD = 0.14;
9
+ function periodicValueNoise(size, cells, seed) {
10
+ const lattice = new Float32Array(cells * cells);
11
+ for (let index = 0; index < lattice.length; index += 1) {
12
+ const h = Math.sin((index + 1) * 12.9898 + seed * 78.233) * 43758.5453;
13
+ lattice[index] = h - Math.floor(h);
14
+ }
15
+ const at = (cx, cy) => lattice[((cy % cells) + cells) % cells * cells + (((cx % cells) + cells) % cells)];
16
+ const field = new Float32Array(size * size);
17
+ const step = cells / size;
18
+ for (let y = 0; y < size; y += 1) {
19
+ for (let x = 0; x < size; x += 1) {
20
+ const fx = x * step;
21
+ const fy = y * step;
22
+ const x0 = Math.floor(fx);
23
+ const y0 = Math.floor(fy);
24
+ const tx = fx - x0;
25
+ const ty = fy - y0;
26
+ const sx = tx * tx * (3 - 2 * tx);
27
+ const sy = ty * ty * (3 - 2 * ty);
28
+ const top = at(x0, y0) * (1 - sx) + at(x0 + 1, y0) * sx;
29
+ const bottom = at(x0, y0 + 1) * (1 - sx) + at(x0 + 1, y0 + 1) * sx;
30
+ field[y * size + x] = top * (1 - sy) + bottom * sy;
31
+ }
32
+ }
33
+ return field;
34
+ }
35
+ function heightField(size) {
36
+ const coarse = periodicValueNoise(size, 4, 1);
37
+ const medium = periodicValueNoise(size, 8, 7);
38
+ const fine = periodicValueNoise(size, 16, 13);
39
+ const field = new Float32Array(size * size);
40
+ for (let index = 0; index < field.length; index += 1) {
41
+ field[index] = coarse[index] * 0.55 + medium[index] * 0.3 + fine[index] * 0.15;
42
+ }
43
+ return field;
44
+ }
45
+ let cached = null;
46
+ /**
47
+ * Shared, lazily-built procedural detail maps (a subtle tangent-space normal map plus a
48
+ * roughness-variation map) used by every un-modeled primitive so a flat colored box reads as a
49
+ * surface, not plastic. Pure JS `DataTexture` — no canvas/DOM, so it imports safely under `bun test`.
50
+ * @internal shell-internal default render surface; games never import it.
51
+ */
52
+ export function detailMaps() {
53
+ if (cached !== null)
54
+ return cached;
55
+ const size = DETAIL_SIZE;
56
+ const height = heightField(size);
57
+ const sample = (x, y) => height[(((y % size) + size) % size) * size + (((x % size) + size) % size)];
58
+ const normalData = new Uint8Array(size * size * 4);
59
+ const roughnessData = new Uint8Array(size * size * 4);
60
+ for (let y = 0; y < size; y += 1) {
61
+ for (let x = 0; x < size; x += 1) {
62
+ const dx = (sample(x - 1, y) - sample(x + 1, y)) * NORMAL_STRENGTH;
63
+ const dy = (sample(x, y - 1) - sample(x, y + 1)) * NORMAL_STRENGTH;
64
+ const length = Math.hypot(dx, dy, 1);
65
+ const offset = (y * size + x) * 4;
66
+ normalData[offset] = Math.round((dx / length * 0.5 + 0.5) * 255);
67
+ normalData[offset + 1] = Math.round((dy / length * 0.5 + 0.5) * 255);
68
+ normalData[offset + 2] = Math.round((1 / length * 0.5 + 0.5) * 255);
69
+ normalData[offset + 3] = 255;
70
+ const rough = Math.round((0.5 + (sample(x, y) - 0.5) * 2 * ROUGHNESS_SPREAD * 0.5) * 255);
71
+ roughnessData[offset] = rough;
72
+ roughnessData[offset + 1] = rough;
73
+ roughnessData[offset + 2] = rough;
74
+ roughnessData[offset + 3] = 255;
75
+ }
76
+ }
77
+ const build = (data) => {
78
+ const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
79
+ texture.wrapS = THREE.RepeatWrapping;
80
+ texture.wrapT = THREE.RepeatWrapping;
81
+ texture.repeat.set(DETAIL_REPEAT, DETAIL_REPEAT);
82
+ texture.minFilter = THREE.LinearMipmapLinearFilter;
83
+ texture.magFilter = THREE.LinearFilter;
84
+ texture.generateMipmaps = true;
85
+ texture.anisotropy = 4;
86
+ texture.needsUpdate = true;
87
+ return texture;
88
+ };
89
+ cached = { normal: build(normalData), roughness: build(roughnessData) };
90
+ return cached;
91
+ }
92
+ /**
93
+ * The upgraded default material for un-modeled entities/objects: tuned PBR roughness/metalness plus
94
+ * the shared procedural detail maps, so primitive fallbacks catch light and image-based reflections
95
+ * instead of reading as flat plastic. Drop-in replacement for a bare `<meshStandardMaterial color>`.
96
+ * @internal shell-internal default render surface; games never import it.
97
+ */
98
+ export function DefaultSurface({ color, roughness = 0.72, metalness = 0.04, detail = true, ...rest }) {
99
+ const maps = useMemo(() => (detail ? detailMaps() : null), [detail]);
100
+ return (_jsx("meshStandardMaterial", { color: color, roughness: roughness, metalness: metalness, envMapIntensity: 0.6, normalMap: maps?.normal ?? null, normalScale: maps === null ? undefined : new THREE.Vector2(0.6, 0.6), roughnessMap: maps?.roughness ?? null, ...rest }));
101
+ }
@@ -0,0 +1,24 @@
1
+ import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+ import { type AssetLoadDiagnosis } from "@jgengine/core/scene/assetDiagnostics";
3
+ /**
4
+ * Re-fetch a model URL that just failed to load and classify what actually came
5
+ * back — a 404, a dev-server HTML fallback, a corrupt or non-model body — so the
6
+ * runtime can replace an opaque GLTF parse error with a diagnosis naming the
7
+ * broken asset contract. Runs only on the error path (loads rarely fail), and
8
+ * never throws: a failed probe returns a `missing` diagnosis instead.
9
+ * @internal
10
+ */
11
+ export declare function probeModelUrl(url: string, logicalId?: string, fetchImpl?: typeof fetch): Promise<AssetLoadDiagnosis>;
12
+ /**
13
+ * A {@link GLTFLoader} whose success path is unchanged but whose failures are
14
+ * enriched: when a load errors, it probes the URL and rejects with an
15
+ * {@link AssetLoadDiagnosis}-derived message (missing / HTML fallback / corrupt /
16
+ * unsupported) instead of the raw parse error. Drop-in for the shared loader, so
17
+ * `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
18
+ * @internal
19
+ */
20
+ export declare class DiagnosticGLTFLoader extends GLTFLoader {
21
+ load(url: string, onLoad: (gltf: GLTF) => void, onProgress?: (event: ProgressEvent) => void, onError?: (event: unknown) => void): void;
22
+ }
23
+ /** Shared GLTF loader used by shell entity/object model mounts. @internal */
24
+ export declare const sharedGltfLoader: DiagnosticGLTFLoader;
@@ -0,0 +1,73 @@
1
+ import * as THREE from "three";
2
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
+ import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
4
+ import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
5
+ /**
6
+ * Dedicated LoadingManager for every GLB load instead of THREE.DefaultLoadingManager.
7
+ * The shared default manager is process-wide; under repeated dev-server navigations its
8
+ * AbortController can already be aborted, which silently stalls GLTFLoader.fetch forever
9
+ * with no thrown error. A private manager sidesteps that (ported from the duet-keys ship).
10
+ * @internal
11
+ */
12
+ const modelLoadingManager = new THREE.LoadingManager();
13
+ /** How many leading bytes to read when probing a failed model URL for its signature. @internal */
14
+ const PROBE_BYTE_LIMIT = 64;
15
+ /**
16
+ * Re-fetch a model URL that just failed to load and classify what actually came
17
+ * back — a 404, a dev-server HTML fallback, a corrupt or non-model body — so the
18
+ * runtime can replace an opaque GLTF parse error with a diagnosis naming the
19
+ * broken asset contract. Runs only on the error path (loads rarely fail), and
20
+ * never throws: a failed probe returns a `missing` diagnosis instead.
21
+ * @internal
22
+ */
23
+ export async function probeModelUrl(url, logicalId, fetchImpl = fetch) {
24
+ const base = logicalId === undefined ? { url } : { url, logicalId };
25
+ try {
26
+ const response = await fetchImpl(url);
27
+ const buffer = await response.arrayBuffer();
28
+ const bytes = new Uint8Array(buffer.slice(0, PROBE_BYTE_LIMIT));
29
+ return classifyAssetResponse({
30
+ ...base,
31
+ status: response.status,
32
+ statusText: response.statusText,
33
+ contentType: response.headers.get("content-type") ?? undefined,
34
+ bytes,
35
+ });
36
+ }
37
+ catch {
38
+ // The probe fetch itself failed (network/CORS) — the file is effectively unreachable.
39
+ return classifyAssetResponse({ ...base, status: 0 });
40
+ }
41
+ }
42
+ /**
43
+ * A {@link GLTFLoader} whose success path is unchanged but whose failures are
44
+ * enriched: when a load errors, it probes the URL and rejects with an
45
+ * {@link AssetLoadDiagnosis}-derived message (missing / HTML fallback / corrupt /
46
+ * unsupported) instead of the raw parse error. Drop-in for the shared loader, so
47
+ * `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
48
+ * @internal
49
+ */
50
+ export class DiagnosticGLTFLoader extends GLTFLoader {
51
+ load(url, onLoad, onProgress, onError) {
52
+ super.load(url, onLoad, onProgress, (event) => {
53
+ if (onError === undefined)
54
+ return;
55
+ void probeModelUrl(url)
56
+ .then((diagnosis) => {
57
+ if (diagnosis.ok) {
58
+ // The bytes look fine on re-fetch — surface the original parse error unchanged.
59
+ onError(event);
60
+ return;
61
+ }
62
+ const enriched = new Error(`[jgengine] ${diagnosis.message}`);
63
+ enriched.diagnosis = diagnosis;
64
+ enriched.cause = event;
65
+ onError(enriched);
66
+ })
67
+ .catch(() => onError(event));
68
+ });
69
+ }
70
+ }
71
+ /** Shared GLTF loader used by shell entity/object model mounts. @internal */
72
+ export const sharedGltfLoader = new DiagnosticGLTFLoader(modelLoadingManager);
73
+ sharedGltfLoader.setMeshoptDecoder(MeshoptDecoder);
@@ -1,23 +1,34 @@
1
1
  import * as THREE from "three";
2
2
  import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
3
3
  export declare const PAINT_TEXTURE_SIZE = 512;
4
+ /** @internal */
4
5
  export declare function cloneModelScene(source: THREE.Object3D, options?: {
5
6
  cloneMaterials?: boolean;
6
7
  }): THREE.Object3D;
8
+ /** @internal */
7
9
  export declare function disposeClonedMaterials(root: THREE.Object3D): void;
10
+ /** @internal */
8
11
  export declare function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[];
9
12
  export interface MaterialCache {
10
13
  materials: THREE.MeshStandardMaterial[];
11
14
  seedColor: THREE.Color;
12
15
  }
16
+ /** @internal */
13
17
  export declare function cacheStandardMaterials(root: THREE.Object3D, into?: MaterialCache | null): MaterialCache;
18
+ /** @internal */
14
19
  export declare function applyPaintTextureToMaterials(materials: readonly THREE.MeshStandardMaterial[], paint: PaintCanvas): void;
15
20
  export interface PaintCanvas {
16
21
  canvas: HTMLCanvasElement;
17
22
  context: CanvasRenderingContext2D;
18
23
  texture: THREE.CanvasTexture;
19
24
  }
25
+ /** @internal */
20
26
  export declare function createPaintCanvas(seed: THREE.MeshStandardMaterial, size?: number): PaintCanvas;
27
+ /** @internal */
28
+ export declare function disposePaintCanvas(paint: PaintCanvas): void;
29
+ /** @internal */
21
30
  export declare function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void;
31
+ /** @internal */
22
32
  export declare function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void;
33
+ /** @internal */
23
34
  export declare function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number;
@@ -1,6 +1,7 @@
1
1
  import * as THREE from "three";
2
2
  import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
3
3
  export const PAINT_TEXTURE_SIZE = 512;
4
+ /** @internal */
4
5
  export function cloneModelScene(source, options) {
5
6
  const clone = cloneSkinned(source);
6
7
  if (options?.cloneMaterials === false)
@@ -15,6 +16,7 @@ export function cloneModelScene(source, options) {
15
16
  });
16
17
  return clone;
17
18
  }
19
+ /** @internal */
18
20
  export function disposeClonedMaterials(root) {
19
21
  const seen = new Set();
20
22
  root.traverse((node) => {
@@ -33,6 +35,7 @@ export function disposeClonedMaterials(root) {
33
35
  function isMeshStandardMaterial(material) {
34
36
  return material.isMeshStandardMaterial === true;
35
37
  }
38
+ /** @internal */
36
39
  export function standardMaterialsOf(root) {
37
40
  const materials = [];
38
41
  root.traverse((node) => {
@@ -46,6 +49,7 @@ export function standardMaterialsOf(root) {
46
49
  });
47
50
  return materials;
48
51
  }
52
+ /** @internal */
49
53
  export function cacheStandardMaterials(root, into) {
50
54
  if (into !== null && into !== undefined)
51
55
  return into;
@@ -53,12 +57,14 @@ export function cacheStandardMaterials(root, into) {
53
57
  const seedColor = materials[0]?.color.clone() ?? new THREE.Color("#ffffff");
54
58
  return { materials, seedColor };
55
59
  }
60
+ /** @internal */
56
61
  export function applyPaintTextureToMaterials(materials, paint) {
57
62
  for (const material of materials) {
58
63
  material.map = paint.texture;
59
64
  material.needsUpdate = true;
60
65
  }
61
66
  }
67
+ /** @internal */
62
68
  export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
63
69
  const canvas = document.createElement("canvas");
64
70
  canvas.width = size;
@@ -76,6 +82,13 @@ export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
76
82
  texture.colorSpace = seed.map?.colorSpace ?? THREE.SRGBColorSpace;
77
83
  return { canvas, context, texture };
78
84
  }
85
+ /** @internal */
86
+ export function disposePaintCanvas(paint) {
87
+ paint.texture.dispose();
88
+ paint.canvas.width = 0;
89
+ paint.canvas.height = 0;
90
+ }
91
+ /** @internal */
79
92
  export function drawPaintStrokes(paint, strokes) {
80
93
  const { canvas, context, texture } = paint;
81
94
  for (const stroke of strokes) {
@@ -86,9 +99,11 @@ export function drawPaintStrokes(paint, strokes) {
86
99
  }
87
100
  texture.needsUpdate = true;
88
101
  }
102
+ /** @internal */
89
103
  export function applyPaintTexture(root, paint) {
90
104
  applyPaintTextureToMaterials(standardMaterialsOf(root), paint);
91
105
  }
106
+ /** @internal */
92
107
  export function syncPaintCanvas(paint, seedColor, strokes, drawnCount) {
93
108
  if (strokes.length < drawnCount) {
94
109
  const { canvas, context } = paint;