blockymodel-web 0.1.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/LICENSE +21 -0
- package/dist/blockymodel-web.js +2717 -0
- package/dist/blockymodel-web.js.map +1 -0
- package/dist/blockymodel-web.umd.cjs +122 -0
- package/dist/blockymodel-web.umd.cjs.map +1 -0
- package/dist/editor/Editor.d.ts +94 -0
- package/dist/editor/Editor.d.ts.map +1 -0
- package/dist/editor/History.d.ts +52 -0
- package/dist/editor/History.d.ts.map +1 -0
- package/dist/editor/SelectionManager.d.ts +57 -0
- package/dist/editor/SelectionManager.d.ts.map +1 -0
- package/dist/editor/Serializer.d.ts +44 -0
- package/dist/editor/Serializer.d.ts.map +1 -0
- package/dist/editor/TransformManager.d.ts +73 -0
- package/dist/editor/TransformManager.d.ts.map +1 -0
- package/dist/editor/commands/AddNodeCommand.d.ts +24 -0
- package/dist/editor/commands/AddNodeCommand.d.ts.map +1 -0
- package/dist/editor/commands/Command.d.ts +50 -0
- package/dist/editor/commands/Command.d.ts.map +1 -0
- package/dist/editor/commands/RemoveNodeCommand.d.ts +28 -0
- package/dist/editor/commands/RemoveNodeCommand.d.ts.map +1 -0
- package/dist/editor/commands/SetPositionCommand.d.ts +24 -0
- package/dist/editor/commands/SetPositionCommand.d.ts.map +1 -0
- package/dist/editor/commands/SetPropertyCommand.d.ts +41 -0
- package/dist/editor/commands/SetPropertyCommand.d.ts.map +1 -0
- package/dist/editor/commands/SetRotationCommand.d.ts +24 -0
- package/dist/editor/commands/SetRotationCommand.d.ts.map +1 -0
- package/dist/editor/commands/SetScaleCommand.d.ts +24 -0
- package/dist/editor/commands/SetScaleCommand.d.ts.map +1 -0
- package/dist/editor/index.d.ts +15 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/loaders/BlockyModelLoader.d.ts +48 -0
- package/dist/loaders/BlockyModelLoader.d.ts.map +1 -0
- package/dist/types/blockymodel.d.ts +72 -0
- package/dist/types/blockymodel.d.ts.map +1 -0
- package/dist/ui/HierarchyPanel.d.ts +73 -0
- package/dist/ui/HierarchyPanel.d.ts.map +1 -0
- package/dist/ui/PropertyPanel.d.ts +59 -0
- package/dist/ui/PropertyPanel.d.ts.map +1 -0
- package/dist/ui/UVEditor.d.ts +56 -0
- package/dist/ui/UVEditor.d.ts.map +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/viewer/ViewerController.d.ts +71 -0
- package/dist/viewer/ViewerController.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/editor/Editor.ts +196 -0
- package/src/editor/History.ts +123 -0
- package/src/editor/SelectionManager.ts +183 -0
- package/src/editor/Serializer.ts +212 -0
- package/src/editor/TransformManager.ts +270 -0
- package/src/editor/commands/AddNodeCommand.ts +53 -0
- package/src/editor/commands/Command.ts +63 -0
- package/src/editor/commands/RemoveNodeCommand.ts +59 -0
- package/src/editor/commands/SetPositionCommand.ts +51 -0
- package/src/editor/commands/SetPropertyCommand.ts +100 -0
- package/src/editor/commands/SetRotationCommand.ts +51 -0
- package/src/editor/commands/SetScaleCommand.ts +51 -0
- package/src/editor/index.ts +19 -0
- package/src/index.ts +49 -0
- package/src/loaders/BlockyModelLoader.ts +281 -0
- package/src/main.ts +290 -0
- package/src/styles.css +597 -0
- package/src/types/blockymodel.ts +82 -0
- package/src/ui/HierarchyPanel.ts +343 -0
- package/src/ui/PropertyPanel.ts +434 -0
- package/src/ui/UVEditor.ts +336 -0
- package/src/ui/index.ts +4 -0
- package/src/viewer/ViewerController.ts +295 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Editor core
|
|
2
|
+
export { Editor } from "./Editor";
|
|
3
|
+
export type { EditorEvent, EditorOptions } from "./Editor";
|
|
4
|
+
|
|
5
|
+
// Subsystems
|
|
6
|
+
export { SelectionManager } from "./SelectionManager";
|
|
7
|
+
export { TransformManager } from "./TransformManager";
|
|
8
|
+
export { History } from "./History";
|
|
9
|
+
export { Serializer } from "./Serializer";
|
|
10
|
+
|
|
11
|
+
// Commands
|
|
12
|
+
export type { Command } from "./commands/Command";
|
|
13
|
+
export { BaseCommand } from "./commands/Command";
|
|
14
|
+
export { SetPositionCommand } from "./commands/SetPositionCommand";
|
|
15
|
+
export { SetRotationCommand } from "./commands/SetRotationCommand";
|
|
16
|
+
export { SetScaleCommand } from "./commands/SetScaleCommand";
|
|
17
|
+
export { SetPropertyCommand } from "./commands/SetPropertyCommand";
|
|
18
|
+
export { AddNodeCommand } from "./commands/AddNodeCommand";
|
|
19
|
+
export { RemoveNodeCommand } from "./commands/RemoveNodeCommand";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Editor core
|
|
2
|
+
export { Editor } from "./editor/Editor";
|
|
3
|
+
export { History } from "./editor/History";
|
|
4
|
+
export { SelectionManager } from "./editor/SelectionManager";
|
|
5
|
+
export { TransformManager } from "./editor/TransformManager";
|
|
6
|
+
export { Serializer } from "./editor/Serializer";
|
|
7
|
+
|
|
8
|
+
// Commands
|
|
9
|
+
export { BaseCommand } from "./editor/commands/Command";
|
|
10
|
+
export type { Command } from "./editor/commands/Command";
|
|
11
|
+
export { SetPositionCommand } from "./editor/commands/SetPositionCommand";
|
|
12
|
+
export { SetRotationCommand } from "./editor/commands/SetRotationCommand";
|
|
13
|
+
export { SetScaleCommand } from "./editor/commands/SetScaleCommand";
|
|
14
|
+
export { SetPropertyCommand } from "./editor/commands/SetPropertyCommand";
|
|
15
|
+
export { AddNodeCommand } from "./editor/commands/AddNodeCommand";
|
|
16
|
+
export { RemoveNodeCommand } from "./editor/commands/RemoveNodeCommand";
|
|
17
|
+
|
|
18
|
+
// UI Components
|
|
19
|
+
export { PropertyPanel } from "./ui/PropertyPanel";
|
|
20
|
+
export { HierarchyPanel } from "./ui/HierarchyPanel";
|
|
21
|
+
export { UVEditor } from "./ui/UVEditor";
|
|
22
|
+
|
|
23
|
+
// Loader
|
|
24
|
+
export { BlockyModelLoader } from "./loaders/BlockyModelLoader";
|
|
25
|
+
|
|
26
|
+
// Viewer
|
|
27
|
+
export { ViewerController } from "./viewer/ViewerController";
|
|
28
|
+
|
|
29
|
+
// Types
|
|
30
|
+
export type {
|
|
31
|
+
BlockyModel,
|
|
32
|
+
BlockyNode,
|
|
33
|
+
BlockyShape,
|
|
34
|
+
ShapeSettings,
|
|
35
|
+
TextureLayout,
|
|
36
|
+
FaceUV,
|
|
37
|
+
Vec3,
|
|
38
|
+
Vec2,
|
|
39
|
+
Quaternion,
|
|
40
|
+
ShapeType,
|
|
41
|
+
ShadingMode,
|
|
42
|
+
NormalDirection,
|
|
43
|
+
} from "./types/blockymodel";
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
DEFAULT_POSITION,
|
|
47
|
+
DEFAULT_ORIENTATION,
|
|
48
|
+
DEFAULT_STRETCH,
|
|
49
|
+
} from "./types/blockymodel";
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type {
|
|
3
|
+
BlockyModel,
|
|
4
|
+
BlockyNode,
|
|
5
|
+
BlockyShape,
|
|
6
|
+
Vec3,
|
|
7
|
+
} from "../types/blockymodel";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_POSITION,
|
|
10
|
+
DEFAULT_ORIENTATION,
|
|
11
|
+
DEFAULT_STRETCH,
|
|
12
|
+
} from "../types/blockymodel";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Loader for .blockymodel files
|
|
16
|
+
* Parses JSON and builds Three.js Object3D hierarchy
|
|
17
|
+
*/
|
|
18
|
+
export class BlockyModelLoader extends THREE.Loader {
|
|
19
|
+
constructor(manager?: THREE.LoadingManager) {
|
|
20
|
+
super(manager);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load a .blockymodel file from URL
|
|
25
|
+
*/
|
|
26
|
+
async load(url: string): Promise<THREE.Group> {
|
|
27
|
+
const response = await fetch(url);
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Failed to load model: ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
const json = await response.json();
|
|
32
|
+
return this.parse(json);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load from File object (for drag-drop / file input)
|
|
37
|
+
*/
|
|
38
|
+
async loadFromFile(file: File): Promise<THREE.Group> {
|
|
39
|
+
const text = await file.text();
|
|
40
|
+
const json = JSON.parse(text);
|
|
41
|
+
return this.parse(json);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse BlockyModel JSON into Three.js scene graph
|
|
46
|
+
*/
|
|
47
|
+
parse(json: BlockyModel): THREE.Group {
|
|
48
|
+
const root = new THREE.Group();
|
|
49
|
+
root.name = "BlockyModel";
|
|
50
|
+
root.userData.format = json.format;
|
|
51
|
+
root.userData.lod = json.lod;
|
|
52
|
+
|
|
53
|
+
for (const node of json.nodes) {
|
|
54
|
+
const object = this.parseNode(node);
|
|
55
|
+
root.add(object);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return root;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Recursively parse a BlockyNode into Three.js Object3D
|
|
63
|
+
*/
|
|
64
|
+
private parseNode(node: BlockyNode): THREE.Object3D {
|
|
65
|
+
const object = this.createNodeObject(node);
|
|
66
|
+
|
|
67
|
+
// Set name and metadata
|
|
68
|
+
object.name = node.name;
|
|
69
|
+
object.userData.id = node.id;
|
|
70
|
+
object.userData.isPiece = node.shape?.settings?.isPiece ?? false;
|
|
71
|
+
|
|
72
|
+
// Apply position
|
|
73
|
+
const pos = node.position ?? DEFAULT_POSITION;
|
|
74
|
+
object.position.set(pos.x, pos.y, pos.z);
|
|
75
|
+
|
|
76
|
+
// Apply orientation (quaternion)
|
|
77
|
+
const orient = node.orientation ?? DEFAULT_ORIENTATION;
|
|
78
|
+
object.quaternion.set(orient.x, orient.y, orient.z, orient.w);
|
|
79
|
+
|
|
80
|
+
// Recursively add children
|
|
81
|
+
for (const child of node.children ?? []) {
|
|
82
|
+
const childObject = this.parseNode(child);
|
|
83
|
+
object.add(childObject);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return object;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create the appropriate Object3D based on shape type
|
|
91
|
+
*/
|
|
92
|
+
private createNodeObject(node: BlockyNode): THREE.Object3D {
|
|
93
|
+
const shape = node.shape;
|
|
94
|
+
|
|
95
|
+
// No shape or invisible - return empty group
|
|
96
|
+
if (!shape || shape.type === "none" || !shape.visible) {
|
|
97
|
+
const group = new THREE.Group();
|
|
98
|
+
group.userData.shapeType = shape?.type ?? "none";
|
|
99
|
+
return group;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (shape.type === "box") {
|
|
103
|
+
return this.createBox(shape);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (shape.type === "quad") {
|
|
107
|
+
return this.createQuad(shape);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fallback to empty group for unknown types
|
|
111
|
+
return new THREE.Group();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a box mesh from shape data
|
|
116
|
+
*/
|
|
117
|
+
private createBox(shape: BlockyShape): THREE.Mesh {
|
|
118
|
+
const size = shape.settings.size ?? { x: 1, y: 1, z: 1 };
|
|
119
|
+
const stretch = shape.stretch ?? DEFAULT_STRETCH;
|
|
120
|
+
const offset = shape.offset ?? DEFAULT_POSITION;
|
|
121
|
+
|
|
122
|
+
// Apply stretch to size
|
|
123
|
+
const finalSize: Vec3 = {
|
|
124
|
+
x: size.x * stretch.x,
|
|
125
|
+
y: size.y * stretch.y,
|
|
126
|
+
z: size.z * stretch.z,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Create geometry
|
|
130
|
+
const geometry = new THREE.BoxGeometry(finalSize.x, finalSize.y, finalSize.z);
|
|
131
|
+
|
|
132
|
+
// Apply offset by translating geometry vertices
|
|
133
|
+
geometry.translate(offset.x, offset.y, offset.z);
|
|
134
|
+
|
|
135
|
+
// Create material based on shading mode
|
|
136
|
+
const material = this.createMaterial(shape);
|
|
137
|
+
|
|
138
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
139
|
+
mesh.userData.shapeType = "box";
|
|
140
|
+
mesh.userData.originalSize = size;
|
|
141
|
+
mesh.userData.shadingMode = shape.shadingMode;
|
|
142
|
+
|
|
143
|
+
// Handle double-sided
|
|
144
|
+
if (shape.doubleSided) {
|
|
145
|
+
material.side = THREE.DoubleSide;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return mesh;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a quad (plane) mesh from shape data
|
|
153
|
+
*/
|
|
154
|
+
private createQuad(shape: BlockyShape): THREE.Mesh {
|
|
155
|
+
const size = shape.settings.size ?? { x: 1, y: 1, z: 0 };
|
|
156
|
+
const stretch = shape.stretch ?? DEFAULT_STRETCH;
|
|
157
|
+
const offset = shape.offset ?? DEFAULT_POSITION;
|
|
158
|
+
const normal = shape.settings.normal ?? "+Z";
|
|
159
|
+
|
|
160
|
+
// Determine plane dimensions based on normal direction
|
|
161
|
+
let width: number, height: number;
|
|
162
|
+
switch (normal) {
|
|
163
|
+
case "+X":
|
|
164
|
+
case "-X":
|
|
165
|
+
width = size.z * stretch.z;
|
|
166
|
+
height = size.y * stretch.y;
|
|
167
|
+
break;
|
|
168
|
+
case "+Y":
|
|
169
|
+
case "-Y":
|
|
170
|
+
width = size.x * stretch.x;
|
|
171
|
+
height = size.z * stretch.z;
|
|
172
|
+
break;
|
|
173
|
+
case "+Z":
|
|
174
|
+
case "-Z":
|
|
175
|
+
default:
|
|
176
|
+
width = size.x * stretch.x;
|
|
177
|
+
height = size.y * stretch.y;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const geometry = new THREE.PlaneGeometry(width, height);
|
|
182
|
+
|
|
183
|
+
// Rotate plane based on normal direction
|
|
184
|
+
switch (normal) {
|
|
185
|
+
case "+X":
|
|
186
|
+
geometry.rotateY(Math.PI / 2);
|
|
187
|
+
break;
|
|
188
|
+
case "-X":
|
|
189
|
+
geometry.rotateY(-Math.PI / 2);
|
|
190
|
+
break;
|
|
191
|
+
case "+Y":
|
|
192
|
+
geometry.rotateX(-Math.PI / 2);
|
|
193
|
+
break;
|
|
194
|
+
case "-Y":
|
|
195
|
+
geometry.rotateX(Math.PI / 2);
|
|
196
|
+
break;
|
|
197
|
+
case "-Z":
|
|
198
|
+
geometry.rotateY(Math.PI);
|
|
199
|
+
break;
|
|
200
|
+
// +Z is default orientation
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
geometry.translate(offset.x, offset.y, offset.z);
|
|
204
|
+
|
|
205
|
+
const material = this.createMaterial(shape);
|
|
206
|
+
material.side = shape.doubleSided ? THREE.DoubleSide : THREE.FrontSide;
|
|
207
|
+
|
|
208
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
209
|
+
mesh.userData.shapeType = "quad";
|
|
210
|
+
mesh.userData.shadingMode = shape.shadingMode;
|
|
211
|
+
|
|
212
|
+
return mesh;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create material based on shading mode
|
|
217
|
+
* Phase 1: Basic color material, texture mapping in Phase 2
|
|
218
|
+
*/
|
|
219
|
+
private createMaterial(shape: BlockyShape): THREE.MeshStandardMaterial {
|
|
220
|
+
const baseColor = 0x888888; // Gray placeholder until texture is applied
|
|
221
|
+
|
|
222
|
+
switch (shape.shadingMode) {
|
|
223
|
+
case "fullbright":
|
|
224
|
+
return new THREE.MeshStandardMaterial({
|
|
225
|
+
color: baseColor,
|
|
226
|
+
emissive: baseColor,
|
|
227
|
+
emissiveIntensity: 1,
|
|
228
|
+
roughness: 1,
|
|
229
|
+
metalness: 0,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
case "flat":
|
|
233
|
+
return new THREE.MeshStandardMaterial({
|
|
234
|
+
color: baseColor,
|
|
235
|
+
flatShading: true,
|
|
236
|
+
roughness: 0.9,
|
|
237
|
+
metalness: 0,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
case "reflective":
|
|
241
|
+
return new THREE.MeshStandardMaterial({
|
|
242
|
+
color: baseColor,
|
|
243
|
+
roughness: 0.1,
|
|
244
|
+
metalness: 0.8,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
case "standard":
|
|
248
|
+
default:
|
|
249
|
+
return new THREE.MeshStandardMaterial({
|
|
250
|
+
color: baseColor,
|
|
251
|
+
roughness: 0.7,
|
|
252
|
+
metalness: 0,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Helper to apply a texture to all meshes in a loaded model
|
|
260
|
+
* Phase 1: Simple texture application without UV mapping
|
|
261
|
+
*/
|
|
262
|
+
export function applyTextureToModel(
|
|
263
|
+
model: THREE.Group,
|
|
264
|
+
texture: THREE.Texture
|
|
265
|
+
): void {
|
|
266
|
+
// Configure texture for pixel art (no filtering)
|
|
267
|
+
texture.magFilter = THREE.NearestFilter;
|
|
268
|
+
texture.minFilter = THREE.NearestFilter;
|
|
269
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
270
|
+
|
|
271
|
+
model.traverse((object) => {
|
|
272
|
+
if (object instanceof THREE.Mesh) {
|
|
273
|
+
const material = object.material as THREE.MeshStandardMaterial;
|
|
274
|
+
if (material.isMeshStandardMaterial) {
|
|
275
|
+
material.map = texture;
|
|
276
|
+
material.color.setHex(0xffffff); // Reset color to white when texture applied
|
|
277
|
+
material.needsUpdate = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { ViewerController } from "./viewer/ViewerController";
|
|
3
|
+
import { Editor } from "./editor/Editor";
|
|
4
|
+
import { PropertyPanel } from "./ui/PropertyPanel";
|
|
5
|
+
import { HierarchyPanel } from "./ui/HierarchyPanel";
|
|
6
|
+
import { UVEditor } from "./ui/UVEditor";
|
|
7
|
+
import { Serializer } from "./editor/Serializer";
|
|
8
|
+
import "./styles.css";
|
|
9
|
+
|
|
10
|
+
// Global instances
|
|
11
|
+
let viewer: ViewerController;
|
|
12
|
+
let editor: Editor;
|
|
13
|
+
let _propertyPanel: PropertyPanel;
|
|
14
|
+
let hierarchyPanel: HierarchyPanel;
|
|
15
|
+
let _uvEditor: UVEditor;
|
|
16
|
+
let serializer: Serializer;
|
|
17
|
+
|
|
18
|
+
function init(): void {
|
|
19
|
+
const container = document.getElementById("viewport");
|
|
20
|
+
if (!container) {
|
|
21
|
+
console.error("Viewport container not found");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Initialize viewer
|
|
26
|
+
viewer = new ViewerController({ container });
|
|
27
|
+
|
|
28
|
+
// Initialize editor
|
|
29
|
+
editor = new Editor({
|
|
30
|
+
scene: viewer.scene,
|
|
31
|
+
camera: viewer.camera,
|
|
32
|
+
renderer: viewer.renderer,
|
|
33
|
+
controls: viewer.controls,
|
|
34
|
+
domElement: container,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Initialize serializer
|
|
38
|
+
serializer = new Serializer();
|
|
39
|
+
|
|
40
|
+
// Initialize UI panels
|
|
41
|
+
_propertyPanel = new PropertyPanel(editor, "property-panel");
|
|
42
|
+
hierarchyPanel = new HierarchyPanel(editor, "hierarchy-panel");
|
|
43
|
+
_uvEditor = new UVEditor(editor, "uv-panel");
|
|
44
|
+
|
|
45
|
+
// Setup file inputs
|
|
46
|
+
setupFileInputs();
|
|
47
|
+
|
|
48
|
+
// Setup toolbar buttons
|
|
49
|
+
setupToolbar();
|
|
50
|
+
|
|
51
|
+
// Setup keyboard shortcuts
|
|
52
|
+
setupKeyboardShortcuts();
|
|
53
|
+
|
|
54
|
+
// Setup collapsible panels
|
|
55
|
+
setupCollapsiblePanels();
|
|
56
|
+
|
|
57
|
+
// Setup editor event handlers
|
|
58
|
+
setupEditorEvents();
|
|
59
|
+
|
|
60
|
+
// Log ready
|
|
61
|
+
console.log("BlockyModel Editor initialized");
|
|
62
|
+
updateStatus("Ready - Drag & drop files or use the toolbar");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setupFileInputs(): void {
|
|
66
|
+
// Model file input
|
|
67
|
+
const modelInput = document.getElementById("model-input") as HTMLInputElement;
|
|
68
|
+
modelInput?.addEventListener("change", async (e) => {
|
|
69
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
70
|
+
if (file) {
|
|
71
|
+
await loadModel(file);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Texture file input
|
|
76
|
+
const textureInput = document.getElementById("texture-input") as HTMLInputElement;
|
|
77
|
+
textureInput?.addEventListener("change", async (e) => {
|
|
78
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
79
|
+
if (file) {
|
|
80
|
+
try {
|
|
81
|
+
updateStatus("Applying texture...");
|
|
82
|
+
await viewer.loadTexture(file);
|
|
83
|
+
updateStatus(`Texture applied: ${file.name}`);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
updateStatus(`Error: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Wireframe toggle
|
|
91
|
+
const wireframeToggle = document.getElementById("wireframe-toggle") as HTMLInputElement;
|
|
92
|
+
wireframeToggle?.addEventListener("change", (e) => {
|
|
93
|
+
viewer.toggleWireframe((e.target as HTMLInputElement).checked);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Drag and drop support
|
|
97
|
+
const viewport = document.getElementById("viewport");
|
|
98
|
+
if (viewport) {
|
|
99
|
+
viewport.addEventListener("dragover", (e) => {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
viewport.classList.add("drag-over");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
viewport.addEventListener("dragleave", () => {
|
|
105
|
+
viewport.classList.remove("drag-over");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
viewport.addEventListener("drop", async (e) => {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
viewport.classList.remove("drag-over");
|
|
111
|
+
|
|
112
|
+
const files = e.dataTransfer?.files;
|
|
113
|
+
if (!files?.length) return;
|
|
114
|
+
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
if (file.name.endsWith(".blockymodel")) {
|
|
117
|
+
await loadModel(file);
|
|
118
|
+
} else if (file.name.match(/\.(png|jpg|jpeg)$/i)) {
|
|
119
|
+
await viewer.loadTexture(file);
|
|
120
|
+
updateStatus(`Texture applied: ${file.name}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function loadModel(file: File): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
updateStatus("Loading model...");
|
|
130
|
+
const model = await viewer.loadModel(file);
|
|
131
|
+
editor.setModel(model);
|
|
132
|
+
hierarchyPanel.refresh();
|
|
133
|
+
updateStatus(`Loaded: ${file.name}`);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
updateStatus(`Error: ${error}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function setupToolbar(): void {
|
|
140
|
+
// Reset camera button
|
|
141
|
+
const resetBtn = document.getElementById("reset-camera");
|
|
142
|
+
resetBtn?.addEventListener("click", () => {
|
|
143
|
+
viewer.resetCamera();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Save button
|
|
147
|
+
const saveBtn = document.getElementById("save-model");
|
|
148
|
+
saveBtn?.addEventListener("click", () => {
|
|
149
|
+
const model = editor.getModel();
|
|
150
|
+
if (model) {
|
|
151
|
+
serializer.download(model, "model.blockymodel");
|
|
152
|
+
updateStatus("Model saved");
|
|
153
|
+
} else {
|
|
154
|
+
updateStatus("No model to save");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Undo button
|
|
159
|
+
const undoBtn = document.getElementById("undo-btn") as HTMLButtonElement;
|
|
160
|
+
undoBtn?.addEventListener("click", () => {
|
|
161
|
+
editor.undo();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Redo button
|
|
165
|
+
const redoBtn = document.getElementById("redo-btn") as HTMLButtonElement;
|
|
166
|
+
redoBtn?.addEventListener("click", () => {
|
|
167
|
+
editor.redo();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Mode buttons
|
|
171
|
+
const modeTranslate = document.getElementById("mode-translate");
|
|
172
|
+
const modeRotate = document.getElementById("mode-rotate");
|
|
173
|
+
const modeScale = document.getElementById("mode-scale");
|
|
174
|
+
|
|
175
|
+
modeTranslate?.addEventListener("click", () => {
|
|
176
|
+
editor.setTransformMode("translate");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
modeRotate?.addEventListener("click", () => {
|
|
180
|
+
editor.setTransformMode("rotate");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
modeScale?.addEventListener("click", () => {
|
|
184
|
+
editor.setTransformMode("scale");
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function setupKeyboardShortcuts(): void {
|
|
189
|
+
document.addEventListener("keydown", (e) => {
|
|
190
|
+
// Ignore if in input field
|
|
191
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Undo/Redo
|
|
196
|
+
if (e.ctrlKey || e.metaKey) {
|
|
197
|
+
if (e.key === "z") {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
if (e.shiftKey) {
|
|
200
|
+
editor.redo();
|
|
201
|
+
} else {
|
|
202
|
+
editor.undo();
|
|
203
|
+
}
|
|
204
|
+
} else if (e.key === "y") {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
editor.redo();
|
|
207
|
+
} else if (e.key === "s") {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
const model = editor.getModel();
|
|
210
|
+
if (model) {
|
|
211
|
+
serializer.download(model, "model.blockymodel");
|
|
212
|
+
updateStatus("Model saved");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function setupCollapsiblePanels(): void {
|
|
220
|
+
document.querySelectorAll(".panel-header.collapsible").forEach((header) => {
|
|
221
|
+
header.addEventListener("click", () => {
|
|
222
|
+
const targetId = header.getAttribute("data-target");
|
|
223
|
+
if (targetId) {
|
|
224
|
+
const content = document.getElementById(targetId);
|
|
225
|
+
if (content) {
|
|
226
|
+
header.classList.toggle("collapsed");
|
|
227
|
+
content.classList.toggle("collapsed");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function setupEditorEvents(): void {
|
|
235
|
+
// Selection changed
|
|
236
|
+
editor.on("selectionChanged", (object) => {
|
|
237
|
+
const selectionInfo = document.getElementById("selection-info");
|
|
238
|
+
if (selectionInfo) {
|
|
239
|
+
if (object) {
|
|
240
|
+
const obj = object as THREE.Object3D;
|
|
241
|
+
selectionInfo.textContent = `Selected: ${obj.name || "(unnamed)"}`;
|
|
242
|
+
} else {
|
|
243
|
+
selectionInfo.textContent = "";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Mode changed
|
|
249
|
+
editor.on("modeChanged", (mode) => {
|
|
250
|
+
const modeInfo = document.getElementById("mode-info");
|
|
251
|
+
if (modeInfo) {
|
|
252
|
+
const modeStr = mode as string;
|
|
253
|
+
modeInfo.textContent = `Mode: ${modeStr.charAt(0).toUpperCase() + modeStr.slice(1)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Update mode buttons
|
|
257
|
+
document.querySelectorAll(".mode-btn").forEach((btn) => {
|
|
258
|
+
btn.classList.remove("active");
|
|
259
|
+
});
|
|
260
|
+
const activeBtn = document.getElementById(`mode-${mode}`);
|
|
261
|
+
activeBtn?.classList.add("active");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// History changed
|
|
265
|
+
editor.on("historyChanged", () => {
|
|
266
|
+
const undoBtn = document.getElementById("undo-btn") as HTMLButtonElement;
|
|
267
|
+
const redoBtn = document.getElementById("redo-btn") as HTMLButtonElement;
|
|
268
|
+
|
|
269
|
+
if (undoBtn) {
|
|
270
|
+
undoBtn.disabled = !editor.canUndo();
|
|
271
|
+
}
|
|
272
|
+
if (redoBtn) {
|
|
273
|
+
redoBtn.disabled = !editor.canRedo();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function updateStatus(message: string): void {
|
|
279
|
+
const status = document.getElementById("status");
|
|
280
|
+
if (status) {
|
|
281
|
+
status.textContent = message;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Initialize when DOM is ready
|
|
286
|
+
if (document.readyState === "loading") {
|
|
287
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
288
|
+
} else {
|
|
289
|
+
init();
|
|
290
|
+
}
|