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 = ''; };