/** * Type declarations for the cartonization display components (dist/widget/cartonization-components.js). * Hand-maintained; widget/dts-check.ts fails `npm run typecheck` when the implementation drifts from it. * * Loading the module defines four custom elements. They only display a result: they never call the API. The page's backend * calls POST /api/pack and the page passes the response (or just its `best` Solution) to the components' `result` property. * * summary tiles, plus the unpacked units with their reasons * the algorithm ranking; rows open that algorithm's solution when the result has `solutions` * the containers of the shown solution; clicking a row selects it * one container in 3D (orbit controls), with a caption and a legend * * Attributes (all four): `group`, a page-wide id. Components with the same group share one state (the result, the selected * algorithm and the selected container), wherever they are in the page and whenever they are attached. Without `group` a * component is standalone. `` also takes `container`, a 1-based index that pins the container it shows. * * Theming. CSS custom properties (inherited through the shadow root): --cw-font-family, --cw-accent, --cw-bg (tile background), * --cw-surface, --cw-text, --cw-muted, --cw-border, --cw-radius, and --cw-3d-height (the 3D view's default height, 420px; a * height set on the element itself wins, and the view needs a definite height). * Parts (style with ::part()): * all: empty (the "no result" message), hint, caption, trial-mark (the TRIAL mark, see below) * summary: tiles, tile (with data-tile = containers | fill-rate | packed | unpacked | objective | cost | weight-utilization | time * in the DOM), value, label, unpacked * ranking: table, header (the objective's column is also `judge`), row (also `winner`, `selected`), cell * containers: table, header, row (also `selected`), cell (the hazmat container label is also `hazmat`) * 3d-view: caption, viewport, canvas, legend, legend-item, swatch * * Trial licenses. A response from an install on an evaluation (trial) license has `license: { trial: true }`. While the shown * result has it, every component shows a TRIAL mark (part `trial-mark`): a label above its content, and a large mark over the * 3D view's viewport. The mark uses fixed colours, not the theme. A bare Solution has no marker and shows no mark. */ export type Objective = 'fill-rate' | 'fewest-containers' | 'lowest-cost' | 'weight-utilization'; export interface Dimensions { length: number; width: number; height: number; } export interface Placement extends Dimensions { uid: string; itemId: string; x: number; y: number; z: number; weight: number; } export interface PackedContainer { index: number; containerId: string; dimensions: Dimensions; placements: Placement[]; itemVolume: number; containerVolume: number; fillRate: number; netWeight: number; grossWeight: number; weightUtilization: number | null; hazmatClass?: string; } export interface SolutionMetrics { containerCount: number; packedUnits: number; unpackedUnits: number; totalItemVolume: number; totalContainerVolume: number; fillRate: number; totalCost: number; weightUtilization: number | null; } export interface Solution { algorithm: string; containers: PackedContainer[]; unpacked: { uid: string; itemId: string; reason: string }[]; metrics: SolutionMetrics; elapsedMs: number; } export interface RankingEntry { rank: number; algorithm: string; metrics: SolutionMetrics; elapsedMs: number; } /** Present on a response (and on each multi-level level) only while the install runs on a trial license. */ export interface LicenseMarker { trial: true; } /** * The POST /api/pack response. `solutions` is present when the request had `includeAllSolutions: true`; `license` only on a * trial license (the components then show a TRIAL mark). */ export interface PackResponse { best: Solution; ranking: RankingEntry[]; objective: Objective; skipped: string[]; solutions?: Solution[]; license?: LicenseMarker; } /** One level of a POST /api/pack/multi-level response. */ export interface LevelResult { name: string; best: Solution; ranking: RankingEntry[]; objective: Objective; skipped: string[]; license?: LicenseMarker; } /** * A result as the components hold it. A bare Solution becomes `{ best: solution, ranking: [], skipped: [] }`. `license` is * kept only when it is exactly `{ trial: true }`. */ export interface CartonizationResult { best: Solution; ranking: RankingEntry[]; objective?: Objective; skipped: string[]; solutions?: Solution[]; license?: LicenseMarker; } /** What `result` and `setResult` accept: a /api/pack response, one multi-level level, or a bare Solution (e.g. a stored `best`). */ export type ResultInput = PackResponse | LevelResult | CartonizationResult | Solution; export interface GroupState { readonly result: CartonizationResult | null; /** The algorithm whose solution is shown: the best one after setResult. Null without a result. */ readonly selectedAlgorithm: string | null; /** 1-based index of the selected container in the shown solution: 1 after setResult or an algorithm change. */ readonly selectedContainer: number; /** The shown solution (the selected algorithm's), or null without a result. */ readonly solution: Solution | null; } export type GroupListener = (state: GroupState) => void; export interface Selection { algorithm?: string; container?: number; } /** The shared state of one group. Changes made here update every component in the group. */ export interface CartonizationGroup { /** The group id; null for a standalone component's private state. */ readonly id: string | null; readonly state: GroupState; readonly result: CartonizationResult | null; readonly selectedAlgorithm: string | null; readonly selectedContainer: number; readonly solution: Solution | null; /** Replaces the result (null clears it) and selects the best algorithm and container #1. Throws a TypeError for other values. */ setResult(result: ResultInput | null | undefined): void; /** * Selects an algorithm and/or a container. An algorithm needs its solution in the result (the best one, or one of * `solutions`); a container must exist in the (newly) selected solution. Changing the algorithm selects container #1 * unless `container` is given. Returns false and changes nothing when the selection is invalid or there is no result. * Doesn't fire DOM events (those are only for user clicks). */ select(selection: Selection): boolean; /** Calls `listener` right away with the current state, then on every change. Returns the unsubscribe function. */ subscribe(listener: GroupListener): () => void; } /** The group with this id, created empty on first use. The same id always returns the same group. */ export declare function getGroup(id: string): CartonizationGroup; /** `cw-solution-select`: a ranking row was clicked and its algorithm's solution is now shown. `group` is null when standalone. */ export interface SolutionSelectDetail { group: string | null; algorithm: string; } /** `cw-container-select`: a container row was clicked. `index` is 1-based. */ export interface ContainerSelectDetail { group: string | null; index: number; containerId: string; } /** CustomEvents fired by the components on user clicks. They bubble and are composed (they cross shadow boundaries). */ export interface CartonizationEventMap extends HTMLElementEventMap { 'cw-solution-select': CustomEvent; 'cw-container-select': CustomEvent; } /** Shared by the four components. */ export declare abstract class CartonizationElement extends HTMLElement { static readonly observedAttributes: string[]; /** The `group` attribute; null when standalone. Changing it leaves the old group and joins the new one. */ get group(): string | null; set group(value: string | null); /** The shown result. Setting it updates the whole group; with no group, only this component. */ get result(): CartonizationResult | null; set result(value: ResultInput | null | undefined); /** The group's selected algorithm. Ignored when set to an algorithm the result has no solution for. */ get selectedAlgorithm(): string | null; set selectedAlgorithm(value: string | null); /** The group's selected container (1-based). Ignored when set to a container the shown solution doesn't have. */ get selectedContainer(): number; set selectedContainer(value: number); connectedCallback(): void; disconnectedCallback(): void; attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void; addEventListener(type: K, listener: (this: this, ev: CartonizationEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: K, listener: (this: this, ev: CartonizationEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } /** ``: containers, fill rate, packed, unpacked, objective, cost, weight utilization, time; and the unpacked units. */ export declare class CartonizationSummary extends CartonizationElement {} /** ``: fires `cw-solution-select` when a row is clicked (only when the result has `solutions`). */ export declare class CartonizationRanking extends CartonizationElement {} /** ``: fires `cw-container-select` when a row is clicked. */ export declare class CartonizationContainers extends CartonizationElement {} /** * ``: the group's selected container, or the pinned `container`. Removing the element releases its WebGL * context; re-adding it redraws the current state. */ export declare class Cartonization3DView extends CartonizationElement { /** The `container` attribute: a pinned 1-based container index, or null to follow the group's selection. */ get container(): number | null; set container(value: number | null); } declare global { interface HTMLElementTagNameMap { 'cartonization-summary': CartonizationSummary; 'cartonization-ranking': CartonizationRanking; 'cartonization-containers': CartonizationContainers; 'cartonization-3d-view': Cartonization3DView; } }