diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7f76e553..14d7f670 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -56,6 +56,18 @@ "reveal": "never" } }, + { + "label": "Mocked Pixel Agent Dev Server", + "type": "shell", + "command": "cd webview-ui && npm run dev", + "isBackground": true, + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated" + }, + "problemMatcher": [] + }, { "label": "Run CI (act)", "type": "shell", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90ad90ad..e9c1487a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,27 @@ This starts parallel watchers for both the extension backend (esbuild) and TypeS > **Note:** The webview (Vite) is not included in `watch` — after changing webview code, run `npm run build:webview` or the full `npm run build`. +## Running the Mocked Pixel Agent + +You can run the mocked Pixel Agent web app either from the CLI or from VS Code tasks. + +### Option 1: CLI + +From the repository root: + +```bash +cd webview-ui +npm run dev +``` + +Vite will print a local URL (typically `http://localhost:5173`) where the mocked app is available. + +### Option 2: VS Code Run Task + +1. Open the command palette and run **Tasks: Run Task**. +2. Select **Mocked Pixel Agent Dev Server**. +3. Open the local URL shown in the task terminal output (typically `http://localhost:5173`). + ### Project Structure | Directory | Description | diff --git a/shared/assets/build.ts b/shared/assets/build.ts new file mode 100644 index 00000000..3bc96267 --- /dev/null +++ b/shared/assets/build.ts @@ -0,0 +1,136 @@ +/** + * Build-time asset generators — shared between Vite plugin, extension host, + * and future standalone backends. + * + * Reads furniture manifests and asset directories and produces + * catalog and index structures. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import type { CatalogEntry } from './types.js'; +import type { FurnitureManifest, InheritedProps, ManifestGroup } from './manifestUtils.js'; +import { flattenManifest } from './manifestUtils.js'; + +// ── Furniture catalog ───────────────────────────────────────────────────────── + +export function buildFurnitureCatalog(assetsDir: string): CatalogEntry[] { + const furnitureDir = path.join(assetsDir, 'furniture'); + if (!fs.existsSync(furnitureDir)) return []; + + const catalog: CatalogEntry[] = []; + const dirs = fs + .readdirSync(furnitureDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + + for (const folderName of dirs) { + const manifestPath = path.join(furnitureDir, folderName, 'manifest.json'); + if (!fs.existsSync(manifestPath)) continue; + try { + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as FurnitureManifest; + + if (manifest.type === 'asset') { + // Single-asset manifest — validate required fields + if ( + manifest.width == null || + manifest.height == null || + manifest.footprintW == null || + manifest.footprintH == null + ) { + continue; + } + const file = manifest.file ?? `${manifest.id}.png`; + catalog.push({ + id: manifest.id, + name: manifest.name, + label: manifest.name, + category: manifest.category, + file, + furniturePath: `furniture/${folderName}/${file}`, + width: manifest.width, + height: manifest.height, + footprintW: manifest.footprintW, + footprintH: manifest.footprintH, + isDesk: manifest.category === 'desks', + canPlaceOnWalls: manifest.canPlaceOnWalls, + canPlaceOnSurfaces: manifest.canPlaceOnSurfaces, + backgroundTiles: manifest.backgroundTiles, + groupId: manifest.id, + }); + } else { + // Group manifest — flatten into individual assets + if (!manifest.members) continue; + const inherited: InheritedProps = { + groupId: manifest.id, + name: manifest.name, + category: manifest.category, + canPlaceOnWalls: manifest.canPlaceOnWalls, + canPlaceOnSurfaces: manifest.canPlaceOnSurfaces, + backgroundTiles: manifest.backgroundTiles, + ...(manifest.rotationScheme ? { rotationScheme: manifest.rotationScheme } : {}), + }; + const rootGroup: ManifestGroup = { + type: 'group', + groupType: manifest.groupType as 'rotation' | 'state' | 'animation', + rotationScheme: manifest.rotationScheme, + members: manifest.members, + }; + const assets = flattenManifest(rootGroup, inherited); + for (const asset of assets) { + catalog.push({ + ...asset, + furniturePath: `furniture/${folderName}/${asset.file}`, + }); + } + } + } catch { + // skip malformed manifests + } + } + return catalog; +} + +// ── Asset index ─────────────────────────────────────────────────────────────── + +export function buildAssetIndex(assetsDir: string) { + function listSorted(subdir: string, pattern: RegExp): string[] { + const dir = path.join(assetsDir, subdir); + if (!fs.existsSync(dir)) return []; + return fs + .readdirSync(dir) + .filter((f) => pattern.test(f)) + .sort((a, b) => { + const na = parseInt(/(\d+)/.exec(a)?.[1] ?? '0', 10); + const nb = parseInt(/(\d+)/.exec(b)?.[1] ?? '0', 10); + return na - nb; + }); + } + + let defaultLayout: string | null = null; + let bestRev = 0; + if (fs.existsSync(assetsDir)) { + for (const f of fs.readdirSync(assetsDir)) { + const m = /^default-layout-(\d+)\.json$/.exec(f); + if (m) { + const rev = parseInt(m[1], 10); + if (rev > bestRev) { + bestRev = rev; + defaultLayout = f; + } + } + } + if (!defaultLayout && fs.existsSync(path.join(assetsDir, 'default-layout.json'))) { + defaultLayout = 'default-layout.json'; + } + } + + return { + floors: listSorted('floors', /^floor_\d+\.png$/i), + walls: listSorted('walls', /^wall_\d+\.png$/i), + characters: listSorted('characters', /^char_\d+\.png$/i), + defaultLayout, + }; +} diff --git a/shared/assets/constants.ts b/shared/assets/constants.ts new file mode 100644 index 00000000..d2f9fac0 --- /dev/null +++ b/shared/assets/constants.ts @@ -0,0 +1,19 @@ +/** + * Shared constants — used by the extension host, Vite build scripts, + * and future standalone backend. + * + * No VS Code dependency. Only asset parsing and layout-related values. + */ + +// ── PNG / Asset Parsing ───────────────────────────────────── +export const PNG_ALPHA_THRESHOLD = 2; +export const WALL_PIECE_WIDTH = 16; +export const WALL_PIECE_HEIGHT = 32; +export const WALL_GRID_COLS = 4; +export const WALL_BITMASK_COUNT = 16; +export const FLOOR_TILE_SIZE = 16; +export const CHARACTER_DIRECTIONS = ['down', 'up', 'right'] as const; +export const CHAR_FRAME_W = 16; +export const CHAR_FRAME_H = 32; +export const CHAR_FRAMES_PER_ROW = 7; +export const CHAR_COUNT = 6; diff --git a/shared/assets/loader.ts b/shared/assets/loader.ts new file mode 100644 index 00000000..41a702f8 --- /dev/null +++ b/shared/assets/loader.ts @@ -0,0 +1,74 @@ +/** + * Server-side asset decoders — shared between Vite plugin, extension host, + * and future standalone backends. + * + * Reads PNG files from an assets directory and decodes them into SpriteData + * format using the shared pngDecoder module. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import { decodeCharacterPng, decodeFloorPng, parseWallPng, pngToSpriteData } from './pngDecoder.js'; +import type { CatalogEntry, CharacterDirectionSprites } from './types.js'; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function listSortedPngs(dir: string, pattern: RegExp): { index: number; filename: string }[] { + if (!fs.existsSync(dir)) return []; + const files: { index: number; filename: string }[] = []; + for (const entry of fs.readdirSync(dir)) { + const match = pattern.exec(entry); + if (match) { + files.push({ index: parseInt(match[1], 10), filename: entry }); + } + } + return files.sort((a, b) => a.index - b.index); +} + +// ── Decoders ───────────────────────────────────────────────────────────────── + +export function decodeAllCharacters(assetsDir: string): CharacterDirectionSprites[] { + const charDir = path.join(assetsDir, 'characters'); + const files = listSortedPngs(charDir, /^char_(\d+)\.png$/i); + return files.map(({ filename }) => { + const pngBuffer = fs.readFileSync(path.join(charDir, filename)); + return decodeCharacterPng(pngBuffer); + }); +} + +export function decodeAllFloors(assetsDir: string): string[][][] { + const floorsDir = path.join(assetsDir, 'floors'); + const files = listSortedPngs(floorsDir, /^floor_(\d+)\.png$/i); + return files.map(({ filename }) => { + const pngBuffer = fs.readFileSync(path.join(floorsDir, filename)); + return decodeFloorPng(pngBuffer); + }); +} + +export function decodeAllWalls(assetsDir: string): string[][][][] { + const wallsDir = path.join(assetsDir, 'walls'); + const files = listSortedPngs(wallsDir, /^wall_(\d+)\.png$/i); + return files.map(({ filename }) => { + const pngBuffer = fs.readFileSync(path.join(wallsDir, filename)); + return parseWallPng(pngBuffer); + }); +} + +export function decodeAllFurniture( + assetsDir: string, + catalog: CatalogEntry[], +): Record { + const sprites: Record = {}; + for (const entry of catalog) { + try { + const filePath = path.join(assetsDir, entry.furniturePath); + if (!fs.existsSync(filePath)) continue; + const pngBuffer = fs.readFileSync(filePath); + sprites[entry.id] = pngToSpriteData(pngBuffer, entry.width, entry.height); + } catch (err) { + console.warn(`[decodeAssets] Failed to decode ${entry.id}:`, err); + } + } + return sprites; +} diff --git a/shared/assets/manifestUtils.ts b/shared/assets/manifestUtils.ts new file mode 100644 index 00000000..236256dd --- /dev/null +++ b/shared/assets/manifestUtils.ts @@ -0,0 +1,173 @@ +/** + * Manifest flattening utilities — shared between the extension host, Vite build + * scripts, and future standalone backend. + * + * Recursively flattens furniture manifest trees into flat asset arrays. + */ + +// ── Manifest types ────────────────────────────────────────── + +export interface ManifestAsset { + type: 'asset'; + id: string; + file: string; + width: number; + height: number; + footprintW: number; + footprintH: number; + orientation?: string; + state?: string; + frame?: number; + mirrorSide?: boolean; +} + +export interface ManifestGroup { + type: 'group'; + groupType: 'rotation' | 'state' | 'animation'; + rotationScheme?: string; + orientation?: string; + state?: string; + members: ManifestNode[]; +} + +export type ManifestNode = ManifestAsset | ManifestGroup; + +export interface FurnitureManifest { + id: string; + name: string; + category: string; + canPlaceOnWalls: boolean; + canPlaceOnSurfaces: boolean; + backgroundTiles: number; + // If type is 'asset', these fields are present: + type: 'asset' | 'group'; + file?: string; + width?: number; + height?: number; + footprintW?: number; + footprintH?: number; + // If type is 'group': + groupType?: string; + rotationScheme?: string; + members?: ManifestNode[]; +} + +export interface InheritedProps { + groupId: string; + name: string; + category: string; + canPlaceOnWalls: boolean; + canPlaceOnSurfaces: boolean; + backgroundTiles: number; + orientation?: string; + state?: string; + rotationScheme?: string; + animationGroup?: string; +} + +export interface FurnitureAsset { + id: string; + name: string; + label: string; + category: string; + file: string; + width: number; + height: number; + footprintW: number; + footprintH: number; + isDesk: boolean; + canPlaceOnWalls: boolean; + groupId?: string; + canPlaceOnSurfaces?: boolean; + backgroundTiles?: number; + orientation?: string; + state?: string; + mirrorSide?: boolean; + rotationScheme?: string; + animationGroup?: string; + frame?: number; +} + +/** + * Recursively flatten a manifest node into FurnitureAsset[]. + * Inherited properties flow from root to all leaf assets. + */ +export function flattenManifest(node: ManifestNode, inherited: InheritedProps): FurnitureAsset[] { + if (node.type === 'asset') { + const asset = node as ManifestAsset; + // Merge orientation: node-level takes priority, then inherited + const orientation = asset.orientation ?? inherited.orientation; + const state = asset.state ?? inherited.state; + return [ + { + id: asset.id, + name: inherited.name, + label: inherited.name, + category: inherited.category, + file: asset.file, + width: asset.width, + height: asset.height, + footprintW: asset.footprintW, + footprintH: asset.footprintH, + isDesk: inherited.category === 'desks', + canPlaceOnWalls: inherited.canPlaceOnWalls, + canPlaceOnSurfaces: inherited.canPlaceOnSurfaces, + backgroundTiles: inherited.backgroundTiles, + groupId: inherited.groupId, + ...(orientation ? { orientation } : {}), + ...(state ? { state } : {}), + ...(asset.mirrorSide ? { mirrorSide: true } : {}), + ...(inherited.rotationScheme ? { rotationScheme: inherited.rotationScheme } : {}), + ...(inherited.animationGroup ? { animationGroup: inherited.animationGroup } : {}), + ...(asset.frame !== undefined ? { frame: asset.frame } : {}), + }, + ]; + } + + // Group node + const group = node as ManifestGroup; + const results: FurnitureAsset[] = []; + + for (const member of group.members) { + // Build inherited props for children + const childProps: InheritedProps = { ...inherited }; + + if (group.groupType === 'rotation') { + // Rotation groups set groupId and pass rotationScheme + if (group.rotationScheme) { + childProps.rotationScheme = group.rotationScheme; + } + } + + if (group.groupType === 'state') { + // State groups propagate orientation from the group level + if (group.orientation) { + childProps.orientation = group.orientation; + } + // Propagate state from group level if set (for animation groups nested in state) + if (group.state) { + childProps.state = group.state; + } + } + + if (group.groupType === 'animation') { + // Animation groups: create animation group ID and propagate state + // Use the parent's orientation to build a unique animation group name + const orient = group.orientation ?? inherited.orientation ?? ''; + const st = group.state ?? inherited.state ?? ''; + childProps.animationGroup = `${inherited.groupId}_${orient}_${st}`.toUpperCase(); + if (group.state) { + childProps.state = group.state; + } + } + + // Propagate orientation from group to children (for state groups that have orientation) + if (group.orientation && !childProps.orientation) { + childProps.orientation = group.orientation; + } + + results.push(...flattenManifest(member, childProps)); + } + + return results; +} diff --git a/shared/assets/pngDecoder.ts b/shared/assets/pngDecoder.ts new file mode 100644 index 00000000..183603a5 --- /dev/null +++ b/shared/assets/pngDecoder.ts @@ -0,0 +1,147 @@ +/** + * Pure PNG decoding utilities — shared between the extension host, Vite build + * scripts, and future standalone backend. + * + * No VS Code dependency. Only uses pngjs and shared constants. + */ + +import { PNG } from 'pngjs'; + +import { + CHAR_FRAME_H, + CHAR_FRAME_W, + CHAR_FRAMES_PER_ROW, + CHARACTER_DIRECTIONS, + FLOOR_TILE_SIZE, + PNG_ALPHA_THRESHOLD, + WALL_BITMASK_COUNT, + WALL_GRID_COLS, + WALL_PIECE_HEIGHT, + WALL_PIECE_WIDTH, +} from './constants.js'; +import type { CharacterDirectionSprites } from './types.js'; +export type { CharacterDirectionSprites } from './types.js'; + +// ── Pixel conversion ───────────────────────────────────────── + +export function rgbaToHex(r: number, g: number, b: number, a: number): string { + if (a < PNG_ALPHA_THRESHOLD) return ''; + const rgb = + `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(); + if (a >= 255) return rgb; + return `${rgb}${a.toString(16).padStart(2, '0').toUpperCase()}`; +} + +// ── Sprite decoding ────────────────────────────────────────── + +/** + * Convert a PNG buffer to SpriteData (2D array of hex color strings). + * '' = transparent, '#RRGGBB' = opaque, '#RRGGBBAA' = semi-transparent. + */ +export function pngToSpriteData(pngBuffer: Buffer, width: number, height: number): string[][] { + try { + const png = PNG.sync.read(pngBuffer); + + if (png.width !== width || png.height !== height) { + console.warn( + `PNG dimensions mismatch: expected ${width}×${height}, got ${png.width}×${png.height}`, + ); + } + + const sprite: string[][] = []; + const data = png.data; + + for (let y = 0; y < height; y++) { + const row: string[] = []; + for (let x = 0; x < width; x++) { + const pixelIndex = (y * png.width + x) * 4; + const r = data[pixelIndex]; + const g = data[pixelIndex + 1]; + const b = data[pixelIndex + 2]; + const a = data[pixelIndex + 3]; + row.push(rgbaToHex(r, g, b, a)); + } + sprite.push(row); + } + + return sprite; + } catch (err) { + console.warn(`Failed to parse PNG: ${err instanceof Error ? err.message : err}`); + const sprite: string[][] = []; + for (let y = 0; y < height; y++) { + sprite.push(new Array(width).fill('')); + } + return sprite; + } +} + +/** + * Parse a single wall PNG (64×128, 4×4 grid of 16×32 pieces) into 16 bitmask sprites. + * Piece at bitmask M: col = M % 4, row = floor(M / 4). + */ +export function parseWallPng(pngBuffer: Buffer): string[][][] { + const png = PNG.sync.read(pngBuffer); + const sprites: string[][][] = []; + for (let mask = 0; mask < WALL_BITMASK_COUNT; mask++) { + const ox = (mask % WALL_GRID_COLS) * WALL_PIECE_WIDTH; + const oy = Math.floor(mask / WALL_GRID_COLS) * WALL_PIECE_HEIGHT; + const sprite: string[][] = []; + for (let r = 0; r < WALL_PIECE_HEIGHT; r++) { + const row: string[] = []; + for (let c = 0; c < WALL_PIECE_WIDTH; c++) { + const idx = ((oy + r) * png.width + (ox + c)) * 4; + const rv = png.data[idx]; + const gv = png.data[idx + 1]; + const bv = png.data[idx + 2]; + const av = png.data[idx + 3]; + row.push(rgbaToHex(rv, gv, bv, av)); + } + sprite.push(row); + } + sprites.push(sprite); + } + return sprites; +} + +/** + * Decode a single character PNG (112×96) into direction-keyed frame arrays. + * Each PNG has 3 direction rows (down, up, right) × 7 frames (16×32 each). + */ +export function decodeCharacterPng(pngBuffer: Buffer): CharacterDirectionSprites { + const png = PNG.sync.read(pngBuffer); + const charData: CharacterDirectionSprites = { down: [], up: [], right: [] }; + + for (let dirIdx = 0; dirIdx < CHARACTER_DIRECTIONS.length; dirIdx++) { + const dir = CHARACTER_DIRECTIONS[dirIdx]; + const rowOffsetY = dirIdx * CHAR_FRAME_H; + const frames: string[][][] = []; + + for (let f = 0; f < CHAR_FRAMES_PER_ROW; f++) { + const sprite: string[][] = []; + const frameOffsetX = f * CHAR_FRAME_W; + for (let y = 0; y < CHAR_FRAME_H; y++) { + const row: string[] = []; + for (let x = 0; x < CHAR_FRAME_W; x++) { + const idx = ((rowOffsetY + y) * png.width + (frameOffsetX + x)) * 4; + const r = png.data[idx]; + const g = png.data[idx + 1]; + const b = png.data[idx + 2]; + const a = png.data[idx + 3]; + row.push(rgbaToHex(r, g, b, a)); + } + sprite.push(row); + } + frames.push(sprite); + } + charData[dir] = frames; + } + + return charData; +} + +/** + * Decode a single floor tile PNG (16×16 grayscale pattern). + */ +export function decodeFloorPng(pngBuffer: Buffer): string[][] { + return pngToSpriteData(pngBuffer, FLOOR_TILE_SIZE, FLOOR_TILE_SIZE); +} diff --git a/shared/assets/types.ts b/shared/assets/types.ts new file mode 100644 index 00000000..9072d383 --- /dev/null +++ b/shared/assets/types.ts @@ -0,0 +1,41 @@ +/** + * Asset pipeline types — shared between the extension host, Vite build + * scripts, browser mock, and future standalone backends. + */ + +export interface CharacterDirectionSprites { + down: string[][][]; + up: string[][][]; + right: string[][][]; +} + +export interface AssetIndex { + floors: string[]; + walls: string[]; + characters: string[]; + defaultLayout: string | null; +} + +export interface CatalogEntry { + id: string; + name: string; + label: string; + category: string; + file: string; + furniturePath: string; + width: number; + height: number; + footprintW: number; + footprintH: number; + isDesk: boolean; + canPlaceOnWalls: boolean; + canPlaceOnSurfaces?: boolean; + backgroundTiles?: number; + groupId?: string; + orientation?: string; + state?: string; + mirrorSide?: boolean; + rotationScheme?: string; + animationGroup?: string; + frame?: number; +} diff --git a/src/assetLoader.ts b/src/assetLoader.ts index 03a1e96c..0e5f93fa 100644 --- a/src/assetLoader.ts +++ b/src/assetLoader.ts @@ -7,196 +7,34 @@ import * as fs from 'fs'; import * as path from 'path'; -import { PNG } from 'pngjs'; import * as vscode from 'vscode'; +import { CHAR_COUNT, CHAR_FRAMES_PER_ROW, WALL_BITMASK_COUNT } from '../shared/assets/constants.js'; +import type { + FurnitureAsset, + FurnitureManifest, + InheritedProps, + ManifestGroup, +} from '../shared/assets/manifestUtils.js'; +import { flattenManifest } from '../shared/assets/manifestUtils.js'; import { - CHAR_COUNT, - CHAR_FRAME_H, - CHAR_FRAME_W, - CHAR_FRAMES_PER_ROW, - CHARACTER_DIRECTIONS, - FLOOR_TILE_SIZE, - LAYOUT_REVISION_KEY, - PNG_ALPHA_THRESHOLD, - WALL_BITMASK_COUNT, - WALL_GRID_COLS, - WALL_PIECE_HEIGHT, - WALL_PIECE_WIDTH, -} from './constants.js'; - -export interface FurnitureAsset { - id: string; - name: string; - label: string; - category: string; - file: string; - width: number; - height: number; - footprintW: number; - footprintH: number; - isDesk: boolean; - canPlaceOnWalls: boolean; - groupId?: string; - canPlaceOnSurfaces?: boolean; - backgroundTiles?: number; - orientation?: string; - state?: string; - mirrorSide?: boolean; - rotationScheme?: string; - animationGroup?: string; - frame?: number; -} + decodeCharacterPng, + decodeFloorPng, + parseWallPng, + pngToSpriteData, +} from '../shared/assets/pngDecoder.js'; +import type { CharacterDirectionSprites } from '../shared/assets/types.js'; +export type { CharacterDirectionSprites } from '../shared/assets/types.js'; + +import { LAYOUT_REVISION_KEY } from './constants.js'; + +export type { FurnitureAsset }; export interface LoadedAssets { catalog: FurnitureAsset[]; sprites: Map; // assetId -> SpriteData } -// ── Manifest types ────────────────────────────────────────── - -interface ManifestAsset { - type: 'asset'; - id: string; - file: string; - width: number; - height: number; - footprintW: number; - footprintH: number; - orientation?: string; - state?: string; - frame?: number; - mirrorSide?: boolean; -} - -interface ManifestGroup { - type: 'group'; - groupType: 'rotation' | 'state' | 'animation'; - rotationScheme?: string; - orientation?: string; - state?: string; - members: ManifestNode[]; -} - -type ManifestNode = ManifestAsset | ManifestGroup; - -interface FurnitureManifest { - id: string; - name: string; - category: string; - canPlaceOnWalls: boolean; - canPlaceOnSurfaces: boolean; - backgroundTiles: number; - // If type is 'asset', these fields are present: - type: 'asset' | 'group'; - file?: string; - width?: number; - height?: number; - footprintW?: number; - footprintH?: number; - // If type is 'group': - groupType?: string; - rotationScheme?: string; - members?: ManifestNode[]; -} - -interface InheritedProps { - groupId: string; - name: string; - category: string; - canPlaceOnWalls: boolean; - canPlaceOnSurfaces: boolean; - backgroundTiles: number; - orientation?: string; - state?: string; - rotationScheme?: string; - animationGroup?: string; -} - -/** - * Recursively flatten a manifest node into FurnitureAsset[]. - * Inherited properties flow from root to all leaf assets. - */ -function flattenManifest(node: ManifestNode, inherited: InheritedProps): FurnitureAsset[] { - if (node.type === 'asset') { - const asset = node as ManifestAsset; - // Merge orientation: node-level takes priority, then inherited - const orientation = asset.orientation ?? inherited.orientation; - const state = asset.state ?? inherited.state; - return [ - { - id: asset.id, - name: inherited.name, - label: inherited.name, - category: inherited.category, - file: asset.file, - width: asset.width, - height: asset.height, - footprintW: asset.footprintW, - footprintH: asset.footprintH, - isDesk: inherited.category === 'desks', - canPlaceOnWalls: inherited.canPlaceOnWalls, - canPlaceOnSurfaces: inherited.canPlaceOnSurfaces, - backgroundTiles: inherited.backgroundTiles, - groupId: inherited.groupId, - ...(orientation ? { orientation } : {}), - ...(state ? { state } : {}), - ...(asset.mirrorSide ? { mirrorSide: true } : {}), - ...(inherited.rotationScheme ? { rotationScheme: inherited.rotationScheme } : {}), - ...(inherited.animationGroup ? { animationGroup: inherited.animationGroup } : {}), - ...(asset.frame !== undefined ? { frame: asset.frame } : {}), - }, - ]; - } - - // Group node - const group = node as ManifestGroup; - const results: FurnitureAsset[] = []; - - for (const member of group.members) { - // Build inherited props for children - const childProps: InheritedProps = { ...inherited }; - - if (group.groupType === 'rotation') { - // Rotation groups set groupId and pass rotationScheme - if (group.rotationScheme) { - childProps.rotationScheme = group.rotationScheme; - } - } - - if (group.groupType === 'state') { - // State groups propagate orientation from the group level - if (group.orientation) { - childProps.orientation = group.orientation; - } - // Propagate state from group level if set (for animation groups nested in state) - if (group.state) { - childProps.state = group.state; - } - } - - if (group.groupType === 'animation') { - // Animation groups: create animation group ID and propagate state - // Use the parent's orientation to build a unique animation group name - const orient = group.orientation ?? inherited.orientation ?? ''; - const state = group.state ?? inherited.state ?? ''; - childProps.animationGroup = `${inherited.groupId}_${orient}_${state}`.toUpperCase(); - if (group.state) { - childProps.state = group.state; - } - } - - // Propagate orientation from group to children (for state groups that have orientation) - if (group.orientation && !childProps.orientation) { - childProps.orientation = group.orientation; - } - - results.push(...flattenManifest(member, childProps)); - } - - return results; -} - /** * Load furniture assets from per-folder manifests */ @@ -322,61 +160,6 @@ export async function loadFurnitureAssets(workspaceRoot: string): Promise= 255) return rgb; - return `${rgb}${a.toString(16).padStart(2, '0').toUpperCase()}`; -} - -function pngToSpriteData(pngBuffer: Buffer, width: number, height: number): string[][] { - try { - // Parse PNG using pngjs - const png = PNG.sync.read(pngBuffer); - - if (png.width !== width || png.height !== height) { - console.warn( - `PNG dimensions mismatch: expected ${width}×${height}, got ${png.width}×${png.height}`, - ); - } - - const sprite: string[][] = []; - const data = png.data; // Uint8Array with RGBA values - - for (let y = 0; y < height; y++) { - const row: string[] = []; - for (let x = 0; x < width; x++) { - const pixelIndex = (y * png.width + x) * 4; - - const r = data[pixelIndex]; - const g = data[pixelIndex + 1]; - const b = data[pixelIndex + 2]; - const a = data[pixelIndex + 3]; - - row.push(rgbaToHex(r, g, b, a)); - } - sprite.push(row); - } - - return sprite; - } catch (err) { - console.warn(`Failed to parse PNG: ${err instanceof Error ? err.message : err}`); - // Return transparent placeholder - const sprite: string[][] = []; - for (let y = 0; y < height; y++) { - sprite.push(new Array(width).fill('')); - } - return sprite; - } -} - // ── Default layout loading ─────────────────────────────────── /** @@ -443,34 +226,6 @@ export interface LoadedWallTiles { sets: string[][][][]; } -/** - * Parse a single wall PNG (64×128, 4×4 grid of 16×32 pieces) into 16 bitmask sprites. - * Piece at bitmask M: col = M % 4, row = floor(M / 4). - */ -function parseWallPng(pngBuffer: Buffer): string[][][] { - const png = PNG.sync.read(pngBuffer); - const sprites: string[][][] = []; - for (let mask = 0; mask < WALL_BITMASK_COUNT; mask++) { - const ox = (mask % WALL_GRID_COLS) * WALL_PIECE_WIDTH; - const oy = Math.floor(mask / WALL_GRID_COLS) * WALL_PIECE_HEIGHT; - const sprite: string[][] = []; - for (let r = 0; r < WALL_PIECE_HEIGHT; r++) { - const row: string[] = []; - for (let c = 0; c < WALL_PIECE_WIDTH; c++) { - const idx = ((oy + r) * png.width + (ox + c)) * 4; - const rv = png.data[idx]; - const gv = png.data[idx + 1]; - const bv = png.data[idx + 2]; - const av = png.data[idx + 3]; - row.push(rgbaToHex(rv, gv, bv, av)); - } - sprite.push(row); - } - sprites.push(sprite); - } - return sprites; -} - /** * Load wall tile sets from assets/walls/ folder. * Each file is named wall_N.png (e.g. wall_0.png, wall_1.png, ...). @@ -574,7 +329,7 @@ export async function loadFloorTiles(assetsRoot: string): Promise=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", - "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -289,6 +309,8 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -300,6 +322,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -308,6 +332,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -321,6 +347,8 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -332,6 +360,8 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -343,6 +373,8 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -365,6 +397,8 @@ }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -376,6 +410,8 @@ }, "node_modules/@eslint/js": { "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -387,6 +423,8 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -395,6 +433,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -407,6 +447,8 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -415,6 +457,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -427,6 +471,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -439,6 +485,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -451,6 +499,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -460,6 +510,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -469,6 +521,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -477,11 +531,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -489,23 +547,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@oxc-project/runtime": { "version": "0.115.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", @@ -526,159 +567,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", - "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.0-rc.9", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", @@ -696,91 +584,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", - "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.7", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", @@ -788,24 +591,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, @@ -819,6 +615,16 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/pngjs": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz", + "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -831,6 +637,8 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1160,6 +968,8 @@ }, "node_modules/acorn": { "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1171,6 +981,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1179,6 +991,8 @@ }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1194,6 +1008,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1208,16 +1024,22 @@ }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1226,6 +1048,8 @@ }, "node_modules/brace-expansion": { "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1235,6 +1059,8 @@ }, "node_modules/browserslist": { "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1267,6 +1093,8 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -1275,6 +1103,8 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { @@ -1294,6 +1124,8 @@ }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -1309,6 +1141,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1320,21 +1154,29 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1348,11 +1190,15 @@ }, "node_modules/csstype": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1369,6 +1215,8 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, @@ -1384,11 +1232,57 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true, "license": "ISC" }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -1397,6 +1291,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -1408,6 +1304,8 @@ }, "node_modules/eslint": { "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -1466,6 +1364,8 @@ }, "node_modules/eslint-config-prettier": { "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -1480,6 +1380,8 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1508,6 +1410,8 @@ }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1516,6 +1420,8 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1531,6 +1437,8 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1542,6 +1450,8 @@ }, "node_modules/espree": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1558,6 +1468,8 @@ }, "node_modules/esquery": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1569,6 +1481,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1580,6 +1494,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -1588,6 +1504,8 @@ }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -1596,21 +1514,29 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -1627,6 +1553,8 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1638,6 +1566,8 @@ }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -1653,6 +1583,8 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -1665,34 +1597,38 @@ }, "node_modules/flatted": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6.9.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -1717,6 +1653,8 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -1725,11 +1663,15 @@ }, "node_modules/hermes-estree": { "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, "node_modules/hermes-parser": { "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { @@ -1738,6 +1680,8 @@ }, "node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -1746,6 +1690,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1761,6 +1707,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -1769,6 +1717,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -1777,6 +1727,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1788,16 +1740,22 @@ }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1809,6 +1767,8 @@ }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -1820,21 +1780,29 @@ }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -1846,6 +1814,8 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -1854,6 +1824,8 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1894,153 +1866,6 @@ "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", @@ -2062,71 +1887,10 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -2141,11 +1905,15 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -2154,6 +1922,8 @@ }, "node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -2165,6 +1935,8 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -2189,16 +1961,22 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -2215,6 +1993,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2229,6 +2009,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -2243,6 +2025,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -2254,6 +2038,8 @@ }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -2262,6 +2048,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -2270,11 +2058,15 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2284,6 +2076,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -2315,6 +2117,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -2323,6 +2127,8 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -2331,6 +2137,8 @@ }, "node_modules/react": { "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2338,6 +2146,8 @@ }, "node_modules/react-dom": { "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -2348,12 +2158,24 @@ }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.9", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", @@ -2397,10 +2219,14 @@ }, "node_modules/scheduler": { "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -2409,6 +2235,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -2420,6 +2248,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -2438,6 +2268,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -2449,6 +2281,8 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -2460,6 +2294,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2486,16 +2322,30 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, - "license": "0BSD", - "optional": true + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -2507,6 +2357,8 @@ }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2550,6 +2402,8 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2579,6 +2433,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2666,6 +2522,8 @@ }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -2680,6 +2538,8 @@ }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -2688,11 +2548,15 @@ }, "node_modules/yallist": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -2704,6 +2568,8 @@ }, "node_modules/zod": { "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { @@ -2712,6 +2578,8 @@ }, "node_modules/zod-validation-error": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true, "license": "MIT", "engines": { diff --git a/webview-ui/package.json b/webview-ui/package.json index a411580e..7b1e3226 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "node --import tsx/esm --test test/*.test.ts" }, "dependencies": { "react": "^19.2.0", @@ -16,6 +17,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^25.5.0", + "@types/pngjs": "^6.0.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -25,8 +27,10 @@ "eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-simple-import-sort": "^12.1.1", "globals": "^17.4.0", + "pngjs": "^7.0.0", "typescript": "~5.9.3", "typescript-eslint": "^8.57.0", + "tsx": "^4.19.3", "vite": "^8.0.0" } } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index ddeca9c9..1c86d706 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { BottomToolbar } from './components/BottomToolbar.js'; import { DebugView } from './components/DebugView.js'; @@ -14,6 +14,7 @@ import { EditorToolbar } from './office/editor/EditorToolbar.js'; import { OfficeState } from './office/engine/officeState.js'; import { isRotatable } from './office/layout/furnitureCatalog.js'; import { EditTool } from './office/types.js'; +import { isBrowserRuntime } from './runtime.js'; import { vscode } from './vscodeApi.js'; // Game state lives outside React — updated imperatively by message handlers @@ -120,6 +121,14 @@ function EditActionBar({ } function App() { + // Browser runtime (dev or static dist): dispatch mock messages after the + // useExtensionMessages listener has been registered. + useEffect(() => { + if (isBrowserRuntime) { + void import('./browserMock.js').then(({ dispatchMockMessages }) => dispatchMockMessages()); + } + }, []); + const editor = useEditorActions(getOfficeState, editorState); const isEditDirty = useCallback( diff --git a/webview-ui/src/browserMock.ts b/webview-ui/src/browserMock.ts new file mode 100644 index 00000000..19289de6 --- /dev/null +++ b/webview-ui/src/browserMock.ts @@ -0,0 +1,270 @@ +/** + * Browser runtime mock — fetches assets and injects the same postMessage + * events the VS Code extension would send. + * + * In Vite dev, it prefers pre-decoded JSON endpoints from middleware. + * In plain browser builds, it falls back to decoding PNGs at runtime. + * + * Only imported in browser runtime; tree-shaken from VS Code webview runtime. + */ + +import { + CHAR_FRAME_H, + CHAR_FRAME_W, + CHAR_FRAMES_PER_ROW, + CHARACTER_DIRECTIONS, + FLOOR_TILE_SIZE, + PNG_ALPHA_THRESHOLD, + WALL_BITMASK_COUNT, + WALL_GRID_COLS, + WALL_PIECE_HEIGHT, + WALL_PIECE_WIDTH, +} from '../../shared/assets/constants.ts'; +import type { + AssetIndex, + CatalogEntry, + CharacterDirectionSprites, +} from '../../shared/assets/types.ts'; + +interface MockPayload { + characters: CharacterDirectionSprites[]; + floorSprites: string[][][]; + wallSets: string[][][][]; + furnitureCatalog: CatalogEntry[]; + furnitureSprites: Record; + layout: unknown; +} + +// ── Module-level state ───────────────────────────────────────────────────────── + +let mockPayload: MockPayload | null = null; + +// ── PNG decode helpers (browser fallback) ─────────────────────────────────── + +interface DecodedPng { + width: number; + height: number; + data: Uint8ClampedArray; +} + +function rgbaToHex(r: number, g: number, b: number, a: number): string { + if (a < PNG_ALPHA_THRESHOLD) return ''; + const rgb = + `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(); + if (a >= 255) return rgb; + return `${rgb}${a.toString(16).padStart(2, '0').toUpperCase()}`; +} + +function getPixel( + data: Uint8ClampedArray, + width: number, + x: number, + y: number, +): [number, number, number, number] { + const idx = (y * width + x) * 4; + return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]; +} + +function readSprite( + png: DecodedPng, + width: number, + height: number, + offsetX = 0, + offsetY = 0, +): string[][] { + const sprite: string[][] = []; + for (let y = 0; y < height; y++) { + const row: string[] = []; + for (let x = 0; x < width; x++) { + const [r, g, b, a] = getPixel(png.data, png.width, offsetX + x, offsetY + y); + row.push(rgbaToHex(r, g, b, a)); + } + sprite.push(row); + } + return sprite; +} + +async function decodePng(url: string): Promise { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch PNG: ${url} (${res.status.toString()})`); + } + const blob = await res.blob(); + const bitmap = await createImageBitmap(blob); + const canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + bitmap.close(); + throw new Error('Failed to create 2d canvas context for PNG decode'); + } + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + return { width: canvas.width, height: canvas.height, data: imageData.data }; +} + +async function fetchJsonOptional(url: string): Promise { + try { + const res = await fetch(url); + if (!res.ok) return null; + return (await res.json()) as T; + } catch { + return null; + } +} + +function getIndexedAssetPath(kind: 'characters' | 'floors' | 'walls', relPath: string): string { + return relPath.startsWith(`${kind}/`) ? relPath : `${kind}/${relPath}`; +} + +async function decodeCharactersFromPng( + base: string, + index: AssetIndex, +): Promise { + const sprites: CharacterDirectionSprites[] = []; + for (const relPath of index.characters) { + const png = await decodePng(`${base}assets/${getIndexedAssetPath('characters', relPath)}`); + const byDir: CharacterDirectionSprites = { down: [], up: [], right: [] }; + + for (let dirIdx = 0; dirIdx < CHARACTER_DIRECTIONS.length; dirIdx++) { + const dir = CHARACTER_DIRECTIONS[dirIdx]; + const rowOffsetY = dirIdx * CHAR_FRAME_H; + const frames: string[][][] = []; + for (let frame = 0; frame < CHAR_FRAMES_PER_ROW; frame++) { + frames.push(readSprite(png, CHAR_FRAME_W, CHAR_FRAME_H, frame * CHAR_FRAME_W, rowOffsetY)); + } + byDir[dir] = frames; + } + + sprites.push(byDir); + } + return sprites; +} + +async function decodeFloorsFromPng(base: string, index: AssetIndex): Promise { + const floors: string[][][] = []; + for (const relPath of index.floors) { + const png = await decodePng(`${base}assets/${getIndexedAssetPath('floors', relPath)}`); + floors.push(readSprite(png, FLOOR_TILE_SIZE, FLOOR_TILE_SIZE)); + } + return floors; +} + +async function decodeWallsFromPng(base: string, index: AssetIndex): Promise { + const wallSets: string[][][][] = []; + for (const relPath of index.walls) { + const png = await decodePng(`${base}assets/${getIndexedAssetPath('walls', relPath)}`); + const set: string[][][] = []; + for (let mask = 0; mask < WALL_BITMASK_COUNT; mask++) { + const ox = (mask % WALL_GRID_COLS) * WALL_PIECE_WIDTH; + const oy = Math.floor(mask / WALL_GRID_COLS) * WALL_PIECE_HEIGHT; + set.push(readSprite(png, WALL_PIECE_WIDTH, WALL_PIECE_HEIGHT, ox, oy)); + } + wallSets.push(set); + } + return wallSets; +} + +async function decodeFurnitureFromPng( + base: string, + catalog: CatalogEntry[], +): Promise> { + const sprites: Record = {}; + for (const entry of catalog) { + const png = await decodePng(`${base}assets/${entry.furniturePath}`); + sprites[entry.id] = readSprite(png, entry.width, entry.height); + } + return sprites; +} + +// ── Public API ───────────────────────────────────────────────────────────────── + +/** + * Call before createRoot() in main.tsx. + * Fetches all pre-decoded assets from the Vite dev server and stores them + * for dispatchMockMessages(). + */ +export async function initBrowserMock(): Promise { + console.log('[BrowserMock] Loading assets...'); + + const base = import.meta.env.BASE_URL; // '/' in dev, '/sub/' with a subpath, './' in production + + const [assetIndex, catalog] = await Promise.all([ + fetch(`${base}assets/asset-index.json`).then((r) => r.json()) as Promise, + fetch(`${base}assets/furniture-catalog.json`).then((r) => r.json()) as Promise, + ]); + + const shouldTryDecoded = import.meta.env.DEV; + const [decodedCharacters, decodedFloors, decodedWalls, decodedFurniture] = shouldTryDecoded + ? await Promise.all([ + fetchJsonOptional(`${base}assets/decoded/characters.json`), + fetchJsonOptional(`${base}assets/decoded/floors.json`), + fetchJsonOptional(`${base}assets/decoded/walls.json`), + fetchJsonOptional>(`${base}assets/decoded/furniture.json`), + ]) + : [null, null, null, null]; + + const hasDecoded = !!(decodedCharacters && decodedFloors && decodedWalls && decodedFurniture); + + if (!hasDecoded) { + if (shouldTryDecoded) { + console.log('[BrowserMock] Decoded JSON not found, decoding PNG assets in browser...'); + } else { + console.log('[BrowserMock] Decoding PNG assets in browser...'); + } + } + + const [characters, floorSprites, wallSets, furnitureSprites] = hasDecoded + ? [decodedCharacters!, decodedFloors!, decodedWalls!, decodedFurniture!] + : await Promise.all([ + decodeCharactersFromPng(base, assetIndex), + decodeFloorsFromPng(base, assetIndex), + decodeWallsFromPng(base, assetIndex), + decodeFurnitureFromPng(base, catalog), + ]); + + const layout = assetIndex.defaultLayout + ? await fetch(`${base}assets/${assetIndex.defaultLayout}`).then((r) => r.json()) + : null; + + mockPayload = { + characters, + floorSprites, + wallSets, + furnitureCatalog: catalog, + furnitureSprites, + layout, + }; + + console.log( + `[BrowserMock] Ready (${hasDecoded ? 'decoded-json' : 'browser-png-decode'}) — ${characters.length} chars, ${floorSprites.length} floors, ${wallSets.length} wall sets, ${catalog.length} furniture items`, + ); +} + +/** + * Call inside a useEffect in App.tsx — after the window message listener + * in useExtensionMessages has been registered. + */ +export function dispatchMockMessages(): void { + if (!mockPayload) return; + + const { characters, floorSprites, wallSets, furnitureCatalog, furnitureSprites, layout } = + mockPayload; + + function dispatch(data: unknown): void { + window.dispatchEvent(new MessageEvent('message', { data })); + } + + // Must match the load order defined in CLAUDE.md: + // characterSpritesLoaded → floorTilesLoaded → wallTilesLoaded → furnitureAssetsLoaded → layoutLoaded + dispatch({ type: 'characterSpritesLoaded', characters }); + dispatch({ type: 'floorTilesLoaded', sprites: floorSprites }); + dispatch({ type: 'wallTilesLoaded', sets: wallSets }); + dispatch({ type: 'furnitureAssetsLoaded', catalog: furnitureCatalog, sprites: furnitureSprites }); + dispatch({ type: 'layoutLoaded', layout }); + dispatch({ type: 'settingsLoaded', soundEnabled: false }); + + console.log('[BrowserMock] Messages dispatched'); +} diff --git a/webview-ui/src/main.tsx b/webview-ui/src/main.tsx index 212ed2da..ef78cd7e 100644 --- a/webview-ui/src/main.tsx +++ b/webview-ui/src/main.tsx @@ -4,9 +4,18 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.tsx'; +import { isBrowserRuntime } from './runtime'; -createRoot(document.getElementById('root')!).render( - - - , -); +async function main() { + if (isBrowserRuntime) { + const { initBrowserMock } = await import('./browserMock.js'); + await initBrowserMock(); + } + createRoot(document.getElementById('root')!).render( + + + , + ); +} + +main().catch(console.error); diff --git a/webview-ui/src/runtime.ts b/webview-ui/src/runtime.ts new file mode 100644 index 00000000..7bd967a9 --- /dev/null +++ b/webview-ui/src/runtime.ts @@ -0,0 +1,16 @@ +/** + * Runtime detection, provider-agnostic + * + * Single source of truth for determining whether the webview is running + * inside an IDE extension (VS Code, Cursor, Windsurf, etc.) or standalone + * in a browser. + */ + +declare function acquireVsCodeApi(): unknown; + +export type Runtime = 'vscode' | 'browser'; +// Future: 'cursor' | 'windsurf' | 'electron' | etc. + +export const runtime: Runtime = typeof acquireVsCodeApi !== 'undefined' ? 'vscode' : 'browser'; + +export const isBrowserRuntime = runtime === 'browser'; diff --git a/webview-ui/src/vscodeApi.ts b/webview-ui/src/vscodeApi.ts index 1dbdd6d0..60a615fd 100644 --- a/webview-ui/src/vscodeApi.ts +++ b/webview-ui/src/vscodeApi.ts @@ -1,3 +1,7 @@ +import { isBrowserRuntime } from './runtime'; + declare function acquireVsCodeApi(): { postMessage(msg: unknown): void }; -export const vscode = acquireVsCodeApi(); +export const vscode: { postMessage(msg: unknown): void } = isBrowserRuntime + ? { postMessage: (msg: unknown) => console.log('[vscode.postMessage]', msg) } + : (acquireVsCodeApi() as { postMessage(msg: unknown): void }); diff --git a/webview-ui/test/dev-assets.test.ts b/webview-ui/test/dev-assets.test.ts new file mode 100644 index 00000000..29c7ff1b --- /dev/null +++ b/webview-ui/test/dev-assets.test.ts @@ -0,0 +1,103 @@ +/** + * Integration tests for the Vite dev server asset endpoints. + * + * Verifies that `browserMock.ts` can reach all asset JSON endpoints both at + * the root path (base: '/') and under a subpath (base: '/sub/'), matching + * how `import.meta.env.BASE_URL` constructs fetch URLs at runtime. + * + * Run with: npm test + */ + +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import type { ViteDevServer } from 'vite'; +import { createServer } from 'vite'; + +import type { AssetIndex, CatalogEntry } from '../../shared/assets/types.ts'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +async function startDevServer(base: string, port: number): Promise { + const server = await createServer({ + configFile: path.resolve(root, 'vite.config.ts'), + base, + server: { port, strictPort: false }, + logLevel: 'silent', + }); + await server.listen(); + return server; +} + +function serverUrl(server: ViteDevServer): string { + const addr = server.httpServer?.address(); + const port = typeof addr === 'object' && addr !== null ? addr.port : 5173; + return `http://localhost:${port}`; +} + +function assetUrl(baseUrl: string, basePath: string, relPath: string): string { + return `${baseUrl}${basePath}assets/${relPath}`; +} + +async function fetchJson(url: string): Promise { + const res = await fetch(url); + assert.equal(res.status, 200, `GET ${url} returned ${res.status.toString()}`); + return res.json() as Promise; +} + +async function assertUrlOk(url: string): Promise { + const res = await fetch(url); + assert.equal(res.status, 200, `GET ${url} returned ${res.status.toString()}`); +} + +function indexedPath(kind: 'characters' | 'floors' | 'walls', relPath: string): string { + return relPath.startsWith(`${kind}/`) ? relPath : `${kind}/${relPath}`; +} + +async function verifyAssetUrls(baseUrl: string, basePath: string): Promise { + const assetIndex = await fetchJson(assetUrl(baseUrl, basePath, 'asset-index.json')); + const catalog = await fetchJson( + assetUrl(baseUrl, basePath, 'furniture-catalog.json'), + ); + + await assertUrlOk(assetUrl(baseUrl, basePath, 'decoded/characters.json')); + await assertUrlOk(assetUrl(baseUrl, basePath, 'decoded/floors.json')); + await assertUrlOk(assetUrl(baseUrl, basePath, 'decoded/walls.json')); + await assertUrlOk(assetUrl(baseUrl, basePath, 'decoded/furniture.json')); + + assert.ok(assetIndex.floors.length > 0, 'floors index should not be empty'); + assert.ok(assetIndex.walls.length > 0, 'walls index should not be empty'); + assert.ok(assetIndex.characters.length > 0, 'characters index should not be empty'); + assert.ok(catalog.length > 0, 'furniture catalog should not be empty'); + + await assertUrlOk(assetUrl(baseUrl, basePath, indexedPath('floors', assetIndex.floors[0]))); + await assertUrlOk(assetUrl(baseUrl, basePath, indexedPath('walls', assetIndex.walls[0]))); + await assertUrlOk( + assetUrl(baseUrl, basePath, indexedPath('characters', assetIndex.characters[0])), + ); + await assertUrlOk(assetUrl(baseUrl, basePath, catalog[0].furniturePath)); + + if (assetIndex.defaultLayout) { + await assertUrlOk(assetUrl(baseUrl, basePath, assetIndex.defaultLayout)); + } +} + +test('asset-index.json is accessible without a subpath (base: /)', async () => { + const server = await startDevServer('/', 5174); + try { + await verifyAssetUrls(serverUrl(server), '/'); + } finally { + await server.close(); + } +}); + +test('asset-index.json is accessible with a subpath (base: /sub/)', async () => { + const server = await startDevServer('/sub/', 5175); + try { + await verifyAssetUrls(serverUrl(server), '/sub/'); + } finally { + await server.close(); + } +}); diff --git a/webview-ui/tsconfig.node.json b/webview-ui/tsconfig.node.json index 8a67f62f..8d1fa1a9 100644 --- a/webview-ui/tsconfig.node.json +++ b/webview-ui/tsconfig.node.json @@ -4,6 +4,10 @@ "target": "ES2023", "lib": ["ES2023"], "module": "ESNext", + "baseUrl": ".", + "paths": { + "pngjs": ["./node_modules/@types/pngjs/index.d.ts"] + }, "types": ["node"], "skipLibCheck": true, @@ -22,5 +26,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "../shared/assets/**/*.ts", "test/**/*.ts"] } diff --git a/webview-ui/vite.config.ts b/webview-ui/vite.config.ts index 4ad463e4..e0d0c058 100644 --- a/webview-ui/vite.config.ts +++ b/webview-ui/vite.config.ts @@ -1,8 +1,105 @@ import react from '@vitejs/plugin-react'; +import * as fs from 'fs'; +import * as path from 'path'; +import type { Plugin } from 'vite'; import { defineConfig } from 'vite'; +import { buildAssetIndex, buildFurnitureCatalog } from '../shared/assets/build.ts'; +import { + decodeAllCharacters, + decodeAllFloors, + decodeAllFurniture, + decodeAllWalls, +} from '../shared/assets/loader.ts'; + +// ── Decoded asset cache (invalidated on file change) ───────────────────────── + +interface DecodedCache { + characters: ReturnType | null; + floors: ReturnType | null; + walls: ReturnType | null; + furniture: ReturnType | null; +} + +// ── Vite plugin ─────────────────────────────────────────────────────────────── + +function browserMockAssetsPlugin(): Plugin { + const assetsDir = path.resolve(__dirname, 'public/assets'); + const distAssetsDir = path.resolve(__dirname, '../dist/webview/assets'); + + const cache: DecodedCache = { characters: null, floors: null, walls: null, furniture: null }; + + function clearCache(): void { + cache.characters = null; + cache.floors = null; + cache.walls = null; + cache.furniture = null; + } + + return { + name: 'browser-mock-assets', + configureServer(server) { + // Strip trailing slash: '/' → '', '/sub/' → '/sub' + const base = server.config.base.replace(/\/$/, ''); + + // Catalog & index (existing) + server.middlewares.use(`${base}/assets/furniture-catalog.json`, (_req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(buildFurnitureCatalog(assetsDir))); + }); + server.middlewares.use(`${base}/assets/asset-index.json`, (_req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(buildAssetIndex(assetsDir))); + }); + + // Pre-decoded sprites (new — eliminates browser-side PNG decoding) + server.middlewares.use(`${base}/assets/decoded/characters.json`, (_req, res) => { + cache.characters ??= decodeAllCharacters(assetsDir); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(cache.characters)); + }); + server.middlewares.use(`${base}/assets/decoded/floors.json`, (_req, res) => { + cache.floors ??= decodeAllFloors(assetsDir); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(cache.floors)); + }); + server.middlewares.use(`${base}/assets/decoded/walls.json`, (_req, res) => { + cache.walls ??= decodeAllWalls(assetsDir); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(cache.walls)); + }); + server.middlewares.use(`${base}/assets/decoded/furniture.json`, (_req, res) => { + cache.furniture ??= decodeAllFurniture(assetsDir, buildFurnitureCatalog(assetsDir)); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(cache.furniture)); + }); + + // Hot-reload on asset file changes (PNGs, manifests, layouts) + server.watcher.add(assetsDir); + server.watcher.on('change', (file) => { + if (file.startsWith(assetsDir)) { + console.log(`[browser-mock-assets] Asset changed: ${path.relative(assetsDir, file)}`); + clearCache(); + server.ws.send({ type: 'full-reload' }); + } + }); + }, + // Build output includes lightweight metadata consumed by browser runtime. + closeBundle() { + fs.mkdirSync(distAssetsDir, { recursive: true }); + + const catalog = buildFurnitureCatalog(assetsDir); + fs.writeFileSync(path.join(distAssetsDir, 'furniture-catalog.json'), JSON.stringify(catalog)); + fs.writeFileSync( + path.join(distAssetsDir, 'asset-index.json'), + JSON.stringify(buildAssetIndex(assetsDir)), + ); + }, + }; +} + export default defineConfig({ - plugins: [react()], + plugins: [react(), browserMockAssetsPlugin()], build: { outDir: '../dist/webview', emptyOutDir: true,