aether-engine 1.0.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.
- package/README.md +15 -0
- package/biome.json +51 -0
- package/bun.lock +192 -0
- package/index.ts +1 -0
- package/package.json +25 -0
- package/serve.ts +125 -0
- package/src/audio/AudioEngine.ts +61 -0
- package/src/components/Animator3D.ts +65 -0
- package/src/components/AudioSource.ts +26 -0
- package/src/components/BitmapText.ts +25 -0
- package/src/components/Camera.ts +33 -0
- package/src/components/CameraFollow.ts +5 -0
- package/src/components/Collider.ts +16 -0
- package/src/components/Components.test.ts +68 -0
- package/src/components/Light.ts +15 -0
- package/src/components/MeshRenderer.ts +58 -0
- package/src/components/ParticleEmitter.ts +59 -0
- package/src/components/RigidBody.ts +9 -0
- package/src/components/ShadowCaster.ts +3 -0
- package/src/components/SkinnedMeshRenderer.ts +25 -0
- package/src/components/SpriteAnimator.ts +42 -0
- package/src/components/SpriteRenderer.ts +26 -0
- package/src/components/Transform.test.ts +39 -0
- package/src/components/Transform.ts +54 -0
- package/src/core/AssetManager.ts +123 -0
- package/src/core/Input.test.ts +67 -0
- package/src/core/Input.ts +94 -0
- package/src/core/Scene.ts +24 -0
- package/src/core/SceneManager.ts +57 -0
- package/src/core/Storage.ts +161 -0
- package/src/desktop/SteamClient.ts +52 -0
- package/src/ecs/System.ts +11 -0
- package/src/ecs/World.test.ts +29 -0
- package/src/ecs/World.ts +149 -0
- package/src/index.ts +115 -0
- package/src/math/Color.ts +100 -0
- package/src/math/Vector2.ts +96 -0
- package/src/math/Vector3.ts +103 -0
- package/src/math/math.test.ts +168 -0
- package/src/renderer/GlowMaterial.ts +66 -0
- package/src/renderer/LitMaterial.ts +337 -0
- package/src/renderer/Material.test.ts +23 -0
- package/src/renderer/Material.ts +80 -0
- package/src/renderer/OcclusionMaterial.ts +43 -0
- package/src/renderer/ParticleMaterial.ts +66 -0
- package/src/renderer/Shader.ts +44 -0
- package/src/renderer/SkinnedLitMaterial.ts +55 -0
- package/src/renderer/WaterMaterial.ts +298 -0
- package/src/renderer/WebGLRenderer.ts +917 -0
- package/src/systems/Animation3DSystem.ts +148 -0
- package/src/systems/AnimationSystem.ts +58 -0
- package/src/systems/AudioSystem.ts +62 -0
- package/src/systems/LightingSystem.ts +114 -0
- package/src/systems/ParticleSystem.ts +278 -0
- package/src/systems/PhysicsSystem.ts +211 -0
- package/src/systems/Systems.test.ts +165 -0
- package/src/systems/TextSystem.ts +153 -0
- package/src/ui/AnimationEditor.tsx +639 -0
- package/src/ui/BottomPanel.tsx +443 -0
- package/src/ui/EntityExplorer.tsx +420 -0
- package/src/ui/GameState.ts +286 -0
- package/src/ui/Icons.tsx +239 -0
- package/src/ui/InventoryPanel.tsx +335 -0
- package/src/ui/PlayerHUD.tsx +250 -0
- package/src/ui/SpriteEditor.tsx +3241 -0
- package/src/ui/SpriteSheetManager.tsx +198 -0
- package/src/utils/GLTFLoader.ts +257 -0
- package/src/utils/ObjLoader.ts +81 -0
- package/src/utils/idb.ts +137 -0
- package/src/utils/packer.ts +85 -0
- package/test_obj.ts +12 -0
- package/tsconfig.json +21 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createSignal } from "solid-js";
|
|
2
|
+
import type { AetherEngine } from "../index";
|
|
3
|
+
import { packAtlas as packAtlasAlgo } from "../utils/packer";
|
|
4
|
+
|
|
5
|
+
export function SpriteSheetManager(_props: { engine: AetherEngine }) {
|
|
6
|
+
const [images, setImages] = createSignal<
|
|
7
|
+
{ name: string; img: HTMLImageElement }[]
|
|
8
|
+
>([]);
|
|
9
|
+
const [atlasName, setAtlasName] = createSignal("packed_atlas");
|
|
10
|
+
const [status, setStatus] = createSignal("");
|
|
11
|
+
|
|
12
|
+
let canvasRef!: HTMLCanvasElement;
|
|
13
|
+
|
|
14
|
+
const handleFileUpload = (e: Event) => {
|
|
15
|
+
const files = (e.target as HTMLInputElement).files;
|
|
16
|
+
if (!files) return;
|
|
17
|
+
Array.from(files).forEach((file) => {
|
|
18
|
+
const reader = new FileReader();
|
|
19
|
+
reader.onload = (ev) => {
|
|
20
|
+
const img = new Image();
|
|
21
|
+
img.onload = () => {
|
|
22
|
+
setImages((prev) => [
|
|
23
|
+
...prev,
|
|
24
|
+
{ name: file.name.split(".")[0], img },
|
|
25
|
+
]);
|
|
26
|
+
};
|
|
27
|
+
img.src = ev.target?.result as string;
|
|
28
|
+
};
|
|
29
|
+
reader.readAsDataURL(file);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const packAtlas = async () => {
|
|
34
|
+
if (images().length === 0) return;
|
|
35
|
+
setStatus("Packing...");
|
|
36
|
+
|
|
37
|
+
const inputs = images().map((img) => {
|
|
38
|
+
const cvs = document.createElement("canvas");
|
|
39
|
+
cvs.width = img.img.width;
|
|
40
|
+
cvs.height = img.img.height;
|
|
41
|
+
cvs.getContext("2d")?.drawImage(img.img, 0, 0);
|
|
42
|
+
return {
|
|
43
|
+
id: img.name,
|
|
44
|
+
width: img.img.width,
|
|
45
|
+
height: img.img.height,
|
|
46
|
+
data: cvs,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = packAtlasAlgo(inputs);
|
|
51
|
+
|
|
52
|
+
const canvas = canvasRef;
|
|
53
|
+
canvas.width = result.atlasCanvas.width;
|
|
54
|
+
canvas.height = result.atlasCanvas.height;
|
|
55
|
+
const ctx = canvas.getContext("2d")!;
|
|
56
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
57
|
+
ctx.drawImage(result.atlasCanvas, 0, 0);
|
|
58
|
+
|
|
59
|
+
const dataUri = canvas.toDataURL("image/png");
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch("/__aether_dev/save-sprite", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
name: atlasName(),
|
|
67
|
+
png: dataUri,
|
|
68
|
+
metadata: result.meta,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
const out = await res.json();
|
|
72
|
+
if (out.success) {
|
|
73
|
+
setStatus(
|
|
74
|
+
`Saved successfully: public/assets/sprites/${atlasName()}.png`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} catch (_err) {
|
|
78
|
+
setStatus("Failed to save via dev API endpoint!");
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
width: "100%",
|
|
86
|
+
height: "100%",
|
|
87
|
+
display: "flex",
|
|
88
|
+
gap: "5px",
|
|
89
|
+
color: "#EEEEEE",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
width: "250px",
|
|
95
|
+
display: "flex",
|
|
96
|
+
"flex-direction": "column",
|
|
97
|
+
gap: "10px",
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<h3
|
|
101
|
+
style={{ margin: "2px 0 0 0", color: "#EEEEEE", "font-size": "14px" }}
|
|
102
|
+
>
|
|
103
|
+
Atlas Tools
|
|
104
|
+
</h3>
|
|
105
|
+
<p style={{ "font-size": "11px", color: "#B4B4B4", margin: 0 }}>
|
|
106
|
+
Upload loose sprites to dynamically package them into an optimized
|
|
107
|
+
Atlas map using bin packing.
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
<input
|
|
111
|
+
type="file"
|
|
112
|
+
multiple
|
|
113
|
+
accept="image/png, image/jpeg"
|
|
114
|
+
onChange={handleFileUpload}
|
|
115
|
+
style={{
|
|
116
|
+
background: "#282828",
|
|
117
|
+
padding: "6px",
|
|
118
|
+
color: "#EEEEEE",
|
|
119
|
+
"border-radius": "3px",
|
|
120
|
+
border: "1px solid #222222",
|
|
121
|
+
"font-size": "11px",
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<div
|
|
126
|
+
style={{ "border-top": "1px solid #222222", margin: "5px 0" }}
|
|
127
|
+
></div>
|
|
128
|
+
|
|
129
|
+
<label style={{ "font-size": "11px", color: "#B4B4B4" }}>
|
|
130
|
+
Output Atlas Name:
|
|
131
|
+
</label>
|
|
132
|
+
<input
|
|
133
|
+
type="text"
|
|
134
|
+
value={atlasName()}
|
|
135
|
+
onInput={(e) => setAtlasName(e.currentTarget.value)}
|
|
136
|
+
style={{
|
|
137
|
+
padding: "6px",
|
|
138
|
+
"border-radius": "3px",
|
|
139
|
+
border: "1px solid #222222",
|
|
140
|
+
background: "#282828",
|
|
141
|
+
color: "#EEEEEE",
|
|
142
|
+
"font-size": "12px",
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
<button
|
|
147
|
+
onClick={packAtlas}
|
|
148
|
+
style={{
|
|
149
|
+
padding: "8px",
|
|
150
|
+
"border-radius": "3px",
|
|
151
|
+
border: "1px solid #222222",
|
|
152
|
+
cursor: "pointer",
|
|
153
|
+
background: "#4A4A4A",
|
|
154
|
+
color: "#EEEEEE",
|
|
155
|
+
"font-weight": "bold",
|
|
156
|
+
"margin-top": "5px",
|
|
157
|
+
"font-size": "12px",
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
Pack & Save to Disk
|
|
161
|
+
</button>
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
style={{
|
|
165
|
+
"margin-top": "10px",
|
|
166
|
+
color: "#EEEEEE",
|
|
167
|
+
"font-size": "11px",
|
|
168
|
+
"word-wrap": "break-word",
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
{status()}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
style={{
|
|
177
|
+
flex: 1,
|
|
178
|
+
background: `repeating-conic-gradient(#333 0% 25%, #2a2a2a 0% 50%) 50% / 16px 16px`,
|
|
179
|
+
"border-radius": "4px",
|
|
180
|
+
border: "1px solid #222222",
|
|
181
|
+
overflow: "auto",
|
|
182
|
+
display: "flex",
|
|
183
|
+
"justify-content": "center",
|
|
184
|
+
"align-items": "center",
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<canvas
|
|
188
|
+
ref={canvasRef}
|
|
189
|
+
style={{
|
|
190
|
+
"max-width": "100%",
|
|
191
|
+
"max-height": "100%",
|
|
192
|
+
"image-rendering": "pixelated",
|
|
193
|
+
}}
|
|
194
|
+
></canvas>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { WebIO, Document } from "@gltf-transform/core";
|
|
2
|
+
import { mat4, quat, vec3 } from "gl-matrix";
|
|
3
|
+
|
|
4
|
+
export interface GLBData {
|
|
5
|
+
positions: number[];
|
|
6
|
+
normals: number[];
|
|
7
|
+
uvs: number[];
|
|
8
|
+
indices: number[];
|
|
9
|
+
joints: number[];
|
|
10
|
+
weights: number[];
|
|
11
|
+
|
|
12
|
+
// Animation Data
|
|
13
|
+
nodes: GLTFNode[];
|
|
14
|
+
animations: GLTFAnimation[];
|
|
15
|
+
skins: GLTFSkin[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GLTFNode {
|
|
19
|
+
id: number;
|
|
20
|
+
name: string;
|
|
21
|
+
translation: vec3;
|
|
22
|
+
rotation: quat;
|
|
23
|
+
scale: vec3;
|
|
24
|
+
|
|
25
|
+
baseTranslation: vec3;
|
|
26
|
+
baseRotation: quat;
|
|
27
|
+
baseScale: vec3;
|
|
28
|
+
|
|
29
|
+
children: number[];
|
|
30
|
+
parent: number;
|
|
31
|
+
globalMatrix: mat4;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GLTFSkin {
|
|
35
|
+
name: string;
|
|
36
|
+
joints: number[]; // node indices
|
|
37
|
+
inverseBindMatrices: mat4[]; // length matches joints
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GLTFAnimation {
|
|
41
|
+
name: string;
|
|
42
|
+
duration: number;
|
|
43
|
+
channels: GLTFChannel[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GLTFChannel {
|
|
47
|
+
node: number; // node idx
|
|
48
|
+
path: "translation" | "rotation" | "scale";
|
|
49
|
+
timestamps: Float32Array; // seconds
|
|
50
|
+
values: Float32Array; // vec3 or quat per timestamp
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class GLTFLoader {
|
|
54
|
+
static async load(url: string): Promise<GLBData> {
|
|
55
|
+
const io = new WebIO();
|
|
56
|
+
const doc = await io.read(url);
|
|
57
|
+
const root = doc.getRoot();
|
|
58
|
+
|
|
59
|
+
const result: GLBData = {
|
|
60
|
+
positions: [],
|
|
61
|
+
normals: [],
|
|
62
|
+
uvs: [],
|
|
63
|
+
indices: [],
|
|
64
|
+
joints: [],
|
|
65
|
+
weights: [],
|
|
66
|
+
nodes: [],
|
|
67
|
+
animations: [],
|
|
68
|
+
skins: []
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// 1. Build Node Hierarchy
|
|
72
|
+
const rootNodes = root.listNodes();
|
|
73
|
+
for (let i = 0; i < rootNodes.length; i++) {
|
|
74
|
+
const n = rootNodes[i];
|
|
75
|
+
const t = n.getTranslation();
|
|
76
|
+
const r = n.getRotation();
|
|
77
|
+
const s = n.getScale();
|
|
78
|
+
|
|
79
|
+
result.nodes.push({
|
|
80
|
+
id: i,
|
|
81
|
+
name: n.getName() || `Node_${i}`,
|
|
82
|
+
translation: vec3.fromValues(t[0], t[1], t[2]),
|
|
83
|
+
rotation: quat.fromValues(r[0], r[1], r[2], r[3]),
|
|
84
|
+
scale: vec3.fromValues(s[0], s[1], s[2]),
|
|
85
|
+
baseTranslation: vec3.fromValues(t[0], t[1], t[2]),
|
|
86
|
+
baseRotation: quat.fromValues(r[0], r[1], r[2], r[3]),
|
|
87
|
+
baseScale: vec3.fromValues(s[0], s[1], s[2]),
|
|
88
|
+
children: n.listChildren().map(c => rootNodes.indexOf(c)),
|
|
89
|
+
parent: -1,
|
|
90
|
+
globalMatrix: mat4.create()
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Assign parents
|
|
95
|
+
for (const node of result.nodes) {
|
|
96
|
+
for (const childId of node.children) {
|
|
97
|
+
result.nodes[childId].parent = node.id;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Extract Geometry (Assuming one main Skinned Mesh for simplicity)
|
|
102
|
+
let vertexOffset = 0;
|
|
103
|
+
for (const mesh of root.listMeshes()) {
|
|
104
|
+
for (const prim of mesh.listPrimitives()) {
|
|
105
|
+
const posAccessor = prim.getAttribute('POSITION');
|
|
106
|
+
const normAccessor = prim.getAttribute('NORMAL');
|
|
107
|
+
const uvAccessor = prim.getAttribute('TEXCOORD_0');
|
|
108
|
+
const jointAccessor = prim.getAttribute('JOINTS_0');
|
|
109
|
+
const weightAccessor = prim.getAttribute('WEIGHTS_0');
|
|
110
|
+
const indicesAccessor = prim.getIndices();
|
|
111
|
+
|
|
112
|
+
if (posAccessor) {
|
|
113
|
+
for (let i = 0; i < posAccessor.getCount(); i++) {
|
|
114
|
+
const el = posAccessor.getElement(i, []);
|
|
115
|
+
result.positions.push(el[0], el[1], el[2]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (normAccessor) {
|
|
120
|
+
for (let i = 0; i < normAccessor.getCount(); i++) {
|
|
121
|
+
const el = normAccessor.getElement(i, []);
|
|
122
|
+
result.normals.push(el[0], el[1], el[2]);
|
|
123
|
+
}
|
|
124
|
+
} else if (posAccessor) {
|
|
125
|
+
// Fallback fill
|
|
126
|
+
for(let i=0; i<posAccessor.getCount(); i++) result.normals.push(0,1,0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (uvAccessor) {
|
|
130
|
+
for (let i = 0; i < uvAccessor.getCount(); i++) {
|
|
131
|
+
const el = uvAccessor.getElement(i, []);
|
|
132
|
+
result.uvs.push(el[0], el[1]);
|
|
133
|
+
}
|
|
134
|
+
} else if (posAccessor) {
|
|
135
|
+
for(let i=0; i<posAccessor.getCount(); i++) result.uvs.push(0,0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (jointAccessor) {
|
|
139
|
+
for (let i = 0; i < jointAccessor.getCount(); i++) {
|
|
140
|
+
const el = jointAccessor.getElement(i, []);
|
|
141
|
+
result.joints.push(el[0], el[1], el[2], el[3]);
|
|
142
|
+
}
|
|
143
|
+
} else if (posAccessor) {
|
|
144
|
+
for(let i=0; i<posAccessor.getCount(); i++) result.joints.push(0,0,0,0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (weightAccessor) {
|
|
148
|
+
for (let i = 0; i < weightAccessor.getCount(); i++) {
|
|
149
|
+
const el = weightAccessor.getElement(i, []);
|
|
150
|
+
|
|
151
|
+
// Normalize weights just in case they are not unorm or slightly off
|
|
152
|
+
const sum = el[0] + el[1] + el[2] + el[3];
|
|
153
|
+
if (sum > 0) {
|
|
154
|
+
result.weights.push(el[0]/sum, el[1]/sum, el[2]/sum, el[3]/sum);
|
|
155
|
+
} else {
|
|
156
|
+
result.weights.push(1, 0, 0, 0);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (posAccessor) {
|
|
160
|
+
for(let i=0; i<posAccessor.getCount(); i++) result.weights.push(1,0,0,0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (indicesAccessor) {
|
|
164
|
+
for (let i = 0; i < indicesAccessor.getCount(); i++) {
|
|
165
|
+
result.indices.push(indicesAccessor.getScalar(i) + vertexOffset);
|
|
166
|
+
}
|
|
167
|
+
} else if (posAccessor) {
|
|
168
|
+
// generate sequential indices
|
|
169
|
+
for (let i = 0; i < posAccessor.getCount(); i++) {
|
|
170
|
+
result.indices.push(i + vertexOffset);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (posAccessor) {
|
|
175
|
+
vertexOffset += posAccessor.getCount();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. Extract Skins (Inverse Bind Matrices)
|
|
181
|
+
for (const skin of root.listSkins()) {
|
|
182
|
+
const joints = skin.listJoints().map(j => rootNodes.indexOf(j));
|
|
183
|
+
const ibmAccessor = skin.getInverseBindMatrices();
|
|
184
|
+
const ibms: mat4[] = [];
|
|
185
|
+
|
|
186
|
+
if (ibmAccessor) {
|
|
187
|
+
for (let i = 0; i < ibmAccessor.getCount(); i++) {
|
|
188
|
+
const el = ibmAccessor.getElement(i, []);
|
|
189
|
+
ibms.push(mat4.fromValues(
|
|
190
|
+
el[0], el[1], el[2], el[3],
|
|
191
|
+
el[4], el[5], el[6], el[7],
|
|
192
|
+
el[8], el[9], el[10], el[11],
|
|
193
|
+
el[12], el[13], el[14], el[15]
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// Default Identity
|
|
198
|
+
for(let i=0; i<joints.length; i++) ibms.push(mat4.create());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
result.skins.push({
|
|
202
|
+
name: skin.getName() || "Skin",
|
|
203
|
+
joints,
|
|
204
|
+
inverseBindMatrices: ibms
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 4. Extract Animations
|
|
209
|
+
for (const anim of root.listAnimations()) {
|
|
210
|
+
const channels: GLTFChannel[] = [];
|
|
211
|
+
let maxTime = 0;
|
|
212
|
+
|
|
213
|
+
for (const channel of anim.listChannels()) {
|
|
214
|
+
const sampler = channel.getSampler();
|
|
215
|
+
const targetNode = channel.getTargetNode();
|
|
216
|
+
if (!sampler || !targetNode) continue;
|
|
217
|
+
|
|
218
|
+
const inputAccessor = sampler.getInput();
|
|
219
|
+
const outputAccessor = sampler.getOutput();
|
|
220
|
+
if (!inputAccessor || !outputAccessor) continue;
|
|
221
|
+
|
|
222
|
+
// Timestamps
|
|
223
|
+
const times = new Float32Array(inputAccessor.getCount());
|
|
224
|
+
for (let i = 0; i < inputAccessor.getCount(); i++) {
|
|
225
|
+
const t = inputAccessor.getScalar(i);
|
|
226
|
+
times[i] = t;
|
|
227
|
+
if (t > maxTime) maxTime = t;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Values (vec3 or vec4)
|
|
231
|
+
const outSize = outputAccessor.getElementSize();
|
|
232
|
+
const values = new Float32Array(outputAccessor.getCount() * outSize);
|
|
233
|
+
for (let i = 0; i < outputAccessor.getCount(); i++) {
|
|
234
|
+
const val = outputAccessor.getElement(i, []);
|
|
235
|
+
for(let j=0; j<outSize; j++) {
|
|
236
|
+
values[i*outSize + j] = val[j];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
channels.push({
|
|
241
|
+
node: rootNodes.indexOf(targetNode),
|
|
242
|
+
path: channel.getTargetPath() as any, // "translation" | "rotation" | "scale"
|
|
243
|
+
timestamps: times,
|
|
244
|
+
values: values
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
result.animations.push({
|
|
249
|
+
name: anim.getName() || `Animation_${result.animations.length}`,
|
|
250
|
+
duration: maxTime,
|
|
251
|
+
channels
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class ObjLoader {
|
|
2
|
+
static async load(
|
|
3
|
+
url: string,
|
|
4
|
+
): Promise<{ positions: number[]; uvs: number[]; normals: number[] }> {
|
|
5
|
+
const response = await fetch(url);
|
|
6
|
+
const text = await response.text();
|
|
7
|
+
|
|
8
|
+
const positions: number[] = [];
|
|
9
|
+
const uvs: number[] = [];
|
|
10
|
+
const normals: number[] = [];
|
|
11
|
+
|
|
12
|
+
const v: number[][] = [];
|
|
13
|
+
const vt: number[][] = [];
|
|
14
|
+
const vn: number[][] = [];
|
|
15
|
+
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i].trim();
|
|
20
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
21
|
+
|
|
22
|
+
const parts = line.split(/\s+/);
|
|
23
|
+
const type = parts[0];
|
|
24
|
+
|
|
25
|
+
if (type === "v") {
|
|
26
|
+
v.push([parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])]);
|
|
27
|
+
} else if (type === "vt") {
|
|
28
|
+
vt.push([parseFloat(parts[1]), parseFloat(parts[2])]);
|
|
29
|
+
} else if (type === "vn") {
|
|
30
|
+
vn.push([parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])]);
|
|
31
|
+
} else if (type === "f") {
|
|
32
|
+
// faces can be triangles or quads (or more)
|
|
33
|
+
// format can be v, v/vt, v/vt/vn, or v//vn
|
|
34
|
+
const vertices = parts.slice(1);
|
|
35
|
+
|
|
36
|
+
// Triangulate if more than 3 vertices (primitive fan)
|
|
37
|
+
for (let j = 1; j < vertices.length - 1; j++) {
|
|
38
|
+
const v0 = vertices[0];
|
|
39
|
+
const v1 = vertices[j];
|
|
40
|
+
const v2 = vertices[j + 1];
|
|
41
|
+
|
|
42
|
+
const processVertex = (vertData: string) => {
|
|
43
|
+
const [vIdx, vtIdx, vnIdx] = vertData.split("/");
|
|
44
|
+
|
|
45
|
+
// Position (1-indexed)
|
|
46
|
+
if (vIdx) {
|
|
47
|
+
const vert = v[parseInt(vIdx) - 1];
|
|
48
|
+
positions.push(vert[0], vert[1], vert[2]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// UVs (1-indexed)
|
|
52
|
+
if (vtIdx) {
|
|
53
|
+
const tex = vt[parseInt(vtIdx) - 1];
|
|
54
|
+
uvs.push(tex[0], tex[1]);
|
|
55
|
+
} else {
|
|
56
|
+
uvs.push(0, 0); // Default if missing
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Normals (1-indexed)
|
|
60
|
+
if (vnIdx) {
|
|
61
|
+
const norm = vn[parseInt(vnIdx) - 1];
|
|
62
|
+
normals.push(norm[0], norm[1], norm[2]);
|
|
63
|
+
} else {
|
|
64
|
+
normals.push(0, 0, 1); // Default if missing
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
processVertex(v0);
|
|
69
|
+
processVertex(v1);
|
|
70
|
+
processVertex(v2);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
positions,
|
|
77
|
+
uvs,
|
|
78
|
+
normals,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/utils/idb.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const getAppNamespace = () => {
|
|
2
|
+
const meta = document.querySelector('meta[name="aether-app-id"]');
|
|
3
|
+
if (meta) {
|
|
4
|
+
const content = meta.getAttribute("content") || "default";
|
|
5
|
+
const safeStr = btoa(content).replace(/=/g, "");
|
|
6
|
+
return `AetherDevTools_${safeStr}`;
|
|
7
|
+
}
|
|
8
|
+
return "AetherDevTools";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getDB = (): Promise<IDBDatabase> => {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const req = indexedDB.open(getAppNamespace(), 5);
|
|
14
|
+
req.onerror = () => reject(req.error);
|
|
15
|
+
req.onsuccess = () => resolve(req.result);
|
|
16
|
+
req.onupgradeneeded = (e: any) => {
|
|
17
|
+
const db = e.target.result as IDBDatabase;
|
|
18
|
+
if (e.oldVersion < 4) {
|
|
19
|
+
if (db.objectStoreNames.contains("SpriteHistory")) {
|
|
20
|
+
db.deleteObjectStore("SpriteHistory");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!db.objectStoreNames.contains("SpriteHistory")) {
|
|
24
|
+
db.createObjectStore("SpriteHistory", {
|
|
25
|
+
keyPath: "id",
|
|
26
|
+
autoIncrement: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (!db.objectStoreNames.contains("AppState")) {
|
|
30
|
+
db.createObjectStore("AppState", { keyPath: "key" });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const setActiveHistoryId = async (id: number): Promise<void> => {
|
|
37
|
+
const db = await getDB();
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const tx = db.transaction("AppState", "readwrite");
|
|
40
|
+
const st = tx.objectStore("AppState");
|
|
41
|
+
const req = st.put({ key: "active_history_id", value: id });
|
|
42
|
+
req.onsuccess = () => resolve();
|
|
43
|
+
req.onerror = () => reject(req.error);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getActiveHistoryId = async (): Promise<number | null> => {
|
|
48
|
+
const db = await getDB();
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const tx = db.transaction("AppState", "readonly");
|
|
51
|
+
const st = tx.objectStore("AppState");
|
|
52
|
+
const req = st.get("active_history_id");
|
|
53
|
+
req.onsuccess = () =>
|
|
54
|
+
resolve(req.result !== undefined ? req.result.value : null);
|
|
55
|
+
req.onerror = () => reject(req.error);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const pushHistory = async (state: any): Promise<number> => {
|
|
60
|
+
const db = await getDB();
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const tx = db.transaction("SpriteHistory", "readwrite");
|
|
63
|
+
const st = tx.objectStore("SpriteHistory");
|
|
64
|
+
const req = st.add({ timestamp: Date.now(), state });
|
|
65
|
+
req.onsuccess = (e: any) => resolve(e.target.result);
|
|
66
|
+
req.onerror = () => reject(req.error);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const getHistory = async (id: number): Promise<any> => {
|
|
71
|
+
const db = await getDB();
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const tx = db.transaction("SpriteHistory", "readonly");
|
|
74
|
+
const st = tx.objectStore("SpriteHistory");
|
|
75
|
+
const req = st.get(id);
|
|
76
|
+
req.onsuccess = () => resolve(req.result?.state);
|
|
77
|
+
req.onerror = () => reject(req.error);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const getLatestHistoryId = async (): Promise<number | null> => {
|
|
82
|
+
const db = await getDB();
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const tx = db.transaction("SpriteHistory", "readonly");
|
|
85
|
+
const st = tx.objectStore("SpriteHistory");
|
|
86
|
+
const req = st.openCursor(null, "prev");
|
|
87
|
+
req.onsuccess = (e: any) => {
|
|
88
|
+
const cursor = e.target.result;
|
|
89
|
+
if (cursor) resolve(cursor.key);
|
|
90
|
+
else resolve(null);
|
|
91
|
+
};
|
|
92
|
+
req.onerror = () => reject(req.error);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const getPrevHistoryId = async (
|
|
97
|
+
currentId: number,
|
|
98
|
+
): Promise<number | null> => {
|
|
99
|
+
const db = await getDB();
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const tx = db.transaction("SpriteHistory", "readonly");
|
|
102
|
+
const st = tx.objectStore("SpriteHistory");
|
|
103
|
+
const req = st.openCursor(IDBKeyRange.upperBound(currentId, true), "prev");
|
|
104
|
+
req.onsuccess = (e: any) => {
|
|
105
|
+
const cursor = e.target.result;
|
|
106
|
+
resolve(cursor ? cursor.key : null);
|
|
107
|
+
};
|
|
108
|
+
req.onerror = () => reject(req.error);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const getNextHistoryId = async (
|
|
113
|
+
currentId: number,
|
|
114
|
+
): Promise<number | null> => {
|
|
115
|
+
const db = await getDB();
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const tx = db.transaction("SpriteHistory", "readonly");
|
|
118
|
+
const st = tx.objectStore("SpriteHistory");
|
|
119
|
+
const req = st.openCursor(IDBKeyRange.lowerBound(currentId, true), "next");
|
|
120
|
+
req.onsuccess = (e: any) => {
|
|
121
|
+
const cursor = e.target.result;
|
|
122
|
+
resolve(cursor ? cursor.key : null);
|
|
123
|
+
};
|
|
124
|
+
req.onerror = () => reject(req.error);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const truncateHistoryAfter = async (id: number): Promise<void> => {
|
|
129
|
+
const db = await getDB();
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const tx = db.transaction("SpriteHistory", "readwrite");
|
|
132
|
+
const st = tx.objectStore("SpriteHistory");
|
|
133
|
+
const req = st.delete(IDBKeyRange.lowerBound(id, true));
|
|
134
|
+
req.onsuccess = () => resolve();
|
|
135
|
+
req.onerror = () => reject(req.error);
|
|
136
|
+
});
|
|
137
|
+
};
|