Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Only the tiles visible on screen are fetched via SSE-based LOD, enabling smooth
- **LRU cache** — Configurable node count and memory limits
- **Eye-Dome Lighting** — EDL post-processing for depth perception
- **Color modes** — RGB, height ramp, intensity, classification, and white
- **Filtering** — By classification, intensity range, or bounding box (WGS84)

## Install

Expand All @@ -40,7 +41,10 @@ const layer = new CopcLayer('https://example.com/pointcloud.copc.laz', {
colorMode: 'rgb',
pointSize: 4,
enableEDL: true,
onInitialized: ({ center }) => map.flyTo({ center, zoom: 16 }),
onInitialized: ({ bounds }) => map.flyTo({
center: [(bounds.minx + bounds.maxx) / 2, (bounds.miny + bounds.maxy) / 2],
zoom: 16,
}),
});

map.on('load', () => map.addLayer(layer));
Expand All @@ -55,7 +59,7 @@ map.on('load', () => map.addLayer(layer));
| `pointSize` | `number` | `6` | Point size in pixels |
| `colorMode` | `'rgb' \| 'height' \| 'intensity' \| 'classification' \| 'white'` | `'rgb'` | Coloring mode |
| `classificationColors` | `Record<number, [number, number, number]>` | `{}` | Override or add classification code colors (0–1 RGB). Merged with ASPRS defaults |
| `filter` | `PointFilter` | `{}` | Filter points by classification or intensity range |
| `filter` | `PointFilter` | `{}` | Filter points by classification, intensity range, or bounding box |
| `alwaysShowRoot` | `boolean` | `false` | Always show root node even when SSE is below threshold |
| `sseThreshold` | `number` | `8` | SSE threshold for LOD — lower loads more detail |
| `depthTest` | `boolean` | `true` | Enable depth testing |
Expand All @@ -65,7 +69,7 @@ map.on('load', () => map.addLayer(layer));
| `edlStrength` | `number` | `0.4` | EDL effect strength |
| `edlRadius` | `number` | `1.5` | EDL sampling radius |
| `debug` | `boolean` | `false` | Enable debug logging |
| `onInitialized` | `(msg) => void` | — | Called with `{ nodeCount, center }` after COPC header loads |
| `onInitialized` | `(msg) => void` | — | Called with `{ nodeCount, bounds }` after COPC header loads. `bounds` contains `minx/maxx/miny/maxy/minz/maxz` in WGS84 |

### Methods

Expand All @@ -76,7 +80,7 @@ map.on('load', () => map.addLayer(layer));
| `setDepthTest(enabled)` | Toggle depth testing |
| `setEDLEnabled(enabled)` | Toggle Eye-Dome Lighting |
| `updateEDLParameters({ strength?, radius? })` | Update EDL parameters |
| `setFilter(filter)` | Update point filter (classification / intensity) |
| `setFilter(filter)` | Update point filter (classification / intensity / bbox) |
| `getFilter()` | Get current point filter |
| `updateCacheConfig(config)` | Update cache limits at runtime |
| `clearCache()` | Clear all cached nodes |
Expand Down
95 changes: 94 additions & 1 deletion dev/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,23 @@ for (const code of Object.keys(CLASSIFICATION_LABELS)) {
const filterState = {
intensityMin: 0,
intensityMax: 1,
bboxMinX: 0,
bboxMaxX: 0,
bboxMinY: 0,
bboxMaxY: 0,
bboxMinZ: 0,
bboxMaxZ: 0,
};

let bboxBounds: {
minx: number;
maxx: number;
miny: number;
maxy: number;
minz: number;
maxz: number;
} | null = null;

// --- Map ---

const map = new maplibregl.Map({
Expand Down Expand Up @@ -148,6 +163,25 @@ function applyFilter() {
];
}

if (bboxBounds) {
const bbox: Record<string, number> = {};
if (filterState.bboxMinX > bboxBounds.minx)
bbox.minx = filterState.bboxMinX;
if (filterState.bboxMaxX < bboxBounds.maxx)
bbox.maxx = filterState.bboxMaxX;
if (filterState.bboxMinY > bboxBounds.miny)
bbox.miny = filterState.bboxMinY;
if (filterState.bboxMaxY < bboxBounds.maxy)
bbox.maxy = filterState.bboxMaxY;
if (filterState.bboxMinZ > bboxBounds.minz)
bbox.minz = filterState.bboxMinZ;
if (filterState.bboxMaxZ < bboxBounds.maxz)
bbox.maxz = filterState.bboxMaxZ;
if (Object.keys(bbox).length > 0) {
filter.bbox = bbox;
}
}

copcLayer.setFilter(filter);
}

Expand Down Expand Up @@ -176,7 +210,15 @@ function loadCopc() {
debug: true,
alwaysShowRoot: true,
onInitialized: (message) => {
map.flyTo({ center: message.center, zoom: 16 });
const { bounds } = message;
map.flyTo({
center: [
(bounds.minx + bounds.maxx) / 2,
(bounds.miny + bounds.maxy) / 2,
],
zoom: 16,
});
setupBboxSliders(bounds);
},
});

Expand Down Expand Up @@ -274,6 +316,57 @@ for (const code of Object.keys(CLASSIFICATION_LABELS)) {
.onChange(applyFilter);
}

let bboxFolder = filterFolder.addFolder('Bbox (WGS84)');

function setupBboxSliders(bounds: {
minx: number;
maxx: number;
miny: number;
maxy: number;
minz: number;
maxz: number;
}) {
bboxBounds = bounds;
filterState.bboxMinX = bounds.minx;
filterState.bboxMaxX = bounds.maxx;
filterState.bboxMinY = bounds.miny;
filterState.bboxMaxY = bounds.maxy;
filterState.bboxMinZ = bounds.minz;
filterState.bboxMaxZ = bounds.maxz;

bboxFolder.destroy();
bboxFolder = filterFolder.addFolder('Bbox (WGS84)');

const lngStep = (bounds.maxx - bounds.minx) / 1000;
const latStep = (bounds.maxy - bounds.miny) / 1000;
const zStep = Math.max(0.1, (bounds.maxz - bounds.minz) / 1000);

bboxFolder
.add(filterState, 'bboxMinX', bounds.minx, bounds.maxx, lngStep)
.name('Min X (lng)')
.onChange(applyFilter);
bboxFolder
.add(filterState, 'bboxMaxX', bounds.minx, bounds.maxx, lngStep)
.name('Max X (lng)')
.onChange(applyFilter);
bboxFolder
.add(filterState, 'bboxMinY', bounds.miny, bounds.maxy, latStep)
.name('Min Y (lat)')
.onChange(applyFilter);
bboxFolder
.add(filterState, 'bboxMaxY', bounds.miny, bounds.maxy, latStep)
.name('Max Y (lat)')
.onChange(applyFilter);
bboxFolder
.add(filterState, 'bboxMinZ', bounds.minz, bounds.maxz, zStep)
.name('Min Z (m)')
.onChange(applyFilter);
bboxFolder
.add(filterState, 'bboxMaxZ', bounds.minz, bounds.maxz, zStep)
.name('Max Z (m)')
.onChange(applyFilter);
}

const intensityFolder = filterFolder.addFolder('Intensity');
intensityFolder
.add(filterState, 'intensityMin', 0, 1, 0.01)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "maplibre-copc-layer",
"type": "module",
"version": "0.1.4",
"version": "0.1.5",
"repository": {
"type": "git",
"url": "https://github.com/Kanahiro/maplibre-copc-layer.git"
Expand Down
79 changes: 78 additions & 1 deletion src/copclayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ export type ColorMode =
| 'classification'
| 'white';

export interface BboxFilter {
minx?: number;
maxx?: number;
miny?: number;
maxy?: number;
minz?: number;
maxz?: number;
}

export interface PointFilter {
classification?: Set<number>;
intensityRange?: [number, number];
bbox?: BboxFilter;
}

export interface CopcLayerOptions {
Expand All @@ -40,7 +50,14 @@ export interface CopcLayerOptions {
edlRadius?: number;
onInitialized?: (message: {
nodeCount: number;
center: [number, number];
bounds: {
minx: number;
maxx: number;
miny: number;
maxy: number;
minz: number;
maxz: number;
};
}) => void;
}

Expand Down Expand Up @@ -635,8 +652,53 @@ export class CopcLayer implements maplibregl.CustomLayerInterface {
this.edlMaterial.uniforms.screenSize.value.set(width, height);
}

private lngLatToMercator(
lng: number,
lat: number,
height: number,
): [number, number, number] {
const latRad = lat * DEG2RAD;
const sinLat = Math.sin(latRad);
const mercX = 0.5 + lng / 360;
const mercY =
0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI);
const mercZ = height / EARTH_CIRCUMFERENCE;
return [mercX, mercY, mercZ];
}

private getBboxMercator(): {
min: [number, number, number];
max: [number, number, number];
} | null {
const bbox = this.options.filter.bbox;
if (!bbox) return null;
const hasAny =
bbox.minx !== undefined ||
bbox.maxx !== undefined ||
bbox.miny !== undefined ||
bbox.maxy !== undefined ||
bbox.minz !== undefined ||
bbox.maxz !== undefined;
if (!hasAny) return null;

const minLng = bbox.minx ?? -180;
const maxLng = bbox.maxx ?? 180;
const minLat = bbox.miny ?? -85;
const maxLat = bbox.maxy ?? 85;
const minZ = bbox.minz ?? -1e10;
const maxZ = bbox.maxz ?? 1e10;

const minMerc = this.lngLatToMercator(minLng, maxLat, minZ);
const maxMerc = this.lngLatToMercator(maxLng, minLat, maxZ);

return { min: minMerc, max: maxMerc };
}

private createPointMaterial(): THREE.ShaderMaterial {
const filter = this.options.filter;
const bboxMerc = this.getBboxMercator();
const sc = this.sceneCenter;

return new THREE.ShaderMaterial({
uniforms: {
size: { value: this.options.pointSize },
Expand All @@ -656,6 +718,21 @@ export class CopcLayer implements maplibregl.CustomLayerInterface {
useIntensityFilter: {
value: filter.intensityRange !== undefined,
},
useBboxFilter: { value: bboxMerc !== null },
bboxMin: {
value: new THREE.Vector3(
bboxMerc ? bboxMerc.min[0] - (sc?.x ?? 0) : 0,
bboxMerc ? bboxMerc.min[1] - (sc?.y ?? 0) : 0,
bboxMerc ? bboxMerc.min[2] - (sc?.z ?? 0) : 0,
),
},
bboxMax: {
value: new THREE.Vector3(
bboxMerc ? bboxMerc.max[0] - (sc?.x ?? 0) : 0,
bboxMerc ? bboxMerc.max[1] - (sc?.y ?? 0) : 0,
bboxMerc ? bboxMerc.max[2] - (sc?.z ?? 0) : 0,
),
},
},
vertexShader: pointsVertexShader,
fragmentShader: pointsFragmentShader,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
type ColorMode,
type NodeStats,
type PointFilter,
type BboxFilter,
} from './copclayer';
export {
CacheManager,
Expand Down
12 changes: 12 additions & 0 deletions src/shaders/points.vert.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ uniform vec2 intensityRange;
uniform bool useClassificationFilter;
uniform bool useIntensityFilter;

uniform bool useBboxFilter;
uniform vec3 bboxMin;
uniform vec3 bboxMax;

#ifdef USE_COLOR
varying vec3 vColor;
#endif
Expand All @@ -31,6 +35,14 @@ void main() {
}
}

if (useBboxFilter) {
if (position.x < bboxMin.x || position.x > bboxMax.x ||
position.y < bboxMin.y || position.y > bboxMax.y ||
position.z < bboxMin.z || position.z > bboxMax.z) {
vFiltered = 1.0;
}
}

#ifdef USE_COLOR
vColor = color;
#endif
Expand Down
28 changes: 21 additions & 7 deletions src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,31 @@ async function initCopc(initUrl: string) {
nodeCenters[k] = calcCubeCenter(copc.info.cube, k);
}

const rootCenter = nodeCenters['0-0-0-0'];
const rootCenterLngLat = proj.inverse([
rootCenter[0],
rootCenter[1],
rootCenter[2],
]);
const cube = copc.info.cube;
const cubeCorners = [
[cube[0], cube[1], cube[2]],
[cube[3], cube[1], cube[2]],
[cube[0], cube[4], cube[2]],
[cube[3], cube[4], cube[2]],
[cube[0], cube[1], cube[5]],
[cube[3], cube[1], cube[5]],
[cube[0], cube[4], cube[5]],
[cube[3], cube[4], cube[5]],
];
const wgs84Corners = cubeCorners.map((c) => proj.inverse(c));
const bounds = {
minx: Math.min(...wgs84Corners.map((c) => c[0])),
maxx: Math.max(...wgs84Corners.map((c) => c[0])),
miny: Math.min(...wgs84Corners.map((c) => c[1])),
maxy: Math.max(...wgs84Corners.map((c) => c[1])),
minz: Math.min(...wgs84Corners.map((c) => c[2])),
maxz: Math.max(...wgs84Corners.map((c) => c[2])),
};

self.postMessage({
type: 'initialized',
center: rootCenterLngLat,
nodeCount: Object.keys(nodes).length,
bounds,
});
} catch (error) {
self.postMessage({
Expand Down
Loading