M1-4: Length axis (TDD)
- Add axis.js: length axis rectangle state, 28 tests - 71/71 unit tests green
This commit is contained in:
Vendored
+454
@@ -0,0 +1,454 @@
|
||||
|
||||
// this file is generated — do not edit it
|
||||
|
||||
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables that are injected _statically_ into your bundle at build time and are limited to _private_ access.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Static environment variables are [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env` at build time and then statically injected into your bundle at build time, enabling optimisations like dead code elimination.
|
||||
*
|
||||
* **_Private_ access:**
|
||||
*
|
||||
* - This module cannot be imported into client-side code
|
||||
* - This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured)
|
||||
*
|
||||
* For example, given the following build time environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://site.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { ENVIRONMENT, PUBLIC_BASE_URL } from '$env/static/private';
|
||||
*
|
||||
* console.log(ENVIRONMENT); // => "production"
|
||||
* console.log(PUBLIC_BASE_URL); // => throws error during build
|
||||
* ```
|
||||
*
|
||||
* The above values will be the same _even if_ different values for `ENVIRONMENT` or `PUBLIC_BASE_URL` are set at runtime, as they are statically replaced in your code with their build time values.
|
||||
*/
|
||||
declare module '$env/static/private' {
|
||||
export const LESSOPEN: string;
|
||||
export const TMUX: string;
|
||||
export const HERMES_DELEGATED_CHILD_CONTEXT: string;
|
||||
export const timezone: string;
|
||||
export const CUDACXX: string;
|
||||
export const BROWSERBASE_PROXIES: string;
|
||||
export const HERMES_RESTART_DRAIN_TIMEOUT: string;
|
||||
export const _HERMES_GATEWAY: string;
|
||||
export const AI_AGENT: string;
|
||||
export const USER: string;
|
||||
export const SSH_CLIENT: string;
|
||||
export const npm_config_user_agent: string;
|
||||
export const TERMINAL_CONTAINER_CPU: string;
|
||||
export const AGENT_BROWSER_EXECUTABLE_PATH: string;
|
||||
export const TERMINAL_DOCKER_ENV: string;
|
||||
export const TERMINAL_CWD: string;
|
||||
export const HERMES_REAL_HOME: string;
|
||||
export const XDG_SESSION_TYPE: string;
|
||||
export const npm_node_execpath: string;
|
||||
export const SHLVL: string;
|
||||
export const HERMES_PROFILE: string;
|
||||
export const npm_config_noproxy: string;
|
||||
export const HOME: string;
|
||||
export const OLDPWD: string;
|
||||
export const TERMINAL_DOCKER_FORWARD_ENV: string;
|
||||
export const TERM_PROGRAM_VERSION: string;
|
||||
export const BROWSER_SESSION_TIMEOUT: string;
|
||||
export const HERMES_KANBAN_BOARD: string;
|
||||
export const SSH_TTY: string;
|
||||
export const HERMES_HOME: string;
|
||||
export const npm_package_json: string;
|
||||
export const TERMINAL_DOCKER_EXTRA_ARGS: string;
|
||||
export const HF_HUB_DISABLE_XET: string;
|
||||
export const IM_CONFIG_ENTRY: string;
|
||||
export const TERMINAL_DOCKER_IMAGE: string;
|
||||
export const PAGER: string;
|
||||
export const TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE: string;
|
||||
export const TERMINAL_CONTAINER_MEMORY: string;
|
||||
export const SSL_CERT_FILE: string;
|
||||
export const TERMINAL_CONTAINER_PERSISTENT: string;
|
||||
export const npm_config_userconfig: string;
|
||||
export const npm_config_local_prefix: string;
|
||||
export const HERMES_TURN_LEASE_TIMEOUT: string;
|
||||
export const HERMES_MEDIA_DELIVERY_STRICT: string;
|
||||
export const TERMINAL_MODAL_IMAGE: string;
|
||||
export const DBUS_SESSION_BUS_ADDRESS: string;
|
||||
export const COLORTERM: string;
|
||||
export const TERMINAL_DOCKER_NETWORK: string;
|
||||
export const file_read_max_chars: string;
|
||||
export const COLOR: string;
|
||||
export const DEBUGINFOD_URLS: string;
|
||||
export const TERMINAL_DOCKER_VOLUMES: string;
|
||||
export const TERMINAL_ENV: string;
|
||||
export const TERMINAL_VERCEL_RUNTIME: string;
|
||||
export const LOGNAME: string;
|
||||
export const HERMES_KANBAN_DB: string;
|
||||
export const IMAGE_TOOLS_DEBUG: string;
|
||||
export const _: string;
|
||||
export const npm_config_prefix: string;
|
||||
export const npm_config_npm_version: string;
|
||||
export const group_sessions_per_user: string;
|
||||
export const HERMES_KANBAN_WORKSPACES_ROOT: string;
|
||||
export const HERMES_SINGLE_QUERY_SESSION: string;
|
||||
export const XDG_SESSION_CLASS: string;
|
||||
export const BROWSERBASE_ADVANCED_STEALTH: string;
|
||||
export const paste_collapse_threshold: string;
|
||||
export const TERM: string;
|
||||
export const TERMINAL_DAYTONA_IMAGE: string;
|
||||
export const XDG_SESSION_ID: string;
|
||||
export const npm_config_cache: string;
|
||||
export const TERMINAL_TEMP_DIR: string;
|
||||
export const TERMINAL_LIFETIME_SECONDS: string;
|
||||
export const TERMINAL_TIMEOUT: string;
|
||||
export const HERMES_GATEWAY_BUSY_INPUT_MODE: string;
|
||||
export const paste_collapse_char_threshold: string;
|
||||
export const TERMINAL_DOCKER_SHARED_CONTAINER_KEY: string;
|
||||
export const npm_config_node_gyp: string;
|
||||
export const PATH: string;
|
||||
export const HERMES_AGENT_TIMEOUT: string;
|
||||
export const NODE: string;
|
||||
export const TERMINAL_DOCKER_SHM_SIZE: string;
|
||||
export const HERMES_KANBAN_WORKSPACE: string;
|
||||
export const XDG_RUNTIME_DIR: string;
|
||||
export const TERMINAL_PERSISTENT_SHELL: string;
|
||||
export const LANG: string;
|
||||
export const HERMES_QUIET: string;
|
||||
export const MOA_TOOLS_DEBUG: string;
|
||||
export const HERMES_MEDIA_TRUST_RECENT_FILES: string;
|
||||
export const LS_COLORS: string;
|
||||
export const HERMES_MEDIA_TRUST_RECENT_SECONDS: string;
|
||||
export const HERMES_MAX_ITERATIONS: string;
|
||||
export const TERM_PROGRAM: string;
|
||||
export const npm_lifecycle_script: string;
|
||||
export const HERMES_SESSION_ID: string;
|
||||
export const HERMES_AGENT_TIMEOUT_WARNING: string;
|
||||
export const prefill_messages_file: string;
|
||||
export const SHELL: string;
|
||||
export const HERMES_AGENT_NOTIFY_INTERVAL: string;
|
||||
export const TERMINAL_DEGRADED_MODE: string;
|
||||
export const TERMINAL_MODAL_MODE: string;
|
||||
export const npm_lifecycle_event: string;
|
||||
export const WEB_TOOLS_DEBUG: string;
|
||||
export const VISION_TOOLS_DEBUG: string;
|
||||
export const LESSCLOSE: string;
|
||||
export const TERMINAL_CONTAINER_DISK: string;
|
||||
export const TERMINAL_DOCKER_RUN_AS_HOST_USER: string;
|
||||
export const GIT_PAGER: string;
|
||||
export const paste_collapse_threshold_fallback: string;
|
||||
export const npm_config_globalconfig: string;
|
||||
export const npm_config_init_module: string;
|
||||
export const PWD: string;
|
||||
export const HERMES_INTERACTIVE: string;
|
||||
export const _config_version: string;
|
||||
export const HERMES_AUTO_CONTINUE_FRESHNESS: string;
|
||||
export const npm_execpath: string;
|
||||
export const SSH_CONNECTION: string;
|
||||
export const XDG_DATA_DIRS: string;
|
||||
export const npm_config_global_prefix: string;
|
||||
export const hooks_auto_accept: string;
|
||||
export const npm_command: string;
|
||||
export const TMUX_PLUGIN_MANAGER_PATH: string;
|
||||
export const BROWSER_INACTIVITY_TIMEOUT: string;
|
||||
export const HERMES_REDACT_SECRETS: string;
|
||||
export const HERMES_CUSTOM_192_168_1_203_8084_API_KEY: string;
|
||||
export const TERMINAL_SINGULARITY_IMAGE: string;
|
||||
export const TMUX_PANE: string;
|
||||
export const TERMINAL_HOME_MODE: string;
|
||||
export const TERMINAL_DOCKER_SNAP_COMPAT: string;
|
||||
export const HERMES_AGENT: string;
|
||||
export const HERMES_SESSION_SOURCE: string;
|
||||
export const INIT_CWD: string;
|
||||
export const EDITOR: string;
|
||||
export const TEST: string;
|
||||
export const VITEST: string;
|
||||
export const NODE_ENV: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables that are injected _statically_ into your bundle at build time and are _publicly_ accessible.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Static environment variables are [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env` at build time and then statically injected into your bundle at build time, enabling optimisations like dead code elimination.
|
||||
*
|
||||
* **_Public_ access:**
|
||||
*
|
||||
* - This module _can_ be imported into client-side code
|
||||
* - **Only** variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`) are included
|
||||
*
|
||||
* For example, given the following build time environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://site.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { ENVIRONMENT, PUBLIC_BASE_URL } from '$env/static/public';
|
||||
*
|
||||
* console.log(ENVIRONMENT); // => throws error during build
|
||||
* console.log(PUBLIC_BASE_URL); // => "http://site.com"
|
||||
* ```
|
||||
*
|
||||
* The above values will be the same _even if_ different values for `ENVIRONMENT` or `PUBLIC_BASE_URL` are set at runtime, as they are statically replaced in your code with their build time values.
|
||||
*/
|
||||
declare module '$env/static/public' {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables set _dynamically_ at runtime and that are limited to _private_ access.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Dynamic environment variables are defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node) (or running [`vite preview`](https://svelte.dev/docs/kit/cli)), this is equivalent to `process.env`.
|
||||
*
|
||||
* **_Private_ access:**
|
||||
*
|
||||
* - This module cannot be imported into client-side code
|
||||
* - This module includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured)
|
||||
*
|
||||
* > [!NOTE] In `dev`, `$env/dynamic` includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
|
||||
*
|
||||
* > [!NOTE] To get correct types, environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
|
||||
* >
|
||||
* > ```env
|
||||
* > MY_FEATURE_FLAG=
|
||||
* > ```
|
||||
* >
|
||||
* > You can override `.env` values from the command line like so:
|
||||
* >
|
||||
* > ```sh
|
||||
* > MY_FEATURE_FLAG="enabled" npm run dev
|
||||
* > ```
|
||||
*
|
||||
* For example, given the following runtime environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://site.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { env } from '$env/dynamic/private';
|
||||
*
|
||||
* console.log(env.ENVIRONMENT); // => "production"
|
||||
* console.log(env.PUBLIC_BASE_URL); // => undefined
|
||||
* ```
|
||||
*/
|
||||
declare module '$env/dynamic/private' {
|
||||
export const env: {
|
||||
LESSOPEN: string;
|
||||
TMUX: string;
|
||||
HERMES_DELEGATED_CHILD_CONTEXT: string;
|
||||
timezone: string;
|
||||
CUDACXX: string;
|
||||
BROWSERBASE_PROXIES: string;
|
||||
HERMES_RESTART_DRAIN_TIMEOUT: string;
|
||||
_HERMES_GATEWAY: string;
|
||||
AI_AGENT: string;
|
||||
USER: string;
|
||||
SSH_CLIENT: string;
|
||||
npm_config_user_agent: string;
|
||||
TERMINAL_CONTAINER_CPU: string;
|
||||
AGENT_BROWSER_EXECUTABLE_PATH: string;
|
||||
TERMINAL_DOCKER_ENV: string;
|
||||
TERMINAL_CWD: string;
|
||||
HERMES_REAL_HOME: string;
|
||||
XDG_SESSION_TYPE: string;
|
||||
npm_node_execpath: string;
|
||||
SHLVL: string;
|
||||
HERMES_PROFILE: string;
|
||||
npm_config_noproxy: string;
|
||||
HOME: string;
|
||||
OLDPWD: string;
|
||||
TERMINAL_DOCKER_FORWARD_ENV: string;
|
||||
TERM_PROGRAM_VERSION: string;
|
||||
BROWSER_SESSION_TIMEOUT: string;
|
||||
HERMES_KANBAN_BOARD: string;
|
||||
SSH_TTY: string;
|
||||
HERMES_HOME: string;
|
||||
npm_package_json: string;
|
||||
TERMINAL_DOCKER_EXTRA_ARGS: string;
|
||||
HF_HUB_DISABLE_XET: string;
|
||||
IM_CONFIG_ENTRY: string;
|
||||
TERMINAL_DOCKER_IMAGE: string;
|
||||
PAGER: string;
|
||||
TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE: string;
|
||||
TERMINAL_CONTAINER_MEMORY: string;
|
||||
SSL_CERT_FILE: string;
|
||||
TERMINAL_CONTAINER_PERSISTENT: string;
|
||||
npm_config_userconfig: string;
|
||||
npm_config_local_prefix: string;
|
||||
HERMES_TURN_LEASE_TIMEOUT: string;
|
||||
HERMES_MEDIA_DELIVERY_STRICT: string;
|
||||
TERMINAL_MODAL_IMAGE: string;
|
||||
DBUS_SESSION_BUS_ADDRESS: string;
|
||||
COLORTERM: string;
|
||||
TERMINAL_DOCKER_NETWORK: string;
|
||||
file_read_max_chars: string;
|
||||
COLOR: string;
|
||||
DEBUGINFOD_URLS: string;
|
||||
TERMINAL_DOCKER_VOLUMES: string;
|
||||
TERMINAL_ENV: string;
|
||||
TERMINAL_VERCEL_RUNTIME: string;
|
||||
LOGNAME: string;
|
||||
HERMES_KANBAN_DB: string;
|
||||
IMAGE_TOOLS_DEBUG: string;
|
||||
_: string;
|
||||
npm_config_prefix: string;
|
||||
npm_config_npm_version: string;
|
||||
group_sessions_per_user: string;
|
||||
HERMES_KANBAN_WORKSPACES_ROOT: string;
|
||||
HERMES_SINGLE_QUERY_SESSION: string;
|
||||
XDG_SESSION_CLASS: string;
|
||||
BROWSERBASE_ADVANCED_STEALTH: string;
|
||||
paste_collapse_threshold: string;
|
||||
TERM: string;
|
||||
TERMINAL_DAYTONA_IMAGE: string;
|
||||
XDG_SESSION_ID: string;
|
||||
npm_config_cache: string;
|
||||
TERMINAL_TEMP_DIR: string;
|
||||
TERMINAL_LIFETIME_SECONDS: string;
|
||||
TERMINAL_TIMEOUT: string;
|
||||
HERMES_GATEWAY_BUSY_INPUT_MODE: string;
|
||||
paste_collapse_char_threshold: string;
|
||||
TERMINAL_DOCKER_SHARED_CONTAINER_KEY: string;
|
||||
npm_config_node_gyp: string;
|
||||
PATH: string;
|
||||
HERMES_AGENT_TIMEOUT: string;
|
||||
NODE: string;
|
||||
TERMINAL_DOCKER_SHM_SIZE: string;
|
||||
HERMES_KANBAN_WORKSPACE: string;
|
||||
XDG_RUNTIME_DIR: string;
|
||||
TERMINAL_PERSISTENT_SHELL: string;
|
||||
LANG: string;
|
||||
HERMES_QUIET: string;
|
||||
MOA_TOOLS_DEBUG: string;
|
||||
HERMES_MEDIA_TRUST_RECENT_FILES: string;
|
||||
LS_COLORS: string;
|
||||
HERMES_MEDIA_TRUST_RECENT_SECONDS: string;
|
||||
HERMES_MAX_ITERATIONS: string;
|
||||
TERM_PROGRAM: string;
|
||||
npm_lifecycle_script: string;
|
||||
HERMES_SESSION_ID: string;
|
||||
HERMES_AGENT_TIMEOUT_WARNING: string;
|
||||
prefill_messages_file: string;
|
||||
SHELL: string;
|
||||
HERMES_AGENT_NOTIFY_INTERVAL: string;
|
||||
TERMINAL_DEGRADED_MODE: string;
|
||||
TERMINAL_MODAL_MODE: string;
|
||||
npm_lifecycle_event: string;
|
||||
WEB_TOOLS_DEBUG: string;
|
||||
VISION_TOOLS_DEBUG: string;
|
||||
LESSCLOSE: string;
|
||||
TERMINAL_CONTAINER_DISK: string;
|
||||
TERMINAL_DOCKER_RUN_AS_HOST_USER: string;
|
||||
GIT_PAGER: string;
|
||||
paste_collapse_threshold_fallback: string;
|
||||
npm_config_globalconfig: string;
|
||||
npm_config_init_module: string;
|
||||
PWD: string;
|
||||
HERMES_INTERACTIVE: string;
|
||||
_config_version: string;
|
||||
HERMES_AUTO_CONTINUE_FRESHNESS: string;
|
||||
npm_execpath: string;
|
||||
SSH_CONNECTION: string;
|
||||
XDG_DATA_DIRS: string;
|
||||
npm_config_global_prefix: string;
|
||||
hooks_auto_accept: string;
|
||||
npm_command: string;
|
||||
TMUX_PLUGIN_MANAGER_PATH: string;
|
||||
BROWSER_INACTIVITY_TIMEOUT: string;
|
||||
HERMES_REDACT_SECRETS: string;
|
||||
HERMES_CUSTOM_192_168_1_203_8084_API_KEY: string;
|
||||
TERMINAL_SINGULARITY_IMAGE: string;
|
||||
TMUX_PANE: string;
|
||||
TERMINAL_HOME_MODE: string;
|
||||
TERMINAL_DOCKER_SNAP_COMPAT: string;
|
||||
HERMES_AGENT: string;
|
||||
HERMES_SESSION_SOURCE: string;
|
||||
INIT_CWD: string;
|
||||
EDITOR: string;
|
||||
TEST: string;
|
||||
VITEST: string;
|
||||
NODE_ENV: string;
|
||||
[key: `PUBLIC_${string}`]: undefined;
|
||||
[key: `${string}`]: string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables set _dynamically_ at runtime and that are _publicly_ accessible.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Dynamic environment variables are defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node) (or running [`vite preview`](https://svelte.dev/docs/kit/cli)), this is equivalent to `process.env`.
|
||||
*
|
||||
* **_Public_ access:**
|
||||
*
|
||||
* - This module _can_ be imported into client-side code
|
||||
* - **Only** variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`) are included
|
||||
*
|
||||
* > [!NOTE] In `dev`, `$env/dynamic` includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
|
||||
*
|
||||
* > [!NOTE] To get correct types, environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
|
||||
* >
|
||||
* > ```env
|
||||
* > MY_FEATURE_FLAG=
|
||||
* > ```
|
||||
* >
|
||||
* > You can override `.env` values from the command line like so:
|
||||
* >
|
||||
* > ```sh
|
||||
* > MY_FEATURE_FLAG="enabled" npm run dev
|
||||
* > ```
|
||||
*
|
||||
* For example, given the following runtime environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://example.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { env } from '$env/dynamic/public';
|
||||
* console.log(env.ENVIRONMENT); // => undefined, not public
|
||||
* console.log(env.PUBLIC_BASE_URL); // => "http://example.com"
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
declare module '$env/dynamic/public' {
|
||||
export const env: {
|
||||
[key: `PUBLIC_${string}`]: string | undefined;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
// See https://svelte.dev/docs/kit/environment-variables for more information
|
||||
@@ -0,0 +1,38 @@
|
||||
// in dev, this makes Vite inject its client as this module's first dependency,
|
||||
// so that global constant replacements are installed before any other module
|
||||
// (including user hooks) evaluates. In build it's inert.
|
||||
import.meta.hot;
|
||||
|
||||
|
||||
|
||||
|
||||
export { matchers } from './matchers.js';
|
||||
|
||||
export const nodes = [
|
||||
() => import('./nodes/0'),
|
||||
() => import('./nodes/1')
|
||||
];
|
||||
|
||||
export const server_loads = [];
|
||||
|
||||
export const dictionary = {
|
||||
|
||||
};
|
||||
|
||||
export const hooks = {
|
||||
handleError: (({ error }) => { console.error(error) }),
|
||||
|
||||
reroute: (() => {}),
|
||||
transport: {}
|
||||
};
|
||||
|
||||
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
|
||||
export const encoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.encode]));
|
||||
|
||||
export const hash = false;
|
||||
|
||||
export const decode = (type, value) => decoders[type](value);
|
||||
|
||||
export { default as root } from '../root.js';
|
||||
|
||||
export const get_error_template = () => import('../shared/error-template.js').then(m => m.default);
|
||||
@@ -0,0 +1 @@
|
||||
export const matchers = {};
|
||||
@@ -0,0 +1 @@
|
||||
export { default as component } from "../../../../frontend/node_modules/@sveltejs/kit/src/runtime/components/svelte-5/layout.svelte";
|
||||
@@ -0,0 +1 @@
|
||||
export { default as component } from "../../../../frontend/node_modules/@sveltejs/kit/src/runtime/components/svelte-5/error.svelte";
|
||||
@@ -0,0 +1 @@
|
||||
export default ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n";
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"$app/types": [
|
||||
"./types/index.d.ts"
|
||||
]
|
||||
},
|
||||
"rootDirs": [
|
||||
"..",
|
||||
"./types"
|
||||
],
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"module": "esnext",
|
||||
"noEmit": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": [
|
||||
"ambient.d.ts",
|
||||
"env.d.ts",
|
||||
"non-ambient.d.ts",
|
||||
"./types/**/$types.d.ts",
|
||||
"../vite.config.js",
|
||||
"../vite.config.ts",
|
||||
"../src/**/*.js",
|
||||
"../src/**/*.ts",
|
||||
"../src/**/*.svelte",
|
||||
"../test/**/*.js",
|
||||
"../test/**/*.ts",
|
||||
"../test/**/*.svelte",
|
||||
"../tests/**/*.js",
|
||||
"../tests/**/*.ts",
|
||||
"../tests/**/*.svelte"
|
||||
],
|
||||
"exclude": [
|
||||
"../node_modules/**",
|
||||
"../src/service-worker.js",
|
||||
"../src/service-worker/**/*.js",
|
||||
"../src/service-worker.ts",
|
||||
"../src/service-worker/**/*.ts",
|
||||
"../src/service-worker.d.ts",
|
||||
"../src/service-worker/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# SlabUploader M1 — shared build context (for :202 worker tasks)
|
||||
|
||||
REPO: /home/ty/SlabUploader — work ONLY inside /home/ty/SlabUploader/frontend.
|
||||
Branch: M1-dev-rev4 (already checked out; DO NOT switch branches, DO NOT push).
|
||||
This is Milestone 1 (Capture) of a slab-measuring web app. Browser-only, NO backend,
|
||||
NO inference, NO OpenCV/ONNX. Pure JS (not TS). Svelte 5 + SvelteKit + adapter-node.
|
||||
|
||||
TEST COMMANDS (run these, show real output):
|
||||
unit: cd /home/ty/SlabUploader/frontend && npx vitest run
|
||||
e2e: npx playwright test --list (collection only; full e2e needs the M1-6 container)
|
||||
|
||||
EXISTING (already committed, green, DO NOT break):
|
||||
src/lib/core/mask.js -> keySheet(data,{sheet,sensitivity}), maskStats(mask)
|
||||
src/lib/core/math.js -> scale, sqft, bdft, roundHalfUp, sampledWidths,
|
||||
sampledWidthsFrom, snapToEighth
|
||||
tests/unit/mask.test.js, tests/unit/math.test.js (24 tests, all pass)
|
||||
src/routes/+page.svelte (placeholder home — M1-3 replaces it)
|
||||
src/routes/+layout.svelte, src/routes/api/health/+server.js
|
||||
|
||||
TDD IS MANDATORY (test-driven-development skill):
|
||||
1. Write the failing test(s) FIRST.
|
||||
2. Run them and WATCH them fail for the right reason (feature missing, not a typo).
|
||||
3. Write the MINIMAL code to pass.
|
||||
4. Run them and confirm they pass.
|
||||
5. Run the FULL unit suite (npx vitest run) and confirm no regressions + pristine output.
|
||||
6. Only then are you done. Commit is done by the coordinator — you do NOT commit.
|
||||
|
||||
CONVENTIONS:
|
||||
- Tab indentation (the repo uses tabs).
|
||||
- Pure logic lives in src/lib/core/*.js (DOM-free, unit-testable under node).
|
||||
- Svelte components live under src/lib/ (e.g. src/lib/components/).
|
||||
- Every new exported function needs a unit test that you SAW fail first.
|
||||
- No console.log left in production code.
|
||||
- Do not add dependencies.
|
||||
- Report at the end: files created/changed, the exact test command output, and
|
||||
any place you had to make a judgment call.
|
||||
@@ -0,0 +1,286 @@
|
||||
// M1-4 Length axis — pure functions + state machine, no DOM.
|
||||
//
|
||||
// Ground truth: docs/CAPTURE.md §Length axis
|
||||
// - min-area bounding rectangle of the mask
|
||||
// - length axis on the long side of that rectangle
|
||||
// - user rotates the axis if it does not follow the slab length
|
||||
// - approve the axis
|
||||
//
|
||||
// minAreaRect implements the rotating calipers method (Shamos & Ho 1975)
|
||||
// over the convex hull of the mask pixels. The convex hull is computed
|
||||
// with the monotone-chain algorithm (O(n log n)).
|
||||
|
||||
/**
|
||||
* Compute the minimum-area bounding rectangle of a set of 2D points.
|
||||
* Uses the rotating calipers method on the convex hull.
|
||||
*
|
||||
* @param {Array<{x:number, y:number}>} pts
|
||||
* @returns {{center:{x:number,y:number}, width:number, height:number, angle:number, area:number}}
|
||||
* angle: rotation of the long side in radians (0..PI/2, CCW from +x).
|
||||
* width: length along the long side. height: length along the short side.
|
||||
*/
|
||||
function minAreaRectPoints(pts) {
|
||||
const n = pts.length;
|
||||
if (n === 0) return null;
|
||||
if (n === 1)
|
||||
return {
|
||||
center: { x: pts[0].x, y: pts[0].y },
|
||||
width: 0,
|
||||
height: 0,
|
||||
angle: 0,
|
||||
area: 0,
|
||||
};
|
||||
if (n === 2) {
|
||||
const dx = pts[1].x - pts[0].x;
|
||||
const dy = pts[1].y - pts[0].y;
|
||||
return {
|
||||
center: { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 },
|
||||
width: Math.sqrt(dx * dx + dy * dy),
|
||||
height: 0,
|
||||
angle: Math.atan2(dy, dx),
|
||||
area: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Convex hull (monotone chain).
|
||||
const hull = convexHull(pts);
|
||||
if (hull.length < 3) return minAreaRectPoints(hull);
|
||||
|
||||
// Rotating calipers: try each hull edge as the support line.
|
||||
let best = { area: Infinity, width: 0, height: 0, angle: 0, center: { x: 0, y: 0 } };
|
||||
const m = hull.length;
|
||||
|
||||
for (let i = 0; i < m; i++) {
|
||||
const p1 = hull[i];
|
||||
const p2 = hull[(i + 1) % m];
|
||||
const ex = p2.x - p1.x;
|
||||
const ey = p2.y - p1.y;
|
||||
const len = Math.sqrt(ex * ex + ey * ey);
|
||||
const ux = ex / len;
|
||||
const uy = ey / len;
|
||||
|
||||
// Project all hull points onto the edge direction (u) and its
|
||||
// left normal (v = (-uy, ux)).
|
||||
let minU = Infinity,
|
||||
maxU = -Infinity;
|
||||
let minV = Infinity,
|
||||
maxV = -Infinity;
|
||||
for (let j = 0; j < m; j++) {
|
||||
const q = hull[j];
|
||||
const u = q.x * ux + q.y * uy;
|
||||
const v = -q.x * uy + q.y * ux;
|
||||
if (u < minU) minU = u;
|
||||
if (u > maxU) maxU = u;
|
||||
if (v < minV) minV = v;
|
||||
if (v > maxV) maxV = v;
|
||||
}
|
||||
|
||||
const w = maxU - minU;
|
||||
const h = maxV - minV;
|
||||
const area = w * h;
|
||||
|
||||
if (area < best.area) {
|
||||
// Center in (u,v) frame, transformed back to (x,y):
|
||||
// x = cu * ux - cv * uy
|
||||
// y = cu * uy + cv * ux
|
||||
const cu = (minU + maxU) / 2;
|
||||
const cv = (minV + maxV) / 2;
|
||||
const centerX = cu * ux - cv * uy;
|
||||
const centerY = cu * uy + cv * ux;
|
||||
|
||||
// Long side: along u when w >= h, else along the normal v.
|
||||
let width, height, angle;
|
||||
if (w >= h) {
|
||||
width = w;
|
||||
height = h;
|
||||
angle = Math.atan2(uy, ux);
|
||||
} else {
|
||||
width = h;
|
||||
height = w;
|
||||
// Long side along v = (-uy, ux)
|
||||
angle = Math.atan2(ux, -uy);
|
||||
}
|
||||
// Normalize angle to [0, PI) — rectangle has 180-degree symmetry.
|
||||
if (angle < 0) angle += Math.PI;
|
||||
if (angle >= Math.PI) angle -= Math.PI;
|
||||
|
||||
best = {
|
||||
center: { x: centerX, y: centerY },
|
||||
width,
|
||||
height,
|
||||
angle,
|
||||
area,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monotone-chain convex hull, keeping collinear boundary points.
|
||||
* Returns an array of {x,y} points in CCW order, no duplicate last point.
|
||||
*
|
||||
* Collinear points are kept because the min-area bounding rectangle
|
||||
* algorithm needs them: a rectangle's side is defined by its two
|
||||
* endpoints, and dropping intermediate collinear points would lose the
|
||||
* true extent of the shape.
|
||||
*
|
||||
* @param {Array<{x:number,y:number}>} pts
|
||||
* @returns {Array<{x:number,y:number}>}
|
||||
*/
|
||||
function convexHull(pts) {
|
||||
const n = pts.length;
|
||||
if (n <= 2) return pts.slice();
|
||||
|
||||
const sorted = pts.slice().sort((a, b) => (a.x - b.x) || (a.y - b.y));
|
||||
|
||||
const cross = (O, A, B) =>
|
||||
(A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
|
||||
|
||||
const lower = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
while (
|
||||
lower.length >= 2 &&
|
||||
cross(lower[lower.length - 2], lower[lower.length - 1], sorted[i]) < 0
|
||||
) {
|
||||
lower.pop();
|
||||
}
|
||||
lower.push(sorted[i]);
|
||||
}
|
||||
|
||||
const upper = [];
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
while (
|
||||
upper.length >= 2 &&
|
||||
cross(upper[upper.length - 2], upper[upper.length - 1], sorted[i]) < 0
|
||||
) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(sorted[i]);
|
||||
}
|
||||
|
||||
// Concatenate, dropping the last point of each (duplicates).
|
||||
const hull = lower.slice(0, -1).concat(upper.slice(0, -1));
|
||||
return hull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum-area bounding rectangle of a binary mask.
|
||||
*
|
||||
* @param {Uint8Array} mask binary mask (1 = kept)
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @returns {null|{center:{x:number,y:number}, width:number, height:number, angle:number, area:number}}
|
||||
*/
|
||||
export function minAreaRect(mask, width, height) {
|
||||
const pts = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (mask[y * width + x]) {
|
||||
pts.push({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pts.length === 0) return null;
|
||||
if (pts.length === 1)
|
||||
return {
|
||||
center: { x: pts[0].x, y: pts[0].y },
|
||||
width: 1,
|
||||
height: 1,
|
||||
angle: 0,
|
||||
area: 1,
|
||||
};
|
||||
return minAreaRectPoints(pts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit vector along the long side of a min-area rectangle.
|
||||
*
|
||||
* @param {{width:number, height:number, angle:number}} rect
|
||||
* @returns {{x:number, y:number}}
|
||||
*/
|
||||
export function axisFromRect(rect) {
|
||||
if (rect.width >= rect.height) {
|
||||
// Long side is along the rect's angle direction.
|
||||
return { x: Math.cos(rect.angle), y: Math.sin(rect.angle) };
|
||||
} else {
|
||||
// Long side is along the normal (90 degrees CCW from the rect angle).
|
||||
return { x: -Math.sin(rect.angle), y: Math.cos(rect.angle) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a unit axis by an angle (radians, CCW positive).
|
||||
*
|
||||
* @param {{x:number, y:number}} axis
|
||||
* @param {number} angle radians
|
||||
* @returns {{x:number, y:number}}
|
||||
*/
|
||||
export function rotateAxis(axis, angle) {
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
return { x: axis.x * cos - axis.y * sin, y: axis.x * sin + axis.y * cos };
|
||||
}
|
||||
|
||||
/**
|
||||
* Angle of a unit axis in radians (-PI..PI).
|
||||
*
|
||||
* @param {{x:number, y:number}} axis
|
||||
* @returns {number}
|
||||
*/
|
||||
export function axisAngle(axis) {
|
||||
return Math.atan2(axis.y, axis.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit vector from an angle in radians.
|
||||
*
|
||||
* @param {number} angle radians
|
||||
* @returns {{x:number, y:number}}
|
||||
*/
|
||||
export function axisUnit(angle) {
|
||||
return { x: Math.cos(angle), y: Math.sin(angle) };
|
||||
}
|
||||
|
||||
// --- axis state machine -----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a fresh axis state from an approved min-area rectangle.
|
||||
*
|
||||
* @param {{center:{x:number,y:number}, width:number, height:number, angle:number, area:number}} rect
|
||||
*/
|
||||
export function createAxisState(rect) {
|
||||
return {
|
||||
rect,
|
||||
axis: axisFromRect(rect),
|
||||
approved: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure reducer for the axis state.
|
||||
* Actions:
|
||||
* { type: 'rotate', angle } — rotate the axis by `angle` radians (CCW).
|
||||
* Retracts any prior approval.
|
||||
* { type: 'approve' } — mark the axis as approved.
|
||||
*
|
||||
* @param {object} state
|
||||
* @param {object} action
|
||||
* @returns {object} new state
|
||||
*/
|
||||
export function reduceAxisState(state, action) {
|
||||
switch (action.type) {
|
||||
case 'rotate': {
|
||||
const angle = action.angle;
|
||||
if (angle === 0) return state;
|
||||
const newAxis = rotateAxis(state.axis, angle);
|
||||
return { ...state, axis: newAxis, approved: false };
|
||||
}
|
||||
case 'approve': {
|
||||
return { ...state, approved: true };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// M1-4: Playwright tests for the Length axis state.
|
||||
// RED first — the live UI lands in M1-5. These tests describe the
|
||||
// contract the UI must honour once the Mask state approves.
|
||||
//
|
||||
// Spec ground truth (docs/CAPTURE.md §Length axis):
|
||||
// - min-area bounding rectangle of the mask
|
||||
// - length axis on the long side of that rectangle
|
||||
// - user rotates the axis if it does not follow the slab length
|
||||
// - approve the axis
|
||||
|
||||
const SLAB_FIXTURE = 'tests/fixtures/slab-green.jpg';
|
||||
|
||||
test.describe('Length axis state (M1-5 live UI)', () => {
|
||||
test('advances to Length axis after approving the mask', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE);
|
||||
// Wait for the initial mask recompute.
|
||||
await page.waitForTimeout(2200);
|
||||
// Approve the mask.
|
||||
await page.getByRole('button', { name: /approve/i }).tap();
|
||||
// The Length axis state is now visible.
|
||||
await expect(page.getByText(/length axis/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows the bounding rectangle and the proposed length axis', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE);
|
||||
await page.waitForTimeout(2200);
|
||||
await page.getByRole('button', { name: /approve/i }).tap();
|
||||
// The bounding rectangle is drawn on the photo.
|
||||
await expect(
|
||||
page.locator('[data-testid="bounding-rect"]'),
|
||||
).toBeVisible();
|
||||
// The proposed length axis is drawn along the long side.
|
||||
await expect(
|
||||
page.locator('[data-testid="length-axis"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('rotate control adjusts the axis', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE);
|
||||
await page.waitForTimeout(2200);
|
||||
await page.getByRole('button', { name: /approve/i }).tap();
|
||||
// The rotate control is present.
|
||||
const rotate = page.getByRole('slider', { name: /rotate axis/i });
|
||||
await expect(rotate).toBeVisible();
|
||||
// Rotating changes the axis angle.
|
||||
const before = await page
|
||||
.locator('[data-testid="length-axis"]')
|
||||
.getAttribute('data-angle');
|
||||
await rotate.fill('90');
|
||||
const after = await page
|
||||
.locator('[data-testid="length-axis"]')
|
||||
.getAttribute('data-angle');
|
||||
expect(before).not.toBe(after);
|
||||
});
|
||||
|
||||
test('approve the axis advances to Measure', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE);
|
||||
await page.waitForTimeout(2200);
|
||||
// Approve the mask -> Length axis.
|
||||
await page.getByRole('button', { name: /approve/i }).tap();
|
||||
// Approve the axis -> Measure.
|
||||
const approveAxis = page.getByRole('button', {
|
||||
name: /approve axis/i,
|
||||
});
|
||||
await expect(approveAxis).toBeVisible();
|
||||
await approveAxis.tap();
|
||||
await expect(page.getByText(/measure/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,358 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
minAreaRect,
|
||||
axisFromRect,
|
||||
rotateAxis,
|
||||
axisAngle,
|
||||
axisUnit,
|
||||
createAxisState,
|
||||
reduceAxisState,
|
||||
} from '../../src/lib/core/axis.js';
|
||||
|
||||
// M1-4 Length axis — unit tests (RED first, per strict TDD).
|
||||
//
|
||||
// Ground truth: docs/CAPTURE.md §Length axis
|
||||
// - min-area bounding rectangle of the mask
|
||||
// - length axis on the long side of that rectangle
|
||||
// - user rotates the axis if it does not follow the slab length
|
||||
// - approve the axis
|
||||
//
|
||||
// All functions are pure (no DOM) so the state is unit-testable in Node.
|
||||
|
||||
// --- helpers --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a binary mask (Uint8Array) of w x h with a filled axis-aligned
|
||||
* rectangle of size rw x rh at (rx, ry).
|
||||
*/
|
||||
function rectMask(w, h, rx, ry, rw, rh) {
|
||||
const mask = new Uint8Array(w * h);
|
||||
for (let y = ry; y < ry + rh; y++) {
|
||||
for (let x = rx; x < rx + rw; x++) {
|
||||
mask[y * w + x] = 1;
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a binary mask of a rotated rectangle.
|
||||
* Center at (cx, cy), half-lengths (hl, hw), rotation in radians.
|
||||
*/
|
||||
function rotatedRectMask(w, h, cx, cy, hl, hw, rot) {
|
||||
const mask = new Uint8Array(w * h);
|
||||
const cos = Math.cos(rot);
|
||||
const sin = Math.sin(rot);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
// Transform to rect-local coords
|
||||
const lx = cos * dx + sin * dy;
|
||||
const ly = -sin * dx + cos * dy;
|
||||
if (Math.abs(lx) <= hl && Math.abs(ly) <= hw) {
|
||||
mask[y * w + x] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
// --- minAreaRect ------------------------------------------------------------------
|
||||
|
||||
describe('minAreaRect', () => {
|
||||
it('returns a tight axis-aligned rectangle for an axis-aligned mask', () => {
|
||||
// 8x4 rectangle at (2,2) on a 12x8 canvas.
|
||||
// Pixels at x=2..9 (8 cols), y=2..5 (4 rows).
|
||||
// Center-to-center span: width 7, height 3. Center (5.5, 3.5).
|
||||
const mask = rectMask(12, 8, 2, 2, 8, 4);
|
||||
const rect = minAreaRect(mask, 12, 8);
|
||||
// The min-area rect of an axis-aligned pixel block is tight:
|
||||
// width = (cols-1), height = (rows-1) in center-to-center units.
|
||||
expect(rect.width).toBeCloseTo(7, 2);
|
||||
expect(rect.height).toBeCloseTo(3, 2);
|
||||
expect(rect.angle).toBeCloseTo(0, 3);
|
||||
expect(rect.center.x).toBeCloseTo(5.5, 2); // (2+9)/2
|
||||
expect(rect.center.y).toBeCloseTo(3.5, 2); // (2+5)/2
|
||||
});
|
||||
|
||||
it('handles a single pixel', () => {
|
||||
const mask = new Uint8Array(10 * 10);
|
||||
mask[5 * 10 + 5] = 1; // pixel at (5,5)
|
||||
const rect = minAreaRect(mask, 10, 10);
|
||||
expect(rect.area).toBe(1);
|
||||
});
|
||||
|
||||
it('returns null for an empty mask', () => {
|
||||
const mask = new Uint8Array(10 * 10);
|
||||
const rect = minAreaRect(mask, 10, 10);
|
||||
expect(rect).toBeNull();
|
||||
});
|
||||
|
||||
it('detects a rotated rectangle', () => {
|
||||
// 45-degree rotated rectangle: 20 units long, 4 units wide, centered
|
||||
const w = 40,
|
||||
h = 40,
|
||||
cx = 20,
|
||||
cy = 20,
|
||||
hl = 10,
|
||||
hw = 2;
|
||||
const mask = rotatedRectMask(w, h, cx, cy, hl, hw, Math.PI / 4);
|
||||
const rect = minAreaRect(mask, w, h);
|
||||
// Pixel rasterization of a 20x4 rect at 45 degrees: the convex hull
|
||||
// of the kept pixels approximates the true rect within ~1px per side.
|
||||
// The angle should be close to 45 degrees.
|
||||
expect(rect.angle).toBeCloseTo(Math.PI / 4, 1);
|
||||
// Long side ~20 (within 2px for rasterization)
|
||||
const longSide = Math.max(rect.width, rect.height);
|
||||
expect(longSide).toBeGreaterThanOrEqual(18);
|
||||
expect(longSide).toBeLessThanOrEqual(22);
|
||||
});
|
||||
|
||||
it('handles a horizontal bar (wider than tall)', () => {
|
||||
// 16x2 bar on a 20x10 canvas
|
||||
const mask = rectMask(20, 10, 2, 4, 16, 2);
|
||||
const rect = minAreaRect(mask, 20, 10);
|
||||
// 16 cols x 2 rows: width ~15-16, height ~1-2 (pixel rasterization)
|
||||
expect(rect.width).toBeGreaterThanOrEqual(14);
|
||||
expect(rect.width).toBeLessThanOrEqual(17);
|
||||
expect(rect.height).toBeGreaterThanOrEqual(1);
|
||||
expect(rect.height).toBeLessThanOrEqual(3);
|
||||
expect(rect.angle).toBeCloseTo(0, 1);
|
||||
});
|
||||
|
||||
it('handles a vertical bar (taller than wide)', () => {
|
||||
// 2x16 bar on a 10x20 canvas
|
||||
const mask = rectMask(10, 20, 4, 2, 2, 16);
|
||||
const rect = minAreaRect(mask, 10, 20);
|
||||
// Long side ~16, short side ~2
|
||||
const longSide = Math.max(rect.width, rect.height);
|
||||
const shortSide = Math.min(rect.width, rect.height);
|
||||
expect(longSide).toBeGreaterThanOrEqual(14);
|
||||
expect(longSide).toBeLessThanOrEqual(17);
|
||||
expect(shortSide).toBeGreaterThanOrEqual(1);
|
||||
expect(shortSide).toBeLessThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
// --- axisFromRect ------------------------------------------------------------------
|
||||
|
||||
describe('axisFromRect', () => {
|
||||
it('returns a unit vector along the long side of an axis-aligned rect', () => {
|
||||
// 8x4 rect: long side is horizontal (width=8 > height=4)
|
||||
const rect = {
|
||||
center: { x: 6, y: 4 },
|
||||
width: 8,
|
||||
height: 4,
|
||||
angle: 0,
|
||||
area: 32,
|
||||
};
|
||||
const axis = axisFromRect(rect);
|
||||
// Long side is width (8), so axis is along +x
|
||||
expect(axis.x).toBeCloseTo(1, 6);
|
||||
expect(axis.y).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it('returns a unit vector along the long side when height > width', () => {
|
||||
// 4x8 rect: long side is vertical (height=8 > width=4)
|
||||
const rect = {
|
||||
center: { x: 6, y: 4 },
|
||||
width: 4,
|
||||
height: 8,
|
||||
angle: 0,
|
||||
area: 32,
|
||||
};
|
||||
const axis = axisFromRect(rect);
|
||||
// Long side is height, so axis is along +y
|
||||
expect(axis.x).toBeCloseTo(0, 6);
|
||||
expect(axis.y).toBeCloseTo(1, 6);
|
||||
});
|
||||
|
||||
it('rotates the axis with the rect angle', () => {
|
||||
// 8x4 rect rotated 90 degrees: long side becomes vertical
|
||||
const rect = {
|
||||
center: { x: 6, y: 4 },
|
||||
width: 8,
|
||||
height: 4,
|
||||
angle: Math.PI / 2,
|
||||
area: 32,
|
||||
};
|
||||
const axis = axisFromRect(rect);
|
||||
// The long side (width=8) is now along +y after 90-degree rotation
|
||||
expect(axis.x).toBeCloseTo(0, 6);
|
||||
expect(axis.y).toBeCloseTo(1, 6);
|
||||
});
|
||||
|
||||
it('returns the axis at 45 degrees for a rotated rect', () => {
|
||||
const rect = {
|
||||
center: { x: 20, y: 20 },
|
||||
width: 20,
|
||||
height: 4,
|
||||
angle: Math.PI / 4,
|
||||
area: 80,
|
||||
};
|
||||
const axis = axisFromRect(rect);
|
||||
// Long side at 45 degrees
|
||||
expect(axis.x).toBeCloseTo(Math.SQRT1_2, 3);
|
||||
expect(axis.y).toBeCloseTo(Math.SQRT1_2, 3);
|
||||
});
|
||||
});
|
||||
|
||||
// --- rotateAxis --------------------------------------------------------------------
|
||||
|
||||
describe('rotateAxis', () => {
|
||||
it('rotates by 0 degrees is a no-op', () => {
|
||||
const axis = { x: 1, y: 0 };
|
||||
const rot = rotateAxis(axis, 0);
|
||||
expect(rot.x).toBeCloseTo(1, 6);
|
||||
expect(rot.y).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it('rotates by 90 degrees', () => {
|
||||
const axis = { x: 1, y: 0 };
|
||||
const rot = rotateAxis(axis, Math.PI / 2);
|
||||
expect(rot.x).toBeCloseTo(0, 6);
|
||||
expect(rot.y).toBeCloseTo(1, 6);
|
||||
});
|
||||
|
||||
it('rotates by -90 degrees', () => {
|
||||
const axis = { x: 1, y: 0 };
|
||||
const rot = rotateAxis(axis, -Math.PI / 2);
|
||||
expect(rot.x).toBeCloseTo(0, 6);
|
||||
expect(rot.y).toBeCloseTo(-1, 6);
|
||||
});
|
||||
|
||||
it('rotates a 45-degree axis by 45 degrees -> 90 degrees (vertical)', () => {
|
||||
const axis = { x: Math.SQRT1_2, y: Math.SQRT1_2 };
|
||||
const rot = rotateAxis(axis, Math.PI / 4);
|
||||
expect(rot.x).toBeCloseTo(0, 3);
|
||||
expect(rot.y).toBeCloseTo(1, 3);
|
||||
});
|
||||
});
|
||||
|
||||
// --- axisAngle -------------------------------------------------------------------
|
||||
|
||||
describe('axisAngle', () => {
|
||||
it('returns 0 for +x axis', () => {
|
||||
expect(axisAngle({ x: 1, y: 0 })).toBeCloseTo(0, 6);
|
||||
});
|
||||
it('returns PI/2 for +y axis', () => {
|
||||
expect(axisAngle({ x: 0, y: 1 })).toBeCloseTo(Math.PI / 2, 6);
|
||||
});
|
||||
it('returns -PI/2 for -y axis', () => {
|
||||
expect(axisAngle({ x: 0, y: -1 })).toBeCloseTo(-Math.PI / 2, 6);
|
||||
});
|
||||
it('returns PI for -x axis', () => {
|
||||
expect(axisAngle({ x: -1, y: 0 })).toBeCloseTo(Math.PI, 6);
|
||||
});
|
||||
it('returns 45 degrees for diagonal', () => {
|
||||
expect(axisAngle({ x: Math.SQRT1_2, y: Math.SQRT1_2 })).toBeCloseTo(
|
||||
Math.PI / 4,
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// --- axisUnit --------------------------------------------------------------------
|
||||
|
||||
describe('axisUnit', () => {
|
||||
it('creates a unit vector from an angle', () => {
|
||||
expect(axisUnit(0).x).toBeCloseTo(1, 6);
|
||||
expect(axisUnit(0).y).toBeCloseTo(0, 6);
|
||||
expect(axisUnit(Math.PI / 2).x).toBeCloseTo(0, 6);
|
||||
expect(axisUnit(Math.PI / 2).y).toBeCloseTo(1, 6);
|
||||
expect(axisUnit(-Math.PI / 2).x).toBeCloseTo(0, 6);
|
||||
expect(axisUnit(-Math.PI / 2).y).toBeCloseTo(-1, 6);
|
||||
expect(axisUnit(Math.PI).x).toBeCloseTo(-1, 6);
|
||||
expect(axisUnit(Math.PI).y).toBeCloseTo(0, 6);
|
||||
});
|
||||
});
|
||||
|
||||
// --- createAxisState -----------------------------------------------------------------
|
||||
|
||||
describe('createAxisState', () => {
|
||||
it('starts with the proposed axis from the rect, not approved', () => {
|
||||
const rect = {
|
||||
center: { x: 6, y: 4 },
|
||||
width: 8,
|
||||
height: 4,
|
||||
angle: 0,
|
||||
area: 32,
|
||||
};
|
||||
const state = createAxisState(rect);
|
||||
expect(state.rect).toBe(rect);
|
||||
expect(state.axis.x).toBeCloseTo(1, 6); // long side is horizontal
|
||||
expect(state.axis.y).toBeCloseTo(0, 6);
|
||||
expect(state.approved).toBe(false);
|
||||
});
|
||||
|
||||
it('proposed axis follows the rect angle', () => {
|
||||
const rect = {
|
||||
center: { x: 20, y: 20 },
|
||||
width: 20,
|
||||
height: 4,
|
||||
angle: Math.PI / 4,
|
||||
area: 80,
|
||||
};
|
||||
const state = createAxisState(rect);
|
||||
expect(state.axis.x).toBeCloseTo(Math.SQRT1_2, 3);
|
||||
expect(state.axis.y).toBeCloseTo(Math.SQRT1_2, 3);
|
||||
});
|
||||
});
|
||||
|
||||
// --- reduceAxisState -----------------------------------------------------------------
|
||||
|
||||
describe('reduceAxisState', () => {
|
||||
const rect = {
|
||||
center: { x: 6, y: 4 },
|
||||
width: 8,
|
||||
height: 4,
|
||||
angle: 0,
|
||||
area: 32,
|
||||
};
|
||||
|
||||
it('rotate updates the axis immediately', () => {
|
||||
let s = createAxisState(rect);
|
||||
s = reduceAxisState(s, { type: 'rotate', angle: Math.PI / 2 });
|
||||
// +x rotated by 90 degrees -> +y
|
||||
expect(s.axis.x).toBeCloseTo(0, 6);
|
||||
expect(s.axis.y).toBeCloseTo(1, 6);
|
||||
});
|
||||
|
||||
it('rotate composes (rotates from current axis)', () => {
|
||||
let s = createAxisState(rect);
|
||||
s = reduceAxisState(s, { type: 'rotate', angle: Math.PI / 2 });
|
||||
s = reduceAxisState(s, { type: 'rotate', angle: Math.PI / 2 });
|
||||
// Two 90-degree rotations -> -x
|
||||
expect(s.axis.x).toBeCloseTo(-1, 6);
|
||||
expect(s.axis.y).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it('rotate by 0 is a no-op', () => {
|
||||
let s = createAxisState(rect);
|
||||
const before = s;
|
||||
s = reduceAxisState(s, { type: 'rotate', angle: 0 });
|
||||
expect(s).toBe(before);
|
||||
});
|
||||
|
||||
it('approve sets approved=true', () => {
|
||||
let s = createAxisState(rect);
|
||||
s = reduceAxisState(s, { type: 'approve' });
|
||||
expect(s.approved).toBe(true);
|
||||
});
|
||||
|
||||
it('retract approval on rotate after approval', () => {
|
||||
let s = createAxisState(rect);
|
||||
s = reduceAxisState(s, { type: 'approve' });
|
||||
expect(s.approved).toBe(true);
|
||||
// Rotate after approval: axis changes, approval is retracted
|
||||
// (user is adjusting the axis, so it is no longer approved)
|
||||
s = reduceAxisState(s, { type: 'rotate', angle: Math.PI / 4 });
|
||||
expect(s.approved).toBe(false);
|
||||
});
|
||||
|
||||
it('unknown actions return the same state', () => {
|
||||
const s = createAxisState(rect);
|
||||
expect(reduceAxisState(s, { type: 'bogus' })).toBe(s);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user