863 lines
26 KiB
JavaScript
863 lines
26 KiB
JavaScript
const BROWSER_SUPPORT_ADAPTERS = [
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Reference glTF renderer and viewer module wired into the first-party browser runtime.',
|
|
id: '1e-gltf-sample-renderer',
|
|
phase: '1E',
|
|
repo: 'KhronosGroup/glTF-Sample-Renderer',
|
|
sourcePathHint: '.external/glTF-Sample-Renderer'
|
|
},
|
|
{
|
|
activation: 'native-sidecar',
|
|
description: 'Native/OpenCV solver adjunct retained as a browser-shell fallback lane rather than a direct npm package.',
|
|
id: '1g-tentone-native-solver',
|
|
phase: '1G',
|
|
repo: 'tentone/rubix-solver',
|
|
sourcePathHint: '.external/rubix-solver'
|
|
},
|
|
{
|
|
activation: 'source-sidecar',
|
|
description: 'Rust donor retained as a source-sidecar surface for follow-up browser spatial composition.',
|
|
id: '1h-code-vr',
|
|
phase: '1H',
|
|
repo: 'NuiLab/code-vr',
|
|
sourcePathHint: '.external/code-vr'
|
|
},
|
|
{
|
|
activation: 'prebuilt-script',
|
|
description: 'Prebuilt ClayGL runtime script bridged into the browser shell for viewer and QA surfaces.',
|
|
id: '1h-claygl',
|
|
phase: '1H',
|
|
repo: 'pissang/claygl',
|
|
sourcePathHint: '.external/claygl/dist/claygl.js'
|
|
},
|
|
{
|
|
activation: 'prebuilt-script',
|
|
description: 'Prebuilt Clay Viewer shell loaded beside ClayGL for bounded browser inspection.',
|
|
id: '1h-clay-viewer',
|
|
phase: '1H',
|
|
repo: 'pissang/clay-viewer',
|
|
sourcePathHint: '.external/clay-viewer/dist/clay-viewer.js'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Three.js post-processing core bundled into the runtime module graph.',
|
|
id: '1h-postprocessing',
|
|
phase: '1H',
|
|
repo: 'pmndrs/postprocessing',
|
|
sourcePathHint: '.external/postprocessing'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'React bridge for post-processing loaded into the browser spatial stack.',
|
|
id: '1h-react-postprocessing',
|
|
phase: '1H',
|
|
repo: 'pmndrs/react-postprocessing',
|
|
sourcePathHint: '.external/react-postprocessing'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Drei helper surface bundled for browser-spatial utility composition.',
|
|
id: '1h-drei',
|
|
phase: '1H',
|
|
repo: 'pmndrs/drei',
|
|
sourcePathHint: '.external/drei'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'World-anchored UI kit bundled for browser-spatial HUD and control surfaces.',
|
|
id: '1h-uikit',
|
|
phase: '1H',
|
|
repo: 'pmndrs/uikit',
|
|
sourcePathHint: '.external/uikit'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Three.js helper library bundled for browser runtime support.',
|
|
id: '1h-three-stdlib',
|
|
phase: '1H',
|
|
repo: 'pmndrs/three-stdlib',
|
|
sourcePathHint: '.external/three-stdlib'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Animation and math helper pack bundled into the runtime.',
|
|
id: '1h-maath',
|
|
phase: '1H',
|
|
repo: 'pmndrs/maath',
|
|
sourcePathHint: '.external/maath'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'State-store runtime bundled for browser shell state and derived panel demos.',
|
|
id: '1h-zustand',
|
|
phase: '1H',
|
|
repo: 'pmndrs/zustand',
|
|
sourcePathHint: '.external/zustand'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Parameter-control runtime bundled for future tuning overlays.',
|
|
id: '1h-leva',
|
|
phase: '1H',
|
|
repo: 'pmndrs/leva',
|
|
sourcePathHint: '.external/leva'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Gesture capture hooks bundled for browser interaction surfaces.',
|
|
id: '1h-use-gesture',
|
|
phase: '1H',
|
|
repo: 'pmndrs/use-gesture',
|
|
sourcePathHint: '.external/use-gesture'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Motion and interpolation runtime bundled for browser spatial transitions.',
|
|
id: '1h-react-spring',
|
|
phase: '1H',
|
|
repo: 'pmndrs/react-spring',
|
|
sourcePathHint: '.external/react-spring'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: '2D render engine bundled beneath the analytics surface.',
|
|
id: '1i-zrender',
|
|
phase: '1I',
|
|
repo: 'ecomfe/zrender',
|
|
sourcePathHint: '.external/zrender'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: '3D analytics extension bundled beneath the ECharts runtime.',
|
|
id: '1i-echarts-gl',
|
|
phase: '1I',
|
|
repo: 'ecomfe/echarts-gl',
|
|
sourcePathHint: '.external/echarts-gl'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Model Viewer web component bundled into the browser shell.',
|
|
id: '1j-model-viewer',
|
|
phase: '1J',
|
|
repo: 'google/model-viewer/packages/model-viewer',
|
|
sourcePathHint: '.external/model-viewer/packages/model-viewer'
|
|
},
|
|
{
|
|
activation: 'bundled-module',
|
|
description: 'Effects package bundled beside model-viewer for decorated previews.',
|
|
id: '1j-model-viewer-effects',
|
|
phase: '1J',
|
|
repo: 'google/model-viewer/packages/model-viewer-effects',
|
|
sourcePathHint: '.external/model-viewer/packages/model-viewer-effects'
|
|
},
|
|
{
|
|
activation: 'editor-sidecar',
|
|
description: 'Editor-oriented inspection UI retained as a sidecar instead of a default runtime export.',
|
|
id: '1j-space-opera',
|
|
phase: '1J',
|
|
repo: 'google/model-viewer/packages/space-opera',
|
|
sourcePathHint: '.external/model-viewer/packages/space-opera'
|
|
},
|
|
{
|
|
activation: 'qa-sidecar',
|
|
description: 'Renderer comparison and fidelity tooling kept in the QA lane rather than loaded by default.',
|
|
id: '1j-render-fidelity-tools',
|
|
phase: '1J',
|
|
repo: 'google/model-viewer/packages/render-fidelity-tools',
|
|
sourcePathHint: '.external/model-viewer/packages/render-fidelity-tools'
|
|
}
|
|
];
|
|
|
|
const BUNDLED_ADAPTER_ACTIVATIONS = new Set(['bundled-module', 'prebuilt-script']);
|
|
|
|
function resolveFallbackBootState()
|
|
{
|
|
const ExistingBootState = window.HyperTwistBrowserBootState;
|
|
if (ExistingBootState && typeof ExistingBootState === 'object')
|
|
{
|
|
return ExistingBootState;
|
|
}
|
|
|
|
const BootState = {};
|
|
window.HyperTwistBrowserBootState = BootState;
|
|
return BootState;
|
|
}
|
|
|
|
function getUnrealBridgeHandle()
|
|
{
|
|
const Candidate = window.ue;
|
|
if (!Candidate || typeof Candidate !== 'object')
|
|
{
|
|
return null;
|
|
}
|
|
|
|
const HyperTwistBridge = Candidate.hypertwist;
|
|
return HyperTwistBridge && typeof HyperTwistBridge === 'object'
|
|
? HyperTwistBridge
|
|
: null;
|
|
}
|
|
|
|
function tryParseJsonPayload(Value)
|
|
{
|
|
if (typeof Value !== 'string')
|
|
{
|
|
return Value;
|
|
}
|
|
|
|
try
|
|
{
|
|
return JSON.parse(Value);
|
|
}
|
|
catch
|
|
{
|
|
return Value;
|
|
}
|
|
}
|
|
|
|
function createElement(TagName, ClassName, TextContent)
|
|
{
|
|
const Element = document.createElement(TagName);
|
|
if (ClassName)
|
|
{
|
|
Element.className = ClassName;
|
|
}
|
|
if (typeof TextContent === 'string')
|
|
{
|
|
Element.textContent = TextContent;
|
|
}
|
|
return Element;
|
|
}
|
|
|
|
function serializePretty(Payload)
|
|
{
|
|
if (typeof Payload === 'string')
|
|
{
|
|
return Payload;
|
|
}
|
|
|
|
try
|
|
{
|
|
return JSON.stringify(Payload, null, 2);
|
|
}
|
|
catch
|
|
{
|
|
return String(Payload);
|
|
}
|
|
}
|
|
|
|
function createSectionHeader(Title, Note)
|
|
{
|
|
const Header = createElement('div', 'section-header');
|
|
Header.append(
|
|
createElement('h2', 'section-title', Title),
|
|
createElement('p', 'section-note', Note)
|
|
);
|
|
return Header;
|
|
}
|
|
|
|
function dispatchToListeners(Listeners, Payload)
|
|
{
|
|
Listeners.forEach((Listener) => Listener(Payload));
|
|
}
|
|
|
|
function createFallbackBridge(BootState)
|
|
{
|
|
const CommandListeners = new Set();
|
|
const ShellStateListeners = new Set();
|
|
let LastCommandPayload;
|
|
let LastShellStatePayload;
|
|
let HasLastCommandPayload = false;
|
|
let HasLastShellStatePayload = false;
|
|
|
|
function postEnvelope(Type, Payload)
|
|
{
|
|
const Envelope = {
|
|
type: Type,
|
|
payload: Payload,
|
|
emittedAtUtc: new Date().toISOString()
|
|
};
|
|
|
|
const SerializedEnvelope = JSON.stringify(Envelope);
|
|
const UnrealBridgeHandle = getUnrealBridgeHandle();
|
|
let DeliveredToUnreal = false;
|
|
|
|
if (UnrealBridgeHandle && typeof UnrealBridgeHandle.notifyEnvelope === 'function')
|
|
{
|
|
UnrealBridgeHandle.notifyEnvelope(SerializedEnvelope);
|
|
DeliveredToUnreal = true;
|
|
}
|
|
|
|
if (Type === 'hypertwist-state'
|
|
&& UnrealBridgeHandle
|
|
&& typeof UnrealBridgeHandle.notifyState === 'function')
|
|
{
|
|
UnrealBridgeHandle.notifyState(JSON.stringify(Payload));
|
|
DeliveredToUnreal = true;
|
|
}
|
|
|
|
if (DeliveredToUnreal)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (window.parent && typeof window.parent.postMessage === 'function')
|
|
{
|
|
window.parent.postMessage(Envelope, '*');
|
|
}
|
|
}
|
|
|
|
window.addEventListener('message', function onBridgeMessage(Event)
|
|
{
|
|
const Envelope = Event.data;
|
|
if (!Envelope || typeof Envelope !== 'object')
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Envelope.type === 'hypertwist-command')
|
|
{
|
|
Bridge.receiveCommand(Envelope.payload);
|
|
}
|
|
else if (Envelope.type === 'hypertwist-shell-state')
|
|
{
|
|
Bridge.setShellState(Envelope.payload);
|
|
}
|
|
});
|
|
|
|
const Bridge = {
|
|
onCommand(Listener)
|
|
{
|
|
CommandListeners.add(Listener);
|
|
if (HasLastCommandPayload)
|
|
{
|
|
Listener(LastCommandPayload);
|
|
}
|
|
return function unsubscribeCommand()
|
|
{
|
|
CommandListeners.delete(Listener);
|
|
};
|
|
},
|
|
onShellState(Listener)
|
|
{
|
|
ShellStateListeners.add(Listener);
|
|
if (HasLastShellStatePayload)
|
|
{
|
|
Listener(LastShellStatePayload);
|
|
}
|
|
return function unsubscribeShellState()
|
|
{
|
|
ShellStateListeners.delete(Listener);
|
|
};
|
|
},
|
|
receiveCommand(Payload)
|
|
{
|
|
LastCommandPayload = tryParseJsonPayload(Payload);
|
|
HasLastCommandPayload = true;
|
|
BootState.commandReceiveCount = (BootState.commandReceiveCount || 0) + 1;
|
|
BootState.lastCommandReceivedAtUtc = new Date().toISOString();
|
|
dispatchToListeners(CommandListeners, LastCommandPayload);
|
|
},
|
|
setShellState(Payload)
|
|
{
|
|
LastShellStatePayload = tryParseJsonPayload(Payload);
|
|
HasLastShellStatePayload = true;
|
|
BootState.shellStateReceiveCount = (BootState.shellStateReceiveCount || 0) + 1;
|
|
BootState.lastShellStateReceivedAtUtc = new Date().toISOString();
|
|
dispatchToListeners(ShellStateListeners, LastShellStatePayload);
|
|
},
|
|
sendState(Payload)
|
|
{
|
|
postEnvelope('hypertwist-state', Payload);
|
|
},
|
|
notifyRuntimeReady(Payload)
|
|
{
|
|
const UnrealBridgeHandle = getUnrealBridgeHandle();
|
|
if (UnrealBridgeHandle && typeof UnrealBridgeHandle.notifyRuntimeReady === 'function')
|
|
{
|
|
UnrealBridgeHandle.notifyRuntimeReady(JSON.stringify(Payload));
|
|
return;
|
|
}
|
|
|
|
postEnvelope('hypertwist-runtime-ready', Payload);
|
|
}
|
|
};
|
|
|
|
return Bridge;
|
|
}
|
|
|
|
async function loadAdapter(AdapterId)
|
|
{
|
|
const Adapter = BROWSER_SUPPORT_ADAPTERS.find((Candidate) => Candidate.id === AdapterId);
|
|
if (!Adapter)
|
|
{
|
|
throw new Error('Unknown browser support adapter: ' + AdapterId);
|
|
}
|
|
|
|
if (BUNDLED_ADAPTER_ACTIVATIONS.has(Adapter.activation))
|
|
{
|
|
return {
|
|
details: ['Clean checkout fallback is active; build the browser bundle to exercise this adapter in-browser.'],
|
|
id: AdapterId,
|
|
note: 'Fallback runtime is active, so bundled browser modules are advertised but not loaded from source checkout state.',
|
|
status: 'deferred'
|
|
};
|
|
}
|
|
|
|
return {
|
|
details: [Adapter.sourcePathHint],
|
|
id: AdapterId,
|
|
note: 'Adapter is intentionally represented as a sidecar in the plain-JS fallback shell.',
|
|
status: 'loaded'
|
|
};
|
|
}
|
|
|
|
function createHeroSection(BootState)
|
|
{
|
|
const HeroElement = createElement('section', 'hero');
|
|
HeroElement.append(
|
|
createElement('h1', undefined, 'HyperTwist Browser Runtime'),
|
|
createElement(
|
|
'p',
|
|
undefined,
|
|
'This clean-checkout fallback keeps the embedded browser lane truthful and runnable. When the built runtime bundle is present the authoritative shell upgrades automatically; when it is absent, the shell still renders, bridges Unreal state, and surfaces which adapters remain bundled or sidecar-only.'
|
|
)
|
|
);
|
|
|
|
const HeroMetrics = createElement('div', 'hero-grid');
|
|
[
|
|
{ label: 'Runtime mode', value: 'plain-js fallback' },
|
|
{ label: 'Fallback reason', value: BootState.fallbackReason || 'bundle unavailable' },
|
|
{ label: 'Adapters surfaced', value: String(BROWSER_SUPPORT_ADAPTERS.length) }
|
|
].forEach(function appendMetric(Metric)
|
|
{
|
|
const MetricElement = createElement('div', 'metric');
|
|
MetricElement.append(
|
|
createElement('strong', undefined, Metric.label),
|
|
createElement('span', undefined, Metric.value)
|
|
);
|
|
HeroMetrics.appendChild(MetricElement);
|
|
});
|
|
|
|
HeroElement.appendChild(HeroMetrics);
|
|
return HeroElement;
|
|
}
|
|
|
|
function createLogPanel()
|
|
{
|
|
const LogPanel = createElement('section', 'panel');
|
|
LogPanel.append(
|
|
createElement('h2', undefined, 'Runtime Log'),
|
|
createElement(
|
|
'p',
|
|
'runtime-note',
|
|
'Bridge and adapter events stay inspectable here during clean-checkout fallback mode.'
|
|
)
|
|
);
|
|
|
|
const LogList = createElement('div', 'log-list');
|
|
LogPanel.appendChild(LogList);
|
|
|
|
return {
|
|
appendLog(Title, Message)
|
|
{
|
|
const Entry = createElement('div', 'log-entry');
|
|
Entry.append(
|
|
createElement('strong', undefined, Title),
|
|
createElement('div', undefined, Message)
|
|
);
|
|
LogList.prepend(Entry);
|
|
},
|
|
panel: LogPanel
|
|
};
|
|
}
|
|
|
|
function buildRuntimeStatusSnapshot(BootState, Counters)
|
|
{
|
|
return {
|
|
runtimeStatusId: BootState.runtimeStatusId || 'runtime-ready',
|
|
runtimeMode: BootState.runtimeMode || 'unknown',
|
|
bootstrapStartedAtUtc: BootState.bootstrapStartedAtUtc || 'pending',
|
|
runtimeReadyAtUtc: BootState.runtimeReadyAtUtc || 'pending',
|
|
shellAuthority: BootState.shellAuthority || 'Content/Browser/index.html',
|
|
adapterCount: BootState.adapterCount || BROWSER_SUPPORT_ADAPTERS.length,
|
|
queuedCommandCountAtRuntimeReady: BootState.queuedCommandCountAtRuntimeReady || 0,
|
|
queuedShellStateCountAtRuntimeReady: BootState.queuedShellStateCountAtRuntimeReady || 0,
|
|
commandReceiveCount: Counters.commandReceiveCount,
|
|
shellStateReceiveCount: Counters.shellStateReceiveCount,
|
|
lastCommandReceivedAtUtc: BootState.lastCommandReceivedAtUtc || 'none',
|
|
lastShellStateReceivedAtUtc: BootState.lastShellStateReceivedAtUtc || 'none',
|
|
bridgeTrafficStatus: Counters.shellStateReceiveCount > 0
|
|
? 'shell-state-live'
|
|
: Counters.commandReceiveCount > 0
|
|
? 'command-only-live'
|
|
: 'awaiting-unreal-traffic',
|
|
fallbackReason: BootState.fallbackReason || null
|
|
};
|
|
}
|
|
|
|
function createRuntimeStatusPanel(BootState, Counters, UEBridge, AppendLog)
|
|
{
|
|
const StatusPanel = createElement('section', 'panel');
|
|
StatusPanel.append(
|
|
createElement('h2', undefined, 'Browser Runtime Status'),
|
|
createElement(
|
|
'p',
|
|
'runtime-note',
|
|
'The clean-checkout fallback shell now exposes first-party runtime mode, retained startup queue carryover, and live Unreal bridge traffic in one compact status surface.'
|
|
)
|
|
);
|
|
|
|
const StatusActions = createElement('div', 'solver-actions');
|
|
const SendStatusButton = createElement('button', undefined, 'Send Runtime Status');
|
|
const StatusData = createElement('div', 'data-block');
|
|
const StatusPre = createElement('pre');
|
|
StatusData.appendChild(StatusPre);
|
|
StatusActions.appendChild(SendStatusButton);
|
|
StatusPanel.append(StatusActions, StatusData);
|
|
|
|
function refresh()
|
|
{
|
|
StatusPre.textContent = serializePretty(buildRuntimeStatusSnapshot(BootState, Counters));
|
|
}
|
|
|
|
SendStatusButton.addEventListener('click', function onSendStatus()
|
|
{
|
|
UEBridge.sendState({
|
|
type: 'browser-runtime-status',
|
|
source: 'browser-runtime-fallback',
|
|
status: buildRuntimeStatusSnapshot(BootState, Counters)
|
|
});
|
|
AppendLog('Browser Runtime Status', 'Sent the current browser runtime status envelope to Unreal.');
|
|
});
|
|
|
|
return {
|
|
panel: StatusPanel,
|
|
refresh
|
|
};
|
|
}
|
|
|
|
function createStatePanel()
|
|
{
|
|
const StatePanel = createElement('section', 'panel');
|
|
StatePanel.append(
|
|
createElement('h2', undefined, 'Browser Shell State'),
|
|
createElement(
|
|
'p',
|
|
'runtime-note',
|
|
'Unreal can push shell-state and command envelopes into the fallback runtime exactly like the bundled runtime.'
|
|
)
|
|
);
|
|
|
|
const StateData = createElement('div', 'data-block');
|
|
const StatePre = createElement('pre');
|
|
StatePre.textContent = '{\n "status": "waiting-for-unreal-shell-state"\n}';
|
|
StateData.appendChild(StatePre);
|
|
StatePanel.appendChild(StateData);
|
|
|
|
return {
|
|
panel: StatePanel,
|
|
setPayload(Payload)
|
|
{
|
|
StatePre.textContent = serializePretty(Payload);
|
|
}
|
|
};
|
|
}
|
|
|
|
function createAdapterCard(Adapter, UEBridge, AppendLog)
|
|
{
|
|
const CardElement = createElement('article', 'card');
|
|
const CardHeader = createElement('div', 'card-header');
|
|
const CardTitleBlock = createElement('div');
|
|
CardTitleBlock.append(
|
|
createElement('span', 'phase-chip', Adapter.phase),
|
|
createElement('h2', undefined, Adapter.repo)
|
|
);
|
|
|
|
const StatusChip = createElement('span', 'status-chip', 'idle');
|
|
StatusChip.dataset.status = 'idle';
|
|
CardHeader.append(CardTitleBlock, StatusChip);
|
|
|
|
const ActivationChip = createElement('span', 'activation-chip', Adapter.activation);
|
|
const Description = createElement('p', undefined, Adapter.description);
|
|
const SourceCode = createElement('code', undefined, Adapter.sourcePathHint);
|
|
const Actions = createElement('div', 'card-actions');
|
|
const LoadButton = createElement('button', undefined, 'Resolve Adapter');
|
|
const EnvelopeButton = createElement('button', 'secondary', 'Send Envelope');
|
|
|
|
LoadButton.addEventListener('click', async function onLoadAdapter()
|
|
{
|
|
StatusChip.dataset.status = 'loading';
|
|
StatusChip.textContent = 'loading';
|
|
|
|
try
|
|
{
|
|
const Result = await loadAdapter(Adapter.id);
|
|
StatusChip.dataset.status = Result.status === 'loaded' ? 'ready' : 'idle';
|
|
StatusChip.textContent = Result.status;
|
|
AppendLog(
|
|
Adapter.repo,
|
|
Result.note || ('Resolved ' + Adapter.id + '.')
|
|
);
|
|
}
|
|
catch (Error)
|
|
{
|
|
StatusChip.dataset.status = 'error';
|
|
StatusChip.textContent = 'error';
|
|
AppendLog(Adapter.repo, Error instanceof Error ? Error.message : 'Unknown load failure.');
|
|
}
|
|
});
|
|
|
|
EnvelopeButton.addEventListener('click', function onSendEnvelope()
|
|
{
|
|
UEBridge.sendState({
|
|
source: 'browser-runtime-fallback',
|
|
adapterId: Adapter.id,
|
|
repo: Adapter.repo,
|
|
activation: Adapter.activation,
|
|
runtimeMode: 'fallback-js'
|
|
});
|
|
AppendLog(Adapter.repo, 'Sent adapter envelope to Unreal.');
|
|
});
|
|
|
|
Actions.append(LoadButton, EnvelopeButton);
|
|
CardElement.append(CardHeader, ActivationChip, Description, SourceCode, Actions);
|
|
return CardElement;
|
|
}
|
|
|
|
function createAdapterSection(UEBridge, AppendLog)
|
|
{
|
|
const AdapterSection = createElement('section', 'section');
|
|
AdapterSection.appendChild(
|
|
createSectionHeader(
|
|
'Adapter Surface',
|
|
'Adapters remain visible in clean checkout state. Bundled browser modules are marked deferred until `npm run build` produces the browser runtime bundle.'
|
|
)
|
|
);
|
|
|
|
const AdapterGrid = createElement('div', 'stack-grid');
|
|
BROWSER_SUPPORT_ADAPTERS.forEach(function appendAdapter(Adapter)
|
|
{
|
|
AdapterGrid.appendChild(createAdapterCard(Adapter, UEBridge, AppendLog));
|
|
});
|
|
AdapterSection.append(AdapterGrid);
|
|
|
|
return AdapterSection;
|
|
}
|
|
|
|
function createSolverPanel(UEBridge, AppendLog)
|
|
{
|
|
const SolverPanel = createElement('section', 'panel');
|
|
SolverPanel.append(
|
|
createElement('h2', undefined, 'Tentone Native Solver Request'),
|
|
createElement(
|
|
'p',
|
|
'runtime-note',
|
|
'The OpenCV donor remains a native-sidecar lane. This fallback shell sends a first-party solver request envelope instead of pretending to import the donor in-browser.'
|
|
)
|
|
);
|
|
|
|
const SolverField = createElement('label', 'field');
|
|
SolverField.appendChild(createElement('span', undefined, 'Classic facelet string'));
|
|
const SolverInput = createElement('textarea');
|
|
SolverInput.value = 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB';
|
|
SolverField.appendChild(SolverInput);
|
|
|
|
const SolverActions = createElement('div', 'solver-actions');
|
|
const SolverButton = createElement('button', undefined, 'Send Solver Request');
|
|
SolverButton.addEventListener('click', function onSolve()
|
|
{
|
|
UEBridge.sendState({
|
|
type: 'tentone-native-solver-request',
|
|
source: 'browser-runtime-fallback',
|
|
faceletString: SolverInput.value.trim()
|
|
});
|
|
AppendLog('Tentone Native Solver', 'Sent a solver request envelope to Unreal.');
|
|
});
|
|
SolverActions.appendChild(SolverButton);
|
|
SolverPanel.append(SolverField, SolverActions);
|
|
|
|
return SolverPanel;
|
|
}
|
|
|
|
function createUtilitySection(BootState, Counters, UEBridge)
|
|
{
|
|
const UtilitySection = createElement('section', 'section');
|
|
UtilitySection.appendChild(
|
|
createSectionHeader(
|
|
'Fallback Utilities',
|
|
'The plain-JS runtime keeps state bridging and native-sidecar requests live even when bundled browser modules have been cleaned after validation.'
|
|
)
|
|
);
|
|
|
|
const { appendLog, panel: LogPanel } = createLogPanel();
|
|
const { panel: StatusPanel, refresh: RefreshRuntimeStatus } = createRuntimeStatusPanel(
|
|
BootState,
|
|
Counters,
|
|
UEBridge,
|
|
appendLog
|
|
);
|
|
const StatePanel = createStatePanel();
|
|
const UtilityGrid = createElement('div', 'panel-grid');
|
|
UtilityGrid.append(
|
|
StatusPanel,
|
|
createSolverPanel(UEBridge, appendLog),
|
|
StatePanel.panel,
|
|
LogPanel
|
|
);
|
|
UtilitySection.appendChild(UtilityGrid);
|
|
|
|
return {
|
|
appendLog,
|
|
refreshRuntimeStatus: RefreshRuntimeStatus,
|
|
section: UtilitySection,
|
|
setShellStatePayload: StatePanel.setPayload
|
|
};
|
|
}
|
|
|
|
function synchronizeCommandCounter(BootState, Counters)
|
|
{
|
|
Counters.commandReceiveCount = BootState.commandReceiveCount || (Counters.commandReceiveCount + 1);
|
|
}
|
|
|
|
function synchronizeShellStateCounter(BootState, Counters)
|
|
{
|
|
Counters.shellStateReceiveCount = BootState.shellStateReceiveCount || (Counters.shellStateReceiveCount + 1);
|
|
}
|
|
|
|
function wireBridgeSubscriptions(BootState, Counters, UEBridge, AppendLog, RefreshRuntimeStatus, SetShellStatePayload)
|
|
{
|
|
UEBridge.onCommand(function onCommand(Payload)
|
|
{
|
|
synchronizeCommandCounter(BootState, Counters);
|
|
RefreshRuntimeStatus();
|
|
AppendLog('UE Command', serializePretty(Payload));
|
|
});
|
|
|
|
UEBridge.onShellState(function onShellState(Payload)
|
|
{
|
|
synchronizeShellStateCounter(BootState, Counters);
|
|
SetShellStatePayload(Payload);
|
|
RefreshRuntimeStatus();
|
|
AppendLog('Browser Shell State', 'Received fresh shell state from Unreal.');
|
|
});
|
|
}
|
|
|
|
function createBrowserShell(RootElement, BootState, UEBridge)
|
|
{
|
|
RootElement.innerHTML = '';
|
|
|
|
const Counters = {
|
|
commandReceiveCount: BootState.commandReceiveCount || 0,
|
|
shellStateReceiveCount: BootState.shellStateReceiveCount
|
|
|| (BootState.lastShellStateReceivedAtUtc ? 1 : 0)
|
|
};
|
|
|
|
const ShellElement = createElement('div', 'shell');
|
|
const HeroElement = createHeroSection(BootState);
|
|
const UtilitySection = createUtilitySection(BootState, Counters, UEBridge);
|
|
const AdapterSection = createAdapterSection(UEBridge, UtilitySection.appendLog);
|
|
|
|
ShellElement.append(HeroElement, AdapterSection, UtilitySection.section);
|
|
RootElement.appendChild(ShellElement);
|
|
UtilitySection.refreshRuntimeStatus();
|
|
|
|
wireBridgeSubscriptions(
|
|
BootState,
|
|
Counters,
|
|
UEBridge,
|
|
UtilitySection.appendLog,
|
|
UtilitySection.refreshRuntimeStatus,
|
|
UtilitySection.setShellStatePayload
|
|
);
|
|
}
|
|
|
|
function createFallbackRuntimeApi(BootState, UEBridge)
|
|
{
|
|
return {
|
|
adapters: BROWSER_SUPPORT_ADAPTERS,
|
|
listAdapters()
|
|
{
|
|
return BROWSER_SUPPORT_ADAPTERS.slice();
|
|
},
|
|
loadAdapter,
|
|
receiveCommand(Payload)
|
|
{
|
|
UEBridge.receiveCommand(Payload);
|
|
},
|
|
setShellState(Payload)
|
|
{
|
|
UEBridge.setShellState(Payload);
|
|
},
|
|
renderShell(RootElement)
|
|
{
|
|
createBrowserShell(RootElement, BootState, UEBridge);
|
|
},
|
|
sendState(Payload)
|
|
{
|
|
UEBridge.sendState(Payload);
|
|
}
|
|
};
|
|
}
|
|
|
|
function replayPendingBridgeTraffic(BootState, RuntimeApi)
|
|
{
|
|
const PendingCommands = Array.isArray(BootState.pendingCommands) ? BootState.pendingCommands.slice() : [];
|
|
BootState.queuedCommandCountAtRuntimeReady = PendingCommands.length;
|
|
BootState.pendingCommands = [];
|
|
PendingCommands.forEach(function replayPendingCommand(Payload)
|
|
{
|
|
RuntimeApi.receiveCommand(Payload);
|
|
});
|
|
|
|
const PendingShellStates = Array.isArray(BootState.pendingShellStates) ? BootState.pendingShellStates.slice() : [];
|
|
BootState.queuedShellStateCountAtRuntimeReady = PendingShellStates.length;
|
|
BootState.pendingShellStates = [];
|
|
PendingShellStates.forEach(function replayPendingShellState(Payload)
|
|
{
|
|
RuntimeApi.setShellState(Payload);
|
|
});
|
|
}
|
|
|
|
function notifyFallbackRuntimeReady(BootState, UEBridge)
|
|
{
|
|
UEBridge.notifyRuntimeReady({
|
|
status: 'ready',
|
|
runtime: 'hypertwist-browser-runtime-fallback',
|
|
mode: 'fallback-js',
|
|
fallbackReason: BootState.fallbackReason || 'bundle unavailable',
|
|
adapterCount: BootState.adapterCount,
|
|
shellAuthority: BootState.shellAuthority,
|
|
queuedCommandCountAtRuntimeReady: BootState.queuedCommandCountAtRuntimeReady,
|
|
queuedShellStateCountAtRuntimeReady: BootState.queuedShellStateCountAtRuntimeReady
|
|
});
|
|
}
|
|
|
|
function mountFallbackShell(RuntimeApi)
|
|
{
|
|
const RootElement = document.getElementById('app');
|
|
if (RootElement instanceof HTMLElement)
|
|
{
|
|
RuntimeApi.renderShell(RootElement);
|
|
}
|
|
}
|
|
|
|
function bootstrapHyperTwistBrowserFallbackRuntime()
|
|
{
|
|
const BootState = resolveFallbackBootState();
|
|
BootState.runtimeMode = 'fallback-js';
|
|
BootState.runtimeReadyAtUtc = new Date().toISOString();
|
|
BootState.runtimeStatusId = 'runtime-ready';
|
|
BootState.adapterCount = BROWSER_SUPPORT_ADAPTERS.length;
|
|
BootState.shellAuthority = 'Content/Browser/index.html';
|
|
|
|
const UEBridge = createFallbackBridge(BootState);
|
|
const RuntimeApi = createFallbackRuntimeApi(BootState, UEBridge);
|
|
window.HyperTwistBrowserRuntime = RuntimeApi;
|
|
|
|
mountFallbackShell(RuntimeApi);
|
|
replayPendingBridgeTraffic(BootState, RuntimeApi);
|
|
notifyFallbackRuntimeReady(BootState, UEBridge);
|
|
}
|
|
|
|
bootstrapHyperTwistBrowserFallbackRuntime();
|