Fixes in views

This commit is contained in:
Jakub Zych
2026-04-27 14:59:24 +02:00
parent 5a56e2f8e9
commit e961d264c0
5 changed files with 207 additions and 124 deletions

View File

@@ -703,12 +703,12 @@
"id": 13, "id": 13,
"group_item_backup": false, "group_item_backup": false,
"pos": { "pos": {
"x": 0.0, "x": 10.0,
"y": 0.0 "y": 10.0
}, },
"pos_rel": { "pos_rel": {
"x": -1.9161676168441772, "x": -1.9011975526809692,
"y": -1.0 "y": -0.98502993583679199
}, },
"scale": { "scale": {
"x": 1.0, "x": 1.0,
@@ -719,12 +719,12 @@
"y": 1.0 "y": 1.0
}, },
"bounds": { "bounds": {
"x": 2560.0, "x": 1985.0,
"y": 1336.0 "y": 1213.0
}, },
"bounds_rel": { "bounds_rel": {
"x": 3.8323352336883545, "x": 2.9715569019317627,
"y": 2.0 "y": 1.8158682584762573
}, },
"scale_filter": "disable", "scale_filter": "disable",
"blend_method": "default", "blend_method": "default",
@@ -850,7 +850,7 @@
{ {
"name": "Camera", "name": "Camera",
"source_uuid": "a8b362a1-91a6-491b-8751-26e2dad55ccd", "source_uuid": "a8b362a1-91a6-491b-8751-26e2dad55ccd",
"visible": false, "visible": true,
"locked": false, "locked": false,
"rot": 0.0, "rot": 0.0,
"scale_ref": { "scale_ref": {
@@ -868,20 +868,20 @@
"id": 16, "id": 16,
"group_item_backup": false, "group_item_backup": false,
"pos": { "pos": {
"x": 1990.0, "x": 2019.0,
"y": 49.0 "y": 20.0
}, },
"pos_rel": { "pos_rel": {
"x": 1.0628741979598999, "x": 1.1062874794006348,
"y": -0.92664670944213867 "y": -0.97005987167358398
}, },
"scale": { "scale": {
"x": 0.28593748807907104, "x": 0.27239581942558289,
"y": 0.28611111640930176 "y": 0.27222222089767456
}, },
"scale_rel": { "scale_rel": {
"x": 0.23114706575870514, "x": 0.22020021080970764,
"y": 0.23128743469715118 "y": 0.22005987167358398
}, },
"bounds": { "bounds": {
"x": 0.0, "x": 0.0,
@@ -923,20 +923,20 @@
"id": 17, "id": 17,
"group_item_backup": false, "group_item_backup": false,
"pos": { "pos": {
"x": 1984.0, "x": 2007.0,
"y": 392.0 "y": 353.0
}, },
"pos_rel": { "pos_rel": {
"x": 1.0538922548294067, "x": 1.0883233547210693,
"y": -0.41317367553710938 "y": -0.4715569019317627
}, },
"scale": { "scale": {
"x": 1.0364298820495605, "x": 0.99089258909225464,
"y": 1.0363636016845703 "y": 0.99090909957885742
}, },
"scale_rel": { "scale_rel": {
"x": 0.68267840147018433, "x": 0.65268373489379883,
"y": 0.68263471126556396 "y": 0.65269464254379272
}, },
"bounds": { "bounds": {
"x": 0.0, "x": 0.0,
@@ -2602,8 +2602,8 @@
"name": "Good Bye" "name": "Good Bye"
} }
], ],
"current_scene": "Good Bye", "current_scene": "Landing",
"current_program_scene": "Good Bye", "current_program_scene": "Landing",
"canvases": [], "canvases": [],
"current_transition": "Luma Wipe", "current_transition": "Luma Wipe",
"transition_duration": 1000, "transition_duration": 1000,
@@ -2635,7 +2635,7 @@
"saved_projectors": [], "saved_projectors": [],
"preview_locked": false, "preview_locked": false,
"scaling_enabled": false, "scaling_enabled": false,
"scaling_level": -13, "scaling_level": -5,
"scaling_off_x": 0.0, "scaling_off_x": 0.0,
"scaling_off_y": 0.0, "scaling_off_y": 0.0,
"modules": { "modules": {

View File

@@ -26,6 +26,13 @@
--cam-w: 549px; --cam-w: 549px;
--cam-h: 309px; --cam-h: 309px;
--cam-pad: 10px; --cam-pad: 10px;
/* Screen-capture frame transform — auto-synced from the Screen Capture
item the same way. Defaults match the current scene's bounds. */
--scr-x: 10px;
--scr-y: 14px;
--scr-w: 1976px;
--scr-h: 1209px;
--scr-pad: 0px;
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
@@ -92,17 +99,38 @@
0 0 28px rgba(79, 210, 255, 0.20), 0 0 28px rgba(79, 210, 255, 0.20),
inset 0 0 60px rgba(0, 0, 0, 0.55); inset 0 0 60px rgba(0, 0, 0, 0.55);
} }
.camera-frame .tick {
/* ───────── Screen-capture frame ─────────
Same pattern as the camera frame — corner ticks + label + tracked size —
but transparent (the desktop renders behind, must show through) and a
thinner perimeter line, since at full-canvas scale a bright cyan rectangle
all around the desktop reads as too loud. Bright ticks carry the "this is
a framed feed" signal; the line itself is just a hint. */
.screen-frame {
position: absolute;
top: calc(var(--scr-y) - var(--scr-pad));
left: calc(var(--scr-x) - var(--scr-pad));
width: calc(var(--scr-w) + var(--scr-pad) * 2);
height: calc(var(--scr-h) + var(--scr-pad) * 2);
pointer-events: none;
background: transparent;
border: 1px solid rgba(79, 210, 255, 0.30);
box-shadow: 0 0 18px rgba(79, 210, 255, 0.08);
}
.camera-frame .tick,
.screen-frame .tick {
position: absolute; position: absolute;
width: 24px; height: 24px; width: 24px; height: 24px;
border: 3px solid var(--hud-edge); border: 3px solid var(--hud-edge);
box-shadow: 0 0 8px var(--hud-glow); box-shadow: 0 0 8px var(--hud-glow);
} }
.camera-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; } .camera-frame .tick.tl, .screen-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
.camera-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; } .camera-frame .tick.tr, .screen-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
.camera-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; } .camera-frame .tick.bl, .screen-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
.camera-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; } .camera-frame .tick.br, .screen-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
.camera-frame .label { .camera-frame .label,
.screen-frame .label {
position: absolute; position: absolute;
top: 10px; left: 14px; top: 10px; left: 14px;
font-family: var(--mono); font-family: var(--mono);
@@ -133,7 +161,8 @@
letter-spacing: 0.20em; letter-spacing: 0.20em;
color: var(--hud-dim); color: var(--hud-dim);
} }
.camera-frame.off { display: none; } .camera-frame.off,
.screen-frame.off { display: none; }
/* ───────── Bottom status bar ───────── */ /* ───────── Bottom status bar ───────── */
.status-bar { .status-bar {
@@ -203,6 +232,14 @@
</div> </div>
</header> </header>
<div class="screen-frame" id="scr">
<span class="tick tl"></span>
<span class="tick tr"></span>
<span class="tick bl"></span>
<span class="tick br"></span>
<span class="label">SCR 01</span>
</div>
<div class="camera-frame" id="cam"> <div class="camera-frame" id="cam">
<span class="tick tl"></span> <span class="tick tl"></span>
<span class="tick tr"></span> <span class="tick tr"></span>
@@ -273,53 +310,68 @@
} }
tickElapsed(); setInterval(tickElapsed, 1000); tickElapsed(); setInterval(tickElapsed, 1000);
// ── OBS WS: live camera frame sync ── // ── OBS WS: live frame sync for camera + screen capture ──
// URL params let you rename the camera source / scene without editing JS: // URL params let you rename the sources / scene without editing JS:
// ?camera=Webcam&scene=Desktop // ?camera=Webcam&screen=Display&scene=Desktop
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const CAM_SOURCE = params.get('camera') || 'Camera'; const CAM_SOURCE = params.get('camera') || 'Camera';
const CAM_SCENE = params.get('scene') || 'Desktop'; const SCR_SOURCE = params.get('screen') || 'Screen Capture (PipeWire)';
const SCENE = params.get('scene') || 'Desktop';
const camEl = document.getElementById('cam'); const camEl = document.getElementById('cam');
const scrEl = document.getElementById('scr');
const rootStyle = document.documentElement.style; const rootStyle = document.documentElement.style;
function setCameraVisible(visible) { camEl.classList.toggle('off', !visible); }
// Map an OBS sceneItemTransform into the four CSS vars driving the frame. // Compute rendered (w, h) on canvas from an OBS sceneItemTransform.
// Assumes alignment 5 (top-left) — OBS's default for newly added items. // Camera item has no bounds → scale × source. Screen capture uses
function setCameraTransform(t) { // bounds_type=stretch (1) → boundsWidth/boundsHeight are authoritative.
if (!t) return; function renderedSize(t) {
const w = (t.sourceWidth ?? 0) * (t.scaleX ?? 1); const useBounds = t.boundsType
const h = (t.sourceHeight ?? 0) * (t.scaleY ?? 1); && t.boundsType !== 'OBS_BOUNDS_NONE'
if (!(w > 0 && h > 0)) return; && (t.boundsWidth ?? 0) > 0;
rootStyle.setProperty('--cam-x', `${t.positionX}px`); if (useBounds) return { w: t.boundsWidth, h: t.boundsHeight };
rootStyle.setProperty('--cam-y', `${t.positionY}px`); return {
rootStyle.setProperty('--cam-w', `${w}px`); w: (t.sourceWidth ?? 0) * (t.scaleX ?? 1),
rootStyle.setProperty('--cam-h', `${h}px`); h: (t.sourceHeight ?? 0) * (t.scaleY ?? 1),
};
} }
async function syncCamera(obs) { // Bind a frame element to a scene item: live-sync size/position into the
let camItemId = null; // --{prefix}-x/y/w/h CSS vars and toggle .off on enable changes.
// Assumes alignment 5 (top-left) — OBS's default for newly added items.
async function syncSourceFrame(obs, { sceneName, sourceName, frameEl, varPrefix }) {
const setVisible = (v) => frameEl.classList.toggle('off', !v);
const setTransform = (t) => {
if (!t) return;
const { w, h } = renderedSize(t);
if (!(w > 0 && h > 0)) return;
rootStyle.setProperty(`--${varPrefix}-x`, `${t.positionX}px`);
rootStyle.setProperty(`--${varPrefix}-y`, `${t.positionY}px`);
rootStyle.setProperty(`--${varPrefix}-w`, `${w}px`);
rootStyle.setProperty(`--${varPrefix}-h`, `${h}px`);
};
let itemId = null;
try { try {
const r = await obs.call('GetSceneItemId', const r = await obs.call('GetSceneItemId', { sceneName, sourceName });
{ sceneName: CAM_SCENE, sourceName: CAM_SOURCE }); itemId = r.sceneItemId;
camItemId = r.sceneItemId;
const e = await obs.call('GetSceneItemEnabled', const e = await obs.call('GetSceneItemEnabled',
{ sceneName: CAM_SCENE, sceneItemId: camItemId }); { sceneName, sceneItemId: itemId });
setCameraVisible(e.sceneItemEnabled); setVisible(e.sceneItemEnabled);
const t = await obs.call('GetSceneItemTransform', const t = await obs.call('GetSceneItemTransform',
{ sceneName: CAM_SCENE, sceneItemId: camItemId }); { sceneName, sceneItemId: itemId });
setCameraTransform(t.sceneItemTransform); setTransform(t.sceneItemTransform);
} catch (err) { } catch (err) {
console.warn(`[desktop] camera sync init failed (${CAM_SCENE}/${CAM_SOURCE}):`, err.message); console.warn(`[desktop] frame sync failed (${sceneName}/${sourceName}):`, err.message);
return; return;
} }
obs.addEventListener('event', (ev) => { obs.addEventListener('event', (ev) => {
const d = ev.detail || {}; const d = ev.detail || {};
if (d.eventData?.sceneName !== CAM_SCENE if (d.eventData?.sceneName !== sceneName
|| d.eventData?.sceneItemId !== camItemId) return; || d.eventData?.sceneItemId !== itemId) return;
if (d.eventType === 'SceneItemEnableStateChanged') { if (d.eventType === 'SceneItemEnableStateChanged') {
setCameraVisible(d.eventData.sceneItemEnabled); setVisible(d.eventData.sceneItemEnabled);
} else if (d.eventType === 'SceneItemTransformChanged') { } else if (d.eventType === 'SceneItemTransformChanged') {
setCameraTransform(d.eventData.sceneItemTransform); setTransform(d.eventData.sceneItemTransform);
} }
}); });
} }
@@ -332,7 +384,11 @@
obs.addEventListener('close', () => { obs.addEventListener('close', () => {
setTimeout(connectOBS, 2500); setTimeout(connectOBS, 2500);
}); });
await syncCamera(obs); // Run both syncs in parallel — independent items, independent failures.
await Promise.all([
syncSourceFrame(obs, { sceneName: SCENE, sourceName: CAM_SOURCE, frameEl: camEl, varPrefix: 'cam' }),
syncSourceFrame(obs, { sceneName: SCENE, sourceName: SCR_SOURCE, frameEl: scrEl, varPrefix: 'scr' }),
]);
} catch (e) { } catch (e) {
// OBS WS optional — silently retry; defaults stand meanwhile. // OBS WS optional — silently retry; defaults stand meanwhile.
setTimeout(connectOBS, 2500); setTimeout(connectOBS, 2500);

View File

@@ -171,7 +171,12 @@ body {
letter-spacing: 0.12em; letter-spacing: 0.12em;
} }
.foot .warn { color: var(--warn); } .foot .warn { color: var(--warn); }
#rig { transition: opacity 0.4s ease; } /* Mirrors .station .rig.glitching — bottom telemetry uses the same scramble
reveal as the top rig identifier, so it shares the warn-yellow tint. */
#rig.glitching {
color: var(--warn);
text-shadow: 0 0 10px rgba(255, 208, 0, 0.4);
}
/* ───────── Overlays ───────── */ /* ───────── Overlays ───────── */
#static { #static {
@@ -323,55 +328,70 @@ body {
// 8 fps — visually almost identical to 12 fps but ~33% less paint work. // 8 fps — visually almost identical to 12 fps but ~33% less paint work.
setInterval(drawNoise, 1000 / 8); setInterval(drawNoise, 1000 / 8);
// ───────── Rig name in the top HUD with periodic glitch-decode ───────── // ───────── Cyberpunk decode/scramble reveal ─────────
// Reads telemetry.js's window.__TEL.rig (set by telemetry.sh per-rig). // Each call: replace target text with random glyphs for `scrambleMs`, then
// Behaviour: settles on the real name, then every ~9s briefly scrambles // resolve to `finalText` left-to-right. Same effect family as the cyberpunk
// each letter through random glyphs and resolves back left-to-right. Same // "decoder" trope; pairs with the CRT aesthetic rather than fighting it.
// effect family as the cyberpunk "decoder" trope; pairs with the CRT // Used by both the top rig identifier and the bottom telemetry rotator.
// aesthetic rather than fighting it. // ASCII-only glyph pool — DejaVu Mono renders these at 1ch each, so the
const rigNameEl = document.getElementById('rigName'); // intermediate frames don't shift width.
const RIG_NAME = (window.__TEL && window.__TEL.rig) || 'UNKNOWN';
// ASCII-only glyph pool — tabular_nums keeps width stable in DejaVu Mono.
const GLITCH_GLYPHS = '01ABCDEFGHIJKLMNOPQRSTUVWXYZ#@*?!§%'; const GLITCH_GLYPHS = '01ABCDEFGHIJKLMNOPQRSTUVWXYZ#@*?!§%';
const glyph = () => GLITCH_GLYPHS[(Math.random() * GLITCH_GLYPHS.length) | 0]; const glyph = () => GLITCH_GLYPHS[(Math.random() * GLITCH_GLYPHS.length) | 0];
const upperRig = RIG_NAME.toUpperCase(); // Tracks the in-flight scramble per element so a new call can cancel the
// previous one (otherwise back-to-back triggers double up timers).
// Initial reveal: scramble for ~400ms, then resolve l→r. const _scrambleHandles = new WeakMap();
// Subsequent: small idle scramble ~every 9s. function scrambleReveal(el, finalText, opts = {}) {
function scrambleOnce({ initial = false } = {}) { const { scrambleMs = 360, stepMs = 50, resolveStepMs = 60 } = opts;
const len = upperRig.length; const prev = _scrambleHandles.get(el);
if (len === 0) { rigNameEl.textContent = '--'; return; } if (prev) { clearInterval(prev.s); clearInterval(prev.r); }
rigNameEl.classList.add('glitching'); const len = finalText.length;
const totalScrambleMs = initial ? 600 : 360; if (!len) {
const stepMs = 50; el.textContent = '';
const scrambleSteps = Math.max(1, Math.floor(totalScrambleMs / stepMs)); el.classList.remove('glitching');
_scrambleHandles.delete(el);
return;
}
el.classList.add('glitching');
const scrambleSteps = Math.max(1, Math.floor(scrambleMs / stepMs));
const handle = { s: null, r: null };
_scrambleHandles.set(el, handle);
let step = 0; let step = 0;
handle.s = setInterval(() => {
const scrambleTimer = setInterval(() => {
let s = ''; let s = '';
for (let i = 0; i < len; i++) s += glyph(); for (let i = 0; i < len; i++) s += glyph();
rigNameEl.textContent = s; el.textContent = s;
if (++step >= scrambleSteps) { if (++step < scrambleSteps) return;
clearInterval(scrambleTimer); clearInterval(handle.s); handle.s = null;
// Resolve left-to-right let resolved = 0;
let resolved = 0; handle.r = setInterval(() => {
const resolveTimer = setInterval(() => { resolved++;
resolved++; let s = finalText.slice(0, resolved);
let s = upperRig.slice(0, resolved); for (let i = resolved; i < len; i++) s += glyph();
for (let i = resolved; i < len; i++) s += glyph(); el.textContent = s;
rigNameEl.textContent = s; if (resolved < len) return;
if (resolved >= len) { clearInterval(handle.r); handle.r = null;
clearInterval(resolveTimer); el.textContent = finalText;
rigNameEl.textContent = upperRig; el.classList.remove('glitching');
rigNameEl.classList.remove('glitching'); _scrambleHandles.delete(el);
} }, resolveStepMs);
}, 60);
}
}, stepMs); }, stepMs);
} }
// ───────── Rig name in the top HUD ─────────
// Reads telemetry.js's window.__TEL.rig (set by telemetry.sh per-rig).
// Settles on the real name, then idle-reglitches every ~9s.
const rigNameEl = document.getElementById('rigName');
const RIG_NAME = (window.__TEL && window.__TEL.rig) || 'UNKNOWN';
const upperRig = RIG_NAME.toUpperCase();
const scrambleRig = ({ initial = false } = {}) => {
if (!upperRig.length) { rigNameEl.textContent = '--'; return; }
scrambleReveal(rigNameEl, upperRig, {
scrambleMs: initial ? 600 : 360, stepMs: 50, resolveStepMs: 60,
});
};
// First reveal happens shortly after page load; then idle re-glitch. // First reveal happens shortly after page load; then idle re-glitch.
setTimeout(() => scrambleOnce({ initial: true }), 600); setTimeout(() => scrambleRig({ initial: true }), 600);
setInterval(scrambleOnce, 9000); setInterval(scrambleRig, 9000);
// ───────── Telemetry: rotating real-data subtitle ───────── // ───────── Telemetry: rotating real-data subtitle ─────────
// All lines derive from telemetry.js (window.__TEL), generated by telemetry.sh. // All lines derive from telemetry.js (window.__TEL), generated by telemetry.sh.
@@ -432,24 +452,31 @@ body {
]; ];
let lineIdx = 0; let lineIdx = 0;
const renderLine = () => { // Pick the next non-null line, advancing lineIdx past dead slots.
const nextLine = () => {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const v = lines[lineIdx](); const v = lines[lineIdx]();
if (v) { rigEl.textContent = v; return; } if (v) return v;
lineIdx = (lineIdx + 1) % lines.length; lineIdx = (lineIdx + 1) % lines.length;
} }
rigEl.textContent = '— TELEMETRY OFFLINE —'; return '— TELEMETRY OFFLINE —';
}; };
// Soft crossfade: 0.4s out → swap text → 0.4s in (matches the CSS transition) // Initial paint goes in directly so the page loads with content visible;
const FADE_MS = 400; // every subsequent transition uses the scramble reveal (same effect as the
// top rig identifier).
rigEl.textContent = nextLine();
// Bottom lines are much longer than the rig name (~2255 chars vs ~58),
// so adapt the per-char resolve step to keep total reveal under ~700ms.
const swapLine = () => { const swapLine = () => {
rigEl.style.opacity = '0'; const text = nextLine();
setTimeout(() => { renderLine(); rigEl.style.opacity = '1'; }, FADE_MS); const len = Math.max(text.length, 1);
scrambleReveal(rigEl, text, {
scrambleMs: 280,
stepMs: 45,
resolveStepMs: Math.max(15, Math.floor(700 / len)),
});
}; };
renderLine();
rigEl.style.opacity = '1';
setInterval(() => { setInterval(() => {
lineIdx = (lineIdx + 1) % lines.length; lineIdx = (lineIdx + 1) % lines.length;
swapLine(); swapLine();

View File

@@ -2,14 +2,14 @@
"collectedAt": "2026-04-26T20:17:38Z", "collectedAt": "2026-04-26T20:17:38Z",
"rig": "Ignia", "rig": "Ignia",
"cpu": { "cpu": {
"model": "AMD Ryzen 5 2600 Six-Core Processor", "model": "AMD Ryzen 5",
"threads": 12 "threads": 12
}, },
"mem": { "mem": {
"totalGB": 31.3 "totalGB": 32
}, },
"host": { "host": {
"kernel": "6.18.23-1-lts" "kernel": "Arch Linux 6.18.23-1-lts"
}, },
"gpu": { "gpu": {
"name": "NVIDIA GeForce RTX 3060 Ti", "name": "NVIDIA GeForce RTX 3060 Ti",

View File

@@ -25,8 +25,8 @@ cx=720
cy=580 cy=580
[BasicWindow] [BasicWindow]
geometry=AdnQywADAAAAAAoAAAAAAAAAEX8AAAQDAAAAAAAAAAAAAAO/AAAEAwAAAAACAAAAB4AAAAoAAAAAAAAAEX8AAAQD geometry=AdnQywADAAAAAAAAAAAAAAAACf8AAAVDAAAKAQAAABkAAA3AAAAEHAAAAAECAAAACgAAAAAAAAAAAAAACf8AAAVD
DockState=AAAA/wAAAAD9AAAAAQAAAAMAAAeAAAABKPwBAAAABvsAAAAUAHMAYwBlAG4AZQBzAEQAbwBjAGsBAAAAAAAAAWEAAACYAP////sAAAAWAHMAbwB1AHIAYwBlAHMARABvAGMAawEAAAFlAAABXwAAAJgA////+wAAABIAbQBpAHgAZQByAEQAbwBjAGsBAAACyAAAAgoAAAECAP////sAAAAeAHQAcgBhAG4AcwBpAHQAaQBvAG4AcwBEAG8AYwBrAQAABNYAAAFdAAAArQD////7AAAAGABjAG8AbgB0AHIAbwBsAHMARABvAGMAawEAAAY3AAABSQAAAKIA////+wAAABIAcwB0AGEAdABzAEQAbwBjAGsCAAAExAAAAXAAAAK8AAAAyAAAB4AAAAKcAAAABAAAAAQAAAAIAAAACPwAAAAA DockState=AAAA/wAAAAD9AAAAAQAAAAMAAAoAAAABKPwBAAAABvsAAAAUAHMAYwBlAG4AZQBzAEQAbwBjAGsBAAAAAAAAAdgAAACYAP////sAAAAWAHMAbwB1AHIAYwBlAHMARABvAGMAawEAAAHcAAAB1QAAAJgA////+wAAABIAbQBpAHgAZQByAEQAbwBjAGsBAAADtQAAAroAAAECAP////sAAAAeAHQAcgBhAG4AcwBpAHQAaQBvAG4AcwBEAG8AYwBrAQAABnMAAAHSAAAArQD////7AAAAGABjAG8AbgB0AHIAbwBsAHMARABvAGMAawEAAAhJAAABtwAAAKIA////+wAAABIAcwB0AGEAdABzAEQAbwBjAGsCAAAExAAAAXAAAAK8AAAAyAAACgAAAAPcAAAABAAAAAQAAAAIAAAACPwAAAAA
PreviewEnabled=true PreviewEnabled=true
AlwaysOnTop=false AlwaysOnTop=false
SceneDuplicationMode=true SceneDuplicationMode=true