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,434 @@
1
+ import * as THREE from "three";
2
+ import type { Editor } from "../editor/Editor";
3
+ import { SetPositionCommand } from "../editor/commands/SetPositionCommand";
4
+ import { SetRotationCommand } from "../editor/commands/SetRotationCommand";
5
+ import { SetScaleCommand } from "../editor/commands/SetScaleCommand";
6
+ import { SetPropertyCommand } from "../editor/commands/SetPropertyCommand";
7
+ import type { ShadingMode } from "../types/blockymodel";
8
+
9
+ /**
10
+ * Property panel for editing selected object properties
11
+ */
12
+ export class PropertyPanel {
13
+ private editor: Editor;
14
+ private container: HTMLElement;
15
+ private currentObject: THREE.Object3D | null = null;
16
+ private isUpdating: boolean = false;
17
+
18
+ // Input elements
19
+ private nameInput!: HTMLInputElement;
20
+ private posInputs!: { x: HTMLInputElement; y: HTMLInputElement; z: HTMLInputElement };
21
+ private rotInputs!: { x: HTMLInputElement; y: HTMLInputElement; z: HTMLInputElement };
22
+ private scaleInputs!: { x: HTMLInputElement; y: HTMLInputElement; z: HTMLInputElement };
23
+ private sizeInputs!: { x: HTMLInputElement; y: HTMLInputElement; z: HTMLInputElement };
24
+ private offsetInputs!: { x: HTMLInputElement; y: HTMLInputElement; z: HTMLInputElement };
25
+ private visibleCheckbox!: HTMLInputElement;
26
+ private doubleSidedCheckbox!: HTMLInputElement;
27
+ private shadingModeSelect!: HTMLSelectElement;
28
+ private shapeTypeSpan!: HTMLElement;
29
+
30
+ constructor(editor: Editor, containerId: string) {
31
+ this.editor = editor;
32
+ const container = document.getElementById(containerId);
33
+ if (!container) {
34
+ throw new Error(`Property panel container not found: ${containerId}`);
35
+ }
36
+ this.container = container;
37
+
38
+ this.buildUI();
39
+ this.setupEventListeners();
40
+ }
41
+
42
+ /**
43
+ * Build the property panel UI
44
+ */
45
+ private buildUI(): void {
46
+ this.container.innerHTML = `
47
+ <div class="property-section" id="properties-transform">
48
+ <div class="section-header">Transform</div>
49
+ <div class="property-row">
50
+ <label>Name</label>
51
+ <input type="text" id="prop-name" class="prop-input prop-text" />
52
+ </div>
53
+ <div class="property-row">
54
+ <label>Position</label>
55
+ <div class="vec3-inputs">
56
+ <input type="number" id="prop-pos-x" class="prop-input" step="0.1" />
57
+ <input type="number" id="prop-pos-y" class="prop-input" step="0.1" />
58
+ <input type="number" id="prop-pos-z" class="prop-input" step="0.1" />
59
+ </div>
60
+ </div>
61
+ <div class="property-row">
62
+ <label>Rotation</label>
63
+ <div class="vec3-inputs">
64
+ <input type="number" id="prop-rot-x" class="prop-input" step="1" />
65
+ <input type="number" id="prop-rot-y" class="prop-input" step="1" />
66
+ <input type="number" id="prop-rot-z" class="prop-input" step="1" />
67
+ </div>
68
+ </div>
69
+ <div class="property-row">
70
+ <label>Scale</label>
71
+ <div class="vec3-inputs">
72
+ <input type="number" id="prop-scale-x" class="prop-input" step="0.1" min="0.01" />
73
+ <input type="number" id="prop-scale-y" class="prop-input" step="0.1" min="0.01" />
74
+ <input type="number" id="prop-scale-z" class="prop-input" step="0.1" min="0.01" />
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="property-section" id="properties-shape">
80
+ <div class="section-header">Shape</div>
81
+ <div class="property-row">
82
+ <label>Type</label>
83
+ <span id="prop-shape-type" class="prop-readonly">-</span>
84
+ </div>
85
+ <div class="property-row">
86
+ <label>Size</label>
87
+ <div class="vec3-inputs">
88
+ <input type="number" id="prop-size-x" class="prop-input" step="1" min="0" />
89
+ <input type="number" id="prop-size-y" class="prop-input" step="1" min="0" />
90
+ <input type="number" id="prop-size-z" class="prop-input" step="1" min="0" />
91
+ </div>
92
+ </div>
93
+ <div class="property-row">
94
+ <label>Offset</label>
95
+ <div class="vec3-inputs">
96
+ <input type="number" id="prop-offset-x" class="prop-input" step="0.5" />
97
+ <input type="number" id="prop-offset-y" class="prop-input" step="0.5" />
98
+ <input type="number" id="prop-offset-z" class="prop-input" step="0.5" />
99
+ </div>
100
+ </div>
101
+ <div class="property-row">
102
+ <label>Visible</label>
103
+ <input type="checkbox" id="prop-visible" class="prop-checkbox" />
104
+ </div>
105
+ <div class="property-row">
106
+ <label>Double Sided</label>
107
+ <input type="checkbox" id="prop-doublesided" class="prop-checkbox" />
108
+ </div>
109
+ <div class="property-row">
110
+ <label>Shading</label>
111
+ <select id="prop-shading" class="prop-select">
112
+ <option value="standard">Standard</option>
113
+ <option value="flat">Flat</option>
114
+ <option value="fullbright">Fullbright</option>
115
+ <option value="reflective">Reflective</option>
116
+ </select>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="property-empty" id="properties-empty">
121
+ <p>Select an object to view properties</p>
122
+ </div>
123
+ `;
124
+
125
+ // Cache input references
126
+ this.nameInput = document.getElementById("prop-name") as HTMLInputElement;
127
+ this.posInputs = {
128
+ x: document.getElementById("prop-pos-x") as HTMLInputElement,
129
+ y: document.getElementById("prop-pos-y") as HTMLInputElement,
130
+ z: document.getElementById("prop-pos-z") as HTMLInputElement,
131
+ };
132
+ this.rotInputs = {
133
+ x: document.getElementById("prop-rot-x") as HTMLInputElement,
134
+ y: document.getElementById("prop-rot-y") as HTMLInputElement,
135
+ z: document.getElementById("prop-rot-z") as HTMLInputElement,
136
+ };
137
+ this.scaleInputs = {
138
+ x: document.getElementById("prop-scale-x") as HTMLInputElement,
139
+ y: document.getElementById("prop-scale-y") as HTMLInputElement,
140
+ z: document.getElementById("prop-scale-z") as HTMLInputElement,
141
+ };
142
+ this.sizeInputs = {
143
+ x: document.getElementById("prop-size-x") as HTMLInputElement,
144
+ y: document.getElementById("prop-size-y") as HTMLInputElement,
145
+ z: document.getElementById("prop-size-z") as HTMLInputElement,
146
+ };
147
+ this.offsetInputs = {
148
+ x: document.getElementById("prop-offset-x") as HTMLInputElement,
149
+ y: document.getElementById("prop-offset-y") as HTMLInputElement,
150
+ z: document.getElementById("prop-offset-z") as HTMLInputElement,
151
+ };
152
+ this.visibleCheckbox = document.getElementById("prop-visible") as HTMLInputElement;
153
+ this.doubleSidedCheckbox = document.getElementById("prop-doublesided") as HTMLInputElement;
154
+ this.shadingModeSelect = document.getElementById("prop-shading") as HTMLSelectElement;
155
+ this.shapeTypeSpan = document.getElementById("prop-shape-type") as HTMLElement;
156
+
157
+ // Initially hide property sections
158
+ this.showEmpty();
159
+ }
160
+
161
+ /**
162
+ * Setup event listeners
163
+ */
164
+ private setupEventListeners(): void {
165
+ // Editor events
166
+ this.editor.on("selectionChanged", (object) => {
167
+ this.setObject(object as THREE.Object3D | null);
168
+ });
169
+
170
+ this.editor.on("objectChanged", () => {
171
+ if (this.currentObject) {
172
+ this.updateFromObject();
173
+ }
174
+ });
175
+
176
+ // Listen for live transform updates
177
+ this.editor.transform.on("objectChanging", () => {
178
+ if (this.currentObject) {
179
+ this.updateFromObject();
180
+ }
181
+ });
182
+
183
+ // Name input
184
+ this.nameInput.addEventListener("change", () => {
185
+ if (this.currentObject && !this.isUpdating) {
186
+ const oldName = this.currentObject.name;
187
+ const newName = this.nameInput.value;
188
+ this.editor.execute(new SetPropertyCommand(this.currentObject, "name", newName, oldName));
189
+ }
190
+ });
191
+
192
+ // Position inputs
193
+ this.setupVec3Input(this.posInputs, (value) => {
194
+ if (this.currentObject && !this.isUpdating) {
195
+ const oldPos = this.currentObject.position.clone();
196
+ this.editor.execute(new SetPositionCommand(this.currentObject, value, oldPos));
197
+ }
198
+ });
199
+
200
+ // Rotation inputs (degrees to radians)
201
+ this.setupVec3Input(this.rotInputs, (value) => {
202
+ if (this.currentObject && !this.isUpdating) {
203
+ const oldRot = this.currentObject.rotation.clone();
204
+ const newRot = new THREE.Euler(
205
+ THREE.MathUtils.degToRad(value.x),
206
+ THREE.MathUtils.degToRad(value.y),
207
+ THREE.MathUtils.degToRad(value.z)
208
+ );
209
+ this.editor.execute(new SetRotationCommand(this.currentObject, newRot, oldRot));
210
+ }
211
+ });
212
+
213
+ // Scale inputs
214
+ this.setupVec3Input(this.scaleInputs, (value) => {
215
+ if (this.currentObject && !this.isUpdating) {
216
+ const oldScale = this.currentObject.scale.clone();
217
+ this.editor.execute(new SetScaleCommand(this.currentObject, value, oldScale));
218
+ }
219
+ });
220
+
221
+ // Size inputs
222
+ this.setupVec3Input(this.sizeInputs, (value) => {
223
+ if (this.currentObject && !this.isUpdating) {
224
+ const oldSize = this.currentObject.userData.originalSize || { x: 1, y: 1, z: 1 };
225
+ this.editor.execute(
226
+ new SetPropertyCommand(this.currentObject, "userData.originalSize", { x: value.x, y: value.y, z: value.z }, oldSize)
227
+ );
228
+ this.rebuildGeometry();
229
+ }
230
+ });
231
+
232
+ // Offset inputs
233
+ this.setupVec3Input(this.offsetInputs, (value) => {
234
+ if (this.currentObject && !this.isUpdating) {
235
+ const oldOffset = this.currentObject.userData.offset || { x: 0, y: 0, z: 0 };
236
+ this.editor.execute(
237
+ new SetPropertyCommand(this.currentObject, "userData.offset", { x: value.x, y: value.y, z: value.z }, oldOffset)
238
+ );
239
+ this.rebuildGeometry();
240
+ }
241
+ });
242
+
243
+ // Visible checkbox
244
+ this.visibleCheckbox.addEventListener("change", () => {
245
+ if (this.currentObject && !this.isUpdating) {
246
+ const oldValue = this.currentObject.visible;
247
+ const newValue = this.visibleCheckbox.checked;
248
+ this.editor.execute(new SetPropertyCommand(this.currentObject, "visible", newValue, oldValue));
249
+ }
250
+ });
251
+
252
+ // Double sided checkbox
253
+ this.doubleSidedCheckbox.addEventListener("change", () => {
254
+ if (this.currentObject && !this.isUpdating && this.currentObject instanceof THREE.Mesh) {
255
+ const material = this.currentObject.material as THREE.MeshStandardMaterial;
256
+ const oldValue = material.side === THREE.DoubleSide;
257
+ const newValue = this.doubleSidedCheckbox.checked;
258
+ material.side = newValue ? THREE.DoubleSide : THREE.FrontSide;
259
+ this.currentObject.userData.doubleSided = newValue;
260
+ this.editor.execute(
261
+ new SetPropertyCommand(this.currentObject, "userData.doubleSided", newValue, oldValue)
262
+ );
263
+ }
264
+ });
265
+
266
+ // Shading mode select
267
+ this.shadingModeSelect.addEventListener("change", () => {
268
+ if (this.currentObject && !this.isUpdating) {
269
+ const oldValue = this.currentObject.userData.shadingMode || "standard";
270
+ const newValue = this.shadingModeSelect.value as ShadingMode;
271
+ this.currentObject.userData.shadingMode = newValue;
272
+ this.editor.execute(
273
+ new SetPropertyCommand(this.currentObject, "userData.shadingMode", newValue, oldValue)
274
+ );
275
+ }
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Setup Vec3 input handlers
281
+ */
282
+ private setupVec3Input(
283
+ inputs: { x: HTMLInputElement; y: HTMLInputElement; z: HTMLInputElement },
284
+ onChange: (value: THREE.Vector3) => void
285
+ ): void {
286
+ const handler = () => {
287
+ const value = new THREE.Vector3(
288
+ parseFloat(inputs.x.value) || 0,
289
+ parseFloat(inputs.y.value) || 0,
290
+ parseFloat(inputs.z.value) || 0
291
+ );
292
+ onChange(value);
293
+ };
294
+
295
+ inputs.x.addEventListener("change", handler);
296
+ inputs.y.addEventListener("change", handler);
297
+ inputs.z.addEventListener("change", handler);
298
+ }
299
+
300
+ /**
301
+ * Set the object to display properties for
302
+ */
303
+ setObject(object: THREE.Object3D | null): void {
304
+ this.currentObject = object;
305
+
306
+ if (object) {
307
+ this.showProperties();
308
+ this.updateFromObject();
309
+ } else {
310
+ this.showEmpty();
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Update UI from current object
316
+ */
317
+ private updateFromObject(): void {
318
+ if (!this.currentObject) return;
319
+
320
+ this.isUpdating = true;
321
+
322
+ const obj = this.currentObject;
323
+
324
+ // Name
325
+ this.nameInput.value = obj.name || "";
326
+
327
+ // Position
328
+ this.posInputs.x.value = obj.position.x.toFixed(2);
329
+ this.posInputs.y.value = obj.position.y.toFixed(2);
330
+ this.posInputs.z.value = obj.position.z.toFixed(2);
331
+
332
+ // Rotation (radians to degrees)
333
+ this.rotInputs.x.value = THREE.MathUtils.radToDeg(obj.rotation.x).toFixed(1);
334
+ this.rotInputs.y.value = THREE.MathUtils.radToDeg(obj.rotation.y).toFixed(1);
335
+ this.rotInputs.z.value = THREE.MathUtils.radToDeg(obj.rotation.z).toFixed(1);
336
+
337
+ // Scale
338
+ this.scaleInputs.x.value = obj.scale.x.toFixed(2);
339
+ this.scaleInputs.y.value = obj.scale.y.toFixed(2);
340
+ this.scaleInputs.z.value = obj.scale.z.toFixed(2);
341
+
342
+ // Shape properties
343
+ const shapeType = obj.userData.shapeType || "none";
344
+ this.shapeTypeSpan.textContent = shapeType;
345
+
346
+ // Size
347
+ const size = obj.userData.originalSize || { x: 1, y: 1, z: 1 };
348
+ this.sizeInputs.x.value = size.x?.toString() || "1";
349
+ this.sizeInputs.y.value = size.y?.toString() || "1";
350
+ this.sizeInputs.z.value = size.z?.toString() || "1";
351
+
352
+ // Offset
353
+ const offset = obj.userData.offset || { x: 0, y: 0, z: 0 };
354
+ this.offsetInputs.x.value = offset.x?.toString() || "0";
355
+ this.offsetInputs.y.value = offset.y?.toString() || "0";
356
+ this.offsetInputs.z.value = offset.z?.toString() || "0";
357
+
358
+ // Visible
359
+ this.visibleCheckbox.checked = obj.visible;
360
+
361
+ // Double sided
362
+ if (obj instanceof THREE.Mesh) {
363
+ const material = obj.material as THREE.MeshStandardMaterial;
364
+ this.doubleSidedCheckbox.checked = material.side === THREE.DoubleSide;
365
+ } else {
366
+ this.doubleSidedCheckbox.checked = false;
367
+ }
368
+
369
+ // Shading mode
370
+ this.shadingModeSelect.value = obj.userData.shadingMode || "standard";
371
+
372
+ this.isUpdating = false;
373
+ }
374
+
375
+ /**
376
+ * Rebuild geometry after size/offset changes
377
+ */
378
+ private rebuildGeometry(): void {
379
+ if (!this.currentObject || !(this.currentObject instanceof THREE.Mesh)) return;
380
+
381
+ const mesh = this.currentObject;
382
+ const size = mesh.userData.originalSize || { x: 1, y: 1, z: 1 };
383
+ const offset = mesh.userData.offset || { x: 0, y: 0, z: 0 };
384
+ const stretch = mesh.userData.stretch || { x: 1, y: 1, z: 1 };
385
+
386
+ const finalSize = {
387
+ x: size.x * stretch.x,
388
+ y: size.y * stretch.y,
389
+ z: size.z * stretch.z,
390
+ };
391
+
392
+ // Dispose old geometry
393
+ mesh.geometry.dispose();
394
+
395
+ // Create new geometry based on shape type
396
+ if (mesh.userData.shapeType === "box") {
397
+ mesh.geometry = new THREE.BoxGeometry(finalSize.x, finalSize.y, finalSize.z);
398
+ mesh.geometry.translate(offset.x, offset.y, offset.z);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Show property sections
404
+ */
405
+ private showProperties(): void {
406
+ const transform = document.getElementById("properties-transform");
407
+ const shape = document.getElementById("properties-shape");
408
+ const empty = document.getElementById("properties-empty");
409
+
410
+ if (transform) transform.style.display = "block";
411
+ if (shape) shape.style.display = "block";
412
+ if (empty) empty.style.display = "none";
413
+ }
414
+
415
+ /**
416
+ * Show empty state
417
+ */
418
+ private showEmpty(): void {
419
+ const transform = document.getElementById("properties-transform");
420
+ const shape = document.getElementById("properties-shape");
421
+ const empty = document.getElementById("properties-empty");
422
+
423
+ if (transform) transform.style.display = "none";
424
+ if (shape) shape.style.display = "none";
425
+ if (empty) empty.style.display = "block";
426
+ }
427
+
428
+ /**
429
+ * Cleanup
430
+ */
431
+ dispose(): void {
432
+ this.container.innerHTML = "";
433
+ }
434
+ }