openvibers/openvibe.games Commits

Games editor: a creator world shows no upload buttons and no Paint tool (both need uploads) (#93)

OP OpenVibers committed 28 days ago · c58f7202d9 · 5 files, +64 −36

The asset browser says 'Built-in materials only here' instead of Import texture / Import GLB, and the
toolbar leaves out Paint (its masks are uploads), so a creator never builds what the save would refuse.
Staff maps are unchanged.

Changes

ddiff --git a/apps/client/src/editor/assets/assetController.ts b/apps/client/src/editor/assets/assetController.ts
iindex bce6650..9bee1ad 100644
---- a/apps/client/src/editor/assets/assetController.ts
++++ b/apps/client/src/editor/assets/assetController.ts
@@@ -119,6 +119,11 @@ export function sanitizeTextureName(raw: string, taken: (name: string) => boolea
export class AssetController {
constructor(private readonly opts: AssetControllerOptions) {}
++ /** Whether this map takes uploads (a creator world does not, for now). */
++ get uploadsAllowed(): boolean {
++ return this.opts.assetsUrl !== null
++ }
++
/** POST raw bytes to the content-addressed store. */
private async upload(blob: Blob): Promise<UploadedAsset | 'forbidden' | null> {
const url = this.opts.assetsUrl === undefined ? '/api/map-assets' : this.opts.assetsUrl
ddiff --git a/apps/client/src/editor/editorApp.ts b/apps/client/src/editor/editorApp.ts
iindex e474626..5289b16 100644
---- a/apps/client/src/editor/editorApp.ts
++++ b/apps/client/src/editor/editorApp.ts
@@@ -101,7 +101,10 @@ function defaultEditorName(): string {
export async function bootEditor(): Promise<void> {
const canvas = document.getElementById('game') as HTMLCanvasElement
const mount = document.getElementById('shell') as HTMLElement
-- const shell = buildShell(mount, canvas)
++ // The place's map, or a creator world's (/editor?world=<id>: the owner's draft, built-in materials, one editor,
++ // no Paint: its masks would be uploads).
++ const endpoints = mapEndpoints()
++ const shell = buildShell(mount, canvas, endpoints.assets ? [] : ['paint'])
const engine = new Engine(canvas, true)
const scene = new Scene(engine)
@@@ -114,8 +117,6 @@ export async function bootEditor(): Promise<void> {
// /map.json is the v2 document, so the editor only ever holds v2 — and a
// missing or invalid map is emptyMapV2(), never a fabricated starter
// heightfield.
-- // The place's map, or a creator world's (/editor?world=<id>: the owner's draft, built-in materials, one editor).
-- const endpoints = mapEndpoints()
const bootResp = await fetchMap(endpoints)
const bootRevision = bootResp.headers.get('etag')?.replace(/"/g, '') ?? ''
const bootParsed = parseMapFile(await bootResp.json().catch(() => null))
ddiff --git a/apps/client/src/editor/ui/editorShell.ts b/apps/client/src/editor/ui/editorShell.ts
iindex a65d3e1..c862896 100644
---- a/apps/client/src/editor/ui/editorShell.ts
++++ b/apps/client/src/editor/ui/editorShell.ts
@@@ -100,13 +100,18 @@ const textureOptions = (): { value: string; label: string }[] => [
]
/** Build the workspace into `mount` and return every element the app drives. */
--export function buildShell(mount: HTMLElement, canvas: HTMLCanvasElement): ShellElements {
++/** `hideTools`: tools a mode cannot use (a creator world has no Paint: its masks would be uploads). */
++export function buildShell(
++ mount: HTMLElement,
++ canvas: HTMLCanvasElement,
++ hideTools: readonly Tool[] = [],
++): ShellElements {
const root = el('div', 'workspace', 'workspace')
// ── Toolbar ─────────────────────────────────────────────────────────
const toolbar = el('div', 'toolbar')
const tools = el('div', 'tools', 'tools')
-- for (const [id, icon, title] of TOOLS) {
++ for (const [id, icon, title] of TOOLS.filter(([t]) => !hideTools.includes(t))) {
const b = el('button', 'tool', `tool-${id}`)
b.type = 'button'
b.dataset['tool'] = id
ddiff --git a/apps/client/src/editor/ui/editorUi.ts b/apps/client/src/editor/ui/editorUi.ts
iindex 638019d..e503b57 100644
---- a/apps/client/src/editor/ui/editorUi.ts
++++ b/apps/client/src/editor/ui/editorUi.ts
@@@ -126,22 +126,26 @@ export function createEditorUi(opts: EditorUiOptions): EditorUi {
)
// ── Dock ────────────────────────────────────────────────────────────
-- const assets = new AssetBrowser(shell.dockPanels['assets']!, {
-- onUseTexture: (name) => {
-- const ids = selection.ids()
-- if (ids.length > 0) opts.setProperty(ids, 'tex', name)
-- else (document.getElementById('paint-tex') as HTMLSelectElement).value = name
++ const assets = new AssetBrowser(
++ shell.dockPanels['assets']!,
++ {
++ onUseTexture: (name) => {
++ const ids = selection.ids()
++ if (ids.length > 0) opts.setProperty(ids, 'tex', name)
++ else (document.getElementById('paint-tex') as HTMLSelectElement).value = name
++ },
++ // Genuinely arms the placement tool rather than telling the user to go
++ // and find it themselves.
++ onPlaceModel: (id) => opts.placeModel(id),
++ onFindUsages: (ids) => selection.replaceMany(ids),
++ onDeleteTexture: (name) => void opts.assets.deleteTexture(name),
++ onDeleteModel: (id) => void opts.assets.deleteModel(id),
++ onRenameTexture: (name, next) => void opts.assets.renameTexture(name, next),
++ onImportTexture: (file) => void opts.assets.importTexture(file),
++ onImportModel: (file) => void opts.assets.importModel(file),
},
-- // Genuinely arms the placement tool rather than telling the user to go
-- // and find it themselves.
-- onPlaceModel: (id) => opts.placeModel(id),
-- onFindUsages: (ids) => selection.replaceMany(ids),
-- onDeleteTexture: (name) => void opts.assets.deleteTexture(name),
-- onDeleteModel: (id) => void opts.assets.deleteModel(id),
-- onRenameTexture: (name, next) => void opts.assets.renameTexture(name, next),
-- onImportTexture: (file) => void opts.assets.importTexture(file),
-- onImportModel: (file) => void opts.assets.importModel(file),
-- })
++ opts.assets.uploadsAllowed,
++ )
const historyPanel = new HistoryPanel(shell.dockPanels['history']!, (steps) => {
for (let i = 0; i < Math.abs(steps); i++) history.undo()
ddiff --git a/apps/client/src/editor/ui/panels.ts b/apps/client/src/editor/ui/panels.ts
iindex 5227506..fd99a7e 100644
---- a/apps/client/src/editor/ui/panels.ts
++++ b/apps/client/src/editor/ui/panels.ts
@@@ -64,6 +64,8 @@ export class AssetBrowser {
constructor(
private readonly root: HTMLElement,
private readonly callbacks: AssetBrowserCallbacks,
++ /** false in a creator world: no import (uploads are staff-only there for now). */
++ private readonly uploads = true,
) {}
setState(next: Partial<AssetBrowserState>): void {
@@@ -71,21 +73,7 @@ export class AssetBrowser {
this.render()
}
-- private render(): void {
-- const { tab, filter } = this.state
-- this.root.textContent = ''
--
-- const tabs = document.createElement('div')
-- tabs.className = 'asset-tabs'
-- for (const name of ['textures', 'models'] as const) {
-- const b = document.createElement('button')
-- b.type = 'button'
-- b.textContent = name
-- b.className = tab === name ? 'active' : ''
-- b.addEventListener('click', () => this.setState({ tab: name }))
-- tabs.append(b)
-- }
-- // Import lives beside the list it adds to, so the flow is obvious.
++ private importControl(tab: 'textures' | 'models'): HTMLLabelElement {
const importLabel = document.createElement('label')
importLabel.className = 'mini asset-import'
importLabel.textContent = tab === 'textures' ? '+ Import texture' : '+ Import GLB'
@@@ -102,7 +90,32 @@ export class AssetBrowser {
importInput.value = ''
})
importLabel.append(importInput)
-- tabs.append(importLabel)
++ return importLabel
++ }
++
++ private render(): void {
++ const { tab, filter } = this.state
++ this.root.textContent = ''
++
++ const tabs = document.createElement('div')
++ tabs.className = 'asset-tabs'
++ for (const name of ['textures', 'models'] as const) {
++ const b = document.createElement('button')
++ b.type = 'button'
++ b.textContent = name
++ b.className = tab === name ? 'active' : ''
++ b.addEventListener('click', () => this.setState({ tab: name }))
++ tabs.append(b)
++ }
++ // Import lives beside the list it adds to, so the flow is obvious. A creator world says why there is none.
++ if (this.uploads) tabs.append(this.importControl(tab))
++ else {
++ const note = document.createElement('span')
++ note.className = 'mini asset-import'
++ note.textContent = 'Built-in materials only here'
++ note.title = 'Uploaded textures and models stay with staff maps for now.'
++ tabs.append(note)
++ }
this.root.append(tabs)
const needle = filter.trim().toLowerCase()