Skip to content

Commit 4f32ed3

Browse files
LimHyungTaeclaude
andcommitted
Polish web demos: i18n, PLY, KdTree-backed pages, RANSAC + clustering
User-facing - English default + Korean toggle (top-right). Locale stored in localStorage. - Three preset inputs in every demo (KITTI / NaverLabs / Stanford Bunny) plus a drag&drop / file-picker fallback. PLY parser added. - Renumbered chapters 01..10 (no gaps); chapters 0/1-x/4 dropped from the sidebar — they don't lend themselves to interactive demos. - New interactive demos: 03 SOR · KdTree-based mean-distance + stddev filter 04 Radius Search · query xyz + radius 05 KNN · query xyz + K 06 Normal Estimation · KdTree + analytic 3×3 symmetric eig 09 RANSAC Plane Segmentation (extra) · ground/wall removal 10 Euclidean Clustering (extra) · BFS over KdTree, optional ground prune - GICP upgraded from stub to precomputed (reuses gicp_*.pcd from the C++ export). Lec11/Lec12 share a PrecomputedRegistration component. Visual - Theme inspired by dgist-slam.github.io: deep navy bg with subtle 60×60 grid overlay, JetBrains Mono headings with teal→sky gradient, Inter body, custom-styled range thumbs and scrollbars, fade-up card entrance animations. Internals - web/src/lib/kdtree.ts — array-backed median-split KdTree with radiusSearch + bounded-heap knnSearch. - web/src/lib/filters/{sor, normal, ransacPlane, euclideanCluster}.ts. - web/src/lib/ply.ts — minimal ASCII + binary little-endian parser. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b6cd58d commit 4f32ed3

34 files changed

Lines changed: 2807 additions & 473 deletions

web/index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
<!doctype html>
2-
<html lang="ko">
2+
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='13' font-size='14'>📍</text></svg>" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>PCL Tutorial · 한글</title>
8-
<meta name="description" content="Point Cloud Library를 어떻게 잘 쓰는지에 초점을 맞춘 한글 인터랙티브 튜토리얼." />
7+
<title>PCL Tutorial · Hands-on Point Cloud Library</title>
8+
<meta name="description" content="Interactive web tutorial for the Point Cloud Library — voxel, passthrough, KdTree, normals, ICP, RANSAC plane and Euclidean clustering, all in your browser." />
9+
<link rel="preconnect" href="https://fonts.googleapis.com" />
10+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11+
<link
12+
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap"
13+
rel="stylesheet"
14+
/>
915
</head>
10-
<body class="bg-slate-950 text-slate-100">
16+
<body class="bg-[var(--bg)] text-[var(--text)]">
1117
<div id="root"></div>
1218
<script type="module" src="/src/main.tsx"></script>
1319
</body>

web/src/chapters.ts

Lines changed: 34 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,94 @@
11
export type ChapterStatus = "interactive" | "precomputed" | "code" | "stub";
22

3-
export type Chapter = {
3+
export type ChapterMeta = {
44
slug: string;
5+
/** 1-based chapter number shown in UI. */
56
number: string;
6-
title: string;
7-
subtitle: string;
8-
source: string; // path to the .cpp file at the repo root
9-
blog: string; // full URL to the matching blog post (or "" if none)
7+
/** Path to the .cpp file at the repo root. Empty for web-only extras. */
8+
source: string;
9+
/** Full URL to the matching blog post (or "" if none). */
10+
blog: string;
1011
status: ChapterStatus;
1112
};
1213

1314
const BLOG = "https://limhyungtae.github.io";
1415

15-
export const chapters: Chapter[] = [
16-
{
17-
slug: "lec00",
18-
number: "0",
19-
title: "PCL 기본 사용법",
20-
subtitle: "PointCloud 선언, point 타입, 합치기",
21-
source: "lec00_usage.cpp",
22-
blog: `${BLOG}/2021-09-09-ROS-Point-Cloud-Library-(PCL)-0.-Tutorial-및-기본-사용법/`,
23-
status: "code",
24-
},
25-
{
26-
slug: "lec01_1",
27-
number: "1-1",
28-
title: "shared_ptr 기본",
29-
subtitle: "C++ shared_ptr 동작 이해",
30-
source: "lec01_1_shared_ptr.cpp",
31-
blog: `${BLOG}/2021-09-09-ROS-Point-Cloud-Library-(PCL)-1.-Ptr,-ConstPtr의-완벽-이해-(1)-shared_ptr/`,
32-
status: "code",
33-
},
34-
{
35-
slug: "lec01_2",
36-
number: "1-2",
37-
title: "PCL에서의 Ptr",
38-
subtitle: "Ptr / ConstPtr 사용법, 주소 공유 시 함정",
39-
source: "lec01_2_ptr.cpp",
40-
blog: `${BLOG}/2021-09-10-ROS-Point-Cloud-Library-(PCL)-1.-Ptr,-ConstPtr의-완벽-이해-(2)-Ptr-in-PCL/`,
41-
status: "code",
42-
},
43-
{
44-
slug: "lec01_3",
45-
number: "1-3",
46-
title: "클래스 멤버변수 Ptr",
47-
subtitle: "선언과 .reset()을 통한 초기화",
48-
source: "lec01_3_ptr_in_class.cpp",
49-
blog: `${BLOG}/2021-09-10-ROS-Point-Cloud-Library-(PCL)-1.-Ptr,-ConstPtr의-완벽-이해-(3)-Ptr-in-클래스-멤버변수/`,
50-
status: "code",
51-
},
52-
{
53-
slug: "lec03",
54-
number: "3",
55-
title: "Transformation",
56-
subtitle: "4×4 행렬 기반 변환",
57-
source: "lec03_transformation.cpp",
58-
blog: `${BLOG}/2021-09-10-ROS-Point-Cloud-Library-(PCL)-3.-Transformation/`,
59-
status: "code",
60-
},
61-
{
62-
slug: "lec04",
63-
number: "4",
64-
title: "Visualization",
65-
subtitle: "PCLVisualizer 사용법",
66-
source: "lec04_visualization.cpp",
67-
blog: `${BLOG}/2021-09-10-ROS-Point-Cloud-Library-(PCL)-4.-Viewer로-visualization하는-법/`,
68-
status: "code",
69-
},
16+
export const chapters: ChapterMeta[] = [
7017
{
7118
slug: "lec05",
72-
number: "5",
73-
title: "Voxelization",
74-
subtitle: "Voxel Grid 필터링 — 크기를 슬라이더로 조절",
19+
number: "1",
7520
source: "lec05_voxelization.cpp",
7621
blog: `${BLOG}/2021-09-12-ROS-Point-Cloud-Library-(PCL)-5.-Voxelization/`,
7722
status: "interactive",
7823
},
7924
{
8025
slug: "lec06",
81-
number: "6",
82-
title: "PassThrough",
83-
subtitle: "축별 박스 필터로 점군 잘라내기",
26+
number: "2",
8427
source: "lec06_pass_through.cpp",
8528
blog: `${BLOG}/2021-09-12-ROS-Point-Cloud-Library-(PCL)-6.-PassThrough/`,
8629
status: "interactive",
8730
},
8831
{
8932
slug: "lec07",
90-
number: "7",
91-
title: "Statistical Outlier Removal",
92-
subtitle: "이웃 거리 분포로 outlier 제거",
33+
number: "3",
9334
source: "lec07_sor.cpp",
9435
blog: `${BLOG}/2021-09-12-ROS-Point-Cloud-Library-(PCL)-7.-Statistical-Outlier-Removal/`,
95-
status: "stub",
36+
status: "interactive",
9637
},
9738
{
9839
slug: "lec08",
99-
number: "8",
100-
title: "Radius Search",
101-
subtitle: "KdTree로 반경 내 이웃 찾기",
40+
number: "4",
10241
source: "lec08_radius_search.cpp",
10342
blog: `${BLOG}/2021-09-12-ROS-Point-Cloud-Library-(PCL)-8.-KdTree를-활용한-Radius-Search/`,
104-
status: "stub",
43+
status: "interactive",
10544
},
10645
{
10746
slug: "lec09",
108-
number: "9",
109-
title: "K-Nearest Neighbor",
110-
subtitle: "KdTree로 K개 최근접점 찾기",
47+
number: "5",
11148
source: "lec09_knn.cpp",
11249
blog: `${BLOG}/2021-09-12-ROS-Point-Cloud-Library-(PCL)-9.-KdTree를-활용한-K-nearest-Neighbor-Search-(KNN)/`,
113-
status: "stub",
50+
status: "interactive",
11451
},
11552
{
11653
slug: "lec10",
117-
number: "10",
118-
title: "Normal Estimation",
119-
subtitle: "KdTree + SVD로 Normal 추정",
54+
number: "6",
12055
source: "lec10_1_normal.cpp",
12156
blog: `${BLOG}/2021-09-13-ROS-Point-Cloud-Library-(PCL)-10.-Normal-Estimation/`,
122-
status: "stub",
57+
status: "interactive",
12358
},
12459
{
12560
slug: "lec11",
126-
number: "11",
127-
title: "Iterative Closest Point",
128-
subtitle: "Pre-computed src ↔ tgt ↔ aligned 비교",
61+
number: "7",
12962
source: "lec11_icp.cpp",
13063
blog: `${BLOG}/2021-09-14-ROS-Point-Cloud-Library-(PCL)-11.-Iterative-Closest-Point-(ICP)/`,
13164
status: "precomputed",
13265
},
13366
{
13467
slug: "lec12",
135-
number: "12",
136-
title: "Generalized ICP",
137-
subtitle: "공분산 기반 ICP 변형",
68+
number: "8",
13869
source: "lec12_gicp.cpp",
13970
blog: `${BLOG}/2021-09-14-ROS-Point-Cloud-Library-(PCL)-12.-Generalized-Iterative-Closest-Point-(G-ICP)/`,
140-
status: "stub",
71+
status: "precomputed",
72+
},
73+
{
74+
slug: "extra01",
75+
number: "9",
76+
source: "",
77+
blog: "",
78+
status: "interactive",
79+
},
80+
{
81+
slug: "extra02",
82+
number: "10",
83+
source: "",
84+
blog: "",
85+
status: "interactive",
14186
},
14287
];
14388

14489
export const findChapter = (slug: string) =>
14590
chapters.find((c) => c.slug === slug);
14691

147-
export const statusLabel: Record<ChapterStatus, string> = {
148-
interactive: "인터랙티브",
149-
precomputed: "Pre-computed",
150-
code: "코드 설명",
151-
stub: "준비중",
152-
};
153-
15492
export const statusColor: Record<ChapterStatus, string> = {
15593
interactive: "bg-emerald-500/15 text-emerald-300 ring-emerald-500/30",
15694
precomputed: "bg-sky-500/15 text-sky-300 ring-sky-500/30",

web/src/components/BeforeAfterPanel.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ export default function BeforeAfterPanel({
1919
}: Props) {
2020
return (
2121
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
22-
<Pane label={beforeLabel} meta={beforeMeta} tone="rose">
22+
<Pane label={beforeLabel} meta={beforeMeta} dot="#f87171" tint="rgba(248,113,113,0.06)">
2323
{before}
2424
</Pane>
25-
<Pane label={afterLabel} meta={afterMeta} tone="emerald">
25+
<Pane label={afterLabel} meta={afterMeta} dot="#00d4aa" tint="rgba(0,212,170,0.06)">
2626
{after}
2727
</Pane>
2828
</div>
@@ -32,30 +32,30 @@ export default function BeforeAfterPanel({
3232
function Pane({
3333
label,
3434
meta,
35-
tone,
35+
dot,
36+
tint,
3637
children,
3738
}: {
3839
label: string;
3940
meta?: string;
40-
tone: "rose" | "emerald";
41+
dot: string;
42+
tint: string;
4143
children: ReactNode;
4244
}) {
43-
const tones = {
44-
rose: "border-rose-500/30 bg-rose-500/[.04]",
45-
emerald: "border-emerald-500/30 bg-emerald-500/[.04]",
46-
} as const;
47-
const dots = {
48-
rose: "bg-rose-400",
49-
emerald: "bg-emerald-400",
50-
} as const;
5145
return (
52-
<div className={`overflow-hidden rounded-xl border ${tones[tone]}`}>
53-
<div className="flex items-center justify-between border-b border-white/5 bg-slate-950/40 px-4 py-2 text-xs">
46+
<div
47+
className="overflow-hidden rounded-xl border border-[var(--border)]"
48+
style={{ background: tint }}
49+
>
50+
<div className="flex items-center justify-between border-b border-white/5 bg-[color:rgba(10,15,26,0.4)] px-4 py-2 text-[11px]">
5451
<div className="flex items-center gap-2">
55-
<span className={`inline-block h-2 w-2 rounded-full ${dots[tone]}`} />
56-
<span className="font-medium text-slate-200">{label}</span>
52+
<span
53+
className="inline-block h-2 w-2 rounded-full"
54+
style={{ background: dot }}
55+
/>
56+
<span className="font-semibold text-[var(--text)]">{label}</span>
5757
</div>
58-
{meta && <span className="code-font text-slate-500">{meta}</span>}
58+
{meta && <span className="code-font text-[var(--dim)]">{meta}</span>}
5959
</div>
6060
<div className="aspect-[4/3] w-full">{children}</div>
6161
</div>

web/src/components/ChapterHeader.tsx

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,49 @@
1-
import { Chapter, statusColor, statusLabel } from "../chapters";
1+
import { ChapterMeta, statusColor } from "../chapters";
2+
import { useT } from "../i18n";
23

34
const REPO = "https://github.com/LimHyungTae/pcl_tutorial/blob/main";
45

5-
export default function ChapterHeader({ chapter }: { chapter: Chapter }) {
6+
export default function ChapterHeader({ chapter }: { chapter: ChapterMeta }) {
7+
const t = useT();
8+
const tc = t.chapters[chapter.slug as keyof typeof t.chapters];
69
return (
7-
<header className="border-b border-slate-800/80 pb-6">
8-
<div className="flex items-center gap-3 text-xs">
9-
<span className="code-font text-slate-500">
10-
Chapter {chapter.number}
10+
<header className="border-b border-[var(--border)] pb-7 fade-up">
11+
<div className="flex items-center gap-3 text-[11px]">
12+
<span className="code-font uppercase tracking-[0.2em] text-[var(--mut)]">
13+
{t.home.chapterPrefix} {chapter.number.padStart(2, "0")}
1114
</span>
1215
<span
1316
className={`rounded px-1.5 py-0.5 text-[10px] ring-1 ring-inset ${statusColor[chapter.status]}`}
1417
>
15-
{statusLabel[chapter.status]}
18+
{t.status[chapter.status]}
1619
</span>
1720
</div>
18-
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-white">
19-
{chapter.title}
21+
<h1 className="heading-mono mt-3 text-3xl font-extrabold tracking-tight">
22+
<span className="gradient-text">{tc.title}</span>
2023
</h1>
21-
<p className="mt-2 text-base text-slate-400">{chapter.subtitle}</p>
24+
<p className="mt-2 max-w-3xl text-[14px] leading-relaxed text-[var(--dim)]">
25+
{tc.subtitle}
26+
</p>
2227

23-
<div className="mt-4 flex flex-wrap gap-3 text-xs">
24-
<a
25-
href={`${REPO}/${chapter.source}`}
26-
target="_blank"
27-
rel="noreferrer"
28-
className="code-font rounded-md border border-slate-700/80 px-3 py-1.5 text-slate-300 transition hover:border-slate-500 hover:text-white"
29-
>
30-
{chapter.source}
31-
</a>
28+
<div className="mt-4 flex flex-wrap gap-2 text-xs">
29+
{chapter.source && (
30+
<a
31+
href={`${REPO}/${chapter.source}`}
32+
target="_blank"
33+
rel="noreferrer"
34+
className="code-font rounded-md border border-[var(--border-strong)] bg-[var(--surface)] px-3 py-1.5 text-[var(--dim)] transition hover:border-[var(--accent)] hover:text-[var(--accent)]"
35+
>
36+
{chapter.source}
37+
</a>
38+
)}
3239
{chapter.blog && (
3340
<a
3441
href={chapter.blog}
3542
target="_blank"
3643
rel="noreferrer"
37-
className="rounded-md border border-slate-700/80 px-3 py-1.5 text-slate-300 transition hover:border-slate-500 hover:text-white"
44+
className="rounded-md border border-[var(--border-strong)] bg-[var(--surface)] px-3 py-1.5 text-[var(--dim)] transition hover:border-[var(--accent-2)] hover:text-[var(--accent-2)]"
3845
>
39-
블로그 글
46+
{t.nav.blogPost}
4047
</a>
4148
)}
4249
</div>

0 commit comments

Comments
 (0)