macos-native: clean branch for cloning and building the Tauri desktop app

Orphan branch with no history — includes the Tauri shell (src-tauri/) and
vendored Three.js (vendor/) that were never pushed before, so a fresh clone
can actually build the native app. Drops the large .pcd sample captures and
editor cruft (.obsidian, .DS_Store); they aren't needed to build or run the
app (point clouds load via drag-and-drop, not bundling).
This commit is contained in:
Dongubak
2026-08-22 02:21:38 +09:00
commit beb4182f04
37 changed files with 66261 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
// Minimal rosbridge v2 stand-in: hand-rolled RFC6455 (no deps) + synthetic
// FAST-LIVO2-shaped topics, so the viewer's live path can be exercised for real.
import http from 'node:http';
import crypto from 'node:crypto';
import zlib from 'node:zlib';
const PORT = 9090;
// ── websocket framing ────────────────────────────────────────────────────
function accept(key) {
return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
}
function encodeFrame(str) {
const payload = Buffer.from(str, 'utf8');
const len = payload.length;
let header;
if (len < 126) { header = Buffer.alloc(2); header[1] = len; }
else if (len < 65536) { header = Buffer.alloc(4); header[1] = 126; header.writeUInt16BE(len, 2); }
else { header = Buffer.alloc(10); header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2); }
header[0] = 0x81; // FIN + text
return Buffer.concat([header, payload]);
}
function* decodeFrames(buf) { // client → server frames are masked
let off = 0;
while (off + 2 <= buf.length) {
const b1 = buf[off + 1];
let len = b1 & 127, p = off + 2;
if (len === 126) { len = buf.readUInt16BE(p); p += 2; }
else if (len === 127) { len = Number(buf.readBigUInt64BE(p)); p += 8; }
const masked = b1 & 128;
const mask = masked ? buf.subarray(p, p + 4) : null;
if (masked) p += 4;
if (p + len > buf.length) return; // partial frame: wait for more
const data = Buffer.from(buf.subarray(p, p + len));
if (mask) for (let i = 0; i < data.length; i++) data[i] ^= mask[i % 4];
yield { opcode: buf[off] & 15, text: data.toString('utf8') };
off = p + len;
}
}
// ── a tiny PNG encoder, to stand in for the camera's compressed frames ───
function crc32(buf) {
let c, crc = 0xffffffff;
for (let n = 0; n < buf.length; n++) {
c = (crc ^ buf[n]) & 0xff;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
crc = (crc >>> 8) ^ c;
}
return (crc ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type, 'ascii'), data]);
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td));
return Buffer.concat([len, td, crc]);
}
function png(w, h, paint) {
const raw = Buffer.alloc(h * (w * 3 + 1));
for (let y = 0; y < h; y++) {
const row = y * (w * 3 + 1);
raw[row] = 0;
for (let x = 0; x < w; x++) {
const [r, g, b] = paint(x, y);
raw[row + 1 + x * 3] = r; raw[row + 2 + x * 3] = g; raw[row + 3 + x * 3] = b;
}
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
ihdr[8] = 8; ihdr[9] = 2; // 8-bit RGB
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))
]);
}
// ── synthetic FAST-LIVO2 session ─────────────────────────────────────────
const TOPICS = [
['/cloud_registered', 'sensor_msgs/msg/PointCloud2'],
['/cloud_effected', 'sensor_msgs/msg/PointCloud2'],
['/aft_mapped_to_init', 'nav_msgs/msg/Odometry'],
['/path', 'nav_msgs/msg/Path'],
['/origin_img', 'sensor_msgs/msg/CompressedImage'],
['/rosout', 'rcl_interfaces/msg/Log']
];
let t0 = Date.now();
const trail = [];
const pose = () => {
const t = (Date.now() - t0) / 1000;
return { x: 6 * Math.cos(t * 0.25), y: 6 * Math.sin(t * 0.25), z: 0.4 + 0.1 * Math.sin(t) };
};
const stamp = () => ({ sec: Math.floor(Date.now() / 1000), nanosec: (Date.now() % 1000) * 1e6 });
function cloudMsg(n = 2500) {
const step = 16; // x,y,z float32 + rgb packed
const buf = Buffer.alloc(n * step);
const p = pose();
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2, r = 2 + Math.random() * 3;
const x = p.x + r * Math.cos(a), y = p.y + r * Math.sin(a), z = Math.random() * 2.4;
const o = i * step;
buf.writeFloatLE(x, o); buf.writeFloatLE(y, o + 4); buf.writeFloatLE(z, o + 8);
const t = Math.min(1, z / 2.4);
const rgb = ((60 + 180 * t | 0) << 16) | ((90 + 100 * (1 - t) | 0) << 8) | (200 - 140 * t | 0);
buf.writeUInt32LE(rgb >>> 0, o + 12);
}
return {
header: { stamp: stamp(), frame_id: 'camera_init' },
height: 1, width: n,
fields: [
{ name: 'x', offset: 0, datatype: 7, count: 1 },
{ name: 'y', offset: 4, datatype: 7, count: 1 },
{ name: 'z', offset: 8, datatype: 7, count: 1 },
{ name: 'rgb', offset: 12, datatype: 7, count: 1 }
],
is_bigendian: false, point_step: step, row_step: step * n,
data: buf.toString('base64'), is_dense: true
};
}
function odomMsg() {
const p = pose();
trail.push(p);
if (trail.length > 600) trail.shift();
return { header: { stamp: stamp(), frame_id: 'camera_init' }, child_frame_id: 'body',
pose: { pose: { position: p, orientation: { x: 0, y: 0, z: 0, w: 1 } } } };
}
function pathMsg() {
return { header: { stamp: stamp(), frame_id: 'camera_init' },
poses: trail.map(p => ({ header: { stamp: stamp(), frame_id: 'camera_init' },
pose: { position: p, orientation: { x: 0, y: 0, z: 0, w: 1 } } })) };
}
function imgMsg() {
const t = (Date.now() - t0) / 400;
const buf = png(320, 180, (x, y) => {
const v = 40 + 90 * Math.sin(x / 22 + t) * Math.cos(y / 18 - t * 0.4);
return [Math.max(0, v * 1.4) | 0, Math.max(0, v) | 0, Math.max(0, v * 0.7) | 0];
});
return { header: { stamp: stamp(), frame_id: 'camera' }, format: 'rgb8; png compressed bgr8',
data: buf.toString('base64') };
}
// ── server ───────────────────────────────────────────────────────────────
const server = http.createServer((_, res) => res.end('mock rosbridge'));
server.on('upgrade', (req, socket) => {
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${accept(req.headers['sec-websocket-key'])}\r\n\r\n`);
const send = o => socket.write(encodeFrame(JSON.stringify(o)));
const timers = [];
const pub = (topic, make, hz) => timers.push(setInterval(() => send({ op: 'publish', topic, msg: make() }), 1000 / hz));
let acc = Buffer.alloc(0);
socket.on('data', d => {
acc = Buffer.concat([acc, d]);
let consumed = 0;
for (const fr of decodeFrames(acc)) {
consumed = acc.length; // our client only sends small whole frames
if (fr.opcode === 8) { socket.end(); return; }
let m; try { m = JSON.parse(fr.text); } catch { continue; }
console.log('→', m.op, m.service || m.topic || '');
if (m.op === 'call_service' && m.service === '/rosapi/topics') {
send({ op: 'service_response', service: m.service, id: m.id, result: true,
values: { topics: TOPICS.map(t => t[0]), types: TOPICS.map(t => t[1]) } });
} else if (m.op === 'subscribe') {
if (m.topic === '/cloud_registered' || m.topic === '/cloud_effected') pub(m.topic, () => cloudMsg(), 10);
else if (m.topic === '/aft_mapped_to_init') pub(m.topic, odomMsg, 20);
else if (m.topic === '/path') pub(m.topic, pathMsg, 5);
else if (m.topic === '/origin_img') pub(m.topic, imgMsg, 5);
}
}
acc = acc.subarray(consumed);
});
socket.on('close', () => timers.forEach(clearInterval));
socket.on('error', () => timers.forEach(clearInterval));
});
server.listen(PORT, () => console.log('mock rosbridge on ws://localhost:' + PORT));