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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 27 additions & 0 deletions deployment-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,33 @@
},
"projectPath": "./dist/playgrounds/cesium-reference"
},
"annotations": {
"deployment": {
"manual": {
"dev": {
"pages": {
"org": "carma-dev-playground-deployments",
"prj": "annotations"
}
},
"live": {
"pages": {
"org": "carma-dev-playground-deployments",
"prj": "annotations"
}
}
},
"auto": {
"dev": {
"pages": {
"org": "carma-dev-playground-deployments",
"prj": "annotations"
}
}
}
},
"projectPath": "./dist/playgrounds/annotations"
},
"vector": {
"deployment": {
"manual": {
Expand Down
1 change: 1 addition & 0 deletions libraries/commons/math/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from "./easingFunctions";
export * from "./scaling";
export * from "./interpolation";
export * from "./geometry2d";
export * from "./vec3";
export * from "./trig";
89 changes: 89 additions & 0 deletions libraries/commons/math/src/lib/vec3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Ray, Vector3 } from "three";

export type PlaneBasis3 = {
xAxis: Vector3;
yAxis: Vector3;
};

export const VEC3_NUMERIC_EPSILON = 1e-6;

export const getClosestLineParamToRay = (
ray: Ray,
lineOrigin: Vector3,
lineDirection: Vector3,
epsilon: number = VEC3_NUMERIC_EPSILON
): number => {
const rayDirection = ray.direction.clone();
const normalizedLineDirection = lineDirection.clone();

if (
rayDirection.lengthSq() <= epsilon ||
normalizedLineDirection.lengthSq() <= epsilon
) {
return 0;
}

rayDirection.normalize();
normalizedLineDirection.normalize();

const originDelta = ray.origin.clone().sub(lineOrigin);

const a = rayDirection.dot(rayDirection);
const b = rayDirection.dot(normalizedLineDirection);
const c = normalizedLineDirection.dot(normalizedLineDirection);
const d = rayDirection.dot(originDelta);
const e = normalizedLineDirection.dot(originDelta);
const denominator = a * c - b * b;

if (Math.abs(denominator) < epsilon) {
return e;
}

return (a * e - b * d) / denominator;
};

export const intersectRayWithPlane = (
ray: Ray,
planeOrigin: Vector3,
planeNormal: Vector3,
epsilon: number = VEC3_NUMERIC_EPSILON
): Vector3 | null => {
const denominator = ray.direction.dot(planeNormal);
if (Math.abs(denominator) <= epsilon) return null;

const originToPlane = planeOrigin.clone().sub(ray.origin);
const t = originToPlane.dot(planeNormal) / denominator;
if (!Number.isFinite(t)) return null;

return ray.origin.clone().add(ray.direction.clone().multiplyScalar(t));
};

export const createPlaneBasisFromNormal = (
normal: Vector3,
epsilon: number = VEC3_NUMERIC_EPSILON
): PlaneBasis3 => {
const up =
normal.lengthSq() > epsilon
? normal.clone().normalize()
: new Vector3(0, 0, 1);
const reference =
Math.abs(up.dot(new Vector3(0, 0, 1))) > 0.9
? new Vector3(1, 0, 0)
: new Vector3(0, 0, 1);

const xAxis = up.clone().cross(reference);
if (xAxis.lengthSq() > epsilon) {
xAxis.normalize();
} else {
xAxis.set(1, 0, 0);
}

const yAxis = xAxis.clone().cross(up);
if (yAxis.lengthSq() > epsilon) {
yAxis.normalize();
} else {
yAxis.set(0, 1, 0);
}

return { xAxis, yAxis };
};
16 changes: 16 additions & 0 deletions libraries/commons/react-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# react-store

A tiny React-friendly external store helper for local monorepo state.

It provides:
- `createStore(initialState)`
- `useStoreSelector(store, selector)`
- `useStoreValue(store)`

The goal is to keep store usage explicit and small:
- one store object
- stable `getState` / `setState` / `subscribe` API
- React reads through `useSyncExternalStore`

This package is intended for internal monorepo use where a lightweight store is
more appropriate than introducing or coupling to a larger state library.
7 changes: 7 additions & 0 deletions libraries/commons/react-store/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@carma-commons/react-store",
"version": "0.0.1",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts"
}
27 changes: 27 additions & 0 deletions libraries/commons/react-store/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "react-store",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libraries/commons/react-store/src",
"projectType": "library",
"tags": ["type:util", "scope:commons", "scope:react"],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libraries/commons/react-store"
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{workspaceRoot}/dist/.vite/react-store"],
"options": {
"config": "libraries/commons/react-store/vite.config.ts",
"passWithNoTests": false
}
}
}
}
1 change: 1 addition & 0 deletions libraries/commons/react-store/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./lib";
2 changes: 2 additions & 0 deletions libraries/commons/react-store/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./store";
export * from "./useStoreSelector";
61 changes: 61 additions & 0 deletions libraries/commons/react-store/src/lib/store.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it, vi } from "vitest";

import { createStore } from "./store";

describe("createStore", () => {
it("returns the initial state", () => {
const store = createStore({ count: 1 });

expect(store.getState()).toEqual({ count: 1 });
});

it("updates state from a plain value", () => {
const store = createStore({ count: 1 });

store.setState({ count: 2 });

expect(store.getState()).toEqual({ count: 2 });
});

it("updates state from an updater function", () => {
const store = createStore({ count: 1 });

store.setState((previousState) => ({
count: previousState.count + 1,
}));

expect(store.getState()).toEqual({ count: 2 });
});

it("notifies subscribers when the state changes", () => {
const store = createStore({ count: 1 });
const listener = vi.fn();

store.subscribe(listener);
store.setState({ count: 2 });

expect(listener).toHaveBeenCalledTimes(1);
});

it("does not notify subscribers when the next state is identical", () => {
const state = { count: 1 };
const store = createStore(state);
const listener = vi.fn();

store.subscribe(listener);
store.setState(state);

expect(listener).not.toHaveBeenCalled();
});

it("stops notifying unsubscribed listeners", () => {
const store = createStore({ count: 1 });
const listener = vi.fn();

const unsubscribe = store.subscribe(listener);
unsubscribe();
store.setState({ count: 2 });

expect(listener).not.toHaveBeenCalled();
});
});
58 changes: 58 additions & 0 deletions libraries/commons/react-store/src/lib/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export type StoreListener = () => void;

export type StoreUpdater<TState> = TState | ((previousState: TState) => TState);

export type ReadonlyStore<TState> = {
getState: () => TState;
subscribe: (listener: StoreListener) => () => void;
};

export type Store<TState> = ReadonlyStore<TState> & {
setState: (updater: StoreUpdater<TState>) => void;
};

const resolveNextState = <TState>(
currentState: TState,
updater: StoreUpdater<TState>
): TState => {
if (typeof updater === "function") {
return (updater as (previousState: TState) => TState)(currentState);
}

return updater;
};

export const createStore = <TState>(initialState: TState): Store<TState> => {
let currentState = initialState;
const listeners = new Set<StoreListener>();

const getState = () => currentState;

const subscribe = (listener: StoreListener) => {
listeners.add(listener);

return () => {
listeners.delete(listener);
};
};

const setState = (updater: StoreUpdater<TState>) => {
const nextState = resolveNextState(currentState, updater);

if (Object.is(nextState, currentState)) {
return;
}

currentState = nextState;

for (const listener of [...listeners]) {
listener();
}
};

return {
getState,
subscribe,
setState,
};
};
64 changes: 64 additions & 0 deletions libraries/commons/react-store/src/lib/useStoreSelector.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { createStore } from "./store";
import { useStoreSelector, useStoreValue } from "./useStoreSelector";

describe("useStoreSelector", () => {
it("returns the selected store state", () => {
const store = createStore({ count: 1, label: "one" });

const { result } = renderHook(() =>
useStoreSelector(store, (state) => state.count)
);

expect(result.current).toBe(1);
});

it("updates when the selected snapshot changes", () => {
const store = createStore({ count: 1, label: "one" });

const { result } = renderHook(() =>
useStoreSelector(store, (state) => state.count)
);

act(() => {
store.setState((previousState) => ({
...previousState,
count: previousState.count + 1,
}));
});

expect(result.current).toBe(2);
});

it("does not force a rerender when the selected snapshot is unchanged", () => {
const store = createStore({ count: 1, label: "one" });
let renderCount = 0;

const { result } = renderHook(() => {
renderCount += 1;
return useStoreSelector(store, (state) => state.count);
});

act(() => {
store.setState((previousState) => ({
...previousState,
label: "two",
}));
});

expect(result.current).toBe(1);
expect(renderCount).toBe(1);
});
});

describe("useStoreValue", () => {
it("returns the full store state", () => {
const store = createStore({ count: 1, label: "one" });

const { result } = renderHook(() => useStoreValue(store));

expect(result.current).toEqual({ count: 1, label: "one" });
});
});
18 changes: 18 additions & 0 deletions libraries/commons/react-store/src/lib/useStoreSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useSyncExternalStore } from "react";

import type { ReadonlyStore } from "./store";

const identity = <TValue>(value: TValue) => value;

export const useStoreSelector = <TState, TSelected>(
store: ReadonlyStore<TState>,
selector: (state: TState) => TSelected
): TSelected =>
useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getState())
);

export const useStoreValue = <TState>(store: ReadonlyStore<TState>): TState =>
useStoreSelector(store, identity);
Loading
Loading