diff --git a/pcd_viewer.html b/pcd_viewer.html
index e24265d..8b78e02 100644
--- a/pcd_viewer.html
+++ b/pcd_viewer.html
@@ -73,9 +73,9 @@ try {
display:flex; align-items:center; justify-content:space-between; padding-left:16px; padding-right:16px; gap:16px;
}
#rail .mark{ display:flex; align-items:baseline; gap:11px; min-width:0; }
- /* drawer toggle — only shown under the small-screen media query below */
+ /* panel show/hide — usable at any viewport width, not just small screens */
#panelToggle{
- display:none; flex:none; background:transparent; color:var(--txt); border:1px solid var(--line-strong);
+ flex:none; background:transparent; color:var(--txt); border:1px solid var(--line-strong);
padding:5px 10px; font:inherit; font-size:9.5px; letter-spacing:0.12em; cursor:pointer;
}
#panelToggle:hover{ background:var(--hover-strong); }
@@ -110,7 +110,9 @@ try {
position:fixed; top:var(--rail-h); left:0; bottom:0; z-index:10; width:284px;
background:var(--panel); border-right:1px solid var(--line);
display:flex; flex-direction:column; overflow-y:auto;
+ transform:translateX(0); transition:transform .18s ease;
}
+ #panel.panel-hidden{ transform:translateX(-100%); }
.sect{ padding:16px 18px; border-bottom:1px solid var(--line); display:flex; flex-direction:column; gap:12px; }
.sect:last-child{ border-bottom:none; }
.lab{ font-size:9.5px; letter-spacing:0.18em; color:var(--faint); }
@@ -240,7 +242,9 @@ try {
#loading.empty .load-live{ display:none; }
#loading.empty .prompt{ display:flex; pointer-events:auto; }
/* the "no cloud yet" state must not block the dashboard — you can go live without a file */
- #loading.empty{ top:var(--rail-h); left:284px; background:transparent; pointer-events:none; }
+ #loading.empty{ top:var(--rail-h); left:284px; background:transparent; pointer-events:none;
+ transition:left .18s ease; }
+ body:has(#panel.panel-hidden) #loading.empty{ left:0; }
#loading .glyph{ font-size:10px; color:var(--faint); letter-spacing:0.5em; text-indent:0.5em; }
/* ── drag & drop overlay ── */
@@ -252,16 +256,11 @@ try {
#drop .frame .s{ display:block; margin-top:9px; font-size:10px; color:var(--faint); letter-spacing:0.24em; }
/* ── small screens (phones; tablets stay on the fixed-sidebar layout above) —
- the instrument column becomes a drawer instead of eating a third of the
- width, since it's a fixed 284px regardless of viewport size ── */
+ the instrument column defaults to hidden (see boot code) and dims the
+ viewport behind it while open, since it now overlays a much bigger
+ fraction of a narrow screen than it does on desktop ── */
@media (max-width:768px){
- #panelToggle{ display:block; }
- #panel{
- width:min(284px, 84vw);
- transform:translateX(-100%);
- transition:transform .18s ease;
- }
- #panel.open{ transform:translateX(0); }
+ #panel{ width:min(284px, 84vw); }
#panelBackdrop{
display:block; position:fixed; top:var(--rail-h); left:0; right:0; bottom:0; z-index:9;
background:var(--overlay); opacity:0; pointer-events:none; transition:opacity .18s ease;
@@ -553,6 +552,10 @@ const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.zoomToCursor = true; // zoom toward the cursor, not the orbit target — matches CAD/CloudCompare
+// two-finger touch: pinch still zooms, but the drag also free-rotates (any direction the
+// fingers move, not locked to one axis) instead of panning — trades away touch-panning,
+// which double-click-to-recenter (below) mostly covers instead.
+controls.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
// ─── on-demand rendering (perf: idle frames cost nothing) ────────────────
let needsRender = true;
@@ -617,16 +620,20 @@ function applyTheme(name) {
}
themeButtons.forEach(b => b.onclick = () => applyTheme(b.dataset.theme));
-// ─── small-screen drawer: the instrument column overlays instead of docking ──
+// ─── panel show/hide — a docked column on wide screens, an overlay drawer on
+// narrow ones (see the max-width:768px rules above); same toggle either way.
+// Canvas already spans the full window behind it, so hiding it needs no resize. ──
const panelEl = $('panel'), panelToggleEl = $('panelToggle'), panelBackdropEl = $('panelBackdrop');
+const panelNarrowMQ = matchMedia('(max-width:768px)');
function setPanelOpen(open) {
- panelEl.classList.toggle('open', open);
+ panelEl.classList.toggle('panel-hidden', !open);
panelBackdropEl.classList.toggle('show', open);
panelToggleEl.setAttribute('aria-expanded', String(open));
}
-panelToggleEl.onclick = () => setPanelOpen(!panelEl.classList.contains('open'));
+panelToggleEl.onclick = () => setPanelOpen(panelEl.classList.contains('panel-hidden'));
panelBackdropEl.onclick = () => setPanelOpen(false);
-matchMedia('(max-width:768px)').addEventListener('change', e => { if (!e.matches) setPanelOpen(false); });
+panelNarrowMQ.addEventListener('change', e => setPanelOpen(!e.matches));
+setPanelOpen(!panelNarrowMQ.matches); // open on desktop/tablet, closed on phones, by default
// segmented mode control
document.querySelectorAll('#modeSeg button').forEach(b => {
@@ -1189,8 +1196,23 @@ function onError(err, isDefault, name) {
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
// file input + drag & drop
+// Android's WebView runs the page in a separate renderer process with its own
+// memory ceiling (independent of the app's Java heap) — a multi-GB .pcd blows
+// through it and the renderer dies, taking the whole app down with no JS
+// exception to catch. 300MB is an empirical line (233MB survived, 1.35GB
+// didn't test on a Galaxy Z Fold) — adjust if it proves too strict or too loose.
+const IS_ANDROID = /Android/i.test(navigator.userAgent);
+const MOBILE_MAX_FILE_MB = 300;
+
function openFile(file) {
if (!/\.pcd$/i.test(file.name)) { onError(new Error('.pcd 파일이 아닙니다'), false, file.name); return; }
+ if (IS_ANDROID && file.size > MOBILE_MAX_FILE_MB * 1024 * 1024) {
+ onError(new Error(
+ `${(file.size / 1024 / 1024).toFixed(0)}MB — 모바일 WebView 렌더러 메모리 한계(약 ${MOBILE_MAX_FILE_MB}MB)를 ` +
+ `넘습니다. 이대로 열면 앱이 강제 종료됩니다. 더 작은/다운샘플된 파일을 쓰거나 데스크톱에서 여세요.`
+ ), false, file.name);
+ return;
+ }
loadPCD(URL.createObjectURL(file), file.name, { revoke: true });
}
fileInput.onchange = e => { if (e.target.files[0]) openFile(e.target.files[0]); e.target.value = ''; };