@fonsecabarreto/genesis-gl-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/dist/{Camera-DY_8gx3C.d.ts → Camera-CJVYy9fH.d.ts} +13 -2
- package/dist/Core/classes/Material.d.ts +1 -1
- package/dist/Core/classes/Material.js +1 -1
- package/dist/Core/classes/Model.d.ts +3 -3
- package/dist/Core/classes/Model.js +1 -1
- package/dist/Core/classes/Renderer.d.ts +11 -5
- package/dist/Core/classes/Renderer.js +4 -4
- package/dist/Core/classes/Scene.d.ts +2 -2
- package/dist/Core/classes/Viewport.d.ts +1 -1
- package/dist/Core/classes/Viewport.js +1 -1
- package/dist/Core/index.d.ts +4 -4
- package/dist/Core/index.js +4 -4
- package/dist/Core/utils/load-glb.d.ts +3 -3
- package/dist/Core/utils/load-glb.js +4 -4
- package/dist/Core/utils/parse-obj.d.ts +2 -2
- package/dist/Core/utils/parse-obj.js +4 -4
- package/dist/Editor/index.d.ts +126 -15
- package/dist/Editor/index.js +471 -74
- package/dist/Editor/index.js.map +1 -1
- package/dist/Game/controls/KeyboardInput.d.ts +4 -4
- package/dist/Game/index.d.ts +308 -7
- package/dist/Game/index.js +470 -24
- package/dist/Game/index.js.map +1 -1
- package/dist/{KeyboardInput-DTsfj3tE.d.ts → KeyboardInput-1xOAabI0.d.ts} +95 -14
- package/dist/{Material-BGLkldxv.d.ts → Material-DhwSRbP2.d.ts} +8 -0
- package/dist/{Model-CQvDXd-b.d.ts → Model-BBZHnUp1.d.ts} +24 -8
- package/dist/{chunk-6LS6AO5H.js → chunk-L66K4AZU.js} +36 -30
- package/dist/chunk-L66K4AZU.js.map +1 -0
- package/dist/{chunk-JK2HEZAT.js → chunk-QOAQVTAB.js} +26 -22
- package/dist/chunk-QOAQVTAB.js.map +1 -0
- package/dist/{chunk-5TAAXI6S.js → chunk-XMW2MS66.js} +39 -16
- package/dist/chunk-XMW2MS66.js.map +1 -0
- package/dist/{chunk-QCQVJCSR.js → chunk-ZCJ3MJZD.js} +103 -67
- package/dist/chunk-ZCJ3MJZD.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-5TAAXI6S.js.map +0 -1
- package/dist/chunk-6LS6AO5H.js.map +0 -1
- package/dist/chunk-JK2HEZAT.js.map +0 -1
- package/dist/chunk-QCQVJCSR.js.map +0 -1
package/dist/Editor/index.js
CHANGED
|
@@ -231,91 +231,172 @@ function createDefaultScene() {
|
|
|
231
231
|
items: []
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
+
function createDefaultNamedScene(name = "Scene 1", isActive = false) {
|
|
235
|
+
return { id: crypto.randomUUID(), name, isActive, ...createDefaultScene() };
|
|
236
|
+
}
|
|
237
|
+
function createDefaultMultiSceneFile() {
|
|
238
|
+
return { scenes: [createDefaultNamedScene("Scene 1", true)] };
|
|
239
|
+
}
|
|
234
240
|
|
|
235
241
|
// src/Editor/sections/scene/SceneStore.ts
|
|
236
|
-
var STORAGE_KEY2 = "
|
|
242
|
+
var STORAGE_KEY2 = "genesisgl__scenes";
|
|
237
243
|
var SceneStore = class {
|
|
238
|
-
|
|
244
|
+
multiScene = createDefaultMultiSceneFile();
|
|
245
|
+
activeSceneId;
|
|
239
246
|
listeners = [];
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
scenesListeners = [];
|
|
248
|
+
gameSceneListeners = [];
|
|
249
|
+
scenesFilePath;
|
|
250
|
+
constructor(options) {
|
|
251
|
+
this.scenesFilePath = options.scenesFilePath;
|
|
252
|
+
this.activeSceneId = this.multiScene.scenes[0].id;
|
|
243
253
|
this.loadFromStorage();
|
|
244
254
|
}
|
|
245
255
|
// ── Initialisation ──────────────────────────────────────────
|
|
246
256
|
async init() {
|
|
247
|
-
if (!this.filePath) return;
|
|
248
257
|
try {
|
|
249
|
-
const res = await fetch(this.
|
|
258
|
+
const res = await fetch(this.scenesFilePath);
|
|
250
259
|
if (!res.ok) {
|
|
251
|
-
console.warn(
|
|
252
|
-
`[SceneStore] Could not load "${this.filePath}" (${res.status})`
|
|
253
|
-
);
|
|
260
|
+
console.warn(`[SceneStore] Could not load "${this.scenesFilePath}" (${res.status})`);
|
|
254
261
|
return;
|
|
255
262
|
}
|
|
256
263
|
const data = await res.json();
|
|
257
|
-
this.
|
|
264
|
+
this.loadData(data);
|
|
258
265
|
this.persist();
|
|
259
266
|
this.emit();
|
|
267
|
+
this.emitScenes();
|
|
260
268
|
} catch {
|
|
261
|
-
console.warn(`[SceneStore] Failed to fetch "${this.
|
|
269
|
+
console.warn(`[SceneStore] Failed to fetch "${this.scenesFilePath}"`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ── Multi-scene management ───────────────────────────────────
|
|
273
|
+
/** All scenes in the project. */
|
|
274
|
+
getScenes() {
|
|
275
|
+
return this.multiScene.scenes;
|
|
276
|
+
}
|
|
277
|
+
/** Id of the currently active scene. */
|
|
278
|
+
getActiveSceneId() {
|
|
279
|
+
return this.activeSceneId;
|
|
280
|
+
}
|
|
281
|
+
/** Switch which scene is being edited. */
|
|
282
|
+
setActiveScene(id) {
|
|
283
|
+
if (!this.multiScene.scenes.find((s) => s.id === id)) return;
|
|
284
|
+
this.activeSceneId = id;
|
|
285
|
+
this.emit();
|
|
286
|
+
this.emitScenes();
|
|
287
|
+
}
|
|
288
|
+
/** Create a new scene and make it active. Returns the new scene. */
|
|
289
|
+
addScene(name = "New Scene") {
|
|
290
|
+
const scene = createDefaultNamedScene(name);
|
|
291
|
+
this.multiScene.scenes.push(scene);
|
|
292
|
+
this.activeSceneId = scene.id;
|
|
293
|
+
this.persist();
|
|
294
|
+
this.emit();
|
|
295
|
+
this.emitScenes();
|
|
296
|
+
void this.syncToServer();
|
|
297
|
+
return scene;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Delete a scene by id.
|
|
301
|
+
* The last remaining scene is never deleted.
|
|
302
|
+
*/
|
|
303
|
+
removeScene(id) {
|
|
304
|
+
if (this.multiScene.scenes.length <= 1) return;
|
|
305
|
+
this.multiScene.scenes = this.multiScene.scenes.filter((s) => s.id !== id);
|
|
306
|
+
if (this.activeSceneId === id) {
|
|
307
|
+
this.activeSceneId = this.multiScene.scenes[0].id;
|
|
308
|
+
}
|
|
309
|
+
this.persist();
|
|
310
|
+
this.emit();
|
|
311
|
+
this.emitScenes();
|
|
312
|
+
void this.syncToServer();
|
|
313
|
+
}
|
|
314
|
+
/** Rename a scene. */
|
|
315
|
+
renameScene(id, name) {
|
|
316
|
+
const scene = this.multiScene.scenes.find((s) => s.id === id);
|
|
317
|
+
if (!scene) return;
|
|
318
|
+
scene.name = name;
|
|
319
|
+
this.persist();
|
|
320
|
+
this.emitScenes();
|
|
321
|
+
void this.syncToServer();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Returns the id of the scene flagged as the **game's** active scene
|
|
325
|
+
* (the one that loads at runtime), or the first scene if none is flagged.
|
|
326
|
+
*/
|
|
327
|
+
getActiveGameSceneId() {
|
|
328
|
+
return this.multiScene.scenes.find((s) => s.isActive)?.id ?? this.multiScene.scenes[0].id;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Set exactly one scene as the game's active scene.
|
|
332
|
+
* All other scenes have their `isActive` flag removed.
|
|
333
|
+
*/
|
|
334
|
+
setActiveGameScene(id) {
|
|
335
|
+
const target = this.multiScene.scenes.find((s) => s.id === id);
|
|
336
|
+
if (!target) return;
|
|
337
|
+
for (const s of this.multiScene.scenes) {
|
|
338
|
+
s.isActive = s.id === id;
|
|
262
339
|
}
|
|
340
|
+
this.persist();
|
|
341
|
+
this.emitScenes();
|
|
342
|
+
this.emitGameScene();
|
|
343
|
+
void this.syncToServer();
|
|
263
344
|
}
|
|
264
|
-
// ── Read
|
|
345
|
+
// ── Read (delegated to active scene) ────────────────────────
|
|
265
346
|
get() {
|
|
266
|
-
|
|
347
|
+
const { id: _id, name: _name, isActive: _ia, ...spec } = this.activeScene;
|
|
348
|
+
return spec;
|
|
267
349
|
}
|
|
268
350
|
getItems() {
|
|
269
|
-
return this.
|
|
351
|
+
return this.activeScene.items;
|
|
270
352
|
}
|
|
271
353
|
getItem(id) {
|
|
272
|
-
return this.
|
|
354
|
+
return this.activeScene.items.find((i) => i.id === id);
|
|
273
355
|
}
|
|
274
356
|
getPlayerSpawn() {
|
|
275
|
-
return [...this.
|
|
357
|
+
return [...this.activeScene.playerSpawn];
|
|
276
358
|
}
|
|
277
|
-
// ── Mutations
|
|
359
|
+
// ── Mutations (delegated to active scene) ───────────────────
|
|
278
360
|
setPlayerSpawn(x, y, z) {
|
|
279
|
-
this.
|
|
361
|
+
this.activeScene.playerSpawn = [x, y, z];
|
|
280
362
|
this.persist();
|
|
281
363
|
this.emit();
|
|
282
364
|
void this.syncToServer();
|
|
283
365
|
}
|
|
284
366
|
addItem(item) {
|
|
285
|
-
this.
|
|
367
|
+
this.activeScene.items.push(item);
|
|
286
368
|
this.persist();
|
|
287
369
|
this.emit();
|
|
288
370
|
void this.syncToServer();
|
|
289
371
|
}
|
|
290
372
|
updateItem(item) {
|
|
291
|
-
const idx = this.
|
|
373
|
+
const idx = this.activeScene.items.findIndex((i) => i.id === item.id);
|
|
292
374
|
if (idx === -1) return;
|
|
293
|
-
this.
|
|
375
|
+
this.activeScene.items[idx] = { ...item };
|
|
294
376
|
this.persist();
|
|
295
377
|
this.emit();
|
|
296
378
|
void this.syncToServer();
|
|
297
379
|
}
|
|
298
380
|
deleteItem(id) {
|
|
299
|
-
this.
|
|
381
|
+
this.activeScene.items = this.activeScene.items.filter((i) => i.id !== id);
|
|
300
382
|
this.persist();
|
|
301
383
|
this.emit();
|
|
302
384
|
void this.syncToServer();
|
|
303
385
|
}
|
|
304
|
-
/** Replace the entire scene definition. */
|
|
386
|
+
/** Replace the entire active scene definition. */
|
|
305
387
|
setScene(spec) {
|
|
306
|
-
this.
|
|
388
|
+
Object.assign(this.activeScene, spec);
|
|
307
389
|
this.persist();
|
|
308
390
|
this.emit();
|
|
309
391
|
void this.syncToServer();
|
|
310
392
|
}
|
|
311
393
|
// ── Server sync ──────────────────────────────────────────────
|
|
312
394
|
async syncToServer() {
|
|
313
|
-
if (!this.filePath) return false;
|
|
314
395
|
try {
|
|
315
|
-
const res = await fetch("/api/
|
|
396
|
+
const res = await fetch("/api/scenes", {
|
|
316
397
|
method: "PUT",
|
|
317
398
|
headers: { "Content-Type": "application/json" },
|
|
318
|
-
body: JSON.stringify(this.
|
|
399
|
+
body: JSON.stringify(this.multiScene, null, 2)
|
|
319
400
|
});
|
|
320
401
|
if (!res.ok) {
|
|
321
402
|
console.warn(`[SceneStore] Server sync failed (${res.status})`);
|
|
@@ -329,7 +410,7 @@ var SceneStore = class {
|
|
|
329
410
|
// ── Persistence ──────────────────────────────────────────────
|
|
330
411
|
persist() {
|
|
331
412
|
try {
|
|
332
|
-
localStorage.setItem(STORAGE_KEY2, JSON.stringify(this.
|
|
413
|
+
localStorage.setItem(STORAGE_KEY2, JSON.stringify(this.multiScene));
|
|
333
414
|
} catch {
|
|
334
415
|
console.warn("[SceneStore] Failed to write to localStorage");
|
|
335
416
|
}
|
|
@@ -338,17 +419,23 @@ var SceneStore = class {
|
|
|
338
419
|
try {
|
|
339
420
|
const raw = localStorage.getItem(STORAGE_KEY2);
|
|
340
421
|
if (!raw) return;
|
|
341
|
-
|
|
342
|
-
if (data && Array.isArray(data.items)) {
|
|
343
|
-
this.scene = data;
|
|
344
|
-
}
|
|
422
|
+
this.loadData(JSON.parse(raw));
|
|
345
423
|
} catch {
|
|
346
424
|
console.warn("[SceneStore] Failed to read from localStorage");
|
|
347
425
|
}
|
|
348
426
|
}
|
|
427
|
+
loadData(data) {
|
|
428
|
+
if (data !== null && typeof data === "object" && Array.isArray(data.scenes) && data.scenes.length > 0) {
|
|
429
|
+
this.multiScene = data;
|
|
430
|
+
if (!this.multiScene.scenes.find((s) => s.id === this.activeSceneId)) {
|
|
431
|
+
this.activeSceneId = this.multiScene.scenes[0].id;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
349
435
|
// ── File I/O ─────────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
|
|
436
|
+
/** Export the full multi-scene file. */
|
|
437
|
+
async exportToFile(filename = "scenes.json") {
|
|
438
|
+
const json = JSON.stringify(this.multiScene, null, 2);
|
|
352
439
|
const blob = new Blob([json], { type: "application/json" });
|
|
353
440
|
if ("showSaveFilePicker" in window) {
|
|
354
441
|
try {
|
|
@@ -372,6 +459,7 @@ var SceneStore = class {
|
|
|
372
459
|
a.click();
|
|
373
460
|
URL.revokeObjectURL(url);
|
|
374
461
|
}
|
|
462
|
+
/** Import a multi-scene JSON file, replacing current data. */
|
|
375
463
|
importFromFile() {
|
|
376
464
|
return new Promise((resolve) => {
|
|
377
465
|
const input = document.createElement("input");
|
|
@@ -386,11 +474,10 @@ var SceneStore = class {
|
|
|
386
474
|
try {
|
|
387
475
|
const text = await file.text();
|
|
388
476
|
const data = JSON.parse(text);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
477
|
+
this.loadData(data);
|
|
478
|
+
this.persist();
|
|
479
|
+
this.emit();
|
|
480
|
+
this.emitScenes();
|
|
394
481
|
} catch {
|
|
395
482
|
console.error("[SceneStore] Failed to parse imported file");
|
|
396
483
|
}
|
|
@@ -400,16 +487,54 @@ var SceneStore = class {
|
|
|
400
487
|
});
|
|
401
488
|
}
|
|
402
489
|
// ── Subscriptions ────────────────────────────────────────────
|
|
490
|
+
/** Subscribe to active-scene data changes. */
|
|
403
491
|
subscribe(fn) {
|
|
404
492
|
this.listeners.push(fn);
|
|
405
493
|
return () => {
|
|
406
494
|
this.listeners = this.listeners.filter((l) => l !== fn);
|
|
407
495
|
};
|
|
408
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Subscribe to scene-list or active-scene-selection changes.
|
|
499
|
+
* Fired when scenes are added/removed/renamed or the active scene switches.
|
|
500
|
+
*/
|
|
501
|
+
subscribeScenes(fn) {
|
|
502
|
+
this.scenesListeners.push(fn);
|
|
503
|
+
return () => {
|
|
504
|
+
this.scenesListeners = this.scenesListeners.filter((l) => l !== fn);
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Subscribe to game-scene changes.
|
|
509
|
+
* Fired only when {@link setActiveGameScene} is called — use this to reload
|
|
510
|
+
* the running game world with the newly flagged scene's data.
|
|
511
|
+
*/
|
|
512
|
+
subscribeGameScene(fn) {
|
|
513
|
+
this.gameSceneListeners.push(fn);
|
|
514
|
+
return () => {
|
|
515
|
+
this.gameSceneListeners = this.gameSceneListeners.filter((l) => l !== fn);
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
// ── Private helpers ──────────────────────────────────────────
|
|
519
|
+
get activeScene() {
|
|
520
|
+
return this.multiScene.scenes.find((s) => s.id === this.activeSceneId) ?? this.multiScene.scenes[0];
|
|
521
|
+
}
|
|
409
522
|
emit() {
|
|
410
|
-
const spec = this.
|
|
523
|
+
const spec = this.get();
|
|
411
524
|
for (const fn of this.listeners) fn(spec);
|
|
412
525
|
}
|
|
526
|
+
emitScenes() {
|
|
527
|
+
const scenes = this.getScenes();
|
|
528
|
+
const activeId = this.activeSceneId;
|
|
529
|
+
for (const fn of this.scenesListeners) fn(scenes, activeId);
|
|
530
|
+
}
|
|
531
|
+
emitGameScene() {
|
|
532
|
+
const activeId = this.getActiveGameSceneId();
|
|
533
|
+
const scene = this.multiScene.scenes.find((s) => s.id === activeId);
|
|
534
|
+
if (!scene) return;
|
|
535
|
+
const { id: _id, name: _name, isActive: _ia, ...spec } = scene;
|
|
536
|
+
for (const fn of this.gameSceneListeners) fn(spec);
|
|
537
|
+
}
|
|
413
538
|
};
|
|
414
539
|
|
|
415
540
|
// src/Editor/sections/scene/SceneSection.ts
|
|
@@ -426,6 +551,8 @@ var SceneSection = class _SceneSection {
|
|
|
426
551
|
selectedId = null;
|
|
427
552
|
formEl = null;
|
|
428
553
|
unsub;
|
|
554
|
+
unsubScenes;
|
|
555
|
+
scenesBar = null;
|
|
429
556
|
modelRegistry = [];
|
|
430
557
|
getMaterialNames;
|
|
431
558
|
modelsFile;
|
|
@@ -443,7 +570,10 @@ var SceneSection = class _SceneSection {
|
|
|
443
570
|
buildContent: (c) => this.buildContent(c),
|
|
444
571
|
onActivate: () => this.onActivate(),
|
|
445
572
|
onDeactivate: () => this.onDeactivate(),
|
|
446
|
-
destroy: () =>
|
|
573
|
+
destroy: () => {
|
|
574
|
+
this.unsub?.();
|
|
575
|
+
this.unsubScenes?.();
|
|
576
|
+
}
|
|
447
577
|
};
|
|
448
578
|
}
|
|
449
579
|
// ── Lifecycle ────────────────────────────────────────────────
|
|
@@ -464,6 +594,10 @@ var SceneSection = class _SceneSection {
|
|
|
464
594
|
// ── Build ────────────────────────────────────────────────────
|
|
465
595
|
buildContent(container) {
|
|
466
596
|
this.root = container;
|
|
597
|
+
this.scenesBar = mk("div", SCENES_BAR_STYLE);
|
|
598
|
+
this.refreshScenesBar();
|
|
599
|
+
container.appendChild(this.scenesBar);
|
|
600
|
+
this.unsubScenes = this.store.subscribeScenes(() => this.refreshScenesBar());
|
|
467
601
|
const toolbar = mk("div", TOOLBAR_STYLE);
|
|
468
602
|
toolbar.appendChild(
|
|
469
603
|
this.mkBtn("\uFF0B Item", () => this.onAdd(), "Add scene item")
|
|
@@ -512,6 +646,92 @@ var SceneSection = class _SceneSection {
|
|
|
512
646
|
});
|
|
513
647
|
this.refreshList();
|
|
514
648
|
}
|
|
649
|
+
// ── Scenes bar ──────────────────────────────────────────────
|
|
650
|
+
refreshScenesBar() {
|
|
651
|
+
if (!this.scenesBar) return;
|
|
652
|
+
this.scenesBar.innerHTML = "";
|
|
653
|
+
const scenes = this.store.getScenes();
|
|
654
|
+
const activeId = this.store.getActiveSceneId();
|
|
655
|
+
for (const scene of scenes) {
|
|
656
|
+
const isEditorActive = scene.id === activeId;
|
|
657
|
+
const isGameActive = !!scene.isActive;
|
|
658
|
+
const tabWrap = mk("div", "display:flex;align-items:stretch;gap:2px;");
|
|
659
|
+
const tab = mk(
|
|
660
|
+
"button",
|
|
661
|
+
SCENE_TAB_STYLE + (isEditorActive ? SCENE_TAB_ACTIVE : "")
|
|
662
|
+
);
|
|
663
|
+
tab.textContent = scene.name;
|
|
664
|
+
tab.title = `Switch to "${scene.name}"`;
|
|
665
|
+
tab.onclick = () => {
|
|
666
|
+
this.selectedId = null;
|
|
667
|
+
this.store.setActiveScene(scene.id);
|
|
668
|
+
this.refreshList();
|
|
669
|
+
this.refreshForm();
|
|
670
|
+
};
|
|
671
|
+
tab.ondblclick = (e) => {
|
|
672
|
+
e.preventDefault();
|
|
673
|
+
const newName = prompt("Rename scene:", scene.name);
|
|
674
|
+
if (newName && newName.trim()) {
|
|
675
|
+
this.store.renameScene(scene.id, newName.trim());
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
tabWrap.appendChild(tab);
|
|
679
|
+
const playBtn = mk(
|
|
680
|
+
"button",
|
|
681
|
+
SCENE_PLAY_BTN_STYLE + (isGameActive ? SCENE_PLAY_BTN_ACTIVE : "")
|
|
682
|
+
);
|
|
683
|
+
playBtn.textContent = "\u25B6";
|
|
684
|
+
playBtn.title = isGameActive ? "This scene is loaded by the game (click another to change)" : `Set "${scene.name}" as the game\u2019s startup scene`;
|
|
685
|
+
playBtn.onclick = (e) => {
|
|
686
|
+
e.stopPropagation();
|
|
687
|
+
this.store.setActiveGameScene(scene.id);
|
|
688
|
+
};
|
|
689
|
+
tabWrap.appendChild(playBtn);
|
|
690
|
+
this.scenesBar.appendChild(tabWrap);
|
|
691
|
+
}
|
|
692
|
+
const spacer = mk("div", "flex:1;");
|
|
693
|
+
this.scenesBar.appendChild(spacer);
|
|
694
|
+
const addBtn = mk("button", SCENE_BAR_BTN_STYLE);
|
|
695
|
+
addBtn.textContent = "\uFF0B";
|
|
696
|
+
addBtn.title = "Add new scene";
|
|
697
|
+
addBtn.onclick = () => {
|
|
698
|
+
const name = prompt("New scene name:", `Scene ${scenes.length + 1}`);
|
|
699
|
+
if (name && name.trim()) {
|
|
700
|
+
this.selectedId = null;
|
|
701
|
+
this.store.addScene(name.trim());
|
|
702
|
+
this.refreshList();
|
|
703
|
+
this.refreshForm();
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
this.scenesBar.appendChild(addBtn);
|
|
707
|
+
const renameBtn = mk("button", SCENE_BAR_BTN_STYLE);
|
|
708
|
+
renameBtn.textContent = "\u270F\uFE0F";
|
|
709
|
+
renameBtn.title = "Rename active scene";
|
|
710
|
+
renameBtn.onclick = () => {
|
|
711
|
+
const current = scenes.find((s) => s.id === activeId);
|
|
712
|
+
if (!current) return;
|
|
713
|
+
const newName = prompt("Rename scene:", current.name);
|
|
714
|
+
if (newName && newName.trim()) {
|
|
715
|
+
this.store.renameScene(activeId, newName.trim());
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
this.scenesBar.appendChild(renameBtn);
|
|
719
|
+
const delBtn = mk("button", SCENE_BAR_BTN_STYLE);
|
|
720
|
+
delBtn.textContent = "\u{1F5D1}";
|
|
721
|
+
delBtn.title = scenes.length <= 1 ? "Cannot delete the last scene" : "Delete active scene";
|
|
722
|
+
delBtn.disabled = scenes.length <= 1;
|
|
723
|
+
delBtn.style.opacity = scenes.length <= 1 ? "0.35" : "1";
|
|
724
|
+
delBtn.onclick = () => {
|
|
725
|
+
const current = scenes.find((s) => s.id === activeId);
|
|
726
|
+
if (!current) return;
|
|
727
|
+
if (!confirm(`Delete scene "${current.name}"?`)) return;
|
|
728
|
+
this.selectedId = null;
|
|
729
|
+
this.store.removeScene(activeId);
|
|
730
|
+
this.refreshList();
|
|
731
|
+
this.refreshForm();
|
|
732
|
+
};
|
|
733
|
+
this.scenesBar.appendChild(delBtn);
|
|
734
|
+
}
|
|
515
735
|
// ── List ─────────────────────────────────────────────────────
|
|
516
736
|
refreshList() {
|
|
517
737
|
if (!this.listEl) return;
|
|
@@ -561,12 +781,22 @@ var SceneSection = class _SceneSection {
|
|
|
561
781
|
this.refreshList();
|
|
562
782
|
this.refreshForm();
|
|
563
783
|
};
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
);
|
|
568
|
-
nameEl.textContent =
|
|
569
|
-
|
|
784
|
+
const cardHeader = mk("div", "display:flex;align-items:center;gap:4px;margin-bottom:2px;");
|
|
785
|
+
const typeIcon = mk("span", "font-size:10px;");
|
|
786
|
+
typeIcon.textContent = item.interactive ? "\u26A1" : "\u{1F4E6}";
|
|
787
|
+
const nameEl = mk("div", "font-weight:bold;color:#9cdcfe;font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;");
|
|
788
|
+
nameEl.textContent = item.name || "(unnamed)";
|
|
789
|
+
cardHeader.appendChild(typeIcon);
|
|
790
|
+
cardHeader.appendChild(nameEl);
|
|
791
|
+
if (item.interactive) {
|
|
792
|
+
const badge = mk("span", "font-size:8px;padding:1px 4px;border-radius:2px;background:#2a3a1a;color:#8c8;border:1px solid #4a7040;flex-shrink:0;");
|
|
793
|
+
badge.textContent = "PHY";
|
|
794
|
+
cardHeader.appendChild(badge);
|
|
795
|
+
}
|
|
796
|
+
card.appendChild(cardHeader);
|
|
797
|
+
if (item.resourceKey) {
|
|
798
|
+
card.appendChild(dataRow("res", item.resourceKey));
|
|
799
|
+
}
|
|
570
800
|
card.appendChild(
|
|
571
801
|
dataRow(
|
|
572
802
|
"pos",
|
|
@@ -614,12 +844,27 @@ var SceneSection = class _SceneSection {
|
|
|
614
844
|
);
|
|
615
845
|
}
|
|
616
846
|
buildItemForm(container, item) {
|
|
617
|
-
const
|
|
847
|
+
const titleBar = mk(
|
|
618
848
|
"div",
|
|
619
|
-
"
|
|
849
|
+
"display:flex;align-items:center;gap:8px;padding:0 0 10px;border-bottom:1px solid #333;margin-bottom:4px;"
|
|
620
850
|
);
|
|
621
|
-
|
|
622
|
-
|
|
851
|
+
const typeIcon = mk("span", "font-size:16px;");
|
|
852
|
+
typeIcon.textContent = item.interactive ? "\u26A1" : "\u{1F4E6}";
|
|
853
|
+
const titleText = mk(
|
|
854
|
+
"div",
|
|
855
|
+
"font-weight:bold;color:#9cdcfe;font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
|
|
856
|
+
);
|
|
857
|
+
titleText.textContent = item.name || "(unnamed)";
|
|
858
|
+
const typeBadge = mk(
|
|
859
|
+
"span",
|
|
860
|
+
"font-size:9px;padding:2px 6px;border-radius:3px;font-family:monospace;flex-shrink:0;" + (item.interactive ? "background:#2a3a1a;color:#8c8;border:1px solid #4a7040;" : "background:#1a1a2a;color:#88a;border:1px solid #445;")
|
|
861
|
+
);
|
|
862
|
+
typeBadge.textContent = item.interactive ? "PHYSICS" : "STATIC";
|
|
863
|
+
titleBar.appendChild(typeIcon);
|
|
864
|
+
titleBar.appendChild(titleText);
|
|
865
|
+
titleBar.appendChild(typeBadge);
|
|
866
|
+
container.appendChild(titleBar);
|
|
867
|
+
container.appendChild(this.sectionHeader("Identity", "\u{1F3F7}"));
|
|
623
868
|
container.appendChild(
|
|
624
869
|
this.textRow("Name", item.name, (v) => {
|
|
625
870
|
this.store.updateItem({ ...item, name: v });
|
|
@@ -634,16 +879,7 @@ var SceneSection = class _SceneSection {
|
|
|
634
879
|
this.store.updateItem({ ...item, resourceKey: v });
|
|
635
880
|
})
|
|
636
881
|
);
|
|
637
|
-
|
|
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
|
-
);
|
|
882
|
+
container.appendChild(this.sectionHeader("Transform", "\u{1F4D0}"));
|
|
647
883
|
container.appendChild(
|
|
648
884
|
this.vec3Row(
|
|
649
885
|
"Position",
|
|
@@ -671,21 +907,39 @@ var SceneSection = class _SceneSection {
|
|
|
671
907
|
}
|
|
672
908
|
)
|
|
673
909
|
);
|
|
674
|
-
|
|
910
|
+
container.appendChild(this.sectionHeader("Rendering", "\u{1F3A8}"));
|
|
911
|
+
const matNames = this.getMaterialNames();
|
|
912
|
+
const matOptions = [
|
|
913
|
+
{ value: "", label: "(default)" },
|
|
914
|
+
...matNames.map((n) => ({ value: n, label: n }))
|
|
915
|
+
];
|
|
675
916
|
container.appendChild(
|
|
676
|
-
this.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
917
|
+
this.selectRow("Material", item.materialKey ?? "", matOptions, (v) => {
|
|
918
|
+
this.store.updateItem({ ...item, materialKey: v || null });
|
|
919
|
+
})
|
|
920
|
+
);
|
|
921
|
+
container.appendChild(
|
|
922
|
+
this.checkRow("Collider", item.collider !== false, (on) => {
|
|
923
|
+
this.store.updateItem({ ...item, collider: on });
|
|
924
|
+
})
|
|
925
|
+
);
|
|
926
|
+
container.appendChild(this.sectionHeader("Bounds Override", "\u{1F4CF}"));
|
|
927
|
+
container.appendChild(
|
|
928
|
+
this.checkRow("Custom", item.bounds != null, (on) => {
|
|
929
|
+
this.store.updateItem({
|
|
930
|
+
...item,
|
|
931
|
+
bounds: on ? [0, 0, 0, 1, 1, 1] : null
|
|
932
|
+
});
|
|
685
933
|
})
|
|
686
934
|
);
|
|
687
935
|
if (item.bounds) {
|
|
688
936
|
const b = item.bounds;
|
|
937
|
+
const hint = mk(
|
|
938
|
+
"div",
|
|
939
|
+
"color:#555;font-size:9px;margin:-4px 0 6px 76px;font-family:monospace;"
|
|
940
|
+
);
|
|
941
|
+
hint.textContent = "offset (x y z) + size (w h d)";
|
|
942
|
+
container.appendChild(hint);
|
|
689
943
|
container.appendChild(
|
|
690
944
|
this.vec3Row("Offset", [b[0], b[1], b[2]], (v) => {
|
|
691
945
|
this.store.updateItem({
|
|
@@ -703,11 +957,56 @@ var SceneSection = class _SceneSection {
|
|
|
703
957
|
})
|
|
704
958
|
);
|
|
705
959
|
}
|
|
960
|
+
container.appendChild(this.sectionHeader("Physics", "\u26A1"));
|
|
706
961
|
container.appendChild(
|
|
707
|
-
this.checkRow("
|
|
708
|
-
this.store.updateItem({
|
|
962
|
+
this.checkRow("Interactive", !!item.interactive, (on) => {
|
|
963
|
+
this.store.updateItem({
|
|
964
|
+
...item,
|
|
965
|
+
interactive: on,
|
|
966
|
+
radius: on ? item.radius ?? 0.5 : item.radius,
|
|
967
|
+
mass: on ? item.mass ?? 1 : item.mass,
|
|
968
|
+
restitution: on ? item.restitution ?? 0.6 : item.restitution
|
|
969
|
+
});
|
|
709
970
|
})
|
|
710
971
|
);
|
|
972
|
+
if (item.interactive) {
|
|
973
|
+
const physHint = mk(
|
|
974
|
+
"div",
|
|
975
|
+
"color:#555;font-size:9px;margin:-4px 0 6px 76px;font-family:monospace;"
|
|
976
|
+
);
|
|
977
|
+
physHint.textContent = "leave resource empty to auto-generate a sphere mesh";
|
|
978
|
+
container.appendChild(physHint);
|
|
979
|
+
container.appendChild(
|
|
980
|
+
this.numberRow(
|
|
981
|
+
"Radius",
|
|
982
|
+
item.radius ?? 0.5,
|
|
983
|
+
{ step: 0.05, min: 0.01 },
|
|
984
|
+
(v) => {
|
|
985
|
+
this.store.updateItem({ ...item, radius: v });
|
|
986
|
+
}
|
|
987
|
+
)
|
|
988
|
+
);
|
|
989
|
+
container.appendChild(
|
|
990
|
+
this.numberRow(
|
|
991
|
+
"Mass",
|
|
992
|
+
item.mass ?? 1,
|
|
993
|
+
{ step: 0.1, min: 0.01 },
|
|
994
|
+
(v) => {
|
|
995
|
+
this.store.updateItem({ ...item, mass: v });
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
);
|
|
999
|
+
container.appendChild(
|
|
1000
|
+
this.numberRow(
|
|
1001
|
+
"Restitution",
|
|
1002
|
+
item.restitution ?? 0.6,
|
|
1003
|
+
{ step: 0.05, min: 0, max: 1 },
|
|
1004
|
+
(v) => {
|
|
1005
|
+
this.store.updateItem({ ...item, restitution: v });
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
711
1010
|
}
|
|
712
1011
|
// ── CRUD actions ─────────────────────────────────────────────
|
|
713
1012
|
onAdd() {
|
|
@@ -737,6 +1036,37 @@ var SceneSection = class _SceneSection {
|
|
|
737
1036
|
this.selectedId = null;
|
|
738
1037
|
}
|
|
739
1038
|
// ── Input helpers ────────────────────────────────────────────
|
|
1039
|
+
sectionHeader(label, icon = "") {
|
|
1040
|
+
const wrap = mk("div", SECTION_HEADER_STYLE);
|
|
1041
|
+
if (icon) {
|
|
1042
|
+
const ico = mk("span", "font-size:11px;");
|
|
1043
|
+
ico.textContent = icon;
|
|
1044
|
+
wrap.appendChild(ico);
|
|
1045
|
+
}
|
|
1046
|
+
const lbl = mk(
|
|
1047
|
+
"span",
|
|
1048
|
+
"color:#666;font-size:9px;letter-spacing:1.5px;font-family:monospace;text-transform:uppercase;"
|
|
1049
|
+
);
|
|
1050
|
+
lbl.textContent = label;
|
|
1051
|
+
wrap.appendChild(lbl);
|
|
1052
|
+
return wrap;
|
|
1053
|
+
}
|
|
1054
|
+
numberRow(label, value, opts = {}, onChange) {
|
|
1055
|
+
const row = mk("div", ROW_STYLE);
|
|
1056
|
+
const lbl = mk("div", LABEL_STYLE);
|
|
1057
|
+
lbl.textContent = label;
|
|
1058
|
+
row.appendChild(lbl);
|
|
1059
|
+
const input = document.createElement("input");
|
|
1060
|
+
input.type = "number";
|
|
1061
|
+
input.step = String(opts.step ?? 0.1);
|
|
1062
|
+
if (opts.min !== void 0) input.min = String(opts.min);
|
|
1063
|
+
if (opts.max !== void 0) input.max = String(opts.max);
|
|
1064
|
+
input.value = String(value);
|
|
1065
|
+
input.style.cssText = INPUT_STYLE + ";width:80px;";
|
|
1066
|
+
input.onchange = () => onChange(parseFloat(input.value) || 0);
|
|
1067
|
+
row.appendChild(input);
|
|
1068
|
+
return row;
|
|
1069
|
+
}
|
|
740
1070
|
vec3Row(label, values, onChange) {
|
|
741
1071
|
const row = mk("div", ROW_STYLE);
|
|
742
1072
|
const lbl = mk("div", LABEL_STYLE);
|
|
@@ -846,6 +1176,52 @@ var TOOLBAR_STYLE = [
|
|
|
846
1176
|
"border-bottom:1px solid #333",
|
|
847
1177
|
"flex-shrink:0"
|
|
848
1178
|
].join(";");
|
|
1179
|
+
var SCENES_BAR_STYLE = [
|
|
1180
|
+
"display:flex",
|
|
1181
|
+
"align-items:center",
|
|
1182
|
+
"gap:4px",
|
|
1183
|
+
"padding:4px 8px",
|
|
1184
|
+
"background:#181818",
|
|
1185
|
+
"border-bottom:1px solid #333",
|
|
1186
|
+
"flex-shrink:0",
|
|
1187
|
+
"overflow-x:auto"
|
|
1188
|
+
].join(";");
|
|
1189
|
+
var SCENE_TAB_STYLE = [
|
|
1190
|
+
"background:#252525",
|
|
1191
|
+
"color:#aaa",
|
|
1192
|
+
"border:1px solid #444",
|
|
1193
|
+
"border-radius:4px 4px 0 0",
|
|
1194
|
+
"padding:3px 10px",
|
|
1195
|
+
"cursor:pointer",
|
|
1196
|
+
"font-size:10px",
|
|
1197
|
+
"font-family:monospace",
|
|
1198
|
+
"white-space:nowrap"
|
|
1199
|
+
].join(";");
|
|
1200
|
+
var SCENE_TAB_ACTIVE = ";background:#2a2d3a;color:#9cdcfe;border-color:#5577aa;border-bottom-color:#2a2d3a;";
|
|
1201
|
+
var SCENE_PLAY_BTN_STYLE = [
|
|
1202
|
+
"background:#252525",
|
|
1203
|
+
"color:#555",
|
|
1204
|
+
"border:1px solid #444",
|
|
1205
|
+
"border-radius:0 4px 0 0",
|
|
1206
|
+
"padding:3px 6px",
|
|
1207
|
+
"cursor:pointer",
|
|
1208
|
+
"font-size:9px",
|
|
1209
|
+
"font-family:monospace",
|
|
1210
|
+
"white-space:nowrap",
|
|
1211
|
+
"line-height:1"
|
|
1212
|
+
].join(";");
|
|
1213
|
+
var SCENE_PLAY_BTN_ACTIVE = ";background:#1a3a1a;color:#4ec94e;border-color:#3a7a3a;";
|
|
1214
|
+
var SCENE_BAR_BTN_STYLE = [
|
|
1215
|
+
"background:transparent",
|
|
1216
|
+
"color:#888",
|
|
1217
|
+
"border:1px solid #444",
|
|
1218
|
+
"border-radius:3px",
|
|
1219
|
+
"padding:2px 6px",
|
|
1220
|
+
"cursor:pointer",
|
|
1221
|
+
"font-size:11px",
|
|
1222
|
+
"font-family:monospace",
|
|
1223
|
+
"flex-shrink:0"
|
|
1224
|
+
].join(";");
|
|
849
1225
|
var BTN_STYLE = [
|
|
850
1226
|
"background:#2a2a2a",
|
|
851
1227
|
"color:#ccc",
|
|
@@ -873,6 +1249,14 @@ var CARD_STYLE = [
|
|
|
873
1249
|
"font-family:monospace",
|
|
874
1250
|
"font-size:10px"
|
|
875
1251
|
].join(";");
|
|
1252
|
+
var SECTION_HEADER_STYLE = [
|
|
1253
|
+
"display:flex",
|
|
1254
|
+
"align-items:center",
|
|
1255
|
+
"gap:6px",
|
|
1256
|
+
"margin:14px 0 6px",
|
|
1257
|
+
"padding-bottom:4px",
|
|
1258
|
+
"border-bottom:1px solid #2a2a2a"
|
|
1259
|
+
].join(";");
|
|
876
1260
|
var ROW_STYLE = ["display:flex", "gap:6px", "margin-bottom:8px"].join(";");
|
|
877
1261
|
var LABEL_STYLE = [
|
|
878
1262
|
"color:#888",
|
|
@@ -1533,6 +1917,17 @@ var TopBar = class _TopBar {
|
|
|
1533
1917
|
};
|
|
1534
1918
|
this.root.appendChild(btn);
|
|
1535
1919
|
}
|
|
1920
|
+
/** Add the Room manager button. */
|
|
1921
|
+
addRoomButton(panel) {
|
|
1922
|
+
const btn = document.createElement("button");
|
|
1923
|
+
btn.textContent = "\u{1F6AA} Rooms";
|
|
1924
|
+
btn.style.cssText = BTN_STYLE3;
|
|
1925
|
+
btn.onclick = () => {
|
|
1926
|
+
panel.toggle();
|
|
1927
|
+
btn.style.cssText = panel.isVisible ? BTN_ACTIVE_STYLE : BTN_STYLE3;
|
|
1928
|
+
};
|
|
1929
|
+
this.root.appendChild(btn);
|
|
1930
|
+
}
|
|
1536
1931
|
mkToggleBtn(label, panel) {
|
|
1537
1932
|
return this.mkPanelToggleBtn(label, panel);
|
|
1538
1933
|
}
|
|
@@ -1730,6 +2125,8 @@ export {
|
|
|
1730
2125
|
SceneSection,
|
|
1731
2126
|
SceneStore,
|
|
1732
2127
|
TopBar,
|
|
2128
|
+
createDefaultMultiSceneFile,
|
|
2129
|
+
createDefaultNamedScene,
|
|
1733
2130
|
createDefaultScene,
|
|
1734
2131
|
createDefaultSceneItem,
|
|
1735
2132
|
createDefaultSpec
|