init commit
This commit is contained in:
commit
1b863fccfe
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
Copyright 2025 yazmeyaa
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
### TS SparseSet
|
||||||
|
|
||||||
|
[![NPM version][npm-image]][npm-url]
|
||||||
|
[![Downloads][downloads-image]][downloads-url]
|
||||||
|
|
||||||
|
**TS SparseSet** — это высокопроизводительная TypeScript структура данных, реализующая *sparse set*, позволяющая хранить и быстро управлять ID-ориентированными коллекциями и компонентами ECS.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* **O(1) Operations:** `add`, `remove`, `get`, and `has` с плотным хранением элементов.
|
||||||
|
* **Dense Storage:** Эффективная плотная память для максимальной производительности и locality.
|
||||||
|
* **Swap-Remove:** При удалении элементов массив остаётся компактным.
|
||||||
|
* **Reusable Structure:** Может использоваться для ECS-компонентов, сущностей, пулов объектов или других ID-ориентированных коллекций.
|
||||||
|
* **Optional Object Pool Integration:** Лёгкая интеграция с пулами объектов для минимизации аллокаций.
|
||||||
|
* **TypeScript Native:** Полная типизация и совместимость с современным TS.
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install ts-sparse-set
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SparseSet } from 'ts-sparse-set';
|
||||||
|
|
||||||
|
// Создаём новый SparseSet для чисел
|
||||||
|
const set = new SparseSet<number>();
|
||||||
|
|
||||||
|
// Добавляем элементы
|
||||||
|
set.add(10, 42);
|
||||||
|
set.add(20, 100);
|
||||||
|
|
||||||
|
// Проверяем наличие
|
||||||
|
console.log(set.has(10)); // true
|
||||||
|
console.log(set.has(5)); // false
|
||||||
|
|
||||||
|
// Получаем значение
|
||||||
|
console.log(set.get(20)); // 100
|
||||||
|
|
||||||
|
// Перезаписываем значение
|
||||||
|
set.add(10, 50);
|
||||||
|
console.log(set.get(10)); // 50
|
||||||
|
|
||||||
|
// Удаляем элемент
|
||||||
|
set.remove(20);
|
||||||
|
console.log(set.has(20)); // false
|
||||||
|
|
||||||
|
// Добавляем несколько элементов и проверяем плотность
|
||||||
|
set.add(30, 300);
|
||||||
|
set.add(40, 400);
|
||||||
|
set.remove(10);
|
||||||
|
console.log(set.get(30)); // 300
|
||||||
|
console.log(set.get(40)); // 400
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Documentation
|
||||||
|
|
||||||
|
* **SparseSet Class**
|
||||||
|
|
||||||
|
* **Constructor:** `new SparseSet<V>()`
|
||||||
|
* **Methods:**
|
||||||
|
|
||||||
|
* `add(id: number, value: V): V` — добавляет новый элемент или обновляет существующий.
|
||||||
|
* `get(id: number): V | null` — возвращает элемент по ID или `null`.
|
||||||
|
* `has(id: number): boolean` — проверяет наличие элемента.
|
||||||
|
* `remove(id: number): void` — удаляет элемент и поддерживает плотность dense массива.
|
||||||
|
|
||||||
|
#### Note
|
||||||
|
|
||||||
|
SparseSet идеально подходит для ECS-сценариев, где сущности и компоненты идентифицируются числами. Эта структура не является обычным Map/Set и оптимизирована под плотные массивы с минимальной стоимостью операций.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Feel free to contribute, report issues, or suggest improvements on [GitHub](https://github.com/yazmeyaa/ts-sparse-set).
|
||||||
|
|
||||||
|
[npm-image]: https://img.shields.io/npm/v/ts-sparse-set.svg?style=flat-square
|
||||||
|
[npm-url]: https://npmjs.org/package/ts-sparse-set
|
||||||
|
[downloads-image]: https://img.shields.io/npm/dm/ts-sparse-set.svg?style=flat-square
|
||||||
|
[downloads-url]: https://npmjs.org/package/ts-sparse-set
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
||||||
|
export default {
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
"^.+\.tsx?$": ["ts-jest",{}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"name": "ts-sparse-set",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"author": "yazmeyaa",
|
||||||
|
"description": "A high-performance, TypeScript-native SparseSet implementation for ECS and other ID-based collections. Provides O(1) add, remove, get, and has operations with dense storage and optional object pooling for components.",
|
||||||
|
"homepage": "https://github.com/yazmeyaa/ts-sparse-set#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/yazmeyaa/ts-sparse-set/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/yazmeyaa/ts-sparse-set.git"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.umd.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.umd.cjs"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext .ts",
|
||||||
|
"lint:fix": "eslint src --ext .ts --fix",
|
||||||
|
"format": "prettier --write ./src/**/*.ts",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^30.2.0",
|
||||||
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||||
|
"@typescript-eslint/parser": "^8.48.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"prettier": "^3.7.3",
|
||||||
|
"rollup-plugin-typescript-paths": "^1.5.0",
|
||||||
|
"ts-jest": "^29.4.6",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.2.4",
|
||||||
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
export { SparseSet } from "./sparse_set";
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { SparseSet } from "./sparse_set";
|
||||||
|
|
||||||
|
describe("SparseSet", () => {
|
||||||
|
let set: SparseSet<number>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
set = new SparseSet<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("add and get component", () => {
|
||||||
|
set.add(5, 42);
|
||||||
|
expect(set.get(5)).toBe(42);
|
||||||
|
expect(set.has(5)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has returns false for missing element", () => {
|
||||||
|
expect(set.has(1)).toBe(false);
|
||||||
|
expect(set.get(1)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overwrite existing component", () => {
|
||||||
|
set.add(3, 10);
|
||||||
|
set.add(3, 20);
|
||||||
|
expect(set.get(3)).toBe(20);
|
||||||
|
expect(set.has(3)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("remove element", () => {
|
||||||
|
set.add(1, 100);
|
||||||
|
set.add(2, 200);
|
||||||
|
set.add(3, 300);
|
||||||
|
|
||||||
|
set.remove(2);
|
||||||
|
expect(set.has(2)).toBe(false);
|
||||||
|
expect(set.get(2)).toBeNull();
|
||||||
|
|
||||||
|
expect(set.has(1)).toBe(true);
|
||||||
|
expect(set.has(3)).toBe(true);
|
||||||
|
expect(set.get(1)).toBe(100);
|
||||||
|
expect(set.get(3)).toBe(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("remove last element", () => {
|
||||||
|
set.add(7, 77);
|
||||||
|
set.remove(7);
|
||||||
|
expect(set.has(7)).toBe(false);
|
||||||
|
expect(set.get(7)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swap-remove maintains correct mapping", () => {
|
||||||
|
set.add(10, 100);
|
||||||
|
set.add(20, 200);
|
||||||
|
set.add(30, 300);
|
||||||
|
|
||||||
|
set.remove(20);
|
||||||
|
|
||||||
|
expect(set.has(10)).toBe(true);
|
||||||
|
expect(set.get(10)).toBe(100);
|
||||||
|
|
||||||
|
expect(set.has(30)).toBe(true);
|
||||||
|
expect(set.get(30)).toBe(300);
|
||||||
|
|
||||||
|
const denseIds = [10, 30].map((id) => set.get(id));
|
||||||
|
expect(denseIds).toEqual([100, 300]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sparse dynamically grows", () => {
|
||||||
|
set.add(50, 500);
|
||||||
|
expect(set.has(50)).toBe(true);
|
||||||
|
expect(set.get(50)).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removing non-existent element does not crash", () => {
|
||||||
|
set.add(1, 10);
|
||||||
|
expect(() => set.remove(99)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adding multiple elements and removing all", () => {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
set.add(i, i * 10);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
expect(set.has(i)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
set.remove(i);
|
||||||
|
expect(set.has(i)).toBe(false);
|
||||||
|
expect(set.get(i)).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
export class SparseSet<V> {
|
||||||
|
private sparse: number[] = [];
|
||||||
|
private dense: number[] = [];
|
||||||
|
private data: V[] = [];
|
||||||
|
|
||||||
|
public has(id: number): boolean {
|
||||||
|
const index = this.sparse[id];
|
||||||
|
return (
|
||||||
|
index !== undefined &&
|
||||||
|
index < this.dense.length &&
|
||||||
|
this.dense[index] === id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(id: number): V | null {
|
||||||
|
const index = this.sparse[id];
|
||||||
|
if (
|
||||||
|
index === undefined ||
|
||||||
|
index >= this.dense.length ||
|
||||||
|
this.dense[index] !== id
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(id: number, value: V): V {
|
||||||
|
const existing = this.sparse[id];
|
||||||
|
if (
|
||||||
|
existing !== undefined &&
|
||||||
|
existing < this.dense.length &&
|
||||||
|
this.dense[existing] === id
|
||||||
|
) {
|
||||||
|
this.data[existing] = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.dense.length;
|
||||||
|
this.dense[index] = id;
|
||||||
|
this.data[index] = value;
|
||||||
|
|
||||||
|
if (this.sparse.length <= id) {
|
||||||
|
this.sparse.length = id + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sparse[id] = index;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(id: number): void {
|
||||||
|
const index = this.sparse[id];
|
||||||
|
if (
|
||||||
|
index === undefined ||
|
||||||
|
index >= this.dense.length ||
|
||||||
|
this.dense[index] !== id
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = this.dense.length - 1;
|
||||||
|
const lastId = this.dense[lastIndex];
|
||||||
|
|
||||||
|
if (index !== lastIndex) {
|
||||||
|
this.dense[index] = lastId;
|
||||||
|
this.data[index] = this.data[lastIndex];
|
||||||
|
this.sparse[lastId] = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dense.pop();
|
||||||
|
this.data.pop();
|
||||||
|
delete this.sparse[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import path from "path";
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
const libConfig = defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, "src/index.ts"),
|
||||||
|
fileName: "index",
|
||||||
|
name: 'bitmap-index'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [dts({exclude: "**/*.test.ts"})],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default libConfig
|
||||||
Loading…
Reference in New Issue