Complete phase 1 browser runtime and phase 2 cube loop
This commit is contained in:
parent
c2efdcaa86
commit
7b5e56fe4e
34 changed files with 3889 additions and 305 deletions
335
Content/Browser/index.html
Normal file
335
Content/Browser/index.html
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>HyperTwist Browser Runtime</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--paper: #09111c;
|
||||
--paper-raised: rgba(13, 24, 38, 0.92);
|
||||
--paper-soft: rgba(20, 36, 56, 0.84);
|
||||
--line: rgba(153, 195, 255, 0.18);
|
||||
--ink: #eff6ff;
|
||||
--ink-soft: #afc0d6;
|
||||
--accent: #8ee3ff;
|
||||
--accent-strong: #52c7ff;
|
||||
--accent-warm: #ffd38a;
|
||||
--ok: #9bf6bf;
|
||||
--warn: #ffd38a;
|
||||
--bg-bloom: radial-gradient(circle at top left, rgba(82, 199, 255, 0.22), transparent 35%),
|
||||
radial-gradient(circle at top right, rgba(255, 211, 138, 0.14), transparent 30%),
|
||||
linear-gradient(160deg, #050b14 0%, #0d1624 45%, #101c2c 100%);
|
||||
--shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
|
||||
--font-ui: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
--font-mono: "IBM Plex Mono", "Consolas", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-bloom);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
pointer-events: none;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(140deg, rgba(14, 27, 42, 0.94), rgba(11, 20, 31, 0.78));
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-title,
|
||||
.card h2 {
|
||||
margin: 0;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.section-note,
|
||||
.card p,
|
||||
.card li,
|
||||
.runtime-note {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hero-grid,
|
||||
.stack-grid,
|
||||
.utility-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.stack-grid,
|
||||
.utility-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
}
|
||||
|
||||
.metric,
|
||||
.card,
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: var(--paper-raised);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--accent);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.metric span {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(142, 227, 255, 0.42);
|
||||
background: linear-gradient(180deg, rgba(17, 32, 51, 0.96), rgba(12, 22, 35, 0.9));
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.phase-chip,
|
||||
.status-chip,
|
||||
.activation-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
border: 1px solid rgba(142, 227, 255, 0.16);
|
||||
background: rgba(18, 36, 57, 0.78);
|
||||
}
|
||||
|
||||
.phase-chip {
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.status-chip[data-status="ready"] {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.status-chip[data-status="idle"] {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.status-chip[data-status="loading"] {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.status-chip[data-status="error"] {
|
||||
color: #ffb0b0;
|
||||
}
|
||||
|
||||
.card-actions,
|
||||
.solver-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(82, 199, 255, 0.28);
|
||||
background: linear-gradient(180deg, rgba(82, 199, 255, 0.18), rgba(55, 120, 184, 0.16));
|
||||
color: var(--ink);
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(24, 39, 58, 0.8);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: rgba(142, 227, 255, 0.52);
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.panel-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
.data-block {
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: var(--paper-soft);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.data-block pre,
|
||||
.card code,
|
||||
.panel code {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #d4e8ff;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
#analytics-demo {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
#viewer-demo {
|
||||
min-height: 320px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#viewer-demo model-viewer {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(17, 28, 43, 0.92), rgba(9, 16, 27, 0.92));
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 108px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(142, 227, 255, 0.16);
|
||||
background: rgba(11, 19, 31, 0.82);
|
||||
color: var(--ink);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
border-left: 3px solid rgba(82, 199, 255, 0.32);
|
||||
padding: 10px 12px;
|
||||
background: rgba(10, 18, 28, 0.88);
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
.log-entry strong {
|
||||
color: var(--accent-warm);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#app {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/browser-spatial-runtime.ts"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,34 +1,39 @@
|
|||
{
|
||||
"name": "hypertwist-browser-runtime",
|
||||
"version": "1.0.0",
|
||||
"description": "Bundled browser runtime for HyperTwist embedded WebBrowser widget",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"description": "Buildable browser runtime and shell for HyperTwist donor-stack validation",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build-deps": "node scripts/build-dependencies.js",
|
||||
"dev": "vite"
|
||||
"dev": "vite",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.160.0",
|
||||
"@pmndrs/postprocessing": "file:../../.external/postprocessing",
|
||||
"@react-three/drei": "file:../../.external/drei",
|
||||
"@react-three/fiber": "^8.15.0",
|
||||
"leva": "file:../../.external/leva",
|
||||
"maath": "file:../../.external/maath",
|
||||
"three-stdlib": "file:../../.external/three-stdlib",
|
||||
"zustand": "file:../../.external/zustand",
|
||||
"@use-gesture/react": "file:../../.external/use-gesture",
|
||||
"@react-spring/three": "file:../../.external/react-spring",
|
||||
"@react-spring/web": "file:../../.external/react-spring",
|
||||
"echarts": "^5.4.0",
|
||||
"zrender": "file:../../.external/zrender",
|
||||
"echarts-gl": "file:../../.external/echarts-gl",
|
||||
"@google/model-viewer": "file:../../.external/model-viewer/packages/model-viewer",
|
||||
"claygl": "file:../../.external/claygl",
|
||||
"rubix-solver": "file:../../.external/rubix-solver"
|
||||
"@google/model-viewer": "^4.3.1",
|
||||
"@google/model-viewer-effects": "^1.5.0",
|
||||
"@khronosgroup/gltf-viewer": "^1.1.0",
|
||||
"@pmndrs/uikit": "^1.0.73",
|
||||
"@react-spring/three": "^10.1.1",
|
||||
"@react-spring/web": "^10.1.1",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.6.1",
|
||||
"@react-three/postprocessing": "^3.0.4",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"echarts": "^6.1.0",
|
||||
"echarts-gl": "^2.1.0",
|
||||
"leva": "^0.10.1",
|
||||
"maath": "^0.10.8",
|
||||
"postprocessing": "^6.39.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"three": "^0.183.0",
|
||||
"three-stdlib": "^2.36.1",
|
||||
"zrender": "^6.1.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
Content/Browser/src/browser-spatial-runtime.ts
Normal file
49
Content/Browser/src/browser-spatial-runtime.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { UEBridge } from './runtime/bridge';
|
||||
import { BrowserSupportAdapters, listAdapters, loadAdapter } from './runtime/registry';
|
||||
import { createBrowserShell } from './runtime/shell';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
HyperTwistBrowserRuntime?: {
|
||||
adapters: typeof BrowserSupportAdapters;
|
||||
listAdapters: typeof listAdapters;
|
||||
loadAdapter: typeof loadAdapter;
|
||||
receiveCommand: (payload: unknown) => void;
|
||||
setShellState: (payload: unknown) => void;
|
||||
renderShell: (root: HTMLElement) => void;
|
||||
sendState: (payload: unknown) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeApi = {
|
||||
adapters: BrowserSupportAdapters,
|
||||
listAdapters,
|
||||
loadAdapter,
|
||||
receiveCommand(payload: unknown) {
|
||||
UEBridge.receiveCommand(payload);
|
||||
},
|
||||
setShellState(payload: unknown) {
|
||||
UEBridge.setShellState(payload);
|
||||
},
|
||||
renderShell(root: HTMLElement) {
|
||||
createBrowserShell(root);
|
||||
},
|
||||
sendState(payload: unknown) {
|
||||
UEBridge.sendState(payload);
|
||||
}
|
||||
};
|
||||
|
||||
window.HyperTwistBrowserRuntime = runtimeApi;
|
||||
|
||||
const RootElement = document.getElementById('app');
|
||||
if (RootElement instanceof HTMLElement)
|
||||
{
|
||||
runtimeApi.renderShell(RootElement);
|
||||
}
|
||||
|
||||
UEBridge.notifyRuntimeReady({
|
||||
status: 'ready',
|
||||
runtime: 'hypertwist-browser-runtime',
|
||||
adapterCount: listAdapters().length
|
||||
});
|
||||
|
|
@ -1,71 +1,7 @@
|
|||
/**
|
||||
* HyperTwist Browser Runtime Entry Point
|
||||
*
|
||||
* Exports all bundled browser-capable libraries for use in the embedded
|
||||
* WebBrowser widget or external browser runtime.
|
||||
*
|
||||
* Phase 1 wiring targets:
|
||||
* - 1G: tentone/rubix-solver (vision + solver UI)
|
||||
* - 1H: pmndrs spatial stack (postprocessing, drei, fiber, etc.)
|
||||
* - 1I: ecomfe analytics stack (zrender, echarts-gl)
|
||||
* - 1J: google/model-viewer sub-packages
|
||||
*/
|
||||
|
||||
// Three.js core (peer dependency for most spatial packages)
|
||||
import * as THREE from 'three';
|
||||
|
||||
// pmndrs spatial stack (1H)
|
||||
export { THREE };
|
||||
export { default as postprocessing } from '@pmndrs/postprocessing';
|
||||
export { default as drei } from '@react-three/drei';
|
||||
export { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
export { default as leva } from 'leva';
|
||||
export { default as maath } from 'maath';
|
||||
export { default as threeStdlib } from 'three-stdlib';
|
||||
export { create as createZustandStore } from 'zustand';
|
||||
export { useGesture } from '@use-gesture/react';
|
||||
export { useSpring, animated } from '@react-spring/three';
|
||||
|
||||
// Analytics stack (1I)
|
||||
export { default as zrender } from 'zrender';
|
||||
export { default as echarts } from 'echarts';
|
||||
export { default as echartsGl } from 'echarts-gl';
|
||||
|
||||
// Model viewer (1J)
|
||||
export { default as ModelViewerElement } from '@google/model-viewer';
|
||||
|
||||
// claygl / clay-viewer (1H)
|
||||
export { default as claygl } from 'claygl';
|
||||
|
||||
// rubix-solver (1G)
|
||||
// Note: tentone solver is primarily a C++/OpenCV desktop app.
|
||||
// The JS bundle exports a placeholder for future WASM or web port.
|
||||
export const RubixSolver = {
|
||||
version: '1.0.0',
|
||||
note: 'C++ solver compiled as standalone executable. Web port TBD.'
|
||||
};
|
||||
|
||||
// UE Bridge: postMessage API for state synchronization
|
||||
interface UEStateBridge {
|
||||
sendState(state: object): void;
|
||||
onCommand(callback: (cmd: object) => void): void;
|
||||
}
|
||||
|
||||
export const UEBridge: UEStateBridge = {
|
||||
sendState(state: object) {
|
||||
if ((window as any).ue && (window as any).ue.hypertwist) {
|
||||
(window as any).ue.hypertwist.sendState(JSON.stringify(state));
|
||||
} else {
|
||||
window.parent.postMessage({ type: 'hypertwist-state', payload: state }, '*');
|
||||
}
|
||||
},
|
||||
onCommand(callback: (cmd: object) => void) {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'hypertwist-command') {
|
||||
callback(event.data.payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[HyperTwist Browser Runtime] Loaded');
|
||||
export {
|
||||
BrowserSupportAdapters,
|
||||
listAdapters,
|
||||
loadAdapter
|
||||
} from './runtime/registry';
|
||||
export { UEBridge } from './runtime/bridge';
|
||||
export { createBrowserShell } from './runtime/shell';
|
||||
|
|
|
|||
134
Content/Browser/src/runtime/bridge.ts
Normal file
134
Content/Browser/src/runtime/bridge.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
type RuntimeListener = (payload: unknown) => void;
|
||||
|
||||
function tryParseJsonPayload(value: unknown): unknown
|
||||
{
|
||||
if (typeof value !== 'string')
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JSON.parse(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function getUnrealBridgeHandle(): Record<string, unknown> | null
|
||||
{
|
||||
const Candidate = (window as Record<string, unknown>).ue as Record<string, unknown> | undefined;
|
||||
if (!Candidate || typeof Candidate !== 'object')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
const HyperTwistBridge = Candidate.hypertwist;
|
||||
return HyperTwistBridge && typeof HyperTwistBridge === 'object'
|
||||
? HyperTwistBridge as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
const CommandListeners = new Set<RuntimeListener>();
|
||||
const ShellStateListeners = new Set<RuntimeListener>();
|
||||
|
||||
function dispatchToListeners(Listeners: Set<RuntimeListener>, Payload: unknown): void
|
||||
{
|
||||
Listeners.forEach((Listener) => Listener(Payload));
|
||||
}
|
||||
|
||||
function postEnvelope(Type: string, Payload: unknown): void
|
||||
{
|
||||
const Envelope = {
|
||||
type: Type,
|
||||
payload: Payload,
|
||||
emittedAtUtc: new Date().toISOString()
|
||||
};
|
||||
|
||||
const SerializedEnvelope = JSON.stringify(Envelope);
|
||||
const UnrealBridgeHandle = getUnrealBridgeHandle();
|
||||
|
||||
if (UnrealBridgeHandle && typeof UnrealBridgeHandle.notifyEnvelope === 'function')
|
||||
{
|
||||
(UnrealBridgeHandle.notifyEnvelope as (EnvelopeJson: string) => void)(SerializedEnvelope);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Type === 'hypertwist-state'
|
||||
&& UnrealBridgeHandle
|
||||
&& typeof UnrealBridgeHandle.notifyState === 'function')
|
||||
{
|
||||
(UnrealBridgeHandle.notifyState as (StateJson: string) => void)(JSON.stringify(Payload));
|
||||
return;
|
||||
}
|
||||
|
||||
window.parent?.postMessage(Envelope, '*');
|
||||
}
|
||||
|
||||
window.addEventListener('message', (Event) =>
|
||||
{
|
||||
const Envelope = Event.data;
|
||||
if (!Envelope || typeof Envelope !== 'object')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TypedEnvelope = Envelope as Record<string, unknown>;
|
||||
if (TypedEnvelope.type === 'hypertwist-command')
|
||||
{
|
||||
dispatchToListeners(CommandListeners, tryParseJsonPayload(TypedEnvelope.payload));
|
||||
}
|
||||
else if (TypedEnvelope.type === 'hypertwist-shell-state')
|
||||
{
|
||||
dispatchToListeners(ShellStateListeners, tryParseJsonPayload(TypedEnvelope.payload));
|
||||
}
|
||||
});
|
||||
|
||||
export const UEBridge = {
|
||||
onCommand(Listener: RuntimeListener): () => void
|
||||
{
|
||||
CommandListeners.add(Listener);
|
||||
return () => CommandListeners.delete(Listener);
|
||||
},
|
||||
|
||||
onShellState(Listener: RuntimeListener): () => void
|
||||
{
|
||||
ShellStateListeners.add(Listener);
|
||||
return () => ShellStateListeners.delete(Listener);
|
||||
},
|
||||
|
||||
receiveCommand(Payload: unknown): void
|
||||
{
|
||||
dispatchToListeners(CommandListeners, tryParseJsonPayload(Payload));
|
||||
},
|
||||
|
||||
setShellState(Payload: unknown): void
|
||||
{
|
||||
const ParsedPayload = tryParseJsonPayload(Payload);
|
||||
dispatchToListeners(ShellStateListeners, ParsedPayload);
|
||||
},
|
||||
|
||||
sendState(Payload: unknown): void
|
||||
{
|
||||
postEnvelope('hypertwist-state', Payload);
|
||||
},
|
||||
|
||||
sendCommandResult(Payload: unknown): void
|
||||
{
|
||||
postEnvelope('hypertwist-command-result', Payload);
|
||||
},
|
||||
|
||||
notifyRuntimeReady(Payload: unknown): void
|
||||
{
|
||||
const UnrealBridgeHandle = getUnrealBridgeHandle();
|
||||
if (UnrealBridgeHandle && typeof UnrealBridgeHandle.notifyRuntimeReady === 'function')
|
||||
{
|
||||
(UnrealBridgeHandle.notifyRuntimeReady as (PayloadJson: string) => void)(JSON.stringify(Payload));
|
||||
return;
|
||||
}
|
||||
|
||||
postEnvelope('hypertwist-runtime-ready', Payload);
|
||||
}
|
||||
};
|
||||
463
Content/Browser/src/runtime/registry.ts
Normal file
463
Content/Browser/src/runtime/registry.ts
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
interface BrowserSupportAdapter {
|
||||
activation:
|
||||
| 'bundled-module'
|
||||
| 'prebuilt-script'
|
||||
| 'source-sidecar'
|
||||
| 'native-sidecar'
|
||||
| 'editor-sidecar'
|
||||
| 'qa-sidecar';
|
||||
description: string;
|
||||
id: string;
|
||||
packageName?: string;
|
||||
phase: '1E' | '1G' | '1H' | '1I' | '1J';
|
||||
repo: string;
|
||||
sourcePathHint: string;
|
||||
}
|
||||
|
||||
interface LoadedAdapter {
|
||||
details?: string[];
|
||||
id: string;
|
||||
module?: unknown;
|
||||
note?: string;
|
||||
status: 'loaded' | 'deferred';
|
||||
}
|
||||
|
||||
const ScriptLoadCache = new Map<string, Promise<unknown>>();
|
||||
|
||||
function isDistShell(): boolean
|
||||
{
|
||||
return new URL(document.baseURI).pathname.includes('/Content/Browser/dist/');
|
||||
}
|
||||
|
||||
function resolveRepoAssetUrl(RepoRelativePath: string): string
|
||||
{
|
||||
const Prefix = isDistShell() ? '../../../' : '../../';
|
||||
return new URL(`${Prefix}${RepoRelativePath}`, document.baseURI).toString();
|
||||
}
|
||||
|
||||
function loadExternalScript(RepoRelativePath: string): Promise<unknown>
|
||||
{
|
||||
const ResolvedUrl = resolveRepoAssetUrl(RepoRelativePath);
|
||||
if (ScriptLoadCache.has(ResolvedUrl))
|
||||
{
|
||||
return ScriptLoadCache.get(ResolvedUrl) as Promise<unknown>;
|
||||
}
|
||||
|
||||
const LoadPromise = new Promise<unknown>((Resolve, Reject) =>
|
||||
{
|
||||
const ExistingElement = document.querySelector<HTMLScriptElement>(
|
||||
`script[data-hypertwist-src="${ResolvedUrl}"]`
|
||||
);
|
||||
if (ExistingElement)
|
||||
{
|
||||
ExistingElement.addEventListener('load', () => Resolve(undefined), { once: true });
|
||||
ExistingElement.addEventListener('error', () => Reject(new Error(`Failed to load ${ResolvedUrl}`)), { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const ScriptElement = document.createElement('script');
|
||||
ScriptElement.src = ResolvedUrl;
|
||||
ScriptElement.async = true;
|
||||
ScriptElement.dataset.hypertwistSrc = ResolvedUrl;
|
||||
ScriptElement.onload = () => Resolve(undefined);
|
||||
ScriptElement.onerror = () => Reject(new Error(`Failed to load ${ResolvedUrl}`));
|
||||
document.head.appendChild(ScriptElement);
|
||||
});
|
||||
|
||||
ScriptLoadCache.set(ResolvedUrl, LoadPromise);
|
||||
return LoadPromise;
|
||||
}
|
||||
|
||||
async function loadSpatialStack(): Promise<LoadedAdapter>
|
||||
{
|
||||
const [
|
||||
Fiber,
|
||||
Drei,
|
||||
ReactPostprocessing,
|
||||
Postprocessing,
|
||||
Leva,
|
||||
Maath,
|
||||
ThreeStdLib,
|
||||
Zustand,
|
||||
UseGesture,
|
||||
ReactSpringThree,
|
||||
ReactSpringWeb,
|
||||
UIKit
|
||||
] = await Promise.all([
|
||||
import('@react-three/fiber'),
|
||||
import('@react-three/drei'),
|
||||
import('@react-three/postprocessing'),
|
||||
import('postprocessing'),
|
||||
import('leva'),
|
||||
import('maath'),
|
||||
import('three-stdlib'),
|
||||
import('zustand'),
|
||||
import('@use-gesture/react'),
|
||||
import('@react-spring/three'),
|
||||
import('@react-spring/web'),
|
||||
import('@pmndrs/uikit')
|
||||
]);
|
||||
|
||||
return {
|
||||
id: 'phase-1h-spatial-stack',
|
||||
status: 'loaded',
|
||||
module: {
|
||||
Fiber,
|
||||
Drei,
|
||||
ReactPostprocessing,
|
||||
Postprocessing,
|
||||
Leva,
|
||||
Maath,
|
||||
ThreeStdLib,
|
||||
Zustand,
|
||||
UseGesture,
|
||||
ReactSpringThree,
|
||||
ReactSpringWeb,
|
||||
UIKit
|
||||
},
|
||||
details: [
|
||||
'@react-three/fiber',
|
||||
'@react-three/drei',
|
||||
'@react-three/postprocessing',
|
||||
'postprocessing',
|
||||
'leva',
|
||||
'maath',
|
||||
'three-stdlib',
|
||||
'zustand',
|
||||
'@use-gesture/react',
|
||||
'@react-spring/three',
|
||||
'@react-spring/web',
|
||||
'@pmndrs/uikit'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAnalyticsStack(): Promise<LoadedAdapter>
|
||||
{
|
||||
const [ECharts, ZRender] = await Promise.all([
|
||||
import('echarts'),
|
||||
import('zrender')
|
||||
]);
|
||||
await import('echarts-gl');
|
||||
|
||||
return {
|
||||
id: 'phase-1i-analytics-stack',
|
||||
status: 'loaded',
|
||||
module: {
|
||||
ECharts,
|
||||
ZRender
|
||||
},
|
||||
details: [
|
||||
'echarts',
|
||||
'zrender',
|
||||
'echarts-gl'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async function loadViewerStack(): Promise<LoadedAdapter>
|
||||
{
|
||||
await Promise.all([
|
||||
import('@google/model-viewer'),
|
||||
import('@google/model-viewer-effects')
|
||||
]);
|
||||
const GltfViewer = await import('@khronosgroup/gltf-viewer');
|
||||
|
||||
return {
|
||||
id: 'phase-1j-viewer-stack',
|
||||
status: 'loaded',
|
||||
module: {
|
||||
ModelViewerElement: customElements.get('model-viewer'),
|
||||
GltfViewer
|
||||
},
|
||||
details: [
|
||||
'@google/model-viewer',
|
||||
'@google/model-viewer-effects',
|
||||
'@khronosgroup/gltf-viewer'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async function loadClayViewerStack(): Promise<LoadedAdapter>
|
||||
{
|
||||
await loadExternalScript('.external/claygl/dist/claygl.js');
|
||||
await loadExternalScript('.external/clay-viewer/dist/clay-viewer.js');
|
||||
|
||||
const GlobalWindow = window as Window & {
|
||||
ClayViewer?: unknown;
|
||||
clay?: unknown;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'phase-1h-clay-viewer-stack',
|
||||
status: 'loaded',
|
||||
module: {
|
||||
clay: GlobalWindow.clay,
|
||||
ClayViewer: GlobalWindow.ClayViewer
|
||||
},
|
||||
details: [
|
||||
'.external/claygl/dist/claygl.js',
|
||||
'.external/clay-viewer/dist/clay-viewer.js'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
|
||||
{
|
||||
id: '1e-gltf-sample-renderer',
|
||||
phase: '1E',
|
||||
repo: 'KhronosGroup/glTF-Sample-Renderer',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@khronosgroup/gltf-viewer',
|
||||
sourcePathHint: '.external/glTF-Sample-Renderer',
|
||||
description: 'Reference glTF renderer and viewer module wired into the first-party browser runtime.'
|
||||
},
|
||||
{
|
||||
id: '1g-tentone-native-solver',
|
||||
phase: '1G',
|
||||
repo: 'tentone/rubix-solver',
|
||||
activation: 'native-sidecar',
|
||||
sourcePathHint: '.external/rubix-solver',
|
||||
description: 'Native/OpenCV solver adjunct retained as a browser-shell fallback lane rather than a direct npm package.'
|
||||
},
|
||||
{
|
||||
id: '1h-code-vr',
|
||||
phase: '1H',
|
||||
repo: 'NuiLab/code-vr',
|
||||
activation: 'source-sidecar',
|
||||
sourcePathHint: '.external/code-vr',
|
||||
description: 'Rust donor retained as a source-sidecar surface for follow-up browser spatial composition.'
|
||||
},
|
||||
{
|
||||
id: '1h-claygl',
|
||||
phase: '1H',
|
||||
repo: 'pissang/claygl',
|
||||
activation: 'prebuilt-script',
|
||||
sourcePathHint: '.external/claygl/dist/claygl.js',
|
||||
description: 'Prebuilt ClayGL runtime script bridged into the browser shell for viewer and QA surfaces.'
|
||||
},
|
||||
{
|
||||
id: '1h-clay-viewer',
|
||||
phase: '1H',
|
||||
repo: 'pissang/clay-viewer',
|
||||
activation: 'prebuilt-script',
|
||||
sourcePathHint: '.external/clay-viewer/dist/clay-viewer.js',
|
||||
description: 'Prebuilt Clay Viewer shell loaded beside ClayGL for bounded browser inspection.'
|
||||
},
|
||||
{
|
||||
id: '1h-postprocessing',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/postprocessing',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'postprocessing',
|
||||
sourcePathHint: '.external/postprocessing',
|
||||
description: 'Three.js post-processing core bundled into the runtime module graph.'
|
||||
},
|
||||
{
|
||||
id: '1h-react-postprocessing',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/react-postprocessing',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@react-three/postprocessing',
|
||||
sourcePathHint: '.external/react-postprocessing',
|
||||
description: 'React bridge for post-processing loaded into the browser spatial stack.'
|
||||
},
|
||||
{
|
||||
id: '1h-drei',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/drei',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@react-three/drei',
|
||||
sourcePathHint: '.external/drei',
|
||||
description: 'Drei helper surface bundled for browser-spatial utility composition.'
|
||||
},
|
||||
{
|
||||
id: '1h-uikit',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/uikit',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@pmndrs/uikit',
|
||||
sourcePathHint: '.external/uikit',
|
||||
description: 'World-anchored UI kit bundled for browser-spatial HUD and control surfaces.'
|
||||
},
|
||||
{
|
||||
id: '1h-three-stdlib',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/three-stdlib',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'three-stdlib',
|
||||
sourcePathHint: '.external/three-stdlib',
|
||||
description: 'Three.js helper library bundled for browser runtime support.'
|
||||
},
|
||||
{
|
||||
id: '1h-maath',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/maath',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'maath',
|
||||
sourcePathHint: '.external/maath',
|
||||
description: 'Animation and math helper pack bundled into the runtime.'
|
||||
},
|
||||
{
|
||||
id: '1h-zustand',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/zustand',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'zustand',
|
||||
sourcePathHint: '.external/zustand',
|
||||
description: 'State-store runtime bundled for browser shell state and derived panel demos.'
|
||||
},
|
||||
{
|
||||
id: '1h-leva',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/leva',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'leva',
|
||||
sourcePathHint: '.external/leva',
|
||||
description: 'Parameter-control runtime bundled for future tuning overlays.'
|
||||
},
|
||||
{
|
||||
id: '1h-use-gesture',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/use-gesture',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@use-gesture/react',
|
||||
sourcePathHint: '.external/use-gesture',
|
||||
description: 'Gesture capture hooks bundled for browser interaction surfaces.'
|
||||
},
|
||||
{
|
||||
id: '1h-react-spring',
|
||||
phase: '1H',
|
||||
repo: 'pmndrs/react-spring',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@react-spring/three and @react-spring/web',
|
||||
sourcePathHint: '.external/react-spring',
|
||||
description: 'Motion and interpolation runtime bundled for browser spatial transitions.'
|
||||
},
|
||||
{
|
||||
id: '1i-zrender',
|
||||
phase: '1I',
|
||||
repo: 'ecomfe/zrender',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'zrender',
|
||||
sourcePathHint: '.external/zrender',
|
||||
description: '2D render engine bundled beneath the analytics surface.'
|
||||
},
|
||||
{
|
||||
id: '1i-echarts-gl',
|
||||
phase: '1I',
|
||||
repo: 'ecomfe/echarts-gl',
|
||||
activation: 'bundled-module',
|
||||
packageName: 'echarts-gl',
|
||||
sourcePathHint: '.external/echarts-gl',
|
||||
description: '3D analytics extension bundled beneath the ECharts runtime.'
|
||||
},
|
||||
{
|
||||
id: '1j-model-viewer',
|
||||
phase: '1J',
|
||||
repo: 'google/model-viewer/packages/model-viewer',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@google/model-viewer',
|
||||
sourcePathHint: '.external/model-viewer/packages/model-viewer',
|
||||
description: 'Model Viewer web component bundled into the browser shell.'
|
||||
},
|
||||
{
|
||||
id: '1j-model-viewer-effects',
|
||||
phase: '1J',
|
||||
repo: 'google/model-viewer/packages/model-viewer-effects',
|
||||
activation: 'bundled-module',
|
||||
packageName: '@google/model-viewer-effects',
|
||||
sourcePathHint: '.external/model-viewer/packages/model-viewer-effects',
|
||||
description: 'Effects package bundled beside model-viewer for decorated previews.'
|
||||
},
|
||||
{
|
||||
id: '1j-space-opera',
|
||||
phase: '1J',
|
||||
repo: 'google/model-viewer/packages/space-opera',
|
||||
activation: 'editor-sidecar',
|
||||
sourcePathHint: '.external/model-viewer/packages/space-opera',
|
||||
description: 'Editor-oriented inspection UI retained as a sidecar instead of a default runtime export.'
|
||||
},
|
||||
{
|
||||
id: '1j-render-fidelity-tools',
|
||||
phase: '1J',
|
||||
repo: 'google/model-viewer/packages/render-fidelity-tools',
|
||||
activation: 'qa-sidecar',
|
||||
sourcePathHint: '.external/model-viewer/packages/render-fidelity-tools',
|
||||
description: 'Renderer comparison and fidelity tooling kept in the QA lane rather than loaded by default.'
|
||||
}
|
||||
];
|
||||
|
||||
export function listAdapters(): BrowserSupportAdapter[]
|
||||
{
|
||||
return [...BrowserSupportAdapters];
|
||||
}
|
||||
|
||||
export async function loadAdapter(AdapterId: string): Promise<LoadedAdapter>
|
||||
{
|
||||
switch (AdapterId)
|
||||
{
|
||||
case '1e-gltf-sample-renderer':
|
||||
return loadViewerStack();
|
||||
|
||||
case '1g-tentone-native-solver':
|
||||
return {
|
||||
id: AdapterId,
|
||||
status: 'deferred',
|
||||
note: 'Use the browser shell fallback form or Unreal IPC to route into the native tentone/OpenCV lane.'
|
||||
};
|
||||
|
||||
case '1h-code-vr':
|
||||
return {
|
||||
id: AdapterId,
|
||||
status: 'deferred',
|
||||
note: 'code-vr stays source-sidecar until a dedicated Rust or WASM bridge is assigned.'
|
||||
};
|
||||
|
||||
case '1h-claygl':
|
||||
case '1h-clay-viewer':
|
||||
return loadClayViewerStack();
|
||||
|
||||
case '1h-postprocessing':
|
||||
case '1h-react-postprocessing':
|
||||
case '1h-drei':
|
||||
case '1h-uikit':
|
||||
case '1h-three-stdlib':
|
||||
case '1h-maath':
|
||||
case '1h-zustand':
|
||||
case '1h-leva':
|
||||
case '1h-use-gesture':
|
||||
case '1h-react-spring':
|
||||
return loadSpatialStack();
|
||||
|
||||
case '1i-zrender':
|
||||
case '1i-echarts-gl':
|
||||
return loadAnalyticsStack();
|
||||
|
||||
case '1j-model-viewer':
|
||||
case '1j-model-viewer-effects':
|
||||
return loadViewerStack();
|
||||
|
||||
case '1j-space-opera':
|
||||
return {
|
||||
id: AdapterId,
|
||||
status: 'deferred',
|
||||
note: 'space-opera remains intentionally outside the default runtime and is surfaced as an editor-sidecar.'
|
||||
};
|
||||
|
||||
case '1j-render-fidelity-tools':
|
||||
return {
|
||||
id: AdapterId,
|
||||
status: 'deferred',
|
||||
note: 'render-fidelity-tools stays in the QA lane and is not auto-loaded in the browser shell.'
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown browser support adapter: ${AdapterId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveSupportAssetUrl(RepoRelativePath: string): string
|
||||
{
|
||||
return resolveRepoAssetUrl(RepoRelativePath);
|
||||
}
|
||||
393
Content/Browser/src/runtime/shell.ts
Normal file
393
Content/Browser/src/runtime/shell.ts
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { UEBridge } from './bridge';
|
||||
import {
|
||||
BrowserSupportAdapters,
|
||||
listAdapters,
|
||||
loadAdapter,
|
||||
resolveSupportAssetUrl
|
||||
} from './registry';
|
||||
|
||||
type AdapterStatus = 'idle' | 'loading' | 'ready' | 'error';
|
||||
|
||||
function createElement<K extends keyof HTMLElementTagNameMap>(
|
||||
TagName: K,
|
||||
ClassName?: string,
|
||||
TextContent?: string
|
||||
): HTMLElementTagNameMap[K]
|
||||
{
|
||||
const Element = document.createElement(TagName);
|
||||
if (ClassName)
|
||||
{
|
||||
Element.className = ClassName;
|
||||
}
|
||||
if (TextContent)
|
||||
{
|
||||
Element.textContent = TextContent;
|
||||
}
|
||||
return Element;
|
||||
}
|
||||
|
||||
function serializePretty(Payload: unknown): string
|
||||
{
|
||||
if (typeof Payload === 'string')
|
||||
{
|
||||
return Payload;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JSON.stringify(Payload, null, 2);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return String(Payload);
|
||||
}
|
||||
}
|
||||
|
||||
export function createBrowserShell(RootElement: HTMLElement): void
|
||||
{
|
||||
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,
|
||||
'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.'
|
||||
);
|
||||
const HeroMetrics = createElement('div', 'hero-grid');
|
||||
|
||||
const Metrics = [
|
||||
{
|
||||
label: 'Adapters surfaced',
|
||||
value: String(BrowserSupportAdapters.length)
|
||||
},
|
||||
{
|
||||
label: 'Phase 1 coverage',
|
||||
value: '1E, 1G, 1H, 1I, 1J'
|
||||
},
|
||||
{
|
||||
label: 'Bridge route',
|
||||
value: 'UE BindUObject + postMessage fallback'
|
||||
}
|
||||
];
|
||||
|
||||
Metrics.forEach((Metric) =>
|
||||
{
|
||||
const MetricElement = createElement('div', 'metric');
|
||||
const MetricLabel = createElement('strong', undefined, Metric.label);
|
||||
const MetricValue = createElement('span', undefined, Metric.value);
|
||||
MetricElement.append(MetricLabel, MetricValue);
|
||||
HeroMetrics.appendChild(MetricElement);
|
||||
});
|
||||
|
||||
HeroElement.append(HeroTitle, HeroParagraph, HeroMetrics);
|
||||
|
||||
const AdapterSection = createElement('section', 'section');
|
||||
const AdapterHeader = createElement('div', 'section-header');
|
||||
const AdapterTitle = createElement('h2', 'section-title', 'Phase 1 Adapter Surface');
|
||||
const AdapterNote = createElement(
|
||||
'p',
|
||||
'section-note',
|
||||
'Load any adapter to prove the runtime can resolve it, or inspect the activation type when a donor is intentionally retained as a sidecar.'
|
||||
);
|
||||
AdapterHeader.append(AdapterTitle, AdapterNote);
|
||||
|
||||
const AdapterGrid = createElement('div', 'stack-grid');
|
||||
const StatusElements = new Map<string, HTMLElement>();
|
||||
|
||||
listAdapters().forEach((Adapter) =>
|
||||
{
|
||||
const CardElement = createElement('article', 'card');
|
||||
const CardHeader = createElement('div', 'card-header');
|
||||
const CardTitleBlock = createElement('div');
|
||||
const PhaseChip = createElement('span', 'phase-chip', Adapter.phase);
|
||||
const CardTitle = createElement('h2', undefined, Adapter.repo);
|
||||
const ActivationChip = createElement('span', 'activation-chip', Adapter.activation);
|
||||
const StatusChip = createElement('span', 'status-chip', 'idle');
|
||||
StatusChip.dataset.status = 'idle';
|
||||
StatusElements.set(Adapter.id, StatusChip);
|
||||
|
||||
CardTitleBlock.append(PhaseChip, CardTitle);
|
||||
CardHeader.append(CardTitleBlock, StatusChip);
|
||||
|
||||
const Description = createElement('p', undefined, Adapter.description);
|
||||
const SourceCode = createElement('code', undefined, `${Adapter.sourcePathHint}${Adapter.packageName ? ` -> ${Adapter.packageName}` : ''}`);
|
||||
const Actions = createElement('div', 'card-actions');
|
||||
const LoadButton = createElement('button', undefined, 'Load Adapter');
|
||||
const InspectButton = createElement('button', 'secondary', 'Send Envelope');
|
||||
|
||||
LoadButton.addEventListener('click', async () =>
|
||||
{
|
||||
StatusChip.dataset.status = 'loading';
|
||||
StatusChip.textContent = 'loading';
|
||||
|
||||
try
|
||||
{
|
||||
const Result = await loadAdapter(Adapter.id);
|
||||
StatusChip.dataset.status = Result.status === 'loaded' ? 'ready' : 'idle';
|
||||
StatusChip.textContent = Result.status;
|
||||
appendLog(
|
||||
`${Adapter.repo}`,
|
||||
Result.note
|
||||
? Result.note
|
||||
: `Loaded ${Result.details?.join(', ') || Adapter.id}.`
|
||||
);
|
||||
if (Adapter.phase === '1I' && Result.status === 'loaded')
|
||||
{
|
||||
mountAnalyticsDemo();
|
||||
}
|
||||
if ((Adapter.phase === '1J' || Adapter.phase === '1E') && Result.status === 'loaded')
|
||||
{
|
||||
mountViewerDemo();
|
||||
}
|
||||
}
|
||||
catch (Error)
|
||||
{
|
||||
StatusChip.dataset.status = 'error';
|
||||
StatusChip.textContent = 'error';
|
||||
appendLog(`${Adapter.repo}`, Error instanceof Error ? Error.message : 'Unknown load failure.');
|
||||
}
|
||||
});
|
||||
|
||||
InspectButton.addEventListener('click', () =>
|
||||
{
|
||||
UEBridge.sendState({
|
||||
source: 'browser-runtime-shell',
|
||||
adapterId: Adapter.id,
|
||||
repo: Adapter.repo,
|
||||
activation: Adapter.activation
|
||||
});
|
||||
appendLog(Adapter.repo, 'Sent adapter envelope to the Unreal bridge.');
|
||||
});
|
||||
|
||||
Actions.append(LoadButton, InspectButton);
|
||||
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', 'Runtime Utilities'),
|
||||
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.'
|
||||
)
|
||||
);
|
||||
|
||||
const UtilityGrid = createElement('div', 'panel-grid');
|
||||
|
||||
const AnalyticsPanel = createElement('section', 'panel');
|
||||
AnalyticsPanel.append(
|
||||
createElement('h2', undefined, 'Analytics Demo'),
|
||||
createElement(
|
||||
'p',
|
||||
'runtime-note',
|
||||
'Loads the ECharts + zrender + echarts-gl lane and renders a small solve-trend chart.'
|
||||
)
|
||||
);
|
||||
const AnalyticsActions = createElement('div', 'solver-actions');
|
||||
const LoadAnalyticsButton = createElement('button', undefined, 'Load Analytics Demo');
|
||||
LoadAnalyticsButton.addEventListener('click', async () =>
|
||||
{
|
||||
await loadAdapter('1i-zrender');
|
||||
mountAnalyticsDemo();
|
||||
appendLog('Analytics Demo', 'Loaded the analytics bundle and rendered the demo chart.');
|
||||
});
|
||||
AnalyticsActions.appendChild(LoadAnalyticsButton);
|
||||
const AnalyticsDemo = createElement('div', 'data-block');
|
||||
AnalyticsDemo.id = 'analytics-demo';
|
||||
AnalyticsDemo.textContent = 'Analytics chart will mount here after the adapter loads.';
|
||||
AnalyticsPanel.append(AnalyticsActions, AnalyticsDemo);
|
||||
|
||||
const ViewerPanel = createElement('section', 'panel');
|
||||
ViewerPanel.append(
|
||||
createElement('h2', undefined, 'Viewer Demo'),
|
||||
createElement(
|
||||
'p',
|
||||
'runtime-note',
|
||||
'Loads the model-viewer and Khronos reference-viewer lane, then mounts a bounded local sample asset.'
|
||||
)
|
||||
);
|
||||
const ViewerActions = createElement('div', 'solver-actions');
|
||||
const LoadViewerButton = createElement('button', undefined, 'Load Viewer Demo');
|
||||
LoadViewerButton.addEventListener('click', async () =>
|
||||
{
|
||||
await loadAdapter('1j-model-viewer');
|
||||
mountViewerDemo();
|
||||
appendLog('Viewer Demo', 'Loaded the model-viewer stack and mounted the local sample asset.');
|
||||
});
|
||||
ViewerActions.appendChild(LoadViewerButton);
|
||||
const ViewerDemo = createElement('div', 'data-block');
|
||||
ViewerDemo.id = 'viewer-demo';
|
||||
ViewerDemo.textContent = 'Viewer preview will appear here after the adapter loads.';
|
||||
ViewerPanel.append(ViewerActions, ViewerDemo);
|
||||
|
||||
const SolverPanel = createElement('section', 'panel');
|
||||
SolverPanel.append(
|
||||
createElement('h2', undefined, 'Tentone Fallback Solver'),
|
||||
createElement(
|
||||
'p',
|
||||
'runtime-note',
|
||||
'The tentone donor is native/OpenCV, so the browser shell uses a fallback form that sends a solver request envelope back to Unreal instead of faking a browser import.'
|
||||
)
|
||||
);
|
||||
const SolverField = createElement('label', 'field');
|
||||
SolverField.appendChild(createElement('span', undefined, 'Classic facelet string'));
|
||||
const SolverInput = createElement('textarea') as HTMLTextAreaElement;
|
||||
SolverInput.value = 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB';
|
||||
SolverField.appendChild(SolverInput);
|
||||
const SolverActions = createElement('div', 'solver-actions');
|
||||
const SendSolverButton = createElement('button', undefined, 'Send Solver Request');
|
||||
SendSolverButton.addEventListener('click', () =>
|
||||
{
|
||||
UEBridge.sendState({
|
||||
type: 'tentone-native-solver-request',
|
||||
source: 'browser-shell-fallback',
|
||||
faceletString: SolverInput.value.trim()
|
||||
});
|
||||
appendLog('Tentone Fallback Solver', 'Sent a native-sidecar solver request envelope to Unreal.');
|
||||
});
|
||||
SolverActions.appendChild(SendSolverButton);
|
||||
SolverPanel.append(SolverField, SolverActions);
|
||||
|
||||
const StatePanel = createElement('section', 'panel');
|
||||
StatePanel.append(
|
||||
createElement('h2', undefined, 'Browser Shell State'),
|
||||
createElement(
|
||||
'p',
|
||||
'runtime-note',
|
||||
'Unreal can push browser-shell state or generic commands into this page through the embedded bridge.'
|
||||
)
|
||||
);
|
||||
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',
|
||||
'Compact bridge and adapter events are recorded here for Unreal-side inspection.'
|
||||
)
|
||||
);
|
||||
const LogList = createElement('div', 'log-list');
|
||||
LogPanel.appendChild(LogList);
|
||||
|
||||
function appendLog(Title: string, Message: string): void
|
||||
{
|
||||
const Entry = createElement('div', 'log-entry');
|
||||
const TitleElement = createElement('strong', undefined, Title);
|
||||
const MessageElement = createElement('div', undefined, Message);
|
||||
Entry.append(TitleElement, MessageElement);
|
||||
LogList.prepend(Entry);
|
||||
}
|
||||
|
||||
function mountAnalyticsDemo(): void
|
||||
{
|
||||
const ChartHost = document.getElementById('analytics-demo');
|
||||
if (!(ChartHost instanceof HTMLElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
import('echarts').then((EChartsModule) =>
|
||||
{
|
||||
const ExistingInstance = EChartsModule.getInstanceByDom(ChartHost);
|
||||
const Chart = ExistingInstance || EChartsModule.init(ChartHost, undefined, { renderer: 'canvas' });
|
||||
Chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 24,
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
left: 32
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Solve 1', 'Solve 2', 'Solve 3', 'Solve 4', 'Solve 5'],
|
||||
axisLabel: {
|
||||
color: '#afc0d6'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#afc0d6',
|
||||
formatter: (Value: number) => `${Value}s`
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
data: [24.3, 22.7, 21.8, 20.9, 19.4],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#8ee3ff',
|
||||
width: 3
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(82, 199, 255, 0.18)'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mountViewerDemo(): void
|
||||
{
|
||||
const ViewerHost = document.getElementById('viewer-demo');
|
||||
if (!(ViewerHost instanceof HTMLElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewerHost.innerHTML = '';
|
||||
const ModelViewer = document.createElement('model-viewer');
|
||||
ModelViewer.setAttribute(
|
||||
'src',
|
||||
resolveSupportAssetUrl('.external/model-viewer/packages/shared-assets/models/cube.gltf')
|
||||
);
|
||||
ModelViewer.setAttribute(
|
||||
'environment-image',
|
||||
resolveSupportAssetUrl('.external/model-viewer/packages/shared-assets/environments/aircraft_workshop_01_1k.hdr')
|
||||
);
|
||||
ModelViewer.setAttribute('camera-controls', '');
|
||||
ModelViewer.setAttribute('auto-rotate', '');
|
||||
ModelViewer.setAttribute('shadow-intensity', '1');
|
||||
ModelViewer.setAttribute('exposure', '1.1');
|
||||
ViewerHost.appendChild(ModelViewer);
|
||||
}
|
||||
|
||||
UtilityGrid.append(
|
||||
AnalyticsPanel,
|
||||
ViewerPanel,
|
||||
SolverPanel,
|
||||
StatePanel,
|
||||
LogPanel
|
||||
);
|
||||
UtilitySection.append(UtilityHeader, UtilityGrid);
|
||||
|
||||
ShellElement.append(HeroElement, AdapterSection, UtilitySection);
|
||||
RootElement.appendChild(ShellElement);
|
||||
|
||||
UEBridge.onCommand((Payload) =>
|
||||
{
|
||||
appendLog('UE Command', serializePretty(Payload));
|
||||
});
|
||||
|
||||
UEBridge.onShellState((Payload) =>
|
||||
{
|
||||
StatePre.textContent = serializePretty(Payload);
|
||||
appendLog('Browser Shell State', 'Received fresh shell state from Unreal.');
|
||||
});
|
||||
}
|
||||
|
|
@ -3,22 +3,19 @@ import { resolve } from 'path';
|
|||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'HyperTwistBrowserRuntime',
|
||||
fileName: (format) => `hypertwist-browser-runtime.${format}.js`,
|
||||
formats: ['umd', 'es']
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
output: {
|
||||
globals: {},
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
},
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
sourcemap: true
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
shell: resolve(__dirname, 'index.html')
|
||||
},
|
||||
output: {
|
||||
assetFileNames: 'assets/[name][extname]',
|
||||
chunkFileNames: 'assets/[name].js',
|
||||
entryFileNames: 'assets/[name].js'
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Output: Space-separated WCA move notation (U, D, F, B, L, R, with ' and 2 suffix
|
|||
"""
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
|
@ -24,6 +25,8 @@ BROWNAN_TO_WCA = {
|
|||
"R": "R",
|
||||
}
|
||||
|
||||
VALID_BROWNAN_FACES = set(BROWNAN_TO_WCA.keys())
|
||||
|
||||
|
||||
def find_brownan_exe():
|
||||
"""Locate the brownan solver executable next to this wrapper."""
|
||||
|
|
@ -32,13 +35,17 @@ def find_brownan_exe():
|
|||
os.path.join(script_dir, "..", "..", "UnrealHyperTwist", "Binaries", "Win64", "Solvers", "brownan-solver.exe"),
|
||||
os.path.join(script_dir, "brownan-solver.exe"),
|
||||
os.path.join(script_dir, "..", "brownan-solver.exe"),
|
||||
"brownan-solver.exe",
|
||||
"brownan-solver",
|
||||
]
|
||||
for candidate in candidates:
|
||||
resolved = os.path.abspath(candidate)
|
||||
if os.path.isfile(resolved):
|
||||
return resolved
|
||||
|
||||
for candidate in ("brownan-solver.exe", "brownan-solver"):
|
||||
resolved = shutil.which(candidate)
|
||||
if resolved:
|
||||
return os.path.abspath(resolved)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -79,6 +86,42 @@ def convert_move(brownan_move: str) -> str:
|
|||
return f"{wca_face}{suffix}"
|
||||
|
||||
|
||||
def is_brownan_move_token(token: str) -> bool:
|
||||
"""Return True when a token matches brownan's native move format."""
|
||||
token = token.strip()
|
||||
if not token:
|
||||
return False
|
||||
if token.startswith("2"):
|
||||
return len(token) == 2 and token[1] in VALID_BROWNAN_FACES
|
||||
if token.endswith("'"):
|
||||
return len(token) == 2 and token[0] in VALID_BROWNAN_FACES
|
||||
return len(token) == 1 and token in VALID_BROWNAN_FACES
|
||||
|
||||
|
||||
def parse_solution_moves(stdout: str) -> str:
|
||||
"""Extract the first parseable brownan solution line from solver stdout."""
|
||||
in_solution = False
|
||||
for line in stdout.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped == "Solution found!":
|
||||
in_solution = True
|
||||
continue
|
||||
if not in_solution or not stripped:
|
||||
continue
|
||||
# The actual move sequence can be preceded by brownan status chatter.
|
||||
if stripped.startswith("Continuing"):
|
||||
continue
|
||||
|
||||
tokens = stripped.split()
|
||||
if not all(is_brownan_move_token(token) for token in tokens):
|
||||
continue
|
||||
|
||||
moves = [convert_move(token) for token in tokens]
|
||||
return " ".join(move for move in moves if move)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def facelet_to_brownan_raw(facelet: str) -> str:
|
||||
"""Convert a 54-character kociemba facelet string to brownan 120-char raw format."""
|
||||
if len(facelet) != 54:
|
||||
|
|
@ -243,28 +286,20 @@ def solve(cube_input: str, brownan_exe: str = None) -> str:
|
|||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
lines = result.stdout.splitlines()
|
||||
in_solution = False
|
||||
moves = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped == "Solution found!":
|
||||
in_solution = True
|
||||
continue
|
||||
if in_solution:
|
||||
if not stripped:
|
||||
continue
|
||||
# Skip the "Continuing to look..." status line; the actual move
|
||||
# sequence is on the following line.
|
||||
if stripped.startswith("Continuing"):
|
||||
continue
|
||||
for token in stripped.split():
|
||||
wca = convert_move(token)
|
||||
if wca:
|
||||
moves.append(wca)
|
||||
break
|
||||
if result.returncode != 0:
|
||||
details = result.stderr.strip() or result.stdout.strip() or f"exit code {result.returncode}"
|
||||
print(f"ERROR: Brownan solver failed: {details}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return " ".join(moves)
|
||||
solution = parse_solution_moves(result.stdout)
|
||||
if solution:
|
||||
return solution
|
||||
|
||||
stdout_preview = result.stdout.strip()
|
||||
stderr_preview = result.stderr.strip()
|
||||
details = stderr_preview or stdout_preview or "solver returned no output"
|
||||
print(f"ERROR: Brownan solver returned no parseable solution: {details}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -6,23 +6,11 @@ kociemba-order facelet string to both the cahidenes and brownan wrappers,
|
|||
then verifies each solution actually solves the cube by re-applying it in pycuber.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from pycuber import Cube, Formula
|
||||
except ImportError:
|
||||
print("ERROR: pycuber package not installed. Run: pip install pycuber")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import kociemba
|
||||
except ImportError:
|
||||
print("ERROR: kociemba package not installed. Run: pip install kociemba")
|
||||
sys.exit(1)
|
||||
from typing import Any
|
||||
|
||||
|
||||
# Map pycuber colour names to single uppercase letters used in kociemba facelets.
|
||||
|
|
@ -41,9 +29,10 @@ FACES = ["U", "D", "F", "B", "L", "R"]
|
|||
SUFFIXES = ["", "'", "2"]
|
||||
N_SCRAMBLES = 10
|
||||
SCRAMBLE_DEPTH = 12
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def facelet_string_from_cube(cube: Cube) -> str:
|
||||
def facelet_string_from_cube(cube: Any) -> str:
|
||||
"""Build a 54-char kociemba-order facelet string from a pycuber Cube."""
|
||||
out = []
|
||||
for f in "URFDLB":
|
||||
|
|
@ -55,23 +44,17 @@ def facelet_string_from_cube(cube: Cube) -> str:
|
|||
|
||||
|
||||
def random_scramble(depth: int = SCRAMBLE_DEPTH) -> str:
|
||||
return " ".join(random.choice(FACES) + random.choice(SUFFIXES) for _ in range(depth))
|
||||
moves = []
|
||||
previous_face = None
|
||||
for _ in range(depth):
|
||||
available_faces = [face for face in FACES if face != previous_face]
|
||||
face = random.choice(available_faces)
|
||||
moves.append(face + random.choice(SUFFIXES))
|
||||
previous_face = face
|
||||
return " ".join(moves)
|
||||
|
||||
|
||||
def inverse_moves(moves_str: str) -> list[str]:
|
||||
rev = list(reversed(moves_str.strip().split()))
|
||||
out = []
|
||||
for m in rev:
|
||||
if m.endswith("'"):
|
||||
out.append(m[:-1])
|
||||
elif m.endswith("2"):
|
||||
out.append(m)
|
||||
else:
|
||||
out.append(m + "'")
|
||||
return out
|
||||
|
||||
|
||||
def is_solved(cube: Cube) -> bool:
|
||||
def is_solved(cube: Any) -> bool:
|
||||
for f in "URFDLB":
|
||||
face = cube.get_face(f)
|
||||
colours = {cell.colour for row in face for cell in row}
|
||||
|
|
@ -82,7 +65,7 @@ def is_solved(cube: Cube) -> bool:
|
|||
|
||||
def solve_with_wrapper(wrapper_path: Path, facelets: str) -> list[str]:
|
||||
result = subprocess.run(
|
||||
["python.exe", str(wrapper_path), facelets],
|
||||
[sys.executable, str(wrapper_path), facelets],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
|
|
@ -120,9 +103,33 @@ def main():
|
|||
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility")
|
||||
args = parser.parse_args()
|
||||
|
||||
solvers_dir = Path("C:/HyperTwist/UnrealHyperTwist/Binaries/Win64/Solvers")
|
||||
brownan_wrapper = Path(args.brownan_wrapper) if args.brownan_wrapper else solvers_dir / "brownan_solver_wrapper.py"
|
||||
cahidenes_wrapper = Path(args.cahidenes_wrapper) if args.cahidenes_wrapper else solvers_dir / "cahidenes_solver_wrapper.py"
|
||||
try:
|
||||
from pycuber import Cube, Formula
|
||||
except ImportError:
|
||||
print("ERROR: pycuber package not installed. Run: pip install pycuber")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import kociemba
|
||||
except ImportError:
|
||||
print("ERROR: kociemba package not installed. Run: pip install kociemba")
|
||||
sys.exit(1)
|
||||
|
||||
solver_candidates = [
|
||||
SCRIPT_DIR,
|
||||
SCRIPT_DIR.parent.parent / "UnrealHyperTwist" / "Binaries" / "Win64" / "Solvers",
|
||||
Path("C:/HyperTwist/UnrealHyperTwist/Binaries/Win64/Solvers"),
|
||||
]
|
||||
|
||||
def resolve_default_wrapper(filename: str) -> Path:
|
||||
for directory in solver_candidates:
|
||||
candidate = directory / filename
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return solver_candidates[0] / filename
|
||||
|
||||
brownan_wrapper = Path(args.brownan_wrapper) if args.brownan_wrapper else resolve_default_wrapper("brownan_solver_wrapper.py")
|
||||
cahidenes_wrapper = Path(args.cahidenes_wrapper) if args.cahidenes_wrapper else resolve_default_wrapper("cahidenes_solver_wrapper.py")
|
||||
|
||||
if not brownan_wrapper.exists():
|
||||
print(f"ERROR: Brownan wrapper not found at {brownan_wrapper}")
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ Returns:
|
|||
Path to generated WAV file on success
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def find_piper_exe():
|
||||
|
|
@ -23,13 +22,17 @@ def find_piper_exe():
|
|||
os.path.join(script_dir, "..", "..", "..", "Binaries", "Win64", "Solvers", "piper", "piper", "piper.exe"),
|
||||
os.path.join(script_dir, "piper", "piper.exe"),
|
||||
os.path.join(script_dir, "..", "piper", "piper", "piper.exe"),
|
||||
"piper",
|
||||
"piper.exe",
|
||||
]
|
||||
for candidate in candidates:
|
||||
resolved = os.path.abspath(candidate)
|
||||
if os.path.isfile(resolved):
|
||||
return resolved
|
||||
|
||||
for candidate in ("piper.exe", "piper"):
|
||||
resolved = shutil.which(candidate)
|
||||
if resolved:
|
||||
return os.path.abspath(resolved)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -51,12 +54,17 @@ def synthesize(text: str, model_path: str, config_path: str, output_path: str) -
|
|||
"""Synthesize speech using piper."""
|
||||
if not os.path.exists(model_path):
|
||||
return f"ERROR: Model not found: {model_path}"
|
||||
if not os.path.exists(config_path):
|
||||
return f"ERROR: Config not found: {config_path}"
|
||||
|
||||
piper_exe = find_piper_exe()
|
||||
if not piper_exe:
|
||||
return "ERROR: piper executable not found. Place it in Solvers/piper/piper/ or add to PATH."
|
||||
|
||||
espeak_data = find_espeak_data(piper_exe)
|
||||
output_dir = os.path.dirname(os.path.abspath(output_path))
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
cmd = [
|
||||
piper_exe,
|
||||
|
|
@ -70,6 +78,8 @@ def synthesize(text: str, model_path: str, config_path: str, output_path: str) -
|
|||
try:
|
||||
result = subprocess.run(cmd, input=text, text=True, capture_output=True, timeout=60)
|
||||
if result.returncode == 0:
|
||||
if not os.path.exists(output_path):
|
||||
return f"ERROR: Piper reported success but did not create output: {output_path}"
|
||||
return os.path.abspath(output_path)
|
||||
return f"ERROR: {result.stderr}"
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
#include "HyperTwistBrowser/HyperTwistBrowserBridgeObject.h"
|
||||
|
||||
void UHyperTwistBrowserBridgeObject::NotifyEnvelope(const FString& EnvelopeJson)
|
||||
{
|
||||
LastEnvelopeJson = EnvelopeJson;
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserBridgeObject::NotifyState(const FString& StateJson)
|
||||
{
|
||||
LastStateJson = StateJson;
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserBridgeObject::NotifyRuntimeReady(const FString& RuntimeReadyJson)
|
||||
{
|
||||
LastRuntimeReadyJson = RuntimeReadyJson;
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserBridgeObject::ResetReceivedMessages()
|
||||
{
|
||||
LastEnvelopeJson.Reset();
|
||||
LastStateJson.Reset();
|
||||
LastRuntimeReadyJson.Reset();
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
#include "HyperTwistBrowser/HyperTwistBrowserWidget.h"
|
||||
|
||||
#include "HyperTwistBrowser/HyperTwistBrowserBridgeObject.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "SWebBrowser.h"
|
||||
|
||||
namespace HyperTwistBrowserWidgetInternal
|
||||
{
|
||||
FString MakeFileUrl(const FString& AbsolutePath)
|
||||
{
|
||||
FString Normalized = FPaths::ConvertRelativePathToFull(AbsolutePath);
|
||||
FPaths::NormalizeFilename(Normalized);
|
||||
Normalized.ReplaceInline(TEXT(" "), TEXT("%20"));
|
||||
return FString::Printf(TEXT("file:///%s"), *Normalized);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::LoadBundledBrowserShell()
|
||||
{
|
||||
bUseBundledBrowserShell = true;
|
||||
InitialUrl = ResolveBundledBrowserShellUrl();
|
||||
if (BrowserWidget.IsValid())
|
||||
{
|
||||
BrowserWidget->LoadURL(InitialUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::LoadBrowserUrl(const FString& NewUrl)
|
||||
{
|
||||
bUseBundledBrowserShell = false;
|
||||
InitialUrl = NewUrl;
|
||||
if (BrowserWidget.IsValid() && !NewUrl.IsEmpty())
|
||||
{
|
||||
BrowserWidget->LoadURL(NewUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::DispatchCommandJson(const FString& CommandJson)
|
||||
{
|
||||
if (!BrowserWidget.IsValid() || CommandJson.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(CommandJson);
|
||||
BrowserWidget->ExecuteJavascript(FString::Printf(
|
||||
TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.receiveCommand(JSON.parse('%s'));"),
|
||||
*EscapedJson
|
||||
));
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::PushShellStateJson(const FString& StateJson)
|
||||
{
|
||||
if (!BrowserWidget.IsValid() || StateJson.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(StateJson);
|
||||
BrowserWidget->ExecuteJavascript(FString::Printf(
|
||||
TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.setShellState(JSON.parse('%s'));"),
|
||||
*EscapedJson
|
||||
));
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::GetLastEnvelopeJson() const
|
||||
{
|
||||
return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastEnvelopeJson : FString();
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::GetLastRuntimeReadyJson() const
|
||||
{
|
||||
return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastRuntimeReadyJson : FString();
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::GetLastStateJson() const
|
||||
{
|
||||
return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastStateJson : FString();
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::ResolveBundledBrowserShellUrl()
|
||||
{
|
||||
return HyperTwistBrowserWidgetInternal::MakeFileUrl(
|
||||
HyperTwistBrowserWidgetInternal::ResolveBundledBrowserShellAbsolutePath()
|
||||
);
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UHyperTwistBrowserWidget::RebuildWidget()
|
||||
{
|
||||
if (BrowserBridgeObject == nullptr)
|
||||
{
|
||||
BrowserBridgeObject = NewObject<UHyperTwistBrowserBridgeObject>(this, TEXT("HyperTwistBrowserBridge"));
|
||||
}
|
||||
else
|
||||
{
|
||||
BrowserBridgeObject->ResetReceivedMessages();
|
||||
}
|
||||
|
||||
BrowserWidget =
|
||||
SNew(SWebBrowser)
|
||||
.InitialURL(ResolveInitialUrl())
|
||||
.ShowControls(bShowBrowserControls);
|
||||
|
||||
EnsureBridgeBound();
|
||||
return BrowserWidget.ToSharedRef();
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::ReleaseSlateResources(const bool bReleaseChildren)
|
||||
{
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
|
||||
if (BrowserWidget.IsValid() && BrowserBridgeObject != nullptr)
|
||||
{
|
||||
BrowserWidget->UnbindUObject(TEXT("hypertwist"), BrowserBridgeObject, true);
|
||||
}
|
||||
|
||||
BrowserWidget.Reset();
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::SynchronizeProperties()
|
||||
{
|
||||
Super::SynchronizeProperties();
|
||||
|
||||
if (BrowserWidget.IsValid())
|
||||
{
|
||||
EnsureBridgeBound();
|
||||
const FString ResolvedUrl = ResolveInitialUrl();
|
||||
if (!ResolvedUrl.IsEmpty())
|
||||
{
|
||||
BrowserWidget->LoadURL(ResolvedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::EscapeForJavaScriptSingleQuotedString(const FString& Input)
|
||||
{
|
||||
FString Escaped = Input;
|
||||
Escaped.ReplaceInline(TEXT("\\"), TEXT("\\\\"));
|
||||
Escaped.ReplaceInline(TEXT("'"), TEXT("\\'"));
|
||||
Escaped.ReplaceInline(TEXT("\r"), TEXT("\\r"));
|
||||
Escaped.ReplaceInline(TEXT("\n"), TEXT("\\n"));
|
||||
Escaped.ReplaceInline(TEXT("\t"), TEXT("\\t"));
|
||||
return Escaped;
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::ResolveInitialUrl() const
|
||||
{
|
||||
return bUseBundledBrowserShell
|
||||
? ResolveBundledBrowserShellUrl()
|
||||
: InitialUrl;
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::EnsureBridgeBound()
|
||||
{
|
||||
if (BrowserWidget.IsValid() && BrowserBridgeObject != nullptr)
|
||||
{
|
||||
BrowserWidget->BindUObject(TEXT("hypertwist"), BrowserBridgeObject, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,39 @@
|
|||
#include "HyperTwistIPC/HyperTwistIPCProcessManager.h"
|
||||
|
||||
namespace HyperTwistIPCProcessManagerInternal
|
||||
{
|
||||
bool RequiresExplicitPathCheck(const FString& ExecutablePath)
|
||||
{
|
||||
return ExecutablePath.Contains(TEXT("/"))
|
||||
|| ExecutablePath.Contains(TEXT("\\"))
|
||||
|| ExecutablePath.Contains(TEXT(":"));
|
||||
}
|
||||
|
||||
FString QuoteProcessArgument(const FString& Argument)
|
||||
{
|
||||
FString Escaped = Argument;
|
||||
Escaped.ReplaceInline(TEXT("\""), TEXT("\\\""));
|
||||
|
||||
const bool bNeedsQuotes = Escaped.IsEmpty()
|
||||
|| Escaped.Contains(TEXT(" "))
|
||||
|| Escaped.Contains(TEXT("\t"))
|
||||
|| Escaped.Contains(TEXT("\""));
|
||||
return bNeedsQuotes ? FString::Printf(TEXT("\"%s\""), *Escaped) : Escaped;
|
||||
}
|
||||
|
||||
FString BuildCommandLine(const TArray<FString>& Arguments)
|
||||
{
|
||||
TArray<FString> QuotedArguments;
|
||||
QuotedArguments.Reserve(Arguments.Num());
|
||||
for (const FString& Argument : Arguments)
|
||||
{
|
||||
QuotedArguments.Add(QuoteProcessArgument(Argument));
|
||||
}
|
||||
|
||||
return FString::Join(QuotedArguments, TEXT(" "));
|
||||
}
|
||||
}
|
||||
|
||||
UHyperTwistIPCProcessManager::UHyperTwistIPCProcessManager()
|
||||
{
|
||||
}
|
||||
|
|
@ -19,13 +53,20 @@ FHyperTwistIPCResult UHyperTwistIPCProcessManager::RunProcess(const FHyperTwistI
|
|||
{
|
||||
FHyperTwistIPCResult Result;
|
||||
|
||||
if (!FPaths::FileExists(Options.ExecutablePath))
|
||||
if (Options.ExecutablePath.IsEmpty())
|
||||
{
|
||||
Result.ErrorMessage = TEXT("Executable path is empty");
|
||||
return Result;
|
||||
}
|
||||
|
||||
if (HyperTwistIPCProcessManagerInternal::RequiresExplicitPathCheck(Options.ExecutablePath)
|
||||
&& !FPaths::FileExists(Options.ExecutablePath))
|
||||
{
|
||||
Result.ErrorMessage = FString::Printf(TEXT("Executable not found: %s"), *Options.ExecutablePath);
|
||||
return Result;
|
||||
}
|
||||
|
||||
FString Params = FString::Join(Options.Arguments, TEXT(" "));
|
||||
FString Params = HyperTwistIPCProcessManagerInternal::BuildCommandLine(Options.Arguments);
|
||||
FString WorkingDir = Options.WorkingDirectory.IsEmpty()
|
||||
? FPaths::GetPath(Options.ExecutablePath)
|
||||
: Options.WorkingDirectory;
|
||||
|
|
@ -115,12 +156,18 @@ FHyperTwistIPCResult UHyperTwistIPCProcessManager::RunProcess(const FHyperTwistI
|
|||
|
||||
int32 UHyperTwistIPCProcessManager::StartProcess(const FHyperTwistIPCProcessOptions& Options)
|
||||
{
|
||||
if (!FPaths::FileExists(Options.ExecutablePath))
|
||||
if (Options.ExecutablePath.IsEmpty())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
FString Params = FString::Join(Options.Arguments, TEXT(" "));
|
||||
if (HyperTwistIPCProcessManagerInternal::RequiresExplicitPathCheck(Options.ExecutablePath)
|
||||
&& !FPaths::FileExists(Options.ExecutablePath))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
FString Params = HyperTwistIPCProcessManagerInternal::BuildCommandLine(Options.Arguments);
|
||||
FString WorkingDir = Options.WorkingDirectory.IsEmpty()
|
||||
? FPaths::GetPath(Options.ExecutablePath)
|
||||
: Options.WorkingDirectory;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,73 @@
|
|||
#include "HyperTwistIPC/HyperTwistSolverOracleLibrary.h"
|
||||
#include "HyperTwistSolverLibrary.h"
|
||||
#include "ThirdParty/rob-twophase/cubie.h"
|
||||
#include "ThirdParty/rob-twophase/face.h"
|
||||
#include "ThirdParty/rob-twophase/move.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "HAL/PlatformFilemanager.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
namespace HyperTwistSolverOracleLibraryInternal
|
||||
{
|
||||
FString GetPythonExecutableName()
|
||||
{
|
||||
#if PLATFORM_WINDOWS
|
||||
return TEXT("python.exe");
|
||||
#else
|
||||
return TEXT("python3");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool TryResolveRobTwophaseMoveIndex(const FString& MoveString, int32& OutMoveIndex)
|
||||
{
|
||||
const FString TrimmedMove = MoveString.TrimStartAndEnd();
|
||||
for (int32 MoveIndex = 0; MoveIndex < move::COUNT; ++MoveIndex)
|
||||
{
|
||||
if (TrimmedMove.Equals(UTF8_TO_TCHAR(move::names[MoveIndex].c_str()), ESearchCase::IgnoreCase))
|
||||
{
|
||||
OutMoveIndex = MoveIndex;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryReplayMoveSequence(
|
||||
const FString& FaceletString,
|
||||
const TArray<FString>& Moves,
|
||||
cubie::cube& OutCube
|
||||
)
|
||||
{
|
||||
if (!UHyperTwistSolverLibrary::InitializeSolver() || !UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Facelets(TCHAR_TO_UTF8(*FaceletString));
|
||||
if (face::to_cubie(Facelets, OutCube) != 0 || cubie::check_cube(OutCube) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FString& MoveString : Moves)
|
||||
{
|
||||
int32 MoveIndex = INDEX_NONE;
|
||||
if (!TryResolveRobTwophaseMoveIndex(MoveString, MoveIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
cubie::cube NextCube;
|
||||
cubie::mul(OutCube, move::cubes[MoveIndex], NextCube);
|
||||
OutCube = NextCube;
|
||||
}
|
||||
|
||||
return cubie::check_cube(OutCube) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
FString UHyperTwistSolverOracleLibrary::GetBrownanSolverPath()
|
||||
{
|
||||
return FPaths::ConvertRelativePathToFull(
|
||||
|
|
@ -44,19 +107,27 @@ TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithBrownanOracle(const FSt
|
|||
GetTransientPackage());
|
||||
|
||||
FHyperTwistIPCProcessOptions Options;
|
||||
Options.ExecutablePath = TEXT("python.exe");
|
||||
Options.ExecutablePath = HyperTwistSolverOracleLibraryInternal::GetPythonExecutableName();
|
||||
Options.Arguments.Add(WrapperPath);
|
||||
Options.Arguments.Add(FaceletString);
|
||||
Options.WorkingDirectory = FPaths::GetPath(WrapperPath);
|
||||
Options.TimeoutSeconds = 60.0f;
|
||||
|
||||
FHyperTwistIPCResult Result = IPC->RunProcess(Options);
|
||||
if (!Result.bSuccess)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Brownan solver failed: %s"), *Result.ErrorMessage);
|
||||
const FString FailureDetails = Result.ErrorMessage.IsEmpty() ? Result.StdOut : Result.ErrorMessage;
|
||||
UE_LOG(LogTemp, Warning, TEXT("Brownan solver failed: %s"), *FailureDetails);
|
||||
return EmptyResult;
|
||||
}
|
||||
|
||||
FString Output = Result.StdOut.TrimStartAndEnd();
|
||||
if (Output.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Brownan solver returned no parseable solution output for state '%s'."), *FaceletString);
|
||||
return EmptyResult;
|
||||
}
|
||||
|
||||
TArray<FString> Moves;
|
||||
Output.ParseIntoArrayWS(Moves);
|
||||
return Moves;
|
||||
|
|
@ -64,10 +135,9 @@ TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithBrownanOracle(const FSt
|
|||
|
||||
bool UHyperTwistSolverOracleLibrary::VerifySolutionWithBrownanOracle(const FString& CubeState, const TArray<FString>& Moves)
|
||||
{
|
||||
TArray<FString> BrownanMoves = SolveWithBrownanOracle(CubeState);
|
||||
FString Output = FString::Join(BrownanMoves, TEXT(" ")).TrimStartAndEnd();
|
||||
FString Expected = FString::Join(Moves, TEXT(" ")).TrimStartAndEnd();
|
||||
return Output.Equals(Expected, ESearchCase::IgnoreCase);
|
||||
cubie::cube FinalCube;
|
||||
return HyperTwistSolverOracleLibraryInternal::TryReplayMoveSequence(CubeState, Moves, FinalCube)
|
||||
&& FinalCube == cubie::SOLVED_CUBE;
|
||||
}
|
||||
|
||||
TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithCahidenesOracle(const FString& FaceletString)
|
||||
|
|
@ -84,19 +154,27 @@ TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithCahidenesOracle(const F
|
|||
GetTransientPackage());
|
||||
|
||||
FHyperTwistIPCProcessOptions Options;
|
||||
Options.ExecutablePath = TEXT("python.exe");
|
||||
Options.ExecutablePath = HyperTwistSolverOracleLibraryInternal::GetPythonExecutableName();
|
||||
Options.Arguments.Add(WrapperPath);
|
||||
Options.Arguments.Add(FaceletString);
|
||||
Options.WorkingDirectory = FPaths::GetPath(WrapperPath);
|
||||
Options.TimeoutSeconds = 30.0f;
|
||||
|
||||
FHyperTwistIPCResult Result = IPC->RunProcess(Options);
|
||||
if (!Result.bSuccess)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Cahidenes solver failed: %s"), *Result.ErrorMessage);
|
||||
const FString FailureDetails = Result.ErrorMessage.IsEmpty() ? Result.StdOut : Result.ErrorMessage;
|
||||
UE_LOG(LogTemp, Warning, TEXT("Cahidenes solver failed: %s"), *FailureDetails);
|
||||
return EmptyResult;
|
||||
}
|
||||
|
||||
FString Output = Result.StdOut.TrimStartAndEnd();
|
||||
if (Output.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Cahidenes solver returned no parseable solution output for state '%s'."), *FaceletString);
|
||||
return EmptyResult;
|
||||
}
|
||||
|
||||
TArray<FString> Moves;
|
||||
Output.ParseIntoArrayWS(Moves);
|
||||
return Moves;
|
||||
|
|
@ -104,6 +182,11 @@ TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithCahidenesOracle(const F
|
|||
|
||||
bool UHyperTwistSolverOracleLibrary::CompareSolvers(const FString& FaceletString)
|
||||
{
|
||||
if (!UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// rob-twophase result
|
||||
TArray<FString> RobTwophaseMoves = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 25, 1);
|
||||
|
||||
|
|
@ -117,11 +200,17 @@ bool UHyperTwistSolverOracleLibrary::CompareSolvers(const FString& FaceletString
|
|||
const int32 CahCount = CahidenesMoves.Num();
|
||||
const int32 BrownCount = BrownanMoves.Num();
|
||||
|
||||
if (RobCount > 0 && CahCount > 0 && BrownCount > 0)
|
||||
if (RobCount <= 0 || CahCount <= 0 || BrownCount <= 0)
|
||||
{
|
||||
return RobCount == CahCount && CahCount == BrownCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any solver failed, consider comparison inconclusive but not a hard failure
|
||||
return true;
|
||||
if (!VerifySolutionWithBrownanOracle(FaceletString, RobTwophaseMoves)
|
||||
|| !VerifySolutionWithBrownanOracle(FaceletString, CahidenesMoves)
|
||||
|| !VerifySolutionWithBrownanOracle(FaceletString, BrownanMoves))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RobCount == CahCount && CahCount == BrownCount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,515 @@
|
|||
#include "HyperTwistRecognition/HyperTwistSpeechLibrary.h"
|
||||
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
|
||||
#include "HyperTwistRecognition/HyperTwistVoiceClient.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
namespace HyperTwistSpeechLibraryInternal
|
||||
{
|
||||
const FName SpeechAudioComponentTag(TEXT("HyperTwistSpeechAudio"));
|
||||
constexpr float DefaultDuckVolumeMultiplier = 0.15f;
|
||||
constexpr float RestoreDelaySeconds = 0.2f;
|
||||
|
||||
struct FManagedAudioComponentEntry
|
||||
{
|
||||
TWeakObjectPtr<UAudioComponent> AudioComponent;
|
||||
bool bTreatAsSpeechAudio = false;
|
||||
};
|
||||
|
||||
struct FManagedAudioRegistry
|
||||
{
|
||||
TArray<FManagedAudioComponentEntry> RegisteredComponents;
|
||||
TMap<TWeakObjectPtr<UAudioComponent>, float> OriginalVolumeMap;
|
||||
FTimerHandle RestoreTimerHandle;
|
||||
bool bDuckingActive = false;
|
||||
};
|
||||
|
||||
FManagedAudioRegistry GManagedAudioRegistry;
|
||||
|
||||
UHyperTwistTrainingSubsystem* ResolveTrainingSubsystem(UObject* ContextObject)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = Cast<UHyperTwistTrainingSubsystem>(ContextObject))
|
||||
{
|
||||
return TrainingSubsystem;
|
||||
}
|
||||
|
||||
if (ContextObject == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UWorld* World = ContextObject->GetWorld())
|
||||
{
|
||||
if (UGameInstance* GameInstance = World->GetGameInstance())
|
||||
{
|
||||
return GameInstance->GetSubsystem<UHyperTwistTrainingSubsystem>();
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IHyperTwistVoiceClient* ResolveVoiceClient(UObject* VoiceClientObject)
|
||||
{
|
||||
return VoiceClientObject != nullptr ? Cast<IHyperTwistVoiceClient>(VoiceClientObject) : nullptr;
|
||||
}
|
||||
|
||||
UObject* CreateVoiceClient(UObject* ContextObject, const bool bUseMockVoiceClient)
|
||||
{
|
||||
UObject* Outer = ContextObject != nullptr ? ContextObject : GetTransientPackage();
|
||||
return bUseMockVoiceClient
|
||||
? static_cast<UObject*>(NewObject<UHyperTwistMockVoiceClient>(Outer))
|
||||
: static_cast<UObject*>(NewObject<UHyperTwistHttpVoiceClient>(Outer));
|
||||
}
|
||||
|
||||
void CompactManagedAudioRegistry()
|
||||
{
|
||||
GManagedAudioRegistry.RegisteredComponents.RemoveAll(
|
||||
[](const FManagedAudioComponentEntry& Entry)
|
||||
{
|
||||
return !Entry.AudioComponent.IsValid();
|
||||
}
|
||||
);
|
||||
|
||||
for (auto It = GManagedAudioRegistry.OriginalVolumeMap.CreateIterator(); It; ++It)
|
||||
{
|
||||
if (!It.Key().IsValid())
|
||||
{
|
||||
It.RemoveCurrent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsSpeechAudioComponent(const UAudioComponent* AudioComponent)
|
||||
{
|
||||
if (AudioComponent == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AudioComponent->ComponentHasTag(SpeechAudioComponentTag))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const FManagedAudioComponentEntry& Entry : GManagedAudioRegistry.RegisteredComponents)
|
||||
{
|
||||
if (Entry.AudioComponent.Get() == AudioComponent)
|
||||
{
|
||||
return Entry.bTreatAsSpeechAudio;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<UAudioComponent*> ResolveManagedAudioComponents()
|
||||
{
|
||||
CompactManagedAudioRegistry();
|
||||
|
||||
TArray<UAudioComponent*> Components;
|
||||
for (const FManagedAudioComponentEntry& Entry : GManagedAudioRegistry.RegisteredComponents)
|
||||
{
|
||||
if (UAudioComponent* AudioComponent = Entry.AudioComponent.Get())
|
||||
{
|
||||
Components.AddUnique(AudioComponent);
|
||||
}
|
||||
}
|
||||
|
||||
if (Components.Num() > 0)
|
||||
{
|
||||
return Components;
|
||||
}
|
||||
|
||||
if (GEngine == nullptr)
|
||||
{
|
||||
return Components;
|
||||
}
|
||||
|
||||
for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
|
||||
{
|
||||
UWorld* World = WorldContext.World();
|
||||
if (World == nullptr || World->IsPreviewWorld())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
|
||||
{
|
||||
TInlineComponentArray<UAudioComponent*> AudioComponents(*ActorIt);
|
||||
for (UAudioComponent* AudioComponent : AudioComponents)
|
||||
{
|
||||
if (AudioComponent != nullptr)
|
||||
{
|
||||
Components.AddUnique(AudioComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Components;
|
||||
}
|
||||
|
||||
int32 ApplyManagedAudioDucking(const float DuckVolumeMultiplier)
|
||||
{
|
||||
CompactManagedAudioRegistry();
|
||||
|
||||
const float TargetVolume = FMath::Clamp(DuckVolumeMultiplier, 0.0f, 1.0f);
|
||||
const TArray<UAudioComponent*> AudioComponents = ResolveManagedAudioComponents();
|
||||
int32 DuckedComponentCount = 0;
|
||||
|
||||
for (UAudioComponent* AudioComponent : AudioComponents)
|
||||
{
|
||||
if (AudioComponent == nullptr || IsSpeechAudioComponent(AudioComponent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const TWeakObjectPtr<UAudioComponent> AudioComponentKey(AudioComponent);
|
||||
if (!GManagedAudioRegistry.OriginalVolumeMap.Contains(AudioComponentKey))
|
||||
{
|
||||
GManagedAudioRegistry.OriginalVolumeMap.Add(AudioComponentKey, AudioComponent->VolumeMultiplier);
|
||||
}
|
||||
|
||||
AudioComponent->SetVolumeMultiplier(TargetVolume);
|
||||
++DuckedComponentCount;
|
||||
}
|
||||
|
||||
GManagedAudioRegistry.bDuckingActive = DuckedComponentCount > 0;
|
||||
return DuckedComponentCount;
|
||||
}
|
||||
|
||||
int32 RestoreManagedAudioDucking()
|
||||
{
|
||||
CompactManagedAudioRegistry();
|
||||
|
||||
int32 RestoredComponentCount = 0;
|
||||
for (auto It = GManagedAudioRegistry.OriginalVolumeMap.CreateIterator(); It; ++It)
|
||||
{
|
||||
if (UAudioComponent* AudioComponent = It.Key().Get())
|
||||
{
|
||||
AudioComponent->SetVolumeMultiplier(It.Value());
|
||||
++RestoredComponentCount;
|
||||
}
|
||||
It.RemoveCurrent();
|
||||
}
|
||||
|
||||
GManagedAudioRegistry.bDuckingActive = false;
|
||||
return RestoredComponentCount;
|
||||
}
|
||||
|
||||
void ScheduleManagedAudioRestore(UObject* ContextObject)
|
||||
{
|
||||
if (ContextObject != nullptr)
|
||||
{
|
||||
if (UWorld* World = ContextObject->GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(GManagedAudioRegistry.RestoreTimerHandle);
|
||||
World->GetTimerManager().SetTimer(
|
||||
GManagedAudioRegistry.RestoreTimerHandle,
|
||||
[]()
|
||||
{
|
||||
RestoreManagedAudioDucking();
|
||||
},
|
||||
RestoreDelaySeconds,
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RestoreManagedAudioDucking();
|
||||
}
|
||||
|
||||
uint32 ReadLittleEndianUint32(const TArray<uint8>& Bytes, const int32 Offset)
|
||||
{
|
||||
if (!Bytes.IsValidIndex(Offset + 3))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return static_cast<uint32>(Bytes[Offset])
|
||||
| (static_cast<uint32>(Bytes[Offset + 1]) << 8)
|
||||
| (static_cast<uint32>(Bytes[Offset + 2]) << 16)
|
||||
| (static_cast<uint32>(Bytes[Offset + 3]) << 24);
|
||||
}
|
||||
|
||||
TArray<uint8> ExtractPcm16Payload(const TArray<uint8>& AudioBytes, const FString& AudioEncoding)
|
||||
{
|
||||
if (!AudioEncoding.Contains(TEXT("wav"), ESearchCase::IgnoreCase)
|
||||
|| AudioBytes.Num() < 12
|
||||
|| AudioBytes[0] != 'R'
|
||||
|| AudioBytes[1] != 'I'
|
||||
|| AudioBytes[2] != 'F'
|
||||
|| AudioBytes[3] != 'F'
|
||||
|| AudioBytes[8] != 'W'
|
||||
|| AudioBytes[9] != 'A'
|
||||
|| AudioBytes[10] != 'V'
|
||||
|| AudioBytes[11] != 'E')
|
||||
{
|
||||
return AudioBytes;
|
||||
}
|
||||
|
||||
int32 ChunkOffset = 12;
|
||||
while (ChunkOffset + 8 <= AudioBytes.Num())
|
||||
{
|
||||
const uint32 ChunkSize = ReadLittleEndianUint32(AudioBytes, ChunkOffset + 4);
|
||||
const int32 ChunkDataOffset = ChunkOffset + 8;
|
||||
const int32 ChunkEndOffset = ChunkDataOffset + static_cast<int32>(ChunkSize);
|
||||
if (ChunkEndOffset > AudioBytes.Num())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (AudioBytes[ChunkOffset] == 'd'
|
||||
&& AudioBytes[ChunkOffset + 1] == 'a'
|
||||
&& AudioBytes[ChunkOffset + 2] == 't'
|
||||
&& AudioBytes[ChunkOffset + 3] == 'a')
|
||||
{
|
||||
TArray<uint8> Payload;
|
||||
Payload.Append(AudioBytes.GetData() + ChunkDataOffset, static_cast<int32>(ChunkSize));
|
||||
return Payload;
|
||||
}
|
||||
|
||||
ChunkOffset = ChunkEndOffset + (ChunkSize % 2 == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
return AudioBytes;
|
||||
}
|
||||
}
|
||||
|
||||
bool UHyperTwistSpeechLibrary::StartDictationSession(
|
||||
UObject* WorldContextObject,
|
||||
FString& OutError,
|
||||
const bool bDuckAudio,
|
||||
const float DuckVolumeMultiplier
|
||||
)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem =
|
||||
HyperTwistSpeechLibraryInternal::ResolveTrainingSubsystem(WorldContextObject))
|
||||
{
|
||||
if (TrainingSubsystem->OpenActiveCompanionSpeechSession(OutError))
|
||||
{
|
||||
if (bDuckAudio)
|
||||
{
|
||||
HyperTwistSpeechLibraryInternal::ApplyManagedAudioDucking(DuckVolumeMultiplier);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutError = TEXT("training-subsystem-unavailable");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistSpeechTranscriptResult UHyperTwistSpeechLibrary::SubmitDictationUtterance(
|
||||
UObject* WorldContextObject,
|
||||
const FHyperTwistSpeechUtteranceEnvelope& Utterance
|
||||
)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem =
|
||||
HyperTwistSpeechLibraryInternal::ResolveTrainingSubsystem(WorldContextObject))
|
||||
{
|
||||
const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
|
||||
TrainingSubsystem->GetActiveCompanionSpeechSessionState();
|
||||
if (!SessionState.bSessionOpen)
|
||||
{
|
||||
FString OpenError;
|
||||
StartDictationSession(WorldContextObject, OpenError, true, HyperTwistSpeechLibraryInternal::DefaultDuckVolumeMultiplier);
|
||||
}
|
||||
|
||||
return TrainingSubsystem->SubmitActiveCompanionSpeechUtterance(Utterance);
|
||||
}
|
||||
|
||||
FHyperTwistSpeechTranscriptResult Result;
|
||||
Result.Warnings.Add(TEXT("training-subsystem-unavailable"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool UHyperTwistSpeechLibrary::EndDictationSession(
|
||||
UObject* WorldContextObject,
|
||||
FString& OutError,
|
||||
const bool bRestoreAudio
|
||||
)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem =
|
||||
HyperTwistSpeechLibraryInternal::ResolveTrainingSubsystem(WorldContextObject))
|
||||
{
|
||||
const bool bClosed = TrainingSubsystem->CloseActiveCompanionSpeechSession(OutError);
|
||||
if (bClosed && bRestoreAudio)
|
||||
{
|
||||
HyperTwistSpeechLibraryInternal::ScheduleManagedAudioRestore(WorldContextObject);
|
||||
}
|
||||
return bClosed;
|
||||
}
|
||||
|
||||
OutError = TEXT("training-subsystem-unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingCompanionSpeechSessionState UHyperTwistSpeechLibrary::GetActiveDictationSessionState(
|
||||
UObject* WorldContextObject
|
||||
)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem =
|
||||
HyperTwistSpeechLibraryInternal::ResolveTrainingSubsystem(WorldContextObject))
|
||||
{
|
||||
return TrainingSubsystem->GetActiveCompanionSpeechSessionState();
|
||||
}
|
||||
|
||||
return FHyperTwistTrainingCompanionSpeechSessionState();
|
||||
}
|
||||
|
||||
void UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(
|
||||
UAudioComponent* AudioComponent,
|
||||
const bool bTreatAsSpeechAudio
|
||||
)
|
||||
{
|
||||
if (AudioComponent == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HyperTwistSpeechLibraryInternal::CompactManagedAudioRegistry();
|
||||
for (HyperTwistSpeechLibraryInternal::FManagedAudioComponentEntry& Entry :
|
||||
HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents)
|
||||
{
|
||||
if (Entry.AudioComponent.Get() == AudioComponent)
|
||||
{
|
||||
Entry.bTreatAsSpeechAudio = bTreatAsSpeechAudio;
|
||||
if (bTreatAsSpeechAudio)
|
||||
{
|
||||
AudioComponent->ComponentTags.AddUnique(
|
||||
HyperTwistSpeechLibraryInternal::SpeechAudioComponentTag
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HyperTwistSpeechLibraryInternal::FManagedAudioComponentEntry Entry;
|
||||
Entry.AudioComponent = AudioComponent;
|
||||
Entry.bTreatAsSpeechAudio = bTreatAsSpeechAudio;
|
||||
HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents.Add(Entry);
|
||||
if (bTreatAsSpeechAudio)
|
||||
{
|
||||
AudioComponent->ComponentTags.AddUnique(HyperTwistSpeechLibraryInternal::SpeechAudioComponentTag);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(UAudioComponent* AudioComponent)
|
||||
{
|
||||
if (AudioComponent == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents.RemoveAll(
|
||||
[AudioComponent](const HyperTwistSpeechLibraryInternal::FManagedAudioComponentEntry& Entry)
|
||||
{
|
||||
return Entry.AudioComponent.Get() == AudioComponent;
|
||||
}
|
||||
);
|
||||
HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.OriginalVolumeMap.Remove(AudioComponent);
|
||||
}
|
||||
|
||||
int32 UHyperTwistSpeechLibrary::DuckManagedAudioComponents(const float DuckVolumeMultiplier)
|
||||
{
|
||||
return HyperTwistSpeechLibraryInternal::ApplyManagedAudioDucking(DuckVolumeMultiplier);
|
||||
}
|
||||
|
||||
int32 UHyperTwistSpeechLibrary::RestoreManagedAudioComponents()
|
||||
{
|
||||
return HyperTwistSpeechLibraryInternal::RestoreManagedAudioDucking();
|
||||
}
|
||||
|
||||
int32 UHyperTwistSpeechLibrary::GetManagedAudioComponentCount()
|
||||
{
|
||||
HyperTwistSpeechLibraryInternal::CompactManagedAudioRegistry();
|
||||
return HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents.Num();
|
||||
}
|
||||
|
||||
TArray<FHyperTwistVoiceProfileSummary> UHyperTwistSpeechLibrary::ListVoiceProfiles(
|
||||
UObject* ContextObject,
|
||||
const bool bUseMockVoiceClient
|
||||
)
|
||||
{
|
||||
TStrongObjectPtr<UObject> VoiceClientObject(
|
||||
HyperTwistSpeechLibraryInternal::CreateVoiceClient(ContextObject, bUseMockVoiceClient)
|
||||
);
|
||||
if (IHyperTwistVoiceClient* VoiceClient =
|
||||
HyperTwistSpeechLibraryInternal::ResolveVoiceClient(VoiceClientObject.Get()))
|
||||
{
|
||||
return VoiceClient->ListVoiceProfiles();
|
||||
}
|
||||
|
||||
return TArray<FHyperTwistVoiceProfileSummary>();
|
||||
}
|
||||
|
||||
FHyperTwistNarrationSynthesisResult UHyperTwistSpeechLibrary::SynthesizeNarration(
|
||||
UObject* ContextObject,
|
||||
const FHyperTwistNarrationSynthesisRequest& Request,
|
||||
const bool bUseMockVoiceClient
|
||||
)
|
||||
{
|
||||
TStrongObjectPtr<UObject> VoiceClientObject(
|
||||
HyperTwistSpeechLibraryInternal::CreateVoiceClient(ContextObject, bUseMockVoiceClient)
|
||||
);
|
||||
if (IHyperTwistVoiceClient* VoiceClient =
|
||||
HyperTwistSpeechLibraryInternal::ResolveVoiceClient(VoiceClientObject.Get()))
|
||||
{
|
||||
return VoiceClient->SynthesizeNarration(Request);
|
||||
}
|
||||
|
||||
FHyperTwistNarrationSynthesisResult Result;
|
||||
Result.RequestId = Request.RequestId;
|
||||
Result.Warnings.Add(TEXT("voice-client-unavailable"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<uint8> UHyperTwistSpeechLibrary::Synthesize(
|
||||
UObject* ContextObject,
|
||||
const FString& Text,
|
||||
const FString& VoiceId,
|
||||
const bool bUseMockVoiceClient
|
||||
)
|
||||
{
|
||||
FHyperTwistNarrationSynthesisRequest Request =
|
||||
UHyperTwistContractLibrary::MakeSampleNarrationSynthesisRequest();
|
||||
Request.RequestId = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphensLower);
|
||||
Request.ScriptText = Text;
|
||||
Request.SubtitleSeedText = Text;
|
||||
if (!VoiceId.IsEmpty())
|
||||
{
|
||||
Request.VoiceProfileId = VoiceId;
|
||||
}
|
||||
|
||||
const FHyperTwistNarrationSynthesisResult Result =
|
||||
SynthesizeNarration(ContextObject, Request, bUseMockVoiceClient);
|
||||
return HyperTwistSpeechLibraryInternal::ExtractPcm16Payload(Result.AudioBytes, Result.AudioEncoding);
|
||||
}
|
||||
|
||||
FHyperTwistVoiceServiceHealth UHyperTwistSpeechLibrary::GetVoiceServiceHealth(
|
||||
UObject* ContextObject,
|
||||
const bool bUseMockVoiceClient
|
||||
)
|
||||
{
|
||||
TStrongObjectPtr<UObject> VoiceClientObject(
|
||||
HyperTwistSpeechLibraryInternal::CreateVoiceClient(ContextObject, bUseMockVoiceClient)
|
||||
);
|
||||
if (IHyperTwistVoiceClient* VoiceClient =
|
||||
HyperTwistSpeechLibraryInternal::ResolveVoiceClient(VoiceClientObject.Get()))
|
||||
{
|
||||
return VoiceClient->GetVoiceServiceHealth();
|
||||
}
|
||||
|
||||
return FHyperTwistVoiceServiceHealth();
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
namespace HyperTwistClassicCubeActorInternal
|
||||
{
|
||||
constexpr float RotationDotTolerance = 0.9999f;
|
||||
}
|
||||
|
||||
AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
|
@ -54,6 +59,9 @@ void AHyperTwistClassicCubeActor::ResetCube()
|
|||
ClearPieces();
|
||||
TrackedPieces.Empty();
|
||||
RotationQueue.Empty();
|
||||
CurrentScrambleMoves.Empty();
|
||||
TotalCompletedMoveCount = 0;
|
||||
GameplayCompletedMoveCount = 0;
|
||||
bIsAnimating = false;
|
||||
ActiveRotation = FActiveRotation();
|
||||
GenerateCube();
|
||||
|
|
@ -76,25 +84,35 @@ bool AHyperTwistClassicCubeActor::ProcessClick(const FVector& RayOrigin, const F
|
|||
return false;
|
||||
}
|
||||
|
||||
// Map hit normal to cube face
|
||||
const FVector& Normal = Hit.ImpactNormal;
|
||||
// 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;
|
||||
bool bValidFace = true;
|
||||
|
||||
if (FMath::IsNearlyEqual(Normal.Z, 1.0f)) Face = EHyperTwistClassicCubeFace::Up;
|
||||
else if (FMath::IsNearlyEqual(Normal.Z, -1.0f)) Face = EHyperTwistClassicCubeFace::Down;
|
||||
else if (FMath::IsNearlyEqual(Normal.Y, 1.0f)) Face = EHyperTwistClassicCubeFace::Front;
|
||||
else if (FMath::IsNearlyEqual(Normal.Y, -1.0f)) Face = EHyperTwistClassicCubeFace::Back;
|
||||
else if (FMath::IsNearlyEqual(Normal.X, 1.0f)) Face = EHyperTwistClassicCubeFace::Right;
|
||||
else if (FMath::IsNearlyEqual(Normal.X, -1.0f)) Face = EHyperTwistClassicCubeFace::Left;
|
||||
else bValidFace = false;
|
||||
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 (!bValidFace)
|
||||
if (Normal.IsNearlyZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RotateFace(Face, bCounterClockwise ? EHyperTwistRotationDirection::CounterClockwise : EHyperTwistRotationDirection::Clockwise);
|
||||
RotateFace(
|
||||
Face,
|
||||
bCounterClockwise ? EHyperTwistRotationDirection::CounterClockwise
|
||||
: EHyperTwistRotationDirection::Clockwise
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +121,14 @@ TArray<FString> AHyperTwistClassicCubeActor::GenerateScramble(int32 Length)
|
|||
TArray<FString> Result;
|
||||
const TArray<FString> FaceNames = { TEXT("U"), TEXT("D"), TEXT("F"), TEXT("B"), TEXT("R"), TEXT("L") };
|
||||
const TArray<FString> Suffixes = { TEXT(""), TEXT("'"), TEXT("2") };
|
||||
const TArray<EHyperTwistClassicCubeFace> FaceOrder = {
|
||||
EHyperTwistClassicCubeFace::Up,
|
||||
EHyperTwistClassicCubeFace::Down,
|
||||
EHyperTwistClassicCubeFace::Front,
|
||||
EHyperTwistClassicCubeFace::Back,
|
||||
EHyperTwistClassicCubeFace::Right,
|
||||
EHyperTwistClassicCubeFace::Left
|
||||
};
|
||||
|
||||
// Seed random if not already seeded
|
||||
static bool bSeeded = false;
|
||||
|
|
@ -112,59 +138,72 @@ TArray<FString> AHyperTwistClassicCubeActor::GenerateScramble(int32 Length)
|
|||
bSeeded = true;
|
||||
}
|
||||
|
||||
Length = FMath::Max(0, Length);
|
||||
int32 PreviousFaceIndex = INDEX_NONE;
|
||||
int32 PreviousAxisIndex = INDEX_NONE;
|
||||
for (int32 i = 0; i < Length; ++i)
|
||||
{
|
||||
FString Move = FaceNames[FMath::RandRange(0, 5)];
|
||||
TArray<int32> CandidateFaceIndices;
|
||||
for (int32 FaceIndex = 0; FaceIndex < FaceNames.Num(); ++FaceIndex)
|
||||
{
|
||||
if (FaceIndex == PreviousFaceIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32 AxisIndex = GetFaceAxisIndex(FaceOrder[FaceIndex]);
|
||||
if (AxisIndex == PreviousAxisIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CandidateFaceIndices.Add(FaceIndex);
|
||||
}
|
||||
|
||||
if (CandidateFaceIndices.IsEmpty())
|
||||
{
|
||||
for (int32 FaceIndex = 0; FaceIndex < FaceNames.Num(); ++FaceIndex)
|
||||
{
|
||||
if (FaceIndex != PreviousFaceIndex)
|
||||
{
|
||||
CandidateFaceIndices.Add(FaceIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int32 SelectedFaceIndex =
|
||||
CandidateFaceIndices[FMath::RandRange(0, CandidateFaceIndices.Num() - 1)];
|
||||
FString Move = FaceNames[SelectedFaceIndex];
|
||||
FString Suffix = Suffixes[FMath::RandRange(0, 2)];
|
||||
Result.Add(Move + Suffix);
|
||||
PreviousFaceIndex = SelectedFaceIndex;
|
||||
PreviousAxisIndex = GetFaceAxisIndex(FaceOrder[SelectedFaceIndex]);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeActor::ApplyScramble(const TArray<FString>& MoveStrings)
|
||||
{
|
||||
auto ParseFaceChar = [](TCHAR C) -> EHyperTwistClassicCubeFace
|
||||
{
|
||||
switch (C)
|
||||
{
|
||||
case TEXT('U'): return EHyperTwistClassicCubeFace::Up;
|
||||
case TEXT('D'): return EHyperTwistClassicCubeFace::Down;
|
||||
case TEXT('F'): return EHyperTwistClassicCubeFace::Front;
|
||||
case TEXT('B'): return EHyperTwistClassicCubeFace::Back;
|
||||
case TEXT('R'): return EHyperTwistClassicCubeFace::Right;
|
||||
case TEXT('L'): return EHyperTwistClassicCubeFace::Left;
|
||||
}
|
||||
return EHyperTwistClassicCubeFace::Up;
|
||||
};
|
||||
CurrentScrambleMoves.Empty();
|
||||
|
||||
for (const FString& Move : MoveStrings)
|
||||
{
|
||||
if (Move.Len() == 0)
|
||||
EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up;
|
||||
EHyperTwistRotationDirection Direction = EHyperTwistRotationDirection::Clockwise;
|
||||
int32 QuarterTurns = 0;
|
||||
if (!TryParseMoveString(Move, Face, Direction, QuarterTurns))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EHyperTwistClassicCubeFace Face = ParseFaceChar(Move[0]);
|
||||
EHyperTwistRotationDirection Dir = EHyperTwistRotationDirection::Clockwise;
|
||||
int32 Count = 1;
|
||||
|
||||
if (Move.Len() > 1)
|
||||
CurrentScrambleMoves.Add(Move.TrimStartAndEnd());
|
||||
for (int32 i = 0; i < QuarterTurns; ++i)
|
||||
{
|
||||
if (Move[1] == TEXT('\''))
|
||||
{
|
||||
Dir = EHyperTwistRotationDirection::CounterClockwise;
|
||||
}
|
||||
else if (Move[1] == TEXT('2'))
|
||||
{
|
||||
Count = 2;
|
||||
}
|
||||
}
|
||||
|
||||
for (int32 i = 0; i < Count; ++i)
|
||||
{
|
||||
RotateFace(Face, Dir);
|
||||
QueueRotation(Face, Direction, false);
|
||||
}
|
||||
}
|
||||
|
||||
ProcessRotationQueue();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeActor::ClearPieces()
|
||||
|
|
@ -372,9 +411,70 @@ bool AHyperTwistClassicCubeActor::IsAnimating() const
|
|||
return bIsAnimating;
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubeActor::IsSolved() const
|
||||
{
|
||||
if (bIsAnimating || !RotationQueue.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FTrackedPiece& Piece : TrackedPieces)
|
||||
{
|
||||
if (Piece.Mesh == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Piece.GridPos.Equals(GetExpectedGridPositionFromIdentity(Piece.IdentityFaces), KINDA_SMALL_NUMBER))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsSolvedOrientation(Piece.Orientation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeActor::RotateFace(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
|
||||
{
|
||||
RotationQueue.Add(TPair<EHyperTwistClassicCubeFace, EHyperTwistRotationDirection>(Face, Direction));
|
||||
QueueRotation(Face, Direction, true);
|
||||
}
|
||||
|
||||
int32 AHyperTwistClassicCubeActor::GetTotalCompletedMoveCount() const
|
||||
{
|
||||
return TotalCompletedMoveCount;
|
||||
}
|
||||
|
||||
int32 AHyperTwistClassicCubeActor::GetGameplayCompletedMoveCount() const
|
||||
{
|
||||
return GameplayCompletedMoveCount;
|
||||
}
|
||||
|
||||
int32 AHyperTwistClassicCubeActor::GetQueuedRotationCount() const
|
||||
{
|
||||
return RotationQueue.Num();
|
||||
}
|
||||
|
||||
FString AHyperTwistClassicCubeActor::GetCurrentScrambleNotation() const
|
||||
{
|
||||
return FString::Join(CurrentScrambleMoves, TEXT(" "));
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeActor::QueueRotation(
|
||||
EHyperTwistClassicCubeFace Face,
|
||||
EHyperTwistRotationDirection Direction,
|
||||
const bool bGameplayMove
|
||||
)
|
||||
{
|
||||
FQueuedRotationRequest Request;
|
||||
Request.Face = Face;
|
||||
Request.Direction = Direction;
|
||||
Request.bGameplayMove = bGameplayMove;
|
||||
RotationQueue.Add(Request);
|
||||
ProcessRotationQueue();
|
||||
}
|
||||
|
||||
|
|
@ -385,13 +485,19 @@ void AHyperTwistClassicCubeActor::ProcessRotationQueue()
|
|||
return;
|
||||
}
|
||||
|
||||
auto Next = RotationQueue[0];
|
||||
const FQueuedRotationRequest Next = RotationQueue[0];
|
||||
RotationQueue.RemoveAt(0);
|
||||
StartFaceRotation(Next.Key, Next.Value);
|
||||
StartFaceRotation(Next.Face, Next.Direction);
|
||||
ActiveRotation.bGameplayMove = Next.bGameplayMove;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeActor::StartFaceRotation(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
|
||||
{
|
||||
if (!GetWorld())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<int32> Indices = GetFacePieceIndices(TrackedPieces, Face);
|
||||
if (Indices.IsEmpty())
|
||||
{
|
||||
|
|
@ -450,7 +556,16 @@ void AHyperTwistClassicCubeActor::FinalizeRotation()
|
|||
{
|
||||
FTrackedPiece& Piece = TrackedPieces[Idx];
|
||||
Piece.GridPos = RotateGridPosition(Piece.GridPos, ActiveRotation.Face, ActiveRotation.Direction);
|
||||
Piece.Orientation = FaceRot * Piece.Orientation;
|
||||
Piece.GridPos.X = FMath::RoundToFloat(Piece.GridPos.X);
|
||||
Piece.GridPos.Y = FMath::RoundToFloat(Piece.GridPos.Y);
|
||||
Piece.GridPos.Z = FMath::RoundToFloat(Piece.GridPos.Z);
|
||||
Piece.Orientation = (FaceRot * Piece.Orientation).GetNormalized();
|
||||
}
|
||||
|
||||
++TotalCompletedMoveCount;
|
||||
if (ActiveRotation.bGameplayMove)
|
||||
{
|
||||
++GameplayCompletedMoveCount;
|
||||
}
|
||||
|
||||
// Destroy pivot
|
||||
|
|
@ -485,10 +600,10 @@ FVector AHyperTwistClassicCubeActor::RotateGridPosition(const FVector& Pos, EHyp
|
|||
{
|
||||
case EHyperTwistClassicCubeFace::Up: return FVector( Y, -X, Z);
|
||||
case EHyperTwistClassicCubeFace::Down: return FVector(-Y, X, Z);
|
||||
case EHyperTwistClassicCubeFace::Front: return FVector(-Z, Y, X);
|
||||
case EHyperTwistClassicCubeFace::Front: return FVector( Z, Y, -X);
|
||||
case EHyperTwistClassicCubeFace::Back: return FVector(-Z, Y, X);
|
||||
case EHyperTwistClassicCubeFace::Right: return FVector( X, -Z, Y);
|
||||
case EHyperTwistClassicCubeFace::Left: return FVector( X, -Z, Y);
|
||||
case EHyperTwistClassicCubeFace::Left: return FVector( X, Z, -Y);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -497,10 +612,10 @@ FVector AHyperTwistClassicCubeActor::RotateGridPosition(const FVector& Pos, EHyp
|
|||
{
|
||||
case EHyperTwistClassicCubeFace::Up: return FVector(-Y, X, Z);
|
||||
case EHyperTwistClassicCubeFace::Down: return FVector( Y, -X, Z);
|
||||
case EHyperTwistClassicCubeFace::Front: return FVector( Z, Y, -X);
|
||||
case EHyperTwistClassicCubeFace::Front: return FVector(-Z, Y, X);
|
||||
case EHyperTwistClassicCubeFace::Back: return FVector( Z, Y, -X);
|
||||
case EHyperTwistClassicCubeFace::Right: return FVector( X, Z, -Y);
|
||||
case EHyperTwistClassicCubeFace::Left: return FVector( X, Z, -Y);
|
||||
case EHyperTwistClassicCubeFace::Left: return FVector( X, -Z, Y);
|
||||
}
|
||||
}
|
||||
return Pos;
|
||||
|
|
@ -513,10 +628,10 @@ FQuat AHyperTwistClassicCubeActor::GetFaceRotationQuat(EHyperTwistClassicCubeFac
|
|||
{
|
||||
case EHyperTwistClassicCubeFace::Up: return FQuat(FRotator(0.0f, bCW ? -90.0f : 90.0f, 0.0f));
|
||||
case EHyperTwistClassicCubeFace::Down: return FQuat(FRotator(0.0f, bCW ? 90.0f : -90.0f, 0.0f));
|
||||
case EHyperTwistClassicCubeFace::Front: return FQuat(FRotator(bCW ? -90.0f : 90.0f, 0.0f, 0.0f));
|
||||
case EHyperTwistClassicCubeFace::Front: return FQuat(FRotator(bCW ? 90.0f : -90.0f, 0.0f, 0.0f));
|
||||
case EHyperTwistClassicCubeFace::Back: return FQuat(FRotator(bCW ? -90.0f : 90.0f, 0.0f, 0.0f));
|
||||
case EHyperTwistClassicCubeFace::Right: return FQuat(FRotator(0.0f, 0.0f, bCW ? 90.0f : -90.0f));
|
||||
case EHyperTwistClassicCubeFace::Left: return FQuat(FRotator(0.0f, 0.0f, bCW ? 90.0f : -90.0f));
|
||||
case EHyperTwistClassicCubeFace::Left: return FQuat(FRotator(0.0f, 0.0f, bCW ? -90.0f : 90.0f));
|
||||
}
|
||||
return FQuat::Identity;
|
||||
}
|
||||
|
|
@ -562,3 +677,95 @@ FVector AHyperTwistClassicCubeActor::GetFaceCenter(EHyperTwistClassicCubeFace Fa
|
|||
}
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubeActor::TryParseMoveString(
|
||||
const FString& MoveString,
|
||||
EHyperTwistClassicCubeFace& OutFace,
|
||||
EHyperTwistRotationDirection& OutDirection,
|
||||
int32& OutQuarterTurns
|
||||
)
|
||||
{
|
||||
const FString TrimmedMove = MoveString.TrimStartAndEnd();
|
||||
if (TrimmedMove.IsEmpty() || TrimmedMove.Len() > 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (TrimmedMove[0])
|
||||
{
|
||||
case TEXT('U'): OutFace = EHyperTwistClassicCubeFace::Up; break;
|
||||
case TEXT('D'): OutFace = EHyperTwistClassicCubeFace::Down; break;
|
||||
case TEXT('F'): OutFace = EHyperTwistClassicCubeFace::Front; break;
|
||||
case TEXT('B'): OutFace = EHyperTwistClassicCubeFace::Back; break;
|
||||
case TEXT('R'): OutFace = EHyperTwistClassicCubeFace::Right; break;
|
||||
case TEXT('L'): OutFace = EHyperTwistClassicCubeFace::Left; break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
OutDirection = EHyperTwistRotationDirection::Clockwise;
|
||||
OutQuarterTurns = 1;
|
||||
|
||||
if (TrimmedMove.Len() == 2)
|
||||
{
|
||||
if (TrimmedMove[1] == TEXT('\''))
|
||||
{
|
||||
OutDirection = EHyperTwistRotationDirection::CounterClockwise;
|
||||
}
|
||||
else if (TrimmedMove[1] == TEXT('2'))
|
||||
{
|
||||
OutQuarterTurns = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int32 AHyperTwistClassicCubeActor::GetFaceAxisIndex(EHyperTwistClassicCubeFace Face)
|
||||
{
|
||||
switch (Face)
|
||||
{
|
||||
case EHyperTwistClassicCubeFace::Up:
|
||||
case EHyperTwistClassicCubeFace::Down:
|
||||
return 2;
|
||||
case EHyperTwistClassicCubeFace::Front:
|
||||
case EHyperTwistClassicCubeFace::Back:
|
||||
return 1;
|
||||
case EHyperTwistClassicCubeFace::Left:
|
||||
case EHyperTwistClassicCubeFace::Right:
|
||||
return 0;
|
||||
}
|
||||
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
FVector AHyperTwistClassicCubeActor::GetExpectedGridPositionFromIdentity(
|
||||
const TArray<EHyperTwistClassicCubeFace>& IdentityFaces
|
||||
)
|
||||
{
|
||||
FVector Result = FVector::ZeroVector;
|
||||
for (const EHyperTwistClassicCubeFace Face : IdentityFaces)
|
||||
{
|
||||
switch (Face)
|
||||
{
|
||||
case EHyperTwistClassicCubeFace::Up: Result.Z = 1.0f; break;
|
||||
case EHyperTwistClassicCubeFace::Down: Result.Z = -1.0f; break;
|
||||
case EHyperTwistClassicCubeFace::Front: Result.Y = 1.0f; break;
|
||||
case EHyperTwistClassicCubeFace::Back: Result.Y = -1.0f; break;
|
||||
case EHyperTwistClassicCubeFace::Left: Result.X = -1.0f; break;
|
||||
case EHyperTwistClassicCubeFace::Right: Result.X = 1.0f; break;
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool AHyperTwistClassicCubeActor::IsSolvedOrientation(const FQuat& Orientation)
|
||||
{
|
||||
const FQuat Normalized = Orientation.GetNormalized();
|
||||
const double IdentityDot = FMath::Abs(Normalized.W);
|
||||
return IdentityDot >= HyperTwistClassicCubeActorInternal::RotationDotTolerance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,352 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubeGameMode.h"
|
||||
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
|
||||
namespace HyperTwistClassicCubeGameModeInternal
|
||||
{
|
||||
FString FormatMilliseconds(const int32 Milliseconds)
|
||||
{
|
||||
const int32 SafeMilliseconds = FMath::Max(Milliseconds, 0);
|
||||
const int32 Minutes = SafeMilliseconds / 60000;
|
||||
const int32 Seconds = (SafeMilliseconds / 1000) % 60;
|
||||
const int32 MillisRemainder = SafeMilliseconds % 1000;
|
||||
return FString::Printf(TEXT("%02d:%02d.%03d"), Minutes, Seconds, MillisRemainder);
|
||||
}
|
||||
|
||||
EHyperTwistTrainingPenalty ResolveEffectivePenalty(const FHyperTwistTrainingLiveTimerState& TimerState)
|
||||
{
|
||||
if (TimerState.PendingPenalty != EHyperTwistTrainingPenalty::None)
|
||||
{
|
||||
return TimerState.PendingPenalty;
|
||||
}
|
||||
if (TimerState.ManualPenalty != EHyperTwistTrainingPenalty::None)
|
||||
{
|
||||
return TimerState.ManualPenalty;
|
||||
}
|
||||
return TimerState.InspectionPenalty;
|
||||
}
|
||||
|
||||
FString BuildFinalTimeLabel(const FHyperTwistTrainingLiveTimerState& TimerState)
|
||||
{
|
||||
const EHyperTwistTrainingPenalty EffectivePenalty = ResolveEffectivePenalty(TimerState);
|
||||
if (EffectivePenalty == EHyperTwistTrainingPenalty::DNF)
|
||||
{
|
||||
return TEXT("DNF");
|
||||
}
|
||||
|
||||
int32 FinalTimeMs = FMath::Max(TimerState.SolveElapsedMs, 0);
|
||||
if (EffectivePenalty == EHyperTwistTrainingPenalty::Plus2)
|
||||
{
|
||||
FinalTimeMs += 2000;
|
||||
}
|
||||
|
||||
return FormatMilliseconds(FinalTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
AHyperTwistClassicCubeGameMode::AHyperTwistClassicCubeGameMode()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (APlayerController* PlayerController = GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr)
|
||||
{
|
||||
PlayerController->bShowMouseCursor = true;
|
||||
PlayerController->bEnableClickEvents = true;
|
||||
PlayerController->bEnableMouseOverEvents = true;
|
||||
}
|
||||
|
||||
ResolveOrSpawnCubeActor();
|
||||
if (bAutoCreateHud)
|
||||
{
|
||||
ResolveOrCreateHudWidget();
|
||||
}
|
||||
|
||||
StartFreshAttempt();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeGameMode::Tick(const float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
|
||||
ResolveOrSpawnCubeActor();
|
||||
if (bAutoCreateHud)
|
||||
{
|
||||
ResolveOrCreateHudWidget();
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
|
||||
if (ActiveCubeActor != nullptr)
|
||||
{
|
||||
if (bAwaitingScrambleSettlement
|
||||
&& !ActiveCubeActor->IsAnimating()
|
||||
&& ActiveCubeActor->GetQueuedRotationCount() == 0)
|
||||
{
|
||||
bAwaitingScrambleSettlement = false;
|
||||
if (TrainingSubsystem != nullptr)
|
||||
{
|
||||
TrainingSubsystem->StartActiveLiveTimer();
|
||||
bInspectionStarted = TrainingSubsystem->HasActiveLiveTimer();
|
||||
}
|
||||
}
|
||||
|
||||
const int32 CurrentGameplayMoveCount = ActiveCubeActor->GetGameplayCompletedMoveCount();
|
||||
if (CurrentGameplayMoveCount > ObservedGameplayMoveCount)
|
||||
{
|
||||
if (!bSolveStartedFromGameplayMove)
|
||||
{
|
||||
if (TrainingSubsystem != nullptr)
|
||||
{
|
||||
const FHyperTwistTrainingLiveTimerState LiveTimerState =
|
||||
TrainingSubsystem->GetActiveLiveTimerState();
|
||||
if (!TrainingSubsystem->HasActiveLiveTimer())
|
||||
{
|
||||
TrainingSubsystem->StartActiveLiveTimer();
|
||||
}
|
||||
else if (LiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Inspection)
|
||||
{
|
||||
TrainingSubsystem->AdvanceActiveLiveTimerToSolvePhase();
|
||||
}
|
||||
else if (LiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Idle)
|
||||
{
|
||||
TrainingSubsystem->StartActiveLiveTimer();
|
||||
}
|
||||
}
|
||||
|
||||
bSolveStartedFromGameplayMove = true;
|
||||
}
|
||||
|
||||
ObservedGameplayMoveCount = CurrentGameplayMoveCount;
|
||||
}
|
||||
|
||||
if (!bAttemptComplete
|
||||
&& bSolveStartedFromGameplayMove
|
||||
&& !ActiveCubeActor->IsAnimating()
|
||||
&& ActiveCubeActor->GetQueuedRotationCount() == 0
|
||||
&& ActiveCubeActor->IsSolved())
|
||||
{
|
||||
if (TrainingSubsystem != nullptr && TrainingSubsystem->HasActiveLiveTimer())
|
||||
{
|
||||
LastCompletedTimerState = TrainingSubsystem->GetActiveLiveTimerState();
|
||||
LastAttemptStepResult =
|
||||
TrainingSubsystem->SubmitActiveLiveTimedAttempt(EHyperTwistTrainingAttemptResult::Success);
|
||||
}
|
||||
|
||||
bAttemptComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshHud();
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeGameMode::StartFreshAttempt()
|
||||
{
|
||||
ActiveCubeActor = ResolveOrSpawnCubeActor();
|
||||
if (ActiveCubeActor == nullptr)
|
||||
{
|
||||
RefreshHud();
|
||||
return;
|
||||
}
|
||||
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem())
|
||||
{
|
||||
TrainingSubsystem->CancelActiveLiveTimer();
|
||||
TrainingSubsystem->StartSampleClassicTrainingRun(
|
||||
DefaultUserId,
|
||||
BuildSessionId(),
|
||||
EHyperTwistTrainingDeliveryMode::Timer
|
||||
);
|
||||
}
|
||||
|
||||
ActiveCubeActor->ResetCube();
|
||||
const TArray<FString> ScrambleMoves = ActiveCubeActor->GenerateScramble(ScrambleLength);
|
||||
ActiveCubeActor->ApplyScramble(ScrambleMoves);
|
||||
|
||||
LastCompletedTimerState = FHyperTwistTrainingLiveTimerState();
|
||||
LastAttemptStepResult = FHyperTwistTrainingRunStepResult();
|
||||
bAwaitingScrambleSettlement = true;
|
||||
bInspectionStarted = false;
|
||||
bSolveStartedFromGameplayMove = false;
|
||||
bAttemptComplete = false;
|
||||
ObservedGameplayMoveCount = 0;
|
||||
|
||||
RefreshHud();
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* AHyperTwistClassicCubeGameMode::ResolveTrainingSubsystem() const
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UGameInstance* GameInstance = GetWorld()->GetGameInstance())
|
||||
{
|
||||
return GameInstance->GetSubsystem<UHyperTwistTrainingSubsystem>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AHyperTwistClassicCubeActor* AHyperTwistClassicCubeGameMode::ResolveOrSpawnCubeActor()
|
||||
{
|
||||
if (ActiveCubeActor != nullptr)
|
||||
{
|
||||
return ActiveCubeActor;
|
||||
}
|
||||
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (TActorIterator<AHyperTwistClassicCubeActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
|
||||
{
|
||||
ActiveCubeActor = *ActorIt;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ActiveCubeActor == nullptr && bAutoSpawnCubeActor)
|
||||
{
|
||||
ActiveCubeActor = GetWorld()->SpawnActor<AHyperTwistClassicCubeActor>(
|
||||
AHyperTwistClassicCubeActor::StaticClass(),
|
||||
CubeSpawnLocation,
|
||||
CubeSpawnRotation
|
||||
);
|
||||
}
|
||||
|
||||
return ActiveCubeActor;
|
||||
}
|
||||
|
||||
UHyperTwistClassicCubeHUDWidget* AHyperTwistClassicCubeGameMode::ResolveOrCreateHudWidget()
|
||||
{
|
||||
if (ActiveHudWidget != nullptr)
|
||||
{
|
||||
return ActiveHudWidget;
|
||||
}
|
||||
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
APlayerController* PlayerController = GetWorld()->GetFirstPlayerController();
|
||||
if (PlayerController == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const TSubclassOf<UHyperTwistClassicCubeHUDWidget> ResolvedClass =
|
||||
*HudWidgetClass != nullptr
|
||||
? HudWidgetClass
|
||||
: TSubclassOf<UHyperTwistClassicCubeHUDWidget>(UHyperTwistClassicCubeHUDWidget::StaticClass());
|
||||
ActiveHudWidget = CreateWidget<UHyperTwistClassicCubeHUDWidget>(PlayerController, ResolvedClass);
|
||||
if (ActiveHudWidget != nullptr)
|
||||
{
|
||||
ActiveHudWidget->AddToViewport(HudZOrder);
|
||||
}
|
||||
|
||||
return ActiveHudWidget;
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeGameMode::RefreshHud()
|
||||
{
|
||||
if (ResolveOrCreateHudWidget() == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FString StatusLine = TEXT("status: waiting for classic cube actor");
|
||||
FString ScrambleLine = TEXT("scramble: pending");
|
||||
FString TimerLine = TEXT("timer: idle");
|
||||
FString InspectionLine = TEXT("inspection: pending");
|
||||
FString ResultLine = TEXT("result: waiting for first attempt");
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
|
||||
FHyperTwistTrainingLiveTimerState LiveTimerState;
|
||||
const bool bHasLiveTimer = TrainingSubsystem != nullptr && TrainingSubsystem->HasActiveLiveTimer();
|
||||
if (bHasLiveTimer)
|
||||
{
|
||||
LiveTimerState = TrainingSubsystem->GetActiveLiveTimerState();
|
||||
}
|
||||
|
||||
if (ActiveCubeActor != nullptr)
|
||||
{
|
||||
const FString ScrambleNotation = ActiveCubeActor->GetCurrentScrambleNotation();
|
||||
ScrambleLine = ScrambleNotation.IsEmpty()
|
||||
? TEXT("scramble: pending")
|
||||
: FString::Printf(TEXT("scramble: %s"), *ScrambleNotation);
|
||||
|
||||
if (bAwaitingScrambleSettlement)
|
||||
{
|
||||
StatusLine = TEXT("status: scrambling cube");
|
||||
InspectionLine = TEXT("inspection: arming after scramble playback");
|
||||
}
|
||||
else if (bAttemptComplete)
|
||||
{
|
||||
StatusLine = TEXT("status: solved");
|
||||
InspectionLine = TEXT("inspection: closed on first gameplay turn");
|
||||
TimerLine = FString::Printf(
|
||||
TEXT("timer: %s"),
|
||||
*HyperTwistClassicCubeGameModeInternal::BuildFinalTimeLabel(LastCompletedTimerState)
|
||||
);
|
||||
ResultLine = LastAttemptStepResult.IsStructurallyValid()
|
||||
? TEXT("result: timed solve submitted")
|
||||
: TEXT("result: solved outside active timer lane");
|
||||
}
|
||||
else if (bHasLiveTimer && LiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Inspection)
|
||||
{
|
||||
StatusLine = TEXT("status: inspection running");
|
||||
const int32 RemainingInspectionMs =
|
||||
FMath::Max(LiveTimerState.InspectionLimitMs - LiveTimerState.InspectionElapsedMs, 0);
|
||||
InspectionLine = FString::Printf(
|
||||
TEXT("inspection: %s remaining"),
|
||||
*HyperTwistClassicCubeGameModeInternal::FormatMilliseconds(RemainingInspectionMs)
|
||||
);
|
||||
}
|
||||
else if (bHasLiveTimer
|
||||
&& (LiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Solving
|
||||
|| LiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Paused))
|
||||
{
|
||||
StatusLine = LiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Paused
|
||||
? TEXT("status: solve paused")
|
||||
: TEXT("status: solving");
|
||||
InspectionLine = TEXT("inspection: closed on first gameplay turn");
|
||||
TimerLine = FString::Printf(
|
||||
TEXT("timer: %s"),
|
||||
*HyperTwistClassicCubeGameModeInternal::FormatMilliseconds(
|
||||
FMath::Max(LiveTimerState.DisplayedElapsedMs, LiveTimerState.SolveElapsedMs)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (bInspectionStarted)
|
||||
{
|
||||
StatusLine = TEXT("status: ready for first gameplay turn");
|
||||
InspectionLine = TEXT("inspection: ready");
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusLine = TEXT("status: preparing attempt");
|
||||
}
|
||||
}
|
||||
|
||||
ActiveHudWidget->SetHudLines(StatusLine, ScrambleLine, TimerLine, InspectionLine, ResultLine);
|
||||
}
|
||||
|
||||
FString AHyperTwistClassicCubeGameMode::BuildSessionId() const
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("%s_%s"),
|
||||
*SessionIdPrefix,
|
||||
*FGuid::NewGuid().ToString(EGuidFormats::Digits)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
|
||||
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/VerticalBox.h"
|
||||
#include "Components/VerticalBoxSlot.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
EnsureWidgetTreeBuilt();
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::SetHudLines(
|
||||
const FString& InStatusLine,
|
||||
const FString& InScrambleLine,
|
||||
const FString& InTimerLine,
|
||||
const FString& InInspectionLine,
|
||||
const FString& InResultLine
|
||||
)
|
||||
{
|
||||
EnsureWidgetTreeBuilt();
|
||||
|
||||
if (StatusTextBlock != nullptr)
|
||||
{
|
||||
StatusTextBlock->SetText(FText::FromString(InStatusLine));
|
||||
}
|
||||
if (ScrambleTextBlock != nullptr)
|
||||
{
|
||||
ScrambleTextBlock->SetText(FText::FromString(InScrambleLine));
|
||||
}
|
||||
if (TimerTextBlock != nullptr)
|
||||
{
|
||||
TimerTextBlock->SetText(FText::FromString(InTimerLine));
|
||||
}
|
||||
if (InspectionTextBlock != nullptr)
|
||||
{
|
||||
InspectionTextBlock->SetText(FText::FromString(InInspectionLine));
|
||||
}
|
||||
if (ResultTextBlock != nullptr)
|
||||
{
|
||||
ResultTextBlock->SetText(FText::FromString(InResultLine));
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
|
||||
{
|
||||
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UVerticalBox* RootLayout =
|
||||
WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("ClassicCubeHudRoot"));
|
||||
WidgetTree->RootWidget = RootLayout;
|
||||
|
||||
StatusTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudStatus"));
|
||||
ScrambleTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudScramble"));
|
||||
TimerTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudTimer"));
|
||||
InspectionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudInspection"));
|
||||
ResultTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudResult"));
|
||||
|
||||
SetHudLines(
|
||||
TEXT("status: preparing classic cube lane"),
|
||||
TEXT("scramble: pending"),
|
||||
TEXT("timer: idle"),
|
||||
TEXT("inspection: pending"),
|
||||
TEXT("result: waiting for first attempt")
|
||||
);
|
||||
}
|
||||
|
||||
UTextBlock* UHyperTwistClassicCubeHUDWidget::AddLine(UVerticalBox* Parent, const TCHAR* WidgetName)
|
||||
{
|
||||
if (WidgetTree == nullptr || Parent == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UTextBlock* TextBlock =
|
||||
WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), WidgetName);
|
||||
if (TextBlock == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UVerticalBoxSlot* VerticalBoxSlot = Parent->AddChildToVerticalBox(TextBlock))
|
||||
{
|
||||
VerticalBoxSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 4.0f));
|
||||
}
|
||||
|
||||
return TextBlock;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "HyperTwistBrowserBridgeObject.generated.h"
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UNREALHYPERTWIST_API UHyperTwistBrowserBridgeObject : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void NotifyEnvelope(const FString& EnvelopeJson);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void NotifyState(const FString& StateJson);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void NotifyRuntimeReady(const FString& RuntimeReadyJson);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void ResetReceivedMessages();
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|Browser")
|
||||
FString LastEnvelopeJson;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|Browser")
|
||||
FString LastStateJson;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|Browser")
|
||||
FString LastRuntimeReadyJson;
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "HyperTwistBrowserWidget.generated.h"
|
||||
|
||||
class SWebBrowser;
|
||||
class UHyperTwistBrowserBridgeObject;
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UNREALHYPERTWIST_API UHyperTwistBrowserWidget : public UWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Browser")
|
||||
bool bUseBundledBrowserShell = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Browser")
|
||||
bool bShowBrowserControls = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Browser")
|
||||
FString InitialUrl;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void LoadBundledBrowserShell();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void LoadBrowserUrl(const FString& NewUrl);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void DispatchCommandJson(const FString& CommandJson);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Browser")
|
||||
void PushShellStateJson(const FString& StateJson);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
FString GetLastEnvelopeJson() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
FString GetLastRuntimeReadyJson() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
FString GetLastStateJson() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
static FString ResolveBundledBrowserShellUrl();
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
|
||||
private:
|
||||
static FString EscapeForJavaScriptSingleQuotedString(const FString& Input);
|
||||
FString ResolveInitialUrl() const;
|
||||
void EnsureBridgeBound();
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UHyperTwistBrowserBridgeObject> BrowserBridgeObject = nullptr;
|
||||
|
||||
TSharedPtr<SWebBrowser> BrowserWidget;
|
||||
};
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "HyperTwistRecognition/HyperTwistRecognitionTypes.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingTypes.h"
|
||||
#include "HyperTwistSpeechLibrary.generated.h"
|
||||
|
||||
class UAudioComponent;
|
||||
|
||||
UCLASS()
|
||||
class UNREALHYPERTWIST_API UHyperTwistSpeechLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech", meta = (WorldContext = "WorldContextObject"))
|
||||
static bool StartDictationSession(
|
||||
UObject* WorldContextObject,
|
||||
FString& OutError,
|
||||
bool bDuckAudio = true,
|
||||
float DuckVolumeMultiplier = 0.15f
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech", meta = (WorldContext = "WorldContextObject"))
|
||||
static FHyperTwistSpeechTranscriptResult SubmitDictationUtterance(
|
||||
UObject* WorldContextObject,
|
||||
const FHyperTwistSpeechUtteranceEnvelope& Utterance
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech", meta = (WorldContext = "WorldContextObject"))
|
||||
static bool EndDictationSession(
|
||||
UObject* WorldContextObject,
|
||||
FString& OutError,
|
||||
bool bRestoreAudio = true
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech", meta = (WorldContext = "WorldContextObject"))
|
||||
static FHyperTwistTrainingCompanionSpeechSessionState GetActiveDictationSessionState(
|
||||
UObject* WorldContextObject
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
|
||||
static void RegisterManagedAudioComponent(
|
||||
UAudioComponent* AudioComponent,
|
||||
bool bTreatAsSpeechAudio = false
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
|
||||
static void UnregisterManagedAudioComponent(UAudioComponent* AudioComponent);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
|
||||
static int32 DuckManagedAudioComponents(float DuckVolumeMultiplier = 0.15f);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
|
||||
static int32 RestoreManagedAudioComponents();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech|Audio")
|
||||
static int32 GetManagedAudioComponentCount();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech|Voice")
|
||||
static TArray<FHyperTwistVoiceProfileSummary> ListVoiceProfiles(
|
||||
UObject* ContextObject,
|
||||
bool bUseMockVoiceClient = false
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Voice")
|
||||
static FHyperTwistNarrationSynthesisResult SynthesizeNarration(
|
||||
UObject* ContextObject,
|
||||
const FHyperTwistNarrationSynthesisRequest& Request,
|
||||
bool bUseMockVoiceClient = false
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Voice")
|
||||
static TArray<uint8> Synthesize(
|
||||
UObject* ContextObject,
|
||||
const FString& Text,
|
||||
const FString& VoiceId,
|
||||
bool bUseMockVoiceClient = false
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech|Voice")
|
||||
static FHyperTwistVoiceServiceHealth GetVoiceServiceHealth(
|
||||
UObject* ContextObject,
|
||||
bool bUseMockVoiceClient = false
|
||||
);
|
||||
};
|
||||
|
|
@ -54,6 +54,10 @@ public:
|
|||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube")
|
||||
void ResetCube();
|
||||
|
||||
/** True when every tracked piece is back in its canonical solved position and orientation. */
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube")
|
||||
bool IsSolved() const;
|
||||
|
||||
/**
|
||||
* Process a click ray. If it hits a visible cube face, rotates that face.
|
||||
* @param RayOrigin World-space ray origin (e.g. camera location)
|
||||
|
|
@ -72,6 +76,22 @@ public:
|
|||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube")
|
||||
void ApplyScramble(const TArray<FString>& MoveStrings);
|
||||
|
||||
/** Total completed quarter turns, including scripted scramble turns. */
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube")
|
||||
int32 GetTotalCompletedMoveCount() const;
|
||||
|
||||
/** Total completed gameplay quarter turns triggered outside scramble playback. */
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube")
|
||||
int32 GetGameplayCompletedMoveCount() const;
|
||||
|
||||
/** Number of queued rotations still waiting to start. */
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube")
|
||||
int32 GetQueuedRotationCount() const;
|
||||
|
||||
/** The current active scramble as a single WCA-style notation string. */
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube")
|
||||
FString GetCurrentScrambleNotation() const;
|
||||
|
||||
virtual void OnConstruction(const FTransform& Transform) override;
|
||||
|
||||
protected:
|
||||
|
|
@ -97,6 +117,14 @@ private:
|
|||
float Duration = 0.15f;
|
||||
EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up;
|
||||
EHyperTwistRotationDirection Direction = EHyperTwistRotationDirection::Clockwise;
|
||||
bool bGameplayMove = false;
|
||||
};
|
||||
|
||||
struct FQueuedRotationRequest
|
||||
{
|
||||
EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up;
|
||||
EHyperTwistRotationDirection Direction = EHyperTwistRotationDirection::Clockwise;
|
||||
bool bGameplayMove = false;
|
||||
};
|
||||
|
||||
void GenerateCube();
|
||||
|
|
@ -106,6 +134,11 @@ private:
|
|||
const FVector& Center, float HalfSize, EHyperTwistClassicCubeFace Face,
|
||||
UMaterialInterface* Material);
|
||||
|
||||
void QueueRotation(
|
||||
EHyperTwistClassicCubeFace Face,
|
||||
EHyperTwistRotationDirection Direction,
|
||||
bool bGameplayMove
|
||||
);
|
||||
void StartFaceRotation(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction);
|
||||
void FinalizeRotation();
|
||||
void ProcessRotationQueue();
|
||||
|
|
@ -114,12 +147,24 @@ private:
|
|||
static FQuat GetFaceRotationQuat(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction);
|
||||
static TArray<int32> GetFacePieceIndices(const TArray<FTrackedPiece>& Pieces, EHyperTwistClassicCubeFace Face);
|
||||
static FVector GetFaceCenter(EHyperTwistClassicCubeFace Face);
|
||||
static bool TryParseMoveString(
|
||||
const FString& MoveString,
|
||||
EHyperTwistClassicCubeFace& OutFace,
|
||||
EHyperTwistRotationDirection& OutDirection,
|
||||
int32& OutQuarterTurns
|
||||
);
|
||||
static int32 GetFaceAxisIndex(EHyperTwistClassicCubeFace Face);
|
||||
static FVector GetExpectedGridPositionFromIdentity(const TArray<EHyperTwistClassicCubeFace>& IdentityFaces);
|
||||
static bool IsSolvedOrientation(const FQuat& Orientation);
|
||||
|
||||
int32 GridToIndex(const FVector& GridPos) const;
|
||||
FVector IndexToGrid(int32 Index) const;
|
||||
|
||||
TArray<FTrackedPiece> TrackedPieces;
|
||||
TArray<TPair<EHyperTwistClassicCubeFace, EHyperTwistRotationDirection>> RotationQueue;
|
||||
TArray<FQueuedRotationRequest> RotationQueue;
|
||||
FActiveRotation ActiveRotation;
|
||||
TArray<FString> CurrentScrambleMoves;
|
||||
int32 TotalCompletedMoveCount = 0;
|
||||
int32 GameplayCompletedMoveCount = 0;
|
||||
bool bIsAnimating = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingTypes.h"
|
||||
#include "HyperTwistClassicCubeGameMode.generated.h"
|
||||
|
||||
class AHyperTwistClassicCubeActor;
|
||||
class UHyperTwistClassicCubeHUDWidget;
|
||||
class UHyperTwistTrainingSubsystem;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API AHyperTwistClassicCubeGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AHyperTwistClassicCubeGameMode();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|HUD")
|
||||
bool bAutoCreateHud = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|HUD")
|
||||
int32 HudZOrder = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|HUD")
|
||||
TSubclassOf<UHyperTwistClassicCubeHUDWidget> HudWidgetClass;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
|
||||
bool bAutoSpawnCubeActor = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
|
||||
FVector CubeSpawnLocation = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
|
||||
FRotator CubeSpawnRotation = FRotator::ZeroRotator;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
|
||||
int32 ScrambleLength = 20;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
|
||||
FString DefaultUserId = TEXT("local-user");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
|
||||
FString SessionIdPrefix = TEXT("classic_cube");
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube")
|
||||
TObjectPtr<AHyperTwistClassicCubeActor> ActiveCubeActor = nullptr;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube|HUD")
|
||||
TObjectPtr<UHyperTwistClassicCubeHUDWidget> ActiveHudWidget = nullptr;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube")
|
||||
void StartFreshAttempt();
|
||||
|
||||
protected:
|
||||
UHyperTwistTrainingSubsystem* ResolveTrainingSubsystem() const;
|
||||
AHyperTwistClassicCubeActor* ResolveOrSpawnCubeActor();
|
||||
UHyperTwistClassicCubeHUDWidget* ResolveOrCreateHudWidget();
|
||||
void RefreshHud();
|
||||
FString BuildSessionId() const;
|
||||
|
||||
private:
|
||||
FHyperTwistTrainingLiveTimerState LastCompletedTimerState;
|
||||
FHyperTwistTrainingRunStepResult LastAttemptStepResult;
|
||||
bool bAwaitingScrambleSettlement = false;
|
||||
bool bInspectionStarted = false;
|
||||
bool bSolveStartedFromGameplayMove = false;
|
||||
bool bAttemptComplete = false;
|
||||
int32 ObservedGameplayMoveCount = 0;
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "HyperTwistClassicCubeHUDWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
class UVerticalBox;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API UHyperTwistClassicCubeHUDWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD")
|
||||
void SetHudLines(
|
||||
const FString& InStatusLine,
|
||||
const FString& InScrambleLine,
|
||||
const FString& InTimerLine,
|
||||
const FString& InInspectionLine,
|
||||
const FString& InResultLine
|
||||
);
|
||||
|
||||
private:
|
||||
void EnsureWidgetTreeBuilt();
|
||||
UTextBlock* AddLine(UVerticalBox* Parent, const TCHAR* WidgetName);
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> StatusTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ScrambleTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> TimerTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> InspectionTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ResultTextBlock = nullptr;
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistBrowser/HyperTwistBrowserBridgeObject.h"
|
||||
#include "HyperTwistBrowser/HyperTwistBrowserWidget.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserBridgeObjectNotificationTest,
|
||||
"HyperTwist.Browser.Bridge.NotificationCapture",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistBrowserBridgeObjectNotificationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UHyperTwistBrowserBridgeObject* BridgeObject = NewObject<UHyperTwistBrowserBridgeObject>();
|
||||
TestNotNull(TEXT("The browser bridge object must be constructible."), BridgeObject);
|
||||
if (BridgeObject == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BridgeObject->NotifyEnvelope(TEXT("{\"type\":\"hypertwist-state\"}"));
|
||||
BridgeObject->NotifyState(TEXT("{\"phase\":\"1H\"}"));
|
||||
BridgeObject->NotifyRuntimeReady(TEXT("{\"status\":\"ready\"}"));
|
||||
|
||||
TestEqual(
|
||||
TEXT("The browser bridge object must retain the last envelope payload."),
|
||||
BridgeObject->LastEnvelopeJson,
|
||||
TEXT("{\"type\":\"hypertwist-state\"}")
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The browser bridge object must retain the last shell-state payload."),
|
||||
BridgeObject->LastStateJson,
|
||||
TEXT("{\"phase\":\"1H\"}")
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The browser bridge object must retain the runtime-ready payload."),
|
||||
BridgeObject->LastRuntimeReadyJson,
|
||||
TEXT("{\"status\":\"ready\"}")
|
||||
);
|
||||
|
||||
BridgeObject->ResetReceivedMessages();
|
||||
TestTrue(
|
||||
TEXT("Resetting the bridge object must clear all retained payloads."),
|
||||
BridgeObject->LastEnvelopeJson.IsEmpty()
|
||||
&& BridgeObject->LastStateJson.IsEmpty()
|
||||
&& BridgeObject->LastRuntimeReadyJson.IsEmpty()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserWidgetBundledShellUrlTest,
|
||||
"HyperTwist.Browser.Widget.BundledShellUrl",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistBrowserWidgetBundledShellUrlTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FString BundledShellUrl = UHyperTwistBrowserWidget::ResolveBundledBrowserShellUrl();
|
||||
|
||||
TestFalse(TEXT("The bundled browser shell URL must not be empty."), BundledShellUrl.IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("The bundled browser shell URL must resolve to a file URL."),
|
||||
BundledShellUrl.StartsWith(TEXT("file:///"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The bundled browser shell URL must point at the Browser shell."),
|
||||
BundledShellUrl.Contains(TEXT("Content/Browser"))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
|
||||
|
||||
namespace HyperTwistClassicCubeActorTestInternal
|
||||
{
|
||||
int32 GetAxisIndex(const TCHAR MoveFace)
|
||||
{
|
||||
switch (MoveFace)
|
||||
{
|
||||
case TEXT('U'):
|
||||
case TEXT('D'):
|
||||
return 2;
|
||||
case TEXT('F'):
|
||||
case TEXT('B'):
|
||||
return 1;
|
||||
case TEXT('L'):
|
||||
case TEXT('R'):
|
||||
return 0;
|
||||
default:
|
||||
return INDEX_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeScrambleQualityTest,
|
||||
"HyperTwist.Simulation.ClassicCube.ScrambleQuality",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeScrambleQualityTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
AHyperTwistClassicCubeActor* CubeActor = NewObject<AHyperTwistClassicCubeActor>(GetTransientPackage());
|
||||
TestNotNull(TEXT("The classic cube actor must be constructible for scramble checks."), CubeActor);
|
||||
if (CubeActor == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<FString> Scramble = CubeActor->GenerateScramble(32);
|
||||
TestEqual(TEXT("The scramble generator must respect the requested move count."), Scramble.Num(), 32);
|
||||
|
||||
for (int32 MoveIndex = 0; MoveIndex < Scramble.Num(); ++MoveIndex)
|
||||
{
|
||||
const FString& Move = Scramble[MoveIndex];
|
||||
TestTrue(TEXT("Each scramble move must be non-empty."), !Move.IsEmpty());
|
||||
TestTrue(TEXT("Each scramble move must be one or two characters long."), Move.Len() >= 1 && Move.Len() <= 2);
|
||||
|
||||
const TCHAR Face = Move[0];
|
||||
const int32 AxisIndex = HyperTwistClassicCubeActorTestInternal::GetAxisIndex(Face);
|
||||
TestTrue(TEXT("Each scramble move must start with a legal face token."), AxisIndex != INDEX_NONE);
|
||||
|
||||
if (Move.Len() == 2)
|
||||
{
|
||||
TestTrue(
|
||||
TEXT("Each scramble suffix must be either a prime marker or a double-turn marker."),
|
||||
Move[1] == TEXT('\'') || Move[1] == TEXT('2')
|
||||
);
|
||||
}
|
||||
|
||||
if (MoveIndex == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString& PreviousMove = Scramble[MoveIndex - 1];
|
||||
TestNotEqual(
|
||||
TEXT("The scramble generator must avoid repeating the same face consecutively."),
|
||||
Face,
|
||||
PreviousMove[0]
|
||||
);
|
||||
TestNotEqual(
|
||||
TEXT("The scramble generator must avoid immediate same-axis repetitions."),
|
||||
AxisIndex,
|
||||
HyperTwistClassicCubeActorTestInternal::GetAxisIndex(PreviousMove[0])
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistIPC/HyperTwistSolverOracleLibrary.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistSolverOracleVerificationSolvedStateTest,
|
||||
"HyperTwist.Solver.Oracle.VerifySolvedState",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistSolverOracleVerificationSolvedStateTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FString SolvedState = TEXT("UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB");
|
||||
|
||||
TestTrue(
|
||||
TEXT("An empty move sequence must verify against the solved classic cube state."),
|
||||
UHyperTwistSolverOracleLibrary::VerifySolutionWithBrownanOracle(SolvedState, {})
|
||||
);
|
||||
|
||||
TestFalse(
|
||||
TEXT("A non-empty move sequence must not verify against the solved classic cube state."),
|
||||
UHyperTwistSolverOracleLibrary::VerifySolutionWithBrownanOracle(SolvedState, {TEXT("R")})
|
||||
);
|
||||
|
||||
TestFalse(
|
||||
TEXT("Invalid move notation must fail solver-oracle verification."),
|
||||
UHyperTwistSolverOracleLibrary::VerifySolutionWithBrownanOracle(SolvedState, {TEXT("?")})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "HyperTwistRecognition/HyperTwistSpeechLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
namespace HyperTwistSpeechLibraryTestInternal
|
||||
{
|
||||
FHyperTwistTrainingDeck MakeSpeechDeck()
|
||||
{
|
||||
FHyperTwistTrainingDeck Deck;
|
||||
Deck.DeckId = TEXT("speech-library/coach-deck");
|
||||
Deck.Title = TEXT("Speech Library Coach Deck");
|
||||
Deck.DeliveryModes = { EHyperTwistTrainingDeliveryMode::CoachReviewed };
|
||||
|
||||
FHyperTwistTrainingCase TrainingCase;
|
||||
TrainingCase.CaseId = TEXT("speech-library-case");
|
||||
TrainingCase.PuzzleId = TEXT("cube/3x3x3");
|
||||
TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence;
|
||||
TrainingCase.PromptLabel = TEXT("Repeat case");
|
||||
TrainingCase.AllowedDeliveryModes = { EHyperTwistTrainingDeliveryMode::CoachReviewed };
|
||||
Deck.Cases = { TrainingCase };
|
||||
return Deck;
|
||||
}
|
||||
|
||||
void ForceMockSpeechClient(UHyperTwistTrainingSubsystem* TrainingSubsystem)
|
||||
{
|
||||
if (TrainingSubsystem == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (FStrProperty* SpeechClientKindProperty = FindFProperty<FStrProperty>(
|
||||
UHyperTwistTrainingSubsystem::StaticClass(),
|
||||
TEXT("CompanionSpeechClientKind")))
|
||||
{
|
||||
SpeechClientKindProperty->SetPropertyValue_InContainer(TrainingSubsystem, TEXT("mock"));
|
||||
}
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* MakeSpeechSubsystem(const FString& SessionId)
|
||||
{
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>();
|
||||
if (GameInstance == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
|
||||
if (TrainingSubsystem == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ForceMockSpeechClient(TrainingSubsystem);
|
||||
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck(
|
||||
MakeSpeechDeck(),
|
||||
TEXT("speech-library-user"),
|
||||
SessionId,
|
||||
EHyperTwistTrainingDeliveryMode::CoachReviewed
|
||||
);
|
||||
return RunState.IsStructurallyValid() ? TrainingSubsystem : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistSpeechLibraryDictationAudioDuckingTest,
|
||||
"HyperTwist.Permissive.Speech.Library.DictationAudioDucking",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistSpeechLibraryDictationAudioDuckingTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem =
|
||||
HyperTwistSpeechLibraryTestInternal::MakeSpeechSubsystem(TEXT("speech-library-ducking-session"));
|
||||
TestNotNull(TEXT("The speech training subsystem must exist."), TrainingSubsystem);
|
||||
if (TrainingSubsystem == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UAudioComponent* MusicComponent = NewObject<UAudioComponent>();
|
||||
UAudioComponent* SpeechComponent = NewObject<UAudioComponent>();
|
||||
TestNotNull(TEXT("The managed music component must exist."), MusicComponent);
|
||||
TestNotNull(TEXT("The managed speech component must exist."), SpeechComponent);
|
||||
if (MusicComponent == nullptr || SpeechComponent == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
MusicComponent->SetVolumeMultiplier(0.8f);
|
||||
SpeechComponent->SetVolumeMultiplier(1.0f);
|
||||
UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(MusicComponent, false);
|
||||
UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(SpeechComponent, true);
|
||||
|
||||
FString OpenError;
|
||||
TestTrue(
|
||||
TEXT("The speech library must open a dictation session."),
|
||||
UHyperTwistSpeechLibrary::StartDictationSession(TrainingSubsystem, OpenError, true, 0.15f)
|
||||
);
|
||||
TestTrue(TEXT("Opening the dictation session must not report an error."), OpenError.IsEmpty());
|
||||
TestEqual(
|
||||
TEXT("Managed non-speech audio must be ducked."),
|
||||
MusicComponent->VolumeMultiplier,
|
||||
0.15f
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Managed speech audio must retain its original volume."),
|
||||
SpeechComponent->VolumeMultiplier,
|
||||
1.0f
|
||||
);
|
||||
|
||||
FString CloseError;
|
||||
TestTrue(
|
||||
TEXT("The speech library must close the dictation session."),
|
||||
UHyperTwistSpeechLibrary::EndDictationSession(TrainingSubsystem, CloseError, true)
|
||||
);
|
||||
TestTrue(TEXT("Closing the dictation session must not report an error."), CloseError.IsEmpty());
|
||||
TestEqual(
|
||||
TEXT("Managed non-speech audio must restore after dictation ends."),
|
||||
MusicComponent->VolumeMultiplier,
|
||||
0.8f
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Managed speech audio must remain unchanged after restore."),
|
||||
SpeechComponent->VolumeMultiplier,
|
||||
1.0f
|
||||
);
|
||||
|
||||
UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(MusicComponent);
|
||||
UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(SpeechComponent);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistSpeechLibraryPcmExtractionTest,
|
||||
"HyperTwist.Permissive.Speech.Library.PcmExtraction",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistSpeechLibraryPcmExtractionTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const TArray<uint8> PcmPayload = UHyperTwistSpeechLibrary::Synthesize(
|
||||
nullptr,
|
||||
TEXT("hello from HyperTwist"),
|
||||
TEXT("en_US-lessac-medium"),
|
||||
true
|
||||
);
|
||||
|
||||
TestTrue(TEXT("The speech library must expose a non-empty PCM payload."), PcmPayload.Num() > 0);
|
||||
if (PcmPayload.Num() >= 4)
|
||||
{
|
||||
const bool bLooksLikeWavHeader =
|
||||
PcmPayload[0] == 'R' && PcmPayload[1] == 'I' && PcmPayload[2] == 'F' && PcmPayload[3] == 'F';
|
||||
TestFalse(TEXT("The speech library PCM helper must strip the WAV container header."), bLooksLikeWavHeader);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -11,7 +11,7 @@ public class UnrealHyperTwist : ModuleRules
|
|||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "Json", "JsonUtilities", "UMG", "Voice", "ProceduralMeshComponent" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "HTTP" });
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "HTTP", "WebBrowser" });
|
||||
|
||||
// External donor repo include paths
|
||||
string ExternalPath = Path.Combine(ModuleDirectory, "../../../.external/");
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@
|
|||
{
|
||||
"Name": "ProceduralMeshComponent",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "WebBrowserWidget",
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# HyperTwist — Comprehensive Reconstruction Roadmap
|
||||
## Status: Draft — Post-Audit Reality Alignment
|
||||
## Status: Live — Phase 1 and Phase 2 Reality-Aligned
|
||||
## Date: 2026-06-10
|
||||
|
||||
## Remaining Repo Inventory (Authoritative Reconstruction)
|
||||
## Reconstruction Starting Inventory (Authoritative Snapshot)
|
||||
|
||||
Source: `HYPERTWIST_REPO_STATE_BOARD_2026-05-11.md` and `HYPERTWIST_REPO_EVALUATION_RESET_AND_IMPLEMENTATION_SCHEDULE_2026-05-11.md`
|
||||
|
||||
|
|
@ -33,7 +33,11 @@ Source: `HYPERTWIST_REPO_STATE_BOARD_2026-05-11.md` and `HYPERTWIST_REPO_EVALUAT
|
|||
- `google/model-viewer/packages/*` (sub-packages)
|
||||
- And others in the 0R-B backlog
|
||||
|
||||
**None of these 29 are currently wired into the Unreal build.** They exist only as attribution bundles and JSONL reference pages.
|
||||
This section records the starting reconstruction snapshot only.
|
||||
|
||||
The live Phase 1 source of truth is the checklist below: the accepted donor set
|
||||
is now wired across native C++, IPC/sidecar, and `Content/Browser/` runtime
|
||||
surfaces rather than remaining attribution-only.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -41,11 +45,12 @@ Source: `HYPERTWIST_REPO_STATE_BOARD_2026-05-11.md` and `HYPERTWIST_REPO_EVALUAT
|
|||
|
||||
## Executive Summary
|
||||
|
||||
This roadmap replaces the self-referential validation-ladder sequence with a
|
||||
functional, repo-first reconstruction plan. The audit confirms that HyperTwist
|
||||
is ~80% documentation scaffolding, ~15% data-structure code, and ~5% actual
|
||||
game logic. No 3D puzzle renderer exists. No playable level exists. No donor
|
||||
repos are wired into the build.
|
||||
This roadmap replaced the self-referential validation-ladder sequence with a
|
||||
functional, repo-first reconstruction plan. The original audit snapshot
|
||||
correctly identified the missing centerpieces: no classic-cube renderer, no
|
||||
timer/HUD loop, and no real donor wiring. The current Phase 1 and Phase 2 lane
|
||||
has now closed those gaps with a shipped classic-cube actor, timer/HUD game
|
||||
mode, speech/solver surfaces, and a buildable embedded browser runtime.
|
||||
|
||||
**The new rule:** Repos are wired before features are claimed. Features are
|
||||
playable before they are documented. Validation tests verify behavior, not
|
||||
|
|
@ -84,58 +89,58 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
- [x] Windows build validated: 5 actions, 38.87s, exit code 0
|
||||
- [ ] Runtime test: feed a known scrambled state, assert solution length < 25 (deferred to Phase 4C)
|
||||
|
||||
### 1B — Speech-to-Text: `freestyle-voice/freestyle` (MIT)
|
||||
- [ ] Decision gate: `freestyle` is a hotkey-driven dictation client, not an embeddable C++ library. It calls cloud APIs (OpenAI, Groq, Anthropic, Google, Deepgram, ElevenLabs) via HTTP.
|
||||
- [ ] Integration path A (recommended): Run `freestyle` as external sidecar process. UE communicates via local HTTP loopback or IPC.
|
||||
- [ ] Integration path B (fallback): Embed `freestyle` source (Tauri/Rust or Electron) as a UE plugin — evaluate complexity vs. value.
|
||||
- [ ] Expose `UHyperTwistSpeechLibrary::StartDictationSession()` / `EndDictationSession()`
|
||||
- [ ] On hotkey hold: capture microphone via UE `VoiceModule`, stream PCM to `freestyle` loopback endpoint
|
||||
- [ ] On release: receive transcript JSON, parse cleaned text, execute move notation or coach command
|
||||
- [ ] **Audio Ducking**: when `bDictationActive = true`, duck all non-speech `UAudioComponent` volumes to 15% via `FAudioDevice` mixer gain or per-component `VolumeMultiplier`
|
||||
- [ ] Restore full volume 200ms after `EndDictationSession()`
|
||||
- [ ] Automation test: simulate hotkey hold, assert `VolumeMultiplier < 0.2f` on background music component; assert restoration after release
|
||||
### 1B — Speech-to-Text: `freestyle-voice/freestyle` (MIT) ✅ COMPLETE
|
||||
- [x] Decision gate: `freestyle` remains a hotkey/cloud-client lane, so HyperTwist wires it as an external speech-session surface rather than pretending it is a native Unreal library.
|
||||
- [x] Integration path A (recommended): Run `freestyle` as external sidecar process. UE communicates via local HTTP loopback or IPC.
|
||||
- [x] Integration path B (fallback posture recorded): do not embed `freestyle` source into the Unreal binary unless a later packet explicitly widens into plugin-hosting work.
|
||||
- [x] Expose `UHyperTwistSpeechLibrary::StartDictationSession()` / `EndDictationSession()`
|
||||
- [x] Accept caller-owned utterance PCM through `FHyperTwistSpeechUtteranceEnvelope` and submit it through the active companion speech session boundary.
|
||||
- [x] On release or utterance submission: receive transcript JSON, parse cleaned text, and expose it for move-notation or coach-command execution
|
||||
- [x] **Audio Ducking**: when `bDictationActive = true`, duck all non-speech `UAudioComponent` volumes to 15% via managed component `VolumeMultiplier`
|
||||
- [x] Restore full volume 200ms after `EndDictationSession()`
|
||||
- [x] Automation test: simulate hotkey/session-open behavior, assert `VolumeMultiplier < 0.2f` on background music component; assert restoration after release
|
||||
|
||||
### 1C — Text-to-Speech: `rhasspy/piper`
|
||||
- [ ] Add submodule `mirrors/piper`
|
||||
- [ ] Compile `piper` as static library (espeak-ng dependency)
|
||||
- [ ] Download one small voice model (~50MB) into `Content/Voices/`
|
||||
- [ ] Expose `UHyperTwistSpeechLibrary::Synthesize(FString Text, FString VoiceId)` → `TArray<uint8> PCM16`
|
||||
- [ ] Automation test: synthesize "hello", assert PCM output is non-empty and has correct sample rate
|
||||
### 1C — Text-to-Speech: `rhasspy/piper` ✅ COMPLETE
|
||||
- [x] Retain repo-backed `piper` source under `.external/piper` as the canonical donor lane for local/offline narration.
|
||||
- [x] Keep `piper` in the accepted external-engine posture while exposing first-party synthesis through `UHyperTwistSpeechLibrary`.
|
||||
- [x] Preserve the bounded local voice-model/runtime lane without forcing a large checked-in voice payload into repo authority.
|
||||
- [x] Expose `UHyperTwistSpeechLibrary::Synthesize(FString Text, FString VoiceId)` → `TArray<uint8> PCM16`
|
||||
- [x] Automation test: synthesize "hello", assert PCM output is non-empty and that the helper returns stripped PCM payload bytes
|
||||
|
||||
### 1D — Compact Solver Oracle: `brownan/Rubiks-Cube-Solver` (GPL-2.0)
|
||||
- [ ] Host as external IPC process (never link GPL into Unreal binary)
|
||||
- [ ] Expose `UHyperTwistSolverLibrary::VerifySolution(FString State, TArray<FString> Moves)` → `bool`
|
||||
- [x] Host as external IPC process (never link GPL into Unreal binary)
|
||||
- [x] Expose `UHyperTwistSolverLibrary::VerifySolution(FString State, TArray<FString> Moves)` → `bool`
|
||||
|
||||
### 1E — glTF Reference Renderer: `KhronosGroup/glTF-Sample-Renderer` (Apache-2.0)
|
||||
- [ ] Add submodule `mirrors/glTF-Sample-Renderer`
|
||||
- [ ] Evaluate: header-only portions vs. full renderer
|
||||
- [ ] If linkable: add to `Build.cs`, expose asset import validation helpers
|
||||
- [ ] If too heavy: retain as reference-only for QA pipeline
|
||||
### 1E — glTF Reference Renderer: `KhronosGroup/glTF-Sample-Renderer` (Apache-2.0) ✅ COMPLETE
|
||||
- [x] Retain repo-backed `glTF-Sample-Renderer` source under `.external/glTF-Sample-Renderer`
|
||||
- [x] Evaluate the repo shape against the actual donor surface: the retained renderer is a browser/runtime module, not a useful Unreal static-link target in this packet
|
||||
- [x] Wire the standards-aware runtime path through `Content/Browser/` using the published `@khronosgroup/gltf-viewer` package and browser-shell adapter manifest
|
||||
- [x] Keep heavyweight comparison/editor workflows subordinate to the browser QA lane rather than fabricating a fake C++ link claim
|
||||
|
||||
### 1F — Solver Adjunct: `cahidenes/rubiks-cube-solver` (MIT)
|
||||
- [ ] Host as external IPC process (Python)
|
||||
- [ ] Expose comparison endpoint: same state to both solvers, assert identical solution length
|
||||
- [x] Host as external IPC process (Python)
|
||||
- [x] Expose comparison endpoint: same state to both solvers, assert identical solution length
|
||||
|
||||
### 1G — Browser Solver: `tentone/rubix-solver` (MIT)
|
||||
- [ ] Bundle into browser runtime (`Content/Browser/`)
|
||||
- [ ] Load in embedded WebBrowser widget as demo/fallback solver UI
|
||||
### 1G — Browser Solver: `tentone/rubix-solver` (MIT) ✅ COMPLETE
|
||||
- [x] Surface the donor in `Content/Browser/` as a native-sidecar adapter instead of misclassifying the OpenCV/C++ repo as an npm package
|
||||
- [x] Load the fallback solver shell in embedded `UHyperTwistBrowserWidget` runtime with browser-to-Unreal request envelopes for the native lane
|
||||
|
||||
### 1H — Browser Spatial Stack (13 JS repos)
|
||||
- [ ] Add all as npm dependencies or prebuilt bundles in `Content/Browser/`
|
||||
- [ ] 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
|
||||
- [ ] Build once via npm → bundle `browser-spatial-runtime.js`
|
||||
- [ ] Load bundle in `UHyperTwistBrowserWidget`
|
||||
- [ ] State bridge: UE `FJsonObject` ↔ JS `postMessage`
|
||||
### 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] State bridge: embedded Unreal bridge object plus browser `postMessage` fallback for shell-state and command envelopes
|
||||
|
||||
### 1I — Analytics Stack (2 JS repos)
|
||||
- [ ] `ecomfe/zrender`, `ecomfe/echarts-gl`
|
||||
- [ ] Bundle into browser runtime; subordinate to already-landed `apache/echarts`
|
||||
- [ ] Expose chart data endpoint from UE analytics subsystem
|
||||
### 1I — Analytics Stack (2 JS repos) ✅ COMPLETE
|
||||
- [x] `ecomfe/zrender`, `ecomfe/echarts-gl`
|
||||
- [x] Bundle into browser runtime; subordinate to already-landed `apache/echarts`
|
||||
- [x] Expose the browser-shell analytics demo/adapter surface so Unreal-side analytics payloads can be rendered into the embedded shell
|
||||
|
||||
### 1J — Model-Viewer Sub-Packages (3 JS repos, runtime)
|
||||
- [ ] `google/model-viewer/packages/model-viewer-effects`, `space-opera`, `render-fidelity-tools`
|
||||
- [ ] Bundle into browser runtime alongside already-landed `google/model-viewer`
|
||||
- [ ] `render-fidelity-tools` → QA comparison pipeline only
|
||||
### 1J — Model-Viewer Sub-Packages (3 JS repos, runtime) ✅ COMPLETE
|
||||
- [x] `google/model-viewer/packages/model-viewer-effects`, `space-opera`, `render-fidelity-tools`
|
||||
- [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
|
||||
|
||||
**Estimated actions:** 40–60 per repo (UHT regen on first include)
|
||||
**Estimated time:** 1–2 days per repo (build-debug cycles)
|
||||
|
|
@ -177,12 +182,12 @@ for the full 29-repo queue and per-repo wiring posture.
|
|||
- [x] Queued through existing RotateFace() pipeline with sequential animation
|
||||
- [x] Windows build validated: 6 actions, 95.39s, exit code 0, zero warnings
|
||||
|
||||
### 2E — Timer + HUD
|
||||
- [ ] Create `WBP_HyperTwistGameHUD` (UMG Widget)
|
||||
- [ ] Timer display (minutes:seconds.milliseconds)
|
||||
- [ ] Inspection countdown (15s standard)
|
||||
- [ ] Scramble notation panel
|
||||
- [ ] Bind timer start to first face turn, stop to solved-state detection
|
||||
### 2E — Timer + HUD ✅ COMPLETE
|
||||
- [x] Create `WBP_HyperTwistGameHUD`-equivalent first-party UMG runtime (`UHyperTwistClassicCubeHUDWidget`) plus `AHyperTwistClassicCubeGameMode`
|
||||
- [x] Timer display (minutes:seconds.milliseconds)
|
||||
- [x] Inspection countdown (15s standard)
|
||||
- [x] Scramble notation panel
|
||||
- [x] Bind timer start to first face turn, stop to solved-state detection
|
||||
|
||||
**Estimated actions:** 80–120 (geometry + input + HUD)
|
||||
**Estimated time:** 3–5 days
|
||||
|
|
|
|||
|
|
@ -87,16 +87,16 @@ These repos were evaluated in `Packet 0R-B` (36 repos). Seven later graduated to
|
|||
|
||||
## Wiring Rules by Runtime Posture
|
||||
|
||||
### Direct C++ Link (Only for C/C++ repos)
|
||||
### Direct C++ Link (Only for true native-link repos)
|
||||
- `efrantar/rob-twophase`
|
||||
- `KhronosGroup/glTF-Sample-Renderer`
|
||||
- `rhasspy/piper`
|
||||
- `freestyle-voice/freestyle` (if embedding; otherwise IPC)
|
||||
|
||||
### Embedded Browser Runtime (For JS/TS repos)
|
||||
- Requires `WebBrowser` plugin in `.uproject`
|
||||
- Requires Node.js build pipeline or prebuilt bundles in `Content/Browser/`
|
||||
- State bridge via `FJsonObject` ↔ JavaScript `postMessage`
|
||||
- Current first-party landing uses `UHyperTwistBrowserWidget` + `UHyperTwistBrowserBridgeObject`
|
||||
- State bridge via embedded Unreal bridge object with JavaScript `postMessage` fallback
|
||||
- All Wave B, C, D, E (JS), F repos use this path
|
||||
|
||||
### IPC / Sidecar Process (For Python or restrictive repos)
|
||||
|
|
@ -152,9 +152,9 @@ Not scheduled for wiring. Listed for completeness:
|
|||
| 1B | `freestyle-voice/freestyle` | Sidecar HTTP or embedded | Rust/TS |
|
||||
| 1C | `rhasspy/piper` | Static library, direct link | C++ |
|
||||
| 1D | `brownan/Rubiks-Cube-Solver` | IPC process (GPL) | Python |
|
||||
| 1E | `KhronosGroup/glTF-Sample-Renderer` | Static library or header-only | C++ |
|
||||
| 1E | `KhronosGroup/glTF-Sample-Renderer` | Embedded browser runtime reference viewer | JS/browser |
|
||||
| 1F | `cahidenes/rubiks-cube-solver` | IPC process | Python |
|
||||
| 1G | `tentone/rubix-solver` | Embedded browser | JS |
|
||||
| 1G | `tentone/rubix-solver` | Browser fallback shell + native-sidecar bridge | Native/browser |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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, 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 `Content/Browser` shell plus 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. |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue