@fonsecabarreto/genesis-gl-core 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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/dist/Camera-DY_8gx3C.d.ts +45 -0
  4. package/dist/Core/classes/Material.d.ts +3 -0
  5. package/dist/Core/classes/Material.js +9 -0
  6. package/dist/Core/classes/Material.js.map +1 -0
  7. package/dist/Core/classes/Model.d.ts +5 -0
  8. package/dist/Core/classes/Model.js +7 -0
  9. package/dist/Core/classes/Model.js.map +1 -0
  10. package/dist/Core/classes/Renderer.d.ts +30 -0
  11. package/dist/Core/classes/Renderer.js +11 -0
  12. package/dist/Core/classes/Renderer.js.map +1 -0
  13. package/dist/Core/classes/Scene.d.ts +37 -0
  14. package/dist/Core/classes/Scene.js +7 -0
  15. package/dist/Core/classes/Scene.js.map +1 -0
  16. package/dist/Core/classes/Viewport.d.ts +37 -0
  17. package/dist/Core/classes/Viewport.js +7 -0
  18. package/dist/Core/classes/Viewport.js.map +1 -0
  19. package/dist/Core/domain/interfaces/Vectors.d.ts +4 -0
  20. package/dist/Core/domain/interfaces/Vectors.js +1 -0
  21. package/dist/Core/domain/interfaces/Vectors.js.map +1 -0
  22. package/dist/Core/index.d.ts +10 -0
  23. package/dist/Core/index.js +51 -0
  24. package/dist/Core/index.js.map +1 -0
  25. package/dist/Core/utils/get-overlap.d.ts +3 -0
  26. package/dist/Core/utils/get-overlap.js +11 -0
  27. package/dist/Core/utils/get-overlap.js.map +1 -0
  28. package/dist/Core/utils/load-glb.d.ts +101 -0
  29. package/dist/Core/utils/load-glb.js +697 -0
  30. package/dist/Core/utils/load-glb.js.map +1 -0
  31. package/dist/Core/utils/parse-obj.d.ts +10 -0
  32. package/dist/Core/utils/parse-obj.js +183 -0
  33. package/dist/Core/utils/parse-obj.js.map +1 -0
  34. package/dist/Editor/index.d.ts +364 -0
  35. package/dist/Editor/index.js +1737 -0
  36. package/dist/Editor/index.js.map +1 -0
  37. package/dist/Game/controls/KeyboardInput.d.ts +8 -0
  38. package/dist/Game/controls/KeyboardInput.js +7 -0
  39. package/dist/Game/controls/KeyboardInput.js.map +1 -0
  40. package/dist/Game/index.d.ts +45 -0
  41. package/dist/Game/index.js +353 -0
  42. package/dist/Game/index.js.map +1 -0
  43. package/dist/KeyboardControl-5w7Vm0J0.d.ts +18 -0
  44. package/dist/KeyboardInput-DTsfj3tE.d.ts +166 -0
  45. package/dist/Material-BGLkldxv.d.ts +74 -0
  46. package/dist/Model-CQvDXd-b.d.ts +302 -0
  47. package/dist/WebGLCore-DR7ZHJB0.d.ts +22 -0
  48. package/dist/chunk-3ULETMWF.js +144 -0
  49. package/dist/chunk-3ULETMWF.js.map +1 -0
  50. package/dist/chunk-5TAAXI6S.js +330 -0
  51. package/dist/chunk-5TAAXI6S.js.map +1 -0
  52. package/dist/chunk-6LS6AO5H.js +296 -0
  53. package/dist/chunk-6LS6AO5H.js.map +1 -0
  54. package/dist/chunk-JK2HEZAT.js +317 -0
  55. package/dist/chunk-JK2HEZAT.js.map +1 -0
  56. package/dist/chunk-P7QOKDLY.js +57 -0
  57. package/dist/chunk-P7QOKDLY.js.map +1 -0
  58. package/dist/chunk-QCQVJCSR.js +968 -0
  59. package/dist/chunk-QCQVJCSR.js.map +1 -0
  60. package/dist/chunk-SUNYSY45.js +81 -0
  61. package/dist/chunk-SUNYSY45.js.map +1 -0
  62. package/package.json +83 -0
@@ -0,0 +1,1737 @@
1
+ // src/Editor/sections/materials/MaterialSpec.ts
2
+ function createDefaultSpec(name = "New Material") {
3
+ return {
4
+ id: crypto.randomUUID(),
5
+ name,
6
+ albedoColor: [1, 1, 1, 1],
7
+ diffuse: [1, 1, 1],
8
+ ambientColor: [0.1, 0.1, 0.1],
9
+ specular: [0.3, 0.3, 0.3],
10
+ shininess: 64,
11
+ dissolve: 1,
12
+ unlit: false,
13
+ doubleSided: false,
14
+ friction: 0.3
15
+ };
16
+ }
17
+
18
+ // src/Editor/sections/materials/MaterialStore.ts
19
+ var STORAGE_KEY = "genesisgl__material_presets";
20
+ var MaterialStore = class {
21
+ specs = /* @__PURE__ */ new Map();
22
+ listeners = [];
23
+ filePath;
24
+ constructor(options = {}) {
25
+ this.filePath = options.filePath ?? null;
26
+ this.loadFromStorage();
27
+ }
28
+ /**
29
+ * Asynchronously loads materials from the configured {@link MaterialStoreOptions.filePath}.
30
+ * Call this once during app / resource initialisation (e.g. inside `loadResources`).
31
+ *
32
+ * The file data is merged on top of any localStorage data; conflicting IDs are
33
+ * overwritten by the file version.
34
+ */
35
+ async init() {
36
+ if (!this.filePath) return;
37
+ try {
38
+ const res = await fetch(this.filePath);
39
+ if (!res.ok) {
40
+ console.warn(
41
+ `[MaterialStore] Could not load "${this.filePath}" (${res.status})`
42
+ );
43
+ return;
44
+ }
45
+ const items = await res.json();
46
+ for (const spec of items) this.specs.set(spec.id, spec);
47
+ this.persist();
48
+ this.emit();
49
+ } catch {
50
+ console.warn(`[MaterialStore] Failed to fetch "${this.filePath}"`);
51
+ }
52
+ }
53
+ // ── CRUD ────────────────────────────────────────────────────
54
+ getAll() {
55
+ return [...this.specs.values()];
56
+ }
57
+ get(id) {
58
+ return this.specs.get(id);
59
+ }
60
+ /** Add a brand-new spec and return it. */
61
+ create(name) {
62
+ const spec = createDefaultSpec(name);
63
+ this.specs.set(spec.id, spec);
64
+ this.persist();
65
+ this.emit();
66
+ void this.syncToServer();
67
+ return spec;
68
+ }
69
+ /** Replace an existing spec (matched by id). */
70
+ update(spec) {
71
+ if (!this.specs.has(spec.id)) return;
72
+ this.specs.set(spec.id, { ...spec });
73
+ this.persist();
74
+ this.emit();
75
+ void this.syncToServer();
76
+ }
77
+ /** Deep-clone a spec under a new id. */
78
+ duplicate(id) {
79
+ const src = this.specs.get(id);
80
+ if (!src) return void 0;
81
+ const copy = {
82
+ ...src,
83
+ id: crypto.randomUUID(),
84
+ name: `${src.name} (copy)`,
85
+ albedoColor: [...src.albedoColor],
86
+ diffuse: [...src.diffuse],
87
+ ambientColor: [...src.ambientColor],
88
+ specular: [...src.specular]
89
+ };
90
+ this.specs.set(copy.id, copy);
91
+ this.persist();
92
+ this.emit();
93
+ void this.syncToServer();
94
+ return copy;
95
+ }
96
+ delete(id) {
97
+ this.specs.delete(id);
98
+ this.persist();
99
+ this.emit();
100
+ void this.syncToServer();
101
+ }
102
+ // ── Server sync ──────────────────────────────────────────────
103
+ /**
104
+ * Write the full collection to the server's `materials.json` via the
105
+ * `PUT /api/materials` endpoint provided by the Vite dev plugin.
106
+ * Silently no-ops if no `filePath` was configured.
107
+ */
108
+ async syncToServer() {
109
+ if (!this.filePath) return false;
110
+ try {
111
+ const res = await fetch("/api/materials", {
112
+ method: "PUT",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(this.getAll(), null, 2)
115
+ });
116
+ if (!res.ok) {
117
+ console.warn(`[MaterialStore] Server sync failed (${res.status})`);
118
+ }
119
+ return res.ok;
120
+ } catch {
121
+ console.warn("[MaterialStore] syncToServer: network error");
122
+ return false;
123
+ }
124
+ }
125
+ // ── Persistence ──────────────────────────────────────────────
126
+ persist() {
127
+ try {
128
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.getAll()));
129
+ } catch {
130
+ console.warn("[MaterialStore] Failed to write to localStorage");
131
+ }
132
+ }
133
+ loadFromStorage() {
134
+ try {
135
+ const raw = localStorage.getItem(STORAGE_KEY);
136
+ if (!raw) return;
137
+ const items = JSON.parse(raw);
138
+ for (const spec of items) this.specs.set(spec.id, spec);
139
+ } catch {
140
+ console.warn("[MaterialStore] Failed to read from localStorage");
141
+ }
142
+ }
143
+ // ── File I/O ─────────────────────────────────────────────────
144
+ /** Download the full collection as a `.json` file (defaults to the configured filePath name). */
145
+ async exportToFile(filename) {
146
+ const defaultName = this.filePath ? this.filePath.split("/").pop() ?? "materials.json" : "materials.json";
147
+ const name = filename ?? defaultName;
148
+ const json = JSON.stringify(this.getAll(), null, 2);
149
+ const blob = new Blob([json], { type: "application/json" });
150
+ if ("showSaveFilePicker" in window) {
151
+ try {
152
+ const handle = await window.showSaveFilePicker({
153
+ suggestedName: name,
154
+ types: [
155
+ { description: "JSON", accept: { "application/json": [".json"] } }
156
+ ]
157
+ });
158
+ const writable = await handle.createWritable();
159
+ await writable.write(blob);
160
+ await writable.close();
161
+ return;
162
+ } catch {
163
+ }
164
+ }
165
+ const url = URL.createObjectURL(blob);
166
+ const a = document.createElement("a");
167
+ a.href = url;
168
+ a.download = name;
169
+ a.click();
170
+ URL.revokeObjectURL(url);
171
+ }
172
+ /**
173
+ * Open a file-picker, parse the JSON, and merge imported specs.
174
+ * Existing specs with the same id are overwritten.
175
+ */
176
+ importFromFile() {
177
+ return new Promise((resolve) => {
178
+ const input = document.createElement("input");
179
+ input.type = "file";
180
+ input.accept = ".json,application/json";
181
+ input.onchange = async () => {
182
+ const file = input.files?.[0];
183
+ if (!file) {
184
+ resolve();
185
+ return;
186
+ }
187
+ try {
188
+ const text = await file.text();
189
+ const items = JSON.parse(text);
190
+ for (const spec of items) this.specs.set(spec.id, spec);
191
+ this.persist();
192
+ this.emit();
193
+ } catch {
194
+ console.error("[MaterialStore] Failed to parse imported file");
195
+ }
196
+ resolve();
197
+ };
198
+ input.click();
199
+ });
200
+ }
201
+ // ── Subscriptions ────────────────────────────────────────────
202
+ subscribe(fn) {
203
+ this.listeners.push(fn);
204
+ return () => {
205
+ this.listeners = this.listeners.filter((l) => l !== fn);
206
+ };
207
+ }
208
+ emit() {
209
+ const all = this.getAll();
210
+ for (const fn of this.listeners) fn(all);
211
+ }
212
+ };
213
+
214
+ // src/Editor/sections/scene/SceneSpec.ts
215
+ function createDefaultSceneItem(name = "New Item", resourceKey = "") {
216
+ return {
217
+ id: crypto.randomUUID(),
218
+ name,
219
+ resourceKey,
220
+ translation: [0, 0, 0],
221
+ rotation: [0, 0, 0],
222
+ scale: [1, 1, 1],
223
+ bounds: null,
224
+ collider: true,
225
+ materialKey: null
226
+ };
227
+ }
228
+ function createDefaultScene() {
229
+ return {
230
+ playerSpawn: [0, 0, 0],
231
+ items: []
232
+ };
233
+ }
234
+
235
+ // src/Editor/sections/scene/SceneStore.ts
236
+ var STORAGE_KEY2 = "genesisgl__scene";
237
+ var SceneStore = class {
238
+ scene = createDefaultScene();
239
+ listeners = [];
240
+ filePath;
241
+ constructor(options = {}) {
242
+ this.filePath = options.filePath ?? null;
243
+ this.loadFromStorage();
244
+ }
245
+ // ── Initialisation ──────────────────────────────────────────
246
+ async init() {
247
+ if (!this.filePath) return;
248
+ try {
249
+ const res = await fetch(this.filePath);
250
+ if (!res.ok) {
251
+ console.warn(
252
+ `[SceneStore] Could not load "${this.filePath}" (${res.status})`
253
+ );
254
+ return;
255
+ }
256
+ const data = await res.json();
257
+ this.scene = data;
258
+ this.persist();
259
+ this.emit();
260
+ } catch {
261
+ console.warn(`[SceneStore] Failed to fetch "${this.filePath}"`);
262
+ }
263
+ }
264
+ // ── Read ─────────────────────────────────────────────────────
265
+ get() {
266
+ return this.scene;
267
+ }
268
+ getItems() {
269
+ return this.scene.items;
270
+ }
271
+ getItem(id) {
272
+ return this.scene.items.find((i) => i.id === id);
273
+ }
274
+ getPlayerSpawn() {
275
+ return [...this.scene.playerSpawn];
276
+ }
277
+ // ── Mutations ────────────────────────────────────────────────
278
+ setPlayerSpawn(x, y, z) {
279
+ this.scene.playerSpawn = [x, y, z];
280
+ this.persist();
281
+ this.emit();
282
+ void this.syncToServer();
283
+ }
284
+ addItem(item) {
285
+ this.scene.items.push(item);
286
+ this.persist();
287
+ this.emit();
288
+ void this.syncToServer();
289
+ }
290
+ updateItem(item) {
291
+ const idx = this.scene.items.findIndex((i) => i.id === item.id);
292
+ if (idx === -1) return;
293
+ this.scene.items[idx] = { ...item };
294
+ this.persist();
295
+ this.emit();
296
+ void this.syncToServer();
297
+ }
298
+ deleteItem(id) {
299
+ this.scene.items = this.scene.items.filter((i) => i.id !== id);
300
+ this.persist();
301
+ this.emit();
302
+ void this.syncToServer();
303
+ }
304
+ /** Replace the entire scene definition. */
305
+ setScene(spec) {
306
+ this.scene = spec;
307
+ this.persist();
308
+ this.emit();
309
+ void this.syncToServer();
310
+ }
311
+ // ── Server sync ──────────────────────────────────────────────
312
+ async syncToServer() {
313
+ if (!this.filePath) return false;
314
+ try {
315
+ const res = await fetch("/api/scene", {
316
+ method: "PUT",
317
+ headers: { "Content-Type": "application/json" },
318
+ body: JSON.stringify(this.scene, null, 2)
319
+ });
320
+ if (!res.ok) {
321
+ console.warn(`[SceneStore] Server sync failed (${res.status})`);
322
+ }
323
+ return res.ok;
324
+ } catch {
325
+ console.warn("[SceneStore] syncToServer: network error");
326
+ return false;
327
+ }
328
+ }
329
+ // ── Persistence ──────────────────────────────────────────────
330
+ persist() {
331
+ try {
332
+ localStorage.setItem(STORAGE_KEY2, JSON.stringify(this.scene));
333
+ } catch {
334
+ console.warn("[SceneStore] Failed to write to localStorage");
335
+ }
336
+ }
337
+ loadFromStorage() {
338
+ try {
339
+ const raw = localStorage.getItem(STORAGE_KEY2);
340
+ if (!raw) return;
341
+ const data = JSON.parse(raw);
342
+ if (data && Array.isArray(data.items)) {
343
+ this.scene = data;
344
+ }
345
+ } catch {
346
+ console.warn("[SceneStore] Failed to read from localStorage");
347
+ }
348
+ }
349
+ // ── File I/O ─────────────────────────────────────────────────
350
+ async exportToFile(filename = "scene.json") {
351
+ const json = JSON.stringify(this.scene, null, 2);
352
+ const blob = new Blob([json], { type: "application/json" });
353
+ if ("showSaveFilePicker" in window) {
354
+ try {
355
+ const handle = await window.showSaveFilePicker({
356
+ suggestedName: filename,
357
+ types: [
358
+ { description: "JSON", accept: { "application/json": [".json"] } }
359
+ ]
360
+ });
361
+ const writable = await handle.createWritable();
362
+ await writable.write(blob);
363
+ await writable.close();
364
+ return;
365
+ } catch {
366
+ }
367
+ }
368
+ const url = URL.createObjectURL(blob);
369
+ const a = document.createElement("a");
370
+ a.href = url;
371
+ a.download = filename;
372
+ a.click();
373
+ URL.revokeObjectURL(url);
374
+ }
375
+ importFromFile() {
376
+ return new Promise((resolve) => {
377
+ const input = document.createElement("input");
378
+ input.type = "file";
379
+ input.accept = ".json,application/json";
380
+ input.onchange = async () => {
381
+ const file = input.files?.[0];
382
+ if (!file) {
383
+ resolve();
384
+ return;
385
+ }
386
+ try {
387
+ const text = await file.text();
388
+ const data = JSON.parse(text);
389
+ if (data && Array.isArray(data.items)) {
390
+ this.scene = data;
391
+ this.persist();
392
+ this.emit();
393
+ }
394
+ } catch {
395
+ console.error("[SceneStore] Failed to parse imported file");
396
+ }
397
+ resolve();
398
+ };
399
+ input.click();
400
+ });
401
+ }
402
+ // ── Subscriptions ────────────────────────────────────────────
403
+ subscribe(fn) {
404
+ this.listeners.push(fn);
405
+ return () => {
406
+ this.listeners = this.listeners.filter((l) => l !== fn);
407
+ };
408
+ }
409
+ emit() {
410
+ const spec = this.scene;
411
+ for (const fn of this.listeners) fn(spec);
412
+ }
413
+ };
414
+
415
+ // src/Editor/sections/scene/SceneSection.ts
416
+ var SceneSection = class _SceneSection {
417
+ constructor(store, options = {}) {
418
+ this.store = store;
419
+ this.modelsFile = options.modelsFile ?? null;
420
+ this.getMaterialNames = options.getMaterialNames ?? (() => []);
421
+ this.renderer = options.renderer ?? null;
422
+ }
423
+ store;
424
+ root = null;
425
+ listEl = null;
426
+ selectedId = null;
427
+ formEl = null;
428
+ unsub;
429
+ modelRegistry = [];
430
+ getMaterialNames;
431
+ modelsFile;
432
+ renderer;
433
+ hitboxCheck = null;
434
+ // ── Factory ──────────────────────────────────────────────────
435
+ static create(store, options) {
436
+ return new _SceneSection(store, options);
437
+ }
438
+ // ── Tab descriptor ───────────────────────────────────────────
439
+ asTab() {
440
+ return {
441
+ id: "scene",
442
+ label: "\u{1F30D} Scene",
443
+ buildContent: (c) => this.buildContent(c),
444
+ onActivate: () => this.onActivate(),
445
+ onDeactivate: () => this.onDeactivate(),
446
+ destroy: () => this.unsub?.()
447
+ };
448
+ }
449
+ // ── Lifecycle ────────────────────────────────────────────────
450
+ onActivate() {
451
+ if (this.modelRegistry.length === 0 && this.modelsFile) {
452
+ void fetch(this.modelsFile).then((r) => r.json()).then((data) => {
453
+ this.modelRegistry = data;
454
+ this.refreshForm();
455
+ }).catch(
456
+ () => console.warn("[SceneSection] Could not load models registry")
457
+ );
458
+ }
459
+ this.refreshList();
460
+ this.refreshForm();
461
+ }
462
+ onDeactivate() {
463
+ }
464
+ // ── Build ────────────────────────────────────────────────────
465
+ buildContent(container) {
466
+ this.root = container;
467
+ const toolbar = mk("div", TOOLBAR_STYLE);
468
+ toolbar.appendChild(
469
+ this.mkBtn("\uFF0B Item", () => this.onAdd(), "Add scene item")
470
+ );
471
+ toolbar.appendChild(
472
+ this.mkBtn("\u2751", () => this.onDuplicate(), "Duplicate selected")
473
+ );
474
+ toolbar.appendChild(
475
+ this.mkBtn("\u{1F5D1}", () => this.onDelete(), "Delete selected")
476
+ );
477
+ const reloadBtn = mk(
478
+ "button",
479
+ BTN_STYLE + ";margin-left:auto;color:#6cf;border-color:#446;"
480
+ );
481
+ reloadBtn.textContent = "\u{1F504} Reload Game";
482
+ reloadBtn.title = "Sync JSON then reload game to apply changes";
483
+ reloadBtn.onclick = () => location.reload();
484
+ toolbar.appendChild(reloadBtn);
485
+ if (this.renderer) {
486
+ const hitboxLabel = document.createElement("label");
487
+ hitboxLabel.style.cssText = "display:flex;align-items:center;gap:4px;cursor:pointer;color:#ccc;font-size:10px;margin-left:6px;";
488
+ this.hitboxCheck = document.createElement("input");
489
+ this.hitboxCheck.type = "checkbox";
490
+ this.hitboxCheck.checked = this.renderer.debug;
491
+ this.hitboxCheck.onchange = () => {
492
+ if (this.renderer) {
493
+ this.renderer.debug = this.hitboxCheck.checked;
494
+ }
495
+ };
496
+ hitboxLabel.appendChild(this.hitboxCheck);
497
+ const hitboxText = document.createElement("span");
498
+ hitboxText.textContent = "Hitboxes";
499
+ hitboxLabel.appendChild(hitboxText);
500
+ toolbar.appendChild(hitboxLabel);
501
+ }
502
+ const body = mk("div", "display:flex;flex:1;overflow:hidden;");
503
+ this.listEl = mk("div", LIST_STYLE);
504
+ this.formEl = mk("div", FORM_STYLE);
505
+ body.appendChild(this.listEl);
506
+ body.appendChild(this.formEl);
507
+ container.appendChild(toolbar);
508
+ container.appendChild(body);
509
+ this.unsub = this.store.subscribe(() => {
510
+ this.refreshList();
511
+ this.refreshForm();
512
+ });
513
+ this.refreshList();
514
+ }
515
+ // ── List ─────────────────────────────────────────────────────
516
+ refreshList() {
517
+ if (!this.listEl) return;
518
+ this.listEl.innerHTML = "";
519
+ const spawn = this.store.getPlayerSpawn();
520
+ const spawnCard = mk("div", CARD_STYLE);
521
+ spawnCard.style.borderLeft = "3px solid #e8a";
522
+ const spawnLabel = mk(
523
+ "div",
524
+ "font-weight:bold;color:#e8a;margin-bottom:2px;font-size:11px;"
525
+ );
526
+ spawnLabel.textContent = "\u{1F9D1} Player Spawn";
527
+ spawnCard.appendChild(spawnLabel);
528
+ spawnCard.appendChild(
529
+ dataRow("pos", `${f(spawn[0])} ${f(spawn[1])} ${f(spawn[2])}`)
530
+ );
531
+ spawnCard.style.cursor = "pointer";
532
+ spawnCard.onclick = () => {
533
+ this.selectedId = "__player_spawn__";
534
+ this.refreshList();
535
+ this.refreshForm();
536
+ };
537
+ if (this.selectedId === "__player_spawn__") {
538
+ spawnCard.style.background = "#2a2d3a";
539
+ }
540
+ this.listEl.appendChild(spawnCard);
541
+ const sep = mk("div", "height:1px;background:#333;margin:6px 0;");
542
+ this.listEl.appendChild(sep);
543
+ const heading = mk(
544
+ "div",
545
+ "color:#666;font-size:10px;letter-spacing:1px;padding:0 0 4px;"
546
+ );
547
+ const items = this.store.getItems();
548
+ heading.textContent = `SCENE ITEMS (${items.length})`;
549
+ this.listEl.appendChild(heading);
550
+ for (const item of items) {
551
+ const active = item.id === this.selectedId;
552
+ const card = mk("div", CARD_STYLE);
553
+ if (active) card.style.background = "#2a2d3a";
554
+ card.style.cursor = "pointer";
555
+ card.onclick = () => {
556
+ this.selectedId = item.id;
557
+ if (this.renderer) {
558
+ this.renderer.debug = true;
559
+ if (this.hitboxCheck) this.hitboxCheck.checked = true;
560
+ }
561
+ this.refreshList();
562
+ this.refreshForm();
563
+ };
564
+ const nameEl = mk(
565
+ "div",
566
+ "font-weight:bold;color:#9cdcfe;margin-bottom:2px;font-size:11px;"
567
+ );
568
+ nameEl.textContent = `${item.name} [${item.resourceKey}]`;
569
+ card.appendChild(nameEl);
570
+ card.appendChild(
571
+ dataRow(
572
+ "pos",
573
+ `${f(item.translation[0])} ${f(item.translation[1])} ${f(item.translation[2])}`
574
+ )
575
+ );
576
+ card.appendChild(
577
+ dataRow(
578
+ "scl",
579
+ `${f(item.scale[0])} ${f(item.scale[1])} ${f(item.scale[2])}`
580
+ )
581
+ );
582
+ this.listEl.appendChild(card);
583
+ }
584
+ }
585
+ // ── Form ─────────────────────────────────────────────────────
586
+ refreshForm() {
587
+ if (!this.formEl) return;
588
+ this.formEl.innerHTML = "";
589
+ if (this.selectedId === "__player_spawn__") {
590
+ this.buildSpawnForm(this.formEl);
591
+ return;
592
+ }
593
+ const item = this.selectedId ? this.store.getItem(this.selectedId) : void 0;
594
+ if (!item) {
595
+ const hint = mk("div", "color:#666;padding:8px;font-size:11px;");
596
+ hint.textContent = "Select an item to edit";
597
+ this.formEl.appendChild(hint);
598
+ return;
599
+ }
600
+ this.buildItemForm(this.formEl, item);
601
+ }
602
+ buildSpawnForm(container) {
603
+ const spawn = this.store.getPlayerSpawn();
604
+ const title = mk(
605
+ "div",
606
+ "font-weight:bold;color:#e8a;padding:0 0 6px;font-size:12px;"
607
+ );
608
+ title.textContent = "\u{1F9D1} Player Spawn";
609
+ container.appendChild(title);
610
+ container.appendChild(
611
+ this.vec3Row("Position", spawn, (v) => {
612
+ this.store.setPlayerSpawn(v[0], v[1], v[2]);
613
+ })
614
+ );
615
+ }
616
+ buildItemForm(container, item) {
617
+ const title = mk(
618
+ "div",
619
+ "font-weight:bold;color:#9cdcfe;padding:0 0 6px;font-size:12px;"
620
+ );
621
+ title.textContent = item.name;
622
+ container.appendChild(title);
623
+ container.appendChild(
624
+ this.textRow("Name", item.name, (v) => {
625
+ this.store.updateItem({ ...item, name: v });
626
+ })
627
+ );
628
+ const modelOptions = this.modelRegistry.map((m) => ({
629
+ value: m.key,
630
+ label: `${m.name} (${m.type})`
631
+ }));
632
+ container.appendChild(
633
+ this.selectRow("Resource", item.resourceKey, modelOptions, (v) => {
634
+ this.store.updateItem({ ...item, resourceKey: v });
635
+ })
636
+ );
637
+ const matNames = this.getMaterialNames();
638
+ const matOptions = [
639
+ { value: "", label: "(default)" },
640
+ ...matNames.map((n) => ({ value: n, label: n }))
641
+ ];
642
+ container.appendChild(
643
+ this.selectRow("Material", item.materialKey ?? "", matOptions, (v) => {
644
+ this.store.updateItem({ ...item, materialKey: v || null });
645
+ })
646
+ );
647
+ container.appendChild(
648
+ this.vec3Row(
649
+ "Position",
650
+ [...item.translation],
651
+ (v) => {
652
+ this.store.updateItem({ ...item, translation: v });
653
+ }
654
+ )
655
+ );
656
+ container.appendChild(
657
+ this.vec3Row(
658
+ "Rotation",
659
+ [...item.rotation],
660
+ (v) => {
661
+ this.store.updateItem({ ...item, rotation: v });
662
+ }
663
+ )
664
+ );
665
+ container.appendChild(
666
+ this.vec3Row(
667
+ "Scale",
668
+ [...item.scale],
669
+ (v) => {
670
+ this.store.updateItem({ ...item, scale: v });
671
+ }
672
+ )
673
+ );
674
+ const hasBounds = item.bounds != null;
675
+ container.appendChild(
676
+ this.checkRow("Custom Bounds", hasBounds, (on) => {
677
+ if (on) {
678
+ this.store.updateItem({
679
+ ...item,
680
+ bounds: [0, 0, 0, 1, 1, 1]
681
+ });
682
+ } else {
683
+ this.store.updateItem({ ...item, bounds: null });
684
+ }
685
+ })
686
+ );
687
+ if (item.bounds) {
688
+ const b = item.bounds;
689
+ container.appendChild(
690
+ this.vec3Row("Offset", [b[0], b[1], b[2]], (v) => {
691
+ this.store.updateItem({
692
+ ...item,
693
+ bounds: [v[0], v[1], v[2], b[3], b[4], b[5]]
694
+ });
695
+ })
696
+ );
697
+ container.appendChild(
698
+ this.vec3Row("Size", [b[3], b[4], b[5]], (v) => {
699
+ this.store.updateItem({
700
+ ...item,
701
+ bounds: [b[0], b[1], b[2], v[0], v[1], v[2]]
702
+ });
703
+ })
704
+ );
705
+ }
706
+ container.appendChild(
707
+ this.checkRow("Collider", item.collider !== false, (on) => {
708
+ this.store.updateItem({ ...item, collider: on });
709
+ })
710
+ );
711
+ }
712
+ // ── CRUD actions ─────────────────────────────────────────────
713
+ onAdd() {
714
+ const item = createDefaultSceneItem();
715
+ this.store.addItem(item);
716
+ this.selectedId = item.id;
717
+ }
718
+ onDuplicate() {
719
+ if (!this.selectedId || this.selectedId === "__player_spawn__") return;
720
+ const src = this.store.getItem(this.selectedId);
721
+ if (!src) return;
722
+ const copy = {
723
+ ...src,
724
+ id: crypto.randomUUID(),
725
+ name: `${src.name} (copy)`,
726
+ translation: [...src.translation],
727
+ rotation: [...src.rotation],
728
+ scale: [...src.scale],
729
+ bounds: src.bounds ? [...src.bounds] : null
730
+ };
731
+ this.store.addItem(copy);
732
+ this.selectedId = copy.id;
733
+ }
734
+ onDelete() {
735
+ if (!this.selectedId || this.selectedId === "__player_spawn__") return;
736
+ this.store.deleteItem(this.selectedId);
737
+ this.selectedId = null;
738
+ }
739
+ // ── Input helpers ────────────────────────────────────────────
740
+ vec3Row(label, values, onChange) {
741
+ const row = mk("div", ROW_STYLE);
742
+ const lbl = mk("div", LABEL_STYLE);
743
+ lbl.textContent = label;
744
+ row.appendChild(lbl);
745
+ const group = mk("div", "display:flex;gap:4px;");
746
+ const labels = ["X", "Y", "Z"];
747
+ for (let i = 0; i < 3; i++) {
748
+ const wrap = mk("div", "display:flex;flex-direction:column;flex:1;");
749
+ const axisLbl = mk("div", "color:#666;font-size:9px;text-align:center;");
750
+ axisLbl.textContent = labels[i];
751
+ const input = document.createElement("input");
752
+ input.type = "number";
753
+ input.step = "0.1";
754
+ input.value = String(values[i]);
755
+ input.style.cssText = INPUT_STYLE;
756
+ input.onchange = () => {
757
+ const v = [...values];
758
+ v[i] = parseFloat(input.value) || 0;
759
+ onChange(v);
760
+ };
761
+ wrap.appendChild(axisLbl);
762
+ wrap.appendChild(input);
763
+ group.appendChild(wrap);
764
+ }
765
+ row.appendChild(group);
766
+ return row;
767
+ }
768
+ textRow(label, value, onChange) {
769
+ const row = mk("div", ROW_STYLE);
770
+ const lbl = mk("div", LABEL_STYLE);
771
+ lbl.textContent = label;
772
+ row.appendChild(lbl);
773
+ const input = document.createElement("input");
774
+ input.type = "text";
775
+ input.value = value;
776
+ input.style.cssText = INPUT_STYLE + ";flex:1;";
777
+ input.onchange = () => onChange(input.value);
778
+ row.appendChild(input);
779
+ return row;
780
+ }
781
+ selectRow(label, value, options, onChange) {
782
+ const row = mk("div", ROW_STYLE);
783
+ const lbl = mk("div", LABEL_STYLE);
784
+ lbl.textContent = label;
785
+ row.appendChild(lbl);
786
+ const select = document.createElement("select");
787
+ select.style.cssText = INPUT_STYLE + ";flex:1;";
788
+ for (const opt of options) {
789
+ const o = document.createElement("option");
790
+ o.value = opt.value;
791
+ o.textContent = opt.label;
792
+ if (opt.value === value) o.selected = true;
793
+ select.appendChild(o);
794
+ }
795
+ select.onchange = () => onChange(select.value);
796
+ row.appendChild(select);
797
+ return row;
798
+ }
799
+ checkRow(label, checked, onChange) {
800
+ const row = mk("div", ROW_STYLE + ";align-items:center;");
801
+ const lbl = mk("div", LABEL_STYLE);
802
+ lbl.textContent = label;
803
+ row.appendChild(lbl);
804
+ const input = document.createElement("input");
805
+ input.type = "checkbox";
806
+ input.checked = checked;
807
+ input.onchange = () => onChange(input.checked);
808
+ row.appendChild(input);
809
+ return row;
810
+ }
811
+ mkBtn(text, onClick, title) {
812
+ const btn = document.createElement("button");
813
+ btn.textContent = text;
814
+ btn.title = title;
815
+ btn.style.cssText = BTN_STYLE;
816
+ btn.onclick = onClick;
817
+ return btn;
818
+ }
819
+ };
820
+ function mk(tag, style = "") {
821
+ const el2 = document.createElement(tag);
822
+ if (style) el2.style.cssText = style;
823
+ return el2;
824
+ }
825
+ function f(n) {
826
+ return n.toFixed(2);
827
+ }
828
+ function dataRow(label, value) {
829
+ const row = document.createElement("div");
830
+ row.style.cssText = "display:flex;gap:4px;line-height:1.6;";
831
+ const lbl = document.createElement("span");
832
+ lbl.style.cssText = "color:#666;width:28px;flex-shrink:0;font-size:10px;";
833
+ lbl.textContent = label;
834
+ const val = document.createElement("span");
835
+ val.style.cssText = "color:#ccc;font-size:10px;";
836
+ val.textContent = value;
837
+ row.appendChild(lbl);
838
+ row.appendChild(val);
839
+ return row;
840
+ }
841
+ var TOOLBAR_STYLE = [
842
+ "display:flex",
843
+ "gap:6px",
844
+ "padding:6px 10px",
845
+ "background:#222",
846
+ "border-bottom:1px solid #333",
847
+ "flex-shrink:0"
848
+ ].join(";");
849
+ var BTN_STYLE = [
850
+ "background:#2a2a2a",
851
+ "color:#ccc",
852
+ "border:1px solid #555",
853
+ "border-radius:4px",
854
+ "padding:3px 10px",
855
+ "cursor:pointer",
856
+ "font-size:11px",
857
+ "font-family:monospace"
858
+ ].join(";");
859
+ var LIST_STYLE = [
860
+ "width:220px",
861
+ "overflow-y:auto",
862
+ "padding:8px",
863
+ "border-right:1px solid #333",
864
+ "flex-shrink:0"
865
+ ].join(";");
866
+ var FORM_STYLE = ["flex:1", "overflow-y:auto", "padding:10px"].join(";");
867
+ var CARD_STYLE = [
868
+ "background:#1e1e1e",
869
+ "border:1px solid #333",
870
+ "border-radius:4px",
871
+ "padding:6px 8px",
872
+ "margin-bottom:4px",
873
+ "font-family:monospace",
874
+ "font-size:10px"
875
+ ].join(";");
876
+ var ROW_STYLE = ["display:flex", "gap:6px", "margin-bottom:8px"].join(";");
877
+ var LABEL_STYLE = [
878
+ "color:#888",
879
+ "font-size:11px",
880
+ "width:70px",
881
+ "flex-shrink:0",
882
+ "padding-top:2px"
883
+ ].join(";");
884
+ var INPUT_STYLE = [
885
+ "background:#1a1a1a",
886
+ "color:#ccc",
887
+ "border:1px solid #444",
888
+ "border-radius:3px",
889
+ "padding:3px 5px",
890
+ "font-size:11px",
891
+ "font-family:monospace",
892
+ "width:60px",
893
+ "outline:none"
894
+ ].join(";");
895
+
896
+ // src/Editor/EditorSidebar.ts
897
+ var EditorSidebar = class _EditorSidebar {
898
+ constructor(store) {
899
+ this.store = store;
900
+ }
901
+ store;
902
+ root;
903
+ tabBar;
904
+ contentArea;
905
+ tabs = [];
906
+ activeTabId = null;
907
+ // material panel state
908
+ listEl;
909
+ formEl;
910
+ selectedId = null;
911
+ unsubMaterials;
912
+ // drag-resize state
913
+ _sidebarWidth = 520;
914
+ _dragging = false;
915
+ // layout mode: 'absolute' overlays, 'shared' shrinks viewport
916
+ _layoutMode = "absolute";
917
+ _layoutBtn = null;
918
+ // ── Public API ──────────────────────────────────────────────
919
+ static mount(store) {
920
+ const s = new _EditorSidebar(store);
921
+ s.build();
922
+ return s;
923
+ }
924
+ show() {
925
+ this.root.style.display = "flex";
926
+ this.applyLayout();
927
+ if (this.activeTabId) {
928
+ this.tabs.find((t) => t.id === this.activeTabId)?.onActivate?.();
929
+ }
930
+ }
931
+ hide() {
932
+ if (this.activeTabId) {
933
+ this.tabs.find((t) => t.id === this.activeTabId)?.onDeactivate?.();
934
+ }
935
+ this.root.style.display = "none";
936
+ this.applyLayout();
937
+ }
938
+ toggle() {
939
+ if (this.root.style.display === "none") this.show();
940
+ else this.hide();
941
+ }
942
+ get isVisible() {
943
+ return this.root.style.display !== "none";
944
+ }
945
+ /** Register an extra tab. */
946
+ addTab(tab) {
947
+ this.tabs.push(tab);
948
+ if (this.tabBar) this.renderTabBar();
949
+ }
950
+ unmount() {
951
+ this.unsubMaterials?.();
952
+ for (const t of this.tabs) t.destroy?.();
953
+ this.root?.remove();
954
+ }
955
+ // ── Build ────────────────────────────────────────────────────
956
+ build() {
957
+ this.root = el("div", { id: "gl-editor-sidebar" }, SIDEBAR_STYLE);
958
+ this.root.style.display = "none";
959
+ this.root.style.width = `${this._sidebarWidth}px`;
960
+ const handle = el("div", {}, DRAG_HANDLE_STYLE);
961
+ handle.addEventListener("mousedown", (e) => {
962
+ e.preventDefault();
963
+ this._dragging = true;
964
+ document.body.style.cursor = "col-resize";
965
+ document.body.style.userSelect = "none";
966
+ });
967
+ document.addEventListener("mousemove", (e) => {
968
+ if (!this._dragging) return;
969
+ const newWidth = window.innerWidth - e.clientX;
970
+ this._sidebarWidth = Math.max(280, Math.min(newWidth, 900));
971
+ this.root.style.width = `${this._sidebarWidth}px`;
972
+ this.applyLayout();
973
+ });
974
+ document.addEventListener("mouseup", () => {
975
+ if (!this._dragging) return;
976
+ this._dragging = false;
977
+ document.body.style.cursor = "";
978
+ document.body.style.userSelect = "";
979
+ });
980
+ this.root.appendChild(handle);
981
+ const header = el("div", {}, HEADER_STYLE);
982
+ const title = el(
983
+ "span",
984
+ {},
985
+ "font-weight:bold;color:#9cdcfe;letter-spacing:1px;"
986
+ );
987
+ title.textContent = "\u{1F6E0} Editor";
988
+ header.appendChild(title);
989
+ this._layoutBtn = el(
990
+ "button",
991
+ {},
992
+ ICON_BTN_STYLE + ";margin-left:auto;font-size:14px;"
993
+ );
994
+ this._layoutBtn.title = "Toggle: overlay / share viewport";
995
+ this._layoutBtn.textContent = "\u21D4";
996
+ this._layoutBtn.onclick = () => this.toggleLayout();
997
+ header.appendChild(this._layoutBtn);
998
+ const closeBtn = el("button", {}, ICON_BTN_STYLE);
999
+ closeBtn.textContent = "\u2715";
1000
+ closeBtn.title = "Close sidebar";
1001
+ closeBtn.onclick = () => this.hide();
1002
+ header.appendChild(closeBtn);
1003
+ this.tabBar = el("div", {}, TAB_BAR_STYLE);
1004
+ this.contentArea = el(
1005
+ "div",
1006
+ {},
1007
+ "flex:1;overflow:hidden;display:flex;flex-direction:column;"
1008
+ );
1009
+ this.root.appendChild(header);
1010
+ this.root.appendChild(this.tabBar);
1011
+ this.root.appendChild(this.contentArea);
1012
+ document.body.appendChild(this.root);
1013
+ this.addTab({
1014
+ id: "materials",
1015
+ label: "\u{1F3A8} Materials",
1016
+ buildContent: (c) => this.buildMaterialsContent(c),
1017
+ destroy: () => this.unsubMaterials?.()
1018
+ });
1019
+ this.activateTab("materials");
1020
+ }
1021
+ // ── Layout mode ──────────────────────────────────────────────
1022
+ toggleLayout() {
1023
+ this._layoutMode = this._layoutMode === "absolute" ? "shared" : "absolute";
1024
+ this.applyLayout();
1025
+ }
1026
+ applyLayout() {
1027
+ const visible = this.root.style.display !== "none";
1028
+ if (this._layoutMode === "shared" && visible) {
1029
+ document.body.style.marginRight = `${this._sidebarWidth}px`;
1030
+ } else {
1031
+ document.body.style.marginRight = "0";
1032
+ }
1033
+ if (this._layoutBtn) {
1034
+ this._layoutBtn.textContent = this._layoutMode === "absolute" ? "\u21D4" : "\u21E4";
1035
+ this._layoutBtn.title = this._layoutMode === "absolute" ? "Switch to shared layout (shrink viewport)" : "Switch to overlay layout";
1036
+ }
1037
+ }
1038
+ // ── Tab system ───────────────────────────────────────────────
1039
+ renderTabBar() {
1040
+ this.tabBar.innerHTML = "";
1041
+ for (const tab of this.tabs) {
1042
+ const btn = el(
1043
+ "button",
1044
+ {},
1045
+ TAB_BTN_STYLE + (tab.id === this.activeTabId ? TAB_BTN_ACTIVE : "")
1046
+ );
1047
+ btn.textContent = tab.label;
1048
+ btn.onclick = () => this.activateTab(tab.id);
1049
+ this.tabBar.appendChild(btn);
1050
+ }
1051
+ }
1052
+ /** Activate a tab by id. Safe to call from outside (e.g. TopBar button). */
1053
+ activateTab(id) {
1054
+ if (this.activeTabId && this.activeTabId !== id) {
1055
+ this.tabs.find((t) => t.id === this.activeTabId)?.onDeactivate?.();
1056
+ }
1057
+ this.activeTabId = id;
1058
+ this.renderTabBar();
1059
+ this.contentArea.innerHTML = "";
1060
+ const tab = this.tabs.find((t) => t.id === id);
1061
+ if (!tab) return;
1062
+ const c = el(
1063
+ "div",
1064
+ {},
1065
+ "flex:1;overflow:hidden;display:flex;flex-direction:column;"
1066
+ );
1067
+ tab.buildContent(c);
1068
+ this.contentArea.appendChild(c);
1069
+ tab.onActivate?.();
1070
+ }
1071
+ // ── Materials tab content ────────────────────────────────────
1072
+ buildMaterialsContent(container) {
1073
+ const toolbar = el("div", {}, TOOLBAR_STYLE2);
1074
+ toolbar.appendChild(this.mkBtn("\uFF0B", () => this.onNew(), "New material"));
1075
+ toolbar.appendChild(this.mkBtn("\u2751", () => this.onDuplicate(), "Duplicate"));
1076
+ toolbar.appendChild(this.mkBtn("\u{1F5D1}", () => this.onDelete(), "Delete"));
1077
+ const reloadBtn = el(
1078
+ "button",
1079
+ {},
1080
+ BTN_STYLE2 + ";margin-left:auto;color:#6cf;border-color:#446;"
1081
+ );
1082
+ reloadBtn.textContent = "\u{1F504} Reload Game";
1083
+ reloadBtn.title = "Sync JSON then reload game to apply changes";
1084
+ reloadBtn.onclick = () => location.reload();
1085
+ toolbar.appendChild(reloadBtn);
1086
+ const body = el("div", {}, "display:flex;flex:1;overflow:hidden;");
1087
+ this.listEl = el("div", {}, LIST_STYLE2);
1088
+ this.formEl = el("div", {}, FORM_STYLE2);
1089
+ body.appendChild(this.listEl);
1090
+ body.appendChild(this.formEl);
1091
+ container.appendChild(toolbar);
1092
+ container.appendChild(body);
1093
+ this.unsubMaterials = this.store.subscribe(() => this.refreshList());
1094
+ this.refreshList();
1095
+ }
1096
+ // ── List ─────────────────────────────────────────────────────
1097
+ refreshList() {
1098
+ this.listEl.innerHTML = "";
1099
+ for (const spec of this.store.getAll()) {
1100
+ const active = spec.id === this.selectedId;
1101
+ const item = el(
1102
+ "div",
1103
+ {},
1104
+ LIST_ITEM_STYLE + (active ? LIST_ITEM_ACTIVE : "")
1105
+ );
1106
+ const swatch = el("div", {}, swatchStyle(spec));
1107
+ item.appendChild(swatch);
1108
+ const nameEl = el(
1109
+ "span",
1110
+ {},
1111
+ "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
1112
+ );
1113
+ nameEl.textContent = spec.name;
1114
+ item.appendChild(nameEl);
1115
+ item.onclick = () => this.select(spec.id);
1116
+ this.listEl.appendChild(item);
1117
+ }
1118
+ if (this.selectedId && !this.store.get(this.selectedId)) {
1119
+ this.selectedId = null;
1120
+ this.formEl.innerHTML = EMPTY_FORM_HTML;
1121
+ }
1122
+ }
1123
+ select(id) {
1124
+ this.selectedId = id;
1125
+ this.refreshList();
1126
+ this.buildForm();
1127
+ }
1128
+ // ── Form ─────────────────────────────────────────────────────
1129
+ buildForm() {
1130
+ const spec = this.selectedId ? this.store.get(this.selectedId) : void 0;
1131
+ this.formEl.innerHTML = "";
1132
+ if (!spec) return;
1133
+ const draft = {
1134
+ ...spec,
1135
+ albedoColor: [...spec.albedoColor],
1136
+ diffuse: [...spec.diffuse],
1137
+ ambientColor: [...spec.ambientColor],
1138
+ specular: [...spec.specular]
1139
+ };
1140
+ const save = () => {
1141
+ this.store.update(draft);
1142
+ this.select(draft.id);
1143
+ };
1144
+ formRow(this.formEl, "Name", () => {
1145
+ const inp = el("input", { type: "text", value: draft.name }, INPUT_STYLE2);
1146
+ inp.oninput = () => {
1147
+ draft.name = inp.value;
1148
+ };
1149
+ inp.onblur = save;
1150
+ return inp;
1151
+ });
1152
+ formRow(this.formEl, "Albedo (RGBA)", () => {
1153
+ const wrap = el("div", {}, "display:flex;gap:4px;align-items:center;");
1154
+ const hex = rgbToHex(
1155
+ draft.albedoColor[0],
1156
+ draft.albedoColor[1],
1157
+ draft.albedoColor[2]
1158
+ );
1159
+ const picker = el(
1160
+ "input",
1161
+ { type: "color", value: hex },
1162
+ "cursor:pointer;"
1163
+ );
1164
+ const alphaIn = el(
1165
+ "input",
1166
+ {
1167
+ type: "range",
1168
+ min: "0",
1169
+ max: "1",
1170
+ step: "0.01",
1171
+ value: String(draft.albedoColor[3])
1172
+ },
1173
+ "flex:1;"
1174
+ );
1175
+ const alphaLbl = el(
1176
+ "span",
1177
+ {},
1178
+ "min-width:30px;color:#ccc;font-size:11px;"
1179
+ );
1180
+ alphaLbl.textContent = draft.albedoColor[3].toFixed(2);
1181
+ const update = () => {
1182
+ const [r, g, b] = hexToRgb(picker.value);
1183
+ draft.albedoColor = [r, g, b, parseFloat(alphaIn.value)];
1184
+ alphaLbl.textContent = parseFloat(alphaIn.value).toFixed(2);
1185
+ save();
1186
+ };
1187
+ picker.oninput = update;
1188
+ alphaIn.oninput = update;
1189
+ wrap.appendChild(picker);
1190
+ wrap.appendChild(alphaIn);
1191
+ wrap.appendChild(alphaLbl);
1192
+ return wrap;
1193
+ });
1194
+ for (const key of ["diffuse", "ambientColor", "specular"]) {
1195
+ const label = {
1196
+ diffuse: "Diffuse",
1197
+ ambientColor: "Ambient",
1198
+ specular: "Specular"
1199
+ }[key];
1200
+ formRow(this.formEl, label, () => {
1201
+ const hex = rgbToHex(draft[key][0], draft[key][1], draft[key][2]);
1202
+ const picker = el(
1203
+ "input",
1204
+ { type: "color", value: hex },
1205
+ "cursor:pointer;"
1206
+ );
1207
+ picker.oninput = () => {
1208
+ [draft[key][0], draft[key][1], draft[key][2]] = hexToRgb(
1209
+ picker.value
1210
+ );
1211
+ save();
1212
+ };
1213
+ return picker;
1214
+ });
1215
+ }
1216
+ numericRow(this.formEl, "Shininess", draft.shininess, 2, 256, 1, (v) => {
1217
+ draft.shininess = v;
1218
+ save();
1219
+ });
1220
+ numericRow(this.formEl, "Dissolve", draft.dissolve, 0, 1, 0.01, (v) => {
1221
+ draft.dissolve = v;
1222
+ save();
1223
+ });
1224
+ numericRow(
1225
+ this.formEl,
1226
+ "Friction",
1227
+ draft.friction ?? 0.75,
1228
+ 0,
1229
+ 1,
1230
+ 0.01,
1231
+ (v) => {
1232
+ draft.friction = v;
1233
+ save();
1234
+ }
1235
+ );
1236
+ checkRow(this.formEl, "Unlit", draft.unlit, (v) => {
1237
+ draft.unlit = v;
1238
+ save();
1239
+ });
1240
+ checkRow(this.formEl, "Double Sided", draft.doubleSided, (v) => {
1241
+ draft.doubleSided = v;
1242
+ save();
1243
+ });
1244
+ formRow(this.formEl, "Texture Path", () => {
1245
+ const inp = el(
1246
+ "input",
1247
+ {
1248
+ type: "text",
1249
+ value: draft.texturePath ?? "",
1250
+ placeholder: "textures/my-tex.png"
1251
+ },
1252
+ INPUT_STYLE2
1253
+ );
1254
+ inp.oninput = () => {
1255
+ draft.texturePath = inp.value || void 0;
1256
+ };
1257
+ inp.onblur = save;
1258
+ return inp;
1259
+ });
1260
+ }
1261
+ // ── Actions ──────────────────────────────────────────────────
1262
+ onNew() {
1263
+ const spec = this.store.create();
1264
+ this.select(spec.id);
1265
+ }
1266
+ onDuplicate() {
1267
+ if (!this.selectedId) return;
1268
+ const copy = this.store.duplicate(this.selectedId);
1269
+ if (copy) this.select(copy.id);
1270
+ }
1271
+ onDelete() {
1272
+ if (!this.selectedId) return;
1273
+ if (!confirm("Delete this material?")) return;
1274
+ this.store.delete(this.selectedId);
1275
+ this.selectedId = null;
1276
+ this.formEl.innerHTML = EMPTY_FORM_HTML;
1277
+ }
1278
+ mkBtn(label, onClick, title = "") {
1279
+ const b = el("button", {}, BTN_STYLE2);
1280
+ b.textContent = label;
1281
+ b.title = title;
1282
+ b.onclick = onClick;
1283
+ return b;
1284
+ }
1285
+ };
1286
+ function el(tag, attrs = {}, style = "") {
1287
+ const e = document.createElement(tag);
1288
+ for (const [k, v] of Object.entries(attrs))
1289
+ e[k] = v;
1290
+ if (style) e.style.cssText = style;
1291
+ return e;
1292
+ }
1293
+ function formRow(parent, label, buildInput) {
1294
+ const row = el(
1295
+ "div",
1296
+ {},
1297
+ "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:8px;"
1298
+ );
1299
+ const lbl = el(
1300
+ "label",
1301
+ {},
1302
+ "color:#bbb;font-size:12px;white-space:nowrap;min-width:90px;"
1303
+ );
1304
+ lbl.textContent = label;
1305
+ row.appendChild(lbl);
1306
+ const wrap = el("div", {}, "flex:1;");
1307
+ wrap.appendChild(buildInput());
1308
+ row.appendChild(wrap);
1309
+ parent.appendChild(row);
1310
+ }
1311
+ function numericRow(parent, label, value, min, max, step, onChange) {
1312
+ formRow(parent, label, () => {
1313
+ const wrap = el("div", {}, "display:flex;gap:4px;align-items:center;");
1314
+ const slider = el(
1315
+ "input",
1316
+ {
1317
+ type: "range",
1318
+ min: String(min),
1319
+ max: String(max),
1320
+ step: String(step),
1321
+ value: String(value)
1322
+ },
1323
+ "flex:1;"
1324
+ );
1325
+ const lbl = el(
1326
+ "span",
1327
+ {},
1328
+ "min-width:36px;color:#ccc;font-size:11px;text-align:right;"
1329
+ );
1330
+ lbl.textContent = value.toString();
1331
+ slider.oninput = () => {
1332
+ lbl.textContent = slider.value;
1333
+ onChange(parseFloat(slider.value));
1334
+ };
1335
+ wrap.appendChild(slider);
1336
+ wrap.appendChild(lbl);
1337
+ return wrap;
1338
+ });
1339
+ }
1340
+ function checkRow(parent, label, value, onChange) {
1341
+ formRow(parent, label, () => {
1342
+ const cb = el("input", { type: "checkbox" }, "");
1343
+ cb.checked = value;
1344
+ cb.onchange = () => onChange(cb.checked);
1345
+ return cb;
1346
+ });
1347
+ }
1348
+ function rgbToHex(r, g, b) {
1349
+ const byte = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0");
1350
+ return `#${byte(r)}${byte(g)}${byte(b)}`;
1351
+ }
1352
+ function hexToRgb(hex) {
1353
+ const n = parseInt(hex.slice(1), 16);
1354
+ return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
1355
+ }
1356
+ function swatchStyle(spec) {
1357
+ const [r, g, b, a] = spec.albedoColor;
1358
+ return [
1359
+ `background:rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`,
1360
+ "width:18px",
1361
+ "min-width:18px",
1362
+ "height:18px",
1363
+ "border-radius:3px",
1364
+ "border:1px solid #666",
1365
+ "flex-shrink:0"
1366
+ ].join(";");
1367
+ }
1368
+ var EMPTY_FORM_HTML = '<p style="color:#888;padding:16px 10px;font-size:12px;">Select or create a material.</p>';
1369
+ var SIDEBAR_STYLE = [
1370
+ "position:fixed",
1371
+ "top:32px",
1372
+ "right:0",
1373
+ "height:calc(100vh - 32px)",
1374
+ "background:#1e1e1e",
1375
+ "border-left:1px solid #444",
1376
+ "display:flex",
1377
+ "flex-direction:column",
1378
+ "color:#ddd",
1379
+ "font-family:monospace",
1380
+ "font-size:12px",
1381
+ "z-index:9998",
1382
+ "box-shadow:-4px 0 20px rgba(0,0,0,.6)",
1383
+ "overflow:hidden"
1384
+ ].join(";");
1385
+ var DRAG_HANDLE_STYLE = [
1386
+ "position:absolute",
1387
+ "top:0",
1388
+ "left:0",
1389
+ "width:5px",
1390
+ "height:100%",
1391
+ "cursor:col-resize",
1392
+ "z-index:1",
1393
+ "background:transparent"
1394
+ ].join(";");
1395
+ var HEADER_STYLE = [
1396
+ "display:flex",
1397
+ "align-items:center",
1398
+ "gap:8px",
1399
+ "padding:0 12px",
1400
+ "height:36px",
1401
+ "background:#252525",
1402
+ "border-bottom:1px solid #444",
1403
+ "flex-shrink:0"
1404
+ ].join(";");
1405
+ var ICON_BTN_STYLE = [
1406
+ "padding:2px 7px",
1407
+ "background:transparent",
1408
+ "color:#888",
1409
+ "border:1px solid #444",
1410
+ "border-radius:4px",
1411
+ "cursor:pointer",
1412
+ "font-size:12px"
1413
+ ].join(";");
1414
+ var TAB_BAR_STYLE = [
1415
+ "display:flex",
1416
+ "background:#1a1a1a",
1417
+ "border-bottom:1px solid #444",
1418
+ "flex-shrink:0"
1419
+ ].join(";");
1420
+ var TAB_BTN_STYLE = [
1421
+ "padding:5px 12px",
1422
+ "background:transparent",
1423
+ "color:#aaa",
1424
+ "border:none",
1425
+ "border-right:1px solid #333",
1426
+ "cursor:pointer",
1427
+ "font-family:monospace",
1428
+ "font-size:11px",
1429
+ "white-space:nowrap"
1430
+ ].join(";");
1431
+ var TAB_BTN_ACTIVE = ";background:#1e1e1e;color:#fff;border-bottom:2px solid #007acc;";
1432
+ var TOOLBAR_STYLE2 = [
1433
+ "display:flex",
1434
+ "align-items:center",
1435
+ "gap:4px",
1436
+ "padding:5px 8px",
1437
+ "background:#252525",
1438
+ "border-bottom:1px solid #444",
1439
+ "flex-shrink:0"
1440
+ ].join(";");
1441
+ var BTN_STYLE2 = [
1442
+ "padding:3px 8px",
1443
+ "background:#3a3a3a",
1444
+ "color:#ddd",
1445
+ "border:1px solid #555",
1446
+ "border-radius:4px",
1447
+ "cursor:pointer",
1448
+ "font-family:monospace",
1449
+ "font-size:11px"
1450
+ ].join(";");
1451
+ var LIST_STYLE2 = [
1452
+ "width:140px",
1453
+ "min-width:140px",
1454
+ "overflow-y:auto",
1455
+ "border-right:1px solid #333",
1456
+ "padding:4px",
1457
+ "display:flex",
1458
+ "flex-direction:column",
1459
+ "gap:2px"
1460
+ ].join(";");
1461
+ var LIST_ITEM_STYLE = [
1462
+ "display:flex",
1463
+ "align-items:center",
1464
+ "gap:6px",
1465
+ "padding:5px 6px",
1466
+ "border-radius:4px",
1467
+ "cursor:pointer",
1468
+ "overflow:hidden",
1469
+ "color:#ccc"
1470
+ ].join(";");
1471
+ var LIST_ITEM_ACTIVE = ";background:#007acc22;outline:1px solid #007acc;color:#fff;";
1472
+ var FORM_STYLE2 = ["flex:1", "overflow-y:auto", "padding:10px 10px 16px"].join(
1473
+ ";"
1474
+ );
1475
+ var INPUT_STYLE2 = [
1476
+ "width:100%",
1477
+ "background:#2a2a2a",
1478
+ "border:1px solid #555",
1479
+ "border-radius:3px",
1480
+ "color:#ddd",
1481
+ "padding:3px 5px",
1482
+ "font-family:monospace",
1483
+ "font-size:11px",
1484
+ "box-sizing:border-box"
1485
+ ].join(";");
1486
+
1487
+ // src/Editor/TopBar.ts
1488
+ var TopBar = class _TopBar {
1489
+ root;
1490
+ constructor() {
1491
+ }
1492
+ static mount(materialPanel) {
1493
+ const bar = new _TopBar();
1494
+ bar.build(materialPanel);
1495
+ return bar;
1496
+ }
1497
+ unmount() {
1498
+ this.root?.remove();
1499
+ }
1500
+ build(materialPanel) {
1501
+ this.root = document.createElement("div");
1502
+ this.root.id = "gl-top-bar";
1503
+ this.root.style.cssText = TOP_BAR_STYLE;
1504
+ const brand = document.createElement("span");
1505
+ brand.textContent = "GenesisGL";
1506
+ brand.style.cssText = "font-weight:bold;color:#9cdcfe;letter-spacing:1px;margin-right:16px;";
1507
+ this.root.appendChild(brand);
1508
+ this.root.appendChild(this.mkToggleBtn("\u{1F3A8} Materials", materialPanel));
1509
+ document.body.prepend(this.root);
1510
+ document.body.style.paddingTop = `${this.root.offsetHeight || 34}px`;
1511
+ }
1512
+ /** Add the Models tab button after the game has started. */
1513
+ addModelsButton(sidebar) {
1514
+ const btn = document.createElement("button");
1515
+ btn.textContent = "\u{1F9E0} Models";
1516
+ btn.style.cssText = BTN_STYLE3;
1517
+ btn.onclick = () => {
1518
+ sidebar.show();
1519
+ sidebar.activateTab("models");
1520
+ btn.style.cssText = sidebar.isVisible ? BTN_ACTIVE_STYLE : BTN_STYLE3;
1521
+ };
1522
+ this.root.appendChild(btn);
1523
+ }
1524
+ /** Add the Scene tab button after the game has started. */
1525
+ addSceneButton(sidebar) {
1526
+ const btn = document.createElement("button");
1527
+ btn.textContent = "\u{1F30D} Scene";
1528
+ btn.style.cssText = BTN_STYLE3;
1529
+ btn.onclick = () => {
1530
+ sidebar.show();
1531
+ sidebar.activateTab("scene");
1532
+ btn.style.cssText = sidebar.isVisible ? BTN_ACTIVE_STYLE : BTN_STYLE3;
1533
+ };
1534
+ this.root.appendChild(btn);
1535
+ }
1536
+ mkToggleBtn(label, panel) {
1537
+ return this.mkPanelToggleBtn(label, panel);
1538
+ }
1539
+ mkPanelToggleBtn(label, panel) {
1540
+ const btn = document.createElement("button");
1541
+ btn.textContent = label;
1542
+ btn.style.cssText = BTN_STYLE3;
1543
+ btn.onclick = () => {
1544
+ panel.toggle();
1545
+ btn.style.cssText = panel.isVisible ? BTN_ACTIVE_STYLE : BTN_STYLE3;
1546
+ };
1547
+ return btn;
1548
+ }
1549
+ };
1550
+ var TOP_BAR_STYLE = [
1551
+ "position:fixed",
1552
+ "top:0",
1553
+ "left:0",
1554
+ "right:0",
1555
+ "height:32px",
1556
+ "z-index:9999",
1557
+ "display:flex",
1558
+ "align-items:center",
1559
+ "padding:0 12px",
1560
+ "gap:6px",
1561
+ "background:#1a1a1a",
1562
+ "border-bottom:1px solid #444",
1563
+ "font-family:monospace",
1564
+ "font-size:12px",
1565
+ "color:#ddd",
1566
+ "box-shadow:0 2px 8px rgba(0,0,0,.5)"
1567
+ ].join(";");
1568
+ var BTN_STYLE3 = [
1569
+ "padding:3px 10px",
1570
+ "background:#2d2d2d",
1571
+ "color:#ddd",
1572
+ "border:1px solid #555",
1573
+ "border-radius:4px",
1574
+ "cursor:pointer",
1575
+ "font-size:11px",
1576
+ "font-family:monospace"
1577
+ ].join(";");
1578
+ var BTN_ACTIVE_STYLE = [
1579
+ "padding:3px 10px",
1580
+ "background:#007acc",
1581
+ "color:#fff",
1582
+ "border:1px solid #007acc",
1583
+ "border-radius:4px",
1584
+ "cursor:pointer",
1585
+ "font-size:11px",
1586
+ "font-family:monospace"
1587
+ ].join(";");
1588
+
1589
+ // src/Editor/sections/models/ModelsSection.ts
1590
+ var ModelsSection = class _ModelsSection {
1591
+ constructor(renderer, scene) {
1592
+ this.renderer = renderer;
1593
+ this.scene = scene;
1594
+ }
1595
+ renderer;
1596
+ scene;
1597
+ listEl = null;
1598
+ hitboxCheck = null;
1599
+ intervalId = null;
1600
+ // ── Factory ──────────────────────────────────────────────────
1601
+ static create(renderer, scene) {
1602
+ return new _ModelsSection(renderer, scene);
1603
+ }
1604
+ // ── Tab descriptor ───────────────────────────────────────────
1605
+ asTab() {
1606
+ return {
1607
+ id: "models",
1608
+ label: "\u{1F9E0} Models",
1609
+ buildContent: (c) => this.buildContent(c),
1610
+ onActivate: () => this.start(),
1611
+ onDeactivate: () => this.stop(),
1612
+ destroy: () => this.stop()
1613
+ };
1614
+ }
1615
+ // ── Lifecycle ────────────────────────────────────────────────
1616
+ start() {
1617
+ this.renderer.debug = true;
1618
+ if (this.hitboxCheck) this.hitboxCheck.checked = true;
1619
+ this.refresh();
1620
+ if (this.intervalId === null) {
1621
+ this.intervalId = setInterval(() => this.refresh(), 200);
1622
+ }
1623
+ }
1624
+ stop() {
1625
+ this.renderer.debug = false;
1626
+ if (this.hitboxCheck) this.hitboxCheck.checked = false;
1627
+ if (this.intervalId !== null) {
1628
+ clearInterval(this.intervalId);
1629
+ this.intervalId = null;
1630
+ }
1631
+ }
1632
+ // ── Build ────────────────────────────────────────────────────
1633
+ buildContent(container) {
1634
+ const controls = mk2("div", CONTROLS_STYLE);
1635
+ const hitboxRow = document.createElement("label");
1636
+ hitboxRow.style.cssText = "display:flex;align-items:center;gap:6px;cursor:pointer;color:#ccc;font-size:11px;";
1637
+ this.hitboxCheck = document.createElement("input");
1638
+ this.hitboxCheck.type = "checkbox";
1639
+ this.hitboxCheck.checked = this.renderer.debug;
1640
+ this.hitboxCheck.onchange = () => {
1641
+ this.renderer.debug = this.hitboxCheck.checked;
1642
+ };
1643
+ hitboxRow.appendChild(this.hitboxCheck);
1644
+ const hitboxLbl = document.createElement("span");
1645
+ hitboxLbl.textContent = "Show Hitboxes";
1646
+ hitboxRow.appendChild(hitboxLbl);
1647
+ controls.appendChild(hitboxRow);
1648
+ this.listEl = mk2("div", LIST_STYLE3);
1649
+ container.appendChild(controls);
1650
+ container.appendChild(this.listEl);
1651
+ }
1652
+ // ── Live refresh ─────────────────────────────────────────────
1653
+ refresh() {
1654
+ if (!this.listEl) return;
1655
+ const entries = this.scene.getEntries();
1656
+ this.listEl.innerHTML = "";
1657
+ const heading = mk2(
1658
+ "div",
1659
+ "color:#666;font-size:10px;letter-spacing:1px;padding:0 0 4px;"
1660
+ );
1661
+ heading.textContent = `MODELS (${entries.length})`;
1662
+ this.listEl.appendChild(heading);
1663
+ for (const [key, model] of entries) {
1664
+ const card = mk2("div", CARD_STYLE2);
1665
+ const nameEl = mk2(
1666
+ "div",
1667
+ "font-weight:bold;color:#9cdcfe;margin-bottom:4px;"
1668
+ );
1669
+ nameEl.textContent = key;
1670
+ card.appendChild(nameEl);
1671
+ const t = model.translation;
1672
+ const r = model.rotation;
1673
+ const s = model.scale;
1674
+ const bb = model.boundingBox;
1675
+ const w = bb.max[0] - bb.min[0];
1676
+ const h = bb.max[1] - bb.min[1];
1677
+ const d = bb.max[2] - bb.min[2];
1678
+ card.appendChild(dataRow2("pos", `${f2(t[0])} ${f2(t[1])} ${f2(t[2])}`));
1679
+ card.appendChild(dataRow2("rot", `${f2(r[0])} ${f2(r[1])} ${f2(r[2])}`));
1680
+ card.appendChild(dataRow2("scl", `${f2(s[0])} ${f2(s[1])} ${f2(s[2])}`));
1681
+ card.appendChild(dataRow2("bbox", `${f2(w)}w ${f2(h)}h ${f2(d)}d`));
1682
+ this.listEl.appendChild(card);
1683
+ }
1684
+ }
1685
+ };
1686
+ function mk2(tag, style = "") {
1687
+ const el2 = document.createElement(tag);
1688
+ if (style) el2.style.cssText = style;
1689
+ return el2;
1690
+ }
1691
+ function f2(n) {
1692
+ return n.toFixed(2);
1693
+ }
1694
+ function dataRow2(label, value) {
1695
+ const row = document.createElement("div");
1696
+ row.style.cssText = "display:flex;gap:4px;line-height:1.6;";
1697
+ const lbl = document.createElement("span");
1698
+ lbl.style.cssText = "color:#666;width:28px;flex-shrink:0;font-size:10px;";
1699
+ lbl.textContent = label;
1700
+ const val = document.createElement("span");
1701
+ val.style.cssText = "color:#ccc;font-size:10px;";
1702
+ val.textContent = value;
1703
+ row.appendChild(lbl);
1704
+ row.appendChild(val);
1705
+ return row;
1706
+ }
1707
+ var CONTROLS_STYLE = [
1708
+ "padding:6px 10px",
1709
+ "background:#222",
1710
+ "border-bottom:1px solid #333",
1711
+ "flex-shrink:0"
1712
+ ].join(";");
1713
+ var LIST_STYLE3 = [
1714
+ "overflow-y:auto",
1715
+ "padding:8px",
1716
+ "display:flex",
1717
+ "flex-direction:column",
1718
+ "gap:6px"
1719
+ ].join(";");
1720
+ var CARD_STYLE2 = [
1721
+ "background:#252525",
1722
+ "border:1px solid #333",
1723
+ "border-radius:4px",
1724
+ "padding:6px 8px"
1725
+ ].join(";");
1726
+ export {
1727
+ EditorSidebar,
1728
+ MaterialStore,
1729
+ ModelsSection,
1730
+ SceneSection,
1731
+ SceneStore,
1732
+ TopBar,
1733
+ createDefaultScene,
1734
+ createDefaultSceneItem,
1735
+ createDefaultSpec
1736
+ };
1737
+ //# sourceMappingURL=index.js.map