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,343 @@
1
+ import * as THREE from "three";
2
+ import type { Editor } from "../editor/Editor";
3
+ import { AddNodeCommand } from "../editor/commands/AddNodeCommand";
4
+ import { RemoveNodeCommand } from "../editor/commands/RemoveNodeCommand";
5
+
6
+ /**
7
+ * Hierarchy panel showing the model tree structure
8
+ */
9
+ export class HierarchyPanel {
10
+ private editor: Editor;
11
+ private container: HTMLElement;
12
+ private treeContainer!: HTMLElement;
13
+ private contextMenu: HTMLElement | null = null;
14
+ private contextTarget: THREE.Object3D | null = null;
15
+
16
+ constructor(editor: Editor, containerId: string) {
17
+ this.editor = editor;
18
+ const container = document.getElementById(containerId);
19
+ if (!container) {
20
+ throw new Error(`Hierarchy panel container not found: ${containerId}`);
21
+ }
22
+ this.container = container;
23
+
24
+ this.buildUI();
25
+ this.setupEventListeners();
26
+ }
27
+
28
+ /**
29
+ * Build the hierarchy panel UI
30
+ */
31
+ private buildUI(): void {
32
+ this.container.innerHTML = `
33
+ <div class="hierarchy-tree" id="hierarchy-tree"></div>
34
+ `;
35
+
36
+ this.treeContainer = document.getElementById("hierarchy-tree") as HTMLElement;
37
+
38
+ // Create context menu (hidden by default)
39
+ this.contextMenu = document.createElement("div");
40
+ this.contextMenu.className = "context-menu";
41
+ this.contextMenu.innerHTML = `
42
+ <div class="context-menu-item" data-action="add-box">Add Child Box</div>
43
+ <div class="context-menu-item" data-action="add-group">Add Child Group</div>
44
+ <div class="context-menu-separator"></div>
45
+ <div class="context-menu-item" data-action="duplicate">Duplicate</div>
46
+ <div class="context-menu-separator"></div>
47
+ <div class="context-menu-item context-menu-danger" data-action="delete">Delete</div>
48
+ `;
49
+ this.contextMenu.style.display = "none";
50
+ document.body.appendChild(this.contextMenu);
51
+ }
52
+
53
+ /**
54
+ * Setup event listeners
55
+ */
56
+ private setupEventListeners(): void {
57
+ // Editor events
58
+ this.editor.on("modelLoaded", () => {
59
+ this.refresh();
60
+ });
61
+
62
+ this.editor.on("selectionChanged", () => {
63
+ this.updateSelection();
64
+ });
65
+
66
+ this.editor.on("objectChanged", () => {
67
+ this.refresh();
68
+ });
69
+
70
+ // Context menu handlers
71
+ this.contextMenu?.addEventListener("click", (e) => {
72
+ const target = e.target as HTMLElement;
73
+ const action = target.dataset.action;
74
+ if (action && this.contextTarget) {
75
+ this.handleContextAction(action, this.contextTarget);
76
+ }
77
+ this.hideContextMenu();
78
+ });
79
+
80
+ // Hide context menu on click outside
81
+ document.addEventListener("click", (e) => {
82
+ if (this.contextMenu && !this.contextMenu.contains(e.target as Node)) {
83
+ this.hideContextMenu();
84
+ }
85
+ });
86
+
87
+ // Keyboard shortcuts
88
+ document.addEventListener("keydown", (e) => {
89
+ if (e.key === "Delete" || e.key === "Backspace") {
90
+ const selected = this.editor.getSelected();
91
+ if (selected && selected.parent && !(e.target instanceof HTMLInputElement)) {
92
+ this.deleteNode(selected);
93
+ }
94
+ }
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Refresh the hierarchy tree
100
+ */
101
+ refresh(): void {
102
+ const model = this.editor.getModel();
103
+ if (!model) {
104
+ this.treeContainer.innerHTML = `<p class="hierarchy-empty">No model loaded</p>`;
105
+ return;
106
+ }
107
+
108
+ this.treeContainer.innerHTML = "";
109
+ this.buildTree(model, this.treeContainer, 0);
110
+ }
111
+
112
+ /**
113
+ * Build tree recursively
114
+ */
115
+ private buildTree(object: THREE.Object3D, parent: HTMLElement, depth: number): void {
116
+ const item = document.createElement("div");
117
+ item.className = "hierarchy-item";
118
+ item.dataset.objectId = object.uuid;
119
+
120
+ const indent = document.createElement("span");
121
+ indent.className = "hierarchy-indent";
122
+ indent.style.width = `${depth * 16}px`;
123
+
124
+ const expander = document.createElement("span");
125
+ expander.className = "hierarchy-expander";
126
+ if (object.children.length > 0) {
127
+ expander.textContent = "▼";
128
+ expander.addEventListener("click", (e) => {
129
+ e.stopPropagation();
130
+ this.toggleExpand(item);
131
+ });
132
+ }
133
+
134
+ const icon = document.createElement("span");
135
+ icon.className = "hierarchy-icon";
136
+ icon.textContent = this.getIcon(object);
137
+
138
+ const label = document.createElement("span");
139
+ label.className = "hierarchy-label";
140
+ label.textContent = object.name || `(${object.type})`;
141
+
142
+ item.appendChild(indent);
143
+ item.appendChild(expander);
144
+ item.appendChild(icon);
145
+ item.appendChild(label);
146
+
147
+ // Click to select
148
+ item.addEventListener("click", () => {
149
+ this.editor.select(object);
150
+ });
151
+
152
+ // Right-click for context menu
153
+ item.addEventListener("contextmenu", (e) => {
154
+ e.preventDefault();
155
+ this.showContextMenu(e, object);
156
+ });
157
+
158
+ parent.appendChild(item);
159
+
160
+ // Children container
161
+ if (object.children.length > 0) {
162
+ const children = document.createElement("div");
163
+ children.className = "hierarchy-children";
164
+ for (const child of object.children) {
165
+ // Skip helper objects
166
+ if (child.type === "TransformControlsGizmo" || child.type === "TransformControlsPlane") {
167
+ continue;
168
+ }
169
+ this.buildTree(child, children, depth + 1);
170
+ }
171
+ parent.appendChild(children);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Get icon for object type
177
+ */
178
+ private getIcon(object: THREE.Object3D): string {
179
+ if (object instanceof THREE.Mesh) {
180
+ const shapeType = object.userData.shapeType;
181
+ if (shapeType === "box") return "📦";
182
+ if (shapeType === "quad") return "▫️";
183
+ return "🔷";
184
+ }
185
+ if (object instanceof THREE.Group) return "📁";
186
+ return "◯";
187
+ }
188
+
189
+ /**
190
+ * Toggle expand/collapse
191
+ */
192
+ private toggleExpand(item: HTMLElement): void {
193
+ const children = item.nextElementSibling;
194
+ if (children?.classList.contains("hierarchy-children")) {
195
+ const isCollapsed = children.classList.toggle("collapsed");
196
+ const expander = item.querySelector(".hierarchy-expander");
197
+ if (expander) {
198
+ expander.textContent = isCollapsed ? "▶" : "▼";
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Update selection highlight
205
+ */
206
+ private updateSelection(): void {
207
+ // Remove all selection highlights
208
+ this.treeContainer.querySelectorAll(".hierarchy-item.selected").forEach((el) => {
209
+ el.classList.remove("selected");
210
+ });
211
+
212
+ // Highlight selected object
213
+ const selected = this.editor.getSelected();
214
+ if (selected) {
215
+ const item = this.treeContainer.querySelector(`[data-object-id="${selected.uuid}"]`);
216
+ if (item) {
217
+ item.classList.add("selected");
218
+ // Scroll into view
219
+ item.scrollIntoView({ behavior: "smooth", block: "nearest" });
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Show context menu
226
+ */
227
+ private showContextMenu(event: MouseEvent, object: THREE.Object3D): void {
228
+ if (!this.contextMenu) return;
229
+
230
+ this.contextTarget = object;
231
+ this.contextMenu.style.display = "block";
232
+ this.contextMenu.style.left = `${event.clientX}px`;
233
+ this.contextMenu.style.top = `${event.clientY}px`;
234
+
235
+ // Ensure menu stays within viewport
236
+ const rect = this.contextMenu.getBoundingClientRect();
237
+ if (rect.right > window.innerWidth) {
238
+ this.contextMenu.style.left = `${window.innerWidth - rect.width - 10}px`;
239
+ }
240
+ if (rect.bottom > window.innerHeight) {
241
+ this.contextMenu.style.top = `${window.innerHeight - rect.height - 10}px`;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Hide context menu
247
+ */
248
+ private hideContextMenu(): void {
249
+ if (this.contextMenu) {
250
+ this.contextMenu.style.display = "none";
251
+ }
252
+ this.contextTarget = null;
253
+ }
254
+
255
+ /**
256
+ * Handle context menu action
257
+ */
258
+ private handleContextAction(action: string, target: THREE.Object3D): void {
259
+ switch (action) {
260
+ case "add-box":
261
+ this.addChildBox(target);
262
+ break;
263
+ case "add-group":
264
+ this.addChildGroup(target);
265
+ break;
266
+ case "duplicate":
267
+ this.duplicateNode(target);
268
+ break;
269
+ case "delete":
270
+ this.deleteNode(target);
271
+ break;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Add a child box
277
+ */
278
+ private addChildBox(parent: THREE.Object3D): void {
279
+ const geometry = new THREE.BoxGeometry(10, 10, 10);
280
+ const material = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.7 });
281
+ const mesh = new THREE.Mesh(geometry, material);
282
+
283
+ mesh.name = `Box_${Date.now().toString(36)}`;
284
+ mesh.userData.id = mesh.name;
285
+ mesh.userData.shapeType = "box";
286
+ mesh.userData.originalSize = { x: 10, y: 10, z: 10 };
287
+
288
+ this.editor.execute(new AddNodeCommand(parent, mesh));
289
+ this.editor.select(mesh);
290
+ }
291
+
292
+ /**
293
+ * Add a child group
294
+ */
295
+ private addChildGroup(parent: THREE.Object3D): void {
296
+ const group = new THREE.Group();
297
+ group.name = `Group_${Date.now().toString(36)}`;
298
+ group.userData.id = group.name;
299
+ group.userData.shapeType = "none";
300
+
301
+ this.editor.execute(new AddNodeCommand(parent, group));
302
+ this.editor.select(group);
303
+ }
304
+
305
+ /**
306
+ * Duplicate a node
307
+ */
308
+ private duplicateNode(node: THREE.Object3D): void {
309
+ if (!node.parent) return;
310
+
311
+ const clone = node.clone(true);
312
+ clone.name = `${node.name}_copy`;
313
+ clone.userData.id = clone.name;
314
+ clone.position.x += 10; // Offset to make it visible
315
+
316
+ this.editor.execute(new AddNodeCommand(node.parent, clone));
317
+ this.editor.select(clone);
318
+ }
319
+
320
+ /**
321
+ * Delete a node
322
+ */
323
+ private deleteNode(node: THREE.Object3D): void {
324
+ if (!node.parent) return;
325
+
326
+ // Don't delete root model
327
+ const model = this.editor.getModel();
328
+ if (node === model) return;
329
+
330
+ this.editor.execute(new RemoveNodeCommand(node));
331
+ this.editor.select(null);
332
+ }
333
+
334
+ /**
335
+ * Cleanup
336
+ */
337
+ dispose(): void {
338
+ if (this.contextMenu) {
339
+ document.body.removeChild(this.contextMenu);
340
+ }
341
+ this.container.innerHTML = "";
342
+ }
343
+ }