Repair browser shell and close more phase 3 runtime
This commit is contained in:
parent
7b5e56fe4e
commit
f529e8c9e9
28 changed files with 1738 additions and 87 deletions
|
|
@ -21,5 +21,5 @@ if errorlevel 1 (
|
|||
)
|
||||
|
||||
echo Browser runtime built successfully.
|
||||
echo Output: dist\hypertwist-browser-runtime.umd.js
|
||||
echo Copy to Content\Browser\ for UE embedded WebBrowser widget loading.
|
||||
echo Output: dist\browser-spatial-runtime.js
|
||||
echo Authoritative shell: index.html ^(loads dist bundle when present, plain-JS fallback when absent^).
|
||||
|
|
|
|||
|
|
@ -327,9 +327,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/browser-spatial-runtime.ts"
|
||||
></script>
|
||||
<script src="./src/browser-runtime-bootstrap.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"verify:shell": "node ./scripts/verify-browser-shell.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/model-viewer": "^4.3.1",
|
||||
|
|
|
|||
46
Content/Browser/scripts/verify-browser-shell.mjs
Normal file
46
Content/Browser/scripts/verify-browser-shell.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const browserRoot = path.resolve(import.meta.dirname, '..');
|
||||
const shellPath = path.join(browserRoot, 'index.html');
|
||||
const bootstrapPath = path.join(browserRoot, 'src', 'browser-runtime-bootstrap.js');
|
||||
const fallbackPath = path.join(browserRoot, 'src', 'browser-spatial-runtime-fallback.js');
|
||||
|
||||
const failures = [];
|
||||
|
||||
function assert(condition, message)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
failures.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
assert(existsSync(shellPath), `Missing authoritative browser shell: ${shellPath}`);
|
||||
assert(existsSync(bootstrapPath), `Missing bootstrap runtime script: ${bootstrapPath}`);
|
||||
assert(existsSync(fallbackPath), `Missing clean-checkout fallback runtime: ${fallbackPath}`);
|
||||
|
||||
if (existsSync(shellPath))
|
||||
{
|
||||
const shellHtml = readFileSync(shellPath, 'utf8');
|
||||
assert(
|
||||
shellHtml.includes('./src/browser-runtime-bootstrap.js'),
|
||||
'The authoritative browser shell must load the bootstrap runtime script.'
|
||||
);
|
||||
assert(
|
||||
!shellHtml.includes('browser-spatial-runtime.ts'),
|
||||
'The authoritative browser shell must not point directly at a TypeScript source entrypoint.'
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0)
|
||||
{
|
||||
for (const failure of failures)
|
||||
{
|
||||
console.error(failure);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('HyperTwist browser shell verification passed.');
|
||||
120
Content/Browser/src/browser-runtime-bootstrap.js
Normal file
120
Content/Browser/src/browser-runtime-bootstrap.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
(function bootstrapHyperTwistBrowserRuntime()
|
||||
{
|
||||
const GlobalWindow = window;
|
||||
const ExistingBootState = GlobalWindow.HyperTwistBrowserBootState;
|
||||
const BootState = ExistingBootState && typeof ExistingBootState === 'object'
|
||||
? ExistingBootState
|
||||
: {};
|
||||
|
||||
if (!Array.isArray(BootState.pendingCommands))
|
||||
{
|
||||
BootState.pendingCommands = [];
|
||||
}
|
||||
if (!Array.isArray(BootState.pendingShellStates))
|
||||
{
|
||||
BootState.pendingShellStates = [];
|
||||
}
|
||||
|
||||
BootState.bundleEntryUrl = new URL('./dist/browser-spatial-runtime.js', GlobalWindow.location.href).toString();
|
||||
BootState.fallbackEntryUrl = new URL('./src/browser-spatial-runtime-fallback.js', GlobalWindow.location.href).toString();
|
||||
BootState.runtimeMode = 'bootstrapping';
|
||||
BootState.bootstrapStartedAtUtc = new Date().toISOString();
|
||||
GlobalWindow.HyperTwistBrowserBootState = BootState;
|
||||
|
||||
if (!GlobalWindow.HyperTwistBrowserRuntime || typeof GlobalWindow.HyperTwistBrowserRuntime !== 'object')
|
||||
{
|
||||
GlobalWindow.HyperTwistBrowserRuntime = {
|
||||
adapters: [],
|
||||
listAdapters()
|
||||
{
|
||||
return [];
|
||||
},
|
||||
loadAdapter()
|
||||
{
|
||||
return Promise.resolve({
|
||||
id: 'bootstrap-stub',
|
||||
note: 'Runtime bundle still bootstrapping.',
|
||||
status: 'deferred'
|
||||
});
|
||||
},
|
||||
receiveCommand(Payload)
|
||||
{
|
||||
BootState.pendingCommands.push(Payload);
|
||||
},
|
||||
setShellState(Payload)
|
||||
{
|
||||
BootState.pendingShellStates.push(Payload);
|
||||
},
|
||||
renderShell()
|
||||
{
|
||||
},
|
||||
sendState()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let bFallbackRequested = false;
|
||||
|
||||
function loadFallbackRuntime(Reason)
|
||||
{
|
||||
if (bFallbackRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bFallbackRequested = true;
|
||||
BootState.runtimeMode = 'fallback-js';
|
||||
BootState.fallbackReason = Reason;
|
||||
|
||||
const FallbackScript = document.createElement('script');
|
||||
FallbackScript.src = BootState.fallbackEntryUrl;
|
||||
FallbackScript.defer = true;
|
||||
FallbackScript.dataset.hypertwistRuntime = 'fallback-js';
|
||||
document.head.appendChild(FallbackScript);
|
||||
}
|
||||
|
||||
const BundledRuntimeScript = document.createElement('script');
|
||||
BundledRuntimeScript.type = 'module';
|
||||
BundledRuntimeScript.src = BootState.bundleEntryUrl;
|
||||
BundledRuntimeScript.dataset.hypertwistRuntime = 'bundled-module';
|
||||
BundledRuntimeScript.onload = function onBundledRuntimeLoaded()
|
||||
{
|
||||
GlobalWindow.setTimeout(function verifyBundledRuntime()
|
||||
{
|
||||
if (GlobalWindow.HyperTwistBrowserBootState?.runtimeMode === 'bundled-module')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GlobalWindow.HyperTwistBrowserRuntime && GlobalWindow.HyperTwistBrowserRuntime.adapters?.length)
|
||||
{
|
||||
BootState.runtimeMode = 'bundled-module';
|
||||
return;
|
||||
}
|
||||
|
||||
loadFallbackRuntime('bundle-loaded-without-runtime');
|
||||
}, 200);
|
||||
};
|
||||
BundledRuntimeScript.onerror = function onBundledRuntimeFailed()
|
||||
{
|
||||
loadFallbackRuntime('bundle-missing-or-failed');
|
||||
};
|
||||
document.head.appendChild(BundledRuntimeScript);
|
||||
|
||||
GlobalWindow.setTimeout(function enforceRuntimeAvailability()
|
||||
{
|
||||
if (GlobalWindow.HyperTwistBrowserBootState?.runtimeMode === 'bundled-module')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GlobalWindow.HyperTwistBrowserRuntime && GlobalWindow.HyperTwistBrowserRuntime.adapters?.length)
|
||||
{
|
||||
BootState.runtimeMode = 'bundled-module';
|
||||
return;
|
||||
}
|
||||
|
||||
loadFallbackRuntime('bundle-timeout');
|
||||
}, 2000);
|
||||
})();
|
||||
649
Content/Browser/src/browser-spatial-runtime-fallback.js
Normal file
649
Content/Browser/src/browser-spatial-runtime-fallback.js
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
(function bootstrapHyperTwistBrowserFallbackRuntime()
|
||||
{
|
||||
const BootState = window.HyperTwistBrowserBootState && typeof window.HyperTwistBrowserBootState === 'object'
|
||||
? window.HyperTwistBrowserBootState
|
||||
: {};
|
||||
window.HyperTwistBrowserBootState = BootState;
|
||||
BootState.runtimeMode = 'fallback-js';
|
||||
BootState.runtimeReadyAtUtc = new Date().toISOString();
|
||||
|
||||
const BrowserSupportAdapters = [
|
||||
{
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const CommandListeners = new Set();
|
||||
const ShellStateListeners = new Set();
|
||||
let LastCommandPayload;
|
||||
let LastShellStatePayload;
|
||||
let HasLastCommandPayload = false;
|
||||
let HasLastShellStatePayload = false;
|
||||
|
||||
function dispatchToListeners(Listeners, Payload)
|
||||
{
|
||||
Listeners.forEach((Listener) => Listener(Payload));
|
||||
}
|
||||
|
||||
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')
|
||||
{
|
||||
runtimeApi.receiveCommand(Envelope.payload);
|
||||
}
|
||||
else if (Envelope.type === 'hypertwist-shell-state')
|
||||
{
|
||||
runtimeApi.setShellState(Envelope.payload);
|
||||
}
|
||||
});
|
||||
|
||||
const UEBridge = {
|
||||
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;
|
||||
dispatchToListeners(CommandListeners, LastCommandPayload);
|
||||
},
|
||||
setShellState(Payload)
|
||||
{
|
||||
LastShellStatePayload = tryParseJsonPayload(Payload);
|
||||
HasLastShellStatePayload = true;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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 appendLog(LogList, Title, Message)
|
||||
{
|
||||
const Entry = createElement('div', 'log-entry');
|
||||
Entry.append(
|
||||
createElement('strong', undefined, Title),
|
||||
createElement('div', undefined, Message)
|
||||
);
|
||||
LogList.prepend(Entry);
|
||||
}
|
||||
|
||||
async function loadAdapter(AdapterId)
|
||||
{
|
||||
const Adapter = BrowserSupportAdapters.find((Candidate) => Candidate.id === AdapterId);
|
||||
if (!Adapter)
|
||||
{
|
||||
throw new Error('Unknown browser support adapter: ' + AdapterId);
|
||||
}
|
||||
|
||||
const BundledActivations = new Set(['bundled-module', 'prebuilt-script']);
|
||||
if (BundledActivations.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 createBrowserShell(RootElement)
|
||||
{
|
||||
RootElement.innerHTML = '';
|
||||
|
||||
const ShellElement = createElement('div', 'shell');
|
||||
const HeroElement = createElement('section', 'hero');
|
||||
const HeroTitle = createElement('h1', undefined, 'HyperTwist Browser Runtime');
|
||||
const HeroParagraph = 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(BrowserSupportAdapters.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.append(HeroTitle, HeroParagraph, HeroMetrics);
|
||||
|
||||
const AdapterSection = createElement('section', 'section');
|
||||
const AdapterHeader = createElement('div', 'section-header');
|
||||
AdapterHeader.append(
|
||||
createElement('h2', 'section-title', 'Adapter Surface'),
|
||||
createElement(
|
||||
'p',
|
||||
'section-note',
|
||||
'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');
|
||||
BrowserSupportAdapters.forEach(function appendAdapter(Adapter)
|
||||
{
|
||||
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(
|
||||
LogList,
|
||||
Adapter.repo,
|
||||
Result.note || ('Resolved ' + Adapter.id + '.')
|
||||
);
|
||||
}
|
||||
catch (Error)
|
||||
{
|
||||
StatusChip.dataset.status = 'error';
|
||||
StatusChip.textContent = 'error';
|
||||
appendLog(LogList, 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(LogList, Adapter.repo, 'Sent adapter envelope to Unreal.');
|
||||
});
|
||||
|
||||
Actions.append(LoadButton, EnvelopeButton);
|
||||
CardElement.append(CardHeader, ActivationChip, Description, SourceCode, Actions);
|
||||
AdapterGrid.appendChild(CardElement);
|
||||
});
|
||||
|
||||
AdapterSection.append(AdapterHeader, AdapterGrid);
|
||||
|
||||
const UtilitySection = createElement('section', 'section');
|
||||
const UtilityHeader = createElement('div', 'section-header');
|
||||
UtilityHeader.append(
|
||||
createElement('h2', 'section-title', 'Fallback Utilities'),
|
||||
createElement(
|
||||
'p',
|
||||
'section-note',
|
||||
'The plain-JS runtime keeps state bridging and native-sidecar requests live even when bundled browser modules have been cleaned after validation.'
|
||||
)
|
||||
);
|
||||
|
||||
const UtilityGrid = createElement('div', 'panel-grid');
|
||||
|
||||
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(LogList, 'Tentone Native Solver', 'Sent a solver request envelope to Unreal.');
|
||||
});
|
||||
SolverActions.appendChild(SolverButton);
|
||||
SolverPanel.append(SolverField, SolverActions);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
UtilityGrid.append(SolverPanel, StatePanel, LogPanel);
|
||||
UtilitySection.append(UtilityHeader, UtilityGrid);
|
||||
|
||||
ShellElement.append(HeroElement, AdapterSection, UtilitySection);
|
||||
RootElement.appendChild(ShellElement);
|
||||
|
||||
UEBridge.onCommand(function onCommand(Payload)
|
||||
{
|
||||
appendLog(LogList, 'UE Command', serializePretty(Payload));
|
||||
});
|
||||
|
||||
UEBridge.onShellState(function onShellState(Payload)
|
||||
{
|
||||
StatePre.textContent = serializePretty(Payload);
|
||||
appendLog(LogList, 'Browser Shell State', 'Received fresh shell state from Unreal.');
|
||||
});
|
||||
}
|
||||
|
||||
const runtimeApi = {
|
||||
adapters: BrowserSupportAdapters,
|
||||
listAdapters()
|
||||
{
|
||||
return BrowserSupportAdapters.slice();
|
||||
},
|
||||
loadAdapter,
|
||||
receiveCommand(Payload)
|
||||
{
|
||||
UEBridge.receiveCommand(Payload);
|
||||
},
|
||||
setShellState(Payload)
|
||||
{
|
||||
UEBridge.setShellState(Payload);
|
||||
},
|
||||
renderShell(RootElement)
|
||||
{
|
||||
createBrowserShell(RootElement);
|
||||
},
|
||||
sendState(Payload)
|
||||
{
|
||||
UEBridge.sendState(Payload);
|
||||
}
|
||||
};
|
||||
|
||||
window.HyperTwistBrowserRuntime = runtimeApi;
|
||||
|
||||
const RootElement = document.getElementById('app');
|
||||
if (RootElement instanceof HTMLElement)
|
||||
{
|
||||
runtimeApi.renderShell(RootElement);
|
||||
}
|
||||
|
||||
const PendingCommands = Array.isArray(BootState.pendingCommands) ? BootState.pendingCommands.slice() : [];
|
||||
BootState.pendingCommands = [];
|
||||
PendingCommands.forEach(function replayPendingCommand(Payload)
|
||||
{
|
||||
runtimeApi.receiveCommand(Payload);
|
||||
});
|
||||
|
||||
const PendingShellStates = Array.isArray(BootState.pendingShellStates) ? BootState.pendingShellStates.slice() : [];
|
||||
BootState.pendingShellStates = [];
|
||||
PendingShellStates.forEach(function replayPendingShellState(Payload)
|
||||
{
|
||||
runtimeApi.setShellState(Payload);
|
||||
});
|
||||
|
||||
UEBridge.notifyRuntimeReady({
|
||||
status: 'ready',
|
||||
runtime: 'hypertwist-browser-runtime-fallback',
|
||||
mode: 'fallback-js',
|
||||
fallbackReason: BootState.fallbackReason || 'bundle unavailable',
|
||||
adapterCount: BrowserSupportAdapters.length,
|
||||
shellAuthority: 'Content/Browser/index.html'
|
||||
});
|
||||
})();
|
||||
|
|
@ -2,8 +2,18 @@ import { UEBridge } from './runtime/bridge';
|
|||
import { BrowserSupportAdapters, listAdapters, loadAdapter } from './runtime/registry';
|
||||
import { createBrowserShell } from './runtime/shell';
|
||||
|
||||
interface BrowserBootState
|
||||
{
|
||||
fallbackReason?: string;
|
||||
pendingCommands?: unknown[];
|
||||
pendingShellStates?: unknown[];
|
||||
runtimeMode?: string;
|
||||
runtimeReadyAtUtc?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
HyperTwistBrowserBootState?: BrowserBootState;
|
||||
HyperTwistBrowserRuntime?: {
|
||||
adapters: typeof BrowserSupportAdapters;
|
||||
listAdapters: typeof listAdapters;
|
||||
|
|
@ -16,6 +26,23 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
function resolveBootState(): BrowserBootState
|
||||
{
|
||||
const ExistingBootState = window.HyperTwistBrowserBootState;
|
||||
if (ExistingBootState && typeof ExistingBootState === 'object')
|
||||
{
|
||||
return ExistingBootState;
|
||||
}
|
||||
|
||||
const BootState: BrowserBootState = {};
|
||||
window.HyperTwistBrowserBootState = BootState;
|
||||
return BootState;
|
||||
}
|
||||
|
||||
const BootState = resolveBootState();
|
||||
BootState.runtimeMode = 'bundled-module';
|
||||
BootState.runtimeReadyAtUtc = new Date().toISOString();
|
||||
|
||||
const runtimeApi = {
|
||||
adapters: BrowserSupportAdapters,
|
||||
listAdapters,
|
||||
|
|
@ -42,8 +69,18 @@ if (RootElement instanceof HTMLElement)
|
|||
runtimeApi.renderShell(RootElement);
|
||||
}
|
||||
|
||||
const PendingCommands = [...(BootState.pendingCommands ?? [])];
|
||||
BootState.pendingCommands = [];
|
||||
PendingCommands.forEach((Payload) => runtimeApi.receiveCommand(Payload));
|
||||
|
||||
const PendingShellStates = [...(BootState.pendingShellStates ?? [])];
|
||||
BootState.pendingShellStates = [];
|
||||
PendingShellStates.forEach((Payload) => runtimeApi.setShellState(Payload));
|
||||
|
||||
UEBridge.notifyRuntimeReady({
|
||||
status: 'ready',
|
||||
runtime: 'hypertwist-browser-runtime',
|
||||
adapterCount: listAdapters().length
|
||||
mode: BootState.runtimeMode,
|
||||
adapterCount: listAdapters().length,
|
||||
shellAuthority: 'Content/Browser/index.html'
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
type RuntimeListener = (payload: unknown) => void;
|
||||
type PendingPayload = unknown;
|
||||
|
||||
function tryParseJsonPayload(value: unknown): unknown
|
||||
{
|
||||
|
|
@ -33,6 +34,10 @@ function getUnrealBridgeHandle(): Record<string, unknown> | null
|
|||
|
||||
const CommandListeners = new Set<RuntimeListener>();
|
||||
const ShellStateListeners = new Set<RuntimeListener>();
|
||||
let LastCommandPayload: PendingPayload | undefined;
|
||||
let LastShellStatePayload: PendingPayload | undefined;
|
||||
let bHasLastCommandPayload = false;
|
||||
let bHasLastShellStatePayload = false;
|
||||
|
||||
function dispatchToListeners(Listeners: Set<RuntimeListener>, Payload: unknown): void
|
||||
{
|
||||
|
|
@ -49,11 +54,12 @@ function postEnvelope(Type: string, Payload: unknown): void
|
|||
|
||||
const SerializedEnvelope = JSON.stringify(Envelope);
|
||||
const UnrealBridgeHandle = getUnrealBridgeHandle();
|
||||
let bDeliveredToUnreal = false;
|
||||
|
||||
if (UnrealBridgeHandle && typeof UnrealBridgeHandle.notifyEnvelope === 'function')
|
||||
{
|
||||
(UnrealBridgeHandle.notifyEnvelope as (EnvelopeJson: string) => void)(SerializedEnvelope);
|
||||
return;
|
||||
bDeliveredToUnreal = true;
|
||||
}
|
||||
|
||||
if (Type === 'hypertwist-state'
|
||||
|
|
@ -61,6 +67,11 @@ function postEnvelope(Type: string, Payload: unknown): void
|
|||
&& typeof UnrealBridgeHandle.notifyState === 'function')
|
||||
{
|
||||
(UnrealBridgeHandle.notifyState as (StateJson: string) => void)(JSON.stringify(Payload));
|
||||
bDeliveredToUnreal = true;
|
||||
}
|
||||
|
||||
if (bDeliveredToUnreal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -90,24 +101,35 @@ export const UEBridge = {
|
|||
onCommand(Listener: RuntimeListener): () => void
|
||||
{
|
||||
CommandListeners.add(Listener);
|
||||
if (bHasLastCommandPayload)
|
||||
{
|
||||
Listener(LastCommandPayload);
|
||||
}
|
||||
return () => CommandListeners.delete(Listener);
|
||||
},
|
||||
|
||||
onShellState(Listener: RuntimeListener): () => void
|
||||
{
|
||||
ShellStateListeners.add(Listener);
|
||||
if (bHasLastShellStatePayload)
|
||||
{
|
||||
Listener(LastShellStatePayload);
|
||||
}
|
||||
return () => ShellStateListeners.delete(Listener);
|
||||
},
|
||||
|
||||
receiveCommand(Payload: unknown): void
|
||||
{
|
||||
dispatchToListeners(CommandListeners, tryParseJsonPayload(Payload));
|
||||
LastCommandPayload = tryParseJsonPayload(Payload);
|
||||
bHasLastCommandPayload = true;
|
||||
dispatchToListeners(CommandListeners, LastCommandPayload);
|
||||
},
|
||||
|
||||
setShellState(Payload: unknown): void
|
||||
{
|
||||
const ParsedPayload = tryParseJsonPayload(Payload);
|
||||
dispatchToListeners(ShellStateListeners, ParsedPayload);
|
||||
LastShellStatePayload = tryParseJsonPayload(Payload);
|
||||
bHasLastShellStatePayload = true;
|
||||
dispatchToListeners(ShellStateListeners, LastShellStatePayload);
|
||||
},
|
||||
|
||||
sendState(Payload: unknown): void
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function createBrowserShell(RootElement: HTMLElement): void
|
|||
const HeroParagraph = createElement(
|
||||
'p',
|
||||
undefined,
|
||||
'Phase 1 browser-capable donors now resolve through a first-party runtime shell instead of broken workspace-root npm links. Native and editor-only donors stay visible as bounded sidecars instead of being misrepresented as direct runtime packages.'
|
||||
'The authoritative shell now lives in Content/Browser/index.html. When the bundled runtime is present the shell upgrades into the full browser stack; when build artifacts have been cleaned, the same shell still remains truthful and runnable through the plain-JS fallback lane.'
|
||||
);
|
||||
const HeroMetrics = createElement('div', 'hero-grid');
|
||||
|
||||
|
|
@ -69,6 +69,10 @@ export function createBrowserShell(RootElement: HTMLElement): void
|
|||
{
|
||||
label: 'Bridge route',
|
||||
value: 'UE BindUObject + postMessage fallback'
|
||||
},
|
||||
{
|
||||
label: 'Shell authority',
|
||||
value: 'index.html + runtime bootstrap'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -175,7 +179,7 @@ export function createBrowserShell(RootElement: HTMLElement): void
|
|||
createElement(
|
||||
'p',
|
||||
'section-note',
|
||||
'The browser shell can render analytics, preview bundled viewer assets, and fall back into native solver routing without pretending native donors are browser packages.'
|
||||
'The browser shell can render analytics, preview bundled viewer assets, and fall back into native solver routing without pretending native donors are browser packages or requiring retained build artifacts.'
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,15 @@ export default defineConfig({
|
|||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/browser-spatial-runtime.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'browser-spatial-runtime.js'
|
||||
},
|
||||
rollupOptions: {
|
||||
input: {
|
||||
shell: resolve(__dirname, 'index.html')
|
||||
},
|
||||
output: {
|
||||
assetFileNames: 'assets/[name][extname]',
|
||||
chunkFileNames: 'assets/[name].js',
|
||||
entryFileNames: 'assets/[name].js'
|
||||
chunkFileNames: 'assets/[name].js'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,20 +16,7 @@ namespace HyperTwistBrowserWidgetInternal
|
|||
|
||||
FString ResolveBundledBrowserShellAbsolutePath()
|
||||
{
|
||||
const TArray<FString> CandidatePaths = {
|
||||
FPaths::Combine(FPaths::ProjectDir(), TEXT(".."), TEXT("Content"), TEXT("Browser"), TEXT("dist"), TEXT("index.html")),
|
||||
FPaths::Combine(FPaths::ProjectDir(), TEXT(".."), TEXT("Content"), TEXT("Browser"), TEXT("index.html"))
|
||||
};
|
||||
|
||||
for (const FString& CandidatePath : CandidatePaths)
|
||||
{
|
||||
if (FPaths::FileExists(CandidatePath))
|
||||
{
|
||||
return CandidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return CandidatePaths.Last();
|
||||
return FPaths::Combine(FPaths::ProjectDir(), TEXT(".."), TEXT("Content"), TEXT("Browser"), TEXT("index.html"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +90,11 @@ FString UHyperTwistBrowserWidget::ResolveBundledBrowserShellUrl()
|
|||
);
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::ResolveBundledBrowserShellSourcePath()
|
||||
{
|
||||
return HyperTwistBrowserWidgetInternal::ResolveBundledBrowserShellAbsolutePath();
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UHyperTwistBrowserWidget::RebuildWidget()
|
||||
{
|
||||
if (BrowserBridgeObject == nullptr)
|
||||
|
|
|
|||
|
|
@ -84,26 +84,8 @@ bool AHyperTwistClassicCubeActor::ProcessClick(const FVector& RayOrigin, const F
|
|||
return false;
|
||||
}
|
||||
|
||||
// Map the dominant hit-normal axis to a cube face so tiny floating-point
|
||||
// deviations on procedural sections do not make clicks unresponsive.
|
||||
const FVector Normal = Hit.ImpactNormal.GetSafeNormal();
|
||||
const FVector AbsNormal = Normal.GetAbs();
|
||||
EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up;
|
||||
|
||||
if (AbsNormal.Z >= AbsNormal.X && AbsNormal.Z >= AbsNormal.Y)
|
||||
{
|
||||
Face = Normal.Z >= 0.0f ? EHyperTwistClassicCubeFace::Up : EHyperTwistClassicCubeFace::Down;
|
||||
}
|
||||
else if (AbsNormal.Y >= AbsNormal.X)
|
||||
{
|
||||
Face = Normal.Y >= 0.0f ? EHyperTwistClassicCubeFace::Front : EHyperTwistClassicCubeFace::Back;
|
||||
}
|
||||
else
|
||||
{
|
||||
Face = Normal.X >= 0.0f ? EHyperTwistClassicCubeFace::Right : EHyperTwistClassicCubeFace::Left;
|
||||
}
|
||||
|
||||
if (Normal.IsNearlyZero())
|
||||
if (!TryResolveFaceFromImpactNormal(Hit.ImpactNormal, Face))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -116,6 +98,36 @@ bool AHyperTwistClassicCubeActor::ProcessClick(const FVector& RayOrigin, const F
|
|||
return true;
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(
|
||||
const FVector& ImpactNormal,
|
||||
EHyperTwistClassicCubeFace& OutFace
|
||||
)
|
||||
{
|
||||
const FVector Normal = ImpactNormal.GetSafeNormal();
|
||||
if (Normal.IsNearlyZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the dominant axis so small floating-point drift on procedural geometry
|
||||
// does not make face selection unstable.
|
||||
const FVector AbsNormal = Normal.GetAbs();
|
||||
if (AbsNormal.Z >= AbsNormal.X && AbsNormal.Z >= AbsNormal.Y)
|
||||
{
|
||||
OutFace = Normal.Z >= 0.0f ? EHyperTwistClassicCubeFace::Up : EHyperTwistClassicCubeFace::Down;
|
||||
}
|
||||
else if (AbsNormal.Y >= AbsNormal.X)
|
||||
{
|
||||
OutFace = Normal.Y >= 0.0f ? EHyperTwistClassicCubeFace::Front : EHyperTwistClassicCubeFace::Back;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutFace = Normal.X >= 0.0f ? EHyperTwistClassicCubeFace::Right : EHyperTwistClassicCubeFace::Left;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TArray<FString> AHyperTwistClassicCubeActor::GenerateScramble(int32 Length)
|
||||
{
|
||||
TArray<FString> Result;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "GameFramework/PlayerController.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeOrbitPawn.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubePlayerController.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
|
||||
namespace HyperTwistClassicCubeGameModeInternal
|
||||
|
|
@ -52,6 +54,8 @@ namespace HyperTwistClassicCubeGameModeInternal
|
|||
AHyperTwistClassicCubeGameMode::AHyperTwistClassicCubeGameMode()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PlayerControllerClass = AHyperTwistClassicCubePlayerController::StaticClass();
|
||||
DefaultPawnClass = AHyperTwistClassicCubeOrbitPawn::StaticClass();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeGameMode::BeginPlay()
|
||||
|
|
@ -270,6 +274,7 @@ void AHyperTwistClassicCubeGameMode::RefreshHud()
|
|||
FString TimerLine = TEXT("timer: idle");
|
||||
FString InspectionLine = TEXT("inspection: pending");
|
||||
FString ResultLine = TEXT("result: waiting for first attempt");
|
||||
FString ControlsLine = TEXT("controls: LMB clockwise, RMB counter-clockwise, touch clockwise, MMB drag orbit, wheel zoom, R new scramble");
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
|
||||
FHyperTwistTrainingLiveTimerState LiveTimerState;
|
||||
|
|
@ -339,7 +344,7 @@ void AHyperTwistClassicCubeGameMode::RefreshHud()
|
|||
}
|
||||
}
|
||||
|
||||
ActiveHudWidget->SetHudLines(StatusLine, ScrambleLine, TimerLine, InspectionLine, ResultLine);
|
||||
ActiveHudWidget->SetHudLines(StatusLine, ScrambleLine, TimerLine, InspectionLine, ResultLine, ControlsLine);
|
||||
}
|
||||
|
||||
FString AHyperTwistClassicCubeGameMode::BuildSessionId() const
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
|
||||
|
||||
#include "Components/Button.h"
|
||||
#include "Components/ButtonSlot.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/VerticalBox.h"
|
||||
#include "Components/VerticalBoxSlot.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeGameMode.h"
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::NativeConstruct()
|
||||
{
|
||||
|
|
@ -16,7 +19,8 @@ void UHyperTwistClassicCubeHUDWidget::SetHudLines(
|
|||
const FString& InScrambleLine,
|
||||
const FString& InTimerLine,
|
||||
const FString& InInspectionLine,
|
||||
const FString& InResultLine
|
||||
const FString& InResultLine,
|
||||
const FString& InControlsLine
|
||||
)
|
||||
{
|
||||
EnsureWidgetTreeBuilt();
|
||||
|
|
@ -41,6 +45,15 @@ void UHyperTwistClassicCubeHUDWidget::SetHudLines(
|
|||
{
|
||||
ResultTextBlock->SetText(FText::FromString(InResultLine));
|
||||
}
|
||||
if (ControlsTextBlock != nullptr)
|
||||
{
|
||||
ControlsTextBlock->SetText(FText::FromString(InControlsLine));
|
||||
}
|
||||
}
|
||||
|
||||
bool UHyperTwistClassicCubeHUDWidget::IsNewScrambleButtonHovered() const
|
||||
{
|
||||
return bNewScrambleButtonHovered;
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
|
||||
|
|
@ -59,13 +72,16 @@ void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
|
|||
TimerTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudTimer"));
|
||||
InspectionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudInspection"));
|
||||
ResultTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudResult"));
|
||||
ControlsTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudControls"));
|
||||
AddActionButton(RootLayout);
|
||||
|
||||
SetHudLines(
|
||||
TEXT("status: preparing classic cube lane"),
|
||||
TEXT("scramble: pending"),
|
||||
TEXT("timer: idle"),
|
||||
TEXT("inspection: pending"),
|
||||
TEXT("result: waiting for first attempt")
|
||||
TEXT("result: waiting for first attempt"),
|
||||
TEXT("controls: LMB clockwise, RMB counter-clockwise, touch clockwise, MMB drag orbit, wheel zoom, R new scramble")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,3 +106,69 @@ UTextBlock* UHyperTwistClassicCubeHUDWidget::AddLine(UVerticalBox* Parent, const
|
|||
|
||||
return TextBlock;
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::AddActionButton(UVerticalBox* Parent)
|
||||
{
|
||||
if (WidgetTree == nullptr || Parent == nullptr || NewScrambleButton != nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NewScrambleButton = WidgetTree->ConstructWidget<UButton>(
|
||||
UButton::StaticClass(),
|
||||
TEXT("ClassicCubeHudNewScrambleButton")
|
||||
);
|
||||
if (NewScrambleButton == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NewScrambleButtonLabel = WidgetTree->ConstructWidget<UTextBlock>(
|
||||
UTextBlock::StaticClass(),
|
||||
TEXT("ClassicCubeHudNewScrambleButtonLabel")
|
||||
);
|
||||
if (NewScrambleButtonLabel != nullptr)
|
||||
{
|
||||
NewScrambleButtonLabel->SetText(FText::FromString(TEXT("New Scramble")));
|
||||
NewScrambleButton->AddChild(NewScrambleButtonLabel);
|
||||
}
|
||||
|
||||
NewScrambleButton->OnClicked.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleNewScrambleClicked);
|
||||
NewScrambleButton->OnHovered.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleNewScrambleHovered);
|
||||
NewScrambleButton->OnUnhovered.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleNewScrambleUnhovered);
|
||||
|
||||
if (UVerticalBoxSlot* VerticalBoxSlot = Parent->AddChildToVerticalBox(NewScrambleButton))
|
||||
{
|
||||
VerticalBoxSlot->SetPadding(FMargin(0.0f, 12.0f, 0.0f, 0.0f));
|
||||
}
|
||||
|
||||
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(NewScrambleButtonLabel != nullptr ? NewScrambleButtonLabel->Slot : nullptr))
|
||||
{
|
||||
ButtonSlot->SetPadding(FMargin(12.0f, 6.0f, 12.0f, 6.0f));
|
||||
ButtonSlot->SetHorizontalAlignment(HAlign_Center);
|
||||
ButtonSlot->SetVerticalAlignment(VAlign_Center);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::HandleNewScrambleClicked()
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AHyperTwistClassicCubeGameMode* GameMode = Cast<AHyperTwistClassicCubeGameMode>(GetWorld()->GetAuthGameMode()))
|
||||
{
|
||||
GameMode->StartFreshAttempt();
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::HandleNewScrambleHovered()
|
||||
{
|
||||
bNewScrambleButtonHovered = true;
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::HandleNewScrambleUnhovered()
|
||||
{
|
||||
bNewScrambleButtonHovered = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubeOrbitPawn.h"
|
||||
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
|
||||
AHyperTwistClassicCubeOrbitPawn::AHyperTwistClassicCubeOrbitPawn()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
|
||||
RootComponent = SceneRoot;
|
||||
|
||||
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
|
||||
SpringArm->SetupAttachment(SceneRoot);
|
||||
SpringArm->bDoCollisionTest = false;
|
||||
SpringArm->bEnableCameraLag = false;
|
||||
SpringArm->bUsePawnControlRotation = false;
|
||||
SpringArm->TargetArmLength = InitialArmLength;
|
||||
|
||||
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
|
||||
CameraComponent->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
|
||||
CameraComponent->bUsePawnControlRotation = false;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeOrbitPawn::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
CurrentYawDegrees = InitialYawDegrees;
|
||||
CurrentPitchDegrees = InitialPitchDegrees;
|
||||
if (SpringArm != nullptr)
|
||||
{
|
||||
SpringArm->TargetArmLength = FMath::Clamp(InitialArmLength, MinimumArmLength, MaximumArmLength);
|
||||
}
|
||||
ApplyOrbitTransform();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeOrbitPawn::Tick(const float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
static_cast<void>(DeltaSeconds);
|
||||
|
||||
APlayerController* PlayerController = Cast<APlayerController>(GetController());
|
||||
if (PlayerController == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SpringArm != nullptr)
|
||||
{
|
||||
const float MouseWheelDelta = PlayerController->GetInputAnalogKeyState(EKeys::MouseWheelAxis);
|
||||
if (!FMath::IsNearlyZero(MouseWheelDelta))
|
||||
{
|
||||
SpringArm->TargetArmLength = FMath::Clamp(
|
||||
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep),
|
||||
MinimumArmLength,
|
||||
MaximumArmLength
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldOrbitFromMouseInput())
|
||||
{
|
||||
float MouseDeltaX = 0.0f;
|
||||
float MouseDeltaY = 0.0f;
|
||||
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
|
||||
if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY))
|
||||
{
|
||||
CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel;
|
||||
CurrentPitchDegrees = FMath::Clamp(
|
||||
CurrentPitchDegrees - (MouseDeltaY * OrbitPitchDegreesPerPixel),
|
||||
MinimumPitchDegrees,
|
||||
MaximumPitchDegrees
|
||||
);
|
||||
ApplyOrbitTransform();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeOrbitPawn::ApplyOrbitTransform()
|
||||
{
|
||||
SetActorLocation(OrbitFocusPoint);
|
||||
if (SpringArm != nullptr)
|
||||
{
|
||||
SpringArm->SetRelativeRotation(FRotator(CurrentPitchDegrees, CurrentYawDegrees, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubeOrbitPawn::ShouldOrbitFromMouseInput() const
|
||||
{
|
||||
const APlayerController* PlayerController = Cast<APlayerController>(GetController());
|
||||
if (PlayerController == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bMiddleMouseOrbit = bUseMiddleMouseOrbit && PlayerController->IsInputKeyDown(EKeys::MiddleMouseButton);
|
||||
const bool bShiftRightMouseOrbit =
|
||||
bUseRightMouseOrbitWithShift
|
||||
&& PlayerController->IsInputKeyDown(EKeys::RightMouseButton)
|
||||
&& (
|
||||
PlayerController->IsInputKeyDown(EKeys::LeftShift)
|
||||
|| PlayerController->IsInputKeyDown(EKeys::RightShift)
|
||||
);
|
||||
|
||||
return bMiddleMouseOrbit || bShiftRightMouseOrbit;
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubePlayerController.h"
|
||||
|
||||
#include "EngineUtils.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeGameMode.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
|
||||
#include "InputCoreTypes.h"
|
||||
|
||||
AHyperTwistClassicCubePlayerController::AHyperTwistClassicCubePlayerController()
|
||||
{
|
||||
bShowMouseCursor = true;
|
||||
bEnableClickEvents = true;
|
||||
bEnableMouseOverEvents = true;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
ApplyClassicCubeInputMode();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
if (InputComponent == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InputComponent->BindKey(
|
||||
EKeys::LeftMouseButton,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistClassicCubePlayerController::HandlePrimaryClick
|
||||
);
|
||||
|
||||
if (bEnableCounterClockwiseRightClick)
|
||||
{
|
||||
InputComponent->BindKey(
|
||||
EKeys::RightMouseButton,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistClassicCubePlayerController::HandleSecondaryClick
|
||||
);
|
||||
}
|
||||
|
||||
if (bEnableTouchTurnInput)
|
||||
{
|
||||
InputComponent->BindTouch(
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistClassicCubePlayerController::HandleTouchPressed
|
||||
);
|
||||
}
|
||||
|
||||
if (bBindFreshAttemptShortcut)
|
||||
{
|
||||
InputComponent->BindKey(
|
||||
EKeys::R,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistClassicCubePlayerController::HandleFreshAttemptShortcut
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubePlayerController::TryProcessCubeClickFromCursor(const bool bCounterClockwise)
|
||||
{
|
||||
float ScreenX = 0.0f;
|
||||
float ScreenY = 0.0f;
|
||||
if (!GetMousePosition(ScreenX, ScreenY))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryProcessCubeClickFromScreenPosition(FVector2D(ScreenX, ScreenY), bCounterClockwise);
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubePlayerController::TryProcessCubeClickFromScreenPosition(
|
||||
const FVector2D& ScreenPosition,
|
||||
const bool bCounterClockwise
|
||||
)
|
||||
{
|
||||
FVector RayOrigin = FVector::ZeroVector;
|
||||
FVector RayDirection = FVector::ZeroVector;
|
||||
if (!DeprojectScreenPositionToWorld(ScreenPosition.X, ScreenPosition.Y, RayOrigin, RayDirection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AHyperTwistClassicCubeActor* CubeActor = ResolveClassicCubeActor())
|
||||
{
|
||||
return CubeActor->ProcessClick(RayOrigin, RayDirection, bCounterClockwise);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::RequestFreshAttempt()
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AHyperTwistClassicCubeGameMode* GameMode =
|
||||
Cast<AHyperTwistClassicCubeGameMode>(GetWorld()->GetAuthGameMode()))
|
||||
{
|
||||
GameMode->StartFreshAttempt();
|
||||
}
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::ApplyClassicCubeInputMode()
|
||||
{
|
||||
bShowMouseCursor = true;
|
||||
bEnableClickEvents = true;
|
||||
bEnableMouseOverEvents = true;
|
||||
|
||||
if (bUseGameAndUiInputMode)
|
||||
{
|
||||
FInputModeGameAndUI InputMode;
|
||||
InputMode.SetHideCursorDuringCapture(false);
|
||||
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
||||
SetInputMode(InputMode);
|
||||
}
|
||||
}
|
||||
|
||||
AHyperTwistClassicCubeActor* AHyperTwistClassicCubePlayerController::ResolveClassicCubeActor() const
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const AHyperTwistClassicCubeGameMode* GameMode =
|
||||
Cast<AHyperTwistClassicCubeGameMode>(GetWorld()->GetAuthGameMode()))
|
||||
{
|
||||
if (GameMode->ActiveCubeActor != nullptr)
|
||||
{
|
||||
return GameMode->ActiveCubeActor;
|
||||
}
|
||||
}
|
||||
|
||||
for (TActorIterator<AHyperTwistClassicCubeActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
|
||||
{
|
||||
return *ActorIt;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::HandlePrimaryClick()
|
||||
{
|
||||
if (const AHyperTwistClassicCubeGameMode* GameMode =
|
||||
Cast<AHyperTwistClassicCubeGameMode>(GetWorld() != nullptr ? GetWorld()->GetAuthGameMode() : nullptr))
|
||||
{
|
||||
if (GameMode->ActiveHudWidget != nullptr && GameMode->ActiveHudWidget->IsNewScrambleButtonHovered())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TryProcessCubeClickFromCursor(false);
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::HandleSecondaryClick()
|
||||
{
|
||||
if (IsInputKeyDown(EKeys::LeftShift) || IsInputKeyDown(EKeys::RightShift))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryProcessCubeClickFromCursor(true);
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::HandleFreshAttemptShortcut()
|
||||
{
|
||||
RequestFreshAttempt();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubePlayerController::HandleTouchPressed(
|
||||
const ETouchIndex::Type FingerIndex,
|
||||
const FVector Location
|
||||
)
|
||||
{
|
||||
static_cast<void>(FingerIndex);
|
||||
TryProcessCubeClickFromScreenPosition(FVector2D(Location.X, Location.Y), false);
|
||||
}
|
||||
|
|
@ -46,6 +46,9 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
static FString ResolveBundledBrowserShellUrl();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
static FString ResolveBundledBrowserShellSourcePath();
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ public:
|
|||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube")
|
||||
bool ProcessClick(const FVector& RayOrigin, const FVector& RayDirection, bool bCounterClockwise = false);
|
||||
|
||||
static bool TryResolveFaceFromImpactNormal(
|
||||
const FVector& ImpactNormal,
|
||||
EHyperTwistClassicCubeFace& OutFace
|
||||
);
|
||||
|
||||
/** Generate a random scramble of the given length (WCA-style notation). */
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube")
|
||||
TArray<FString> GenerateScramble(int32 Length = 20);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#include "HyperTwistClassicCubeGameMode.generated.h"
|
||||
|
||||
class AHyperTwistClassicCubeActor;
|
||||
class AHyperTwistClassicCubeOrbitPawn;
|
||||
class AHyperTwistClassicCubePlayerController;
|
||||
class UHyperTwistClassicCubeHUDWidget;
|
||||
class UHyperTwistTrainingSubsystem;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "Blueprint/UserWidget.h"
|
||||
#include "HyperTwistClassicCubeHUDWidget.generated.h"
|
||||
|
||||
class UButton;
|
||||
class UTextBlock;
|
||||
class UVerticalBox;
|
||||
|
||||
|
|
@ -21,12 +22,26 @@ public:
|
|||
const FString& InScrambleLine,
|
||||
const FString& InTimerLine,
|
||||
const FString& InInspectionLine,
|
||||
const FString& InResultLine
|
||||
const FString& InResultLine,
|
||||
const FString& InControlsLine
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|HUD")
|
||||
bool IsNewScrambleButtonHovered() const;
|
||||
|
||||
private:
|
||||
void EnsureWidgetTreeBuilt();
|
||||
UTextBlock* AddLine(UVerticalBox* Parent, const TCHAR* WidgetName);
|
||||
void AddActionButton(UVerticalBox* Parent);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleNewScrambleClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleNewScrambleHovered();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleNewScrambleUnhovered();
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> StatusTextBlock = nullptr;
|
||||
|
|
@ -42,4 +57,15 @@ private:
|
|||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ResultTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ControlsTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UButton> NewScrambleButton = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> NewScrambleButtonLabel = nullptr;
|
||||
|
||||
bool bNewScrambleButtonHovered = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "HyperTwistClassicCubeOrbitPawn.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
class USceneComponent;
|
||||
class USpringArmComponent;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API AHyperTwistClassicCubeOrbitPawn : public APawn
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AHyperTwistClassicCubeOrbitPawn();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
FVector OrbitFocusPoint = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float InitialArmLength = 360.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float MinimumArmLength = 180.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float MaximumArmLength = 720.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float ZoomStep = 42.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float OrbitYawDegreesPerPixel = 0.25f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float OrbitPitchDegreesPerPixel = 0.2f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float MinimumPitchDegrees = -75.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float MaximumPitchDegrees = -10.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float InitialYawDegrees = 45.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
float InitialPitchDegrees = -28.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
bool bUseMiddleMouseOrbit = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
|
||||
bool bUseRightMouseOrbitWithShift = false;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube|Camera")
|
||||
TObjectPtr<USceneComponent> SceneRoot = nullptr;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube|Camera")
|
||||
TObjectPtr<USpringArmComponent> SpringArm = nullptr;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube|Camera")
|
||||
TObjectPtr<UCameraComponent> CameraComponent = nullptr;
|
||||
|
||||
protected:
|
||||
void ApplyOrbitTransform();
|
||||
bool ShouldOrbitFromMouseInput() const;
|
||||
|
||||
float CurrentYawDegrees = 0.0f;
|
||||
float CurrentPitchDegrees = 0.0f;
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "HyperTwistClassicCubePlayerController.generated.h"
|
||||
|
||||
class AHyperTwistClassicCubeActor;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API AHyperTwistClassicCubePlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AHyperTwistClassicCubePlayerController();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void SetupInputComponent() override;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
|
||||
bool bUseGameAndUiInputMode = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
|
||||
bool bEnableCounterClockwiseRightClick = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
|
||||
bool bEnableTouchTurnInput = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
|
||||
bool bBindFreshAttemptShortcut = true;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|Input")
|
||||
bool TryProcessCubeClickFromCursor(bool bCounterClockwise = false);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|Input")
|
||||
bool TryProcessCubeClickFromScreenPosition(const FVector2D& ScreenPosition, bool bCounterClockwise = false);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|Input")
|
||||
void RequestFreshAttempt();
|
||||
|
||||
protected:
|
||||
void ApplyClassicCubeInputMode();
|
||||
AHyperTwistClassicCubeActor* ResolveClassicCubeActor() const;
|
||||
void HandlePrimaryClick();
|
||||
void HandleSecondaryClick();
|
||||
void HandleFreshAttemptShortcut();
|
||||
void HandleTouchPressed(ETouchIndex::Type FingerIndex, FVector Location);
|
||||
};
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
#include "HyperTwistBrowser/HyperTwistBrowserBridgeObject.h"
|
||||
#include "HyperTwistBrowser/HyperTwistBrowserWidget.h"
|
||||
|
|
@ -69,8 +71,55 @@ bool FHyperTwistBrowserWidgetBundledShellUrlTest::RunTest(const FString& Paramet
|
|||
BundledShellUrl.StartsWith(TEXT("file:///"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The bundled browser shell URL must point at the Browser shell."),
|
||||
BundledShellUrl.Contains(TEXT("Content/Browser"))
|
||||
TEXT("The bundled browser shell URL must point at the authoritative Browser shell source."),
|
||||
BundledShellUrl.Contains(TEXT("Content/Browser/index.html"))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserWidgetAuthoritativeShellArtifactsTest,
|
||||
"HyperTwist.Browser.Widget.AuthoritativeShellArtifacts",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistBrowserWidgetAuthoritativeShellArtifactsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FString ShellSourcePath = UHyperTwistBrowserWidget::ResolveBundledBrowserShellSourcePath();
|
||||
const FString BootstrapScriptPath = FPaths::Combine(
|
||||
FPaths::ProjectDir(),
|
||||
TEXT(".."),
|
||||
TEXT("Content"),
|
||||
TEXT("Browser"),
|
||||
TEXT("src"),
|
||||
TEXT("browser-runtime-bootstrap.js")
|
||||
);
|
||||
const FString FallbackRuntimePath = FPaths::Combine(
|
||||
FPaths::ProjectDir(),
|
||||
TEXT(".."),
|
||||
TEXT("Content"),
|
||||
TEXT("Browser"),
|
||||
TEXT("src"),
|
||||
TEXT("browser-spatial-runtime-fallback.js")
|
||||
);
|
||||
|
||||
TestTrue(TEXT("The authoritative browser shell source file must exist."), FPaths::FileExists(ShellSourcePath));
|
||||
TestTrue(TEXT("The browser bootstrap script must exist."), FPaths::FileExists(BootstrapScriptPath));
|
||||
TestTrue(TEXT("The clean-checkout fallback runtime must exist."), FPaths::FileExists(FallbackRuntimePath));
|
||||
|
||||
FString ShellSourceHtml;
|
||||
TestTrue(
|
||||
TEXT("The authoritative browser shell HTML must be readable."),
|
||||
FFileHelper::LoadFileToString(ShellSourceHtml, *ShellSourcePath)
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The authoritative browser shell must bootstrap through the plain-JS loader."),
|
||||
ShellSourceHtml.Contains(TEXT("browser-runtime-bootstrap.js"))
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("The authoritative browser shell must not point directly at a TypeScript source entrypoint."),
|
||||
ShellSourceHtml.Contains(TEXT("browser-spatial-runtime.ts"))
|
||||
);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,53 @@ namespace HyperTwistClassicCubeActorTestInternal
|
|||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeImpactNormalMappingTest,
|
||||
"HyperTwist.Simulation.ClassicCube.ImpactNormalMapping",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeImpactNormalMappingTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
EHyperTwistClassicCubeFace ResolvedFace = EHyperTwistClassicCubeFace::Up;
|
||||
TestTrue(
|
||||
TEXT("Positive Z normals must resolve to the Up face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector(0.01f, 0.02f, 1.0f), ResolvedFace)
|
||||
&& ResolvedFace == EHyperTwistClassicCubeFace::Up
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Negative Z normals must resolve to the Down face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector(0.0f, -0.02f, -1.0f), ResolvedFace)
|
||||
&& ResolvedFace == EHyperTwistClassicCubeFace::Down
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Positive Y normals must resolve to the Front face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector(0.01f, 1.0f, 0.2f), ResolvedFace)
|
||||
&& ResolvedFace == EHyperTwistClassicCubeFace::Front
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Negative Y normals must resolve to the Back face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector(0.01f, -1.0f, 0.2f), ResolvedFace)
|
||||
&& ResolvedFace == EHyperTwistClassicCubeFace::Back
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Positive X normals must resolve to the Right face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector(1.0f, 0.1f, 0.1f), ResolvedFace)
|
||||
&& ResolvedFace == EHyperTwistClassicCubeFace::Right
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Negative X normals must resolve to the Left face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector(-1.0f, 0.1f, 0.1f), ResolvedFace)
|
||||
&& ResolvedFace == EHyperTwistClassicCubeFace::Left
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("A zero impact normal must not resolve to a face."),
|
||||
AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(FVector::ZeroVector, ResolvedFace)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeScrambleQualityTest,
|
||||
"HyperTwist.Simulation.ClassicCube.ScrambleQuality",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeGameMode.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeOrbitPawn.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubePlayerController.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeGameModeDefaultsTest,
|
||||
"HyperTwist.Simulation.ClassicCube.GameModeDefaults",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeGameModeDefaultsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
AHyperTwistClassicCubeGameMode* GameMode = NewObject<AHyperTwistClassicCubeGameMode>();
|
||||
TestNotNull(TEXT("The classic cube game mode must be constructible."), GameMode);
|
||||
if (GameMode == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("The classic cube game mode must default to the first-party classic cube player controller."),
|
||||
GameMode->PlayerControllerClass,
|
||||
AHyperTwistClassicCubePlayerController::StaticClass()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The classic cube game mode must default to the first-party orbit camera pawn."),
|
||||
GameMode->DefaultPawnClass,
|
||||
AHyperTwistClassicCubeOrbitPawn::StaticClass()
|
||||
);
|
||||
TestTrue(TEXT("The classic cube game mode must still auto-create its HUD by default."), GameMode->bAutoCreateHud);
|
||||
TestTrue(TEXT("The classic cube game mode must still auto-spawn the cube actor by default."), GameMode->bAutoSpawnCubeActor);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubePlayerControllerDefaultsTest,
|
||||
"HyperTwist.Simulation.ClassicCube.PlayerControllerDefaults",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubePlayerControllerDefaultsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
AHyperTwistClassicCubePlayerController* PlayerController =
|
||||
NewObject<AHyperTwistClassicCubePlayerController>();
|
||||
TestNotNull(TEXT("The classic cube player controller must be constructible."), PlayerController);
|
||||
if (PlayerController == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestTrue(TEXT("The classic cube player controller must show the mouse cursor by default."), PlayerController->bShowMouseCursor);
|
||||
TestTrue(TEXT("The classic cube player controller must keep touch input enabled by default."), PlayerController->bEnableTouchTurnInput);
|
||||
TestTrue(
|
||||
TEXT("The classic cube player controller must bind a fresh-attempt shortcut by default."),
|
||||
PlayerController->bBindFreshAttemptShortcut
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeOrbitPawnDefaultsTest,
|
||||
"HyperTwist.Simulation.ClassicCube.OrbitPawnDefaults",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeOrbitPawnDefaultsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
AHyperTwistClassicCubeOrbitPawn* OrbitPawn = NewObject<AHyperTwistClassicCubeOrbitPawn>();
|
||||
TestNotNull(TEXT("The classic cube orbit pawn must be constructible."), OrbitPawn);
|
||||
if (OrbitPawn == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestNotNull(TEXT("The orbit pawn must allocate a spring arm component."), OrbitPawn->SpringArm);
|
||||
TestNotNull(TEXT("The orbit pawn must allocate a camera component."), OrbitPawn->CameraComponent);
|
||||
TestTrue(
|
||||
TEXT("The orbit pawn must expose a sane zoom range."),
|
||||
OrbitPawn->MinimumArmLength < OrbitPawn->InitialArmLength
|
||||
&& OrbitPawn->InitialArmLength < OrbitPawn->MaximumArmLength
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The orbit pawn must support at least one mouse-orbit gesture by default."),
|
||||
OrbitPawn->bUseMiddleMouseOrbit || OrbitPawn->bUseRightMouseOrbitWithShift
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -128,8 +128,9 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
### 1H — Browser Spatial Stack (13 JS repos) ✅ COMPLETE
|
||||
- [x] Add the browser-capable repos as npm dependencies or prebuilt runtime scripts in `Content/Browser/`
|
||||
- [x] Repos: `NuiLab/code-vr`, `pissang/claygl`, `pissang/clay-viewer`, `pmndrs/postprocessing`, `pmndrs/react-postprocessing`, `pmndrs/drei`, `pmndrs/uikit`, `pmndrs/three-stdlib`, `pmndrs/maath`, `pmndrs/zustand`, `pmndrs/leva`, `pmndrs/use-gesture`, `pmndrs/react-spring` + sub-packages
|
||||
- [x] Build once via npm → browser runtime shell and bundle under `Content/Browser/`
|
||||
- [x] Load the runtime in `UHyperTwistBrowserWidget`
|
||||
- [x] Build the browser runtime bundle via npm into `Content/Browser/dist/browser-spatial-runtime.js`
|
||||
- [x] Keep `Content/Browser/index.html` as the authoritative browser shell; it upgrades into the bundled runtime when present and remains runnable through the committed plain-JS fallback when build artifacts have been cleaned
|
||||
- [x] Load the authoritative shell in `UHyperTwistBrowserWidget`
|
||||
- [x] State bridge: embedded Unreal bridge object plus browser `postMessage` fallback for shell-state and command envelopes
|
||||
|
||||
### 1I — Analytics Stack (2 JS repos) ✅ COMPLETE
|
||||
|
|
@ -142,6 +143,12 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
- [x] Bundle the runtime-capable packages into `Content/Browser/` alongside already-landed `google/model-viewer`
|
||||
- [x] Keep `space-opera` visible as an editor-sidecar and `render-fidelity-tools` visible as a QA-sidecar instead of pretending either is default gameplay runtime
|
||||
|
||||
### 1K — Authoritative Browser Shell & Clean-Checkout Runtime ✅ COMPLETE
|
||||
- [x] Make `Content/Browser/index.html` the runtime authority instead of treating transient `dist/index.html` output as the shell
|
||||
- [x] Add a bootstrap loader that prefers `dist/browser-spatial-runtime.js` and falls back to a committed plain-JS runtime when the bundle is absent
|
||||
- [x] Queue early Unreal command and shell-state pushes during bootstrap so the browser lane remains deterministic even if the JS runtime finishes loading slightly later than the CEF widget
|
||||
- [x] Add shell-verification coverage proving the authoritative shell, bootstrap script, and fallback runtime exist and no longer point directly at a TypeScript source entrypoint
|
||||
|
||||
**Estimated actions:** 40–60 per repo (UHT regen on first include)
|
||||
**Estimated time:** 1–2 days per repo (build-debug cycles)
|
||||
|
||||
|
|
@ -189,6 +196,17 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
- [x] Scramble notation panel
|
||||
- [x] Bind timer start to first face turn, stop to solved-state detection
|
||||
|
||||
### 2F — First-Party Mouse/Touch Ownership Lane ✅ COMPLETE
|
||||
- [x] Add `AHyperTwistClassicCubePlayerController` as the first-party click/touch owner instead of leaving `ProcessClick()` behind implied Blueprint glue
|
||||
- [x] Bind left-click to clockwise turns, right-click to counter-clockwise turns, and touch press to clockwise turns
|
||||
- [x] Default `AHyperTwistClassicCubeGameMode` to the first-party player controller so the classic cube loop is owned in code, not only in roadmap prose
|
||||
- [x] Add focused coverage for face-resolution mapping and controller ownership defaults
|
||||
|
||||
### 2G — Loop Readiness Hardening ✅ COMPLETE
|
||||
- [x] Add a visible control-hint line to the first-party HUD so the runtime communicates how to interact with the cube without requiring extra Blueprint notes
|
||||
- [x] Bind `R` as a first-party fresh-attempt shortcut so the training loop can restart without waiting on a future content-only HUD button
|
||||
- [x] Keep the packaged-loop next step narrowed to actual packaging/level/camera/material work instead of unfinished click ownership
|
||||
|
||||
**Estimated actions:** 80–120 (geometry + input + HUD)
|
||||
**Estimated time:** 3–5 days
|
||||
|
||||
|
|
@ -202,13 +220,14 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
- [ ] Create `L_HyperTwist_ClassicTraining` (`.umap`)
|
||||
- [ ] Place `AHyperTwistClassicCubeActor` at world origin
|
||||
- [ ] Add directional light + ambient environment (basic, not immersive yet)
|
||||
- [ ] Add `WBP_HyperTwistGameHUD` to viewport
|
||||
- [ ] Add camera pawn with orbit controls (mouse drag orbits, scroll zooms)
|
||||
- [ ] Add `WBP_HyperTwistGameHUD` to viewport (optional polish replacement for the first-party runtime HUD that already auto-adds to viewport)
|
||||
- [x] Add first-party orbit camera pawn with mouse-drag orbit and scroll-wheel zoom (`AHyperTwistClassicCubeOrbitPawn`) so camera interaction is owned in code before the `.umap` polish pass exists
|
||||
|
||||
### 3B — Game Mode
|
||||
- [ ] Create `AHyperTwistClassicGameMode` : `AGameModeBase`
|
||||
- [ ] Handle game state: `Idle`, `Inspecting`, `Solving`, `Solved`
|
||||
- [ ] On `Solved`: stop timer, show solve time, offer "New Scramble"
|
||||
- [x] Create first-party `AHyperTwistClassicCubeGameMode` : `AGameModeBase`
|
||||
- [x] Handle game state across preparation, scramble playback, inspection, solving, and solved transitions
|
||||
- [x] Default to the first-party `AHyperTwistClassicCubePlayerController` so click/touch interaction is owned in code
|
||||
- [x] Add an in-HUD button for "New Scramble" alongside the keyboard shortcut so restart does not depend on remembering `R`
|
||||
|
||||
### 3C — Build & Package Validation
|
||||
- [ ] Package `UnrealHyperTwist` for Windows (not just Editor build)
|
||||
|
|
@ -323,13 +342,13 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
**Prerequisite:** Phase 2 (classic renderer)
|
||||
|
||||
### 8A — Embedded Browser Runtime
|
||||
- [ ] Add `WebBrowser` plugin to `.uproject`
|
||||
- [ ] Create `UHyperTwistBrowserWidget` wrapping `SWebBrowser`
|
||||
- [ ] Load local `three.js` + `react-three-fiber` build
|
||||
- [x] Add `WebBrowserWidget` plugin to `.uproject`
|
||||
- [x] Create `UHyperTwistBrowserWidget` wrapping `SWebBrowser`
|
||||
- [x] Load the authoritative local browser shell in `Content/Browser/index.html`, with bundled-runtime upgrade plus clean-checkout plain-JS fallback
|
||||
|
||||
### 8B — State Synchronization
|
||||
- [ ] Bridge UE cube state to browser via `FJsonObject` → JavaScript `postMessage`
|
||||
- [ ] Browser renders cube using Three.js; UE handles input and logic
|
||||
- [x] Bridge UE cube state to browser via embedded Unreal bridge object plus JavaScript `postMessage` fallback
|
||||
- [x] Browser shell surfaces adapter, analytics, viewer, and native-sidecar request flows while UE retains native input, game state, and puzzle logic ownership
|
||||
- [ ] Alternative: full browser client with HTTP API to UE backend
|
||||
|
||||
**Estimated actions:** 50–80
|
||||
|
|
@ -414,24 +433,30 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
| Add struct pairs (J-HG → J-HM) | Wire repos (freestyle, piper, rob-twophase) |
|
||||
| Tests verify contract IDs | Tests verify cube rotation, solve accuracy, speech |
|
||||
| Docs claim features are "landed" | Features are playable before documented |
|
||||
| 318 test files, 0 gameplay | 30 test files, 1 playable level |
|
||||
| 318 test files, 0 gameplay | 30+ targeted tests, 1 controller-owned classic-cube runtime loop |
|
||||
| 1,142 docs, 20 content assets | Docs trimmed to functional truth |
|
||||
| No donor repos linked | C++ repos compiled and linked |
|
||||
| No .uasset, .umap | Procedural geometry + playable level |
|
||||
| No .uasset, .umap | Procedural geometry + first-party runtime loop, with level packaging still pending |
|
||||
|
||||
---
|
||||
|
||||
## Immediate Next Step
|
||||
|
||||
**Phase 2E + 3:** Timer/HUD and First Playable Level. These require Unreal Editor
|
||||
content creation (UMG widgets, materials, levels, game mode blueprints) that cannot
|
||||
be created from text/C++ alone. The C++ backend is ready:
|
||||
**Phase 3A + 3C:** first playable level packaging and presentation proof. The repaired
|
||||
code lane now owns browser-shell authority, click/touch interaction, scramble playback,
|
||||
HUD timing, fresh-attempt restart, and orbit-camera ownership in first-party code. The remaining next move is
|
||||
content/presentation/package proof that cannot be fully closed from the current text-only lane:
|
||||
|
||||
- `AHyperTwistClassicCubeActor` with geometry, rotation, click input, scramble
|
||||
- `AHyperTwistClassicCubePlayerController` with left/right click, touch, and `R`-to-restart ownership
|
||||
- `AHyperTwistClassicCubeOrbitPawn` with middle-mouse drag orbit and scroll-wheel zoom
|
||||
- `AHyperTwistClassicCubeGameMode` with scramble settlement, inspection timing, solve timing, and solved-state submission
|
||||
- `UHyperTwistSolverLibrary` with rob-twophase two-phase solver
|
||||
- `UHyperTwistBrowserWidget` with authoritative shell loading plus bundled-runtime/fallback browser bootstrapping
|
||||
|
||||
Editor-side work needed:
|
||||
- Create 6 simple colored materials (White, Yellow, Green, Blue, Orange, Red)
|
||||
- Create `WBP_HyperTwistGameHUD` UMG widget with timer and scramble display
|
||||
- Create `L_HyperTwist_ClassicTraining` level with cube actor, lights, camera
|
||||
- Create Blueprint GameMode binding click input to `ProcessClick()`
|
||||
- Optional polish-only replacement `WBP_HyperTwistGameHUD` if the first-party runtime widget should be visually upgraded beyond the code-built HUD
|
||||
- Create `L_HyperTwist_ClassicTraining` level with cube actor, lights, and the orbit camera pawn placed or configured as the preferred presentation lane
|
||||
- Add orbit-camera controls and any packaged-build specific presentation polish
|
||||
- Package and launch the Windows build to prove the repaired runtime loop in a packaged environment
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
**Date:** 2026-06-10
|
||||
**Source authority:** `HYPERTWIST_REPO_STATE_BOARD_2026-05-11.md`, `HYPERTWIST_PHASE_0R_PACKET_0R_B_EVALUATION_2026-05-12.md`, `HYPERTWIST_REPO_EVALUATION_RESET_AND_IMPLEMENTATION_SCHEDULE_2026-05-11.md`
|
||||
**Status:** Reconstructed from canonical docs; counts verified against board totals.
|
||||
**2026-06-11 repair update:** this inventory now preserves the original queue history while also reflecting the repaired current posture for the browser-runtime and speech rows that were previously left stale after newer Phase 1 implementation work.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -16,13 +17,13 @@
|
|||
| Speech rows outside 71-row set (attribution-only) | 4 |
|
||||
| **Total non-live** | **46** |
|
||||
|
||||
The **29 in the wiring queue** are the implementation targets. The 13 reference/discard rows and the 4 speech rows are accounted for but not scheduled for direct wiring unless a later decision changes their posture.
|
||||
The **29 in the wiring queue** are the original implementation targets from the reconstructed board snapshot. The 13 reference/discard rows and the 4 speech rows are accounted for separately. Some of the original queue items have now landed through the repaired Phase 1 browser-runtime work; their current posture is called out below rather than erased from the historical count view.
|
||||
|
||||
---
|
||||
|
||||
## The 29 — Permissive Wiring Queue (0R-B minus live promotions)
|
||||
|
||||
These repos were evaluated in `Packet 0R-B` (36 repos). Seven later graduated to live status via Phase 3R and 6R packets. The remaining 29 are retained but **not yet wired** into the Unreal build.
|
||||
These repos were evaluated in `Packet 0R-B` (36 repos). Seven later graduated to live status via Phase 3R and 6R packets. This section preserves the original retained queue, but the current repaired posture now includes live/browser-backed closure for the `1E`, `1G`, `1H`, `1I`, and `1J` browser-runtime families through the authoritative `Content/Browser` shell plus the embedded `UHyperTwistBrowserWidget` bridge.
|
||||
|
||||
### Wave A — Solver & Recognition Adjuncts (2 repos)
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ These repos were evaluated in `Packet 0R-B` (36 repos). Seven later graduated to
|
|||
| # | Repo | License | Lang | What It Does | Wiring Approach | Target Phase |
|
||||
|---|------|---------|------|--------------|-----------------|--------------|
|
||||
| 22 | `KhronosGroup/glTF-Sample-Viewer` | Apache-2.0 | JS | Reference glTF viewer | Embedded browser runtime; standard compliance check | 8 (Browser renderer) |
|
||||
| 23 | `KhronosGroup/glTF-Sample-Renderer` | Apache-2.0 | C++ | Reference glTF renderer | **Direct C++ link** via `ThirdParty/`; evaluate for asset import pipeline | 2A (Geometry) |
|
||||
| 23 | `KhronosGroup/glTF-Sample-Renderer` | Apache-2.0 | C++/JS mix | Reference glTF renderer | Browser/runtime reference viewer posture through the first-party `Content/Browser` shell; do not over-claim a direct Unreal static-link lane for this packet | 1E (Browser runtime) |
|
||||
|
||||
### Wave F — React-Spring Sub-Packages (6 repos)
|
||||
|
||||
|
|
@ -89,9 +90,12 @@ These repos were evaluated in `Packet 0R-B` (36 repos). Seven later graduated to
|
|||
|
||||
### Direct C++ Link (Only for true native-link repos)
|
||||
- `efrantar/rob-twophase`
|
||||
- `rhasspy/piper`
|
||||
- `freestyle-voice/freestyle` (if embedding; otherwise IPC)
|
||||
|
||||
### First-Party External Engine / Sidecar Seam
|
||||
- `rhasspy/piper` — first-party synthesis boundary with retained donor source and local/offline voice-model posture, without overstating direct Unreal static-link closure in the current packet
|
||||
- `freestyle-voice/freestyle` — sidecar/session boundary is still the primary accepted posture unless a later packet explicitly reopens plugin-hosting work
|
||||
|
||||
### Embedded Browser Runtime (For JS/TS repos)
|
||||
- Requires `WebBrowser` plugin in `.uproject`
|
||||
- Requires Node.js build pipeline or prebuilt bundles in `Content/Browser/`
|
||||
|
|
@ -116,12 +120,12 @@ These repos were evaluated in `Packet 0R-B` (36 repos). Seven later graduated to
|
|||
| Repo | License | Status | New Wiring Plan |
|
||||
|------|---------|--------|-----------------|
|
||||
| `ggml-org/whisper.cpp` | MIT | **Replaced** | No longer primary STT; retain as benchmark/reference |
|
||||
| `rhasspy/piper` | MIT | **Queued** | Phase 1C — compile as static lib, link directly |
|
||||
| `rhasspy/piper` | MIT | **Live in first-party external-engine posture** | Phase 1C now lands `UHyperTwistSpeechLibrary::Synthesize(...)` plus bounded local/offline narration seams without pretending the donor is already a direct Unreal static-link lane |
|
||||
| `SYSTRAN/faster-whisper` | MIT | **Queued** | Retain as alternative STT benchmark; no active wiring |
|
||||
| `coqui-ai/TTS` | MPL-2.0 | **Queued** | Retain as TTS benchmark; piper is primary |
|
||||
|
||||
**STT primary:** `freestyle-voice/freestyle` (MIT, cloud-API client)
|
||||
**TTS primary:** `rhasspy/piper` (MIT, local C++ engine)
|
||||
**TTS primary:** `rhasspy/piper` (MIT, retained local/offline engine with first-party synthesis boundary)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -150,14 +154,18 @@ Not scheduled for wiring. Listed for completeness:
|
|||
|-----------|------|-------------|------------|
|
||||
| 1A | `efrantar/rob-twophase` | Static library, direct link | C++ |
|
||||
| 1B | `freestyle-voice/freestyle` | Sidecar HTTP or embedded | Rust/TS |
|
||||
| 1C | `rhasspy/piper` | Static library, direct link | C++ |
|
||||
| 1C | `rhasspy/piper` | First-party synthesis boundary over retained local/offline donor posture | C++/sidecar |
|
||||
| 1D | `brownan/Rubiks-Cube-Solver` | IPC process (GPL) | Python |
|
||||
| 1E | `KhronosGroup/glTF-Sample-Renderer` | Embedded browser runtime reference viewer | JS/browser |
|
||||
| 1F | `cahidenes/rubiks-cube-solver` | IPC process | Python |
|
||||
| 1G | `tentone/rubix-solver` | Browser fallback shell + native-sidecar bridge | Native/browser |
|
||||
|
||||
Current repaired note:
|
||||
- `1E`, `1G`, `1H`, `1I`, and `1J` now close through the authoritative `Content/Browser/index.html` shell, bundled-runtime upgrade path, committed clean-checkout fallback runtime, and embedded `UHyperTwistBrowserWidget` / bridge-object lane.
|
||||
- The browser queue is no longer blocked on retaining `dist/index.html`; build artifacts can be cleaned after validation because the authoritative shell remains runnable without them.
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
**Phase 0:** Create `.external/`, `ThirdParty/`, `.gitmodules`, and `Build.cs` rules so that Phase 1 repos can actually compile and link.
|
||||
**Current next step after repair:** package-proof and presentation-proof work, not Phase 0. The repaired code lane already owns the authoritative browser shell, native-sidecar browser bridge, first-party classic-cube player controller, first-party orbit camera pawn, and first-party timer/HUD loop including GUI or keyboard scramble restart. The remaining best move is Windows packaged-build validation plus the level/material polish that still requires Unreal Editor content work.
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ Do not collapse those three tiers into one undifferentiated "features" voice.
|
|||
| First-party provider-neutral custom-endpoint runtime routing | Implemented now | landed first-party `Phase 6R-AI` | First-party per-session provider-profile endpoint selection, runtime routing, fallback-health reflection, and bounded provider-backed transport-failure posture are now live above the landed provider-profile/BYOK and provider-routing seams. Provider-specific overlays, payment execution, provider-portal ownership, and payload shipping remain deferred. |
|
||||
| Analytics/reporting surfaces | Implemented now | landed analytics/reporting packets | Reporting is real, but bounded to accepted retained slices. |
|
||||
| Rewritten training analytics and report reference grounding | Implemented now | `apache/echarts` retained permissive lane + first-party current code | Current live `Training Analytics` reference side includes four rewritten first-party targets grounded in retained `apache/echarts`: session outcome and progress reporting, analytics data-view and export, timing-trend history and overview interaction, and the optional richer explainer or sidecar boundary. This does not displace the landed `Phase 3R-C` first-party analytics/reporting owner or elevate `ecomfe/echarts-gl` and `ecomfe/zrender` beyond support-only sidecars. |
|
||||
| Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, and the current Phase 1 browser-runtime landing now includes a first-party `Content/Browser` shell plus embedded `UHyperTwistBrowserWidget` bridge. This still is not proof of unlimited browser-shell parity. |
|
||||
| Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, and the current Phase 1 browser-runtime landing now includes a first-party authoritative `Content/Browser/index.html` shell, bundled-runtime upgrade path, committed clean-checkout plain-JS fallback runtime, and embedded `UHyperTwistBrowserWidget` bridge. This still is not proof of unlimited browser-shell parity. |
|
||||
| Rewritten browser spatial scene and renderer reference grounding | Implemented now | `mrdoob/three.js` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `mrdoob/three.js`: the browser spatial scene and renderer contract. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `react-three-fiber` renderer-bridge and `xr` session slices. |
|
||||
| Rewritten React-side browser renderer and event-bridge grounding | Implemented now | `pmndrs/react-three-fiber` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `react-three-fiber`: the React scene renderer and event bridge. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `three.js` scene substrate and `xr` session slices. |
|
||||
| Rewritten browser XR session and immersive-interaction grounding | Implemented now | `pmndrs/xr` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `pmndrs/xr`: the browser XR session and immersive interaction contract. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `three.js` scene substrate and `react-three-fiber` renderer-bridge slices. |
|
||||
|
|
@ -161,6 +161,7 @@ repo.
|
|||
| Feature | Status | Primary authority | Notes |
|
||||
|---|---|---|---|
|
||||
| Native puzzle-state runtime | Implemented now | first-party runtime + landed donor packets | Core product identity. |
|
||||
| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, and solved-state submission in code. Packaged-build proof plus art/material/level polish remain separate next-step work. |
|
||||
| Classic-cubing semantic/runtime adapter | Implemented now | landed `cubing/cubing.js` packet | Live adapter family. |
|
||||
| Classic-cubing semantic, bridge, and `MPL`-boundary reference grounding | Implemented now | `cubing/cubing.js` retained boundary-sensitive lane + first-party current code | Current live `Classic Cubing Semantics and Runtime` reference side includes seven rewritten first-party contract/reference targets grounded in `cubing/cubing.js`: semantics, geometry, viewer adapter, device boundary, search contract, Melinda bridge, and explicit `MPL` compliance-boundary notes. This does not displace the landed `Phase 4R-A` first-party owner lane, the separate `cubing/twisty.js` replay shell lane, the separate `cubing/alg.js` parser/AST lane, or the explicit practical `MPL` path and notice-retention boundary. |
|
||||
| Seeded competition scramble workflow and lightweight scramble-operator shell adjuncts | Deep-source grounded retained | `cubing/cubing.js` retained lane + `cubing/mark3` / `cubing/scramble.cubing.net` successor evaluation | Source-backed successor surfaces sharpen competition-spec workflow and operator-shell expectations above the retained scramble and visualization seams, but they do not displace `cubing/cubing.js` or `cubing/twisty.js`; `scramble-display` remains comparison-only. |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue