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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/dist/blockymodel-web.js +2717 -0
  3. package/dist/blockymodel-web.js.map +1 -0
  4. package/dist/blockymodel-web.umd.cjs +122 -0
  5. package/dist/blockymodel-web.umd.cjs.map +1 -0
  6. package/dist/editor/Editor.d.ts +94 -0
  7. package/dist/editor/Editor.d.ts.map +1 -0
  8. package/dist/editor/History.d.ts +52 -0
  9. package/dist/editor/History.d.ts.map +1 -0
  10. package/dist/editor/SelectionManager.d.ts +57 -0
  11. package/dist/editor/SelectionManager.d.ts.map +1 -0
  12. package/dist/editor/Serializer.d.ts +44 -0
  13. package/dist/editor/Serializer.d.ts.map +1 -0
  14. package/dist/editor/TransformManager.d.ts +73 -0
  15. package/dist/editor/TransformManager.d.ts.map +1 -0
  16. package/dist/editor/commands/AddNodeCommand.d.ts +24 -0
  17. package/dist/editor/commands/AddNodeCommand.d.ts.map +1 -0
  18. package/dist/editor/commands/Command.d.ts +50 -0
  19. package/dist/editor/commands/Command.d.ts.map +1 -0
  20. package/dist/editor/commands/RemoveNodeCommand.d.ts +28 -0
  21. package/dist/editor/commands/RemoveNodeCommand.d.ts.map +1 -0
  22. package/dist/editor/commands/SetPositionCommand.d.ts +24 -0
  23. package/dist/editor/commands/SetPositionCommand.d.ts.map +1 -0
  24. package/dist/editor/commands/SetPropertyCommand.d.ts +41 -0
  25. package/dist/editor/commands/SetPropertyCommand.d.ts.map +1 -0
  26. package/dist/editor/commands/SetRotationCommand.d.ts +24 -0
  27. package/dist/editor/commands/SetRotationCommand.d.ts.map +1 -0
  28. package/dist/editor/commands/SetScaleCommand.d.ts +24 -0
  29. package/dist/editor/commands/SetScaleCommand.d.ts.map +1 -0
  30. package/dist/editor/index.d.ts +15 -0
  31. package/dist/editor/index.d.ts.map +1 -0
  32. package/dist/index.d.ts +21 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/loaders/BlockyModelLoader.d.ts +48 -0
  35. package/dist/loaders/BlockyModelLoader.d.ts.map +1 -0
  36. package/dist/types/blockymodel.d.ts +72 -0
  37. package/dist/types/blockymodel.d.ts.map +1 -0
  38. package/dist/ui/HierarchyPanel.d.ts +73 -0
  39. package/dist/ui/HierarchyPanel.d.ts.map +1 -0
  40. package/dist/ui/PropertyPanel.d.ts +59 -0
  41. package/dist/ui/PropertyPanel.d.ts.map +1 -0
  42. package/dist/ui/UVEditor.d.ts +56 -0
  43. package/dist/ui/UVEditor.d.ts.map +1 -0
  44. package/dist/ui/index.d.ts +4 -0
  45. package/dist/ui/index.d.ts.map +1 -0
  46. package/dist/viewer/ViewerController.d.ts +71 -0
  47. package/dist/viewer/ViewerController.d.ts.map +1 -0
  48. package/package.json +63 -0
  49. package/src/editor/Editor.ts +196 -0
  50. package/src/editor/History.ts +123 -0
  51. package/src/editor/SelectionManager.ts +183 -0
  52. package/src/editor/Serializer.ts +212 -0
  53. package/src/editor/TransformManager.ts +270 -0
  54. package/src/editor/commands/AddNodeCommand.ts +53 -0
  55. package/src/editor/commands/Command.ts +63 -0
  56. package/src/editor/commands/RemoveNodeCommand.ts +59 -0
  57. package/src/editor/commands/SetPositionCommand.ts +51 -0
  58. package/src/editor/commands/SetPropertyCommand.ts +100 -0
  59. package/src/editor/commands/SetRotationCommand.ts +51 -0
  60. package/src/editor/commands/SetScaleCommand.ts +51 -0
  61. package/src/editor/index.ts +19 -0
  62. package/src/index.ts +49 -0
  63. package/src/loaders/BlockyModelLoader.ts +281 -0
  64. package/src/main.ts +290 -0
  65. package/src/styles.css +597 -0
  66. package/src/types/blockymodel.ts +82 -0
  67. package/src/ui/HierarchyPanel.ts +343 -0
  68. package/src/ui/PropertyPanel.ts +434 -0
  69. package/src/ui/UVEditor.ts +336 -0
  70. package/src/ui/index.ts +4 -0
  71. 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
+ }