5 Commits

Author SHA1 Message Date
Dongubak daa2257865 Z-up orbit default and three-finger touch pan
camera.up defaulted to three.js's Y-up while the actual data (ROS/FAST-
LIVO2) and WALK mode are Z-up. Orbiting with the pole misaligned from the
cloud's real vertical made yaw look like a diagonal tumble instead of a
clean spin around what's visually "up" in the rendered cloud. Set Z-up at
camera creation and on WALK exit (previously reverted to Y-up).

OrbitControls only tracks 1-2 simultaneous touches — a 3rd pointer makes
it go idle rather than doing anything. Added a three-finger pan on top,
replicating OrbitControls' own (private, unexported) screen-space pan math
so it feels identical to the existing right-drag/two-finger-pan.
2026-08-23 15:40:21 +09:00
Dongubak 0acddf3f5e Mobile polish: panel toggle everywhere, file-size guard, free two-finger rotate
- The MENU toggle now works at any viewport width, not just small screens —
  desktop/tablet default to the panel open, phones default to closed, and
  crossing the 768px breakpoint resets to that side's default. The canvas
  already spans the full window behind the panel, so hiding it needs no
  resize. The empty-state "open a file" prompt re-centers when the panel
  is hidden.

- Android's WebView renders the page in a separate process with its own
  memory ceiling, independent of the app's Java heap (android:largeHeap
  wouldn't help). A 1.35GB .pcd blew through it and crashed the renderer,
  taking the whole app down with no catchable JS exception — confirmed via
  a device tombstone: "Render process crash wasn't handled by all
  associated webviews, triggering application crash." openFile() now
  blocks anything over 300MB on Android (empirical: 233MB survived, 1.35GB
  didn't) with a clear in-app message instead of a silent kill. Desktop is
  unaffected — it has far more headroom.

- Two-finger touch now free-rotates (any drag direction, not locked to one
  axis) while pinch still zooms (OrbitControls' DOLLY_ROTATE instead of the
  default DOLLY_PAN). Trades away two-finger panning, which double-click
  to recenter mostly substitutes for.

Verified on a physical Galaxy Z Fold.
2026-08-23 15:32:30 +09:00
Dongubak 2f570681eb Fix rail rendering under the Android status bar
targetSdk 36 (Android 15+) forces edge-to-edge, so the fixed-position
top rail was drawing behind the status bar clock/battery icons on a
real device (title and DARK/LIGHT toggle both obscured) — invisible on
desktop since there's no inset there. Add env(safe-area-inset-top) via
a --rail-h variable used everywhere the rail's height was hardcoded as
42px, plus viewport-fit=cover so the inset actually gets populated.

Verified on a physical Galaxy Z Fold over adb.
2026-08-23 15:11:05 +09:00
Dongubak 31cfed7023 Document Android build steps
Covers the rustup requirement (Homebrew rust can't cross-compile to
Android), env vars needed for cargo tauri android build, install via
adb, and the cleartext-traffic gotcha for release builds against ws://
rosbridge endpoints.
2026-08-23 15:03:20 +09:00
Dongubak 8730d5cbaf Add Android build target and responsive/drawer UI for phones and tablets
Restructure src-tauri into lib+bin (Tauri 2's mobile requirement — Android
embeds the app as a JNI cdylib, which needs a [lib] target with
tauri::mobile_entry_point) and scaffold gen/android/ via `cargo tauri
android init`. Builds and packages cleanly to a debug APK/AAB.

pcd_viewer.html: the 284px docked sidebar becomes a slide-in drawer under
768px viewport width (toggled from the rail, closes on backdrop tap), the
rail sheds its subtitle and secondary telemetry chips under 480px, and the
camera panel caps its width to the viewport. Also disables native pinch-zoom
(the canvas has its own via OrbitControls) and sets overscroll-behavior:none
for a full-bleed touch surface.
2026-08-23 15:02:40 +09:00
47 changed files with 5725 additions and 11 deletions
+62 -1
View File
@@ -1,4 +1,4 @@
# PCD Viewer — macOS 네이티브 빌드 # PCD Viewer — 네이티브 빌드 (macOS / Android)
FAST-LIVO2용 포인트 클라우드 뷰어(`pcd_viewer.html`, Three.js)를 Tauri 2로 감싼 macOS 앱. FAST-LIVO2용 포인트 클라우드 뷰어(`pcd_viewer.html`, Three.js)를 Tauri 2로 감싼 macOS 앱.
빌드 없는 단일 HTML이 웹의 단일 소스이고, `src-tauri/`가 그걸 그대로 실어 네이티브 창으로 띄운다. 빌드 없는 단일 HTML이 웹의 단일 소스이고, `src-tauri/`가 그걸 그대로 실어 네이티브 창으로 띄운다.
@@ -44,6 +44,67 @@ open "target/release/bundle/macos/PCD Viewer.app"
`web/`은 빌드 시점에 바이너리로 임베드되므로, `pcd_viewer.html`만 고쳐서는 네이티브 앱에 반영되지 `web/`은 빌드 시점에 바이너리로 임베드되므로, `pcd_viewer.html`만 고쳐서는 네이티브 앱에 반영되지
않는다. 위 빌드 절차(`stage-web.sh``cargo build --release` → 바이너리 동기화)를 다시 밟아야 한다. 않는다. 위 빌드 절차(`stage-web.sh``cargo build --release` → 바이너리 동기화)를 다시 밟아야 한다.
## Android 빌드
같은 `pcd_viewer.html`을 Android WebView로 감싼다(React Native/Expo 대신 — WebView도 진짜
하드웨어 가속 WebGL을 쓰므로 렌더링 경로 자체는 동일하고, 코드베이스를 두 벌로 안 나눠도 된다).
`src-tauri/gen/android/``cargo tauri android init`이 생성한 Android Studio 프로젝트다.
### 준비물
- Rust는 **rustup으로 설치되어 있어야 함** — Homebrew의 `rust` 패키지(고정 타깃 하나만 빌드)로는
`aarch64-linux-android` 같은 크로스 타깃을 추가할 수 없다. `brew install rustup`(또는 공식
설치 스크립트) 후:
```sh
rustup target add aarch64-linux-android armv7-linux-androideabi \
i686-linux-android x86_64-linux-android
```
- Android SDK + NDK (`sdkmanager`로 platform-tools, platform, NDK 설치, 라이선스 동의까지)
- JDK 17+ (Temurin 21 확인됨)
- `cargo install tauri-cli --version "^2" --locked`
macOS에 Homebrew rust와 rustup이 공존하면 `cargo`/`rustc`가 PATH상 Homebrew 쪽으로 잡힐 수 있다
(데스크톱 빌드는 그대로 Homebrew rust를 쓰도록 건드리지 않았다) — Android 명령은 rustup 툴체인의
`cargo`를 명시적으로 가리켜서 실행한다:
```sh
export JAVA_HOME=/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
export ANDROID_HOME=~/Library/Android/sdk
export NDK_HOME=~/Library/Android/sdk/ndk/<설치된 버전>
export PATH="$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin:$HOME/.cargo/bin:$PATH"
```
### 빌드
```sh
cd src-tauri
sh stage-web.sh
cargo tauri android build --debug --target aarch64 # 갤럭시 대부분은 arm64-v8a
```
산출물:
- APK: `gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk`
- AAB: `gen/android/app/build/outputs/bundle/universalDebug/app-universal-debug.aab`
### 기기에 설치
```sh
adb install "gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk"
```
(갤럭시에서 설정 → 휴대전화 정보 → 빌드 번호 7번 탭으로 개발자 옵션 열고, USB 디버깅 켠 뒤
USB로 연결 — `adb devices`에 기기가 떠야 설치된다.) USB 없이는 APK 파일을 기기로 옮겨 직접
설치(출처를 알 수 없는 앱 허용 필요)해도 된다.
### 알아둘 점
- `usesCleartextTraffic`이 디버그 빌드에선 자동으로 켜지지만 **release 빌드에선 꺼진다** —
rosbridge 주소가 `ws://`(평문)라면 release APK에서 연결이 막힌다. `wss://`로 옮기거나
`gen/android/app/src/main/AndroidManifest.xml`에서 명시적으로 허용해야 한다.
- 아이콘은 기본 Tauri 플레이스홀더다. `cargo tauri icon <source.png>`로 교체 가능.
- `gen/android/`는 커밋되어 있지만 `build/`·`.gradle/`·생성된 Kotlin 소스·`jniLibs/*.so`는
중첩 `.gitignore`로 제외된다 — 클론 후 첫 빌드에서 다시 만들어진다.
## 그 밖의 문서 ## 그 밖의 문서
- `PCD_Viewer_가이드.md` — 사용법(측정/실시간 모니터링). 아직 vendoring·Tauri 반영 전 버전. - `PCD_Viewer_가이드.md` — 사용법(측정/실시간 모니터링). 아직 vendoring·Tauri 반영 전 버전.
+132 -7
View File
@@ -2,7 +2,10 @@
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<!-- user-scalable=no: the canvas has its own pinch-to-zoom (OrbitControls) —
letting the OS page-zoom fight it over the same two-finger gesture is worse
than losing native page zoom, which this app has no text layout to need anyway. -->
<title>PCD Viewer — 포인트 클라우드 계측</title> <title>PCD Viewer — 포인트 클라우드 계측</title>
<script> <script>
// Applied synchronously, before first paint, so a stored "light" preference // Applied synchronously, before first paint, so a stored "light" preference
@@ -19,6 +22,9 @@ try {
Hierarchy comes from hairlines, scale and space alone. Hierarchy comes from hairlines, scale and space alone.
The only color in the app is marker identity (viewport + its legend). ── */ The only color in the app is marker identity (viewport + its legend). ── */
:root{ :root{
/* rail height + the device's own status-bar/notch inset (0 on desktop) —
every fixed element anchored below the rail must offset by this, not 42px */
--rail-h:calc(42px + env(safe-area-inset-top));
--bg:#0a0b0c; /* viewport ground, neutral near-black */ --bg:#0a0b0c; /* viewport ground, neutral near-black */
--panel:#0c0d0f; --panel:#0c0d0f;
--line:rgba(255,255,255,0.10); --line:rgba(255,255,255,0.10);
@@ -53,7 +59,7 @@ try {
--measure:#17181a; --measure:#17181a;
} }
*{ box-sizing:border-box; } *{ box-sizing:border-box; }
html,body{ margin:0; height:100%; overflow:hidden; background:var(--bg); color:var(--txt); html,body{ margin:0; height:100%; overflow:hidden; overscroll-behavior:none; background:var(--bg); color:var(--txt);
font-family:var(--mono); -webkit-font-smoothing:antialiased; } font-family:var(--mono); -webkit-font-smoothing:antialiased; }
#app{ position:fixed; inset:0; } #app{ position:fixed; inset:0; }
canvas{ display:block; } canvas{ display:block; }
@@ -61,11 +67,19 @@ try {
/* ── Top rail: identity, session telemetry ── */ /* ── Top rail: identity, session telemetry ── */
#rail{ #rail{
position:fixed; top:0; left:0; right:0; height:42px; z-index:11; position:fixed; top:0; left:0; right:0; height:var(--rail-h); z-index:11;
padding-top:env(safe-area-inset-top); /* status bar/notch — the 42px content stays below it */
background:var(--panel); border-bottom:1px solid var(--line); background:var(--panel); border-bottom:1px solid var(--line);
display:flex; align-items:center; justify-content:space-between; padding:0 16px; gap:16px; 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; } #rail .mark{ display:flex; align-items:baseline; gap:11px; min-width:0; }
/* panel show/hide — usable at any viewport width, not just small screens */
#panelToggle{
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); }
#panelBackdrop{ display:none; }
#rail h1{ font-size:11.5px; font-weight:500; margin:0; letter-spacing:0.22em; text-transform:uppercase; } #rail h1{ font-size:11.5px; font-weight:500; margin:0; letter-spacing:0.22em; text-transform:uppercase; }
#rail .tag{ font-size:9.5px; color:var(--faint); letter-spacing:0.14em; white-space:nowrap; } #rail .tag{ font-size:9.5px; color:var(--faint); letter-spacing:0.14em; white-space:nowrap; }
#rail .rail-right{ display:flex; align-items:center; gap:16px; flex:none; } #rail .rail-right{ display:flex; align-items:center; gap:16px; flex:none; }
@@ -93,10 +107,12 @@ try {
/* ── Docked instrument column ── */ /* ── Docked instrument column ── */
#panel{ #panel{
position:fixed; top:42px; left:0; bottom:0; z-index:10; width:284px; position:fixed; top:var(--rail-h); left:0; bottom:0; z-index:10; width:284px;
background:var(--panel); border-right:1px solid var(--line); background:var(--panel); border-right:1px solid var(--line);
display:flex; flex-direction:column; overflow-y:auto; 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{ padding:16px 18px; border-bottom:1px solid var(--line); display:flex; flex-direction:column; gap:12px; }
.sect:last-child{ border-bottom:none; } .sect:last-child{ border-bottom:none; }
.lab{ font-size:9.5px; letter-spacing:0.18em; color:var(--faint); } .lab{ font-size:9.5px; letter-spacing:0.18em; color:var(--faint); }
@@ -226,7 +242,9 @@ try {
#loading.empty .load-live{ display:none; } #loading.empty .load-live{ display:none; }
#loading.empty .prompt{ display:flex; pointer-events:auto; } #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 */ /* the "no cloud yet" state must not block the dashboard — you can go live without a file */
#loading.empty{ top:42px; 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; } #loading .glyph{ font-size:10px; color:var(--faint); letter-spacing:0.5em; text-indent:0.5em; }
/* ── drag & drop overlay ── */ /* ── drag & drop overlay ── */
@@ -237,6 +255,25 @@ try {
color:var(--txt); font-size:12px; letter-spacing:0.06em; text-align:center; } color:var(--txt); font-size:12px; letter-spacing:0.06em; text-align:center; }
#drop .frame .s{ display:block; margin-top:9px; font-size:10px; color:var(--faint); letter-spacing:0.24em; } #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 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){
#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;
}
#panelBackdrop.show{ opacity:1; pointer-events:auto; }
#cam{ width:min(340px, 92vw); }
}
@media (max-width:480px){
#rail{ padding:0 10px; gap:8px; }
#rail .tag{ display:none; } /* subtitle — first to go */
#rail .tele .chip:nth-child(2), #rail .tele .chip:nth-child(3){ display:none; } /* keep LIVE only */
}
@media (prefers-reduced-motion:reduce){ *{ transition:none !important; } } @media (prefers-reduced-motion:reduce){ *{ transition:none !important; } }
</style> </style>
</head> </head>
@@ -261,6 +298,7 @@ try {
<div id="rail"> <div id="rail">
<div class="mark"> <div class="mark">
<button id="panelToggle" aria-label="패널 열기/닫기" aria-expanded="false" aria-controls="panel">MENU</button>
<h1>PCD Viewer</h1> <h1>PCD Viewer</h1>
<span class="tag">FAST-LIVO2&nbsp;·&nbsp;POINT&nbsp;CLOUD&nbsp;METROLOGY</span> <span class="tag">FAST-LIVO2&nbsp;·&nbsp;POINT&nbsp;CLOUD&nbsp;METROLOGY</span>
</div> </div>
@@ -277,6 +315,8 @@ try {
</div> </div>
</div> </div>
<div id="panelBackdrop"></div>
<div id="panel"> <div id="panel">
<div class="sect"> <div class="sect">
<div class="lab">LIVE · FAST-LIVO2</div> <div class="lab">LIVE · FAST-LIVO2</div>
@@ -501,6 +541,10 @@ scene.background = new THREE.Color(THEME_BG[currentTheme]);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 10000); const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 10000);
camera.position.set(0, 0, 10); camera.position.set(0, 0, 10);
// Z-up everywhere, matching the actual data (ROS/FAST-LIVO2) and WALK mode —
// orbiting with camera.up misaligned from the cloud's real vertical makes yaw
// look like a diagonal tumble instead of a clean spin. See also exitFP().
camera.up.set(0, 0, 1);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' }); const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
@@ -512,6 +556,57 @@ const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.enableDamping = true;
controls.dampingFactor = 0.08; controls.dampingFactor = 0.08;
controls.zoomToCursor = true; // zoom toward the cursor, not the orbit target — matches CAD/CloudCompare 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) — OrbitControls only handles 1-2 simultaneous
// touches (a 3rd pointer makes it go idle), so three-finger pan below covers panning.
controls.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
// ─── three-finger pan — replicates OrbitControls' own (private, unexported) screen-
// space pan math so it feels identical to right-drag/two-finger-pan elsewhere ──
const panTouches = new Map(); // pointerId -> {x,y}
function touchCentroid(map) {
let x = 0, y = 0;
for (const p of map.values()) { x += p.x; y += p.y; }
return { x: x / map.size, y: y / map.size };
}
function panByScreenDelta(deltaX, deltaY) {
const offset = new THREE.Vector3().copy(camera.position).sub(controls.target);
const targetDistance = offset.length() * Math.tan((camera.fov / 2) * Math.PI / 180);
const panLeftDist = 2 * deltaX * targetDistance / renderer.domElement.clientHeight * controls.panSpeed;
const panUpDist = 2 * deltaY * targetDistance / renderer.domElement.clientHeight * controls.panSpeed;
const panOffset = new THREE.Vector3().setFromMatrixColumn(camera.matrix, 0).multiplyScalar(-panLeftDist);
const yCol = new THREE.Vector3();
if (controls.screenSpacePanning) yCol.setFromMatrixColumn(camera.matrix, 1);
else { yCol.setFromMatrixColumn(camera.matrix, 0); yCol.crossVectors(camera.up, yCol); }
panOffset.add(yCol.multiplyScalar(panUpDist));
camera.position.add(panOffset);
controls.target.add(panOffset);
controls.update();
invalidate();
}
let panCentroid = null;
renderer.domElement.addEventListener('pointerdown', e => {
if (e.pointerType !== 'touch') return;
panTouches.set(e.pointerId, { x: e.clientX, y: e.clientY });
panCentroid = panTouches.size === 3 ? touchCentroid(panTouches) : null;
});
renderer.domElement.addEventListener('pointermove', e => {
if (e.pointerType !== 'touch' || !panTouches.has(e.pointerId)) return;
panTouches.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (panTouches.size !== 3) return;
const c = touchCentroid(panTouches);
if (panCentroid) panByScreenDelta(c.x - panCentroid.x, c.y - panCentroid.y);
panCentroid = c;
});
function releaseTouchPan(e) {
if (e.pointerType !== 'touch' || !panTouches.has(e.pointerId)) return;
panTouches.delete(e.pointerId);
panCentroid = panTouches.size === 3 ? touchCentroid(panTouches) : null;
}
renderer.domElement.addEventListener('pointerup', releaseTouchPan);
renderer.domElement.addEventListener('pointercancel', releaseTouchPan);
// ─── on-demand rendering (perf: idle frames cost nothing) ──────────────── // ─── on-demand rendering (perf: idle frames cost nothing) ────────────────
let needsRender = true; let needsRender = true;
@@ -576,6 +671,21 @@ function applyTheme(name) {
} }
themeButtons.forEach(b => b.onclick = () => applyTheme(b.dataset.theme)); themeButtons.forEach(b => b.onclick = () => applyTheme(b.dataset.theme));
// ─── 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('panel-hidden', !open);
panelBackdropEl.classList.toggle('show', open);
panelToggleEl.setAttribute('aria-expanded', String(open));
}
panelToggleEl.onclick = () => setPanelOpen(panelEl.classList.contains('panel-hidden'));
panelBackdropEl.onclick = () => setPanelOpen(false);
panelNarrowMQ.addEventListener('change', e => setPanelOpen(!e.matches));
setPanelOpen(!panelNarrowMQ.matches); // open on desktop/tablet, closed on phones, by default
// segmented mode control // segmented mode control
document.querySelectorAll('#modeSeg button').forEach(b => { document.querySelectorAll('#modeSeg button').forEach(b => {
b.onclick = () => setMode(b.dataset.mode); b.onclick = () => setMode(b.dataset.mode);
@@ -964,7 +1074,7 @@ function exitFP() {
const d = new THREE.Vector3(); camera.getWorldDirection(d); const d = new THREE.Vector3(); camera.getWorldDirection(d);
const r = points?.geometry.boundingSphere?.radius || 5; const r = points?.geometry.boundingSphere?.radius || 5;
controls.target.copy(camera.position).addScaledVector(d, Math.max(r, 5)); controls.target.copy(camera.position).addScaledVector(d, Math.max(r, 5));
camera.up.set(0, 1, 0); camera.up.set(0, 0, 1); // back to the app-wide Z-up default, not three.js's Y-up
controls.enabled = true; controls.enabled = true;
controls.update(); controls.update();
fpStartBtn.disabled = false; fpExitBtn.disabled = true; fpStartBtn.disabled = false; fpExitBtn.disabled = true;
@@ -1137,8 +1247,23 @@ function onError(err, isDefault, name) {
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); } function escapeHtml(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
// file input + drag & drop // 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) { function openFile(file) {
if (!/\.pcd$/i.test(file.name)) { onError(new Error('.pcd 파일이 아닙니다'), false, file.name); return; } 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 }); loadPCD(URL.createObjectURL(file), file.name, { revoke: true });
} }
fileInput.onchange = e => { if (e.target.files[0]) openFile(e.target.files[0]); e.target.value = ''; }; fileInput.onchange = e => { if (e.target.files[0]) openFile(e.target.files[0]); e.target.value = ''; };
+8
View File
@@ -4,6 +4,14 @@ version = "0.1.0"
edition = "2021" edition = "2021"
rust-version = "1.77.2" rust-version = "1.77.2"
[lib]
name = "pcd_viewer_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "pcd-viewer"
path = "src/main.rs"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.6.3", features = [] } tauri-build = { version = "2.6.3", features = [] }
+12
View File
@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
+20
View File
@@ -0,0 +1,20 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
keystore.properties
/.tauri
/tauri.settings.gradle
+6
View File
@@ -0,0 +1,6 @@
/src/main/**/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties
@@ -0,0 +1,71 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 36
namespace = "com.khj.pcdviewer"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.khj.pcdviewer"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.pcd_viewer"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,11 @@
package com.khj.pcdviewer
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}
}
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.pcd_viewer" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<resources>
<string name="app_name">"PCD Viewer"</string>
<string name="main_activity_title">"PCD Viewer"</string>
</resources>
@@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.pcd_viewer" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
+22
View File
@@ -0,0 +1,22 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}
@@ -0,0 +1,23 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0")
}
@@ -0,0 +1,68 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """cargo""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}
@@ -0,0 +1,85 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false
Binary file not shown.
@@ -0,0 +1,6 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+3
View File
@@ -0,0 +1,3 @@
include ':app'
apply from: 'tauri.settings.gradle'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running pcd viewer");
}
+1 -3
View File
@@ -1,7 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
tauri::Builder::default() pcd_viewer_lib::run();
.run(tauri::generate_context!())
.expect("error while running pcd viewer");
} }