diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5643671 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +dist2/ +*.local +.DS_Store +*.tsbuildinfo diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..42a4739 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "HMDesign"] + path = HMDesign + url = https://gitea.hmac.kr/b25027/HMDesign.git diff --git a/HMDesign b/HMDesign new file mode 160000 index 0000000..d18e3c3 --- /dev/null +++ b/HMDesign @@ -0,0 +1 @@ +Subproject commit d18e3c33801e165e17b6d971e2f6198eed483c3c diff --git a/index.html b/index.html new file mode 100644 index 0000000..109d5e9 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + DWG/DXF Viewer + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7e2b266 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7481 @@ +{ + "name": "dwg-dxf-viewer-hm", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dwg-dxf-viewer-hm", + "version": "0.1.0", + "workspaces": [ + "HMDesign/packages/ui" + ], + "dependencies": { + "@hm-design/ui": "*", + "dxf-parser": "^1.1.2", + "lucide-react": "^1.49.0", + "opentype.js": "^2.0.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "three": "^0.185.1" + }, + "devDependencies": { + "@tailwindcss/vite": "4.3.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@types/three": "^0.185.1", + "@vitejs/plugin-react": "^4.3.0", + "tailwindcss": "4.3.3", + "typescript": "^5.6.0", + "vite": "^5.4.0" + } + }, + "HMDesign": {}, + "HMDesign/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "HMDesign/node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "HMDesign/node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "HMDesign/node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "HMDesign/node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "HMDesign/node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "HMDesign/node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "HMDesign/node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "HMDesign/node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "HMDesign/node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "HMDesign/node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "HMDesign/node_modules/@date-fns/tz": { + "version": "1.5.0", + "license": "MIT" + }, + "HMDesign/node_modules/@dotenvx/dotenvx": { + "version": "1.75.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@dotenvx/primitives": "^0.8.0", + "commander": "^11.1.0", + "conf": "^10.2.0", + "dotenv": "^17.2.1", + "enquirer": "^2.4.1", + "env-paths": "^2.2.1", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "open": "^8.4.2", + "picomatch": "^4.0.4", + "systeminformation": "^5.22.11", + "undici": "^7.11.0", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/open": { + "version": "8.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "HMDesign/node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/@dotenvx/primitives": { + "version": "0.8.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "HMDesign/node_modules/@floating-ui/core": { + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "HMDesign/node_modules/@floating-ui/dom": { + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "HMDesign/node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "HMDesign/node_modules/@floating-ui/utils": { + "version": "0.2.12", + "license": "MIT" + }, + "HMDesign/node_modules/@hono/node-server": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "HMDesign/node_modules/@modelcontextprotocol/sdk": { + "version": "1.31.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "HMDesign/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "HMDesign/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "HMDesign/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "HMDesign/node_modules/@radix-ui/number": { + "version": "1.1.3", + "license": "MIT" + }, + "HMDesign/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "license": "MIT" + }, + "HMDesign/node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-accordion": { + "version": "1.2.20", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.23", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-checkbox": { + "version": "1.3.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-collapsible": { + "version": "1.1.20", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-context-menu": { + "version": "2.3.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-form": { + "version": "0.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-hover-card": { + "version": "1.1.23", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-label": { + "version": "2.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-menubar": { + "version": "1.1.24", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.22", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-popover": { + "version": "1.1.23", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-progress": { + "version": "1.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-radio-group": { + "version": "1.4.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-slider": { + "version": "1.4.7", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-switch": { + "version": "1.3.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-toast": { + "version": "1.2.23", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-toggle": { + "version": "1.1.18", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.19", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-toolbar": { + "version": "1.1.19", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.19" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.5", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/@radix-ui/rect": { + "version": "1.1.3", + "license": "MIT" + }, + "HMDesign/node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/@shadcn/registry": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "cn": "^0.2.4", + "cosmiconfig": "^9.0.0", + "deepmerge": "^4.3.1", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "kleur": "^4.1.5", + "ora": "^8.2.0", + "postcss": "^8.5.23", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "socks": "^2.8.8", + "stringify-object": "^5.0.0", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "type-fest": "^4.41.0", + "undici": "^7.29.0", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.18.1" + } + }, + "HMDesign/node_modules/@shadcn/registry/node_modules/cn": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "HMDesign/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/@tanstack/react-store": { + "version": "0.11.2", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.11.2", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "HMDesign/node_modules/@tanstack/react-table": { + "version": "9.2.4", + "license": "MIT", + "dependencies": { + "@tanstack/react-store": "^0.11.1", + "@tanstack/table-core": "9.2.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "HMDesign/node_modules/@tanstack/store": { + "version": "0.11.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "HMDesign/node_modules/@tanstack/table-core": { + "version": "9.2.4", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.11.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "HMDesign/node_modules/@ts-morph/common": { + "version": "0.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, + "HMDesign/node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/accepts": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "HMDesign/node_modules/ajv": { + "version": "8.20.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "HMDesign/node_modules/ajv-formats": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "HMDesign/node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "HMDesign/node_modules/aria-hidden": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "HMDesign/node_modules/ast-types": { + "version": "0.16.3", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/atomically": { + "version": "1.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.12.0" + } + }, + "HMDesign/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "HMDesign/node_modules/body-parser": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/brace-expansion": { + "version": "5.0.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "HMDesign/node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/bundle-name": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/chalk": { + "version": "5.6.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "HMDesign/node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "HMDesign/node_modules/cli-cursor": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/cli-spinners": { + "version": "2.9.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/cmdk": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "HMDesign/node_modules/cn": { + "version": "0.4.0", + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "HMDesign/node_modules/code-block-writer": { + "version": "13.0.3", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/commander": { + "version": "14.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "HMDesign/node_modules/conf": { + "version": "10.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/conf/node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "HMDesign/node_modules/conf/node_modules/json-schema-typed": { + "version": "7.0.3", + "dev": true, + "license": "BSD-2-Clause" + }, + "HMDesign/node_modules/conf/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "HMDesign/node_modules/content-disposition": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "HMDesign/node_modules/cookie": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "HMDesign/node_modules/cookie-signature": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "HMDesign/node_modules/cors": { + "version": "2.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/cosmiconfig": { + "version": "9.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "HMDesign/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "HMDesign/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "HMDesign/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "HMDesign/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/date-fns": { + "version": "4.4.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "HMDesign/node_modules/debounce-fn": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/dedent": { + "version": "1.7.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "HMDesign/node_modules/deepmerge": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/default-browser": { + "version": "5.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/default-browser-id": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/define-lazy-prop": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/detect-node-es": { + "version": "1.1.0", + "license": "MIT" + }, + "HMDesign/node_modules/diff": { + "version": "8.0.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "HMDesign/node_modules/dot-prop": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/dotenv": { + "version": "17.4.2", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "HMDesign/node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/embla-carousel": { + "version": "8.6.0", + "license": "MIT" + }, + "HMDesign/node_modules/embla-carousel-react": { + "version": "8.6.0", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "HMDesign/node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "HMDesign/node_modules/emoji-regex": { + "version": "10.6.0", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/encodeurl": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/enquirer": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "HMDesign/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/error-ex": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "HMDesign/node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/es-object-atoms": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "HMDesign/node_modules/eventsource": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "HMDesign/node_modules/eventsource-parser": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "HMDesign/node_modules/execa": { + "version": "9.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "HMDesign/node_modules/express": { + "version": "5.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/express-rate-limit": { + "version": "8.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "HMDesign/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "HMDesign/node_modules/fast-uri": { + "version": "3.1.8", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "HMDesign/node_modules/fastq": { + "version": "1.20.3", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "HMDesign/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "HMDesign/node_modules/figures": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/finalhandler": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/find-up": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "HMDesign/node_modules/fresh": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/fs-extra": { + "version": "11.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "HMDesign/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/fuzzysort": { + "version": "3.1.0", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/get-east-asian-width": { + "version": "1.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/get-nonce": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/get-stream": { + "version": "9.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "HMDesign/node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/hasown": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/hono": { + "version": "4.13.12", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "HMDesign/node_modules/http-errors": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/human-signals": { + "version": "8.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "HMDesign/node_modules/iconv-lite": { + "version": "0.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "HMDesign/node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "HMDesign/node_modules/ip-address": { + "version": "10.7.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "HMDesign/node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "HMDesign/node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/is-docker": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/is-in-ssh": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-inside-container": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-interactive": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "HMDesign/node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/is-plain-obj": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-promise": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/is-regexp": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-stream": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-unicode-supported": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/is-wsl": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/isexe": { + "version": "3.1.5", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "HMDesign/node_modules/jose": { + "version": "6.2.12", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "HMDesign/node_modules/js-yaml": { + "version": "4.3.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "HMDesign/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/json-schema-typed": { + "version": "8.0.2", + "dev": true, + "license": "BSD-2-Clause" + }, + "HMDesign/node_modules/jsonfile": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "HMDesign/node_modules/kleur": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/locate-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/log-symbols": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "HMDesign/node_modules/media-typer": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/merge-descriptors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "HMDesign/node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "HMDesign/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "HMDesign/node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "HMDesign/node_modules/mime-types": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/mimic-fn": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/mimic-function": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/minimatch": { + "version": "10.2.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "HMDesign/node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/negotiator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/npm-run-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/object-treeify": { + "version": "1.1.33", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "HMDesign/node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "HMDesign/node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/ora": { + "version": "8.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/ora/node_modules/ansi-regex": { + "version": "6.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "HMDesign/node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "HMDesign/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/p-locate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/parse-ms": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/path-browserify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/path-to-regexp": { + "version": "8.4.2", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/picomatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "HMDesign/node_modules/pkce-challenge": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "HMDesign/node_modules/pkg-up": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/postcss-selector-parser": { + "version": "7.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/powershell-utils": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/pretty-ms": { + "version": "9.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/prompts": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "HMDesign/node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/proxy-addr": { + "version": "2.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/qs": { + "version": "6.16.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "HMDesign/node_modules/radix-ui": { + "version": "1.6.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-accessible-icon": "1.1.15", + "@radix-ui/react-accordion": "1.2.20", + "@radix-ui/react-alert-dialog": "1.1.23", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-aspect-ratio": "1.1.15", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-checkbox": "1.3.11", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-context-menu": "2.3.7", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-form": "0.1.16", + "@radix-ui/react-hover-card": "1.1.23", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-menubar": "1.1.24", + "@radix-ui/react-navigation-menu": "1.2.22", + "@radix-ui/react-one-time-password-field": "0.1.16", + "@radix-ui/react-password-toggle-field": "0.1.11", + "@radix-ui/react-popover": "1.1.23", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-progress": "1.1.16", + "@radix-ui/react-radio-group": "1.4.7", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-scroll-area": "1.2.18", + "@radix-ui/react-select": "2.3.7", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slider": "1.4.7", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-switch": "1.3.7", + "@radix-ui/react-tabs": "1.1.21", + "@radix-ui/react-toast": "1.2.23", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-toggle-group": "1.1.19", + "@radix-ui/react-toolbar": "1.1.19", + "@radix-ui/react-tooltip": "1.2.16", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-escape-keydown": "1.1.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "HMDesign/node_modules/range-parser": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/raw-body": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "HMDesign/node_modules/react-day-picker": { + "version": "10.0.2", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/react-remove-scroll": { + "version": "2.7.2", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/react-resizable-panels": { + "version": "4.14.2", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "HMDesign/node_modules/react-style-singleton": { + "version": "2.2.3", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/recast": { + "version": "0.23.21", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "HMDesign/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/restore-cursor": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/router": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "HMDesign/node_modules/run-applescript": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "HMDesign/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/send": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/serve-static": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "HMDesign/node_modules/shadcn": { + "version": "4.21.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "@modelcontextprotocol/sdk": "^1.26.0", + "@shadcn/registry": "0.1.0", + "@types/validate-npm-package-name": "^4.0.2", + "browserslist": "^4.26.2", + "cn": "^0.2.4", + "commander": "^14.0.0", + "cosmiconfig": "^9.0.0", + "dedent": "^1.6.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "kleur": "^4.1.5", + "open": "^11.0.0", + "ora": "^8.2.0", + "postcss": "^8.5.23", + "postcss-selector-parser": "^7.1.3", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "socks": "^2.8.8", + "stringify-object": "^5.0.0", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "undici": "^7.29.0", + "validate-npm-package-name": "^7.0.1", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.6" + }, + "bin": { + "shadcn": "dist/index.js" + }, + "engines": { + "node": ">=20.18.1" + } + }, + "HMDesign/node_modules/shadcn/node_modules/cn": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "HMDesign/node_modules/shadcn/node_modules/open": { + "version": "11.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/shadcn/node_modules/wsl-utils": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/side-channel": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/side-channel-list": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "HMDesign/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "HMDesign/node_modules/sisteransi": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "HMDesign/node_modules/socks": { + "version": "2.8.10", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "HMDesign/node_modules/sonner": { + "version": "2.0.8", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "HMDesign/node_modules/statuses": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/stdin-discarder": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/string-width/node_modules/ansi-regex": { + "version": "6.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "HMDesign/node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "HMDesign/node_modules/stringify-object": { + "version": "5.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" + } + }, + "HMDesign/node_modules/stringify-object/node_modules/is-obj": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "HMDesign/node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "HMDesign/node_modules/strip-final-newline": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/systeminformation": { + "version": "5.33.15", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "HMDesign/node_modules/tiny-invariant": { + "version": "1.3.3", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "HMDesign/node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "HMDesign/node_modules/ts-morph": { + "version": "26.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, + "HMDesign/node_modules/tsconfig-paths": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "HMDesign/node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "HMDesign/node_modules/tw-animate-css": { + "version": "1.4.0", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "HMDesign/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/type-is": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "HMDesign/node_modules/undici": { + "version": "7.30.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "HMDesign/node_modules/unicorn-magic": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "HMDesign/node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/use-callback-ref": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/use-sidecar": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "HMDesign/node_modules/use-sync-external-store": { + "version": "1.7.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "HMDesign/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "HMDesign/node_modules/validate-npm-package-name": { + "version": "7.0.2", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "HMDesign/node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "HMDesign/node_modules/vaul": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "HMDesign/node_modules/which": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "HMDesign/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "HMDesign/node_modules/yocto-spinner": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/yoctocolors": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "HMDesign/node_modules/zod": { + "version": "3.25.76", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "HMDesign/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "HMDesign/packages/ui": { + "name": "@hm-design/ui", + "version": "0.1.0", + "dependencies": { + "@tanstack/react-table": "9.2.4", + "@tanstack/table-core": "9.2.4", + "class-variance-authority": "^0.7.1", + "cmdk": "^1.1.1", + "cn": "^0.4.0", + "embla-carousel-react": "^8.6.0", + "lucide-react": "^1.49.0", + "radix-ui": "^1.6.7", + "react-day-picker": "^10.0.2", + "react-resizable-panels": "^4.14.2", + "sonner": "^2.0.8", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" + }, + "devDependencies": { + "shadcn": "^4.21.1" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hm-design/ui": { + "resolved": "HMDesign/packages/ui", + "link": true + }, + "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": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.5.tgz", + "integrity": "sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.5.tgz", + "integrity": "sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.5.tgz", + "integrity": "sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.5.tgz", + "integrity": "sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.5.tgz", + "integrity": "sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.5.tgz", + "integrity": "sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.5.tgz", + "integrity": "sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.5.tgz", + "integrity": "sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.5.tgz", + "integrity": "sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.5.tgz", + "integrity": "sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.5.tgz", + "integrity": "sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.5.tgz", + "integrity": "sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.5.tgz", + "integrity": "sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.5.tgz", + "integrity": "sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.5.tgz", + "integrity": "sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.5.tgz", + "integrity": "sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.5.tgz", + "integrity": "sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.5.tgz", + "integrity": "sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.5.tgz", + "integrity": "sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.5.tgz", + "integrity": "sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.5.tgz", + "integrity": "sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.5.tgz", + "integrity": "sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.5.tgz", + "integrity": "sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.5.tgz", + "integrity": "sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.5.tgz", + "integrity": "sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.185.4", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.4.tgz", + "integrity": "sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": 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": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dxf-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/dxf-parser/-/dxf-parser-1.1.2.tgz", + "integrity": "sha512-GPTumUvRkounlIazLIyJMmTWt+nlg+ksS0Hdm8jWvejmZKBTz6gvHTam76wRm4PQMma5sgKLThblQyeIJcH79Q==", + "license": "MIT", + "dependencies": { + "loglevel": "^1.7.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.452", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz", + "integrity": "sha512-cVvk1YBSj6571VmV6hdJTqj94Y79xSu8cj6Y9t6ae4Y4431z8/7EJFvu3rVp90xhfuv9BpPKUmaLfTo4+FOGBg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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": { + "node": ">=6" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/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": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "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", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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-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/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "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": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.55.0.tgz", + "integrity": "sha512-bSozQHiI3/8q7JvgoqVRGOYUi7fYT3siAZwmZsljjsOH2Husp/2ODdFf/leD+ZC/2yKfn7r8oh6yeoR3ITR5vA==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/opentype.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-2.0.0.tgz", + "integrity": "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==", + "license": "MIT", + "bin": { + "ot": "bin/ot" + } + }, + "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/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.5.tgz", + "integrity": "sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.5", + "@rollup/rollup-android-arm64": "4.64.5", + "@rollup/rollup-darwin-arm64": "4.64.5", + "@rollup/rollup-darwin-x64": "4.64.5", + "@rollup/rollup-freebsd-arm64": "4.64.5", + "@rollup/rollup-freebsd-x64": "4.64.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.5", + "@rollup/rollup-linux-arm-musleabihf": "4.64.5", + "@rollup/rollup-linux-arm64-gnu": "4.64.5", + "@rollup/rollup-linux-arm64-musl": "4.64.5", + "@rollup/rollup-linux-loong64-gnu": "4.64.5", + "@rollup/rollup-linux-loong64-musl": "4.64.5", + "@rollup/rollup-linux-ppc64-gnu": "4.64.5", + "@rollup/rollup-linux-ppc64-musl": "4.64.5", + "@rollup/rollup-linux-riscv64-gnu": "4.64.5", + "@rollup/rollup-linux-riscv64-musl": "4.64.5", + "@rollup/rollup-linux-s390x-gnu": "4.64.5", + "@rollup/rollup-linux-x64-gnu": "4.64.5", + "@rollup/rollup-linux-x64-musl": "4.64.5", + "@rollup/rollup-openbsd-x64": "4.64.5", + "@rollup/rollup-openharmony-arm64": "4.64.5", + "@rollup/rollup-win32-arm64-msvc": "4.64.5", + "@rollup/rollup-win32-ia32-msvc": "4.64.5", + "@rollup/rollup-win32-x64-gnu": "4.64.5", + "@rollup/rollup-win32-x64-msvc": "4.64.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "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": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT" + }, + "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": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f363608 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "dwg-dxf-viewer-hm", + "private": true, + "version": "0.1.0", + "type": "module", + "workspaces": [ + "HMDesign/packages/ui" + ], + "scripts": { + "dev": "vite --port 5173 --strictPort", + "build": "tsc -b --noEmit && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --noEmit" + }, + "dependencies": { + "@hm-design/ui": "*", + "dxf-parser": "^1.1.2", + "lucide-react": "^1.49.0", + "opentype.js": "^2.0.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "three": "^0.185.1" + }, + "devDependencies": { + "@tailwindcss/vite": "4.3.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@types/three": "^0.185.1", + "@vitejs/plugin-react": "^4.3.0", + "tailwindcss": "4.3.3", + "typescript": "^5.6.0", + "vite": "^5.4.0" + } +} diff --git a/public/fonts/NanumGothic-Regular.ttf b/public/fonts/NanumGothic-Regular.ttf new file mode 100644 index 0000000..2aa2b20 Binary files /dev/null and b/public/fonts/NanumGothic-Regular.ttf differ diff --git a/public/licenses/THIRD-PARTY-NOTICES.md b/public/licenses/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..a3ecaef --- /dev/null +++ b/public/licenses/THIRD-PARTY-NOTICES.md @@ -0,0 +1,70 @@ +# Third-party notices + +This viewer bundles third-party code. The notices below apply to the source +tree **and** to the built site (`dist2/`, deployed at +`https://dwg-dxf-viewer-sample.pages.dev/`), because the build embeds the +components listed here. + +Served copies of the license texts live under +[`public/licenses/`](./public/licenses/) → `/licenses/…` on the deployed site. + +--- + +## acadrust 0.4.1 — Mozilla Public License 2.0 — **MODIFIED** + +| | | +|---|---| +| Component | `acadrust` (DWG/DXF parser, Rust) | +| Version | 0.4.1 | +| License | MPL-2.0 — full text: [`public/licenses/acadrust-MPL-2.0.txt`](./public/licenses/acadrust-MPL-2.0.txt) (`/licenses/acadrust-MPL-2.0.txt`) | +| Upstream | https://github.com/hakanaktt/acadrust · https://crates.io/crates/acadrust/0.4.1 | +| Upstream commit | `f249c2f816acf36ee51cd5533716bdd443c2517e` (from the crate's `.cargo_vcs_info.json`) | +| Author | Hakan AK | +| Shipped as | compiled into `assets/acadrust_dwg_bg-*.wasm` (Executable Form) | + +**This copy is modified.** One file differs from the published crate: + +``` +src/io/dwg/dwg_stream_readers/object_reader/entities.rs (read_mesh array-count bounds) +``` + +The modification bounds `read_mesh`'s array counts by the bits remaining in the +object stream instead of the blanket 100 000-item guard, which truncated and +thereby corrupted large SubD meshes. + +### How to obtain the Source Code Form (MPL-2.0 §3.2) + +1. Get the unmodified crate source — `cargo vendor`, or + `https://crates.io/api/v1/crates/acadrust/0.4.1/download`, or the upstream + commit above. +2. Apply [`public/licenses/acadrust-0.4.1-read_mesh.patch`](./public/licenses/acadrust-0.4.1-read_mesh.patch) + (`/licenses/acadrust-0.4.1-read_mesh.patch` on the deployed site): + + ```bash + tar xf acadrust-0.4.1.crate && cd acadrust-0.4.1 + patch -p1 < acadrust-0.4.1-read_mesh.patch + ``` + +The result is the exact Source Code Form of the acadrust code compiled into the +shipped `.wasm`. Both the original and the modified file remain under MPL-2.0. + +--- + +## dwg-wasm — MIT + +The wasm-bindgen wrapper that turns acadrust output into this viewer's +`parseResult` (`hmwebviewer/rust/dwg-wasm/`). Our own code, MIT. MPL-2.0 §3.3 +permits distributing this Larger Work under different terms as long as the +MPL-covered files above keep their license — which they do. + +--- + +## Other components + +| Component | License | +|---|---| +| three.js | MIT | +| dxf-parser | MIT | +| opentype.js | MIT | +| NanumGothic (`public/fonts/`) | SIL Open Font License 1.1 | +| Viewer2D / sample glue | MIT | diff --git a/public/licenses/acadrust-0.4.1-read_mesh.patch b/public/licenses/acadrust-0.4.1-read_mesh.patch new file mode 100644 index 0000000..59193bd --- /dev/null +++ b/public/licenses/acadrust-0.4.1-read_mesh.patch @@ -0,0 +1,111 @@ +--- a/src/io/dwg/dwg_stream_readers/object_reader/entities.rs ++++ b/src/io/dwg/dwg_stream_readers/object_reader/entities.rs +@@ -1682,20 +1682,49 @@ + MLineData { scale_factor, justification, start_point, normal, openclosed, lines_in_style, vertex_count, vertices, style_handle } + } + ++/// Cap an array count against the bits actually left in the object's main ++/// stream instead of the blanket [`MAX_ARRAY_COUNT`]. ++/// ++/// MESH is the one entity whose arrays legitimately run past 100 000 items: a ++/// road-surface mesh in a civil drawing carries hundreds of thousands of ++/// vertices. Clamping those to the blanket cap does not merely truncate the ++/// mesh — the reader then continues from the wrong bit offset, so the face, ++/// edge and crease lists that follow decode as garbage (observed: a 100 000 ++/// vertex clamp left a 14-face mesh with face sizes `[64, 0, 64, 23, 0, …]`). ++/// Bounding by the remaining bits keeps the corrupt-data protection — no ++/// unbounded allocation is possible — without mis-parsing a valid drawing. ++/// ++/// `min_bits_per_item` is the shortest legal encoding of one element: a ++/// BitDouble is 2 bits at minimum (the 0.0 / 1.0 forms), so a 3BD vertex is 6 ++/// and a BitLong is 2. ++fn stream_bounded_count(reader: &DwgMergedReader, raw: i32, min_bits_per_item: i64) -> i32 { ++ let raw = raw.max(0) as i64; ++ let total_bits = (reader.main().data_len() as i64).saturating_mul(8); ++ // The handle stream follows the main stream; anything past its start is not ++ // array data. handle_start() is 0 when the framing did not supply it. ++ let handle_start = reader.handle_start(); ++ let end_bits = if handle_start > 0 { handle_start.min(total_bits) } else { total_bits }; ++ let remaining = (end_bits - reader.position_in_bits()).max(0); ++ raw.min(remaining / min_bits_per_item.max(1)).min(i32::MAX as i64) as i32 ++} ++ + pub fn read_mesh(reader: &mut DwgMergedReader) -> MeshData { + let version = reader.read_bit_short(); + let blend_crease = reader.read_bit(); + let subdivision_level = reader.read_bit_long(); + +- let num_verts = safe_count(reader.read_bit_long()); ++ let raw_verts = reader.read_bit_long(); ++ let num_verts = stream_bounded_count(reader, raw_verts, 6); + let mut vertices = Vec::with_capacity(num_verts as usize); + for _ in 0..num_verts { vertices.push(reader.read_3bit_double()); } + +- let total_face_data = safe_count(reader.read_bit_long()); ++ let raw_face_data = reader.read_bit_long(); ++ let total_face_data = stream_bounded_count(reader, raw_face_data, 2); + let mut faces = Vec::new(); + let mut i = 0; + while i < total_face_data { +- let n = safe_count(reader.read_bit_long()); ++ let raw_n = reader.read_bit_long(); ++ let n = stream_bounded_count(reader, raw_n, 2); + i += 1; + let mut face = Vec::new(); + for _ in 0..n { +@@ -1705,7 +1734,8 @@ + faces.push(face); + } + +- let num_edges = safe_count(reader.read_bit_long()); ++ let raw_edges = reader.read_bit_long(); ++ let num_edges = stream_bounded_count(reader, raw_edges, 4); + let mut edges = Vec::with_capacity(num_edges as usize); + for _ in 0..num_edges { + let s = reader.read_bit_long(); +@@ -1713,7 +1743,8 @@ + edges.push((s, e)); + } + +- let num_creases = safe_count(reader.read_bit_long()); ++ let raw_creases = reader.read_bit_long(); ++ let num_creases = stream_bounded_count(reader, raw_creases, 2); + let mut crease_values = Vec::with_capacity(num_creases as usize); + for _ in 0..num_creases { crease_values.push(reader.read_bit_double()); } + +@@ -2350,14 +2381,31 @@ + pub fn read_ole2frame(reader: &mut DwgMergedReader, version: DwgVersion) -> Ole2FrameData { + let ver = reader.read_bit_short(); + let mode = if version.r2000_plus() { reader.read_bit_short() } else { 0 }; +- // OLE binary data can be very large (embedded images/documents), +- // so don't use safe_count (100 KB cap). Use a generous 10 MB cap instead. + let data_len = (reader.read_bit_long().max(0) as usize).min(10_000_000); + let data = reader.read_bytes(data_len); + let trailing_byte = if version.r2000_plus() { reader.read_byte() } else { 3 }; +- let upper_left = reader.read_3bit_double(); +- let lower_right = reader.read_3bit_double(); ++ ++ let mut upper_left = Vector3::new(0.0, 0.0, 0.0); ++ let mut lower_right = Vector3::new(0.0, 0.0, 0.0); ++ ++ // Extract 4-corner coordinates stored in the OLE binary data header starting at offset 2 ++ if data.len() >= 66 { ++ let ul_x = f64::from_le_bytes(data[2..10].try_into().unwrap()); ++ let ul_y = f64::from_le_bytes(data[10..18].try_into().unwrap()); ++ let ul_z = f64::from_le_bytes(data[18..26].try_into().unwrap()); ++ let lr_x = f64::from_le_bytes(data[50..58].try_into().unwrap()); ++ let lr_y = f64::from_le_bytes(data[58..66].try_into().unwrap()); ++ let lr_z = if data.len() >= 74 { f64::from_le_bytes(data[66..74].try_into().unwrap()) } else { 0.0 }; ++ ++ if ul_x.is_finite() && ul_y.is_finite() && lr_x.is_finite() && lr_y.is_finite() ++ && (ul_x != 0.0 || ul_y != 0.0 || lr_x != 0.0 || lr_y != 0.0) { ++ upper_left = Vector3::new(ul_x, ul_y, if ul_z.is_finite() { ul_z } else { 0.0 }); ++ lower_right = Vector3::new(lr_x, lr_y, if lr_z.is_finite() { lr_z } else { 0.0 }); ++ } ++ } ++ + let ole_type = if version.r2000_plus() { reader.read_byte() } else { 2 }; ++ + Ole2FrameData { version: ver, mode, data, trailing_byte, upper_left, lower_right, ole_type } + } + diff --git a/public/licenses/acadrust-MPL-2.0.txt b/public/licenses/acadrust-MPL-2.0.txt new file mode 100644 index 0000000..7321068 --- /dev/null +++ b/public/licenses/acadrust-MPL-2.0.txt @@ -0,0 +1,375 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under the name "LEGAL", and included in all Covered Software +distributed in Source Code Form. Except to the extent prohibited by +statute or regulation, such description must be sufficiently detailed +for a recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + diff --git a/public/samples/BasicSample.dwg b/public/samples/BasicSample.dwg new file mode 100644 index 0000000..cd34d2c Binary files /dev/null and b/public/samples/BasicSample.dwg differ diff --git a/public/samples/civil.dwg b/public/samples/civil.dwg new file mode 100644 index 0000000..3ee01bb Binary files /dev/null and b/public/samples/civil.dwg differ diff --git a/public/samples/simple.dxf b/public/samples/simple.dxf new file mode 100644 index 0000000..10fa791 --- /dev/null +++ b/public/samples/simple.dxf @@ -0,0 +1,146 @@ +0 +SECTION +2 +HEADER +9 +$ACADVER +1 +AC1015 +9 +$INSUNITS +70 +4 +0 +ENDSEC +0 +SECTION +2 +TABLES +0 +TABLE +2 +LAYER +0 +LAYER +2 +0 +70 +0 +62 +7 +0 +LAYER +2 +OUTLINE +70 +0 +62 +1 +0 +ENDTAB +0 +ENDSEC +0 +SECTION +2 +BLOCKS +0 +ENDSEC +0 +SECTION +2 +ENTITIES +0 +LINE +8 +OUTLINE +10 +0.0 +20 +0.0 +30 +0.0 +11 +100.0 +21 +0.0 +31 +0.0 +0 +LINE +8 +OUTLINE +10 +100.0 +20 +0.0 +30 +0.0 +11 +100.0 +21 +60.0 +31 +0.0 +0 +LINE +8 +OUTLINE +10 +100.0 +20 +60.0 +30 +0.0 +11 +0.0 +21 +60.0 +31 +0.0 +0 +LINE +8 +OUTLINE +10 +0.0 +20 +60.0 +30 +0.0 +11 +0.0 +21 +0.0 +31 +0.0 +0 +CIRCLE +8 +0 +10 +50.0 +20 +30.0 +30 +0.0 +40 +15.0 +0 +TEXT +8 +0 +10 +10.0 +20 +70.0 +30 +0.0 +40 +5.0 +1 +Hello DWG/DXF package +0 +ENDSEC +0 +EOF diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..e9cbecf --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from 'react'; +import { useDwgViewer } from './hooks/useDwgViewer'; +import { Toolbar } from './components/Toolbar'; +import { LayerPanel } from './components/LayerPanel'; +import { PropertiesPanel } from './components/PropertiesPanel'; +import { OptionsPanel } from './components/OptionsPanel'; +import { CoordReadout } from './components/CoordReadout'; +import { LoadProgress } from './components/LoadProgress'; + +export default function App() { + const v = useDwgViewer(); + const [layersOpen, setLayersOpen] = useState(false); + const [optionsOpen, setOptionsOpen] = useState(false); + const [propsOpen, setPropsOpen] = useState(false); + + // selecting an entity opens the Properties panel and updates the status line, + // mirroring the original app's `viewer.onSelect` wiring in main.ts + useEffect(() => { + const entity = v.selectedEntity as + | { type?: string; typeName?: string; layer?: string; layerName?: string; handle?: { value?: unknown } | unknown } + | null; + if (!entity) { + v.setStatusMessage('deselected'); + return; + } + setPropsOpen(true); + const type = (entity.type || entity.typeName || 'ENTITY').toUpperCase(); + const layer = entity.layer ?? entity.layerName ?? '0'; + const handle = (entity.handle as { value?: unknown })?.value ?? entity.handle ?? '-'; + v.setStatusMessage(`select ${type} · layer=${layer} · h=${handle}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [v.selectedEntity]); + + // F key → fit (ignored while typing in an input) + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA') return; + if (e.key === 'f' || e.key === 'F') v.fit(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [v]); + + // drag & drop + useEffect(() => { + const onDragOver = (e: DragEvent) => e.preventDefault(); + const onDrop = (e: DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer?.files?.[0]; + if (file) void v.loadFile(file); + }; + window.addEventListener('dragover', onDragOver); + window.addEventListener('drop', onDrop); + return () => { + window.removeEventListener('dragover', onDragOver); + window.removeEventListener('drop', onDrop); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ?model= query param auto-load (once) + useEffect(() => { + const model = new URLSearchParams(window.location.search).get('model'); + if (model) void v.loadUrl(model); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const activeKind = v.spaces.find((s) => s.handleHex === v.activeSpace)?.kind ?? null; + + return ( +
+
+ + + + void v.loadFile(file)} + onLoadSample={(url, label) => void v.loadUrl(url, label)} + onFit={v.fit} + is25D={v.is25D} + onToggle25D={v.set25D} + canvasDark={v.theme === 'dark'} + onToggleTheme={v.toggleTheme} + layersOpen={layersOpen} + onToggleLayers={() => setLayersOpen((o) => !o)} + optionsOpen={optionsOpen} + onToggleOptions={() => setOptionsOpen((o) => !o)} + propsOpen={propsOpen} + onToggleProps={() => setPropsOpen((o) => !o)} + /> + + + + setLayersOpen(false)} + layers={v.layers} + hiddenLayers={v.hiddenLayers} + onToggleLayer={v.toggleLayer} + onShowAll={v.showAllLayers} + onHideAll={v.hideAllLayers} + /> + + setOptionsOpen(false)} + options={v.options} + is25D={v.is25D} + lineweightClasses={v.getLineweightClasses()} + onSetGrid={v.setGrid} + onSetZoomSpeed={v.setZoomSpeed} + onSetZScale={v.setZScale} + onSetLineweightOn={v.setLineweightOn} + onSetLineweightScale={v.setLineweightScale} + onSet25D={v.set25D} + onReset={v.resetOptions} + /> + + setPropsOpen(false)} + entity={v.selectedEntity} + onZoomToSelection={v.zoomToSelection} + /> +
+ ); +} diff --git a/src/components/CoordReadout.tsx b/src/components/CoordReadout.tsx new file mode 100644 index 0000000..4607a29 --- /dev/null +++ b/src/components/CoordReadout.tsx @@ -0,0 +1,24 @@ +function formatCadCoord(v: number | null): string { + if (v === null || !Number.isFinite(v)) return '—'; + const abs = Math.abs(v); + const digits = abs >= 1e6 ? 1 : abs >= 1e3 ? 2 : abs >= 1 ? 3 : 4; + return v.toFixed(digits); +} + +export interface CoordReadoutProps { + point: { x: number; y: number; z: number } | null; + spaceKind: 'model' | 'paper' | 'other' | null; +} + +export function CoordReadout({ point, spaceKind }: CoordReadoutProps) { + const unitLabel = spaceKind === 'paper' ? 'Paper' : 'UCS'; + return ( +
+ X {formatCadCoord(point?.x ?? null)} · Y {formatCadCoord(point?.y ?? null)}{' '} + ({unitLabel}) +
+ ); +} diff --git a/src/components/DraggablePanel.tsx b/src/components/DraggablePanel.tsx new file mode 100644 index 0000000..c4733bd --- /dev/null +++ b/src/components/DraggablePanel.tsx @@ -0,0 +1,86 @@ +import type { ReactNode } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardAction } from '@hm-design/ui'; +import { Button } from '@hm-design/ui'; +import { ChevronDown, ChevronRight, X } from 'lucide-react'; +import { useDraggablePanel } from '../hooks/useDraggablePanel'; + +export interface DraggablePanelProps { + title: string; + icon?: ReactNode; + initial: { left: number; top: number }; + width?: number; + height?: number; + open: boolean; + onClose: () => void; + headerActions?: ReactNode; + children: ReactNode; +} + +export function DraggablePanel({ + title, + icon, + initial, + width = 320, + height = 420, + open, + onClose, + headerActions, + children, +}: DraggablePanelProps) { + const { panelRef, style, collapsed, toggleCollapse, headerProps } = useDraggablePanel(initial); + + if (!open) return null; + + return ( + + + + {icon} + {title} + + + {headerActions} + + + + + {!collapsed && ( + + {children} + + )} + + ); +} diff --git a/src/components/LayerPanel.tsx b/src/components/LayerPanel.tsx new file mode 100644 index 0000000..bdae04e --- /dev/null +++ b/src/components/LayerPanel.tsx @@ -0,0 +1,88 @@ +import { useMemo, useState } from 'react'; +import { Badge, Button, FilterBar, FilterBarActions, FilterBarSearch } from '@hm-design/ui'; +import { Eye, EyeOff, Layers } from 'lucide-react'; +import { DraggablePanel } from './DraggablePanel'; +import type { LayerRow } from '../hooks/useDwgViewer'; + +export interface LayerPanelProps { + open: boolean; + onClose: () => void; + layers: LayerRow[]; + hiddenLayers: Set; + onToggleLayer: (name: string) => void; + onShowAll: () => void; + onHideAll: () => void; +} + +export function LayerPanel({ open, onClose, layers, hiddenLayers, onToggleLayer, onShowAll, onHideAll }: LayerPanelProps) { + const [query, setQuery] = useState(''); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return layers; + return layers.filter((l) => l.name.toLowerCase().includes(q)); + }, [layers, query]); + + return ( + } + initial={{ left: window.innerWidth - 344, top: 56 }} + open={open} + onClose={onClose} + > +
+ + + + + + + + + {layers.length === 0 ? ( +

도면에 레이어 정보가 없습니다

+ ) : filtered.length === 0 ? ( +

검색 결과가 없습니다 ("{query}")

+ ) : ( +
    + {filtered.map((layer) => { + const visible = !hiddenLayers.has(layer.name); + return ( +
  • + + + {layer.name} + + {layer.count} + +
  • + ); + })} +
+ )} +
+
+ ); +} diff --git a/src/components/LicenseNotice.tsx b/src/components/LicenseNotice.tsx new file mode 100644 index 0000000..91a4d9b --- /dev/null +++ b/src/components/LicenseNotice.tsx @@ -0,0 +1,50 @@ +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@hm-design/ui'; +import { Info } from 'lucide-react'; + +const PIPELINE_STEPS = [ + '파일 로드 (.dwg → acadrust WASM, .dxf → dxf-parser)', + 'CAD 파싱 결과 정규화 (레이어 / 스페이스 / 엔티티)', + 'Three.js 씬 구성 및 렌더링', +]; + +export function LicenseNotice() { + return ( + + + + + + + 모듈 구성 · 라이선스 고지 + 이 뷰어가 사용하는 처리 파이프라인과 서드파티 라이선스 고지입니다. + +
    + {PIPELINE_STEPS.map((step) => ( +
  1. {step}
  2. + ))} +
+
+ + THIRD-PARTY-NOTICES.md + + + acadrust MPL-2.0 전문 + + + acadrust 0.4.1 read_mesh 패치 + +
+
+
+ ); +} diff --git a/src/components/LoadProgress.tsx b/src/components/LoadProgress.tsx new file mode 100644 index 0000000..d8aa7c8 --- /dev/null +++ b/src/components/LoadProgress.tsx @@ -0,0 +1,19 @@ +import { Progress } from '@hm-design/ui'; + +export interface LoadProgressProps { + active: boolean; + value: number | null; + error?: boolean; +} + +export function LoadProgress({ active, value, error }: LoadProgressProps) { + if (!active) return null; + return ( + *]:bg-destructive' : ''}`} + /> + ); +} diff --git a/src/components/OptionsPanel.tsx b/src/components/OptionsPanel.tsx new file mode 100644 index 0000000..7876545 --- /dev/null +++ b/src/components/OptionsPanel.tsx @@ -0,0 +1,168 @@ +import { Button, Input, Label, Slider, Switch } from '@hm-design/ui'; +import { RotateCcw } from 'lucide-react'; +import { DraggablePanel } from './DraggablePanel'; +import { SlidersHorizontal } from 'lucide-react'; +import type { PersistedOptions } from '../hooks/useDwgViewer'; + +function SliderNumberRow({ + label, + value, + min, + max, + step, + unit, + onChange, +}: { + label: string; + value: number; + min: number; + max: number; + step: number; + unit?: string; + onChange: (v: number) => void; +}) { + return ( +
+
+ +
+ { + const v = Number(e.target.value); + if (Number.isFinite(v)) onChange(v); + }} + /> + {unit && {unit}} +
+
+ onChange(v)} + /> +
+ ); +} + +export interface OptionsPanelProps { + open: boolean; + onClose: () => void; + options: PersistedOptions; + is25D: boolean; + lineweightClasses: number[]; + onSetGrid: (on: boolean) => void; + onSetZoomSpeed: (v: number) => void; + onSetZScale: (v: number) => void; + onSetLineweightOn: (on: boolean | null) => void; + onSetLineweightScale: (v: number | null) => void; + onSet25D: (on: boolean) => void; + onReset: () => void; +} + +export function OptionsPanel({ + open, + onClose, + options, + is25D, + lineweightClasses, + onSetGrid, + onSetZoomSpeed, + onSetZScale, + onSetLineweightOn, + onSetLineweightScale, + onSet25D, + onReset, +}: OptionsPanelProps) { + const lwScale = options.lineweightScale ?? 11; + const lwPreview = + lineweightClasses.length > 0 + ? lineweightClasses.map((mm) => `${mm.toFixed(2)}mm → ${(mm * lwScale).toFixed(1)}px`).join(' · ') + : `이 도면에는 굵은 선이 없습니다 (0.50mm 였다면 ${(0.5 * lwScale).toFixed(1)}px)`; + + return ( + } + initial={{ left: window.innerWidth - 676, top: 56 }} + width={340} + height={520} + open={open} + onClose={onClose} + headerActions={ + + } + > +
+
+

선가중치

+ + +

{lwPreview}

+

AutoCAD의 "선가중치 설정 → 표시 배율"과 동일합니다.

+
+ +
+

표시

+ + +
+ +
+

2.5D

+ +
+ +
+

+ 좌클릭 드래그로 회전, 중간/우클릭 드래그로 이동합니다. +

+
+
+
+ ); +} diff --git a/src/components/PropertiesPanel.tsx b/src/components/PropertiesPanel.tsx new file mode 100644 index 0000000..4ae5062 --- /dev/null +++ b/src/components/PropertiesPanel.tsx @@ -0,0 +1,76 @@ +import { Badge, Button } from '@hm-design/ui'; +import { Info, MousePointerClick, ZoomIn } from 'lucide-react'; +import { DraggablePanel } from './DraggablePanel'; +import { formatEntityProperties } from '../lib/propertyFormat'; + +function PropertyRows({ rows }: { rows: [string, string][] }) { + return ( +
+ {rows.map(([k, v]) => ( +
+ {k} + + {v} + +
+ ))} +
+ ); +} + +export interface PropertiesPanelProps { + open: boolean; + onClose: () => void; + entity: unknown; + onZoomToSelection: () => void; +} + +export function PropertiesPanel({ open, onClose, entity, onZoomToSelection }: PropertiesPanelProps) { + const prop = formatEntityProperties(entity); + + return ( + } + initial={{ left: window.innerWidth - 12 - 320, top: 56 }} + open={open} + onClose={onClose} + headerActions={ + entity ? ( + + ) : null + } + > + {!entity ? ( +
+ + 도면에서 객체(엔티티)를 클릭하면 +
+ 상세 속성이 표시됩니다 +
+ ) : ( +
+ + {prop.id} + +
+

+ 일반 정보 (General) +

+ +
+ {prop.geometry.length > 0 && ( +
+

+ 기하 속성 (Geometry) +

+ +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx new file mode 100644 index 0000000..f5adc38 --- /dev/null +++ b/src/components/Toolbar.tsx @@ -0,0 +1,122 @@ +import { useRef } from 'react'; +import { Button, Switch, Tabs, TabsList, TabsTrigger, Badge, Toggle } from '@hm-design/ui'; +import { FolderOpen, Scan, Layers, SlidersHorizontal, Info as InfoIcon } from 'lucide-react'; +import type { CadSpace } from '../viewer2d/cadSpaces'; +import { LicenseNotice } from './LicenseNotice'; + +export interface ToolbarProps { + status: string; + statusError: boolean; + spaces: CadSpace[]; + activeSpace: string | null; + onSelectSpace: (handleHex: string | null) => void; + onOpenFile: (file: File) => void; + onLoadSample: (url: string, label: string) => void; + onFit: () => void; + is25D: boolean; + onToggle25D: (on: boolean) => void; + canvasDark: boolean; + onToggleTheme: () => void; + layersOpen: boolean; + onToggleLayers: () => void; + optionsOpen: boolean; + onToggleOptions: () => void; + propsOpen: boolean; + onToggleProps: () => void; +} + +export function Toolbar({ + status, + statusError, + spaces, + activeSpace, + onSelectSpace, + onOpenFile, + onLoadSample, + onFit, + is25D, + onToggle25D, + canvasDark, + onToggleTheme, + layersOpen, + onToggleLayers, + optionsOpen, + onToggleOptions, + propsOpen, + onToggleProps, +}: ToolbarProps) { + const fileInputRef = useRef(null); + const showSpaceTabs = spaces.length > 1 || (spaces[0]?.entityCount ?? 0) > 0; + + return ( +
+ { + const file = e.target.files?.[0]; + if (file) onOpenFile(file); + e.currentTarget.value = ''; + }} + /> + + + + + + {showSpaceTabs && ( + onSelectSpace(v)}> + + {spaces.map((s) => ( + + {s.label} + {s.entityCount > 0 ? ` (${s.entityCount})` : ''} + + ))} + + + )} + + + + + + + +
+ + + + + + + + + + +
+ + + {status} + +
+ ); +} diff --git a/src/hooks/useDraggablePanel.ts b/src/hooks/useDraggablePanel.ts new file mode 100644 index 0000000..76f13ab --- /dev/null +++ b/src/hooks/useDraggablePanel.ts @@ -0,0 +1,101 @@ +import { useCallback, useRef, useState } from 'react'; + +/** + * React port of dwg-dxf-viewer-sample's panelUtils.ts drag+collapse mechanics. + * Resize is left to native CSS `resize: both` on the panel element, same as the original. + */ + +let globalMaxZIndex = 30; + +const INTERACTIVE_SELECTOR = 'button, input, select, a, label, [data-panel-no-drag]'; + +export interface UseDraggablePanelResult { + panelRef: React.RefObject; + style: React.CSSProperties; + zIndex: number; + collapsed: boolean; + toggleCollapse: () => void; + headerProps: { + onPointerDown: (e: React.PointerEvent) => void; + onDoubleClick: (e: React.MouseEvent) => void; + style: React.CSSProperties; + }; + bringToFront: () => void; +} + +export function useDraggablePanel(initial: { left: number; top: number }): UseDraggablePanelResult { + const panelRef = useRef(null); + const [pos, setPos] = useState(initial); + const [collapsed, setCollapsed] = useState(false); + const [zIndex, setZIndex] = useState(() => ++globalMaxZIndex); + const dragState = useRef<{ startX: number; startY: number; startLeft: number; startTop: number } | null>(null); + + const bringToFront = useCallback(() => { + setZIndex(++globalMaxZIndex); + }, []); + + const onPointerMove = useCallback((e: PointerEvent) => { + const st = dragState.current; + const panel = panelRef.current; + if (!st || !panel) return; + const dx = e.clientX - st.startX; + const dy = e.clientY - st.startY; + const maxLeft = window.innerWidth - panel.offsetWidth - 10; + const maxTop = window.innerHeight - panel.offsetHeight - 10; + const left = Math.min(Math.max(st.startLeft + dx, 10), Math.max(10, maxLeft)); + const top = Math.min(Math.max(st.startTop + dy, 10), Math.max(10, maxTop)); + setPos({ left, top }); + }, []); + + const onPointerUp = useCallback(() => { + dragState.current = null; + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); + document.body.style.cursor = ''; + }, [onPointerMove]); + + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + bringToFront(); + const target = e.target as HTMLElement; + if (target.closest(INTERACTIVE_SELECTOR)) return; + const panel = panelRef.current; + if (!panel) return; + dragState.current = { + startX: e.clientX, + startY: e.clientY, + startLeft: panel.offsetLeft, + startTop: panel.offsetTop, + }; + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); + document.body.style.cursor = 'grabbing'; + }, + [bringToFront, onPointerMove, onPointerUp], + ); + + const toggleCollapse = useCallback(() => setCollapsed((c) => !c), []); + + const onDoubleClick = useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + if (target.closest(INTERACTIVE_SELECTOR)) return; + toggleCollapse(); + }, + [toggleCollapse], + ); + + return { + panelRef, + style: { position: 'fixed', left: pos.left, top: pos.top, zIndex }, + zIndex, + collapsed, + toggleCollapse, + headerProps: { + onPointerDown, + onDoubleClick, + style: { cursor: 'grab', touchAction: 'none' }, + }, + bringToFront, + }; +} diff --git a/src/hooks/useDwgViewer.ts b/src/hooks/useDwgViewer.ts new file mode 100644 index 0000000..cad2d86 --- /dev/null +++ b/src/hooks/useDwgViewer.ts @@ -0,0 +1,336 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Viewer2D } from '../viewer2d/Viewer2D'; +import { parseDwgBuffer } from '../viewer2d/dwgParser'; +import { parseDxfBuffer } from '../viewer2d/parseDxf'; +import { isDwg, isDxf, is2D } from '../viewer2d/ext2d'; +import { listCadSpaces, pickDefaultSpace, type CadSpace } from '../viewer2d/cadSpaces'; +import { setDefaultFontUrl } from '../viewer2d/slugText'; +import { getAllOptions, setOption } from '../lib/optionsStore'; + +setDefaultFontUrl('/fonts/NanumGothic-Regular.ttf'); + +export interface LayerRow { + name: string; + colorHex: string; + count: number; + visible: boolean; +} + +export type LoadStatus = 'idle' | 'loading' | 'ready' | 'error'; + +export interface PersistedOptions { + grid: boolean; + zoomSpeed: number; + zScale: number; + lineweightOn: boolean | null; + lineweightScale: number | null; +} + +const DEFAULT_OPTIONS: PersistedOptions = { + grid: false, + zoomSpeed: 1, + zScale: 1, + lineweightOn: null, + lineweightScale: null, +}; + +async function parseByName(name: string, buf: ArrayBuffer) { + if (isDwg(name)) return parseDwgBuffer(new Uint8Array(buf)); + if (isDxf(name)) return parseDxfBuffer(buf); + if (!is2D(name)) throw new Error('지원하지 않는 파일 형식입니다 (.dwg/.dxf만 지원)'); + return parseDxfBuffer(buf); +} + +export function useDwgViewer() { + const containerRef = useRef(null); + const viewerRef = useRef(null); + + const [status, setStatus] = useState('idle'); + const [statusMessage, setStatusMessage] = useState('준비됨'); + const [loadingProgress, setLoadingProgress] = useState(null); + + const [selectedEntity, setSelectedEntity] = useState(null); + const [pointerWorld, setPointerWorld] = useState<{ x: number; y: number; z: number } | null>(null); + + const [spaces, setSpaces] = useState([]); + const [activeSpace, setActiveSpace] = useState(null); + + const [layers, setLayers] = useState([]); + const [hiddenLayers, setHiddenLayersState] = useState>(new Set()); + + const [theme, setThemeState] = useState<'dark' | 'light'>('dark'); + const [is25D, setIs25DState] = useState(false); + const [options, setOptions] = useState(DEFAULT_OPTIONS); + + const refreshLayers = useCallback(() => { + const viewer = viewerRef.current; + if (!viewer) return; + const info = viewer.getLayerInfo(); + setLayers(info.map((l) => ({ name: l.name, colorHex: l.colorHex, count: l.count, visible: l.visible }))); + }, []); + + const refreshSpaces = useCallback(() => { + const viewer = viewerRef.current; + if (!viewer) return; + setSpaces(viewer.getSpaces()); + setActiveSpace(viewer.getActiveSpace()); + }, []); + + // mount Viewer2D once + useEffect(() => { + if (!containerRef.current) return; + const viewer = new Viewer2D(containerRef.current); + viewerRef.current = viewer; + viewer.setTheme(true); + viewer.onSelect((entity) => setSelectedEntity(entity)); + viewer.onPointerWorld((p) => setPointerWorld(p)); + + const onResize = () => viewer.resize(); + window.addEventListener('resize', onResize); + + (async () => { + const saved = await getAllOptions(); + const next: PersistedOptions = { + grid: typeof saved.grid === 'boolean' ? saved.grid : DEFAULT_OPTIONS.grid, + zoomSpeed: typeof saved.zoomSpeed === 'number' ? saved.zoomSpeed : DEFAULT_OPTIONS.zoomSpeed, + zScale: typeof saved.zScale === 'number' ? saved.zScale : DEFAULT_OPTIONS.zScale, + lineweightOn: + saved.lineweightOn === true || saved.lineweightOn === false ? saved.lineweightOn : null, + lineweightScale: typeof saved.lineweightScale === 'number' ? saved.lineweightScale : null, + }; + setOptions(next); + viewer.setGrid(next.grid); + viewer.setZoomSpeed(next.zoomSpeed); + viewer.setZScale(next.zScale); + viewer.setLineweightEnabled(next.lineweightOn); + viewer.setLineweightScale(next.lineweightScale); + })(); + + return () => { + window.removeEventListener('resize', onResize); + viewer.onPointerWorld(null); + viewerRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const loadParseResult = useCallback( + (result: unknown) => { + const viewer = viewerRef.current; + if (!viewer) return; + const spaceList = listCadSpaces(result as any); + const def = pickDefaultSpace(spaceList); + viewer.load(result, { spaceHandle: def?.handleHex ?? null }); + refreshSpaces(); + refreshLayers(); + setHiddenLayersState(new Set()); + setSelectedEntity(null); + }, + [refreshSpaces, refreshLayers], + ); + + const loadArrayBuffer = useCallback( + async (name: string, buf: ArrayBuffer) => { + setStatus('loading'); + setStatusMessage(`불러오는 중… (${name})`); + setLoadingProgress(null); + try { + const result = await parseByName(name, buf); + loadParseResult(result); + setStatus('ready'); + setStatusMessage(`로드 완료: ${name}`); + } catch (err) { + setStatus('error'); + setStatusMessage(err instanceof Error ? err.message : '파일을 불러오지 못했습니다'); + } finally { + setLoadingProgress(null); + } + }, + [loadParseResult], + ); + + const loadFile = useCallback( + async (file: File) => { + const buf = await file.arrayBuffer(); + await loadArrayBuffer(file.name, buf); + }, + [loadArrayBuffer], + ); + + const loadUrl = useCallback( + async (url: string, label?: string) => { + const name = label ?? url.split('/').pop() ?? url; + setStatus('loading'); + setStatusMessage(`다운로드 중… (${name})`); + setLoadingProgress(0); + try { + const res = await fetch(url); + if (!res.ok) throw new Error(`다운로드 실패 (${res.status})`); + const total = Number(res.headers.get('content-length') ?? 0); + let buf: ArrayBuffer; + if (total > 0 && res.body) { + const reader = res.body.getReader(); + const chunks: Uint8Array[] = []; + let received = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + if (value) { + chunks.push(value); + received += value.length; + setLoadingProgress(Math.min(99, Math.round((received / total) * 100))); + } + } + const merged = new Uint8Array(received); + let offset = 0; + for (const chunk of chunks) { + merged.set(chunk, offset); + offset += chunk.length; + } + buf = merged.buffer; + } else { + setLoadingProgress(null); + buf = await res.arrayBuffer(); + } + await loadArrayBuffer(name, buf); + } catch (err) { + setStatus('error'); + setStatusMessage(err instanceof Error ? err.message : '다운로드하지 못했습니다'); + setLoadingProgress(null); + } + }, + [loadArrayBuffer], + ); + + const fit = useCallback(() => viewerRef.current?.fit(), []); + + const zoomToSelection = useCallback(() => viewerRef.current?.zoomToSelection(), []); + + const selectSpace = useCallback( + (handleHex: string | null) => { + viewerRef.current?.setSpace(handleHex); + refreshSpaces(); + refreshLayers(); + }, + [refreshSpaces, refreshLayers], + ); + + const toggleTheme = useCallback(() => { + setThemeState((prev) => { + const next = prev === 'dark' ? 'light' : 'dark'; + viewerRef.current?.setTheme(next === 'dark'); + refreshLayers(); + return next; + }); + }, [refreshLayers]); + + const setGrid = useCallback((on: boolean) => { + viewerRef.current?.setGrid(on); + setOptions((o) => ({ ...o, grid: on })); + void setOption('grid', on); + }, []); + + const setZoomSpeed = useCallback((v: number) => { + viewerRef.current?.setZoomSpeed(v); + setOptions((o) => ({ ...o, zoomSpeed: v })); + void setOption('zoomSpeed', v); + }, []); + + const setZScale = useCallback((v: number) => { + viewerRef.current?.setZScale(v); + setOptions((o) => ({ ...o, zScale: v })); + void setOption('zScale', v); + }, []); + + const setLineweightOn = useCallback((on: boolean | null) => { + viewerRef.current?.setLineweightEnabled(on); + setOptions((o) => ({ ...o, lineweightOn: on })); + void setOption('lineweightOn', on); + }, []); + + const setLineweightScale = useCallback((v: number | null) => { + viewerRef.current?.setLineweightScale(v); + setOptions((o) => ({ ...o, lineweightScale: v })); + void setOption('lineweightScale', v); + }, []); + + const set25D = useCallback((on: boolean) => { + viewerRef.current?.set25D(on); + setIs25DState(on); + }, []); + + const resetOptions = useCallback(() => { + setLineweightScale(null); + setLineweightOn(null); + setGrid(false); + setZoomSpeed(1); + setZScale(1); + set25D(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const toggleLayer = useCallback((name: string) => { + setHiddenLayersState((prev) => { + const next = new Set(prev); + if (next.has(name)) next.delete(name); + else next.add(name); + viewerRef.current?.setHiddenLayers(next); + return next; + }); + }, []); + + const showAllLayers = useCallback(() => { + setHiddenLayersState(() => { + const next = new Set(); + viewerRef.current?.setHiddenLayers(next); + return next; + }); + }, []); + + const hideAllLayers = useCallback(() => { + setHiddenLayersState(() => { + const next = new Set(layers.map((l) => l.name)); + viewerRef.current?.setHiddenLayers(next); + return next; + }); + }, [layers]); + + const getLineweightClasses = useCallback(() => viewerRef.current?.getLineweightClasses() ?? [], []); + + return { + containerRef, + viewerRef, + status, + statusMessage, + setStatusMessage, + loadingProgress, + selectedEntity, + pointerWorld, + spaces, + activeSpace, + layers, + hiddenLayers, + theme, + is25D, + options, + loadFile, + loadUrl, + fit, + zoomToSelection, + selectSpace, + toggleTheme, + set25D, + setGrid, + setZoomSpeed, + setZScale, + setLineweightOn, + setLineweightScale, + resetOptions, + toggleLayer, + showAllLayers, + hideAllLayers, + getLineweightClasses, + }; +} + +export type UseDwgViewer = ReturnType; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..7786bc4 --- /dev/null +++ b/src/index.css @@ -0,0 +1,10 @@ +@import "tailwindcss" source("./"); +@import "@hm-design/ui/tailwind.css"; +@import "@hm-design/ui/theme.css"; + +html, +body, +#root { + height: 100%; + margin: 0; +} diff --git a/src/lib/optionsStore.ts b/src/lib/optionsStore.ts new file mode 100644 index 0000000..b0b83c7 --- /dev/null +++ b/src/lib/optionsStore.ts @@ -0,0 +1,75 @@ +/** + * 뷰어 옵션 영구 저장소 (IndexedDB). + * + * localStorage 대신 IndexedDB 를 쓰는 이유: 값이 문자열로 강제되지 않고(숫자/불린/ + * 객체 그대로), 용량 제한이 사실상 없으며, 나중에 도면별 설정처럼 구조가 커져도 + * 같은 저장소를 그대로 쓸 수 있다. + * + * DB 하나(`hmw-viewer`) 안에 key-value 스토어(`options`) 하나만 둔다. + * 실패(사생활 보호 모드, 스토리지 차단 등)해도 앱이 죽지 않도록 모든 API 는 + * 조용히 기본값으로 떨어진다. + */ + +const DB_NAME = 'hmw-viewer'; +const DB_VERSION = 1; +const STORE = 'options'; + +let dbPromise: Promise | null = null; + +function openDb(): Promise { + if (dbPromise) return dbPromise; + dbPromise = new Promise((resolve) => { + if (typeof indexedDB === 'undefined') return resolve(null); + let req: IDBOpenDBRequest; + try { + req = indexedDB.open(DB_NAME, DB_VERSION); + } catch { + return resolve(null); + } + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE); + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => resolve(null); + req.onblocked = () => resolve(null); + }); + return dbPromise; +} + +function tx(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest): Promise { + return openDb().then( + (db) => + new Promise((resolve) => { + if (!db) return resolve(null); + try { + const t = db.transaction(STORE, mode); + const req = run(t.objectStore(STORE)); + req.onsuccess = () => resolve(req.result as T); + req.onerror = () => resolve(null); + } catch { + resolve(null); + } + }), + ); +} + +/** 저장된 옵션 하나. 없으면 `fallback`. */ +export async function getOption(key: string, fallback: T): Promise { + const v = await tx('readonly', (s) => s.get(key)); + return v === null || v === undefined ? fallback : v; +} + +/** 옵션 하나 저장. 저장이 막혀 있어도 예외를 던지지 않는다. */ +export async function setOption(key: string, value: unknown): Promise { + await tx('readwrite', (s) => s.put(value, key)); +} + +/** 전체 옵션을 한 번에 읽는다 (기동 시 1회 호출용). */ +export async function getAllOptions(): Promise> { + const keys = (await tx('readonly', (s) => s.getAllKeys())) ?? []; + const vals = (await tx('readonly', (s) => s.getAll())) ?? []; + const out: Record = {}; + keys.forEach((k, i) => { out[String(k)] = vals[i]; }); + return out; +} diff --git a/src/lib/propertyFormat.ts b/src/lib/propertyFormat.ts new file mode 100644 index 0000000..b349766 --- /dev/null +++ b/src/lib/propertyFormat.ts @@ -0,0 +1,287 @@ +/** + * Entity Property formatter, ported unchanged from dwg-dxf-viewer-sample's + * propertyInspector.ts (DOM-rendering half replaced by PropertiesPanel.tsx). + */ + +export interface FormattedProperties { + id: string; + type: string; + handle: string; + layer: string; + general: [string, string][]; + geometry: [string, string][]; +} + +const f2 = (v: any): string => (typeof v === 'number' ? v.toFixed(2) : (v ?? '-')); +const f3 = (v: any): string => (typeof v === 'number' ? v.toFixed(3) : (v ?? '-')); +const deg = (r: any): string => { + if (typeof r !== 'number') return '-'; + let d = (r * 180) / Math.PI; + while (d < 0) d += 360; + while (d >= 360) d -= 360; + return d.toFixed(1) + '°'; +}; + +const pt = (p: any): string => { + if (!p) return '-'; + const x = typeof p.x === 'number' ? f2(p.x) : '0.00'; + const y = typeof p.y === 'number' ? f2(p.y) : '0.00'; + const z = typeof p.z === 'number' && Math.abs(p.z) > 1e-6 ? `, ${f2(p.z)}` : ''; + return `${x}, ${y}${z}`; +}; + +function formatColor(colorVal: any): string { + if (colorVal == null) return 'BYLAYER'; + if (typeof colorVal === 'number') { + if (colorVal === 256) return 'BYLAYER (256)'; + if (colorVal === 0) return 'BYBLOCK (0)'; + return `ACI ${colorVal}`; + } + return String(colorVal); +} + +/** DWG lineweight: i16 in 1/100 mm, with -1/-2/-3 reserved for the inherit modes. */ +function formatLineweight(raw: unknown): string { + if (typeof raw !== 'number') return 'BYLAYER'; + if (raw === -1) return 'BYLAYER'; + if (raw === -2) return 'BYBLOCK'; + if (raw === -3) return 'DEFAULT (0.25mm)'; + if (raw < 0) return String(raw); + return `${(raw / 100).toFixed(2)}mm`; +} + +export function formatEntityProperties(entity: any): FormattedProperties { + if (!entity) { + return { id: '', type: '', handle: '', layer: '', general: [], geometry: [] }; + } + + const d = entity.data || entity; + const type = (entity.type || entity.typeName || 'ENTITY').toUpperCase(); + const handle = entity.handle?.value ?? entity.handle ?? '-'; + const layer = entity.layer ?? entity.layerName ?? '0'; + const color = formatColor(entity.color ?? d.color); + const linetype = entity.lineType ?? entity.linetype ?? d.lineType ?? 'BYLAYER'; + const lineweight = formatLineweight( + entity.entityHeader?.lineWeight ?? entity.lineWeight ?? entity.lineweight ?? d.lineweight, + ); + + const general: [string, string][] = [ + ['유형 (Type)', type], + ['핸들 (Handle)', String(handle)], + ['레이어 (Layer)', String(layer)], + ['색상 (Color)', color], + ['선종류 (Linetype)', String(linetype)], + ['선가중치 (Lineweight)', lineweight], + ]; + + const geometry: [string, string][] = []; + + switch (type) { + case 'LINE': { + if (d.start && d.end) { + const dx = d.end.x - d.start.x; + const dy = d.end.y - d.start.y; + const dz = (d.end.z || 0) - (d.start.z || 0); + const len = Math.hypot(dx, dy, dz); + const angle = Math.atan2(dy, dx); + + geometry.push(['시작점 (Start)', pt(d.start)]); + geometry.push(['끝점 (End)', pt(d.end)]); + geometry.push(['증분 (ΔX, ΔY)', `${f2(dx)}, ${f2(dy)}`]); + geometry.push(['길이 (Length)', f3(len)]); + geometry.push(['각도 (Angle)', deg(angle)]); + } + break; + } + + case 'CIRCLE': { + if (d.center && d.radius != null) { + const r = d.radius; + const diameter = r * 2; + const circumference = 2 * Math.PI * r; + const area = Math.PI * r * r; + + geometry.push(['중심점 (Center)', pt(d.center)]); + geometry.push(['반지름 (Radius)', f3(r)]); + geometry.push(['지름 (Diameter)', f3(diameter)]); + geometry.push(['둘레 (Circumference)', f3(circumference)]); + geometry.push(['넓이 (Area)', f3(area)]); + } + break; + } + + case 'ARC': { + if (d.center && d.radius != null) { + const r = d.radius; + const sa = d.startAngle ?? 0; + const ea = d.endAngle ?? Math.PI * 2; + let sweep = ea - sa; + while (sweep < 0) sweep += Math.PI * 2; + const arcLen = sweep * r; + + geometry.push(['중심점 (Center)', pt(d.center)]); + geometry.push(['반지름 (Radius)', f3(r)]); + geometry.push(['시작각 (Start Angle)', deg(sa)]); + geometry.push(['끝각 (End Angle)', deg(ea)]); + geometry.push(['중심각 (Sweep Angle)', deg(sweep)]); + geometry.push(['호 길이 (Arc Length)', f3(arcLen)]); + } + break; + } + + case 'LWPOLYLINE': + case 'POLYLINE': { + const rawPts = d.points ?? d.vertices ?? []; + const isClosed = !!(d.closed || (d.flags & 1)); + let totalLen = 0; + + for (let i = 0; i < rawPts.length - (isClosed ? 0 : 1); i++) { + const p1 = rawPts[i]; + const p2 = rawPts[(i + 1) % rawPts.length]; + if (p1 && p2) { + totalLen += Math.hypot(p2.x - p1.x, p2.y - p1.y); + } + } + + geometry.push(['정점 개수 (Vertices)', String(rawPts.length)]); + geometry.push(['닫힘 여부 (Closed)', isClosed ? '예 (Closed)' : '아니오 (Open)']); + geometry.push(['총 길이 (Total Length)', f3(totalLen)]); + + if (isClosed && rawPts.length >= 3) { + let shoelace = 0; + for (let i = 0; i < rawPts.length; i++) { + const p1 = rawPts[i]; + const p2 = rawPts[(i + 1) % rawPts.length]; + shoelace += p1.x * p2.y - p2.x * p1.y; + } + geometry.push(['면적 (Area)', f3(Math.abs(shoelace) / 2)]); + } + + if (rawPts.length > 0) { + const sampleCount = Math.min(rawPts.length, 4); + for (let i = 0; i < sampleCount; i++) { + geometry.push([`정점 P${i + 1}`, pt(rawPts[i])]); + } + if (rawPts.length > sampleCount) { + geometry.push(['...', `외 ${rawPts.length - sampleCount}개 정점`]); + } + } + break; + } + + // MESH (AcDbSubDMesh). dwg-wasm emits flat arrays: vertices [x,y,z,…], + // faceList [n, i0 … i(n-1)] repeated, edges [a,b,…]. + case 'MESH': { + const verts: number[] = d.vertices ?? []; + const vCount = d.vertexCount ?? Math.floor(verts.length / 3); + const fCount = d.faceCount ?? 0; + const eCount = Math.floor((d.edges?.length ?? 0) / 2); + const sub = d.subdivisionLevel ?? 0; + + geometry.push(['정점 개수 (Vertices)', String(vCount)]); + geometry.push(['면 개수 (Faces)', String(fCount)]); + geometry.push(['모서리 개수 (Edges)', String(eCount)]); + geometry.push(['세분 레벨 (Subdivision)', String(sub)]); + geometry.push(['크리스 혼합 (Blend Crease)', d.blendCrease ? '예 (Yes)' : '아니오 (No)']); + + if (vCount > 0) { + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + for (let i = 0; i + 2 < verts.length; i += 3) { + const x = verts[i], y = verts[i + 1], z = verts[i + 2]; + if (x < minX) minX = x; if (x > maxX) maxX = x; + if (y < minY) minY = y; if (y > maxY) maxY = y; + if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; + } + geometry.push(['최소점 (Min)', pt({ x: minX, y: minY, z: minZ })]); + geometry.push(['최대점 (Max)', pt({ x: maxX, y: maxY, z: maxZ })]); + geometry.push(['표고 범위 (Z Range)', `${f3(minZ)} ~ ${f3(maxZ)}`]); + } + break; + } + + case 'TEXT': + case 'MTEXT': + case 'ATTRIB': { + const content = d.text ?? entity.text ?? d.textValue ?? '-'; + const height = d.textHeight ?? d.height ?? entity.textHeight; + const rot = d.rotationAngle ?? d.rotation ?? 0; + const pos = d.insertionPoint ?? d.insertionPt ?? d.alignmentPt ?? d.pos; + + geometry.push(['텍스트 내용 (Content)', String(content)]); + if (height != null) geometry.push(['높이 (Height)', f3(height)]); + geometry.push(['회전각 (Rotation)', deg(rot)]); + if (pos) geometry.push(['삽입점 (Position)', pt(pos)]); + break; + } + + case 'INSERT': { + const name = d.name ?? d.blockName ?? entity.name ?? '-'; + const pos = d.insertionPoint ?? d.insertionPt ?? entity.insertionPt; + const sx = d.xScale ?? d.scale?.x ?? 1; + const sy = d.yScale ?? d.scale?.y ?? 1; + const sz = d.zScale ?? d.scale?.z ?? 1; + const rot = d.rotation ?? 0; + + geometry.push(['블록 이름 (Block)', String(name)]); + if (pos) geometry.push(['삽입점 (Position)', pt(pos)]); + geometry.push(['축척 (Scale X,Y,Z)', `${f2(sx)}, ${f2(sy)}, ${f2(sz)}`]); + geometry.push(['회전각 (Rotation)', deg(rot)]); + break; + } + + case 'HATCH': { + const isSolid = d.solidFill ?? entity.solidFill ?? false; + const pattern = d.patternName ?? d.name ?? (isSolid ? 'SOLID' : 'HATCH'); + const paths = d.paths ?? entity.paths ?? []; + + geometry.push(['패턴 (Pattern)', String(pattern)]); + geometry.push(['채우기 유형 (Type)', isSolid ? '단색 채우기 (Solid)' : '패턴 (Pattern)']); + geometry.push(['경계 루프 수 (Loops)', String(paths.length)]); + break; + } + + case 'DIMENSION': + case 'DIMENSION_LINEAR': + case 'DIMENSION_ALIGNED': + case 'DIMENSION_RADIUS': + case 'DIMENSION_DIAMETER': + case 'DIMENSION_ANG_3PT': + case 'DIMENSION_ANG_2LN': + case 'DIMENSION_ORDINATE': { + const meas = d.actualMeasurement ?? entity.actualMeasurement; + const textVal = d.userText || (meas != null ? f3(meas) : '-'); + const midPt = d.textMidPt ?? entity.textMidPt; + + geometry.push(['치수 유형 (Dim Type)', type]); + if (meas != null) geometry.push(['측정값 (Measurement)', f3(meas)]); + geometry.push(['표시 텍스트 (Text)', String(textVal)]); + if (midPt) geometry.push(['텍스트 위치 (Text Mid)', pt(midPt)]); + break; + } + + case 'ELLIPSE': { + if (d.center) { + geometry.push(['중심점 (Center)', pt(d.center)]); + if (d.majorAxis) geometry.push(['주축 벡터 (Major Axis)', pt(d.majorAxis)]); + if (d.axisRatio != null || d.ratio != null) { + geometry.push(['단축 비율 (Ratio)', f3(d.axisRatio ?? d.ratio)]); + } + } + break; + } + + default: { + let count = 0; + for (const [k, v] of Object.entries(d)) { + if (typeof v === 'object' || v == null || typeof v === 'function') continue; + geometry.push([k, String(v)]); + count++; + if (count >= 12) break; + } + } + } + + const id = `${type}${handle !== '-' ? ' · #' + handle : ''}`; + return { id, type, handle: String(handle), layer: String(layer), general, geometry }; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..dfacde0 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/viewer2d/Viewer2D.d.ts b/src/viewer2d/Viewer2D.d.ts new file mode 100644 index 0000000..f18295e --- /dev/null +++ b/src/viewer2d/Viewer2D.d.ts @@ -0,0 +1,50 @@ +// Types for the ported JS 2D engine (Viewer2D.js). Colocated so tsc resolves +// `./Viewer2D.js` imports without allowJs. Only the methods the app uses are typed. +import type { CadSpace } from './cadSpaces'; + +export class Viewer2D { + constructor(container: HTMLElement); + /** Render a DWG/DXF parseResult. `keepView` preserves pan/zoom on re-render. */ + load(result: unknown, opts?: { keepView?: boolean; keepTheme?: boolean; spaceHandle?: string | number | null }): void; + /** Zoom-extents to the loaded drawing. */ + fit(): void; + /** Resync renderer/camera to the container size (call after un-hiding). */ + resize(): void; + onSelect(cb: (entity: unknown) => void): void; + getSelectedEntity(): unknown; + zoomToSelection(): void; + setAccent(hex: string): void; + setTheme(dark: boolean): void; + setGrid(visible: boolean): void; + setZoomSpeed(speed: number): void; + getZoomSpeed(): number; + /** 2.5D (tilted 3D) view: orbit the drawing so entity elevation (Z) is visible. */ + set25D(on: boolean, opts?: { keepView?: boolean }): void; + is25D(): boolean; + /** Height exaggeration for 2.5D (1 = true scale); ignored in top view. */ + setZScale(k: number): void; + getZScale(): number; + /** Lineweight display (AutoCAD LWDISPLAY); null follows the drawing's own flag. */ + setLineweightEnabled(on: boolean | null): void; + getLineweightEnabled(): boolean; + /** Lineweight display scale in px per mm (AutoCAD's display-scale slider). Default 20. */ + setLineweightScale(pxPerMm: number | null): void; + getLineweightScale(): number; + /** Distinct lineweights (mm) present in the loaded drawing, ascending. */ + getLineweightClasses(): number[]; + getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[]; + setHiddenLayers(nameSet: Set): void; + /** Model / Paper (Layout) spaces for the loaded drawing. */ + getSpaces(): CadSpace[]; + getActiveSpace(): string | null; + /** Switch Model ↔ Layout; re-renders (fit unless keepView). */ + setSpace(spaceHandle: string | number | null, opts?: { keepView?: boolean }): void; + getZoomPercent(): number; + onViewChange(cb: () => void): void; + /** Screen client coords → drawing world (Z=0 plane). */ + screenToWorld(clientX: number, clientY: number): { x: number; y: number; z: number } | null; + /** Live CAD coords under the cursor; null when pointer leaves the canvas. */ + onPointerWorld(cb: ((p: { x: number; y: number; z: number } | null) => void) | null): void; + startMeasure(cb: (r: { phase: string; ax: number; ay: number; az: number; bx: number; by: number; bz: number; d: number }) => void): void; + stopMeasure(): void; +} diff --git a/src/viewer2d/Viewer2D.js b/src/viewer2d/Viewer2D.js new file mode 100644 index 0000000..0154861 --- /dev/null +++ b/src/viewer2d/Viewer2D.js @@ -0,0 +1,4532 @@ +/** + * Viewer2D — three.js renderer for DWG/DXF parseResult (orthographic 2D drawing). + * Ported from webviewer/src/viewer/Viewer3D.js (class renamed 3D→2D) for the + * 2D+3D merge. Renders CAD entities as LineSegments / Mesh / CanvasTexture sprites. + * Supported: LINE, CIRCLE, ARC, LWPOLYLINE, POLYLINE, POINT, ELLIPSE, SOLID, + * TEXT, MTEXT, INSERT (ownerHandle block expansion), HATCH (solid+outline+bulge), + * MESH (AcDbSubDMesh, base-mesh wireframe), + * DIMENSION_LINEAR/ALIGNED/RADIUS/DIAMETER/ANG_3PT/ANG_2LN/ORDINATE + */ +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js'; +import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; +import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; +import { aciToHex } from './aciColors.js'; +import { buildAllowedOwnerHexes, listCadSpaces } from './cadSpaces'; +import { SlugTextEngine, SlugTextBatch } from './slugText'; +import { getLinPattern, mergeDwgLinetypePattern } from './linParser'; + +const DEFAULT_COLOR = 0xc9d1d9; +const ARC_SEGS = 64; +const ELLIPSE_SEGS = 72; +const CLICK_THRESHOLD_PX = 14; + +// ── Lineweight (AutoCAD LWDISPLAY) ───────────────────────────────────────── +// DWG stores lineweight as an i16 in 1/100 mm (-1 ByLayer, -2 ByBlock, +// -3 Default). AutoCAD shows it in *screen* pixels — the width does not grow +// when you zoom in. The px-per-mm factor is AutoCAD's "Adjust Display Scale" +// slider and depends on the monitor, so it is only a default: 11 px/mm is the +// value that matched an AutoCAD reference side by side (0.35 mm ≈ 4 px, +// 0.50 mm ≈ 5.5 px). Retune per host/monitor with setLineweightScale(). +const LW_PX_PER_MM = 11; +// LWDEFAULT: what -3 (and a layer that never set one) resolves to. +const LW_DEFAULT_MM = 0.25; +// At or below the CAD default the line stays in the 1-px hairline batch: a +// drawing that never assigned weights must look exactly as it did before. +const LW_HAIRLINE_MM = 0.25; +const LW_MAX_PX = 48; +// Fat lines cost 12 floats + 8 verts per segment (instanced quads). Past this +// many segments in one weight bucket, fall back to hairline instead of risking +// a GPU/heap stall on huge survey drawings. +const LW_MAX_FAT_SEGS = 400000; + +/** Bucket pending texts by their owning CAD layer (insertion order preserved). */ +function groupByLayer(list) { + const m = new Map(); + for (const t of list) { + const k = t.layer ?? '0'; + let arr = m.get(k); + if (!arr) m.set(k, arr = []); + arr.push(t); + } + return m; +} + +function stripMText(s) { + if (!s) return ''; + return s + .replace(/\\A\d+;/g, '') // \A1; vertical alignment + .replace(/\\p[^;]*;/g, '') // \p...; paragraph properties (lowercase, has ;) + .replace(/\\P/g, '\n') // \P paragraph break → newline (capital, no ;) + .replace(/\\[a-zA-Z][^;]*;/g, '') // remaining inline codes \f \H \C \W \Q \T... + .replace(/\{[^{}]*\}/g, m => stripMText(m.slice(1, -1))) + .replace(/%%d/gi, '°').replace(/%%p/gi, '±').replace(/%%c/gi, 'Ø') + .replace(/[{}]/g, '') + .replace(/[ \t]{2,}/g, ' ') // collapse runs of spaces + .replace(/^[ \t]+|[ \t]+$/gm, '') // trim each line's edges (keeps interior \n) + .replace(/\n{3,}/g, '\n\n') + .replace(/^\n+|\n+$/g, ''); // drop leading/trailing blank lines +} + +export class Viewer2D { + constructor(container) { + this._container = container; + this._entityMeta = []; + this._layerByHandle = new Map(); + this._layerByName = new Map(); + this._onSelectCb = null; + this._measureActive = false; + this._measureCb = null; + this._measurePtA = null; + this._measureMarkers = []; + this._textGen = 0; // invalidation token for the async Slug text flush + // ── Layer visibility index (see _applyLayerVisibility) ─────────────────── + // Layer on/off used to re-run load() = full re-tessellation (5 s+ on an + // 18 MB DWG). Instead we build every layer once and toggle visibility: + // · _layerObjs layer name → Object3D[] (hatch/solid/text/sprite meshes) + // · _indexedBufs merged LineSegments whose index buffer is rebuilt from a + // per-segment layer id — keeps the original draw order. + // · _layerBoxes / _layerSamples per-layer AABB + fit samples so Fit still + // frames only what is visible. + this._curLayer = null; + this._curLayerId = -1; + this._layerObjs = new Map(); + this._layerBoxes = new Map(); + this._layerSamples = new Map(); + this._indexedBufs = []; + // · _fatBufs LineSegments2 batches (one per lineweight/dash bucket). + // Instanced, so layer masking compacts the instance buffer + // instead of rewriting an index (see _applyLayerVisibility). + this._fatBufs = []; + this._metaHidden = null; + this._fullContentBox = null; + this._hiddenLayers = new Set(); + // Lineweight display: null = follow the drawing's LWDISPLAY header var, + // true/false = forced by the host UI (setLineweightEnabled). + this._lwForced = null; + this._lwDisplay = false; + this._curLwPx = 0; + this._lwScale = null; // null = LW_PX_PER_MM (setLineweightScale) + // ── 2.5D view ──────────────────────────────────────────────────────────── + // Entities already carry their CAD elevation (Z) in the vertex buffers — + // the classic view simply stares straight down at them. 2.5D re-aims the + // same orthographic camera at an isometric angle and unlocks OrbitControls' + // rotate, so contours / TIN / 3D polylines show their relief. Nothing is + // re-tessellated: toggling is a camera + controls change only. + this._is25D = false; + this._zScale = 1; // height exaggeration (flat relief over a wide site) + this._need25Reset = false; // next _fit should snap back to the default angle + this._contentZ = null; // true Z range of the load (the fit box is flattened) + // Active viewport transform for filled geometry, set only while + // _emitModelThroughViewport runs. Lines reach the viewport through pushSeg, + // but SOLID/HATCH fills and dimension arrowheads hand a finished mesh built + // from model coordinates straight to _addObj — this is how that mesh finds + // out it belongs on a sheet. null outside the viewport pass. + this._fillXf = null; + SlugTextEngine.shared().catch(() => {}); // warm the font load; sprite fallback covers failure + + this._scene = new THREE.Scene(); + this._scene.background = new THREE.Color(0x0d1117); + this._isDark = true; + this._isLoading = false; + + const w = container.clientWidth || 800; + const h = container.clientHeight || 600; + this._camera = new THREE.OrthographicCamera(-w/2, w/2, h/2, -h/2, -10000, 10000); + this._camera.position.set(0, 0, 100); + + this._renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); + this._renderer.setPixelRatio(window.devicePixelRatio); + this._renderer.setSize(w, h); + container.appendChild(this._renderer.domElement); + + this._controls = new OrbitControls(this._camera, this._renderer.domElement); + this._controls.enableRotate = false; + this._controls.screenSpacePanning = true; + this._controls.zoomToCursor = true; + this._controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN }; + this._controls.touches = { ONE: THREE.TOUCH.PAN, TWO: THREE.TOUCH.DOLLY_PAN }; + + const savedSpeed = localStorage.getItem('hmw:zoom-speed'); + if (savedSpeed) { + const speed = parseFloat(savedSpeed); + if (!isNaN(speed)) { + this._controls.zoomSpeed = speed; + } + } + + window.addEventListener('keydown', (e) => { if (e.key === 'f' || e.key === 'F') this.fit(); }); + + this._group = new THREE.Group(); + this._scene.add(this._group); + + window.addEventListener('resize', () => this._onResize()); + // Prevent browser auto-scroll popup when middle-button is pressed on the WebGL canvas + this._renderer.domElement.addEventListener('pointerdown', (e) => { + if (e.button === 1) e.preventDefault(); + }); + this._renderer.domElement.addEventListener('mousedown', (e) => { + this._downXY = [e.clientX, e.clientY]; + if (e.button === 1) e.preventDefault(); + }); + this._renderer.domElement.addEventListener('click', (e) => this._onClick(e)); + this._renderer.domElement.addEventListener('pointermove', (e) => this._onPointerMove(e)); + this._renderer.domElement.addEventListener('pointerleave', () => { + if (this._onPointerWorldCb) this._onPointerWorldCb(null); + }); + this._selColor = { r: 0xd8 / 255, g: 0x3a / 255, b: 0x2f / 255 }; + this._gridVisible = false; + this._gridMesh = null; + /** @type {string|null} active Model/Paper space handle hex; null = no space filter */ + this._activeSpaceHex = null; + /** @type {null|((p:{x:number,y:number,z:number}|null)=>void)} */ + this._onPointerWorldCb = null; + this._ptrWorldTmp = new THREE.Vector3(); + // Display frame for coordinates (Model UCS vs WCS). Paper uses identity. + this._coordOrigin = { x: 0, y: 0, z: 0 }; + this._coordXAxis = { x: 1, y: 0, z: 0 }; + this._coordYAxis = { x: 0, y: 1, z: 0 }; + this._controls.addEventListener('change', () => { if (this._gridVisible) this._rebuildGrid(); }); + this._animate(); + } + + onSelect(cb) { this._onSelectCb = cb; } + resize() { this._onResize(); } + setZoomSpeed(speed) { if (this._controls) this._controls.zoomSpeed = speed; } + getZoomSpeed() { return this._controls ? this._controls.zoomSpeed : 1.0; } + + // ── 2.5D view ────────────────────────────────────────────────────────────── + + /** + * Toggle the 2.5D (tilted 3D) view. + * + * ON — camera moves to an SE-isometric angle (+X/−Y/+Z, the AutoCAD default), + * Z becomes the orbit pole and LEFT-drag orbits (MIDDLE/RIGHT still pan). + * OFF — straight-down top view with rotate locked; identical to before. + * + * Geometry is untouched, so layer masks, lineweights and picking survive. + * @param {boolean} on + * @param {{ keepView?: boolean }} [opts] keepView skips the zoom-extents re-fit + */ + set25D(on, opts = {}) { + const want = !!on; + if (want === this._is25D) return; + this._is25D = want; + this._need25Reset = want; + const c = this._controls; + if (want) { + c.enableRotate = true; + c.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN }; + c.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN }; + // CAD elevation is +Z, so the orbit pole must be Z or dragging feels sideways. + this._camera.up.set(0, 0, 1); + } else { + c.enableRotate = false; + c.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN }; + c.touches = { ONE: THREE.TOUCH.PAN, TWO: THREE.TOUCH.DOLLY_PAN }; + } + this._applyZScale(); + if (opts.keepView) { c.update(); return; } + this.fit(); + } + + is25D() { return this._is25D; } + + /** + * Height exaggeration for 2.5D. A survey sheet is often kilometres wide with + * only metres of relief, which reads as dead flat at any orbit angle — + * multiplying Z makes the terrain legible. Only applied in 2.5D (in top view + * it would be invisible anyway) and only as a scale on the scene group, so + * pick coordinates and the X/Y readout stay true to the drawing. + * @param {number} k >0; 1 = true scale + */ + setZScale(k) { + const v = Number(k); + this._zScale = Number.isFinite(v) && v > 0 ? v : 1; + this._applyZScale(); + this._refreshClip25(); + } + + getZScale() { return this._zScale; } + + _applyZScale() { + this._group.scale.set(1, 1, this._is25D ? (this._zScale || 1) : 1); + this._group.updateMatrixWorld(true); + } + + /** True Z extents of the drawing (the fit box is flattened to z=0). */ + _zRange() { + const z = this._contentZ; + if (!z || !Number.isFinite(z.min) || !Number.isFinite(z.max)) return { min: 0, max: 0 }; + return z; + } + + /** + * Re-cut the orthographic clip slab around the (possibly Z-exaggerated) + * content without moving the camera — otherwise a big exaggeration pushes + * geometry through `near`/`far` and it vanishes mid-drag. + */ + _refreshClip25() { + if (!this._is25D) return; + const box = this._box25(this._contentBox || this._fullContentBox); + if (!box) return; + const diag = Math.max(box.getSize(new THREE.Vector3()).length(), 1e-6); + const span = this._camera.position.distanceTo(this._controls.target) + diag + 1; + this._camera.near = -span; + this._camera.far = span; + this._camera.updateProjectionMatrix(); + } + + /** Fit box with the real (exaggerated) Z range restored. */ + _box25(src) { + if (!src || src.isEmpty()) return null; + const b = src.clone(); + if (b.max.z - b.min.z < 1e-9) { + const z = this._zRange(); + b.min.z = z.min; + b.max.z = z.max; + } + const k = this._zScale || 1; + b.min.z *= k; + b.max.z *= k; + if (b.max.z < b.min.z) { const t = b.min.z; b.min.z = b.max.z; b.max.z = t; } + return b; + } + + /** + * Subscribe to mouse world-coordinate updates (CAD WCS / drawing units). + * Callback receives `{ x, y, z }` or `null` when the pointer leaves the canvas. + */ + onPointerWorld(cb) { this._onPointerWorldCb = cb || null; } + + /** + * Convert browser client coords → **CAD display** coordinates (Z=0 plane). + * Model Space: model UCS (matches AutoCAD status bar when UCS = drawing UCS). + * Paper/Layout: paper drawing units (identity). + * Pick/measure still use raw WCS via `_screenToWcs`. + */ + screenToWorld(clientX, clientY) { + const wcs = this._screenToWcs(clientX, clientY); + if (!wcs) return null; + return this._wcsToCad(wcs.x, wcs.y); + } + + /** + * Raw WCS (geometry storage frame) under the cursor. + * Ortho 2D: map NDC through camera right/up axes (handles view-twist up vector). + */ + _screenToWcs(clientX, clientY) { + const el = this._renderer?.domElement; + const cam = this._camera; + if (!el || !cam) return null; + const rect = el.getBoundingClientRect(); + if (rect.width < 1 || rect.height < 1) return null; + + cam.updateMatrixWorld(true); + const ndcX = ((clientX - rect.left) / rect.width) * 2 - 1; + const ndcY = -((clientY - rect.top) / rect.height) * 2 + 1; + + // 2.5D: the view no longer lies in the drawing plane, so shoot the (parallel) + // ortho ray through the pixel and intersect the Z=0 plane. Exact for the + // drawing plane itself; entities lifted by their elevation read as the point + // of the plane under them, which is what a plan-coordinate readout means. + if (this._is25D) { + // Cached vectors: this runs on every pointermove. + const o = (this._ray25O ||= new THREE.Vector3()).set(ndcX, ndcY, -1).unproject(cam); + const d = cam.getWorldDirection(this._ray25D ||= new THREE.Vector3()); + if (Math.abs(d.z) < 1e-9) return null; + const t = -o.z / d.z; + return { x: o.x + d.x * t, y: o.y + d.y * t, z: 0 }; + } + + // Orthographic frustum half-sizes in world units (respect zoom). + const zoom = cam.zoom || 1; + const halfW = ((cam.right - cam.left) * 0.5) / zoom; + const halfH = ((cam.top - cam.bottom) * 0.5) / zoom; + + // Camera basis in world (columns of matrixWorld). + const te = cam.matrixWorld.elements; + const rx = te[0], ry = te[1]; // camera local +X (right) + const ux = te[4], uy = te[5]; // camera local +Y (up) + // Anchor on the look target (z=0 plane), not camera.position.z + const tx = this._controls?.target?.x ?? cam.position.x; + const ty = this._controls?.target?.y ?? cam.position.y; + + return { + x: tx + ndcX * halfW * rx + ndcY * halfH * ux, + y: ty + ndcX * halfW * ry + ndcY * halfH * uy, + z: 0, + }; + } + + /** WCS → CAD display (model UCS when axes are available). */ + _wcsToCad(wx, wy) { + const o = this._coordOrigin || { x: 0, y: 0 }; + const xx = this._coordXAxis || { x: 1, y: 0 }; + const yy = this._coordYAxis || { x: 0, y: 1 }; + const dx = wx - (o.x || 0); + const dy = wy - (o.y || 0); + // Orthogonal axes: project onto X/Y directions (identity → subtract origin). + return { + x: dx * (xx.x || 0) + dy * (xx.y || 0), + y: dx * (yy.x || 0) + dy * (yy.y || 0), + z: 0, + }; + } + + /** Configure coordinate readout frame for the active space. */ + _setCoordFrame(result) { + const spaces = listCadSpaces(result); + const active = spaces.find((s) => s.handleHex === this._activeSpaceHex); + const vars = result?.vars || {}; + // Model: AutoCAD status bar uses model UCS origin/axes when set. + // Paper: sheet coordinates — identity. + if (active?.kind === 'model' && vars.ucsOrigin) { + this._coordOrigin = { + x: vars.ucsOrigin.x || 0, + y: vars.ucsOrigin.y || 0, + z: vars.ucsOrigin.z || 0, + }; + this._coordXAxis = vars.ucsXAxis || { x: 1, y: 0, z: 0 }; + this._coordYAxis = vars.ucsYAxis || { x: 0, y: 1, z: 0 }; + } else { + this._coordOrigin = { x: 0, y: 0, z: 0 }; + this._coordXAxis = { x: 1, y: 0, z: 0 }; + this._coordYAxis = { x: 0, y: 1, z: 0 }; + } + } + + _onPointerMove(e) { + if (!this._onPointerWorldCb) return; + const p = this.screenToWorld(e.clientX, e.clientY); + this._onPointerWorldCb(p); + } + + /** + * Zoom extents to visible entity AABB (space/layer filtered). + * Prefer the load-time content box (same filter as draw); recompute from + * scene if missing. Percentile-trim drops far junk verts. + */ + fit() { + // Prefer AABB recorded while drawing (matches visible entities exactly). + if (this._contentBox && !this._contentBox.isEmpty()) { + this._fit(this._contentBox); + return; + } + const box = new THREE.Box3(); + const samples = []; + this._group.traverse((obj) => { + if (!obj.geometry || obj.visible === false) return; + box.expandByObject(obj); + const pos = obj.geometry.attributes?.position; + if (!pos?.array) return; + const arr = pos.array; + const step = Math.max(3, Math.floor(arr.length / 3 / 4000) * 3); + for (let i = 0; i + 1 < arr.length; i += step) { + if (Number.isFinite(arr[i]) && Number.isFinite(arr[i + 1])) samples.push(arr[i], arr[i + 1]); + } + }); + if (box.isEmpty()) return; + const use = this._trimmedContentBox(samples, box) || box; + // Don't cache while layers are off — the merged line buffer still holds the + // hidden segments, so this box is wider than what's on screen. + if (!this._hiddenLayers.size) this._contentBox = use.clone(); + this._fit(use); + } + + load(result, opts = {}) { + this._isLoading = true; + try { + this._lastResult = result; + this._selMeta = null; + this._clear(); + this._entityMeta = []; + + // Model vs Paper (Layout): never draw both spaces at once. + // - opts.spaceHandle provided (including null) → set active space + // - omitted → keep previous _activeSpaceHex if still valid for this file + if (Object.prototype.hasOwnProperty.call(opts, 'spaceHandle')) { + const sh = opts.spaceHandle; + if (sh == null || sh === '') this._activeSpaceHex = null; + else this._activeSpaceHex = typeof sh === 'number' ? sh.toString(16) : String(sh).toLowerCase(); + } + + if (!opts.keepTheme) { + const spacesList = listCadSpaces(result); + const activeSp = spacesList.find((s) => s.handleHex === this._activeSpaceHex); + const isPaper = activeSp?.kind === 'paper'; + this.setTheme(!isPaper); + } + + this._buildLayerMap(result?.tables?.layers); + this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles); + this._buildLinetypeMap(result); + // LWDISPLAY decides whether weights show at all — same switch as AutoCAD's + // status-bar "Show/Hide Lineweight". The host UI can override it. + // DXF spells the header var $LWDISPLAY; dwg-wasm emits vars.lwdisplay. + this._lwDisplay = this._lwForced + ?? !!(result?.vars?.lwdisplay ?? result?.vars?.$LWDISPLAY ?? result?.vars?.LWDISPLAY); + // Sheet orientation: the active model-space viewport's VIEWTWIST rotates the + // view so a drawing stored tilted in WCS (rotated survey sheets — header UCS + // stays identity) displays with its title border upright. Applied as a + // camera-up rotation in _fit (geometry untouched → picking stays exact). + this._viewTwist = this._readViewTwist(result); + // Geometric pick index — the ACTUAL rendered geometry, not a bounds proxy. + // _pickSegs: flat [ax,ay,bx,by,…] of every line segment (lines, arcs, circles, + // polylines, block wires, dimension leaders/arrows) with a parallel owner-meta + // index. _pickFills: filled regions (hatch loops, SOLID quads, text quads) + // tested by even-odd point-in-polygon. _onClick raycasts these directly. + this._pickSegs = []; + this._pickSegMeta = []; + this._pickFills = []; + this._pickCurIdx = -1; + + const entities = result?.entities || []; + const lineVerts = []; + const lineColors = []; + // Layer id per emitted segment (2 verts). Drives the index rebuild that + // hides/shows layers without touching positions or colors. + const lineSegLayer = []; + const layerIds = new Map(); + const layerNames = []; + const layerIdOf = (name) => { + let id = layerIds.get(name); + if (id === undefined) { id = layerNames.length; layerNames.push(name); layerIds.set(name, id); } + return id; + }; + // Segments that can't join the one merged hairline batch go into buckets + // keyed by dash|gap size and lineweight; each becomes its own mesh at + // assembly (LineDashedMaterial for thin dashes, LineSegments2 for anything + // with a real lineweight). Solid hairlines stay in lineVerts/lineColors. + const segBuckets = new Map(); + let curDash = null; // {key,dash,gap} for the entity currently being emitted + let entIdx = -1; // _entityMeta index of that entity (-1 before the loop) + let curSpans = []; // buckets it has written to so far + const box = new THREE.Box3(); + const _tmp = new THREE.Vector3(); + const pendingTexts = []; + + // Build set of user block definition handles. + // Block-def entities have ownerHandle = their block's handle. They're rendered + // via INSERT (_insertEntities) and must be SKIPPED in the main loop to avoid + // expanding the bounding box with block-local coordinates (near 0,0), which + // would make the real drawing appear as a tiny speck on zoom-extents. + const blockDefHandles = new Set(); + // Also track model/paper space handles to verify correctness + const spaceHandles = new Set(); + // Block header handle (hex) → basePoint, for nested INSERT transform composition. + const blockBaseByHex = new Map(); + const blockNameByHex = new Map(); + for (const b of (result?.tables?.blocks ?? [])) { + const name = (b.name ?? '').toLowerCase().replace(/\*/g, '').trim(); + const h = typeof b.handle === 'object' ? b.handle?.value?.toString(16) : b.handle?.toString(16); + if (!h) continue; + if (b.basePoint) blockBaseByHex.set(h, b.basePoint); + blockNameByHex.set(h, (b.name ?? '').toLowerCase()); + // System space blocks: any of these names → entity owner, render directly + if (name === 'model_space' || name === 'paper_space' || + name === 'ms' || name === 'ps' || + name.startsWith('model') || name.startsWith('paper') || + name === '') { + spaceHandles.add(h); + } else { + blockDefHandles.add(h); + } + } + // Safety: if spaceHandles is empty we couldn't identify model space → + // disable the filter entirely to avoid hiding everything. + const useBlockFilter = spaceHandles.size > 0; + this._blockCount = blockDefHandles.size; + + // If active space is set but no longer present (new file), clear it. + if (this._activeSpaceHex && spaceHandles.size && !spaceHandles.has(this._activeSpaceHex)) { + this._activeSpaceHex = null; + } + // Owner closure for ATTRIB under INSERT in the active space, etc. + const allowedOwners = this._activeSpaceHex + ? buildAllowedOwnerHexes(entities, this._activeSpaceHex) + : null; + + // Pre-group all entities by ownerHandle hex string for O(1) lookup. + // Without this, every INSERT/DIMENSION does entities.filter() = O(n) per call = O(n²) total. + const entsByOwner = new Map(); + for (const ent of entities) { + const key = ent.ownerHandle?.value?.toString(16); + if (key != null) { + if (!entsByOwner.has(key)) entsByOwner.set(key, []); + entsByOwner.get(key).push(ent); + } + } + + // AABB of whatever is actually drawn (current Model/Layout + layers). + // Samples feed percentile trim so Fit matches CAD extents (ignore 1–2 junk verts). + const fitSamples = []; + let fitSampleN = 0; + // Per-layer AABB + fit samples: Fit must frame only the VISIBLE layers, and + // toggling no longer re-runs load(), so the split has to happen at build. + let curLayerBox = null; + let curLayerSamples = null; + const setCurLayer = (name) => { + const key = name || '0'; + if (key === this._curLayer) return; + this._curLayer = key; + this._curLayerId = layerIdOf(key); + curLayerBox = this._layerBoxes.get(key); + if (!curLayerBox) this._layerBoxes.set(key, curLayerBox = new THREE.Box3()); + curLayerSamples = this._layerSamples.get(key); + if (!curLayerSamples) this._layerSamples.set(key, curLayerSamples = []); + }; + const expand = (x, y, z = 0) => { + if (!Number.isFinite(x) || !Number.isFinite(y)) return; + box.expandByPoint(_tmp.set(x, y, z)); + if (curLayerBox) curLayerBox.expandByPoint(_tmp); + fitSampleN++; + if ((fitSampleN & 7) === 0 && fitSamples.length < 100000) { + fitSamples.push(x, y); + if (curLayerSamples) curLayerSamples.push(x, y); + } + }; + const pushSeg = (ax, ay, bx, by, z, color) => { + const r = ((color >> 16) & 0xFF) / 255; + const g = ((color >> 8) & 0xFF) / 255; + const b = (color & 0xFF) / 255; + const lwPx = this._curLwPx; + if (curDash || lwPx > 0) { + const key = `${curDash ? curDash.key : ''}#${lwPx}`; + let bk = segBuckets.get(key); + if (!bk) { + // lwMm is kept so setLineweightScale() can retune the material width + // without rebuilding the scene (the hairline cut-off is in mm, so + // bucket membership does not depend on the scale). + bk = { dash: curDash?.dash ?? 0, gap: curDash?.gap ?? 0, lwPx, lwMm: this._curLwMm || 0, + verts: [], colors: [], segLayer: [], entIdx: -2 }; + segBuckets.set(key, bk); + } + // Selection highlight recolors an entity's own vertices. Its colors no + // longer live in one array, so remember where this entity's run starts + // in every bucket it touches (see meta.spans). + if (bk.entIdx !== entIdx) { bk.entIdx = entIdx; bk.entStart = bk.colors.length; curSpans.push(bk); } + bk.verts.push(ax, ay, z, bx, by, z); + bk.colors.push(r, g, b, r, g, b); + bk.segLayer.push(this._curLayerId); + } else { + lineVerts.push(ax, ay, z, bx, by, z); + lineColors.push(r, g, b, r, g, b); + lineSegLayer.push(this._curLayerId); + } + if (this._pickCurIdx >= 0) { this._pickSegs.push(ax, ay, bx, by); this._pickSegMeta.push(this._pickCurIdx); } + expand(ax, ay, z); expand(bx, by, z); + }; + + // ── Layout draw order ──────────────────────────────────────────────── + // AutoCAD: model shown through VIEWPORT sits *under* paper-space entities + // (title block, notes, borders). We used to append viewport geometry after + // the paper pass → VP content painted on top. Fix: emit VP model first + // (z slightly behind) so paper entities and paper text overdraw it. + // + // DWG draw-order: acadrust has SORTENTSTABLE (ACAD_SORTENTS) + SORTENTS + // header flag, but dwg-wasm parseResult does not export them yet — so we + // apply the standard Layout stacking: viewport content below paper. + const VIEWPORT_Z = -1; // camera looks from +Z; lower Z is behind + const spacesList = listCadSpaces(result); + const activeSp = spacesList.find((s) => s.handleHex === this._activeSpaceHex); + if (activeSp?.kind === 'paper') { + const modelSp = spacesList.find((s) => s.kind === 'model'); + if (modelSp) { + const modelAllowed = buildAllowedOwnerHexes(entities, modelSp.handleHex); + const viewports = entities.filter((e) => { + if ((e.type || e.typeName || '').toUpperCase() !== 'VIEWPORT') return false; + const oh = e.ownerHandle?.value?.toString(16); + return oh === this._activeSpaceHex && this._isModelViewport(e); + }); + const vpPush = (ax, ay, bx, by, _z, color) => { + pushSeg(ax, ay, bx, by, VIEWPORT_Z, color); + }; + const vpExpand = (x, y, _z = 0) => expand(x, y, VIEWPORT_Z); + for (const vp of viewports) { + this._emitModelThroughViewport(vp, { + entities, + modelAllowed, + blockDefHandles, + entsByOwner, + blockBaseByHex, + blockNameByHex, + pushSeg: vpPush, + expand: vpExpand, + pendingTexts, + setCurLayer, + box, + _tmp, + fillZ: VIEWPORT_Z, // fills ride the same underlay plane as the wires + underlay: true, // tag pending texts drawn under paper labels + }); + } + } + } + + for (const e of entities) { + // Skip block-definition entities — they live at block-local coords and are + // rendered at correct world positions via INSERT / _insertEntities. + // Only apply filter when we've successfully identified model/paper space handles. + const ownerHex = e.ownerHandle?.value?.toString(16); + if (useBlockFilter && ownerHex && blockDefHandles.has(ownerHex)) continue; + // Model/Paper filter: only entities belonging to the active space (and + // their INSERT children such as ATTRIB). Other space geometry is hidden. + if (allowedOwners && ownerHex && !allowedOwners.has(ownerHex)) continue; + // Hidden layers are NOT skipped here: geometry for every layer is built + // once and switched on/off by _applyLayerVisibility, so a toggle costs an + // index rebuild instead of a full reload. + setCurLayer(this._layerNameOf(e)); + const d = e.data || e; + const type = (e.type || e.typeName || '').toUpperCase(); + const color = this._entityColor(e); + const complexLt = this._resolveComplexLinetype(e); + if (complexLt) curDash = null; + else curDash = this._resolveDash(e); + this._curLwPx = this._lwPx(e); + const meta = { entity: e, type, bounds: null, layer: this._curLayer, colStart: lineColors.length }; + entIdx = this._entityMeta.length; + curSpans = []; + this._entityMeta.push(meta); + this._pickCurIdx = this._entityMeta.length - 1; // owner for pick geometry emitted below + const textStart = pendingTexts.length; + + try { + switch (type) { + + // ── Basic geometry ──────────────────────────────────────────────── + case 'LINE': + if (d.start && d.end) { + if (complexLt) { + this._drawComplexPath([d.start, d.end], false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + } else { + pushSeg(d.start.x, d.start.y, d.end.x, d.end.y, d.start.z || 0, color); + } + meta.bounds = { type:'line', x1:d.start.x, y1:d.start.y, x2:d.end.x, y2:d.end.y }; + } + break; + + case 'CIRCLE': + if (d.center && d.radius != null) { + if (complexLt) { + const pts = this._sampleArcPoints(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0); + this._drawComplexPath(pts, true, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + } else { + this._arcSegs(d.center, d.radius, 0, Math.PI*2, d.center.z||0, color, pushSeg); + } + meta.bounds = { type:'circle', cx:d.center.x, cy:d.center.y, r:d.radius }; + } + break; + + case 'ARC': + if (d.center && d.radius != null) { + if (complexLt) { + const pts = this._sampleArcPoints(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0); + this._drawComplexPath(pts, false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + } else { + this._arcSegs(d.center, d.radius, d.startAngle??0, d.endAngle??Math.PI*2, d.center.z||0, color, pushSeg); + } + meta.bounds = { type:'circle', cx:d.center.x, cy:d.center.y, r:d.radius }; + } + break; + + case 'LWPOLYLINE': + if (d.points?.length) { + const hasBulge = d.bulges?.some(b => Math.abs(b) >= 1e-6); + const closed = !!(d.closed || (d.flags & 1)); + const nSeg = closed ? d.points.length : d.points.length - 1; + if (this._polyHasWidth(e, nSeg, 1)) { + this._widePolyMesh(d.points, d.bulges, closed, e, color, 1, expand); + } else if (complexLt) { + const pts = this._samplePolylinePoints(d.points, d.bulges, closed, d.elevation || 0); + this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + pts.forEach(p => expand(p.x, p.y, p.z)); + } else if (hasBulge) { + this._bulgePolySegs(d.points, d.bulges, closed, color, pushSeg, expand); + } else { + this._polylineSegs(d.points, closed, d.elevation||0, color, pushSeg); + d.points.forEach(p => expand(p.x, p.y, d.elevation||0)); + } + // Note: uncovered-poly fill is only applied in _insertEntities (block + // glyphs). Model-space polylines can be huge parcels — never fill those. + meta.bounds = { type:'point', cx:d.points[0].x, cy:d.points[0].y }; + } + break; + + case 'POLYLINE': + if (complexLt && (d.vertices || d.points)) { + const closed = !!(d.closed || (d.flags & 1)); + const pts = d.vertices || d.points; + this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + pts.forEach(p => expand(p.x, p.y, p.z || 0)); + } else { + this._polylineSegs(d.vertices||d.points, d.closed||(d.flags&1), 0, color, pushSeg); + } + break; + + case 'POLYLINE_2D': { + // Old-style 2D polyline: vertices are separate VERTEX_2D entities + // owned by this polyline (point/bulge at top level, not under .data). + const kids = (entsByOwner.get(e.handle?.value?.toString(16)) ?? []) + .filter(v => (v.type||'').toUpperCase().startsWith('VERTEX') && v.point); + if (kids.length >= 2) { + const pts = kids.map(v => v.point); + const bulges = kids.map(v => v.bulge || 0); + const closed = (d.flags & 1) === 1; + const elev = d.elevation || 0; + if (complexLt) { + const sampled = this._samplePolylinePoints(pts, bulges, closed, elev); + this._drawComplexPath(sampled, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + sampled.forEach(p => expand(p.x, p.y, p.z)); + } else if (bulges.some(b => Math.abs(b) >= 1e-6)) { + this._bulgePolySegs(pts, bulges, closed, color, pushSeg, expand); + } else { + this._polylineSegs(pts, closed, elev, color, pushSeg); + pts.forEach(p => expand(p.x, p.y, elev)); + } + meta.bounds = { type:'point', cx: pts[0].x, cy: pts[0].y }; + } + break; + } + + case 'POINT': { + const p = d.position || d; + if (typeof p.x === 'number') { + const s = 1; + pushSeg(p.x-s, p.y, p.x+s, p.y, p.z||0, color); + pushSeg(p.x, p.y-s, p.x, p.y+s, p.z||0, color); + meta.bounds = { type:'point', cx:p.x, cy:p.y }; + } + break; + } + + case 'ELLIPSE': + if (d.center) { + if (complexLt) { + const pts = this._sampleEllipsePoints(d); + this._drawComplexPath(pts, false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + pts.forEach(p => expand(p.x, p.y, p.z || 0)); + } else { + this._ellipseSegs(d, color, pushSeg); + } + meta.bounds = { type:'point', cx:d.center.x, cy:d.center.y }; + } + break; + + case 'SPLINE': { + const cps = d.controlPoints ?? d.fitPoints; + if (cps?.length >= 2) { + const pts = cps.length >= 3 + ? (() => { + const segs = cps.length * 10; + const result = []; + for (let i = 0; i <= segs; i++) { + const t = i / segs; + result.push(this._catmullRom(cps, t)); + } + return result; + })() + : cps; + if (complexLt) { + this._drawComplexPath(pts, !!d.closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + pts.forEach(p => expand(p.x, p.y, p.z || 0)); + } else { + this._polylineSegs(pts, false, 0, color, pushSeg); + pts.forEach(p => expand(p.x, p.y, p.z || 0)); + } + meta.bounds = { type:'point', cx:pts[0].x, cy:pts[0].y }; + } + break; + } + + case 'ATTRIB': + case 'ATTDEF': { + if ((d.flags ?? 0) & 1) break; // invisible attribute + const raw = e.text ?? d.text ?? d.textValue ?? d.defaultValue ?? ''; + const text = stripMText(raw); + const height = e.textHeight ?? e.height ?? d.textHeight ?? d.height ?? 2.5; + const rotation = e.rotationAngle ?? d.rotationAngle ?? 0; + const alignH = e.horizAlignment ?? d.horizAlignment ?? 0; + const alignV = e.vertAlignment ?? d.vertAlignment ?? 0; + // When aligned (non-default H/V), the alignmentPt is the true anchor; + // insertionPt is the "first point" and differs for centered/right text. + const useAlignPt = alignH !== 0 || alignV !== 0; + const pos = useAlignPt + ? (e.alignmentPt ?? d.alignmentPt ?? e.insertionPt ?? d.insertionPt ?? d.insertionPoint ?? { x:0, y:0 }) + : (e.insertionPt ?? d.insertionPt ?? d.insertionPoint ?? { x:0, y:0 }); + if (text) { + // Force vertical center for ATTRIB — attribute values (e.g. node tags) appear centered + pendingTexts.push({ text, pos, height, rotation, color, alignH, alignV: 2 }); + meta.bounds = { type:'point', cx:pos.x, cy:pos.y }; + this._addTextPick(text, pos, height, alignH, 2); + expand(pos.x, pos.y, 0); + } + break; + } + + case 'XLINE': + case 'RAY': { + // Infinite/semi-infinite lines — render as very long line segment + const bp = d.basePoint ?? d.point ?? d.startPoint; + const dir = d.direction ?? d.unitDir ?? d.vector; + if (bp && dir) { + const len = 1e6; + const ex = bp.x + dir.x * len, ey = bp.y + dir.y * len; + if (type === 'XLINE') { + pushSeg(bp.x - dir.x * len, bp.y - dir.y * len, ex, ey, 0, color); + } else { + pushSeg(bp.x, bp.y, ex, ey, 0, color); + } + meta.bounds = { type:'point', cx:bp.x, cy:bp.y }; + expand(bp.x, bp.y); + } + break; + } + + case 'SOLID': { + const corners = [d.corner1||d.pt1||d.point1, d.corner2||d.pt2||d.point2, + d.corner3||d.pt3||d.point3, d.corner4||d.pt4||d.point4].filter(Boolean); + if (corners.length >= 3) { + this._solidMesh(corners, color); + meta.bounds = { type:'point', cx:corners[0].x, cy:corners[0].y }; + } + break; + } + + // ── MESH (AcDbSubDMesh) ─────────────────────────────────────────── + // Drawn as its face wireframe, matching AutoCAD's 2D wireframe view. + case 'MESH': { + if (d.vertices?.length >= 3) { + this._meshSegs(d, color, pushSeg); + meta.bounds = { type:'point', cx:d.vertices[0], cy:d.vertices[1] }; + } + break; + } + + // ── Text ────────────────────────────────────────────────────────── + case 'TEXT': + case 'MTEXT': { + const raw = e.text ?? d.text ?? ''; + const text = stripMText(raw); + const height = e.textHeight ?? e.height ?? d.textHeight ?? d.height ?? 2.5; + let rotation = 0, alignH = 0, alignV = 0, pos; + if (type === 'TEXT') { + rotation = e.rotationAngle ?? d.rotationAngle ?? 0; + alignH = e.horizAlignment ?? d.horizAlignment ?? 0; + alignV = e.vertAlignment ?? d.vertAlignment ?? 0; + // DXF group 72 = 4 ("Middle") centers the text both horizontally AND + // vertically on the alignment point — group 73 (vertical) is ignored. + // Without this, vA=0 (baseline) placed such text half a line too high + // (titleblock labels 노선이정/설계사/… floated up). + if (alignH === 4) alignV = 2; + // DWG: when alignment is non-default, alignmentPt is the actual anchor + // (insertionPt is the "first point" which may differ from anchor) + const useAlignPt = alignH !== 0 || alignV !== 0; + pos = useAlignPt + ? (e.alignmentPt ?? d.alignmentPt ?? e.insertionPt ?? d.insertionPt ?? { x:0, y:0 }) + : (e.insertionPt ?? d.insertionPt ?? d.insertionPoint ?? { x:0, y:0 }); + } else { + const xd = e.xAxisDir ?? d.xAxisDir; + if (xd) rotation = Math.atan2(xd.y, xd.x); + // MTEXT: attachmentPoint 1-3=top, 4-6=mid, 7-9=bottom + // (parsers emit the field as `attachment`; keep both names) + const ap = e.attachment ?? d.attachment ?? e.attachmentPoint ?? d.attachmentPoint ?? 5; + alignH = ([1,4,7].includes(ap) ? 0 : [3,6,9].includes(ap) ? 2 : 1); + alignV = ([1,2,3].includes(ap) ? 3 : [7,8,9].includes(ap) ? 1 : 2); + pos = e.insertionPt ?? d.insertionPt ?? d.insertionPoint ?? { x:0, y:0 }; + } + if (text) { + const entry = { text, pos, height, rotation, color, alignH, alignV }; + if (type === 'MTEXT') { + entry.linespacingFactor = e.linespacingFactor ?? d.linespacingFactor ?? 1; + entry.rectWidth = e.rectWidth ?? d.rectWidth ?? 0; + entry.rectHeight = e.rectHeight ?? d.rectHeight ?? 0; + // Background mask (DXF 90/45/63): bit0=use fill color, bit1=window color, bit4=frame + entry.bgFillFlags = e.bgFillFlags ?? d.bgFillFlags ?? 0; + entry.bgScale = e.bgScale ?? d.bgScale ?? 1.5; + entry.bgColorIndex = e.bgColorIndex ?? d.bgColorIndex; + entry.bgColorRgb = e.bgColorRgb ?? d.bgColorRgb; + } + pendingTexts.push(entry); + meta.bounds = { type:'point', cx:pos.x, cy:pos.y }; + this._addTextPick(text, pos, height, alignH, alignV); + expand(pos.x, pos.y, 0); + } + break; + } + + // ── INSERT (block reference) ────────────────────────────────────── + case 'INSERT': { + const ip = e.insertionPt ?? d.insertionPt ?? d.insertionPoint; + const sx = d.scaleX ?? d.scale?.x ?? e.scale?.x ?? 1; + const sy = d.scaleY ?? d.scale?.y ?? e.scale?.y ?? 1; + const rot = d.rotation ?? e.rotation ?? 0; + const bhVal = e.blockHeaderHandle?.value ?? d.blockHeaderHandle?.value; + + if (bhVal != null) { + // Block definition entities sit in the flat entities array, + // identified by ownerHandle.value === block header handle + const bHandleHex = bhVal.toString(16); + const bEnts = entsByOwner.get(bHandleHex) ?? []; + if (bEnts.length > 0) { + const bh = result?.tables?.blocks?.find(b => { + const bh2 = typeof b.handle === 'object' ? b.handle?.value : b.handle; + return bh2 === bhVal; + }); + this._insertEntities( + bEnts, + { insertionPoint: ip ?? {x:0,y:0,z:0}, xScale:sx, yScale:sy, rotation:rot, basePoint: bh?.basePoint }, + color, pushSeg, pendingTexts, + { entsByOwner, blockBase: blockBaseByHex } + ); + } + } + if (ip) { meta.bounds = { type:'point', cx:ip.x, cy:ip.y }; expand(ip.x, ip.y, 0); } + break; + } + + // ── HATCH ───────────────────────────────────────────────────────── + case 'HATCH': { + const paths = d.paths ?? e.paths; + const solidFill = d.solidFill ?? e.solidFill ?? false; + if (!paths?.length) break; + + let firstPt = null; + for (const path of paths) { + if (!path.points?.length || path.points.length < 2) continue; + if (!firstPt) firstPt = path.points[0]; + const hasBulge = path.bulges?.some(b => Math.abs(b) >= 1e-6); + if (hasBulge) { + this._bulgePolySegs(path.points, path.bulges, true, color, pushSeg, expand); + } else { + this._polylineSegs(path.points, true, 0, color, pushSeg); + path.points.forEach(p => expand(p.x, p.y, 0)); + } + } + if (solidFill) this._hatchFill(paths, color); + if (firstPt) meta.bounds = { type:'point', cx:firstPt.x, cy:firstPt.y }; + break; + } + + // ── DIMENSION (all subtypes) ─────────────────────────────────────── + case 'DIMENSION': + case 'DIMENSION_LINEAR': + case 'DIMENSION_ALIGNED': + case 'DIMENSION_RADIUS': + case 'DIMENSION_DIAMETER': + case 'DIMENSION_ANG_3PT': + case 'DIMENSION_ANG_2LN': + case 'DIMENSION_ORDINATE': { + const bhVal = e.blockHeaderHandle?.value ?? d.blockHeaderHandle?.value; + let dimEnts = []; + if (bhVal != null) { + const hex = bhVal.toString(16); + dimEnts = entsByOwner.get(hex) ?? []; + } + + if (dimEnts.length > 0) { + // Render pre-built dimension block geometry + for (const child of dimEnts) { + const ct = (child.type || child.typeName || '').toUpperCase(); + const cd = child.data || child; + const cc = this._entityColor(child) || color; + if (ct === 'LINE' && cd.start && cd.end) { + pushSeg(cd.start.x, cd.start.y, cd.end.x, cd.end.y, 0, cc); + expand(cd.start.x, cd.start.y); expand(cd.end.x, cd.end.y); + } else if (ct === 'SOLID') { + const crs = [cd.corner1||cd.pt1, cd.corner2||cd.pt2, + cd.corner3||cd.pt3, cd.corner4||cd.pt4].filter(Boolean); + if (crs.length >= 3) { this._solidMesh(crs, cc); crs.forEach(c => expand(c.x, c.y)); } + } else if (ct === 'ARC' && cd.center && cd.radius != null) { + this._arcSegs(cd.center, cd.radius, cd.startAngle??0, cd.endAngle??Math.PI*2, 0, cc, pushSeg); + } else if ((ct === 'TEXT' || ct === 'MTEXT')) { + const raw = child.text ?? cd.text ?? cd.textValue ?? ''; + const text = stripMText(raw); + const pos = child.insertionPt ?? cd.insertionPt ?? cd.insertionPoint ?? {x:0,y:0}; + const ht = cd.textHeight ?? cd.height ?? child.textHeight ?? 2.5; + if (text) { pendingTexts.push({ text, pos, height:ht, rotation: cd.rotationAngle??0, color:cc, alignH:1, alignV:2 }); expand(pos.x, pos.y); } + } else if (ct === 'INSERT') { + // Arrowhead block INSERT within the dimension block: render the real + // arrow geometry; fall back to a named default for geometry-less + // system blocks (_ClosedFilled / _Dot / _Oblique / _Open / …). + const cip = child.insertionPt ?? cd.insertionPt ?? cd.insertionPoint ?? {x:0,y:0}; + const crot = cd.rotation ?? child.rotation ?? 0; + const csx = cd.scaleX ?? cd.scale?.x ?? child.scale?.x ?? 1; + const csy = cd.scaleY ?? cd.scale?.y ?? child.scale?.y ?? csx; + const abh = child.blockHeaderHandle?.value ?? cd.blockHeaderHandle?.value; + const aHex = abh?.toString(16); + const aname = blockNameByHex.get(aHex) ?? ''; + const arrowEnts = aHex ? (entsByOwner.get(aHex) ?? []) : []; + if (aname.includes('dot')) { + // Dot arrowhead → filled disc (radius from the block's geometry × scale) + this._dotMesh(cip, this._arrowDotRadius(arrowEnts) * Math.abs(csx) || Math.abs(csx) * 0.5, cc); + } else if (arrowEnts.length) { + this._insertEntities( + arrowEnts, + { insertionPoint: cip, xScale: csx, yScale: csy, rotation: crot, basePoint: blockBaseByHex.get(aHex) }, + cc, pushSeg, pendingTexts, + { entsByOwner, blockBase: blockBaseByHex } + ); + } else { + this._defaultArrow(aname, cip, crot, Math.abs(csx) || 1, cc, pushSeg); + } + expand(cip.x, cip.y); + } + } + const fl = dimEnts.find(c => (c.type||c.typeName||'').toUpperCase() === 'LINE'); + const fd = fl?.data || fl; + if (fd?.start) meta.bounds = { type:'point', cx:fd.start.x, cy:fd.start.y }; + } else { + // Fallback: reconstruct from entity properties + this._renderDimFallback(e, d, type, color, pushSeg, pendingTexts, expand); + const pt10 = e.pt10 ?? d.pt10; + if (pt10) { meta.bounds = { type:'point', cx:pt10.x, cy:pt10.y }; expand(pt10.x, pt10.y); } + } + break; + } + + case 'LEADER': { + const pts = d.points ?? e.points; + if (pts?.length >= 2) { + this._polylineSegs(pts, false, 0, color, pushSeg); + pts.forEach(p => expand(p.x, p.y, p.z || 0)); + if (d.arrowheadOn ?? e.arrowheadOn ?? true) { + const p0 = pts[0], p1 = pts[1]; + const rot = Math.atan2(p1.y - p0.y, p1.x - p0.x); + const arrowSz = d.arrowSize ?? e.arrowSize ?? 2.5; + this._arrowMesh(p0, rot, arrowSz, color); + } + meta.bounds = { type:'point', cx:pts[0].x, cy:pts[0].y }; + } + break; + } + + // ── OLE2FRAME (Embedded OLE Object) ────────────────────────────── + case 'OLE2FRAME': { + const ul = e.upperLeft ?? d.upperLeft ?? { x: 0, y: 0, z: 0 }; + const lr = e.lowerRight ?? d.lowerRight ?? { x: 0, y: 0, z: 0 }; + const appName = e.sourceApp ?? d.sourceApp ?? e.source_application ?? ''; + const imageDataUrl = e.imageDataUrl ?? d.imageDataUrl; + + // Use actual entity bounds from the WASM parser (upper_left / lower_right) + let minX = Math.min(ul.x, lr.x); + let maxX = Math.max(ul.x, lr.x); + let minY = Math.min(ul.y, lr.y); + let maxY = Math.max(ul.y, lr.y); + let width = maxX - minX; + let height = maxY - minY; + const z = ul.z || 0; + + // Always draw border rectangle + pushSeg(minX, minY, maxX, minY, z, color); + pushSeg(maxX, minY, maxX, maxY, z, color); + pushSeg(maxX, maxY, minX, maxY, z, color); + pushSeg(minX, maxY, minX, minY, z, color); + + // If embedded image texture (BMP/DIB) is present, render it as a 3D Plane Mesh! + if (imageDataUrl) { + try { + // Pure-JS BMP decoder — works in ALL browsers (Brave/Firefox don't support data:image/bmp in ) + const decodeBmpDataUrl = (dataUrl) => { + const b64 = dataUrl.split(',')[1]; + const bin = atob(b64); + const buf = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i); + + const dv = new DataView(buf.buffer); + // BITMAPFILEHEADER (14 bytes) + const sig = String.fromCharCode(buf[0], buf[1]); + if (sig !== 'BM') throw new Error('Not a BMP file'); + const pixelOffset = dv.getUint32(10, true); + // BITMAPINFOHEADER (40 bytes, starts at offset 14) + const bmpWidth = dv.getInt32(18, true); + const bmpHeight = dv.getInt32(22, true); // positive = bottom-up + const bitCount = dv.getUint16(28, true); + const absH = Math.abs(bmpHeight); + const flipY = bmpHeight > 0; // bottom-up storage + + const cvs = document.createElement('canvas'); + cvs.width = bmpWidth; + cvs.height = absH; + const ctx2 = cvs.getContext('2d'); + const imgData = ctx2.createImageData(bmpWidth, absH); + const px = imgData.data; + + if (bitCount === 32) { + const bpr = bmpWidth * 4; + for (let y = 0; y < absH; y++) { + const srcRow = flipY ? (absH - 1 - y) : y; + const srcOff = pixelOffset + srcRow * bpr; + const dstOff = y * bmpWidth * 4; + for (let x = 0; x < bmpWidth; x++) { + const s = srcOff + x * 4; + const d = dstOff + x * 4; + px[d] = buf[s + 2]; // R (BMP stores BGR) + px[d + 1] = buf[s + 1]; // G + px[d + 2] = buf[s]; // B + px[d + 3] = 255; // A — GDI 32bpp has zeroed alpha padding + } + } + } else if (bitCount === 24) { + const stride = Math.ceil(bmpWidth * 3 / 4) * 4; // row padded to 4 bytes + for (let y = 0; y < absH; y++) { + const srcRow = flipY ? (absH - 1 - y) : y; + const srcOff = pixelOffset + srcRow * stride; + const dstOff = y * bmpWidth * 4; + for (let x = 0; x < bmpWidth; x++) { + const s = srcOff + x * 3; + const d = dstOff + x * 4; + px[d] = buf[s + 2]; + px[d + 1] = buf[s + 1]; + px[d + 2] = buf[s]; + px[d + 3] = 255; + } + } + } + ctx2.putImageData(imgData, 0, 0); + return cvs; + }; + + const bmpCanvas = decodeBmpDataUrl(imageDataUrl); + const texture = new THREE.CanvasTexture(bmpCanvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.needsUpdate = true; + const planeGeom = new THREE.PlaneGeometry(width, Math.abs(height)); + const planeMat = new THREE.MeshBasicMaterial({ + map: texture, + side: THREE.DoubleSide, + transparent: false, + }); + const mesh = new THREE.Mesh(planeGeom, planeMat); + mesh.position.set(minX + width / 2, minY + Math.abs(height) / 2, z + 10); + this._addObj(mesh); + this._renderer.render(this._scene, this._camera); + } catch (err) { + console.error('Failed rendering CanvasTexture for OLE image:', err); + } + } else { + // Draw diagonal cross + overlay label fallback + pushSeg(minX, minY, maxX, maxY, z, color); + pushSeg(minX, maxY, maxX, minY, z, color); + + const hSpan = Math.abs(height); + const lblHeight = Math.max(Math.min(hSpan * 0.05, 300), 40); + const labelText = appName ? `[OLE: ${appName}]` : '[OLE2Frame]'; + const labelPos = { x: (minX + maxX) / 2, y: (minY + maxY) / 2, z }; + pendingTexts.push({ + text: labelText, + pos: labelPos, + height: lblHeight, + rotation: 0, + color, + alignH: 1, + alignV: 2, + }); + } + + // Expand scene bounds & set meta bounds for picking/fitting + expand(minX, minY, z); + expand(maxX, maxY, z); + meta.bounds = { type: 'point', cx: (minX + maxX) / 2, cy: (minY + maxY) / 2 }; + break; + } + + // Paper-space VIEWPORT: drawn later as a model-space projection window. + case 'VIEWPORT': + break; + + default: break; + } + } catch { /* skip malformed entity */ } + meta.colEnd = lineColors.length; + if (curSpans.length) { + meta.spans = curSpans.map((bk) => ({ bk, start: bk.entStart, end: bk.colors.length })); + curSpans = []; + } + // Deferred texts are flushed after the loop (async) — stamp the owning + // layer now so _drawTexts can batch per layer. + for (let ti = textStart; ti < pendingTexts.length; ti++) { + if (pendingTexts[ti].layer === undefined) pendingTexts[ti].layer = this._curLayer; + } + } + + // ── Render deferred text (underlay / paper stack) ───────────────────── + // Viewport-sourced labels were pushed first (underlay:true); paper labels + // follow. Draw underlay first so paper text meshes land later = on top. + if (pendingTexts.length) { + // Historical floor: diag×0.0002 made paper-sheet labels readable at + // zoom-extents. On survey Model Space (diag ~1e6 m) that floor is + // ~200–300 drawing units while native TEXT height is ~0.3–3.5 → every + // label becomes a giant billboard over the contours (MOT/FLOW etc.). + // Cap the floor at the median native height so we never upscale past CAD. + const sz = box.isEmpty() ? new THREE.Vector3(1, 1, 0) : box.getSize(new THREE.Vector3()); + const diagSize = Math.hypot(Math.max(sz.x, 1), Math.max(sz.y, 1)); + const heights = []; + for (const t of pendingTexts) { + const h = t.height; + if (h > 0 && Number.isFinite(h)) heights.push(h); + } + heights.sort((a, b) => a - b); + const medianH = heights.length ? heights[heights.length >> 1] : 2.5; + const minTextH = Math.min(diagSize * 0.0002, medianH); + // Cap sprite count to avoid GPU/heap OOM on large drawings. With the + // per-(text,color) texture cache in _textSprite, memory scales with + // UNIQUE strings, so the cap is a sprite-object guard, not a raster one + // (3000 previously culled half the texts on table sheets, e.g. BLOCK4). + const MAX_TEXTS = 20000; + const underlay = pendingTexts.filter((t) => t.underlay); + const overlay = pendingTexts.filter((t) => !t.underlay); + const ordered = underlay.concat(overlay); + const renderTexts = ordered.length > MAX_TEXTS + ? (console.warn(`텍스트 ${ordered.length}개 → ${MAX_TEXTS}개로 제한`), + // Prefer keeping paper (overlay) labels when capping + overlay.concat(underlay).sort((a, b) => b.height - a.height).slice(0, MAX_TEXTS)) + : ordered; + void this._drawTexts(renderTexts, minTextH); + } + + this._layerNames = layerNames; + this._lineColorAttr = null; this._origColors = null; this._hairRt = null; + + // A weighted bucket only earns its own fat-line mesh while it stays inside + // the segment cap; past that it is folded back into the hairline batch + // (solid) or drawn as a thin dash bucket, which is what the viewer did + // before lineweight existed. + for (const bk of segBuckets.values()) { + bk.fat = bk.lwPx > 0 && bk.verts.length / 6 <= LW_MAX_FAT_SEGS; + if (bk.lwPx > 0 && !bk.fat) { + console.warn(`선가중치 ${bk.lwPx}px 세그먼트 ${bk.verts.length / 6}개 → 헤어라인으로 대체`); + } + if (!bk.fat && bk.dash <= 0) { + bk.mergeOffset = lineColors.length; + for (let i = 0; i < bk.verts.length; i++) lineVerts.push(bk.verts[i]); + for (let i = 0; i < bk.colors.length; i++) lineColors.push(bk.colors[i]); + for (let i = 0; i < bk.segLayer.length; i++) lineSegLayer.push(bk.segLayer[i]); + bk.merged = true; + } + } + + if (lineVerts.length) { + const geom = new THREE.BufferGeometry(); + geom.setAttribute('position', new THREE.Float32BufferAttribute(lineVerts, 3)); + const colorAttr = new THREE.Float32BufferAttribute(lineColors, 3); + geom.setAttribute('color', colorAttr); + const lmesh = new THREE.LineSegments(geom, new THREE.LineBasicMaterial({ vertexColors: true })); + this._registerIndexed(lmesh, lineSegLayer); + this._group.add(lmesh); + this._lineColorAttr = colorAttr; + this._origColors = Float32Array.from(colorAttr.array); + this._hairRt = { attr: colorAttr, orig: this._origColors }; + } + + // One mesh per remaining bucket. + // · lineweight > 0 → LineSegments2: screen-space quads, so the width stays + // constant in pixels while zooming (AutoCAD LWDISPLAY semantics). + // · otherwise → LineSegments + LineDashedMaterial for the dash + // linetypes (HIDDEN / CENTER / …). computeLineDistances() is REQUIRED + // for the pattern to appear; on LineSegments each 2-vertex pair dashes + // independently from its own start (correct for CAD segments). + for (const bk of segBuckets.values()) { + if (!bk.verts.length || bk.merged) continue; + if (bk.fat) { + const fgeom = new LineSegmentsGeometry(); + fgeom.setPositions(bk.verts); + fgeom.setColors(bk.colors); + const fmat = new LineMaterial({ + vertexColors: true, linewidth: bk.lwPx, worldUnits: false, + dashed: bk.dash > 0, dashSize: bk.dash, gapSize: bk.gap, + }); + this._sizeLineMaterial(fmat); + const fline = new LineSegments2(fgeom, fmat); + if (bk.dash > 0) fline.computeLineDistances(); + this._registerFat(fline, bk.segLayer, bk.lwMm); + bk.rt = this._fatBufs[this._fatBufs.length - 1]; + this._group.add(fline); + } else { + const dgeom = new THREE.BufferGeometry(); + dgeom.setAttribute('position', new THREE.Float32BufferAttribute(bk.verts, 3)); + const dcol = new THREE.Float32BufferAttribute(bk.colors, 3); + dgeom.setAttribute('color', dcol); + const dline = new THREE.LineSegments(dgeom, new THREE.LineDashedMaterial({ + vertexColors: true, dashSize: bk.dash, gapSize: bk.gap, + })); + dline.computeLineDistances(); // must run BEFORE setIndex (three skips indexed geometry) + this._registerIndexed(dline, bk.segLayer); + bk.rt = { attr: dcol, orig: Float32Array.from(dcol.array) }; + this._group.add(dline); + } + } + + // Coordinate readout frame (Model UCS vs paper identity). + this._setCoordFrame(result); + + // Visible-content AABB for Fit — trim extreme outliers (matches CAD zoom extents better). + const fitBox = box.isEmpty() ? null : (this._trimmedContentBox(fitSamples, box) || box); + this._fullContentBox = fitBox ? fitBox.clone() : null; + this._contentBox = fitBox ? fitBox.clone() : null; + // The trimmed fit box is flattened to z=0 (percentile trim is X/Y only), so + // keep the drawing's real elevation range for the 2.5D fit. + this._contentZ = box.isEmpty() ? null : { min: box.min.z, max: box.max.z }; + // Geometry for every layer is in the scene now — switch off the hidden ones + // (also refreshes _contentBox to the visible subset). + this._applyLayerVisibility(); + const useBox = this._contentBox; + if (!opts.keepView && useBox && !useBox.isEmpty()) this._fit(useBox); + } finally { + this._isLoading = false; + } + } + + // ── UI integration ───────────────────────────────────────────────────────── + + /** Swap canvas background for theme. dark=true → near-black (Model), false → white (Layout). */ + setTheme(dark) { + const isDarkBool = !!dark; + const changed = (this._isDark !== isDarkBool); + this._isDark = isDarkBool; + this._scene.background = new THREE.Color(this._isDark ? 0x0a0b0d : 0xffffff); + if (this._gridVisible) this._rebuildGrid(); + + if (changed && this._lastResult && !this._isLoading) { + this.load(this._lastResult, { keepView: true, keepTheme: true, spaceHandle: this._activeSpaceHex }); + } + } + + setGrid(visible) { + this._gridVisible = visible; + if (!visible) { + if (this._gridMesh) { this._scene.remove(this._gridMesh); this._gridMesh.geometry.dispose(); this._gridMesh.material.dispose(); this._gridMesh = null; } + } else { + this._rebuildGrid(); + } + } + + _rebuildGrid() { + if (this._gridMesh) { this._scene.remove(this._gridMesh); this._gridMesh.geometry.dispose(); this._gridMesh.material.dispose(); this._gridMesh = null; } + const zoom = this._camera.zoom || 1; + // Centre on the orbit target, not the camera: in 2.5D the camera sits off to + // the side of what it is looking at. + const cx = this._controls?.target?.x ?? this._camera.position.x; + const cy = this._controls?.target?.y ?? this._camera.position.y; + const hw = (this._camera.right - this._camera.left) / zoom / 2; + const hh = (this._camera.top - this._camera.bottom) / zoom / 2; + const ext = Math.max(hw, hh) * 3; + const spacing = this._niceSpacing(Math.max(hw, hh) * 2 / 20); + const [minX, maxX, minY, maxY] = [cx - ext, cx + ext, cy - ext, cy + ext]; + const isDark = this._scene.background.r < 0.5; + const color = isDark ? 0x2b2b2b : 0xbbbbbb; + const verts = []; + for (let x = Math.ceil(minX / spacing) * spacing; x <= maxX; x += spacing) verts.push(x, minY, -0.5, x, maxY, -0.5); + for (let y = Math.ceil(minY / spacing) * spacing; y <= maxY; y += spacing) verts.push(minX, y, -0.5, maxX, y, -0.5); + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3)); + this._gridMesh = new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color })); + this._scene.add(this._gridMesh); + } + + _niceSpacing(raw) { + if (raw <= 0) return 1; + const exp = Math.pow(10, Math.floor(Math.log10(raw))); + const norm = raw / exp; + return (norm < 2 ? 1 : norm < 5 ? 2 : 5) * exp; + } + + /** Zoom % relative to fit (ortho camera zoom). */ + getZoomPercent() { return Math.round((this._camera?.zoom ?? 1) * 100); } + + /** Subscribe to pan/zoom changes (for live zoom readout). */ + onViewChange(cb) { this._controls?.addEventListener('change', cb); } + + /** + * Hide entities on the given layer names (Set). + * O(scene objects + segments) — no re-parse, no re-tessellation, no reload. + * Falls back to a full load() only if the scene predates the layer index. + */ + setHiddenLayers(nameSet) { + // Copy: the Layers panel keeps mutating the Set it handed us. + this._hiddenLayers = new Set(nameSet || []); + if (this._applyLayerVisibility()) return; + if (this._lastResult) this.load(this._lastResult, { keepView: true }); + } + + /** Layer name of an entity ('0' when the file gives none). */ + _layerNameOf(e) { + const lh = e.layerHandle?.value ?? e.layerHandle; + return (lh != null && this._layerNameByHandle?.get(String(lh))) ?? e.layer ?? e.layerName ?? '0'; + } + + /** + * Give a merged LineSegments an index buffer so layers can be masked by + * rewriting indices (positions/colors/lineDistances stay put, which keeps + * draw order and the selection-highlight offsets in meta.colStart valid). + */ + _registerIndexed(mesh, segLayerArr) { + const vcount = mesh.geometry.attributes.position.count; + if (!vcount) return; + const IdxArr = vcount > 65535 ? Uint32Array : Uint16Array; + const index = new IdxArr(vcount); + for (let i = 0; i < vcount; i++) index[i] = i; + const attr = new THREE.BufferAttribute(index, 1); + attr.setUsage(THREE.DynamicDrawUsage); + mesh.geometry.setIndex(attr); + this._indexedBufs.push({ mesh, segLayer: Int32Array.from(segLayerArr), index, attr }); + } + + /** + * Track a LineSegments2 batch for layer masking. Its geometry is instanced + * (one instance per segment, positions/colors interleaved 6 floats each), so + * there is no index to rewrite: hiding a layer compacts the visible segments + * into the front of the instance buffers and drops `instanceCount`. + * The untouched originals are kept so any later mask starts from clean data. + */ + _registerFat(mesh, segLayerArr, lwMm = 0) { + const geo = mesh.geometry; + const posBuf = geo.attributes.instanceStart?.data; + const colBuf = geo.attributes.instanceColorStart?.data; + if (!posBuf) return; + posBuf.setUsage(THREE.DynamicDrawUsage); + colBuf?.setUsage(THREE.DynamicDrawUsage); + this._fatBufs.push({ + mesh, lwMm, + segLayer: Int32Array.from(segLayerArr), + posBuf, colBuf, + posSrc: Float32Array.from(posBuf.array), + colSrc: colBuf ? Float32Array.from(colBuf.array) : null, + }); + } + + /** Fat-line materials need the canvas size in px to convert linewidth. */ + _sizeLineMaterial(mat) { + const w = this._container?.clientWidth || this._renderer?.domElement?.clientWidth || 1; + const h = this._container?.clientHeight || this._renderer?.domElement?.clientHeight || 1; + mat.resolution.set(w, h); + } + + /** + * Lineweight display switch (AutoCAD LWDISPLAY). + * @param {boolean|null} on true/false forces it, null follows the drawing. + */ + setLineweightEnabled(on) { + this._lwForced = (on == null) ? null : !!on; + if (this._lastResult && !this._isLoading) { + this.load(this._lastResult, { keepView: true, keepTheme: true, spaceHandle: this._activeSpaceHex }); + } + } + + /** Whether lineweights are currently being drawn. */ + getLineweightEnabled() { return this._lwDisplay; } + + /** + * Display scale in screen pixels per mm of lineweight — AutoCAD's + * "Adjust Display Scale" slider. Default 20 (1 px per 0.05 mm). + * + * Applied to the live materials, not by rebuilding: the hairline cut-off is + * a millimetre value, so changing the scale moves every weight by the same + * factor without changing which bucket a segment belongs to. Retuning is + * therefore instant even on a 37 MB drawing. + * + * @param {number|null} pxPerMm null restores the default. + */ + setLineweightScale(pxPerMm) { + this._lwScale = (pxPerMm > 0) ? pxPerMm : null; + const scale = this.getLineweightScale(); + for (const buf of this._fatBufs) { + if (!(buf.lwMm > 0)) continue; + buf.mesh.material.linewidth = Math.min(LW_MAX_PX, Math.round(buf.lwMm * scale * 10) / 10); + } + } + + /** Current lineweight display scale (px per mm). */ + getLineweightScale() { return this._lwScale || LW_PX_PER_MM; } + + /** Lineweights present in the current drawing, in mm (ascending). */ + getLineweightClasses() { + return [...new Set(this._fatBufs.map((b) => b.lwMm).filter((m) => m > 0))].sort((a, b) => a - b); + } + + /** Track an entity-owned object so layer toggles can flip its visibility. */ + _addObj(obj, layer) { + // Inside a paper-space VIEWPORT this mesh is still in model coordinates. + // Drop it when it maps outside the viewport frame, exactly as _clipSegToRect + // drops line work. + if (this._fillXf && !this._bakeFillXf(obj)) { + obj.geometry?.dispose(); + if (obj.material) { obj.material.map?.dispose(); obj.material.dispose(); } + return; + } + const key = layer ?? this._curLayer; + if (key != null) { + obj.userData.cadLayer = key; + let arr = this._layerObjs.get(key); + if (!arr) this._layerObjs.set(key, arr = []); + arr.push(obj); + if (this._hiddenLayers.has(key)) obj.visible = false; + } + this._group.add(obj); + } + + /** + * Apply _hiddenLayers to the already-built scene. + * @returns {boolean} false when there is nothing built yet (caller reloads). + */ + _applyLayerVisibility() { + if (!this._layerNames && !this._layerObjs.size) return false; + const hidden = this._hiddenLayers; + + // 1) Whole objects (hatch fills, SOLID quads, arrows, images, text meshes). + for (const [name, objs] of this._layerObjs) { + const vis = !hidden.has(name); + for (const o of objs) o.visible = vis; + } + + // 2) Merged line buffers → rewrite the index to skip hidden segments. + const names = this._layerNames || []; + const hiddenId = new Uint8Array(names.length); + for (let i = 0; i < names.length; i++) if (hidden.has(names[i])) hiddenId[i] = 1; + for (const buf of this._indexedBufs) { + const seg = buf.segLayer, idx = buf.index; + let w = 0; + for (let s = 0; s < seg.length; s++) { + const id = seg[s]; + if (id >= 0 && hiddenId[id]) continue; + const v = s * 2; + idx[w++] = v; idx[w++] = v + 1; + } + buf.attr.needsUpdate = true; + buf.mesh.geometry.setDrawRange(0, w); + } + + // 2b) Fat-line batches → compact the instance buffers, then cap instanceCount. + // slotOf records where each source segment ended up so the selection + // highlight can still find its vertices (-1 = this segment is hidden). + for (const buf of this._fatBufs) { + const seg = buf.segLayer; + if (!buf.slotOf) buf.slotOf = new Int32Array(seg.length); + const slotOf = buf.slotOf; + let w = 0; + for (let s = 0; s < seg.length; s++) { + const id = seg[s]; + if (id >= 0 && hiddenId[id]) { slotOf[s] = -1; continue; } + buf.posBuf.array.set(buf.posSrc.subarray(s * 6, s * 6 + 6), w * 6); + if (buf.colBuf) buf.colBuf.array.set(buf.colSrc.subarray(s * 6, s * 6 + 6), w * 6); + slotOf[s] = w; + w++; + } + buf.posBuf.needsUpdate = true; + if (buf.colBuf) buf.colBuf.needsUpdate = true; + buf.mesh.geometry.instanceCount = w; + buf.mesh.visible = w > 0; + } + + // 3) Picking mask — _onClick skips segments/fills owned by hidden layers. + const metas = this._entityMeta; + const mh = new Uint8Array(metas.length); + if (hidden.size) { + for (let i = 0; i < metas.length; i++) if (hidden.has(metas[i].layer)) mh[i] = 1; + } + this._metaHidden = mh; + if (this._selMeta && hidden.has(this._selMeta.layer)) { + this._highlight(null); + this._onSelectCb?.(null); + } else if (this._selMeta && this._fatBufs.length) { + // Compaction above rewrote the fat instance colors from the pristine + // source — repaint the selection on its new slots. + this._paintMeta(this._selMeta, this._selColor); + } + + // 4) Fit box for the visible subset. + this._contentBox = this._visibleContentBox(); + return true; + } + + /** AABB (outlier-trimmed) of the layers currently switched on. */ + _visibleContentBox() { + const hidden = this._hiddenLayers; + if (!hidden.size) return this._fullContentBox ? this._fullContentBox.clone() : null; + const box = new THREE.Box3(); + const samples = []; + for (const [name, b] of this._layerBoxes) { + if (hidden.has(name) || b.isEmpty()) continue; + box.union(b); + const s = this._layerSamples.get(name); + if (s) for (let i = 0; i < s.length; i++) samples.push(s[i]); + } + if (box.isEmpty()) return null; + return (this._trimmedContentBox(samples, box) || box).clone(); + } + + /** + * Model / Paper (Layout) spaces present in the last loaded result. + * @returns {import('./cadSpaces').CadSpace[]} + */ + getSpaces() { + return listCadSpaces(this._lastResult); + } + + /** Currently active space handle hex, or null if unfiltered. */ + getActiveSpace() { + return this._activeSpaceHex; + } + + /** + * Switch Model ↔ Layout (paper) space and re-render. + * @param {string|number|null} spaceHandle hex string or numeric handle; null clears filter + * @param {{ keepView?: boolean }} [opts] + */ + setSpace(spaceHandle, opts = {}) { + if (spaceHandle == null || spaceHandle === '') { + this._activeSpaceHex = null; + } else if (typeof spaceHandle === 'number') { + this._activeSpaceHex = spaceHandle.toString(16); + } else { + this._activeSpaceHex = String(spaceHandle).toLowerCase(); + } + if (this._lastResult) { + this.load(this._lastResult, { + keepView: !!opts.keepView, + spaceHandle: this._activeSpaceHex, + }); + // load() refreshes coord frame for Model UCS vs paper identity + } + } + + /** + * True if this VIEWPORT is a window into model space (not the overall paper VP). + * R2000 often stores id=0 for all VPs, so we detect the overall paper viewport by + * 1:1 scale + viewTarget≈0 + viewCenter≈paper center. + */ + _isModelViewport(vp) { + if (!vp) return false; + if (vp.status && vp.status.isOn === false) return false; + const id = vp.id ?? 0; + // Classic DXF: id 1 = overall paper-space viewport (not a model window). + if (id === 1) return false; + const H = Number(vp.height) || 0; + const vH = Number(vp.viewHeight) || 0; + if (H <= 0 || vH <= 0) return false; + const scale = H / vH; + const c = vp.center || {}; + const vc = vp.viewCenter || {}; + const vt = vp.viewTarget || {}; + const targetNear0 = Math.hypot(vt.x || 0, vt.y || 0) < 1e-3; + const viewCtrNearPaper = Math.hypot((vc.x || 0) - (c.x || 0), (vc.y || 0) - (c.y || 0)) < Math.max(H, 1) * 0.05; + // Overall paper VP: identity mapping of the sheet onto itself. + if (Math.abs(scale - 1) < 0.03 && targetNear0 && viewCtrNearPaper) return false; + return true; + } + + /** + * Model WCS (x,y) → paper-space point for a VIEWPORT (orthographic / top). + * DCS = R(+twist) * (model - viewTarget) [matches acadrust/ODA view-center convention] + * paper = center + scale * (DCS - viewCenter) + * scale = paperHeight / viewHeight + */ + _viewportModelToPaper(vp) { + const c = vp.center || { x: 0, y: 0 }; + const vc = vp.viewCenter || { x: 0, y: 0 }; + const vt = vp.viewTarget || { x: 0, y: 0 }; + const H = Number(vp.height) || 1; + const vH = (Number(vp.viewHeight) > 1e-12) ? Number(vp.viewHeight) : H; + const scale = H / vH; + const twist = Number(vp.twistAngle) || 0; + const cos = Math.cos(twist); + const sin = Math.sin(twist); + const fn = (mx, my) => { + const dx = mx - (vt.x || 0); + const dy = my - (vt.y || 0); + // R(+twist) · [dx, dy] + const rx = cos * dx - sin * dy; + const ry = sin * dx + cos * dy; + return { + x: (c.x || 0) + (rx - (vc.x || 0)) * scale, + y: (c.y || 0) + (ry - (vc.y || 0)) * scale, + }; + }; + fn.scale = scale; + fn.twist = twist; + return fn; + } + + _viewportPaperClip(vp) { + const c = vp.center || { x: 0, y: 0 }; + const hw = (Number(vp.width) || 0) / 2; + const hh = (Number(vp.height) || 0) / 2; + // Slight expand to avoid hairline gaps at edges + const pad = Math.max(hw, hh) * 1e-6; + return { + minX: (c.x || 0) - hw - pad, + maxX: (c.x || 0) + hw + pad, + minY: (c.y || 0) - hh - pad, + maxY: (c.y || 0) + hh + pad, + }; + } + + /** Cohen–Sutherland clip; calls out(x1,y1,x2,y2) for the visible segment (if any). */ + _clipSegToRect(x1, y1, x2, y2, rect, out) { + const INSIDE = 0, LEFT = 1, RIGHT = 2, BOTTOM = 4, TOP = 8; + const code = (x, y) => { + let c = INSIDE; + if (x < rect.minX) c |= LEFT; + else if (x > rect.maxX) c |= RIGHT; + if (y < rect.minY) c |= BOTTOM; + else if (y > rect.maxY) c |= TOP; + return c; + }; + let c1 = code(x1, y1), c2 = code(x2, y2); + for (;;) { + if (!(c1 | c2)) { out(x1, y1, x2, y2); return; } + if (c1 & c2) return; + const c = c1 || c2; + let x = 0, y = 0; + if (c & TOP) { + x = x1 + (x2 - x1) * (rect.maxY - y1) / (y2 - y1 || 1e-30); + y = rect.maxY; + } else if (c & BOTTOM) { + x = x1 + (x2 - x1) * (rect.minY - y1) / (y2 - y1 || 1e-30); + y = rect.minY; + } else if (c & RIGHT) { + y = y1 + (y2 - y1) * (rect.maxX - x1) / (x2 - x1 || 1e-30); + x = rect.maxX; + } else { + y = y1 + (y2 - y1) * (rect.minX - x1) / (x2 - x1 || 1e-30); + x = rect.minX; + } + if (c === c1) { x1 = x; y1 = y; c1 = code(x1, y1); } + else { x2 = x; y2 = y; c2 = code(x2, y2); } + } + } + + /** + * Bake the active viewport transform into a filled mesh and report whether it + * survives the viewport frame. + * + * _solidMesh / _hatchFill / _arrowMesh / _dotMesh build their own THREE.Mesh + * and never touch pushSeg, so the model→paper mapping has to happen here or + * not at all. Left unmapped they sit at raw survey coordinates: off the sheet, + * and dragging the content box out to the drawing's WCS origin — a sheet 18 + * units wide framed inside a 480,000-unit box renders as a blank screen. + * + * The mesh's own transform is folded in first (arrowheads carry their position + * and rotation on the object, not in the buffer), then reset, so the paper + * coordinates end up in the geometry where the fit box can see them. + * @returns {boolean} false when the mesh lies wholly outside the viewport. + */ + _bakeFillXf(obj) { + const { xf, clip, z } = this._fillXf; + const pos = obj.geometry?.attributes?.position; + if (!pos?.array) return true; + obj.updateMatrix(); + const m = obj.matrix; + const v = (this._fillTmp ||= new THREE.Vector3()); + const arr = pos.array; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let i = 0; i + 2 < arr.length; i += 3) { + v.set(arr[i], arr[i + 1], arr[i + 2]).applyMatrix4(m); + const p = xf(v.x, v.y); + arr[i] = p.x; + arr[i + 1] = p.y; + // Viewport content sits behind paper entities (see VIEWPORT_Z), but keep + // the helpers' own bias — arrowheads are lifted 0.5 above the dimension + // line they cap — scaled down so it can't cross into the paper layer. + arr[i + 2] = z + (v.z || 0) * 1e-3; + if (p.x < minX) minX = p.x; + if (p.x > maxX) maxX = p.x; + if (p.y < minY) minY = p.y; + if (p.y > maxY) maxY = p.y; + } + pos.needsUpdate = true; + obj.position.set(0, 0, 0); + obj.rotation.set(0, 0, 0); + obj.scale.set(1, 1, 1); + obj.updateMatrix(); + obj.geometry.computeBoundingSphere(); + return !(maxX < clip.minX || minX > clip.maxX || maxY < clip.minY || minY > clip.maxY); + } + + /** + * Emit model-space geometry into a paper VIEWPORT (transformed + clipped). + * Covers the entity types that dominate civil plan/profile sheets. + */ + _emitModelThroughViewport(vp, ctx) { + const { + entities, modelAllowed, blockDefHandles, entsByOwner, blockBaseByHex, + pushSeg: basePush, expand: baseExpand, pendingTexts, setCurLayer, + underlay = false, + } = ctx; + const xf = this._viewportModelToPaper(vp); + const clip = this._viewportPaperClip(vp); + const scale = xf.scale || 1; + const frozen = new Set((vp.frozenLayers || []).map((h) => String(h))); + + const pushSeg = (ax, ay, bx, by, z, color) => { + const a = xf(ax, ay); + const b = xf(bx, by); + this._clipSegToRect(a.x, a.y, b.x, b.y, clip, (x1, y1, x2, y2) => { + basePush(x1, y1, x2, y2, z || 0, color); + }); + }; + const expand = (x, y, z = 0) => { + const p = xf(x, y); + if (p.x >= clip.minX && p.x <= clip.maxX && p.y >= clip.minY && p.y <= clip.maxY) { + baseExpand(p.x, p.y, z); + } + }; + const inClip = (x, y) => x >= clip.minX && x <= clip.maxX && y >= clip.minY && y <= clip.maxY; + + // Hand the mapping to _addObj for the fills that bypass pushSeg. Cleared at + // the end of the pass (and in _clear) so paper-space entities, which are + // already in sheet coordinates, are never re-mapped. + this._fillXf = { xf, clip, z: ctx.fillZ ?? 0 }; + + // Quick reject: model bbox far from view focus (optional optimisation skip for correctness) + + for (const e of entities) { + const ownerHex = e.ownerHandle?.value?.toString(16); + if (!ownerHex || !modelAllowed.has(ownerHex)) continue; + if (blockDefHandles.has(ownerHex)) continue; + // Layer visibility is applied post-build (see _applyLayerVisibility). + setCurLayer?.(this._layerNameOf(e)); + if (frozen.size) { + const lh = e.layerHandle?.value; + if (lh != null && frozen.has(String(lh))) continue; + } + + const d = e.data || e; + const type = (e.type || e.typeName || '').toUpperCase(); + const color = this._entityColor(e); + const complexLt = this._resolveComplexLinetype(e); + const vpTextStart = pendingTexts.length; + + try { + switch (type) { + case 'LINE': + if (d.start && d.end) { + if (complexLt) { + this._drawComplexPath([d.start, d.end], false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + } else { + pushSeg(d.start.x, d.start.y, d.end.x, d.end.y, d.start.z || 0, color); + } + } + break; + case 'CIRCLE': + if (d.center && d.radius != null) { + if (complexLt) { + const pts = this._sampleArcPoints(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0); + this._drawComplexPath(pts, true, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + } else { + // pushSeg maps model endpoints → paper; keep radius in model units + this._arcSegs(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0, color, pushSeg); + } + } + break; + case 'ARC': + if (d.center && d.radius != null) { + if (complexLt) { + const pts = this._sampleArcPoints(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0); + this._drawComplexPath(pts, false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + } else { + this._arcSegs(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0, color, pushSeg); + } + } + break; + case 'LWPOLYLINE': + if (d.points?.length) { + const hasBulge = d.bulges?.some((b) => Math.abs(b) >= 1e-6); + const closed = !!(d.closed || (d.flags & 1)); + if (complexLt) { + const pts = this._samplePolylinePoints(d.points, d.bulges, closed, d.elevation || 0); + this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + pts.forEach((p) => expand(p.x, p.y, p.z)); + } else if (hasBulge) this._bulgePolySegs(d.points, d.bulges, closed, color, pushSeg, expand); + else { + this._polylineSegs(d.points, closed, d.elevation || 0, color, pushSeg); + d.points.forEach((p) => expand(p.x, p.y, d.elevation || 0)); + } + } + break; + case 'POLYLINE': + if (complexLt && (d.vertices || d.points)) { + const closed = !!(d.closed || (d.flags & 1)); + const pts = d.vertices || d.points; + this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts); + pts.forEach((p) => expand(p.x, p.y, p.z || 0)); + } else { + this._polylineSegs(d.vertices || d.points, d.closed || (d.flags & 1), 0, color, pushSeg); + } + break; + case 'ELLIPSE': + if (d.center) { + // Reuse main-path ellipse helper if present; else approximate via parametric segs + if (typeof this._ellipseSegs === 'function') { + this._ellipseSegs(d, color, pushSeg); + } else { + const cx = d.center.x, cy = d.center.y; + const maj = d.smAxis || d.majorAxis || { x: d.radius || 1, y: 0 }; + const ratio = d.axisRatio ?? d.minorAxisRatio ?? 0.5; + const a0 = d.startAngle ?? d.startParameter ?? 0; + const a1 = d.endAngle ?? d.endParameter ?? Math.PI * 2; + const n = 64; + let px, py; + for (let i = 0; i <= n; i++) { + const t = a0 + (a1 - a0) * (i / n); + const cos = Math.cos(t), sin = Math.sin(t); + const x = cx + maj.x * cos - maj.y * ratio * sin; + const y = cy + maj.y * cos + maj.x * ratio * sin; + if (i > 0) pushSeg(px, py, x, y, 0, color); + px = x; py = y; + } + } + } + break; + case 'SOLID': { + const corners = [d.corner1 || d.pt1, d.corner2 || d.pt2, d.corner3 || d.pt3, d.corner4 || d.pt4].filter(Boolean); + if (corners.length >= 2) { + for (let i = 0; i < corners.length; i++) { + const a = corners[i], b = corners[(i + 1) % corners.length]; + if (a && b) pushSeg(a.x, a.y, b.x, b.y, 0, color); + } + } + break; + } + case 'MESH': + // pushSeg already maps model → paper and clips, so no xf here. + if (d.vertices?.length >= 3) this._meshSegs(d, color, pushSeg); + break; + case 'HATCH': { + const paths = d.paths || e.paths || []; + for (const path of paths) { + const pts = path.points || path.vertices; + if (!pts?.length) continue; + const closed = path.closed !== false; + const bulges = path.bulges; + if (bulges?.some((b) => Math.abs(b) >= 1e-6)) { + this._bulgePolySegs(pts, bulges, closed, color, pushSeg, expand); + } else { + this._polylineSegs(pts, closed, 0, color, pushSeg); + pts.forEach((p) => expand(p.x, p.y)); + } + } + break; + } + case 'SPLINE': { + const pts = d.fitPoints?.length ? d.fitPoints : d.controlPoints; + if (pts?.length >= 2) this._polylineSegs(pts, !!d.closed, 0, color, pushSeg); + break; + } + case 'TEXT': + case 'MTEXT': { + const raw = e.text ?? d.text ?? ''; + const text = stripMText(raw); + if (!text) break; + const height = (e.textHeight ?? e.height ?? d.textHeight ?? d.height ?? 2.5) * scale; + // Geometry uses R(+twist); text angle in paper is modelAngle + twist. + let rotation = (e.rotationAngle ?? d.rotationAngle ?? 0) + (xf.twist || 0); + let alignH = 0, alignV = 0, pos; + if (type === 'TEXT') { + alignH = e.horizAlignment ?? d.horizAlignment ?? 0; + alignV = e.vertAlignment ?? d.vertAlignment ?? 0; + if (alignH === 4) alignV = 2; + const useAlignPt = alignH !== 0 || alignV !== 0; + pos = useAlignPt + ? (e.alignmentPt ?? d.alignmentPt ?? e.insertionPt ?? d.insertionPt ?? { x: 0, y: 0 }) + : (e.insertionPt ?? d.insertionPt ?? d.insertionPoint ?? { x: 0, y: 0 }); + } else { + const xd = e.xAxisDir ?? d.xAxisDir; + if (xd) rotation = Math.atan2(xd.y, xd.x) + (xf.twist || 0); + const ap = e.attachment ?? d.attachment ?? e.attachmentPoint ?? d.attachmentPoint ?? 5; + alignH = ([1, 4, 7].includes(ap) ? 0 : [3, 6, 9].includes(ap) ? 2 : 1); + alignV = ([1, 2, 3].includes(ap) ? 3 : [7, 8, 9].includes(ap) ? 1 : 2); + pos = e.insertionPt ?? d.insertionPt ?? d.insertionPoint ?? { x: 0, y: 0 }; + } + const pp = xf(pos.x, pos.y); + if (!inClip(pp.x, pp.y)) break; + pendingTexts.push({ + text, pos: pp, height, rotation, color, alignH, alignV, + underlay: !!underlay, + }); + expand(pos.x, pos.y); + break; + } + case 'INSERT': { + const ip = e.insertionPt ?? d.insertionPt ?? d.insertionPoint; + if (!ip) break; + const sx = (d.scaleX ?? d.scale?.x ?? e.scale?.x ?? 1); + const sy = (d.scaleY ?? d.scale?.y ?? e.scale?.y ?? 1); + const bhVal = e.blockHeaderHandle?.value ?? d.blockHeaderHandle?.value; + const bHex = bhVal != null ? bhVal.toString(16) : null; + const bEnts = bHex ? (entsByOwner.get(bHex) ?? []) : []; + // Transform block children by composing INSERT xf with viewport xf via pushSeg + if (bEnts.length) { + const textStart = pendingTexts.length; + this._insertEntities( + bEnts, + { + insertionPoint: ip, + xScale: sx, + yScale: sy, + rotation: d.rotation ?? e.rotation ?? 0, + basePoint: blockBaseByHex.get(bHex), + }, + color, + pushSeg, + pendingTexts, + { entsByOwner, blockBase: blockBaseByHex }, + ); + // INSERT may push model-space text; re-map into paper + underlay flag + for (let ti = textStart; ti < pendingTexts.length; ti++) { + const t = pendingTexts[ti]; + if (!t?.pos) continue; + const q = xf(t.pos.x, t.pos.y); + t.pos = q; + t.height = (t.height || 2.5) * scale; + t.rotation = (t.rotation || 0) + (xf.twist || 0); + t.underlay = !!underlay; + } + } + expand(ip.x, ip.y); + break; + } + default: + break; + } + if (complexLt && pendingTexts.length > vpTextStart) { + for (let ti = vpTextStart; ti < pendingTexts.length; ti++) { + const t = pendingTexts[ti]; + if (!t?.pos) continue; + const q = xf(t.pos.x, t.pos.y); + t.pos = q; + t.height = (t.height || 2.5) * scale; + t.rotation = (t.rotation || 0) + (xf.twist || 0); + t.underlay = !!underlay; + } + } + } catch { /* skip malformed */ } + for (let ti = vpTextStart; ti < pendingTexts.length; ti++) { + if (pendingTexts[ti].layer === undefined) pendingTexts[ti].layer = this._curLayer; + } + } + + this._fillXf = null; + + // Transform any pending texts that are still in model space (from INSERT helpers). + // Entries created above already have paper pos. Detect model-space leftovers by + // checking if pos lies far outside the paper clip *and* inside a model-ish range — + // safer: tag viewport texts. For insert-pushed texts, re-map those outside paper + // sheet bounds of the whole drawing is hard. Instead, convert texts whose height + // wasn't scaled (model heights are often large): skip — INSERT text in viewports + // is rare on these sheets. + } + + /** Entity count, block definition count — for recent-files metadata. */ + getStats() { + return { entities: this._entityMeta.length, blocks: this._blockCount ?? 0 }; + } + + /** Capture current view as a WebP Blob. Requires preserveDrawingBuffer:true (already set). */ + snapshotWebPBlob() { + return new Promise((resolve) => { + this._renderer.render(this._scene, this._camera); + this._renderer.domElement.toBlob((b) => resolve(b), 'image/webp', 0.75); + }); + } + + /** + * Layer list for the Layers panel: [{ name, colorHex, count, visible }]. + * The name/color/count part is O(entities) — cached per load so the panel can + * call this on every toggle without re-walking a 500k-entity drawing. + */ + getLayerInfo() { + const result = this._lastResult; + if (!result) return []; + let base = this._layerInfoCache; + if (!base) { + const counts = new Map(); + const allowed = this._activeSpaceHex + ? buildAllowedOwnerHexes(result.entities || [], this._activeSpaceHex) + : null; + for (const e of (result.entities || [])) { + const ownerHex = e.ownerHandle?.value?.toString(16); + if (allowed && ownerHex && !allowed.has(ownerHex)) continue; + const name = this._layerNameOf(e); + counts.set(name, (counts.get(name) || 0) + 1); + } + base = (result.tables?.layers || []).map(l => { + const name = l.name ?? l.layerName ?? '0'; + const aci = Math.abs(l.colorIndex ?? l.color ?? l.colorNumber ?? 7); + return { + name, + colorHex: '#' + ((aciToHex(aci, this._isDark) ?? DEFAULT_COLOR).toString(16).padStart(6, '0')), + count: counts.get(name) || 0, + }; + }).filter(l => l.count > 0 || !l.name.startsWith('*')); + this._layerInfoCache = base; + } + const hidden = this._hiddenLayers || new Set(); + return base.map(l => ({ ...l, visible: !hidden.has(l.name) })); + } + + // ── Private helpers ──────────────────────────────────────────────────────── + + _resolveComplexLinetype(e) { + let ltName = e.lineType ?? e.linetype; + if (!ltName || ltName === 'ByLayer' || ltName === 'BYLAYER' || + ltName === 'ByBlock' || ltName === 'BYBLOCK') { + const lh = e.layerHandle?.value ?? e.layerHandle; + ltName = (lh != null && this._layerLtByHandle?.get(String(lh))) + || this._layerLtByName?.get(e.layer ?? e.layerName) || null; + } + if (!ltName || ltName === 'Continuous' || ltName === 'ByLayer' || ltName === 'ByBlock') return null; + + const upperKey = ltName.toUpperCase().trim(); + const pattern = this._dwgLinPatterns?.get(upperKey) || getLinPattern(ltName); + if (!pattern) return null; + + const entScale = (e.entityHeader?.linetypeScale > 0 ? e.entityHeader.linetypeScale : 1); + const ltscale = (this._globalLtscale || 1) * entScale; + + return { pattern, ltscale }; + } + + _sampleArcPoints(center, radius, startAngle, endAngle, elevation = 0) { + let sweep = endAngle - startAngle; + if (sweep <= 0) sweep += Math.PI * 2; + const steps = Math.max(16, Math.ceil((sweep / (Math.PI * 2)) * ARC_SEGS)); + const pts = []; + const z = center.z || elevation || 0; + for (let i = 0; i <= steps; i++) { + const a = startAngle + (sweep * i) / steps; + pts.push({ + x: center.x + radius * Math.cos(a), + y: center.y + radius * Math.sin(a), + z, + }); + } + return pts; + } + + _samplePolylinePoints(points, bulges, closed, elevation = 0) { + if (!points || points.length < 2) return points || []; + const res = []; + const n = closed ? points.length : points.length - 1; + for (let i = 0; i < n; i++) { + const u = points[i]; + const v = points[(i + 1) % points.length]; + const b = bulges?.[i] || 0; + const z = u.z || elevation || 0; + if (Math.abs(b) < 1e-6) { + if (res.length === 0) res.push({ x: u.x, y: u.y, z }); + res.push({ x: v.x, y: v.y, z }); + } else { + const dx = v.x - u.x; + const dy = v.y - u.y; + const dist = Math.hypot(dx, dy); + if (dist > 1e-9) { + const theta = 4 * Math.atan(b); + const halfTheta = Math.abs(theta) / 2; + const R = dist / 2 / Math.sin(halfTheta); + const dCenter = R * Math.cos(halfTheta); + const mx = (u.x + v.x) / 2; + const my = (u.y + v.y) / 2; + const nx = -dy / dist; + const ny = dx / dist; + const dir = b > 0 ? 1 : -1; + const cx = mx + dir * nx * dCenter; + const cy = my + dir * ny * dCenter; + const a1 = Math.atan2(u.y - cy, u.x - cx); + const steps = Math.max(4, Math.ceil(Math.abs(theta) / (Math.PI / 16))); + for (let k = (res.length === 0 ? 0 : 1); k <= steps; k++) { + const a = a1 + (theta * k) / steps; + res.push({ + x: cx + R * Math.cos(a), + y: cy + R * Math.sin(a), + z, + }); + } + } + } + } + return res; + } + + _sampleEllipsePoints(d) { + const major = d.majorAxis ?? d.smAxis ?? { x: d.radius || 1, y: 0 }; + const center = d.center || { x: 0, y: 0 }; + const ratio = d.ratio ?? d.axisRatio ?? d.minorAxisRatio ?? 0.5; + const startParam = d.startParam ?? d.startAngle ?? 0; + const endParam = d.endParam ?? d.endAngle ?? Math.PI * 2; + const aLen = Math.hypot(major.x ?? 1, major.y ?? 0); + const bLen = aLen * ratio; + const rot = Math.atan2(major.y ?? 0, major.x ?? 1); + let sweep = endParam - startParam; + if (sweep <= 0) sweep += Math.PI * 2; + const steps = ELLIPSE_SEGS; + const pts = []; + const z = center.z || 0; + for (let i = 0; i <= steps; i++) { + const t = startParam + (sweep * i) / steps; + const ex = aLen * Math.cos(t); + const ey = bLen * Math.sin(t); + const x = center.x + ex * Math.cos(rot) - ey * Math.sin(rot); + const y = center.y + ex * Math.sin(rot) + ey * Math.cos(rot); + pts.push({ x, y, z }); + } + return pts; + } + + _drawComplexPath(points, closed, linPattern, ltscale, color, pushSeg, pendingTexts) { + if (!points || points.length < 2) return; + const pts = points.slice(); + if (closed && (Math.hypot(pts[pts.length - 1].x - pts[0].x, pts[pts.length - 1].y - pts[0].y) > 1e-6)) { + pts.push(pts[0]); + } + + const segLens = []; + let totalLen = 0; + for (let i = 0; i < pts.length - 1; i++) { + const dx = pts[i + 1].x - pts[i].x; + const dy = pts[i + 1].y - pts[i].y; + const len = Math.hypot(dx, dy); + segLens.push(len); + totalLen += len; + } + if (totalLen < 1e-6) return; + + const getPathState = (dist) => { + let s = Math.max(0, Math.min(dist, totalLen)); + for (let i = 0; i < segLens.length; i++) { + const len = segLens[i]; + if (s <= len || i === segLens.length - 1) { + const t = len > 1e-9 ? s / len : 0; + const p0 = pts[i]; + const p1 = pts[i + 1]; + const x = p0.x + (p1.x - p0.x) * t; + const y = p0.y + (p1.y - p0.y) * t; + const z = (p0.z || 0) + ((p1.z || 0) - (p0.z || 0)) * t; + const angle = Math.atan2(p1.y - p0.y, p1.x - p0.x); + return { x, y, z, angle }; + } + s -= len; + } + const last = pts[pts.length - 1]; + const prev = pts[pts.length - 2]; + return { + x: last.x, + y: last.y, + z: last.z || 0, + angle: Math.atan2(last.y - prev.y, last.x - prev.x), + }; + }; + + const elements = linPattern.elements || []; + const patternLen = linPattern.patternLength * ltscale; + if (patternLen < 1e-6 || !elements.length) return; + + // Calculate distance from pattern start to shape (arrowhead) element if present + let shapeOffset = -1; + let accum = 0; + for (const e of elements) { + if (e.type === 'shape') { + shapeOffset = accum; + break; + } + if (e.type === 'dash') accum += Math.max((e.val || 0.05) * ltscale, 0.01 * ltscale); + else if (e.type === 'gap') accum += Math.abs(e.val || 0.1) * ltscale; + } + + let s = 0; + let elemIdx = 0; + let lastShapePos = -1; + + while (s < totalLen) { + const elem = elements[elemIdx % elements.length]; + elemIdx++; + + if (elem.type === 'dash') { + const dashLen = Math.max((elem.val || 0.05) * ltscale, 0.01 * ltscale); + const startDist = s; + const endDist = Math.min(s + dashLen, totalLen); + + if (endDist > startDist) { + const pStart = getPathState(startDist); + const pEnd = getPathState(endDist); + pushSeg(pStart.x, pStart.y, pEnd.x, pEnd.y, pStart.z, color); + } + s += dashLen; + } else if (elem.type === 'gap') { + const gapLen = Math.abs(elem.val || 0.1) * ltscale; + s += gapLen; + } else if (elem.type === 'shape') { + lastShapePos = s; + const pState = getPathState(s); + this._renderShapeSymbol(elem, pState, ltscale, color, pushSeg); + } else if (elem.type === 'text') { + const pState = getPathState(s); + if (elem.text && pendingTexts) { + // 1. Data-driven Text Height: (fixedH * elem.scale) or (elem.scale * 0.5) to match arrowhead scale ratio + const sName = (elem.style || 'STANDARD').toUpperCase(); + const fixedH = this._textStyleHeightMap?.get(sName) || 0; + const sFactor = (elem.scale > 0 ? elem.scale * 0.5 : 0.1); + const baseH = fixedH > 0 ? (fixedH * sFactor) : sFactor; + const h = baseH * ltscale; + + let angle = pState.angle + ((elem.rotation || 0) * Math.PI) / 180; + if (elem.isAbsoluteAngle) { + angle = ((elem.rotation || 0) * Math.PI) / 180; + } + + // CAD Standard Upright Rule: text must always read from bottom or right (-90deg to +90deg) + let normA = Math.atan2(Math.sin(angle), Math.cos(angle)); + if (normA > Math.PI / 2) normA -= Math.PI; + else if (normA < -Math.PI / 2) normA += Math.PI; + angle = normA; + + const cosT = Math.cos(angle); + const sinT = Math.sin(angle); + + const xo = (elem.xOffset || 0) * ltscale; + // Vertical mid alignment: set yo = 0 so text middle lies directly on line center axis + const yo = 0; + + // 2. Exact Visual Midpoint: determine primary text gap (previous vs next gap) + const prevElem = elements[(elemIdx - 2 + elements.length) % elements.length]; + const nextElem = elements[elemIdx % elements.length]; + + const prevGapLen = (prevElem && prevElem.type === 'gap') ? Math.abs(prevElem.val || 0) * ltscale : 0; + const nextGapLen = (nextElem && nextElem.type === 'gap') ? Math.abs(nextElem.val || 0) * ltscale : 0; + + let midState = pState; + if (prevGapLen > nextGapLen && prevGapLen > 1e-6) { + // Text belongs inside previous gap (e.g. H-DICHL06R reverse mode S2 = -0.99) + midState = getPathState(s - prevGapLen / 2); + } else if (nextGapLen > 1e-6) { + // Text belongs inside next gap (e.g. H-DICHL06 forward mode S3 = -0.74) + midState = getPathState(s + nextGapLen / 2); + } + + const tx = midState.x + (xo * cosT - yo * sinT); + const ty = midState.y + (xo * sinT + yo * cosT); + + pendingTexts.push({ + text: elem.text, + pos: { x: tx, y: ty, z: midState.z }, + height: Math.max(h, 0.01), + rotation: angle, + color, + alignH: 1, // center horizontally in gap + alignV: 2, // center vertically on line axis (vertical mid) + }); + } + } + } + } + + _renderShapeSymbol(elem, pState, ltscale, color, pushSeg) { + const name = (elem.shapeName || '').toUpperCase(); + const sc = (elem.scale || 0.1) * ltscale; + const angle = elem.isAbsoluteAngle + ? ((elem.rotation || 0) * Math.PI) / 180 + : pState.angle + ((elem.rotation || 0) * Math.PI) / 180; + + const xo = (elem.xOffset || 0) * ltscale; + const yo = (elem.yOffset || 0) * ltscale; + + const cosA = Math.cos(pState.angle); + const sinA = Math.sin(pState.angle); + const cx = pState.x + (xo * cosA - yo * sinA); + const cy = pState.y + (xo * sinA + yo * cosA); + const cz = pState.z || 0; + + const cosR = Math.cos(angle); + const sinR = Math.sin(angle); + + const transform = (lx, ly) => ({ + x: cx + (lx * cosR - ly * sinR) * sc, + y: cy + (lx * sinR + ly * cosR) * sc, + }); + + if (name.includes('KSC35') || name.includes('KSC36') || name.includes('KSC19') || name.includes('ARROW')) { + // Solid hatched arrowhead (matching AutoCAD KSC35 standard symbol) + const steps = 12; + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const lx = -0.5 + t; + const hy = 0.25 * (1 - t); + const top = transform(lx, hy); + const bot = transform(lx, -hy); + pushSeg(top.x, top.y, bot.x, bot.y, cz, color); + } + const p1 = transform(0.5, 0); + const p2 = transform(-0.5, 0.25); + const p3 = transform(-0.5, -0.25); + pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color); + pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color); + pushSeg(p3.x, p3.y, p1.x, p1.y, cz, color); + } else if (name.includes('CIRC') || name.includes('KSC11') || name.includes('KSC01')) { + const segs = 12; + let prev = transform(0.3, 0); + for (let i = 1; i <= segs; i++) { + const a = (i / segs) * Math.PI * 2; + const curr = transform(0.3 * Math.cos(a), 0.3 * Math.sin(a)); + pushSeg(prev.x, prev.y, curr.x, curr.y, cz, color); + prev = curr; + } + } else if (name.includes('BOX') || name.includes('KSC02') || name.includes('KSC05')) { + const p1 = transform(-0.3, -0.3); + const p2 = transform(0.3, -0.3); + const p3 = transform(0.3, 0.3); + const p4 = transform(-0.3, 0.3); + pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color); + pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color); + pushSeg(p3.x, p3.y, p4.x, p4.y, cz, color); + pushSeg(p4.x, p4.y, p1.x, p1.y, cz, color); + } else if (name.includes('TRACK1')) { + const p1 = transform(0, -0.4); + const p2 = transform(0, 0.4); + pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color); + } else if (name.includes('BAT') || name.includes('DIAMOND')) { + const p1 = transform(0, 0.4); + const p2 = transform(0.4, 0); + const p3 = transform(0, -0.4); + const p4 = transform(-0.4, 0); + pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color); + pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color); + pushSeg(p3.x, p3.y, p4.x, p4.y, cz, color); + pushSeg(p4.x, p4.y, p1.x, p1.y, cz, color); + } else { + const p1 = transform(0.3, 0); + const p2 = transform(-0.3, 0.2); + const p3 = transform(-0.3, -0.2); + pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color); + pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color); + pushSeg(p3.x, p3.y, p1.x, p1.y, cz, color); + } + } + + _buildStyleMap(styles) { + if (!this._textStyleHeightMap) this._textStyleHeightMap = new Map(); + this._textStyleHeightMap.clear(); + if (!styles || !Array.isArray(styles)) return; + for (const st of styles) { + const sName = (st.name || st.styleName || '').toUpperCase(); + const h = st.height ?? st.fixedTextHeight ?? 0; + if (sName && h > 0) { + this._textStyleHeightMap.set(sName, h); + } + } + } + + _buildLayerMap(layers) { + this._layerByHandle.clear(); + this._layerByName.clear(); + this._layerNameByHandle = new Map(); + this._layerLwByHandle = new Map(); + this._layerLwByName = new Map(); + this._layer0Handle = null; + if (!layers) return; + for (const l of layers) { + const handle = l.handle?.value ?? l.handle; + const name = l.name ?? l.layerName; + const aci = Math.abs(l.colorIndex ?? l.color ?? l.colorNumber ?? 7); + const hex = aciToHex(aci, this._isDark) ?? DEFAULT_COLOR; + if (handle != null) this._layerByHandle.set(String(handle), hex); + if (name) this._layerByName.set(name, hex); + if (handle != null && name) this._layerNameByHandle.set(String(handle), name); + if (name === '0' && handle != null) this._layer0Handle = String(handle); + // Raw i16 lineweight (1/100 mm, or -1/-2/-3). DXF spells it 370. + const lw = l.lineWeight ?? l.lineweight ?? l.lineWeightRaw; + if (typeof lw === 'number') { + if (handle != null) this._layerLwByHandle.set(String(handle), lw); + if (name) this._layerLwByName.set(name, lw); // DXF layers carry no handle + } + } + } + + /** + * Effective lineweight of an entity, in screen pixels (0 = hairline). + * + * ByLayer (-1) resolves against the LAYER table, ByBlock (-2) against the + * weight inherited from the owning INSERT, Default (-3) against LWDEFAULT. + * Anything at or below the CAD default stays hairline so drawings that never + * assigned a weight render exactly as before. + */ + _lwPx(entity, inheritedPx = 0) { + this._curLwMm = 0; + if (!this._lwDisplay) return 0; + let raw = entity?.entityHeader?.lineWeight ?? entity?.lineWeight ?? entity?.lineweight; + if (typeof raw !== 'number') return 0; + if (raw === -2) { // ByBlock + const scale = this.getLineweightScale(); + this._curLwMm = inheritedPx > 0 ? inheritedPx / scale : 0; + return inheritedPx; + } + if (raw === -1) { // ByLayer + const lh = entity.layerHandle?.value ?? entity.layerHandle; + const byHandle = lh != null ? this._layerLwByHandle.get(String(lh)) : undefined; + const ln = entity.layer ?? entity.layerName; + raw = byHandle ?? (ln != null ? this._layerLwByName.get(ln) : undefined) ?? -3; + if (raw === -1 || raw === -2) raw = -3; // layer can't be ByLayer/ByBlock + } + const mm = raw === -3 ? LW_DEFAULT_MM : raw / 100; + if (!(mm > LW_HAIRLINE_MM)) return 0; + this._curLwMm = mm; + const scale = this.getLineweightScale(); + return Math.min(LW_MAX_PX, Math.round(mm * scale * 10) / 10); + } + + // Active-viewport VIEWTWIST (radians). The sheet is un-twisted by rotating the + // camera up by -viewTwist (verified against samples/11.dwg: title border upright). + _readViewTwist(result) { + const vports = result?.tables?.vports; + if (!Array.isArray(vports) || !vports.length) return 0; + const vp = vports.find(v => /active/i.test(v.name || '')) || vports[0]; + const tw = vp?.viewTwist; + return (typeof tw === 'number' && isFinite(tw)) ? tw : 0; + } + + // Build linetype lookup: pattern-by-name (signed element lengths, drawing + // units) + layer→linetype-name maps + the global LTSCALE. Consumed by + // _resolveDash to turn an entity's linetype into a dash/gap size. + _buildLinetypeMap(result) { + this._ltPatterns = new Map(); + this._dwgLinPatterns = new Map(); + this._layerLtByName = new Map(); + this._layerLtByHandle = new Map(); + this._globalLtscale = (result?.vars?.ltscale > 0 ? result.vars.ltscale : 1); + for (const lt of (result?.tables?.lineTypes ?? [])) { + if (lt?.name) { + const patArr = Array.isArray(lt.pattern) ? lt.pattern : []; + this._ltPatterns.set(lt.name, patArr); + if (patArr.length > 0) { + const merged = mergeDwgLinetypePattern(lt.name, patArr, lt.description); + if (merged) { + this._dwgLinPatterns.set(lt.name.toUpperCase().trim(), merged); + } + } + } + } + for (const l of (result?.tables?.layers ?? [])) { + const name = l.name ?? l.layerName; + const handle = l.handle?.value ?? l.handle; + const ltn = l.lineType ?? l.linetype; + if (name && ltn) this._layerLtByName.set(name, ltn); + if (handle != null && ltn) this._layerLtByHandle.set(String(handle), ltn); + } + } + + // Resolve an entity's dash pattern → { key, dash, gap } world-unit sizes, or + // null for a solid line. BYLAYER / BYBLOCK / empty fall back to the layer's + // linetype; dash/gap = Σ|element| × LTSCALE × entity linetypeScale (CELTSCALE). + _resolveDash(e) { + if (!this._ltPatterns || this._ltPatterns.size === 0) return null; + let ltName = e.lineType ?? e.linetype; + if (!ltName || ltName === 'ByLayer' || ltName === 'BYLAYER' || + ltName === 'ByBlock' || ltName === 'BYBLOCK') { + const lh = e.layerHandle?.value ?? e.layerHandle; + ltName = (lh != null && this._layerLtByHandle.get(String(lh))) + || this._layerLtByName.get(e.layer ?? e.layerName) || null; + } + if (!ltName || ltName === 'Continuous' || ltName === 'ByLayer' || ltName === 'ByBlock') return null; + const pattern = this._ltPatterns.get(ltName); + if (!pattern || pattern.length === 0) return null; + const entScale = (e.entityHeader?.linetypeScale > 0 ? e.entityHeader.linetypeScale : 1); + const scale = this._globalLtscale * entScale; + let dash = 0, gap = 0; + for (const d of pattern) { + const len = Math.abs(d) * scale; + if (d > 1e-6) dash += len; + else if (d < -1e-6) gap += len; + else dash += Math.max(len, 0.01 * scale); // dot → tiny dash + } + if (dash < 1e-4 || gap < 1e-4) return null; + return { key: `${dash.toFixed(3)}|${gap.toFixed(3)}`, dash, gap }; + } + + // Resolve an entity's color inside a block, given the color inherited from the + // INSERT context. BYBLOCK and layer-0 BYLAYER entities inherit the INSERT color + // (AutoCAD's layer-0 rule); everything else uses its own color/layer. + _resolveBlockColor(entity, inherited) { + const aci = entity.entityHeader?.colorIndex + ?? entity.color ?? entity.colorIndex ?? entity.colorNumber ?? 256; + if (aci === 0) return inherited; // BYBLOCK + const lh = String(entity.layerHandle?.value ?? entity.layerHandle ?? ''); + if (aci === 256 && this._layer0Handle && lh === this._layer0Handle) return inherited; + return this._entityColor(entity); + } + + _entityColor(entity) { + const aci = entity.entityHeader?.colorIndex + ?? entity.color ?? entity.colorIndex ?? entity.colorNumber ?? 256; + if (aci != null) { + const hex = aciToHex(aci, this._isDark); + if (hex !== null) return hex; + } + const lh = entity.layerHandle?.value ?? entity.layerHandle; + if (lh != null) { + const c = this._layerByHandle.get(String(lh)); + if (c !== undefined) return c; + } + const ln = entity.layer ?? entity.layerName; + if (ln) { + const c = this._layerByName.get(ln); + if (c !== undefined) return c; + } + return DEFAULT_COLOR; + } + + // Filled triangle arrow mesh (replaces V-line arrowhead) + _arrowMesh(pt, rotation, size, color) { + if (!pt || size < 1e-4) return; + const w = size * 0.166; + const r = ((color >> 16) & 0xFF) / 255; + const g = ((color >> 8) & 0xFF) / 255; + const b = (color & 0xFF) / 255; + const geom = new THREE.BufferGeometry(); + geom.setAttribute('position', new THREE.Float32BufferAttribute([0,0,0, -size,w,0, -size,-w,0], 3)); + geom.setAttribute('color', new THREE.Float32BufferAttribute([r,g,b, r,g,b, r,g,b], 3)); + const mesh = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide })); + mesh.position.set(pt.x, pt.y, (pt.z || 0) + 0.5); + mesh.rotation.z = rotation; + this._addObj(mesh); + } + + // Default geometry for AutoCAD standard arrowhead blocks that ship no geometry + // in the file (system blocks). `name` is the lowercased block name. + _defaultArrow(name, pt, rotation, size, color, pushSeg) { + if (!pt || size < 1e-4) return; + const n = (name || '').replace(/^[_*]+/, ''); + const cos = Math.cos(rotation), sin = Math.sin(rotation); + // local→world: arrow points toward +X at the insertion point + const P = (lx, ly) => ({ x: pt.x + lx * cos - ly * sin, y: pt.y + lx * sin + ly * cos }); + if (n === 'none' || n === 'small' || n === 'integral') return; + if (n === 'dot' || n === 'dotsmall' || n === 'dotblank' || n === 'dotsmallblank' || n === 'origin' || n === 'origin2') { + this._dotMesh(pt, size * (n === 'dotsmall' ? 0.12 : 0.5), color); + return; + } + if (n === 'oblique' || n === 'archtick') { + const a = P(size * 0.5, size * 0.5), b = P(-size * 0.5, -size * 0.5); + pushSeg(a.x, a.y, b.x, b.y, (pt.z || 0) + 0.5, color); + return; + } + if (n === 'open' || n === 'open30' || n === 'open90') { + const w = n === 'open90' ? size : size * 0.42; + const t = P(0, 0), u = P(-size, w), v = P(-size, -w); + pushSeg(u.x, u.y, t.x, t.y, (pt.z || 0) + 0.5, color); + pushSeg(v.x, v.y, t.x, t.y, (pt.z || 0) + 0.5, color); + return; + } + // default (_ClosedFilled, "", closed, boxfilled, datumfilled, …) → filled triangle + this._arrowMesh(pt, rotation, size, color); + } + + // Filled disc (dot arrowhead). + _dotMesh(center, radius, color) { + if (!center || !(radius > 1e-5)) return; + const r = ((color >> 16) & 0xFF) / 255, g = ((color >> 8) & 0xFF) / 255, b = (color & 0xFF) / 255; + const segs = 20, verts = [], cols = [], z = (center.z || 0) + 0.5; + for (let i = 0; i < segs; i++) { + const a0 = i / segs * Math.PI * 2, a1 = (i + 1) / segs * Math.PI * 2; + verts.push(center.x, center.y, z, + center.x + radius * Math.cos(a0), center.y + radius * Math.sin(a0), z, + center.x + radius * Math.cos(a1), center.y + radius * Math.sin(a1), z); + cols.push(r, g, b, r, g, b, r, g, b); + } + const geom = new THREE.BufferGeometry(); + geom.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3)); + geom.setAttribute('color', new THREE.Float32BufferAttribute(cols, 3)); + this._addObj(new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide }))); + } + + // Dot radius (block-local) from an arrow block's geometry, measured from its centroid. + _arrowDotRadius(ents) { + let r = 0; + for (const ae of (ents || [])) { + const ad = ae.data || ae; + if ((ae.type || '').toUpperCase() === 'CIRCLE' && ad.radius) r = Math.max(r, ad.radius); + const pts = ad.points ?? ad.vertices; + if (pts?.length) { + let cx = 0, cy = 0; pts.forEach(p => { cx += p.x; cy += p.y; }); cx /= pts.length; cy /= pts.length; + pts.forEach(p => { r = Math.max(r, Math.hypot(p.x - cx, p.y - cy)); }); + } + } + return r; + } + + _arcSegs(center, r, a0, a1, z, color, pushSeg) { + let span = a1 - a0; + if (span <= 0) span += Math.PI * 2; + const steps = Math.max(8, Math.ceil((span / (Math.PI * 2)) * ARC_SEGS)); + let prev = null; + for (let i = 0; i <= steps; i++) { + const a = a0 + (span * i) / steps; + const x = center.x + r * Math.cos(a); + const y = center.y + r * Math.sin(a); + if (prev) pushSeg(prev.x, prev.y, x, y, z, color); + prev = { x, y }; + } + } + + // Centripetal CatmullRom interpolation for SPLINE + _catmullRom(pts, t) { + const n = pts.length; + const f = t * (n - 1); + const i = Math.min(Math.floor(f), n - 2); + const tt = f - i; + const p0 = pts[Math.max(i - 1, 0)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(i + 2, n - 1)]; + const t2 = tt * tt, t3 = t2 * tt; + return { + x: 0.5 * ((2*p1.x) + (-p0.x+p2.x)*tt + (2*p0.x-5*p1.x+4*p2.x-p3.x)*t2 + (-p0.x+3*p1.x-3*p2.x+p3.x)*t3), + y: 0.5 * ((2*p1.y) + (-p0.y+p2.y)*tt + (2*p0.y-5*p1.y+4*p2.y-p3.y)*t2 + (-p0.y+3*p1.y-3*p2.y+p3.y)*t3), + z: 0.5 * ((2*(p1.z||0)) + (-(p0.z||0)+(p2.z||0))*tt + (2*(p0.z||0)-5*(p1.z||0)+4*(p2.z||0)-(p3.z||0))*t2 + (-(p0.z||0)+3*(p1.z||0)-3*(p2.z||0)+(p3.z||0))*t3), + }; + } + + _ellipseSegs(d, color, pushSeg) { + // field names: majorAxis or smAxis; ratio or axisRatio + const ma = d.majorAxis ?? d.smAxis ?? { x: 1, y: 0, z: 0 }; + const center = d.center; + const ratio = d.ratio ?? d.axisRatio ?? 1; + const startParam = d.startParam ?? 0; + const endParam = d.endParam ?? Math.PI * 2; + const mx = ma?.x ?? 1, my = ma?.y ?? 0; + const a = Math.sqrt(mx*mx + my*my); + const b = a * ratio; + const rot = Math.atan2(my, mx); + let span = endParam - startParam; + if (span <= 0) span += Math.PI * 2; + let prev = null; + for (let i = 0; i <= ELLIPSE_SEGS; i++) { + const t = startParam + (span * i) / ELLIPSE_SEGS; + const ex = a * Math.cos(t), ey = b * Math.sin(t); + const x = center.x + ex * Math.cos(rot) - ey * Math.sin(rot); + const y = center.y + ex * Math.sin(rot) + ey * Math.cos(rot); + if (prev) pushSeg(prev.x, prev.y, x, y, center.z || 0, color); + prev = { x, y }; + } + } + + _polylineSegs(points, closed, z, color, pushSeg) { + if (!points || points.length < 2) return; + for (let i = 0; i < points.length - 1; i++) { + pushSeg(points[i].x, points[i].y, points[i+1].x, points[i+1].y, z, color); + } + if (closed) { + const a = points[points.length-1], b = points[0]; + pushSeg(a.x, a.y, b.x, b.y, z, color); + } + } + + /** + * MESH (AcDbSubDMesh) → wireframe segments. + * + * dwg-wasm emits the mesh as flat number arrays (see its `EntityType::Mesh` + * arm) because a civil road-surface mesh runs to ~10^5 vertices: + * vertices [x, y, z, …] + * edges deduplicated vertex-index pairs [a, b, …] + * faceList DXF group-93 layout — [n, i0 … i(n-1)] repeated, n-gons included + * + * The edge list is preferred: it is already deduplicated, so an edge shared by + * two triangles is drawn once instead of twice (~2× fewer segments on a TIN). + * faceList is the fallback for a mesh whose edge list is empty, and it is what + * a future filled pass would triangulate. + * + * Only the base (control) mesh is drawn. `subdivisionLevel > 0` means AutoCAD + * displays a Catmull-Clark refinement of these vertices; refining is not + * implemented, so such a mesh renders slightly more angular than in AutoCAD. + * + * `xf(x, y) -> [x, y]` optionally maps the coordinates (block-local INSERT, + * or model space seen through a paper-space viewport). + */ + _meshSegs(d, color, pushSeg, xf = null) { + const v = d.vertices; + if (!v?.length) return; + const n = (v.length / 3) | 0; + const seg = (a, b) => { + if (a === b || !(a >= 0 && a < n) || !(b >= 0 && b < n)) return; + let ax = v[a*3], ay = v[a*3+1]; + let bx = v[b*3], by = v[b*3+1]; + // pushSeg carries one z per segment; a sloped edge uses its midpoint + // elevation, which is what a top view needs for depth ordering. + const z = ((v[a*3+2] || 0) + (v[b*3+2] || 0)) / 2; + if (xf) { [ax, ay] = xf(ax, ay); [bx, by] = xf(bx, by); } + pushSeg(ax, ay, bx, by, z, color); + }; + + const edges = d.edges; + if (edges?.length >= 2) { + for (let i = 0; i + 1 < edges.length; i += 2) seg(edges[i], edges[i+1]); + return; + } + const fl = d.faceList; + if (!fl?.length) return; + for (let i = 0; i < fl.length; ) { + const cnt = fl[i++]; + // A corrupt count would run the cursor off the end — stop instead. + if (!(cnt >= 3) || i + cnt > fl.length) break; + for (let k = 0; k < cnt; k++) seg(fl[i + k], fl[i + ((k + 1) % cnt)]); + i += cnt; + } + } + + // Resolve start/end width for segment i→i+1 (DXF 40/41, fallback 43 constant). + _segWidths(ent, i, scale = 1) { + const d = ent?.data || ent || {}; + const cw = (d.constantWidth ?? d.constWidth ?? 0) * scale; + const dsw = (d.defaultStartWidth ?? 0) * scale; + const dew = (d.defaultEndWidth ?? 0) * scale; + const sw0 = (d.startWidths?.[i] ?? 0) * scale; + const ew0 = (d.endWidths?.[i] ?? 0) * scale; + const sw = Math.abs(sw0) > 1e-12 ? sw0 : (Math.abs(cw) > 1e-12 ? cw : dsw); + const ew = Math.abs(ew0) > 1e-12 ? ew0 : (Math.abs(cw) > 1e-12 ? cw : dew); + return { sw: Math.abs(sw), ew: Math.abs(ew) }; + } + + _polyHasWidth(ent, nSeg, scale = 1) { + for (let i = 0; i < nSeg; i++) { + const { sw, ew } = this._segWidths(ent, i, scale); + if (sw > 1e-9 || ew > 1e-9) return true; + } + return false; + } + + // Draw one trapezoid segment (variable start/end half-width) as two triangles. + _pushWideSeg(verts, p1, p2, w0, w1) { + const dx = p2.x - p1.x, dy = p2.y - p1.y; + const len = Math.hypot(dx, dy); + if (len < 1e-12) return; + const nx = -dy / len, ny = dx / len; + const h0 = w0 * 0.5, h1 = w1 * 0.5; + const a = { x: p1.x + nx * h0, y: p1.y + ny * h0 }; + const b = { x: p1.x - nx * h0, y: p1.y - ny * h0 }; + const c = { x: p2.x - nx * h1, y: p2.y - ny * h1 }; + const d = { x: p2.x + nx * h1, y: p2.y + ny * h1 }; + // two triangles a-b-c, a-c-d + verts.push(a.x, a.y, 0, b.x, b.y, 0, c.x, c.y, 0); + verts.push(a.x, a.y, 0, c.x, c.y, 0, d.x, d.y, 0); + } + + // Variable-width polyline → solid mesh (MeshBasicMaterial). Used when any + // segment has start/end/constant width > 0 (도곽 outer frame etc.). + _widePolyMesh(points, bulges, closed, ent, color, scale = 1, expand = null) { + if (!points || points.length < 2) return; + const n = points.length; + const segs = closed ? n : n - 1; + const verts = []; + for (let i = 0; i < segs; i++) { + const p1 = points[i]; + const p2 = points[(i + 1) % n]; + const { sw, ew } = this._segWidths(ent, i, scale); + if (sw < 1e-9 && ew < 1e-9) { + // Zero-width segment: still emit a tiny hair so it isn't dropped entirely. + this._pushWideSeg(verts, p1, p2, 0.01 * scale, 0.01 * scale); + if (expand) { expand(p1.x, p1.y); expand(p2.x, p2.y); } + continue; + } + const bulge = bulges?.[i] || 0; + if (Math.abs(bulge) < 1e-6) { + this._pushWideSeg(verts, p1, p2, sw, ew); + if (expand) { expand(p1.x, p1.y); expand(p2.x, p2.y); } + } else { + // Tessellate bulge arc; interpolate width along the arc. + const dx = p2.x - p1.x, dy = p2.y - p1.y; + const dist = Math.hypot(dx, dy); + if (dist < 1e-9) continue; + const inc = 4 * Math.atan(bulge); + const half = Math.abs(inc) / 2; + const radius = (dist / 2) / Math.sin(half); + const apothem = radius * Math.cos(half); + const mx2 = (p1.x + p2.x) / 2, my2 = (p1.y + p2.y) / 2; + const nx2 = -dy / dist, ny2 = dx / dist; + const sign = bulge > 0 ? 1 : -1; + const cx = mx2 + sign * nx2 * apothem; + const cy = my2 + sign * ny2 * apothem; + const sa = Math.atan2(p1.y - cy, p1.x - cx); + const steps = Math.max(2, Math.ceil(Math.abs(inc) / (Math.PI / 16))); + let prev = { x: p1.x, y: p1.y }; + let prevW = sw; + for (let k = 1; k <= steps; k++) { + const t = k / steps; + const a = sa + (inc * k) / steps; + const cur = { x: cx + radius * Math.cos(a), y: cy + radius * Math.sin(a) }; + const curW = sw + (ew - sw) * t; + this._pushWideSeg(verts, prev, cur, prevW, curW); + prev = cur; + prevW = curW; + } + if (expand) { expand(p1.x, p1.y); expand(p2.x, p2.y); } + } + } + if (verts.length < 9) return; + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3)); + geo.computeVertexNormals(); + const mat = new THREE.MeshBasicMaterial({ + color: new THREE.Color(color), + side: THREE.DoubleSide, + depthWrite: false, + }); + this._addObj(new THREE.Mesh(geo, mat)); + // Pick as edge segments only — do NOT register the poly as a fill loop. + // A closed outer frame (도곽 cw=2) used as _pickFills would mark the whole + // interior as "covered" and (with a weak size check) hide every inner line. + if (this._pickCurIdx >= 0 && this._pickSegs) { + for (let i = 0; i < segs; i++) { + const a = points[i], b = points[(i + 1) % n]; + this._pickSegs.push(a.x, a.y, b.x, b.y); + this._pickSegMeta.push(this._pickCurIdx); + } + } + } + + // Polyline with bulge arcs (LWPOLYLINE, HATCH boundaries) + _bulgePolySegs(points, bulges, closed, color, pushSeg, expand) { + const n = points.length; + const segs = closed ? n : n - 1; + for (let i = 0; i < segs; i++) { + const p1 = points[i]; + const p2 = points[(i + 1) % n]; + const bulge = bulges?.[i] || 0; + if (Math.abs(bulge) < 1e-6) { + pushSeg(p1.x, p1.y, p2.x, p2.y, 0, color); + if (expand) { expand(p1.x, p1.y); expand(p2.x, p2.y); } + } else { + const dx = p2.x - p1.x, dy = p2.y - p1.y; + const dist = Math.sqrt(dx*dx + dy*dy); + if (dist < 1e-9) continue; + // Sweep by the signed included angle Δ = 4·atan(bulge). Sweeping the angle + // directly (rather than computing both endpoint angles and resolving the + // 2π wrap) avoids drawing the major arc when |bulge| is tiny. + const inc = 4 * Math.atan(bulge); // signed; >0 = CCW + const half = Math.abs(inc) / 2; + const radius = (dist / 2) / Math.sin(half); + const apothem = radius * Math.cos(half); + const mx2 = (p1.x + p2.x) / 2, my2 = (p1.y + p2.y) / 2; + const nx2 = -dy / dist, ny2 = dx / dist; // left normal of p1→p2 + const sign = bulge > 0 ? 1 : -1; + const cx = mx2 + sign * nx2 * apothem; + const cy = my2 + sign * ny2 * apothem; + const sa = Math.atan2(p1.y - cy, p1.x - cx); + const steps = Math.max(2, Math.ceil(Math.abs(inc) / (Math.PI / 16))); + let prev = p1; + for (let k = 1; k <= steps; k++) { + const a = sa + (inc * k) / steps; + const x = cx + radius * Math.cos(a), y = cy + radius * Math.sin(a); + pushSeg(prev.x, prev.y, x, y, 0, color); + prev = { x, y }; + } + if (expand) { expand(p1.x, p1.y); expand(p2.x, p2.y); } + } + } + } + + _solidMesh(corners, color) { + const r = ((color >> 16) & 0xFF) / 255; + const g = ((color >> 8) & 0xFF) / 255; + const b = (color & 0xFF) / 255; + const verts = [], cols = []; + const addTri = (...pts) => { + for (const p of pts) { verts.push(p.x, p.y, p.z || 0); cols.push(r, g, b); } + }; + if (corners.length >= 4) { + addTri(corners[0], corners[1], corners[3]); + addTri(corners[0], corners[3], corners[2]); + } else { + addTri(corners[0], corners[1], corners[2]); + } + const geom = new THREE.BufferGeometry(); + geom.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3)); + geom.setAttribute('color', new THREE.Float32BufferAttribute(cols, 3)); + this._addObj(new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide }))); + // Filled quad/tri → pickable region (DWG SOLID vertex order is 0,1,3,2). + if (this._pickCurIdx >= 0 && corners.length >= 3) { + const loop = corners.length >= 4 ? [corners[0], corners[1], corners[3], corners[2]] : corners.slice(0, 3); + this._pickFills.push({ metaIdx: this._pickCurIdx, loops: [loop] }); + } + } + + // Text has no line geometry — register its box as a pickable fill so a click + // anywhere on the text selects it. Box follows the sprite's anchor rules + // (alignH 0/3/5=left,2=right,1/4=center; alignV 3=top,0/1=bottom,2=middle). + _addTextPick(text, pos, height, alignH = 0, alignV = 0) { + if (this._pickCurIdx < 0 || !pos) return; + const rows = String(text).split('\n'); + const cols = rows.reduce((m, s) => Math.max(m, s.length), 1); + const w = cols * height * 0.62; // ~ per-glyph advance + const h = rows.length * height * 1.2; + let x0 = pos.x; + if (alignH === 2) x0 = pos.x - w; // right anchor + else if (alignH === 1 || alignH === 4) x0 = pos.x - w / 2; // center + let y0 = pos.y; // bottom anchor + if (alignV === 3) y0 = pos.y - h; // top anchor + else if (alignV === 2) y0 = pos.y - h / 2; // middle + this._pickFills.push({ metaIdx: this._pickCurIdx, loops: [[ + { x: x0, y: y0 }, { x: x0 + w, y: y0 }, { x: x0 + w, y: y0 + h }, { x: x0, y: y0 + h }, + ]] }); + } + + // Even-odd point-in-region across ALL loops of a fill (holes exclude correctly). + _fillHit(loops, x, y) { + let inside = false; + for (const poly of loops) { + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y; + if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) inside = !inside; + } + } + return inside; + } + + // True if (x,y) lies inside ANY single loop (NOT even-odd across loops). + // Used to detect "this point belongs to hatch geometry" including hole + // interiors — even-odd would report holes as uncovered and let a later + // LWPOLYLINE re-solid them (CXGLOGO ㅇ/ㅎ counters). + _pointInAnyLoop(loops, x, y) { + for (const poly of loops) { + if (!poly?.length) continue; + let c = false; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const a = poly[i], b = poly[j]; + if (((a.y > y) !== (b.y > y)) && (x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x)) c = !c; + } + if (c) return true; + } + return false; + } + + // Signed area of a loop (for topmost = smallest-area tie-break on overlapping fills). + _loopArea(poly) { + let s = 0; + for (let i = 0; i < poly.length; i++) { const a = poly[i], b = poly[(i + 1) % poly.length]; s += a.x * b.y - b.x * a.y; } + return Math.abs(s) / 2; + } + + // Solid-fill a polyline that is geometrically closed but not covered by any + // existing hatch fill. Text-as-hatch logos sometimes ship glyph strokes as + // LWPOLYLINE outlines only (e.g. ㅅ of 사 in CXGLOGO) while sibling glyphs + // have solid HATCH — without this they render as empty outlines. + // + // Coverage test is ANY-loop containment (not even-odd). Logo blocks list + // HATCH entities first, then the same glyph outlines as LWPOLYLINE. Even-odd + // treats hole interiors as "uncovered", so the post-hatch polyline pass used + // to re-solid ㅇ/ㅎ counters and wipe the punches. Any-loop still lets true + // orphan outlines (ㅅ with no hatch) fill, while skipping anything that + // already belongs to a hatch loop — fill or hole. + _fillClosedPolyIfUncovered(points, bulges, color) { + if (!points || points.length < 3) return; + const a = points[0], b = points[points.length - 1]; + const gap = Math.hypot(a.x - b.x, a.y - b.y); + // Allow tiny gaps relative to polyline size (floating-point closed loops). + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const p of points) { + if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; + if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; + } + const diag = Math.hypot(maxX - minX, maxY - minY) || 1; + if (gap > diag * 1e-4 && gap > 1e-6) return; + // Area gate: logo glyph strokes are small (area ~1–100 in block units). + // Refuse huge loops even inside blocks (defensive). + let sa = 0; + for (let i = 0; i < points.length; i++) { + const p0 = points[i], p1 = points[(i + 1) % points.length]; + sa += p0.x * p1.y - p1.x * p0.y; + } + const absA = Math.abs(sa) / 2; + if (absA < 1e-6 || absA > 5000) return; + let cx = 0, cy = 0; + for (const p of points) { cx += p.x; cy += p.y; } + cx /= points.length; cy /= points.length; + for (const f of this._pickFills) { + if (this._pointInAnyLoop(f.loops, cx, cy)) return; + } + this._hatchFill([{ points, bulges: bulges || null, closed: true }], color); + } + + // Solid fill HATCH using THREE.ShapeGeometry + // Tessellate one (closed) hatch boundary loop → flat {x,y}[] (resolves bulge arcs, + // including the closing edge — a 2-point/2-bulge loop is a full circle). + _pathPoints(path) { + const pts = path.points; + if (!pts?.length) return null; + const bulges = path.bulges; + const n = pts.length; + if (!bulges?.some(b => Math.abs(b) >= 1e-6)) return pts.map(p => ({ x: p.x, y: p.y })); + const out = []; + for (let i = 0; i < n; i++) { + const p1 = pts[i], p2 = pts[(i + 1) % n], bulge = bulges[i] || 0; + out.push({ x: p1.x, y: p1.y }); + if (Math.abs(bulge) < 1e-6) continue; + const dx = p2.x - p1.x, dy = p2.y - p1.y, dist = Math.hypot(dx, dy); + if (dist < 1e-9) continue; + const inc = 4 * Math.atan(bulge), half = Math.abs(inc) / 2; + const radius = (dist / 2) / Math.sin(half), apo = radius * Math.cos(half); + const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2; + const nx = -dy / dist, ny = dx / dist, sign = bulge > 0 ? 1 : -1; + const cx = mx + sign * nx * apo, cy = my + sign * ny * apo; + const sa = Math.atan2(p1.y - cy, p1.x - cx); + const segs = Math.max(6, Math.ceil(Math.abs(inc) / (Math.PI / 16))); + for (let j = 1; j < segs; j++) { // intermediate arc points; p2 added next iter + const a = sa + inc * j / segs; + out.push({ x: cx + radius * Math.cos(a), y: cy + radius * Math.sin(a) }); + } + } + return out; + } + + // Solid fill with true even-odd parity across all boundary loops. + // + // DWG solid HATCH (style Normal / odd parity) is even-odd, not "outer + + // nested holes". Text-as-hatch logos (CXGLOGO 한국도로공사) ship + // self-intersecting multi-stroke loops where ShapeGeometry/earcut + geometric + // nesting mis-classifies ㅇ/ㅎ counters. Verified against samples/11.dwg: + // SVG fill-rule=evenodd over all non-outlier loops matches the CAD logo. + _hatchFill(paths, color) { + const totalPts = paths.reduce((s, p) => s + (p.points?.length ?? 0), 0); + if (totalPts > 8000) return; // safety for pathological boundaries + let polys = []; + for (const p of paths) { const pp = this._pathPoints(p); if (pp && pp.length >= 3) polys.push(pp); } + if (!polys.length) return; + + const area = (poly) => { let s = 0; for (let i = 0; i < poly.length; i++) { const a = poly[i], b = poly[(i + 1) % poly.length]; s += a.x * b.y - b.x * a.y; } return Math.abs(s) / 2; }; + + // Drop spurious giant boundaries (parse-corrupt arcs expanded to huge loops). + // Many-loop hatches (≥5) or empty-loop hatches get median×10 outlier filter. + if ((paths.some(p => !(p.points?.length)) && polys.length >= 3) || polys.length >= 5) { + const sorted = polys.map(area).sort((a, b) => a - b); + const med = sorted[Math.floor(sorted.length / 2)] || 0; + if (med > 0) polys = polys.filter(p => area(p) <= med * 10); + if (!polys.length) return; + } + + // Picking uses even-odd across all loops (holes exclude correctly). + if (this._pickCurIdx >= 0 && polys.length) this._pickFills.push({ metaIdx: this._pickCurIdx, loops: polys }); + + // When does this hatch need true even-odd (vs independent union of islands)? + // - Self-intersecting loops (text-as-hatch glyph strokes) + // - Many loops (≥5): logo words like 한국도로공사 + // - Geometric nesting: a smaller loop sits inside a larger one (island/hole) + // Separate solid islands that only touch/overlap (EX logo 4 paths) must be + // UNION — even-odd would punch diamond holes at the overlaps. + const needsEvenOdd = + polys.length >= 5 || + polys.some((p) => this._polySelfIntersects(p)) || + this._hatchHasNestedLoop(polys); + + if (needsEvenOdd) { + this._hatchFillEvenOddCanvas(polys, color); + return; + } + + // Union of simple islands: one ShapeGeometry per loop (no hole punching). + const mat = new THREE.MeshBasicMaterial({ color: new THREE.Color(color), side: THREE.DoubleSide }); + for (const poly of polys) { + try { + const shape = new THREE.Shape(); + shape.moveTo(poly[0].x, poly[0].y); + for (let k = 1; k < poly.length; k++) shape.lineTo(poly[k].x, poly[k].y); + shape.closePath(); + this._addObj(new THREE.Mesh(new THREE.ShapeGeometry(shape), mat)); + } catch { /* degenerate */ } + } + } + + // True if any loop is (mostly) inside a larger loop — island/hole topology. + _hatchHasNestedLoop(polys) { + if (polys.length < 2) return false; + const area = (poly) => { + let s = 0; + for (let i = 0; i < poly.length; i++) { + const a = poly[i], b = poly[(i + 1) % poly.length]; + s += a.x * b.y - b.x * a.y; + } + return Math.abs(s) / 2; + }; + const bboxOf = (poly) => { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const p of poly) { + if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; + if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; + } + return { minX, minY, maxX, maxY }; + }; + const inside = (poly, pt) => { + let c = false; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const a = poly[i], b = poly[j]; + if (((a.y > pt.y) !== (b.y > pt.y)) && (pt.x < (b.x - a.x) * (pt.y - a.y) / (b.y - a.y) + a.x)) c = !c; + } + return c; + }; + const ar = polys.map(area); + const bbs = polys.map(bboxOf); + for (let i = 0; i < polys.length; i++) { + let cx = 0, cy = 0; + for (const p of polys[i]) { cx += p.x; cy += p.y; } + cx /= polys[i].length; cy /= polys[i].length; + for (let j = 0; j < polys.length; j++) { + if (i === j || ar[j] <= ar[i]) continue; + const bb = bbs[j], bi = bbs[i]; + const bboxHit = bb.minX <= bi.minX && bb.minY <= bi.minY && bb.maxX >= bi.maxX && bb.maxY >= bi.maxY; + if (bboxHit || inside(polys[j], { x: cx, y: cy })) return true; + } + } + return false; + } + + // Cheap self-intersection probe (adjacent edges ignored). Used only to choose + // ShapeGeometry vs even-odd canvas; false negatives still go through canvas + // when there are multiple loops. + _polySelfIntersects(poly) { + const n = poly.length; + if (n < 4) return false; + const cross = (u, v, w) => (v.x - u.x) * (w.y - u.y) - (v.y - u.y) * (w.x - u.x); + const hits = (a, b, c, d) => { + const d1 = cross(a, b, c), d2 = cross(a, b, d), d3 = cross(c, d, a), d4 = cross(c, d, b); + return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)); + }; + // Cap checks for large loops (logo glyphs are small). + const lim = Math.min(n, 80); + for (let i = 0; i < lim; i++) { + const a = poly[i], b = poly[(i + 1) % n]; + for (let j = i + 2; j < lim; j++) { + if (i === 0 && j === n - 1) continue; + if (j === (i + n - 1) % n) continue; + if (hits(a, b, poly[j], poly[(j + 1) % n])) return true; + } + } + return false; + } + + // Diameter-like edge through bbox center: corrupt hatch chords (CXGLOGO ㅎ) + // that turn even-odd into a Mercedes/pie pattern. True when shoelace area is + // also inflated above the convex hull (self-intersecting junk). + _isDiameterInflatedPath(pts) { + if (!pts || pts.length < 8) return false; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const p of pts) { + if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; + if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; + } + const w = maxX - minX, h = maxY - minY; + const minDim = Math.min(w, h); + if (!(minDim > 1e-9)) return false; + const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2; + const thr = 0.55 * minDim; + const centerR = 0.22 * minDim; + let diamN = 0; + for (let i = 0; i < pts.length; i++) { + const a = pts[i], b = pts[(i + 1) % pts.length]; + const len = Math.hypot(b.x - a.x, b.y - a.y); + if (len < thr) continue; + const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2; + if (Math.hypot(mx - cx, my - cy) <= centerR) diamN++; + } + if (!diamN) return false; + // shoelace abs area vs convex hull — inflated ⇒ self-intersecting + let sa = 0; + for (let i = 0; i < pts.length; i++) { + const a = pts[i], b = pts[(i + 1) % pts.length]; + sa += a.x * b.y - b.x * a.y; + } + const absA = Math.abs(sa) / 2; + const hull = this._convexHull(pts); + if (hull.length < 3) return false; + let ha = 0; + for (let i = 0; i < hull.length; i++) { + const a = hull[i], b = hull[(i + 1) % hull.length]; + ha += a.x * b.y - b.x * a.y; + } + ha = Math.abs(ha) / 2; + return ha > 1e-9 && absA > ha * 1.15; + } + + _convexHull(pts) { + const p = pts.map((q) => ({ x: q.x, y: q.y })).sort((a, b) => a.x - b.x || a.y - b.y); + if (p.length <= 2) return p; + const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); + const lower = []; + for (const pt of p) { + while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pt) <= 0) lower.pop(); + lower.push(pt); + } + const upper = []; + for (let i = p.length - 1; i >= 0; i--) { + const pt = p[i]; + while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pt) <= 0) upper.pop(); + upper.push(pt); + } + lower.pop(); + upper.pop(); + return lower.concat(upper); + } + + _polyCentroid(poly) { + let x = 0, y = 0; + for (const p of poly) { x += p.x; y += p.y; } + return { x: x / poly.length, y: y / poly.length }; + } + + _pointInPoly(poly, pt) { + let c = false; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const a = poly[i], b = poly[j]; + if (((a.y > pt.y) !== (b.y > pt.y)) && (pt.x < (b.x - a.x) * (pt.y - a.y) / (b.y - a.y) + a.x)) c = !c; + } + return c; + } + + // Raster even-odd fill into a CanvasTexture plane. Sharp enough for logo-scale + // glyphs (tens of units); large site hatches still use ShapeGeometry when they + // are a single simple loop. Texture is disposed with the mesh on reload. + // + // Diameter-inflated loops (ㅎ of 한 in CXGLOGO): pure even-odd yields a pie/ + // Mercedes pattern from spurious center chords. Those loops are filled with + // nonzero (solid glyph + cross), then nested sibling loops punch the counters + // (ㅇ hole). Remaining loops use normal even-odd. + _hatchFillEvenOddCanvas(polys, color) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const poly of polys) { + for (const p of poly) { + if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; + if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; + } + } + const worldW = maxX - minX; + const worldH = maxY - minY; + if (!(worldW > 1e-12) || !(worldH > 1e-12)) return; + + // Pad 1px equivalent so AA edges aren't clipped. + const MAX = 2048; + const pxPerUnit = Math.min(MAX / worldW, MAX / worldH, 64); + const tw = Math.max(2, Math.ceil(worldW * pxPerUnit) + 2); + const th = Math.max(2, Math.ceil(worldH * pxPerUnit) + 2); + const sx = (tw - 2) / worldW; + const sy = (th - 2) / worldH; + + const canvas = document.createElement('canvas'); + canvas.width = tw; + canvas.height = th; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.clearRect(0, 0, tw, th); + + const toCanvas = (p) => ({ + x: 1 + (p.x - minX) * sx, + y: 1 + (maxY - p.y) * sy, + }); + const addPoly = (poly) => { + if (poly.length < 3) return; + const p0 = toCanvas(poly[0]); + ctx.moveTo(p0.x, p0.y); + for (let i = 1; i < poly.length; i++) { + const p = toCanvas(poly[i]); + ctx.lineTo(p.x, p.y); + } + ctx.closePath(); + }; + + const solidPolys = []; + const evenPolys = []; + for (const poly of polys) { + if (this._isDiameterInflatedPath(poly)) solidPolys.push(poly); + else evenPolys.push(poly); + } + // Self-intersecting solid outlines fail ordinary PIP for hole nesting — use + // their convex hull as the containment oracle (ㅎ outer vs ㅇ counter). + const solidHulls = solidPolys.map((p) => this._convexHull(p)); + + const isHoleOfSolid = (poly) => { + if (poly.length < 3 || !solidHulls.length) return false; + const c = this._polyCentroid(poly); + for (const hull of solidHulls) { + if (hull.length >= 3 && this._pointInPoly(hull, c)) return true; + } + return false; + }; + + // 1) Nonzero fill for diameter-inflated glyphs (ㅎ solid + cross, no pie). + if (solidPolys.length) { + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + for (const poly of solidPolys) addPoly(poly); + ctx.fill('nonzero'); + + // 2) Punch counters nested inside a solid glyph. + const holes = evenPolys.filter(isHoleOfSolid); + if (holes.length) { + ctx.globalCompositeOperation = 'destination-out'; + ctx.beginPath(); + for (const poly of holes) addPoly(poly); + ctx.fill('nonzero'); + ctx.globalCompositeOperation = 'source-over'; + } + } + + // 3) Remaining loops (not used as hole punches) → even-odd. + const rest = evenPolys.filter((p) => !isHoleOfSolid(p)); + if (rest.length) { + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + for (const poly of rest) addPoly(poly); + ctx.fill('evenodd'); + } + + const tex = new THREE.CanvasTexture(canvas); + tex.colorSpace = THREE.NoColorSpace; + tex.magFilter = THREE.LinearFilter; + tex.minFilter = THREE.LinearMipmapLinearFilter; + tex.generateMipmaps = true; + tex.needsUpdate = true; + + const mat = new THREE.MeshBasicMaterial({ + map: tex, + color: new THREE.Color(color), + transparent: true, + alphaTest: 0.4, + side: THREE.DoubleSide, + depthWrite: false, + }); + const geo = new THREE.PlaneGeometry(worldW, worldH); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set((minX + maxX) / 2, (minY + maxY) / 2, 0); + // PlaneGeometry is XY; our 2D world is already XY. No rotation needed. + this._addObj(mesh); + } + + // Multi-line MTEXT: one canvas with stacked lines, aligned as a block. + // alignH 0=left 1/4=center 2=right (per line); alignV 3=top 2=middle 0/1=bottom anchor. + _multilineSprite(lines, pos, height, rotation, color, alignH, alignV, renderOrder = 0) { + const fontSize = 96; + const font = `${fontSize}px 'Malgun Gothic', 'Apple SD Gothic Neo', monospace`; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = font; + let tw = 1, asc = fontSize * 0.70, desc = fontSize * 0.20; + for (const ln of lines) { + const m = ctx.measureText(ln || ' '); + tw = Math.max(tw, Math.ceil(m.width)); + asc = Math.max(asc, m.actualBoundingBoxAscent || fontSize * 0.70); + desc = Math.max(desc, m.actualBoundingBoxDescent || fontSize * 0.20); + } + const pad = Math.ceil(fontSize * 0.15); + const step = Math.ceil(asc + desc + fontSize * 0.35); // baseline-to-baseline + canvas.width = tw + pad * 2; + canvas.height = step * lines.length + pad * 2; + ctx.font = font; // resizing the canvas clears state + + const ri = (color >> 16) & 0xFF, gi = (color >> 8) & 0xFF, bi = color & 0xFF; + ctx.fillStyle = `rgb(${ri},${gi},${bi})`; + ctx.textBaseline = 'alphabetic'; + let ax = pad; + if (alignH === 1 || alignH === 4) { ctx.textAlign = 'center'; ax = canvas.width / 2; } + else if (alignH === 2) { ctx.textAlign = 'right'; ax = canvas.width - pad; } + else { ctx.textAlign = 'left'; ax = pad; } + for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], ax, pad + asc + i * step); + + const texture = new THREE.CanvasTexture(canvas); + const pxToWorld = height / asc; // one line's cap height == DWG text height + const vw = canvas.width * pxToWorld; + const vh = canvas.height * pxToWorld; + + // Anchor the text block so the requested datum lands on pos (block edges). + let ox = 0, oy = 0; + if (alignH === 0 || alignH === 3 || alignH === 5) ox = vw / 2; // left edge at pos.x + else if (alignH === 2) ox = -vw / 2; // right edge at pos.x + if (alignV === 3) oy = -vh / 2; // top edge at pos.y + else if (alignV === 0 || alignV === 1) oy = vh / 2; // bottom edge at pos.y + + const mat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false }); + if (rotation) mat.rotation = rotation; + const sprite = new THREE.Sprite(mat); + sprite.scale.set(vw, vh, 1); + sprite.position.set(pos.x + ox, pos.y + oy, 1); + sprite.renderOrder = renderOrder; + this._addObj(sprite); + } + + // MTEXT background mask (opaque fill behind text). Flags (DXF 90): + // 0x01 = use background fill color (63) + // 0x02 = use drawing window color + // 0x10 = text frame only (R2018+) + // bgScale (DXF 45, default 1.5) = border offset factor × text height. + _drawMTextBackground(t, engine, minTextH) { + const flags = t.bgFillFlags | 0; + if (!flags) return; + const useFill = !!(flags & 0x01) || !!(flags & 0x02); + const useFrame = !!(flags & 0x10); + if (!useFill && !useFrame) return; + + const height = Math.max(t.height || 2.5, minTextH || 0); + const lines = String(t.text || '').split('\n'); + const ls = t.linespacingFactor > 0 ? t.linespacingFactor : 1; + // Match slugText line step (CAD MTEXT default ~ 5/3 of height × factor) + const lineStep = height * (5 / 3) * ls; + let maxEm = 0; + if (engine?.measureEm) { + for (const line of lines) maxEm = Math.max(maxEm, engine.measureEm(line || ' ')); + } else { + // ~0.9 em per Hangul/Latin fallback when engine unavailable + for (const line of lines) maxEm = Math.max(maxEm, (line || ' ').length * 0.9); + } + let textW = Math.max(maxEm * height, height * 0.5); + let textH = lines.length <= 1 + ? height + : height + (lines.length - 1) * lineStep; + if (t.rectWidth > 0) textW = Math.max(textW, t.rectWidth); + if (t.rectHeight > 0) textH = Math.max(textH, t.rectHeight); + + const scale = t.bgScale > 0 ? t.bgScale : 1.5; + // Scale factor expands the text bbox (1.5 → 50% larger total). + const boxW = textW * scale; + const boxH = textH * scale; + + // Text-center offset from insertion/attachment point (same rules as slug/sprite). + const alignH = t.alignH ?? 1; + const alignV = t.alignV ?? 2; + let lx = 0, ly = 0; + if (alignH === 0 || alignH === 3 || alignH === 5) lx = textW / 2; + else if (alignH === 2) lx = -textW / 2; + if (alignV === 3) ly = -textH / 2; + else if (alignV === 0 || alignV === 1) ly = textH / 2; + + const rot = t.rotation || 0; + const cos = Math.cos(rot), sin = Math.sin(rot); + const cx = t.pos.x + lx * cos - ly * sin; + const cy = t.pos.y + lx * sin + ly * cos; + + let bgColor = 0xFFFF00; // default yellow if color missing + if (flags & 0x02) { + // Drawing window color — follow viewer theme + const dark = this._scene?.background?.r < 0.5; + bgColor = dark ? 0x0a0b0d : 0xf7f6f3; + } else if (t.bgColorRgb && (t.bgColorRgb.r != null)) { + bgColor = ((t.bgColorRgb.r & 255) << 16) | ((t.bgColorRgb.g & 255) << 8) | (t.bgColorRgb.b & 255); + } else if (t.bgColorIndex != null) { + const hex = aciToHex(t.bgColorIndex, this._isDark); + if (hex != null) bgColor = hex; + } + + if (useFill) { + const geo = new THREE.PlaneGeometry(boxW, boxH); + const mat = new THREE.MeshBasicMaterial({ + color: new THREE.Color(bgColor), + side: THREE.DoubleSide, + depthWrite: false, + transparent: false, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(cx, cy, 0.4); // behind text (z≈1) + mesh.rotation.z = rot; + this._addObj(mesh); + } + if (useFrame && !useFill) { + // Outline only — four edges as hairline segs (rare path) + const hw = boxW / 2, hh = boxH / 2; + const corners = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh], [-hw, -hh]]; + const world = corners.map(([x, y]) => ({ + x: cx + x * cos - y * sin, + y: cy + x * sin + y * cos, + })); + // Use a thin mesh strip ring via Line if we had a helper; simple solid edges: + for (let i = 0; i < 4; i++) { + const a = world[i], b = world[i + 1]; + this._widePolyMesh( + [a, b], null, false, + { constantWidth: Math.max(height * 0.05, 0.01) }, + bgColor, 1, null, + ); + } + } + } + + // Slug vector text (GPU winding-number glyphs, see slugText.ts): one merged + // mesh per load for ALL texts. Async because the TTF loads once on demand; + // falls back to the legacy canvas sprites if the font can't be fetched. + // Underlay (viewport model labels) and overlay (paper labels) are two meshes + // so paper text always composites above viewport content (Layout draw order). + async _drawTexts(texts, minTextH) { + const gen = this._textGen; + const under = texts.filter((t) => t.underlay); + const over = texts.filter((t) => !t.underlay); + const layers = [ + { list: under, order: 0 }, + { list: over, order: 1 }, + ]; + try { + const engine = await SlugTextEngine.shared(); + if (gen !== this._textGen) return; + for (const { list, order } of layers) { + if (!list.length) continue; + for (const t of list) { + if ((t.bgFillFlags | 0) !== 0) { + this._curLayer = t.layer ?? '0'; // _drawMTextBackground emits via _addObj + this._drawMTextBackground(t, engine, minTextH); + } + } + // One merged mesh per CAD layer (not one per order): a layer toggle then + // just flips mesh.visible instead of rebuilding the whole text batch. + for (const [lname, items] of groupByLayer(list)) { + const batch = new SlugTextBatch(engine); + for (const t of items) { + batch.add(t.text, t.pos, Math.max(t.height, minTextH), t.rotation || 0, t.color, t.alignH ?? 0, t.alignV ?? 0); + } + const mesh = batch.build(); + if (!mesh) continue; + mesh.renderOrder = order; + // Transparent text: draw later order on top regardless of z + if (mesh.material) { + mesh.material.depthWrite = false; + mesh.material.transparent = true; + } + this._addObj(mesh, lname); + } + } + } catch (e) { + console.warn('[Viewer2D] Slug text unavailable, falling back to canvas sprites:', e?.message ?? e); + if (gen !== this._textGen) return; + for (const { list, order } of layers) { + for (const t of list) { + this._curLayer = t.layer ?? '0'; + if ((t.bgFillFlags | 0) !== 0) this._drawMTextBackground(t, null, minTextH); + this._textSprite(t.text, t.pos, Math.max(t.height, minTextH), t.rotation, t.color, t.alignH ?? 0, t.alignV ?? 0, order); + } + } + } + } + + // alignH: 0=left, 1=center, 2=right, 4=middle-center. alignV: 0=baseline,1=bottom,2=middle,3=top. + _textSprite(text, pos, height, rotation, color, alignH = 0, alignV = 0, renderOrder = 0) { + // MTEXT paragraph breaks (\P → \n) produce multi-line text: render stacked. + const lines = String(text).split('\n'); + if (lines.length > 1) { + this._multilineSprite(lines, pos, height, rotation, color, alignH, alignV, renderOrder); + return; + } + // Texture cache: table drawings repeat the same strings thousands of times + // ("D25", sizes, counts). Rasterizing each occurrence separately OOMs on + // dense sheets (e.g. 6k+ texts) — share one CanvasTexture per (text, color). + // Alignment/rotation are per-sprite (scale/offset/material), not per-raster. + const cacheKey = text + '' + color; + let tex = (this._texCache ??= new Map()).get(cacheKey); + if (!tex) { + const fontSize = 96; + const font = `${fontSize}px 'Malgun Gothic', 'Apple SD Gothic Neo', monospace`; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = font; + const m = ctx.measureText(text); + const tw = Math.max(1, Math.ceil(m.width)); + // Actual ink extents above/below the baseline (fallback to typical font ratios). + const asc = m.actualBoundingBoxAscent || fontSize * 0.70; // ≈ cap/glyph height + const desc = m.actualBoundingBoxDescent || fontSize * 0.20; + const pad = Math.ceil(fontSize * 0.15); + // Symmetric padding → the ink bounding box stays centered in the canvas. + canvas.width = tw + pad * 2; + canvas.height = Math.ceil(asc + desc) + pad * 2; + ctx.font = font; // reset (resizing the canvas clears state) + const ri = (color >> 16) & 0xFF, gi = (color >> 8) & 0xFF, bi = color & 0xFF; + ctx.fillStyle = `rgb(${ri},${gi},${bi})`; + ctx.textBaseline = 'alphabetic'; + ctx.fillText(text, pad, pad + asc); + tex = { texture: new THREE.CanvasTexture(canvas), w: canvas.width, h: canvas.height, asc, pad }; + this._texCache.set(cacheKey, tex); + } + const { texture, asc, pad } = tex; + + // Size so the visible ascent (≈ DWG cap height) equals the DWG text `height`. + const vh = height * tex.h / asc; + const vw = vh * (tex.w / tex.h); + const pxToWorld = vh / tex.h; + const centerPx = tex.h / 2; + + // X: place sprite so the requested horizontal datum lands on pos.x. + let ox = 0; + if (alignH === 0 || alignH === 3 || alignH === 5) ox = vw / 2; // left edge at pos.x + else if (alignH === 1 || alignH === 4) ox = 0; // center at pos.x + else if (alignH === 2) ox = -vw / 2; // right edge at pos.x + + // Y: map the alignment datum (in canvas px) to pos.y. Canvas y grows downward, + // world y upward → world offset of a canvas row = (centerPx - rowPx) * pxToWorld. + // ah=4 (DWG "middle" justification) forces both-axis centering regardless of av. + let oy = 0; + if (alignH !== 4) { + if (alignV === 0 || alignV === 1) oy = (pad + asc - centerPx) * pxToWorld; // baseline/bottom datum + else if (alignV === 3) oy = (pad - centerPx) * pxToWorld; // top datum + // alignV === 2 (middle): ink already centered → oy = 0 + } + + const mat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false }); + if (rotation) mat.rotation = rotation; + const sprite = new THREE.Sprite(mat); + sprite.scale.set(vw, vh, 1); + sprite.position.set(pos.x + ox, pos.y + oy, 1); + sprite.renderOrder = renderOrder; + this._addObj(sprite); + } + + // Render block definition entities transformed by INSERT params + _insertEntities(entities, d, color, pushSeg, pendingTexts, ctx) { + const entsByOwner = ctx?.entsByOwner; + const blockBase = ctx?.blockBase; + const parentXf = ctx?.parentXf ?? null; + const parentScale = ctx?.scale ?? 1; + const parentRot = ctx?.rot ?? 0; + const depth = ctx?.depth ?? 0; + + const ip = d.insertionPoint ?? { x: 0, y: 0 }; + const base = d.basePoint; + const sx = d.xScale ?? 1, sy = d.yScale ?? 1; + const rot = d.rotation ?? 0; + const cos = Math.cos(rot), sin = Math.sin(rot); + + // Local transform: block-local coords → parent-local (or world if top level). + const localXf = (px, py) => { + const lx = base ? px - base.x : px; + const ly = base ? py - base.y : py; + return [ + ip.x + (lx * sx) * cos - (ly * sy) * sin, + ip.y + (lx * sx) * sin + (ly * sy) * cos, + ]; + }; + // Compose with parent transform for nested INSERTs. + const xf = parentXf ? (px, py) => parentXf(...localXf(px, py)) : localXf; + // Accumulated uniform scale (for radii) and rotation (for arc/ellipse angles). + const scaleAcc = parentScale * Math.max(Math.abs(sx), Math.abs(sy)); + const rotAcc = parentRot + rot; + + // Weight the INSERT itself carries — what a ByBlock child resolves to. + const insertLwPx = this._curLwPx; + for (const be of entities) { + const bd = be.data || be; + const bt = (be.type || be.typeName || '').toUpperCase(); + if (bt === 'ATTDEF') continue; // skip attribute definitions + const ecol = this._resolveBlockColor(be, color); + this._curLwPx = this._lwPx(be, insertLwPx); + try { + if (bt === 'LINE' && bd.start && bd.end) { + const [ax, ay] = xf(bd.start.x, bd.start.y); + const [bx, by] = xf(bd.end.x, bd.end.y); + pushSeg(ax, ay, bx, by, 0, ecol); + + } else if (bt === 'MESH' && bd.vertices?.length >= 3) { + this._meshSegs(bd, ecol, pushSeg, xf); + + } else if ((bt === 'CIRCLE' || bt === 'ARC') && bd.center && bd.radius != null) { + const [cx, cy] = xf(bd.center.x, bd.center.y); + const r = bd.radius * scaleAcc; + const a0 = bt === 'ARC' ? (bd.startAngle ?? 0) + rotAcc : 0; + const a1 = bt === 'ARC' ? (bd.endAngle ?? Math.PI*2) + rotAcc : Math.PI*2; + this._arcSegs({ x:cx, y:cy, z:0 }, r, a0, a1, 0, ecol, pushSeg); + + } else if (bt === 'ELLIPSE' && bd.center) { + const [cx, cy] = xf(bd.center.x, bd.center.y); + const ma = bd.majorAxis ?? bd.smAxis ?? { x:1, y:0 }; + const mlen = Math.hypot(ma.x ?? 0, ma.y ?? 0) * scaleAcc; + const mang = Math.atan2(ma.y ?? 0, ma.x ?? 1) + rotAcc; + this._ellipseSegs({ + center: { x:cx, y:cy, z:0 }, + majorAxis: { x: mlen * Math.cos(mang), y: mlen * Math.sin(mang) }, + ratio: bd.ratio ?? bd.axisRatio ?? 1, + startParam: bd.startParam ?? 0, + endParam: bd.endParam ?? Math.PI*2, + }, ecol, pushSeg); + + } else if ((bt === 'LWPOLYLINE' || bt === 'POLYLINE') && (bd.points || bd.vertices)) { + const raw = bd.points ?? bd.vertices; + const pts = raw.map(p => { const [nx, ny] = xf(p.x, p.y); return { x:nx, y:ny }; }); + // Preserve per-vertex bulges through the transform (scalar, not position). + const bulges = bd.bulges ?? null; + const closed = !!(bd.closed || (bd.flags & 1)); + // Logo blocks list solid HATCH first, then the same glyph outlines as + // LWPOLYLINE. Re-stroking those glyph outlines draws chords across ㅇ + // holes — skip ONLY true re-outlines: same scale as the covering loop. + // (Not "poly smaller than loop" — that also killed 도곽 안쪽 가는 선 + // whose centroid sat inside the outer frame / a large hatch.) + let skipGlyphReoutline = false; + if (pts.length >= 2 && this._pickFills.length) { + let cx = 0, cy = 0, pMinX = Infinity, pMinY = Infinity, pMaxX = -Infinity, pMaxY = -Infinity; + for (const p of pts) { + cx += p.x; cy += p.y; + if (p.x < pMinX) pMinX = p.x; if (p.y < pMinY) pMinY = p.y; + if (p.x > pMaxX) pMaxX = p.x; if (p.y > pMaxY) pMaxY = p.y; + } + cx /= pts.length; cy /= pts.length; + const pw = Math.max(pMaxX - pMinX, 1e-9); + const ph = Math.max(pMaxY - pMinY, 1e-9); + for (const f of this._pickFills) { + if (!this._pointInAnyLoop(f.loops, cx, cy)) continue; + for (const loop of f.loops) { + if (!loop?.length) continue; + let lMinX = Infinity, lMinY = Infinity, lMaxX = -Infinity, lMaxY = -Infinity; + for (const q of loop) { + if (q.x < lMinX) lMinX = q.x; if (q.y < lMinY) lMinY = q.y; + if (q.x > lMaxX) lMaxX = q.x; if (q.y > lMaxY) lMaxY = q.y; + } + const lw = Math.max(lMaxX - lMinX, 1e-9); + const lh = Math.max(lMaxY - lMinY, 1e-9); + // Similar size both ways (re-outline of same glyph/hatch). + const similar = + pw <= lw * 1.5 && ph <= lh * 1.5 && + lw <= pw * 1.5 && lh <= ph * 1.5; + if (similar) { skipGlyphReoutline = true; break; } + } + if (skipGlyphReoutline) break; + } + } + if (!skipGlyphReoutline) { + const nSeg = closed ? pts.length : pts.length - 1; + // start/end/constant width (도곽 frame etc.) — mesh strip, scaled by INSERT + if (this._polyHasWidth(be, nSeg, scaleAcc)) { + this._widePolyMesh(pts, bulges, closed, be, ecol, scaleAcc, null); + } else { + if (bulges?.some(b => Math.abs(b) >= 1e-6)) this._bulgePolySegs(pts, bulges, closed, ecol, pushSeg); + else this._polylineSegs(pts, closed, 0, ecol, pushSeg); + this._fillClosedPolyIfUncovered(pts, bulges, ecol); + } + } + + } else if (bt === 'POLYLINE_2D' && entsByOwner) { + // Old-style 2D polyline inside a block: vertices are separate VERTEX_2D + // entities owned by the polyline (point/bulge at top level). + const kids = (entsByOwner.get(be.handle?.value?.toString(16)) ?? []) + .filter(v => (v.type||'').toUpperCase().startsWith('VERTEX') && v.point); + if (kids.length >= 2) { + const pts = kids.map(v => { const [nx, ny] = xf(v.point.x, v.point.y); return { x:nx, y:ny }; }); + const bulges = kids.map(v => v.bulge || 0); + const closed = (bd.flags & 1) === 1; + if (bulges.some(b => Math.abs(b) >= 1e-6)) this._bulgePolySegs(pts, bulges, closed, ecol, pushSeg); + else this._polylineSegs(pts, closed, 0, ecol, pushSeg); + } + + } else if (bt === 'HATCH') { + const paths = bd.paths ?? be.paths; + if (paths?.length) { + const solidFill = bd.solidFill ?? be.solidFill ?? false; + const tpaths = paths.map(p => ({ + ...p, + points: (p.points ?? []).map(pt => { const [nx, ny] = xf(pt.x, pt.y); return { x:nx, y:ny }; }), + })); + // Solid fill: draw fill only (CAD does not render hatch boundaries). + // Pattern hatch: draw boundary outline as a proxy (no pattern support). + if (solidFill) { + this._hatchFill(tpaths, ecol); + } else { + for (const tp of tpaths) { + if ((tp.points?.length ?? 0) < 2) continue; + if (tp.bulges?.some(b => Math.abs(b) >= 1e-6)) this._bulgePolySegs(tp.points, tp.bulges, true, ecol, pushSeg); + else this._polylineSegs(tp.points, true, 0, ecol, pushSeg); + } + } + } + + } else if (bt === 'SOLID') { + const crs = [bd.corner1||bd.pt1, bd.corner2||bd.pt2, + bd.corner3||bd.pt3, bd.corner4||bd.pt4].filter(Boolean); + if (crs.length >= 3) { + const mapped = crs.map(p => { const [nx,ny] = xf(p.x, p.y); return {x:nx, y:ny, z:p.z||0}; }); + this._solidMesh(mapped, ecol); + } + + } else if (bt === 'INSERT' && entsByOwner && depth < 8) { + // Nested block reference: recurse with composed transform. + const bhVal = be.blockHeaderHandle?.value ?? bd.blockHeaderHandle?.value; + if (bhVal != null) { + const hex = bhVal.toString(16); + const childEnts = entsByOwner.get(hex) ?? []; + if (childEnts.length) { + const nip = be.insertionPt ?? bd.insertionPt ?? bd.insertionPoint ?? { x:0, y:0 }; + const nsx = bd.scaleX ?? bd.scale?.x ?? be.scale?.x ?? 1; + const nsy = bd.scaleY ?? bd.scale?.y ?? be.scale?.y ?? 1; + const nrot = bd.rotation ?? be.rotation ?? 0; + this._insertEntities( + childEnts, + { insertionPoint: nip, xScale: nsx, yScale: nsy, rotation: nrot, basePoint: blockBase?.get(hex) }, + ecol, pushSeg, pendingTexts, + { entsByOwner, blockBase, parentXf: xf, scale: scaleAcc, rot: rotAcc, depth: depth + 1 } + ); + } + } + + } else if ((bt === 'TEXT' || bt === 'MTEXT' || bt === 'ATTRIB' || bt === 'ATTDEF') && pendingTexts) { + if ((bt === 'ATTRIB' || bt === 'ATTDEF') && ((bd.flags ?? 0) & 1)) continue; + const raw = be.text ?? bd.text ?? bd.textValue ?? bd.defaultValue ?? ''; + const text = stripMText(raw); + let ah, av, rot = (bd.rotationAngle ?? 0) + rotAcc; + if (bt === 'MTEXT') { + // MTEXT anchors via attachment point 1-9 (1-3 top / 4-6 mid / 7-9 + // bottom, columns L/C/R), not horiz/vertAlignment — the fallback + // 0/0 (left-baseline) pushed table cells to the top-right. + const ap = be.attachment ?? bd.attachment ?? be.attachmentPoint ?? bd.attachmentPoint ?? 5; + ah = ([1,4,7].includes(ap) ? 0 : [3,6,9].includes(ap) ? 2 : 1); + av = ([1,2,3].includes(ap) ? 3 : [7,8,9].includes(ap) ? 1 : 2); + const xd = be.xAxisDir ?? bd.xAxisDir; + if (xd) rot = Math.atan2(xd.y, xd.x) + rotAcc; + } else { + ah = bd.horizAlignment ?? be.horizAlignment ?? 0; + av = bd.vertAlignment ?? be.vertAlignment ?? 0; + } + const useAp = bt !== 'MTEXT' && (ah !== 0 || av !== 0); + const alignPt = be.alignmentPt ?? bd.alignmentPt; + const insertPt = be.insertionPt ?? bd.insertionPt ?? bd.insertionPoint; + const tpos = useAp + ? (alignPt ?? insertPt ?? {x:0,y:0}) + : (insertPt ?? {x:0,y:0}); + const [tx, ty] = xf(tpos.x, tpos.y); + const ht = (bd.textHeight ?? bd.height ?? be.textHeight ?? 2.5) * scaleAcc; + if (text) { + const entry = { text, pos:{x:tx, y:ty}, height:ht, rotation:rot, color: ecol, alignH:ah, alignV:av }; + if (bt === 'MTEXT') { + entry.linespacingFactor = be.linespacingFactor ?? bd.linespacingFactor ?? 1; + entry.rectWidth = (be.rectWidth ?? bd.rectWidth ?? 0) * scaleAcc; + entry.rectHeight = (be.rectHeight ?? bd.rectHeight ?? 0) * scaleAcc; + entry.bgFillFlags = be.bgFillFlags ?? bd.bgFillFlags ?? 0; + entry.bgScale = be.bgScale ?? bd.bgScale ?? 1.5; + entry.bgColorIndex = be.bgColorIndex ?? bd.bgColorIndex; + entry.bgColorRgb = be.bgColorRgb ?? bd.bgColorRgb; + } + pendingTexts.push(entry); + } + } + } catch { /* skip */ } + } + this._curLwPx = insertLwPx; // hand the INSERT's own weight back to the caller + } + + // Fallback dimension rendering from entity properties when no block geometry available + _renderDimFallback(e, d, type, color, pushSeg, pendingTexts, expand) { + const pt10 = e.pt10 ?? d.pt10; + const pt13 = e.pt13 ?? d.pt13; + const pt14 = e.pt14 ?? d.pt14; + const pt15 = e.pt15 ?? d.pt15; + const textMidPt = e.textMidPt ?? d.textMidPt; + const actualMeasurement = e.actualMeasurement ?? d.actualMeasurement ?? 0; + const textRot = e.textRot ?? d.textRot ?? 0; + const dimRot = e.dimRot ?? d.dimRot ?? 0; + const userText = e.userText ?? d.userText ?? ''; + + const fmtLen = (v) => Math.abs(v) < 1 ? v.toFixed(3) : v.toFixed(2); + + const drawLine = (a, b) => { + if (!a || !b) return; + pushSeg(a.x, a.y, b.x, b.y, 0, color); + expand(a.x, a.y); expand(b.x, b.y); + }; + const drawArc = (cx, cy, r, sa, ea) => { + this._arcSegs({ x:cx, y:cy }, r, sa, ea, 0, color, pushSeg); + }; + const arrow = (pt, rot, sz) => this._arrowMesh(pt, rot, sz, color); + const drawText = (pos, text, height, rot) => { + if (text && pos) pendingTexts.push({ text, pos, height, rotation: rot ?? 0, color, alignH: 1, alignV: 2 }); + }; + + switch (type) { + case 'DIMENSION': + case 'DIMENSION_LINEAR': + case 'DIMENSION_ALIGNED': { + if (!pt13 || !pt14 || !pt10) return; + let d1x, d1y, d2x, d2y; + if (type === 'DIMENSION_ALIGNED') { + const ddx = pt14.x-pt13.x, ddy = pt14.y-pt13.y; + const len = Math.hypot(ddx, ddy); + if (len < 1e-9) return; + const ux = ddx/len, uy = ddy/len; + const half = actualMeasurement / 2; + d1x = pt10.x - ux*half; d1y = pt10.y - uy*half; + d2x = pt10.x + ux*half; d2y = pt10.y + uy*half; + } else { + const isV = Math.abs(Math.sin(dimRot)) > 0.5; + const half = actualMeasurement / 2; + d1x = pt10.x - (isV ? 0 : half); d1y = pt10.y - (isV ? half : 0); + d2x = pt10.x + (isV ? 0 : half); d2y = pt10.y + (isV ? half : 0); + } + const arrowSz = Math.max(actualMeasurement * 0.04, 1); + drawLine({x:d1x,y:d1y}, {x:d2x,y:d2y}); + drawLine(pt13, {x:d1x,y:d1y}); + drawLine(pt14, {x:d2x,y:d2y}); + arrow({x:d1x,y:d1y,z:0}, Math.atan2(d1y-d2y, d1x-d2x), arrowSz); + arrow({x:d2x,y:d2y,z:0}, Math.atan2(d2y-d1y, d2x-d1x), arrowSz); + drawText(textMidPt ?? {x:(d1x+d2x)/2, y:(d1y+d2y)/2}, userText || fmtLen(actualMeasurement), Math.max(arrowSz*1.5,1), textRot); + break; + } + case 'DIMENSION_RADIUS': { + if (!pt10 || !pt15) return; + const arrowSzR = Math.max(actualMeasurement * 0.05, 0.5); + drawLine(pt10, pt15); + arrow({x:pt15.x,y:pt15.y,z:0}, Math.atan2(pt15.y-pt10.y, pt15.x-pt10.x), arrowSzR); + drawText(textMidPt ?? {x:(pt10.x+pt15.x)/2,y:(pt10.y+pt15.y)/2}, userText || `R${fmtLen(actualMeasurement)}`, Math.max(arrowSzR*2,1)); + break; + } + case 'DIMENSION_DIAMETER': { + if (!pt10 || !pt15) return; + const opp = {x:2*pt10.x-pt15.x, y:2*pt10.y-pt15.y}; + const arrowSzD = Math.max((actualMeasurement/2)*0.05, 0.5); + drawLine(pt15, opp); + arrow({x:pt15.x,y:pt15.y,z:0}, Math.atan2(pt15.y-pt10.y, pt15.x-pt10.x), arrowSzD); + arrow({x:opp.x,y:opp.y,z:0}, Math.atan2(opp.y-pt10.y, opp.x-pt10.x), arrowSzD); + drawText(textMidPt ?? pt10, userText || `Ø${fmtLen(actualMeasurement)}`, Math.max(arrowSzD*2,1)); + break; + } + case 'DIMENSION_ANG_3PT': + case 'DIMENSION_ANG_2LN': { + if (!pt10 || !pt13 || !pt14) return; + const vx = pt10.x, vy = pt10.y; + const rr = Math.hypot(pt13.x-vx, pt13.y-vy); + if (rr < 1e-9) return; + let sa = Math.atan2(pt13.y-vy, pt13.x-vx); + let ea = Math.atan2(pt14.y-vy, pt14.x-vx); + let sweep = ea - sa; + while (sweep < 0) sweep += Math.PI*2; + if (sweep > Math.PI) { [sa, ea] = [ea, sa]; sweep = Math.PI*2 - sweep; } + drawArc(vx, vy, rr, sa, sa+sweep); + const arrowSzA = Math.max(rr * 0.05, 0.5); + arrow({x:vx+Math.cos(sa)*rr, y:vy+Math.sin(sa)*rr, z:0}, sa-Math.PI/2, arrowSzA); + arrow({x:vx+Math.cos(sa+sweep)*rr, y:vy+Math.sin(sa+sweep)*rr, z:0}, (sa+sweep)+Math.PI/2, arrowSzA); + const midA = sa + sweep/2; + drawText(textMidPt ?? {x:vx+Math.cos(midA)*rr*1.2, y:vy+Math.sin(midA)*rr*1.2}, + userText || `${(actualMeasurement*180/Math.PI).toFixed(1)}°`, Math.max(rr*0.1,1)); + break; + } + case 'DIMENSION_ORDINATE': { + if (!pt10 || !pt13) return; + drawLine(pt10, pt13); + drawText(textMidPt ?? pt13, userText || (actualMeasurement?.toFixed(2) ?? ''), 2.5); + break; + } + } + } + + _onClick(e) { + // ignore clicks that were really a pan/zoom drag (>5px movement) + if (this._downXY) { + const dx = e.clientX - this._downXY[0], dy = e.clientY - this._downXY[1]; + if (Math.hypot(dx, dy) > 5) return; + } + const cam = this._camera; + const rect = this._renderer.domElement.getBoundingClientRect(); + // WCS for pick/measure (not UCS display frame) + const wcs = this._screenToWcs(e.clientX, e.clientY); + if (!wcs) return; + const wx = wcs.x, wy = wcs.y; + + if (this._measureActive) { this._doMeasure2D(wx, wy); return; } + + const thresh = CLICK_THRESHOLD_PX * (cam.right - cam.left) / (rect.width * (cam.zoom || 1)); + + // Geometric pick — raycast the actual rendered geometry, not a bounds proxy. + // 1) Nearest real line segment within the pixel threshold (lines, arcs, + // circles, polylines, block wires, dimension leaders/arrows). + let bestIdx = -1, bestDist = thresh; + const S = this._pickSegs, SM = this._pickSegMeta; + // Hidden layers stay in the buffers (only masked out of the draw index), so + // picking has to skip them explicitly. + const MH = this._hiddenLayers.size ? this._metaHidden : null; + for (let i = 0, j = 0; i < S.length; i += 4, j++) { + if (MH && MH[SM[j]]) continue; + const d = this._segDist(wx, wy, S[i], S[i + 1], S[i + 2], S[i + 3]); + if (d < bestDist) { bestDist = d; bestIdx = SM[j]; } + } + // 2) No line under the cursor → a filled region (hatch / SOLID / text box). + // Pick the smallest-area fill so text on top of a hatch wins. + if (bestIdx < 0) { + let bestArea = Infinity; + for (const f of this._pickFills) { + if (MH && MH[f.metaIdx]) continue; + if (this._fillHit(f.loops, wx, wy)) { + const a = this._loopArea(f.loops[0] || []); + if (a < bestArea) { bestArea = a; bestIdx = f.metaIdx; } + } + } + } + const bestMeta = bestIdx >= 0 ? this._entityMeta[bestIdx] : null; + this._highlight(bestMeta); + this._onSelectCb?.(bestMeta ? bestMeta.entity : null); + } + + // Distance from point (px,py) to segment (ax,ay)-(bx,by). + _segDist(px, py, ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay; + const len2 = dx * dx + dy * dy; + if (!len2) return Math.hypot(px - ax, py - ay); + const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2)); + return Math.hypot(px - (ax + t * dx), py - (ay + t * dy)); + } + + startMeasure(cb) { + this.stopMeasure(); + this._measureCb = cb; + this._measureActive = true; + } + + stopMeasure() { + this._measureActive = false; + this._measureCb = null; + this._measurePtA = null; + for (const m of this._measureMarkers) { + this._group.remove(m); + m.geometry?.dispose(); + m.material?.dispose(); + } + this._measureMarkers = []; + } + + _doMeasure2D(wx, wy) { + if (!this._measurePtA) { + this._measurePtA = { x: wx, y: wy }; + const mk = this._mkMeasureMarker(wx, wy); + this._measureMarkers.push(mk); + this._group.add(mk); + this._measureCb?.({ phase: 'a', ax: wx, ay: wy, az: 0, bx: wx, by: wy, bz: 0, d: 0 }); + } else { + const a = this._measurePtA; + const d = Math.hypot(wx - a.x, wy - a.y); + const mkB = this._mkMeasureMarker(wx, wy); + const ln = this._mkMeasureLine(a.x, a.y, wx, wy); + this._measureMarkers.push(mkB, ln); + this._group.add(mkB, ln); + this._measureCb?.({ phase: 'done', ax: a.x, ay: a.y, az: 0, bx: wx, by: wy, bz: 0, d }); + this._measurePtA = null; + } + } + + _mkMeasureMarker(x, y) { + const cam = this._camera; + const rect = this._renderer.domElement.getBoundingClientRect(); + const s = ((cam.right - cam.left) / (rect.width * (cam.zoom || 1))) * 8; + const pts = [ + new THREE.Vector3(x - s, y, 0), new THREE.Vector3(x + s, y, 0), + new THREE.Vector3(x, y - s, 0), new THREE.Vector3(x, y + s, 0), + ]; + const geo = new THREE.BufferGeometry().setFromPoints(pts); + geo.setIndex([0, 1, 2, 3]); + const mat = new THREE.LineBasicMaterial({ color: 0xe63030, depthTest: false }); + const m = new THREE.LineSegments(geo, mat); + m.renderOrder = 100; + return m; + } + + _mkMeasureLine(x1, y1, x2, y2) { + const geo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(x1, y1, 0), new THREE.Vector3(x2, y2, 0), + ]); + const mat = new THREE.LineBasicMaterial({ color: 0xe63030, depthTest: false }); + const m = new THREE.Line(geo, mat); + m.renderOrder = 100; + return m; + } + + /** Recolor the selected entity's line geometry to the accent color (deselect previous). */ + _highlight(meta) { + if (this._selMeta) this._paintMeta(this._selMeta, null); + this._selMeta = meta || null; + if (meta) this._paintMeta(meta, this._selColor); + } + + /** + * Paint every vertex an entity owns. Its segments are spread over the + * hairline batch plus one run per dash/lineweight bucket it touched + * (meta.spans), so all of them get recolored — or restored when rgb is null. + */ + _paintMeta(meta, rgb) { + const hair = this._hairRt; + if (hair && meta.colEnd > meta.colStart) this._paintSpan(hair, meta.colStart, meta.colEnd, rgb); + if (!meta.spans) return; + for (const s of meta.spans) { + const bk = s.bk; + if (bk.merged) { + if (hair) this._paintSpan(hair, s.start + bk.mergeOffset, s.end + bk.mergeOffset, rgb); + } else if (bk.rt) { + this._paintSpan(bk.rt, s.start, s.end, rgb); + } + } + } + + /** Write one color run: `rgb` to select, null to restore the original color. */ + _paintSpan(rt, start, end, rgb) { + if (rt.attr) { // plain vertex-color batch (hairline / thin dashes) + const arr = rt.attr.array, orig = rt.orig; + for (let i = start; i < end && i < arr.length; i += 3) { + if (rgb) { arr[i] = rgb.r; arr[i + 1] = rgb.g; arr[i + 2] = rgb.b; } + else { arr[i] = orig[i]; arr[i + 1] = orig[i + 1]; arr[i + 2] = orig[i + 2]; } + } + rt.attr.needsUpdate = true; + return; + } + // Fat batch: hiding a layer compacts the instance buffer, so segment s of + // the source colors may now live at a different slot (slotOf, -1 = hidden). + const arr = rt.colBuf?.array, orig = rt.colSrc, slotOf = rt.slotOf; + if (!arr || !orig) return; + for (let i = start; i < end && i < orig.length; i += 3) { + const slot = slotOf ? slotOf[(i / 6) | 0] : (i / 6) | 0; + if (slot < 0) continue; + const t = slot * 6 + (i % 6); + if (rgb) { arr[t] = rgb.r; arr[t + 1] = rgb.g; arr[t + 2] = rgb.b; } + else { arr[t] = orig[i]; arr[t + 1] = orig[i + 1]; arr[t + 2] = orig[i + 2]; } + } + rt.colBuf.needsUpdate = true; + } + + /** Selection highlight color (keeps in sync with the UI accent). */ + setAccent(hex) { + this._selColor = { + r: parseInt(hex.slice(1, 3), 16) / 255, + g: parseInt(hex.slice(3, 5), 16) / 255, + b: parseInt(hex.slice(5, 7), 16) / 255, + }; + if (this._selMeta) { const m = this._selMeta; this._selMeta = null; this._highlight(m); } + } + + /** Get currently selected CAD entity (or null). */ + getSelectedEntity() { + return this._selMeta ? this._selMeta.entity : null; + } + + /** Zoom extents to the currently selected CAD entity. */ + zoomToSelection() { + if (!this._selMeta) return; + const idx = this._entityMeta.indexOf(this._selMeta); + if (idx < 0) return; + const box = new THREE.Box3(); + const S = this._pickSegs, SM = this._pickSegMeta; + if (S && SM) { + for (let i = 0, j = 0; i < S.length; i += 4, j++) { + if (SM[j] === idx) { + box.expandByPoint(new THREE.Vector3(S[i], S[i + 1], 0)); + box.expandByPoint(new THREE.Vector3(S[i + 2], S[i + 3], 0)); + } + } + } + if (this._pickFills) { + for (const f of this._pickFills) { + if (f.metaIdx === idx) { + for (const loop of f.loops || []) { + for (const p of loop) { + box.expandByPoint(new THREE.Vector3(p.x, p.y, 0)); + } + } + } + } + } + if (box.isEmpty() && this._selMeta.entity) { + const e = this._selMeta.entity; + const d = e.data || e; + const pt = d.start || d.center || d.pos || d.insertionPoint || d.insertionPt; + if (pt) { + const pad = 10; + box.expandByPoint(new THREE.Vector3(pt.x - pad, pt.y - pad, 0)); + box.expandByPoint(new THREE.Vector3(pt.x + pad, pt.y + pad, 0)); + } + } + if (!box.isEmpty()) { + this._fit(box); + } + } + + _distToBounds(wx, wy, b) { + if (b.type === 'point') return Math.hypot(wx - b.cx, wy - b.cy); + if (b.type === 'circle') return Math.abs(Math.hypot(wx - b.cx, wy - b.cy) - b.r); + if (b.type === 'line') { + const dx = b.x2-b.x1, dy = b.y2-b.y1; + const len2 = dx*dx + dy*dy; + if (!len2) return Math.hypot(wx - b.x1, wy - b.y1); + const t = Math.max(0, Math.min(1, ((wx-b.x1)*dx + (wy-b.y1)*dy) / len2)); + return Math.hypot(wx - (b.x1+t*dx), wy - (b.y1+t*dy)); + } + return Infinity; + } + + /** + * Percentile-trim an AABB from samples so 1–2 far junk vertices (or a + * degenerate polyline) do not dominate Zoom Fit. Falls back to fullBox. + * @param {number[]} samples flat [x,y,x,y,…] + * @param {THREE.Box3} fullBox + */ + _trimmedContentBox(samples, fullBox) { + if (!fullBox || fullBox.isEmpty()) return null; + const n = samples ? (samples.length >> 1) : 0; + if (n < 32) return fullBox.clone(); + + const xs = new Float64Array(n); + const ys = new Float64Array(n); + let k = 0; + for (let i = 0; i + 1 < samples.length; i += 2) { + const x = samples[i], y = samples[i + 1]; + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + xs[k] = x; ys[k] = y; k++; + } + if (k < 32) return fullBox.clone(); + const xsl = xs.subarray(0, k); + const ysl = ys.subarray(0, k); + xsl.sort(); + ysl.sort(); + // 0.5% … 99.5% — drops a handful of extreme verts on ~100k+ samples + const lo = Math.max(0, Math.floor(k * 0.005)); + const hi = Math.min(k - 1, Math.floor(k * 0.995)); + let minX = xsl[lo], maxX = xsl[hi], minY = ysl[lo], maxY = ysl[hi]; + if (!(maxX > minX) || !(maxY > minY)) return fullBox.clone(); + + // Pad slightly so content is not flush to the view edge + const padX = Math.max((maxX - minX) * 0.02, 1); + const padY = Math.max((maxY - minY) * 0.02, 1); + // Trimming is X/Y only — carry the full box's Z through. Pinning Z to 0 here + // and then clamping to fullBox below collapsed the box to empty whenever the + // drawing sat entirely off the z=0 plane, which is exactly where viewport + // underlay geometry lives (VIEWPORT_Z), and an empty box makes _fit a no-op. + const box = new THREE.Box3( + new THREE.Vector3(minX - padX, minY - padY, fullBox.min.z), + new THREE.Vector3(maxX + padX, maxY + padY, fullBox.max.z), + ); + // Never expand beyond the true full box + box.min.max(fullBox.min); + box.max.min(fullBox.max); + return box; + } + + /** + * Zoom camera to a world AABB of visible content (camera-local frustum + pan). + * `box` must already reflect only currently drawn entities (space/layer filter). + * + * Civil model drawings are often extreme horizontal strips (aspect 50–200:1). + * Classic "contain" fit then letterboxes so the strip is only a few pixels tall + * (looks like a single line at the top/center). For such strips, fit so the + * **short axis fills the screen** (Y fills for wide strips); pan to see the rest. + */ + _fit(box) { + if (!box || box.isEmpty()) return; + if (this._is25D) { this._fit25(box); return; } + const c = box.getCenter(new THREE.Vector3()); + const size = box.getSize(new THREE.Vector3()); + const w = this._container.clientWidth || 800; + const h = this._container.clientHeight || 600; + const aspect = w / Math.max(h, 1); + const pad = 1.12; + const bw = Math.max(size.x, 1e-9); + const bh = Math.max(size.y, 1e-9); + const contentAspect = bw / bh; + + let vw, vh; + // Extreme strip: prefer filling the short side so geometry is readable. + // Threshold ~8× screen aspect (e.g. content 16:1 on a 2:1 window). + if (contentAspect > aspect * 8) { + // Very wide (plan corridor / bb-style double box): fill view height + vh = bh * pad; + vw = vh * aspect; + } else if (contentAspect < aspect / 8) { + // Very tall: fill view width + vw = bw * pad; + vh = vw / aspect; + } else { + // Normal sheet / roughly viewport-shaped content: classic contain + vw = bw * pad; + vh = bh * pad; + if (vw / vh > aspect) vh = vw / aspect; + else vw = vh * aspect; + } + + this._camera.zoom = 1; + this._camera.left = -vw / 2; this._camera.right = vw / 2; + this._camera.top = vh / 2; this._camera.bottom = -vh / 2; + const span = Math.max(vw, vh, 1000); + this._camera.near = -span; + this._camera.far = span; + this._camera.position.set(c.x, c.y, 100); + const a = -(this._viewTwist || 0); + this._camera.up.set(-Math.sin(a), Math.cos(a), 0); + this._controls.target.set(c.x, c.y, 0); + this._camera.lookAt(this._controls.target); + this._camera.updateProjectionMatrix(); + this._camera.updateMatrixWorld(true); + this._controls.update(); + } + + /** + * Zoom-extents for the tilted view. The box is sized by its 3D diagonal so the + * drawing stays framed at *any* orbit angle — re-fitting on every rotation + * would fight the user's drag. + */ + _fit25(box) { + const b = this._box25(box); + if (!b) return; + const c = b.getCenter(new THREE.Vector3()); + const diag = Math.max(b.getSize(new THREE.Vector3()).length(), 1e-6); + const w = this._container.clientWidth || 800; + const h = this._container.clientHeight || 600; + const aspect = w / Math.max(h, 1); + const need = diag * 1.1; + const vw = aspect >= 1 ? need * aspect : need; + const vh = aspect >= 1 ? need : need / aspect; + + // Entering 2.5D snaps to SE isometric (+X/−Y/+Z, AutoCAD's default view); + // a later Fit keeps whatever angle the user orbited to. + const dir = new THREE.Vector3(); + if (this._need25Reset) dir.set(1, -1, 1); + else dir.subVectors(this._camera.position, this._controls.target); + if (dir.lengthSq() < 1e-12) dir.set(1, -1, 1); + dir.normalize(); + this._need25Reset = false; + + const dist = diag * 2 + 1; // ortho: only anchors the clip slab, not the scale + this._camera.zoom = 1; + this._camera.left = -vw / 2; this._camera.right = vw / 2; + this._camera.top = vh / 2; this._camera.bottom = -vh / 2; + const span = dist + diag + 1; + this._camera.near = -span; + this._camera.far = span; + this._camera.up.set(0, 0, 1); + this._controls.target.copy(c); + this._camera.position.copy(c).addScaledVector(dir, dist); + this._camera.lookAt(this._controls.target); + this._camera.updateProjectionMatrix(); + this._camera.updateMatrixWorld(true); + this._controls.update(); + } + + _clear() { + for (let i = this._group.children.length - 1; i >= 0; i--) { + const o = this._group.children[i]; + o.geometry?.dispose(); + if (o.material) { o.material.map?.dispose(); o.material.dispose(); } + this._group.remove(o); + } + // Shared text textures were disposed via the sprites above (re-dispose is a + // no-op); drop the cache so the next load rasterizes fresh. + this._texCache?.clear(); + this._textGen++; // invalidate any in-flight async text flush + this._contentBox = null; + this._fullContentBox = null; + this._contentZ = null; + this._layerObjs.clear(); + this._layerBoxes.clear(); + this._layerSamples.clear(); + this._indexedBufs = []; + this._fatBufs = []; + this._layerNames = null; + this._metaHidden = null; + this._curLayer = null; + this._curLayerId = -1; + this._layerInfoCache = null; + this._fillXf = null; // a load that threw mid-viewport must not leak its transform + } + + _onResize() { + const w = this._container.clientWidth, h = this._container.clientHeight; + if (!w || !h) return; + this._renderer.setSize(w, h); + // Frustum planes are CAMERA-LOCAL (relative to camera.position), not world coords. + // Just preserve half-widths and re-adjust aspect — camera position tracks panning via OrbitControls. + const halfW = (this._camera.right - this._camera.left) / 2; + const halfH = halfW / (w / h); + this._camera.left = -halfW; this._camera.right = halfW; + this._camera.top = halfH; this._camera.bottom = -halfH; + this._camera.updateProjectionMatrix(); + // Fat lines size themselves against the canvas resolution — restate it. + for (const buf of this._fatBufs) this._sizeLineMaterial(buf.mesh.material); + } + + _animate() { + requestAnimationFrame(() => this._animate()); + this._controls.update(); + this._renderer.render(this._scene, this._camera); + } +} diff --git a/src/viewer2d/acadrust-dwg/acadrust_dwg.d.ts b/src/viewer2d/acadrust-dwg/acadrust_dwg.d.ts new file mode 100644 index 0000000..42effc3 --- /dev/null +++ b/src/viewer2d/acadrust-dwg/acadrust_dwg.d.ts @@ -0,0 +1,53 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function parse_dwg(bytes: Uint8Array): any; + +/** + * The same parseResult as [`parse_dwg`], returned as a JSON string for the + * caller to hand to `JSON.parse`. Prefer this on anything but small drawings. + * + * Materializing the whole result as a `serde_json::Value` is what makes big + * files unopenable. The tree costs ~150× the DWG on the heap (a 19 MB drawing + * peaks near 2 GB), and the wasm allocator's per-allocation cost rises with the + * live heap, so conversion goes quadratic — measured 352 s in wasm for work + * that takes 2.5 s natively. Converting one entity at a time and appending its + * JSON text keeps the live heap flat and the cost linear. + */ +export function parse_dwg_json(bytes: Uint8Array): string; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly parse_dwg: (a: number, b: number) => [number, number, number]; + readonly parse_dwg_json: (a: number, b: number) => [number, number, number, number]; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/src/viewer2d/acadrust-dwg/acadrust_dwg.js b/src/viewer2d/acadrust-dwg/acadrust_dwg.js new file mode 100644 index 0000000..a2ef7d2 --- /dev/null +++ b/src/viewer2d/acadrust-dwg/acadrust_dwg.js @@ -0,0 +1,320 @@ +/* @ts-self-types="./acadrust_dwg.d.ts" */ + +/** + * @param {Uint8Array} bytes + * @returns {any} + */ +export function parse_dwg(bytes) { + const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.parse_dwg(ptr0, len0); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); +} + +/** + * The same parseResult as [`parse_dwg`], returned as a JSON string for the + * caller to hand to `JSON.parse`. Prefer this on anything but small drawings. + * + * Materializing the whole result as a `serde_json::Value` is what makes big + * files unopenable. The tree costs ~150× the DWG on the heap (a 19 MB drawing + * peaks near 2 GB), and the wasm allocator's per-allocation cost rises with the + * live heap, so conversion goes quadratic — measured 352 s in wasm for work + * that takes 2.5 s natively. Converting one entity at a time and appending its + * JSON text keeps the live heap flat and the cost linear. + * @param {Uint8Array} bytes + * @returns {string} + */ +export function parse_dwg_json(bytes) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.parse_dwg_json(ptr0, len0); + var ptr2 = ret[0]; + var len2 = ret[1]; + if (ret[3]) { + ptr2 = 0; len2 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Error_92b29b0548f8b746: function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }, + __wbg_String_8564e559799eccda: function(arg0, arg1) { + const ret = String(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_string_ea5e6cc2e4141dfe: function(arg0) { + const ret = typeof(arg0) === 'string'; + return ret; + }, + __wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_new_32b398fb48b6d94a: function() { + const ret = new Array(); + return ret; + }, + __wbg_new_7796ffc7ed656783: function() { + const ret = new Map(); + return ret; + }, + __wbg_new_da52cf8fe3429cb2: function() { + const ret = new Object(); + return ret; + }, + __wbg_set_575dd786d51585f8: function(arg0, arg1, arg2) { + const ret = arg0.set(arg1, arg2); + return ret; + }, + __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { + arg0[arg1] = arg2; + }, + __wbg_set_8a16b38e4805b298: function(arg0, arg1, arg2) { + arg0[arg1 >>> 0] = arg2; + }, + __wbindgen_cast_0000000000000001: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0) { + // Cast intrinsic for `I64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0) { + // Cast intrinsic for `U64 -> Externref`. + const ret = BigInt.asUintN(64, arg0); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./acadrust_dwg_bg.js": import0, + }; +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasmInstance, wasm; +function __wbg_finalize_init(instance, module) { + wasmInstance = instance; + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('acadrust_dwg_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm b/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm new file mode 100644 index 0000000..36bd496 Binary files /dev/null and b/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm differ diff --git a/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm.d.ts b/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm.d.ts new file mode 100644 index 0000000..309dbd8 --- /dev/null +++ b/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm.d.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const parse_dwg: (a: number, b: number) => [number, number, number]; +export const parse_dwg_json: (a: number, b: number) => [number, number, number, number]; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/src/viewer2d/acadrust-dwg/package.json b/src/viewer2d/acadrust-dwg/package.json new file mode 100644 index 0000000..870cb90 --- /dev/null +++ b/src/viewer2d/acadrust-dwg/package.json @@ -0,0 +1,17 @@ +{ + "name": "dwg-wasm", + "type": "module", + "description": "WASM wrapper around acadrust (MPL-2.0, used unmodified) emitting the hmwebviewer parseResult shape", + "version": "0.1.0", + "license": "MIT", + "files": [ + "acadrust_dwg_bg.wasm", + "acadrust_dwg.js", + "acadrust_dwg.d.ts" + ], + "main": "acadrust_dwg.js", + "types": "acadrust_dwg.d.ts", + "sideEffects": [ + "./snippets/*" + ] +} diff --git a/src/viewer2d/acadrustParser.ts b/src/viewer2d/acadrustParser.ts new file mode 100644 index 0000000..7d534e6 --- /dev/null +++ b/src/viewer2d/acadrustParser.ts @@ -0,0 +1,28 @@ +/** + * acadrust DWG parser wrapper — loads the wasm built from rust/dwg-wasm + * (wasm-pack --target web, committed under ./acadrust-dwg). + */ +import init, { parse_dwg_json } from './acadrust-dwg/acadrust_dwg.js'; +import wasmUrl from './acadrust-dwg/acadrust_dwg_bg.wasm?url'; +import type { CadParseResult } from './dwgParser'; + +let ready: Promise | null = null; + +/** Initialize the acadrust WASM module once (idempotent). */ +export function initAcadrustParser(): Promise { + if (!ready) ready = init({ module_or_path: wasmUrl }).then(() => undefined); + return ready; +} + +/** + * Parse a DWG buffer. Requires initAcadrustParser() first. + * + * Goes through `parse_dwg_json` + `JSON.parse` rather than the `parse_dwg` + * export that hands back a JsValue. The latter keeps the entire result live on + * the wasm heap as it builds, and the allocator's cost grows with that heap — + * a 19 MB drawing took ~354 s and froze the tab. Streaming the result out as + * text keeps it linear: the same file now lands in ~2.5 s. + */ +export function parseDwgAcadrust(bytes: Uint8Array): CadParseResult { + return JSON.parse(parse_dwg_json(bytes)) as CadParseResult; +} diff --git a/src/viewer2d/aciColors.js b/src/viewer2d/aciColors.js new file mode 100644 index 0000000..cb01809 --- /dev/null +++ b/src/viewer2d/aciColors.js @@ -0,0 +1,64 @@ +/** + * AutoCAD Color Index (ACI) → hex RGB integer. + * Returns null for BYBLOCK (0) and BYLAYER (256) — caller uses layer/default color. + */ + +function hsl2hex(h, s, l) { + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r, g, b; + if (h < 60) { r = c; g = x; b = 0; } + else if (h < 120) { r = x; g = c; b = 0; } + else if (h < 180) { r = 0; g = c; b = x; } + else if (h < 240) { r = 0; g = x; b = c; } + else if (h < 300) { r = x; g = 0; b = c; } + else { r = c; g = 0; b = x; } + return Math.round((r + m) * 255) * 65536 + + Math.round((g + m) * 255) * 256 + + Math.round((b + m) * 255); +} + +// ACI 0-9: exact fixed colors +const FIXED = [ + null, // 0: BYBLOCK + 0xFF0000, // 1: red + 0xFFFF00, // 2: yellow + 0x00FF00, // 3: green + 0x00FFFF, // 4: cyan + 0x0000FF, // 5: blue + 0xFF00FF, // 6: magenta + 0xFFFFFF, // 7: white + 0x414141, // 8: dark gray + 0x808080, // 9: gray +]; + +// ACI 10-249: 24 hue groups × 10 saturation/lightness variants +const VARIANTS_SL = [ + [1.00, 0.50], // 0: full + [1.00, 0.28], // 1: dark + [0.65, 0.65], // 2: light pastel + [0.65, 0.38], // 3: pastel dark + [0.35, 0.80], // 4: pale + [0.35, 0.48], // 5: pale mid + [0.18, 0.68], // 6: faint light + [0.18, 0.38], // 7: faint dark + [0.00, 0.55], // 8: light gray + [0.00, 0.30], // 9: dark gray +]; + +export function aciToHex(aci, isDark = true) { + if (aci == null || aci === 0 || aci === 256) return null; + if (aci === 7) return isDark ? 0xFFFFFF : 0x000000; + if (aci <= 9) return FIXED[aci] ?? null; + if (aci >= 250 && aci <= 255) { + const v = Math.round(40 + (aci - 250) * 43); + return v * 65536 + v * 256 + v; + } + if (aci >= 10 && aci <= 249) { + const idx = aci - 10; + const [s, l] = VARIANTS_SL[idx % 10]; + return hsl2hex(Math.floor(idx / 10) * 15, s, l); + } + return 0xC9D1D9; +} diff --git a/src/viewer2d/cadSpaces.ts b/src/viewer2d/cadSpaces.ts new file mode 100644 index 0000000..f289a15 --- /dev/null +++ b/src/viewer2d/cadSpaces.ts @@ -0,0 +1,166 @@ +/** + * Model Space / Paper Space (Layout) discovery for CadParseResult. + * + * DWG entities are owned by a block header handle. System blocks + * *Model_Space / *Paper_Space (and *Paper_Space0…) are the two spaces + * AutoCAD switches between. Viewer2D must render only one at a time — + * drawing both stacks survey-model geometry on top of layout sheet + * geometry and produces the "all text piled in the center" glitch. + */ + +export type CadSpaceKind = 'model' | 'paper' | 'other'; + +export type CadSpace = { + /** Stable id for UI (hex handle string, same as ownerHandle.value.toString(16)). */ + id: string; + /** Display label, e.g. "Model" / "Layout" / raw block name. */ + label: string; + /** Raw block name from tables.blocks. */ + name: string; + /** Block handle as number when available. */ + handle: number | null; + /** Hex string matching entity ownerHandle.value.toString(16). */ + handleHex: string; + kind: CadSpaceKind; + entityCount: number; +}; + +function handleToHex(handle: unknown): string | null { + if (handle == null) return null; + if (typeof handle === 'object' && handle !== null && 'value' in handle) { + const v = (handle as { value?: unknown }).value; + if (typeof v === 'number' && Number.isFinite(v)) return v.toString(16); + if (typeof v === 'string' && v.length) { + const n = Number(v); + return Number.isFinite(n) ? n.toString(16) : v.toLowerCase(); + } + return null; + } + if (typeof handle === 'number' && Number.isFinite(handle)) return handle.toString(16); + if (typeof handle === 'string' && handle.length) { + const n = Number(handle); + return Number.isFinite(n) ? n.toString(16) : handle.toLowerCase(); + } + return null; +} + +function ownerHex(entity: { ownerHandle?: unknown }): string | null { + return handleToHex(entity.ownerHandle); +} + +function classifySpaceName(raw: string): CadSpaceKind | null { + const name = raw.toLowerCase().replace(/\*/g, '').trim(); + if (!name) return null; + if (name === 'model_space' || name === 'ms' || name.startsWith('model')) return 'model'; + if (name === 'paper_space' || name === 'ps' || name.startsWith('paper')) return 'paper'; + return null; +} + +function spaceLabel(kind: CadSpaceKind, name: string, paperIndex: number): string { + if (kind === 'model') return 'Model'; + if (kind === 'paper') { + // First paper space → "Layout"; further ones keep a disambiguator. + if (paperIndex <= 1) return 'Layout'; + return `Layout (${name.replace(/^\*/, '')})`; + } + return name; +} + +/** + * List model/paper space blocks that own at least zero entities. + * Always includes Model/Paper headers found in tables.blocks. + */ +export function listCadSpaces(result: { + entities?: unknown[]; + tables?: { blocks?: Array<{ name?: string; handle?: unknown }> }; +} | null | undefined): CadSpace[] { + const blocks = result?.tables?.blocks ?? []; + const entities = (result?.entities ?? []) as Array<{ ownerHandle?: unknown }>; + + const counts = new Map(); + for (const e of entities) { + const hex = ownerHex(e); + if (!hex) continue; + counts.set(hex, (counts.get(hex) || 0) + 1); + } + + const spaces: CadSpace[] = []; + let paperIndex = 0; + for (const b of blocks) { + const kind = classifySpaceName(b.name ?? ''); + if (!kind) continue; + const hex = handleToHex(b.handle); + if (!hex) continue; + if (kind === 'paper') paperIndex += 1; + const handleNum = + typeof b.handle === 'number' + ? b.handle + : typeof b.handle === 'object' && b.handle && 'value' in b.handle + ? Number((b.handle as { value: unknown }).value) + : null; + spaces.push({ + id: hex, + label: spaceLabel(kind, b.name ?? '', paperIndex), + name: b.name ?? '', + handle: Number.isFinite(handleNum as number) ? (handleNum as number) : null, + handleHex: hex, + kind, + entityCount: counts.get(hex) || 0, + }); + } + + // Stable order: Model first, then papers in table order. + spaces.sort((a, b) => { + if (a.kind !== b.kind) { + if (a.kind === 'model') return -1; + if (b.kind === 'model') return 1; + } + return 0; + }); + return spaces; +} + +/** + * Default tab when a file is opened: + * - Paper/Layout with entities if any (civil sheet drawings often leave Layout active) + * - else Model + * - else first listed space + */ +export function pickDefaultSpace(spaces: CadSpace[]): CadSpace | null { + if (!spaces.length) return null; + const paperWithContent = spaces.find((s) => s.kind === 'paper' && s.entityCount > 0); + if (paperWithContent) return paperWithContent; + const model = spaces.find((s) => s.kind === 'model'); + if (model) return model; + return spaces[0] ?? null; +} + +/** + * Owners allowed when filtering to one space: the space handle itself plus + * handles of entities that live under it (e.g. INSERT → ATTRIB children). + */ +export function buildAllowedOwnerHexes( + entities: Array<{ handle?: unknown; ownerHandle?: unknown }>, + activeSpaceHex: string, + rounds = 3, +): Set { + const allowed = new Set([activeSpaceHex]); + for (let pass = 0; pass < rounds; pass++) { + let grew = false; + for (const ent of entities) { + const oh = ownerHex(ent); + if (!oh || !allowed.has(oh)) continue; + const h = handleToHex(ent.handle); + if (h && !allowed.has(h)) { + allowed.add(h); + grew = true; + } + } + if (!grew) break; + } + return allowed; +} + +export function isSystemSpaceBlockName(name: string): boolean { + return classifySpaceName(name) != null; +} diff --git a/src/viewer2d/decodeDxf.ts b/src/viewer2d/decodeDxf.ts new file mode 100644 index 0000000..a487136 --- /dev/null +++ b/src/viewer2d/decodeDxf.ts @@ -0,0 +1,35 @@ +/** + * Decode a DXF buffer to text. + * + * Modern CAD exports are UTF-8 but often keep a legacy $DWGCODEPAGE + * (e.g. ANSI_949) that no longer matches the bytes — so the header is + * unreliable. Strategy: try UTF-8 strictly (self-validating); on failure + * fall back to the header's single-byte codepage. The DXF group/value + * structure is ASCII regardless, so this only affects TEXT/MTEXT strings. + */ +export function decodeDxf(buf: ArrayBuffer): string { + try { + return new TextDecoder('utf-8', { fatal: true }).decode(buf); + } catch { + /* not valid UTF-8 → legacy single-byte codepage */ + } + const head = new TextDecoder('latin1').decode( + new Uint8Array(buf, 0, Math.min(buf.byteLength, 4096)), + ); + const cp = head + .match(/\$DWGCODEPAGE[\s\S]*?\n\s*3\s*\n\s*([^\r\n]+)/i)?.[1] + ?.trim() + .toLowerCase(); + let label = 'windows-1252'; + if (cp) { + if (/949/.test(cp)) label = 'euc-kr'; + else if (/932/.test(cp)) label = 'shift_jis'; + else if (/936/.test(cp)) label = 'gbk'; + else if (/950/.test(cp)) label = 'big5'; + } + try { + return new TextDecoder(label).decode(buf); + } catch { + return new TextDecoder('utf-8').decode(buf); + } +} diff --git a/src/viewer2d/dwgParser.ts b/src/viewer2d/dwgParser.ts new file mode 100644 index 0000000..f8307bd --- /dev/null +++ b/src/viewer2d/dwgParser.ts @@ -0,0 +1,31 @@ +/** + * DWG parser — rust/dwg-wasm wrapper around the acadrust crate (MPL-2.0, + * vendored with a local read_mesh patch; see hmwebviewer rust/dwg-wasm). Emits + * the parseResult shape Viewer2D.load() consumes. Lazy-imported so the wasm is + * only fetched when a DWG is actually opened. + * + * History: until 2026-07-10 this module selected between acadrust and + * @horu2day/pure-cad-parser (?dwgparser= toggle). The old parser was removed + * after side-by-side validation (PROGRESS.md Phase 15). + * + * Post-process: fixDwgKoreanText repairs CP949→Latin-1 mojibake common in + * older Korean DWGs (see fixDwgKoreanText.ts). Unicode-correct drawings + * (e.g. BasicSample) are left unchanged. + */ + +import { fixDwgKoreanText } from './fixDwgKoreanText'; + +export type CadParseResult = { + version?: string; + vars?: Record; + entities?: unknown[]; + tables?: Record; + stats?: Record; +}; + +/** Parse a DWG buffer. Returns a parseResult for Viewer2D.load(). */ +export async function parseDwgBuffer(bytes: Uint8Array): Promise { + const { initAcadrustParser, parseDwgAcadrust } = await import('./acadrustParser'); + await initAcadrustParser(); + return fixDwgKoreanText(parseDwgAcadrust(bytes)); +} diff --git a/src/viewer2d/dxfAdapter.d.ts b/src/viewer2d/dxfAdapter.d.ts new file mode 100644 index 0000000..14f4365 --- /dev/null +++ b/src/viewer2d/dxfAdapter.d.ts @@ -0,0 +1,7 @@ +/** Convert dxf-parser output into the parseResult shape Viewer2D.load() consumes. */ +export function dxfToParseResult(dxf: unknown): { + version: string; + vars: Record; + entities: unknown[]; + tables: { blocks: unknown[]; layers: unknown[] }; +}; diff --git a/src/viewer2d/dxfAdapter.js b/src/viewer2d/dxfAdapter.js new file mode 100644 index 0000000..d631b72 --- /dev/null +++ b/src/viewer2d/dxfAdapter.js @@ -0,0 +1,160 @@ +/** + * dxfAdapter — convert dxf-parser (MIT) output into the parseResult shape that + * Viewer2D.load() consumes, so the existing renderer (bulge arcs, dimension + * block geometry, arrowhead blocks, text alignment) is reused unchanged for DXF. + * + * Why an adapter and not dxf-parser's handles directly: dxf-parser exposes the + * raw DXF handle graph where a BLOCK entity's handle differs from the BLOCK_RECORD + * handle its child entities are owned by, and INSERT/DIMENSION reference their + * block by NAME, not handle. Viewer2D matches blocks by handle. So we discard the + * incoherent handles and synthesize one consistent integer handle PER BLOCK NAME: + * - block table entry handle = {value: H} + * - every child entity ownerHandle = {value: H} + * - INSERT.blockHeaderHandle = {value: H of insert.name} + * - DIMENSION.blockHeaderHandle = {value: H of dim.block} + * Model-space entities are owned by the synthetic '*MODEL_SPACE' handle so + * Viewer2D's space-handle filter keeps them visible. + * + * Units: dxf-parser gives ARC/ELLIPSE angles in radians (pass through) but + * INSERT/TEXT/MTEXT rotation in DEGREES → converted to radians here. + * + * Not supported by dxf-parser 1.1.x: HATCH (no handler) → hatches are absent. + */ +const DEG2RAD = Math.PI / 180; + +const vec = (p) => (p ? { x: p.x, y: p.y, z: p.z || 0 } : p); + +/** Map one dxf-parser entity → Viewer2D field names. Returns a shallow copy. */ +function adaptEntity(e, nameToH) { + const o = { ...e }; + delete o.color; // dxf-parser's `color` is a 24-bit RGB int; let colorIndex/layer drive ACI + const t = (e.type || '').toUpperCase(); + + switch (t) { + case 'LINE': + if (e.vertices?.length >= 2) { o.start = vec(e.vertices[0]); o.end = vec(e.vertices[1]); } + break; + + case 'POLYLINE': + case 'LWPOLYLINE': + o.type = 'LWPOLYLINE'; + o.points = (e.vertices || []).map(vec); + o.bulges = (e.vertices || []).map((v) => v.bulge || 0); + o.closed = !!e.shape || ((e.flags || 0) & 1) === 1; + break; + + case 'ARC': + case 'CIRCLE': + o.center = vec(e.center); // radius + (radian) angles already match + break; + + case 'ELLIPSE': + o.center = vec(e.center); + o.majorAxis = vec(e.majorAxisEndPoint); + o.ratio = e.axisRatio; + o.startParam = e.startAngle; + o.endParam = e.endAngle; + break; + + case 'POINT': + o.position = vec(e.position); + break; + + case 'SOLID': + case '3DFACE': { + const p = e.points || []; + if (p[0]) o.corner1 = vec(p[0]); + if (p[1]) o.corner2 = vec(p[1]); + if (p[2]) o.corner3 = vec(p[2]); + if (p[3]) o.corner4 = vec(p[3]); + o.type = 'SOLID'; + break; + } + + case 'TEXT': + case 'ATTRIB': + case 'ATTDEF': + o.insertionPoint = vec(e.startPoint || e.position); + o.alignmentPt = vec(e.endPoint); + o.rotationAngle = (e.rotation || 0) * DEG2RAD; + o.horizAlignment = e.halign || 0; + o.vertAlignment = e.valign || 0; + o.textHeight = e.textHeight ?? e.height; + break; + + case 'MTEXT': + o.insertionPoint = vec(e.position); + o.textHeight = e.height ?? e.textHeight; + if (e.directionVector) o.xAxisDir = vec(e.directionVector); + else if (e.rotation != null) o.xAxisDir = { x: Math.cos(e.rotation * DEG2RAD), y: Math.sin(e.rotation * DEG2RAD) }; + break; + + case 'INSERT': { + o.insertionPoint = vec(e.position); + o.scaleX = e.xScale ?? 1; + o.scaleY = e.yScale ?? 1; + o.rotation = (e.rotation || 0) * DEG2RAD; + const h = nameToH.get(e.name); + if (h != null) o.blockHeaderHandle = { value: h }; + break; + } + + case 'DIMENSION': { + const h = nameToH.get(e.block); + if (h != null) o.blockHeaderHandle = { value: h }; + break; + } + + default: + break; + } + return o; +} + +/** + * Convert a full dxf-parser result → Viewer2D parseResult. + * @param {object} dxf output of `new DxfParser().parseSync(text)` + * @returns {{version:string, vars:object, entities:object[], tables:{blocks:object[], layers:object[]}}} + */ +export function dxfToParseResult(dxf) { + const blocks = dxf.blocks || {}; + const names = Object.keys(blocks); + const nameToH = new Map(); + names.forEach((n, i) => nameToH.set(n, i + 1)); // 1-based synthetic handles + + const msName = names.find((n) => /model_space/i.test(n)) ?? names.find((n) => /model/i.test(n)); + const msH = msName != null ? nameToH.get(msName) : 0; + + const entities = []; + // Model-space entities (top-level array) → owned by the synthetic *MODEL_SPACE handle. + for (const e of (dxf.entities || [])) { + const o = adaptEntity(e, nameToH); + o.ownerHandle = { value: msH }; + entities.push(o); + } + // Block-definition entities → owned by their block's synthetic handle. + for (const n of names) { + const H = nameToH.get(n); + for (const e of (blocks[n].entities || [])) { + const o = adaptEntity(e, nameToH); + o.ownerHandle = { value: H }; + entities.push(o); + } + } + + const blockTable = names.map((n) => ({ + name: n, + handle: { value: nameToH.get(n) }, + basePoint: vec(blocks[n].position) || { x: 0, y: 0, z: 0 }, + })); + + const layerSrc = dxf.tables?.layer?.layers || {}; + const layerTable = Object.values(layerSrc).map((l) => ({ name: l.name, colorIndex: l.colorIndex })); + + return { + version: 'dxf', + vars: dxf.header || {}, + entities, + tables: { blocks: blockTable, layers: layerTable }, + }; +} diff --git a/src/viewer2d/dxfHatchHandler.d.ts b/src/viewer2d/dxfHatchHandler.d.ts new file mode 100644 index 0000000..7a1eb37 --- /dev/null +++ b/src/viewer2d/dxfHatchHandler.d.ts @@ -0,0 +1,5 @@ +/** HATCH entity handler for dxf-parser's registerEntityHandler(). */ +export default class HatchHandler { + ForEntityName: string; + parseEntity(scanner: unknown, curr: { code: number; value: unknown }): unknown; +} diff --git a/src/viewer2d/dxfHatchHandler.js b/src/viewer2d/dxfHatchHandler.js new file mode 100644 index 0000000..6135d11 --- /dev/null +++ b/src/viewer2d/dxfHatchHandler.js @@ -0,0 +1,190 @@ +/** + * HATCH entity handler for dxf-parser (which ships none — dxf-parser 1.1.x drops + * HATCH silently). Registered via the library's public `registerEntityHandler()`, + * so dxf-parser itself stays unmodified (MIT-clean). + * + * Emits the shape Viewer2D's HATCH renderer consumes: + * entity = { type:'HATCH', solidFill:boolean, paths:[{ points:[{x,y}], bulges:[number] }] } + * Viewer2D fills solid hatches (with island/hole nesting) and draws boundary + * outlines; bulge arcs on polyline boundaries are honored. + * + * Boundary kinds handled: + * - polyline boundary (92 flag bit 1) → vertices 10/20 (+ bulge 42) + * - edge boundary: LINE(72=1), ARC(72=2), → tessellated to points + * ELLIPSE(72=3), SPLINE(72=4, control-point polyline) + * + * Pattern fill (non-SOLID) is not expanded into hatch lines — Viewer2D draws the + * boundary outline as a proxy, matching its DWG behavior. + */ +const DEG2RAD = Math.PI / 180; +const ARC_STEPS = 48; + +function tessArc(cx, cy, r, a0, a1, ccw, out) { + let span = a1 - a0; + if (ccw) { while (span <= 0) span += Math.PI * 2; } + else { while (span >= 0) span -= Math.PI * 2; } + const steps = Math.max(2, Math.ceil(Math.abs(span) / (Math.PI * 2) * ARC_STEPS)); + for (let i = 0; i <= steps; i++) { + const a = a0 + (span * i) / steps; + out.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) }); + } +} + +export default class HatchHandler { + constructor() { + this.ForEntityName = 'HATCH'; + } + + parseEntity(scanner, curr) { + const entity = { type: curr.value, paths: [], solidFill: false }; + curr = scanner.next(); + while (!scanner.isEOF()) { + if (curr.code === 0) break; + switch (curr.code) { + case 70: // solid fill flag + entity.solidFill = curr.value !== 0; + break; + case 2: // pattern name + entity.patternName = curr.value; + break; + case 8: // layer + entity.layer = curr.value; + break; + case 62: // color index + entity.colorIndex = curr.value; + break; + case 91: { // number of boundary paths → parse them by count + const n = curr.value; + let g = scanner.next(); + for (let i = 0; i < n && !scanner.isEOF(); i++) { + g = this._readPath(scanner, entity, g); + } + curr = g; // g is the group following the last path's data + continue; // curr already advanced — skip the trailing next() + } + default: + break; + } + curr = scanner.next(); + } + return entity; + } + + /** Parse one boundary path starting at its 92 group; return the next group after it. */ + _readPath(scanner, entity, g) { + // g.code should be 92 (path type flag); if not, skip until we find it. + while (!scanner.isEOF() && g.code !== 92) g = scanner.next(); + const flag = g.value | 0; + const isPolyline = (flag & 2) !== 0; + const path = { points: [], bulges: [] }; + let cur = scanner.next(); + + if (isPolyline) { + let hasBulge = false; + while (cur.code === 72 || cur.code === 73) { + if (cur.code === 72) hasBulge = cur.value !== 0; + cur = scanner.next(); + } + let numVerts = 0; + if (cur.code === 93) { numVerts = cur.value; cur = scanner.next(); } + for (let v = 0; v < numVerts && !scanner.isEOF(); v++) { + let x = 0, y = 0, b = 0; + if (cur.code === 10) { x = cur.value; cur = scanner.next(); } + if (cur.code === 20) { y = cur.value; cur = scanner.next(); } + if (hasBulge && cur.code === 42) { b = cur.value; cur = scanner.next(); } + path.points.push({ x, y }); + path.bulges.push(b); + } + } else { + let numEdges = 0; + if (cur.code === 93) { numEdges = cur.value; cur = scanner.next(); } + for (let e = 0; e < numEdges && !scanner.isEOF(); e++) { + let edgeType = 0; + if (cur.code === 72) { edgeType = cur.value; cur = scanner.next(); } + cur = this._readEdge(scanner, path, edgeType, cur); + } + } + + // Consume the per-path trailing "source boundary objects" block (97 + 330×count). + if (cur.code === 97) { + const srcN = cur.value | 0; + cur = scanner.next(); + for (let s = 0; s < srcN && !scanner.isEOF(); s++) { + if (cur.code === 330) cur = scanner.next(); + else break; + } + } + if (path.points.length >= 2) entity.paths.push(path); + return cur; + } + + /** Tessellate one edge into path.points; return the next group. */ + _readEdge(scanner, path, edgeType, cur) { + if (edgeType === 1) { + // LINE: 10/20 start, 11/21 end + let sx = 0, sy = 0, ex = 0, ey = 0; + if (cur.code === 10) { sx = cur.value; cur = scanner.next(); } + if (cur.code === 20) { sy = cur.value; cur = scanner.next(); } + if (cur.code === 11) { ex = cur.value; cur = scanner.next(); } + if (cur.code === 21) { ey = cur.value; cur = scanner.next(); } + path.points.push({ x: sx, y: sy }); + path.points.push({ x: ex, y: ey }); // last edge's end closes the loop + } else if (edgeType === 2) { + // ARC: 10/20 center, 40 radius, 50 start deg, 51 end deg, 73 ccw + let cx = 0, cy = 0, r = 0, a0 = 0, a1 = 0, ccw = 1; + if (cur.code === 10) { cx = cur.value; cur = scanner.next(); } + if (cur.code === 20) { cy = cur.value; cur = scanner.next(); } + if (cur.code === 40) { r = cur.value; cur = scanner.next(); } + if (cur.code === 50) { a0 = cur.value * DEG2RAD; cur = scanner.next(); } + if (cur.code === 51) { a1 = cur.value * DEG2RAD; cur = scanner.next(); } + if (cur.code === 73) { ccw = cur.value; cur = scanner.next(); } + tessArc(cx, cy, r, a0, a1, ccw !== 0, path.points); + } else if (edgeType === 3) { + // ELLIPSE: 10/20 center, 11/21 major-axis endpoint, 40 ratio, 50/51 param deg, 73 ccw + let cx = 0, cy = 0, mx = 1, my = 0, ratio = 1, a0 = 0, a1 = Math.PI * 2, ccw = 1; + if (cur.code === 10) { cx = cur.value; cur = scanner.next(); } + if (cur.code === 20) { cy = cur.value; cur = scanner.next(); } + if (cur.code === 11) { mx = cur.value; cur = scanner.next(); } + if (cur.code === 21) { my = cur.value; cur = scanner.next(); } + if (cur.code === 40) { ratio = cur.value; cur = scanner.next(); } + if (cur.code === 50) { a0 = cur.value * DEG2RAD; cur = scanner.next(); } + if (cur.code === 51) { a1 = cur.value * DEG2RAD; cur = scanner.next(); } + if (cur.code === 73) { ccw = cur.value; cur = scanner.next(); } + const major = Math.hypot(mx, my); + const minor = major * ratio; + const rot = Math.atan2(my, mx); + let span = a1 - a0; + if (ccw !== 0) { while (span <= 0) span += Math.PI * 2; } + else { while (span >= 0) span -= Math.PI * 2; } + const steps = Math.max(4, Math.ceil(Math.abs(span) / (Math.PI * 2) * 72)); + for (let i = 0; i <= steps; i++) { + const t = a0 + (span * i) / steps; + const ex = major * Math.cos(t), ey = minor * Math.sin(t); + path.points.push({ x: cx + ex * Math.cos(rot) - ey * Math.sin(rot), y: cy + ex * Math.sin(rot) + ey * Math.cos(rot) }); + } + } else if (edgeType === 4) { + // SPLINE: approximate by its control points (10/20). Skip knots/weights. + // 94 degree, 73 rational, 74 periodic, 95 numKnots, 96 numCtrl, 40 knots, 10/20 ctrl, 42 weights + let numKnots = 0, numCtrl = 0; + const readInt = () => { const v = cur.value | 0; cur = scanner.next(); return v; }; + while (!scanner.isEOF()) { + if (cur.code === 95) { numKnots = readInt(); } + else if (cur.code === 96) { numCtrl = readInt(); } + else if (cur.code === 94 || cur.code === 73 || cur.code === 74) { cur = scanner.next(); } + else break; + } + for (let k = 0; k < numKnots && cur.code === 40 && !scanner.isEOF(); k++) cur = scanner.next(); + for (let c = 0; c < numCtrl && !scanner.isEOF(); c++) { + let x = 0, y = 0; + if (cur.code === 10) { x = cur.value; cur = scanner.next(); } + if (cur.code === 20) { y = cur.value; cur = scanner.next(); } + if (cur.code === 42) cur = scanner.next(); // weight + path.points.push({ x, y }); + } + } else { + // Unknown edge type: advance one group to avoid stalling. + cur = scanner.next(); + } + return cur; + } +} diff --git a/src/viewer2d/ext2d.ts b/src/viewer2d/ext2d.ts new file mode 100644 index 0000000..3fad55a --- /dev/null +++ b/src/viewer2d/ext2d.ts @@ -0,0 +1,25 @@ +/** + * 2D file-format detection for the 2D+3D merge dispatch. + * + * 2D = CAD drawings (dwg/dxf) → rendered by Viewer2D via the WASM parser. + * Everything else (glb/gltf/obj/fbx/dae/ifc/ply/3ds/3dm) is 3D → ThreeDViewer. + * Kept dependency-free so both the dropzone and the controller can import it + * without pulling in either viewer. + */ +export const DWG_EXT = /\.dwg$/i; +export const DXF_EXT = /\.dxf$/i; + +/** True if the name is a binary DWG (parsed by the WASM parser). */ +export function isDwg(name: string): boolean { + return DWG_EXT.test(name); +} + +/** True if the name is a text DXF (parsed by the dxf-parser JS lib). */ +export function isDxf(name: string): boolean { + return DXF_EXT.test(name); +} + +/** True if the file/URL name is a 2D CAD drawing (dwg or dxf) → Viewer2D. */ +export function is2D(name: string): boolean { + return DWG_EXT.test(name) || DXF_EXT.test(name); +} diff --git a/src/viewer2d/fixDwgKoreanText.ts b/src/viewer2d/fixDwgKoreanText.ts new file mode 100644 index 0000000..78cea1f --- /dev/null +++ b/src/viewer2d/fixDwgKoreanText.ts @@ -0,0 +1,106 @@ +/** + * Repair Korean text that acadrust emits as Latin-1 mojibake of CP949/EUC-KR. + * + * Older Korean DWGs store strings in code page 949. When those bytes are + * decoded as ISO-8859-1/Windows-1252, Hangul becomes garbled Latin + * (e.g. "½Ã°ø½Ã" instead of "시공시"). Unicode-correct drawings + * (already containing Hangul syllables) are left unchanged. + * + * Apply after parse_dwg, before Viewer2D.load(). + */ + +const HANGUL_SYLLABLE = /[\uAC00-\uD7A3]/; +const HANGUL_JAMO = /[\u1100-\u11FF\u3130-\u318F]/; + +/** Labels tried in order; first supported decoder wins. */ +const KOREAN_LABELS = ['cp949', 'windows-949', 'euc-kr'] as const; + +let koreanDecoder: TextDecoder | null | undefined; + +function getKoreanDecoder(): TextDecoder | null { + if (koreanDecoder !== undefined) return koreanDecoder; + for (const label of KOREAN_LABELS) { + try { + koreanDecoder = new TextDecoder(label); + return koreanDecoder; + } catch { + /* label not available in this runtime */ + } + } + koreanDecoder = null; + return null; +} + +/** + * If `s` looks like CP949 bytes misread as Latin-1, return the Hangul + * form; otherwise return `s` unchanged. + */ +export function fixCp949Mojibake(s: string): string { + if (!s) return s; + // Already real Hangul → leave alone (e.g. BasicSample.dwg). + if (HANGUL_SYLLABLE.test(s) || HANGUL_JAMO.test(s)) return s; + + let hasHigh = false; + const bytes = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) { + const code = s.charCodeAt(i); + if (code > 255) return s; // true Unicode beyond Latin-1 + if (code > 127) hasHigh = true; + bytes[i] = code; + } + if (!hasHigh) return s; // pure ASCII + + const decoder = getKoreanDecoder(); + if (!decoder) return s; + + let fixed: string; + try { + fixed = decoder.decode(bytes); + } catch { + return s; + } + + // Accept only when we gain Hangul and the decode is clean. + if ( + (HANGUL_SYLLABLE.test(fixed) || HANGUL_JAMO.test(fixed)) && + !fixed.includes('\uFFFD') + ) { + return fixed; + } + return s; +} + +/** + * Deep-walk a CadParseResult (or any JSON-like tree) and fix string fields + * in place. Returns the same object for chaining. + */ +export function fixDwgKoreanText(root: T): T { + if (root == null || typeof root !== 'object') return root; + const seen = new WeakSet(); + + const walk = (value: unknown): void => { + if (value == null || typeof value !== 'object') return; + if (seen.has(value as object)) return; + seen.add(value as object); + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const item = value[i]; + if (typeof item === 'string') value[i] = fixCp949Mojibake(item); + else walk(item); + } + return; + } + + const obj = value as Record; + for (const key of Object.keys(obj)) { + if (key === 'imageDataUrl') continue; + const item = obj[key]; + if (typeof item === 'string') obj[key] = fixCp949Mojibake(item); + else walk(item); + } + }; + + walk(root); + return root; +} diff --git a/src/viewer2d/linData.ts b/src/viewer2d/linData.ts new file mode 100644 index 0000000..ecaed8e --- /dev/null +++ b/src/viewer2d/linData.ts @@ -0,0 +1,3 @@ +// Auto-generated standard LIN file definitions for Icad.lin and KOSDIC.lin +export const ICAD_LIN = "; IntelliCAD Linetypes\r\n;\r\n; icad.lin\r\n; Release 1.0 \r\n; \r\n; .............................................................................\r\n;\r\n; Copyright (c) 1998 by Visio Corporation. All rights reserved.\r\n;\r\n; The Software is subject to the license agreement that accompanies \r\n; or is included with the Software, which specifies the permitted \r\n; and prohibited uses of the Software. Any unauthorized duplication \r\n; or use of Visio Corporation Software, in whole or in part, in print, \r\n; or in any other storage and retrieval system is prohibited.\r\n; \r\n; To the maximum extent permitted by applicable law, Visio Corporation\r\n; and its suppliers disclaim any and all warranties and conditions,\r\n; either express or implied, including, without limitation, implied\r\n; warranties of merchantability, fitness for a particular purpose,\r\n; title, and non-infringement, and those arising out of usage of trade\r\n; or course of dealing, concerning these materials. These materials\r\n; are provided \"as is\" without warranty of any kind.\r\n; .............................................................................\r\n\r\n\r\n\r\n*BORDER,__ __ . __ __ . __ __ . __ __ . __ __ . __ __ . __ __ . __ __ . \r\nA,0.50,-0.250,0.50,-0.250,0,-0.250\r\n*BORDER2,__.__.__.__.__.__.__.__.__.__.__.__.__.__.__.__.__.__.__.__.__ \r\nA,0.25,-0.125,0.25,-0.125,0,-0.125\r\n*BORDERX2,____ ____ . ____ ____ . ____ ____ . ____ ____ . \r\nA,1.0,-0.5,1.0,-0.5,0,-0.5\r\n\r\n*CENTER,____ _ ____ _ ____ _ ____ _ ____ _ ____ _ ____ _ ____ _ ____ _\r\nA,1.250,-0.25,0.25,-0.25\r\n*CENTER2,___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ _\r\nA,0.75,-0.1250,0.1250,-0.125\r\n*CENTERX2,________ __ ________ __ ________ __ ________ __ ____\r\nA,2.5,-0.50,0.50,-0.50\r\n\r\n*DASHDOT,__ . __ . __ . __ . __ . __ . __ . __ . __ . __ . __ . __ . _\r\nA,0.50,-0.250,0,-0.250\r\n*DASHDOT2,_._._._._._._._._._._._._._._._._._._._._._._._._._._._._._.\r\nA,0.25,-0.125,0,-0.125\r\n*DASHDOTX2,____ . ____ . ____ . ____ . ____ . ____ . ____ \r\nA,1.0,-0.5,0,-0.5\r\n\r\n*DASHED,__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __\r\nA,0.50,-0.250\r\n*DASHED2,_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.250,-0.1250\r\n*DASHEDX2,____ ____ ____ ____ ____ ____ ____ ____ ____ ____ \r\nA,1.0,-0.5\r\n\r\n*DIVIDE,____ . . ____ . . ____ . . ____ . . ____ . . ____ . . ____ . .\r\nA,0.50,-0.25,0,-0.25,0,-0.25\r\n*DIVIDE2,__..__..__..__..__..__..__..__..__..__..__..__..__..__..__..__..\r\nA,0.25,-0.125,0,-0.125,0,-0.125\r\n*DIVIDEX2,________ . . ________ . . ________ . . ________ . .\r\nA,1.0,-0.5,0,-0.5,0,-0.50\r\n\r\n*DOT,. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\r\nA,0,-0.25\r\n*DOT2,................................................................\r\nA,0,-0.125\r\n*DOTX2,. . . . . . . . . . . . . . . . . . . . . \r\nA,0,-0.5\r\n\r\n*HIDDEN,__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ \r\nA,0.250,-0.1250\r\n*HIDDEN2,_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.125,-0.06250\r\n*HIDDENX2,____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ \r\nA,0.5,-0.25\r\n\r\n*PHANTOM,______ __ __ ______ __ __ ______ __ __ ______ __ __\r\nA,1.25,-0.25,0.250,-0.25,0.25,-0.25\r\n*PHANTOM2,___ _ _ ___ _ _ ___ _ _ ___ _ _ ___ _ _ ___ _ _ ___ _ _ ___ _ _\r\nA,0.625,-0.125,0.1250,-0.125,0.125,-0.1250\r\n*PHANTOMX2,____________ ____ ____ ____________ ____ ____ \r\nA,2.5,-0.5,0.5,-0.5,0.50,-0.5\r\n\r\n*PHANTOM4,___ _ _ ___ _ _ (1/2 of Phantom2)__ _ _ ___ _ _ ___ _ _ ___ _ _\r\nA,0.3125,-0.0625,0.0625,-0.0625,0.0625,-0.0625\r\n\r\n*PHANTOM8,___ _ _ ___ _ _ (1/2 of Phantom4)__ _ _ ___ _ _ ___ _ _ ___ _ _\r\nA,0.15625,-0.03125,0.03125,-0.03125,0.03125,-0.03125\r\n\r\n*BORDER4,__.__.__.__.__.__._ (1/2 of Border2) _.__.__.__.__.__.__.__.__ \r\nA,0.125,-0.0625,0.125,-0.0625,0,-0.0625\r\n\r\n*CENTER4,___ _ ___ _ ___ _ __ (1/2 of Center2)__ _ ___ _ ___ _ ___ _ _\r\nA,0.375,-0.0625,0.0625,-0.0625\r\n*CENTER8,___ _ ___ _ ___ _ __ (1/2 of Center4)__ _ ___ _ ___ _ ___ _ _\r\nA,0.1875,-0.03125,0.03125,-0.03125\r\n\r\n*CENTERF,_____ _ _____ (Finer short dashes than Center) _____\r\nA,1.25,-0.125,0.125,-0.125\r\n*CENTERF2,___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ _\r\nA,0.75,-0.0625,0.0625,-0.0625\r\n;; Auto-Arch additions for finer linetypes.\r\n*CENTERF4,___ _ ___ _ ___ _ __ (1/2 of CenterF2)__ _ ___ _ ___ _ ___ _ _\r\nA,0.375,-0.03125,0.03125,-0.03125\r\n*CENTERF8,___ _ ___ _ ___ _ __ (1/2 of CenterF4)__ _ ___ _ ___ _ ___ _ _\r\nA,0.1875,-0.0015625,0.0015625,-0.0015625\r\n\r\n*DASHED4,_ _ _ _ _ _ _ _ (1/2 of Dashed2)_ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.125,-0.0625\r\n*DASHED8,_ _ _ _ _ _ _ _ (1/2 of Dashed4)_ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.0625,-0.03125\r\n\r\n*DEMOx2,____ ____ ____ ____ ____\r\nA,0.250,-0.5\r\n*DEMO,__ __ __ __ __ __ __ __ __ __ __\r\nA,0.125,-0.25\r\n*DEMO2,_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.0625,-0.125\r\n*DEMO4,_ _ _ _ _ _ (1/2 of Demo2) _ _ _ _ _ _ _ _ _ _ _\r\nA,0.0625,-0.125\r\n*DEMO8,_ _ _ _ _ _ (1/2 of Demo4) _ _ _ _ _ _ _ _ _ _ _\r\nA,0.03125,-0.0625\r\n\r\n*INVISIBLE,. (Dot at endpoints only) .\r\nA,0,-1000\r\n\r\n*DOT4,...................(1/2 of Dot2)................................\r\nA,0,-0.06125\r\n*DOT8,...................(1/2 of Dot4)................................\r\nA,0,-0.03125\r\n*DOT16,..................(1/2 of Dot8)................................\r\nA,0,-0.0015625\r\n\r\n*HIDDEN4,_ _ _ _ _ _ _ _ (1/2 of Hidden2)_ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.0625,-0.03125\r\n*HIDDEN8,_ _ _ _ _ _ _ _ (1/2 of Hidden4)_ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.03125,-0.015\r\n\r\n*JIS_02_0.7, Hot Water Supply __ __ __ __ __ __ __\r\nA,0.8,-0.5\r\n*JIS_02_1.0, HIDDEN 0.8 - - - - - - - - - -\r\nA,0.8,-0.8\r\n*JIS_02_1.2, HIDDEN01.25 - - - - - - - - - -\r\nA,1.26,-1.26\r\n*JIS_02_2.0, HIDDEN02 __ __ __ __ __ __ __ __ __ __ __ \r\nA,1.99,-0.99\r\n*JIS_02_4.0, HIDDEN04 ____ ____ ____ ____ ____ ____ ____ \r\nA,3.999,-0.999\r\n*JIS_08_11, ___ _ ___ _ ___ _ ___ _ ___ _ ___ _ ___\r\nA,10.9999,-0.499,0.499,-0.499\r\n*JIS_08_15, ____ _ ____ _ ____ _ ____ _ ____ _ ____\r\nA,14.999,-0.499,0.499,-0.499\r\n*JIS_08_25, ________ __ ________ __ ________ __\r\nA,24.999,-1,0.8,-1\r\n*JIS_08_37, _____________ __ _____________ __ _____________ __ \r\nA,36.999,-1.0,0.8,-1\r\n*JIS_08_50, , ___________________ __ ___________________ __ \r\nA,49.999,-1.0,0.8,-1\r\n*JIS_09_08, _____ _ _ _____ _ _ _____ _ _ _____ _ _ _____ _ _ \r\nA,7.999,-0.5,0.5,-0.5,0.5,-0.5\r\n*JIS_09_15, _________ _ _ _________ _ _ _________ _ _ _________ _ _ \r\nA,14.999,-0.999,0.499,-0.999,0.499,-0.999\r\n*JIS_09_29, _____________ _ _ _____________ _ _ _____________ _ _ \r\nA,28.999,-1.499,0.999,-1.499,0.999,-1.499\r\n*JIS_09_50, _______________________ __ __ _______________________\r\nA,49.999,-1.499,0.999,-1.499,0.999,-1.499\r\n\r\n\r\n\r\n*CONTOUR,______ ______ ______ ______ ______ ______ ______ ______\r\nA,0.375,-0.09375\r\n*CONTOUR2,____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ \r\nA,0.25,-0.0625\r\n*COLDWR,___ _ ____ _ _____ _ _____ _ _____ _ ____\r\nA,0.25,-0.075,0.075,-0.075\r\n*HOTWR,___ _ _ ___ _ _ ___ _ _ ___ _ _ ___ _ _ ___\r\nA,0.25,-0.075,0.075,-0.075,0.075,-0.075\r\n*HCW,_____ _ _ _ _____ _ _ _ _____ _ _ _ _____\r\nA,1,-0.125,0.125,-0.125,0.125,-0.125,0.125,-0.125\r\n*BOUNDARY,___________ __ __ ___________ __ __ ______ \r\nA,1.75,-0.125,0.125,-0.125,0.125,-0.125\r\n*BOUNDARY2,______ _ _ ______ _ _ ______ _ _ ______ _ _ ______\r\nA,1,-0.0625,0.0625,-0.0625,0.0625,-0.0625\r\n*STREAM,_________ . . ___________ . . ___________\r\nA,1,-0.125,0,-0.125,0,-0.125 \r\n*PNEUM,__ ___ ___ ___ ___ ___ ___ ___ ___ ___ __\r\nA,0.2,-0.1,0.2,-0.1\r\n*WASTE,__ __ __ __ __ __ __ __ __ __ __ __ __ __\r\nA,0.25,-0.125\r\n*2DASHES,____ _ _ _____ _ _ ____ _ _ ____ _ _ ____\r\nA,0.75,-0.25,0.1,-0.25,0.1,-0.25\r\n*EWIREUP,__ . __ . __ . __ . __ . __ . __ . __ .__\r\nA,0.25,-.075,0,-.075\r\n*EWCELL,___ ___ . ___ ___ . __ __ . __ __ . __ __\r\nA,0.25,-0.075,0.25,-.075,0,-.075\r\n\r\n*EVENDASH,-- -- -- -- -- -- -- -- -- -- --\r\nA,0.125,-0.125\r\n\r\n;; Additional Plumbing linetypes\r\n*DCW,Domestic Cold Water ___ _ ______________ _ ____\r\nA,1.25,-0.06,0.08,-0.06\r\n*DCW2,Domestic Cold Water __ _ ______ _ _____ _ ____\r\nA,0.625,-0.03,0.04,-0.03\r\n*DCWX2,Domestic Cold Water ________ ___ __________\r\nA,2.5,-0.12,0.16,-0.12\r\n*DHW,Domestic Hot Water ___ _ _ ___________ _ _ ____\r\nA,1.25,-0.06,0.08,-0.06,0.08,-0.06\r\n*DHW2,Domestic Hot Water __ _ _ _____ _ _ ____ _ _ __\r\nA,0.625,-0.03,0.04,-0.03,0.04,-0.03\r\n*DHWX2,Domestic Hot Water _____ ___ ___ ___ _____\r\nA,2.5,-0.12,0.16,-0.12,0.16,-0.12\r\n*DHWR,Domestic Hot Wtr Return _____ __ __ __ ________\r\nA,1.25,-0.06,0.08,-0.06,0.08,-0.06,0.08,-0.06\r\n*DHWR2,Domestic Hot Wtr Return ___ _ _ _ ____________\r\nA,0.625,-0.03,0.04,-0.03,0.04,-0.03,0.04,-0.03\r\n*DHWRX2,Domestic Hot Wtr Return ____ ___ ____ ____ ____\r\nA,2.5,-0.12,0.16,-0.12,0.16,-0.12,0.16,-0.12\r\n*VENT,Sanitary Vent Pipe __ __ __ __ __ __ __ __ __ __\r\nA,0.16,-0.08\r\n*VENT2,Sanitary Vent Pipe _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.08,-0.04\r\n*VENTX2,Sanitary Vent Pipe ____ ____ ____ ___\r\nA,0.32,-0.16\r\n*COND,Steam Condensate __ __ __ __ __ __ __ __ __ __ __\r\nA,0.16,-0.08\r\n*COND2,Steam Condensate _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\nA,0.08,-0.04\r\n*CONDX2,Steam Condensate ____ ____ ____ ____\r\nA,0.32,-0.16\r\n\r\n;;; ISO LINTYPES\r\n*DaL013,02b013 Dashed line 0.13mm\r\nA,1.56,-0.39\r\n*DaSpL013,03b013 Dashed space line 0.13mm\r\nA,1.56,-2.34\r\n*LDaDoL013,04b013 Long dashed dotted line 0.13mm \r\nA,3.12,-0.39,0.065,-0.39\r\n*LDaDbDoL,05b013 Long dash double dotted line 0.13mm \r\nA,3.12,-0.39,0.065,-0.39,0.065,-0.39\r\n*LDaTrDoL,06b013 Long dashed triplicate dotted line 0.13mm \r\nA,3.12,-0.39,0.065,-0.39,0.065,-0.39,0.065,-0.39\r\n*DoL013,07b013 Dotted line\r\nA,0.065,-0.39\r\n*LDaShDaL013,08b013 Long dashed short dashed line 0.13mm \r\nA,3.12,-0.39,0.78,-0.39\r\n*09b013,Long dashed double-short-dashed line 0.13mm \r\nA,3.12,-0.39,0.78,-0.39,0.78,-0.39\r\n*10b013,Dashed dotted line 0.13mm \r\nA,1.56,-0.39,0.065,-0.39\r\n*11b013,Double-dashed dotted line 0.13mm \r\nA,1.56,-0.39,1.56,-0.39,0.065,-0.39\r\n*12b013,Dashed double-dotted line 0.13mm \r\nA,1.56,-0.39,0.065,-0.39,0.065,-0.39\r\n*13b013,Double-dashed double-dotted line 0.13mm \r\nA,1.56,-0.39,1.56,-0.39,0.065,-0.39,0.065,-0.39\r\n*14b013,Dashed triplicate-dotted line 0.13mm \r\nA,1.56,-0.39,0.065,-0.39,0.065,-0.39,0.065,-0.39\r\n*15b013,Double-dashed triplicate-dotted line 0.13mm \r\nA,1.56,-0.39,1.56,-0.39,0.065,-0.39,0.065,-0.39,0.065,-0.39\r\n*02b018,Dashed line 0.18mm \r\nA,2.16,-0.54\r\n*03b018,Dashed space line 0.18mm \r\nA,2.16,-3.24\r\n*04b018,Long dashed dotted line 0.18mm \r\nA,4.32,-0.54,0.09,-0.54\r\n*05b018,Long dash double dotted line 0.18mm \r\nA,4.32,-0.54,0.09,-0.54,0.09,-0.54\r\n*06b018,Long dashed triplicate dotted line 0.18mm \r\nA,4.32,-0.54,0.09,-0.54,0.09,-0.54,0.09,-0.54\r\n*07b018,Lotted line\r\nA,0.09,-0.54\r\n*08b018,Long dashed short dashed line 0.18mm \r\nA,4.32,-0.54,1.08,-0.54\r\n*09b018,Long dashed double-short-dashed line 0.18mm \r\nA,4.32,-0.54,1.08,-0.54,1.08,-0.54\r\n*10b018,Dashed dotted line 0.18mm \r\nA,2.16,-0.54,0.09,-0.54\r\n*11b018,Double-dashed dotted line 0.18mm \r\nA,2.16,-0.54,2.16,-0.54,.09,-0.54\r\n*12b018,Dashed double-dotted line 0.18mm \r\nA,2.16,-0.54,0.09,-0.54,0.09,-0.54\r\n*13b018,Double-dashed double-dotted line 0.18mm \r\nA,2.16,-0.54,2.16,-0.54,0.09,-0.54,0.09,-0.54\r\n*14b018,Dashed triplicate-dotted line 0.18mm \r\nA,2.16,-0.54,0.09,-0.54,0.09,-0.54,0.09,-0.54\r\n*15b018,Double-dashed triplicate-dotted line 0.18mm \r\nA,2.16,-0.54,2.16,-0.54,0.09,-0.54,0.09,-0.54,0.09,-0.54\r\n*02b025,Dashed Line 0.25mm \r\nA,3,-0.75\r\n*03b025,Dashed space line 0.25mm \r\nA,3,-4.5\r\n*04b025,Long dashed dotted line 0.25mm \r\nA,6,-0.75,0.125,-0.75\r\n*05b025,Long dash double dotted line 0.25mm \r\nA,6,-0.75,0.125,-0.75,0.125,-0.75\r\n*06b025,Long dashed triplicate dotted line 0.25mm \r\nA,6,-0.75,0.125,-0.75,0.125,-0.75,0.125,-0.75\r\n*07b025,Dotted line 0.25 mm \r\nA,0.125,-0.75\r\n*08b025,Long dashed short dashed line 0.25mm \r\nA,6,-0.75,1.5,-0.75\r\n*09b025,Long dashed double-short-dashed line 0.25mm \r\nA,6,-0.75,1.5,-0.75,1.5,-0.75\r\n*10b025,Dashed dotted line 0.25mm \r\nA,3,-0.75,0.125,-0.75\r\n*11b025,Double-dashed dotted line 0.25mm \r\nA,3,-0.75,3,-0.75,0.125,-0.75\r\n*12b025,Dashed double-dotted line 0.25mm \r\nA,3,-0.75,0.125,-0.75,0.125,-0.75\r\n*13b025,Double-dashed double-dotted line 0.25mm \r\nA,3,-0.75,3,-0.75,0.125,-0.75,0.125,-0.75\r\n*14b025,Dashed triplicate-dotted line 0.25mm \r\nA,3,-0.75,0.125,-0.75,0.125,-0.75,0.125,-0.75\r\n*15b025,Double-dashed triplicate-dotted line 0.25mm \r\nA,3,-0.75,3,-0.75,0.125,-0.75,0.125,-0.75,0.125,-0.75\r\n*02b035,Dashed line 0.35mm \r\nA,4.2,-1.05\r\n*03b035,Dashed space line 0.35mm \r\nA,4.2,-6.3\r\n*04b035,Long dashed dotted line 0.35mm \r\nA,8.4,-1.05,0.175,-1.05\r\n*05b035,Long dash double dotted line 0.35mm \r\nA,8.4,-1.05,0.175,-1.05,0.175,-1.05\r\n*06b035,Long dashed triplicate dotted line 0.35mm \r\nA,8.4,-1.05,.175,-1.05,.175,-1.05,0.175,-1.05\r\n*07b035,Dotted line 0.35mm \r\nA,.175,-1.05\r\n*08b035,Long dashed short dashed line 0.35mm \r\nA,8.4,-1.05,2.1,-1.05\r\n*09b035,Long dashed double-short-dashed line 0.35mm \r\nA,8.4,-1.05,2.1,-1.05,2.1,-1.05\r\n*10b035,Lashed dotted line 0.35mm \r\nA,4.2,-1.05,0.175,-1.05\r\n*11b035,Double-dashed dotted line 0.35mm \r\nA,4.2,-1.05,4.2,-1.05,.175,-1.05\r\n*12b035,Dashed double-dotted line 0.35mm \r\nA,4.2,-1.05,.175,-1.05,.175,-1.05\r\n*13b035,Double-dashed double-dotted line 0.35mm \r\nA,4.2,-1.05,4.2,-1.05,.175,-1.05,.175,-1.05\r\n*14b035,Dashed triplicate-dotted line 0.35mm \r\nA,4.2,-1.05,.175,-1.05,.175,-1.05,.175,-1.05\r\n*15b035,Double-dashed triplicate-dotted line 0.35mm \r\nA,4.2,-1.05,4.2,-1.05,.175,-1.05,.175,-1.05,.175,-1.05\r\n*02b05,Dashed line 0.5mm \r\nA,6,-1.5\r\n*03b05,Dashed space line 0.5mm \r\nA,6,-9\r\n*04b05,Long dashed dotted line 0.5mm \r\nA,12,-1.5,0.25,-1.5\r\n*05b05,Long dash double dotted line 0.5mm \r\nA,12,-1.5,0.25,-1.5,0.25,-1.5\r\n*06b05,Long dashed triplicate dotted line 0.5mm \r\nA,12,-1.5,0.25,-1.5,.25,-1.5,0.25,-1.5\r\n*07b05,Dotted line 0.5mm \r\nA,0.25,-1.5\r\n*08b05,Long dashed short dashed line 0.5mm \r\nA,12,-1.5,3,-1.5\r\n*09b05,Long dashed double-short-dashed line 0.5mm \r\nA,12,-1.5,3,-1.5,3,-1.5\r\n*10b05,Dashed dotted line 0.5mm \r\nA,6,-1.5,0.25,-1.5\r\n*11b05,Double-dashed dotted line 0.5mm \r\nA,6,-1.5,6,-1.5,0.25,-1.5\r\n*12b05,Dashed double-dotted line 0.5mm \r\nA,6,-1.5,.25,-1.5,0.25,-1.5\r\n*13b05,Double-dashed double-dotted line 0.5mm \r\nA,6,-1.5,6,-1.5,0.25,-1.5,0.25,-1.5\r\n*14b05,Dashed triplicate-dotted line 0.5mm \r\nA,6,-1.5,0.25,-1.5,0.25,-1.5,0.25,-1.5\r\n*15b05,Double-dashed triplicate-dotted line 0.5mm \r\nA,6,-1.5,6,-1.5,0.25,-1.5,0.25,-1.5,0.25,-1.5\r\n*02b07,Dashed Line 0.7mm \r\nA,8.4,-2.1\r\n*03b07,Dashed space line 0.7mm \r\nA,8.4,-12.6\r\n*04b07,Long dashed dotted line 0.7mm \r\nA,16.8,-2.1,0.35,-2.1\r\n*05b07,Long dash double dotted line 0.7mm \r\nA,16.8,-2.1,0.35,-2.1,0.35,-2.1\r\n*06b07,Long dashed triplicate dotted line 0.7mm \r\nA,16.8,-2.1,0.35,-2.1,0.35,-2.1,0.35,-2.1\r\n*07b07,Dotted line 0.7mm \r\nA,0.35,-2.1\r\n*08b07,Long dashed short dashed line 0.7mm \r\nA,16.8,-2.1,4.2,-2.1\r\n*09b07,Long dashed double-short-dashed line 0.7mm \r\nA,16.8,-2.1,4.2,-2.1,4.2,-2.1\r\n*10b07,Dashed dotted line 0.7mm \r\nA,8.4,-2.1,0.35,-2.1\r\n*11b07,Double-dashed dotted line 0.7mm \r\nA,8.4,-2.1,8.4,-2.1,0.35,-2.1\r\n*12b07,Dashed double-dotted line 0.7mm \r\nA,8.4,-2.1,0.35,-2.1,0.35,-2.1\r\n*13b07,Double-dashed double-dotted line 0.7mm \r\nA,8.4,-2.1,8.4,-2.1,0.35,-2.1,0.35,-2.1\r\n*14b07,Dashed triplicate-dotted line 0.7mm \r\nA,8.4,-2.1,0.35,-2.1,0.35,-2.1,0.35,-2.1\r\n*15b07,Double-dashed triplicate-dotted line 0.7mm \r\nA,8.4,-2.1,8.4,-2.1,0.35,-2.1,0.35,-2.1,0.35,-2.1\r\n*02b1,Dashed Line 1mm \r\nA,12,-3\r\n*03b1,Dashed space line 1mm \r\nA,12,-18\r\n*04b1,Long dashed dotted line 1mm \r\nA,24,-3,.5,-3\r\n*05b1,Long dash double dotted line 1mm \r\nA,24,-3,.5,-3,.5,-3\r\n*06b1,Long dashed triplicate dotted line 1mm \r\nA,24,-3,.5,-3,.5,-3,.5,-3\r\n*07b1,Dotted line\r\nA,.5,-3\r\n*08b1,Long dashed short dashed line 1mm \r\nA,24,-3,6,-3\r\n*09b1,Long dashed double-short-dashed line 1mm \r\nA,24,-3,6,-3,6,-3\r\n*10b1,Dashed dotted line 1mm \r\nA,12,-3,.5,-3\r\n*11b1,Double-dashed dotted line 1mm \r\nA,12,-3,12,-3,0.5,-3\r\n*12b1,Dashed double-dotted line 1mm \r\nA,12,-3,0.5,-3,0.5,-3\r\n*13b1,Double-dashed double-dotted line 1mm \r\nA,12,-3,12,-3,0.5,-3,0.5,-3\r\n*14b1,Dashed triplicate-dotted line 1mm \r\nA,12,-3,0.5,-3,0.5,-3,0.5,-3\r\n*15b1,Double-dashed triplicate-dotted line 1mm \r\nA,12,-3,12,-3,0.5,-3,.5,-3,0.5,-3\r\n*02b14,Dashed Line 1.4mm \r\nA,16.8,-4.2\r\n*03b14,Dashed space line 1.4mm \r\nA,16.8,25.2\r\n*04b14,Long dashed dotted line 1.4mm \r\nA,33.6,-4.2,.7,-4.2\r\n*05b14,Long dash double dotted line 1.4mm \r\nA,33.6,-4.2,.7,-4.2,.7,-4.2\r\n*06b14,Long dashed triplicate dotted line 1.4mm \r\nA,33.6,-4.2,.7,-4.2,.7,-4.2,.7,-4.2\r\n*07b14,Dotted line 1.4mm \r\nA,.7,-4.2\r\n*08b14,Long dashed short dashed line 1.4mm \r\nA,33.6,-4.2,8.4,-4.2\r\n*09b14,Long dashed double-short-dashed line 1.4mm \r\nA,33.6,-4.2,8.4,-4.2,8.4,-4.2\r\n*10b14,Dashed dotted line 1.4mm \r\nA,16.8,-4.2,.7,-4.2\r\n*11b14,Double-dashed dotted line 1.4mm \r\nA,16.8,-4.2,16.8,-4.2,.7,-4.2\r\n*12b14,Dashed double-dotted line 1.4mm \r\nA,16.8,-4.2,.7,-4.2,.7,-4.2\r\n*13b14,Double-dashed double-dotted line 1.4mm \r\nA,16.8,-4.2,16.8,-4.2,.7,-4.2,.7,-4.2\r\n*14b14,Dashed triplicate-dotted line 1.4mm \r\nA,16.8,-4.2,.7,-4.2,.7,-4.2,.7,-4.2\r\n*15b14,Double-dashed triplicate-dotted line 1.4mm \r\nA,16.8,-4.2,16.8,-4.2,.7,-4.2,.7,-4.2,.7,-4.2\r\n*02b2,Dashed Line 2mm \r\nA,24,-6\r\n*03b2,Dashed space line 2mm \r\nA,24,-36\r\n*04b2,Long dashed dotted line 2mm \r\nA,48,-6,1,-6\r\n*05b2,Long dash double dotted line 2mm \r\nA,48,-6,1,-6,1,-6\r\n*06b2,Long dashed triplicate dotted line 2mm \r\nA,48,-6,1,-6,1,-6,1,-6\r\n*07b2,Dotted line 2mm \r\nA,1,-6\r\n*08b2,Long dashed short dashed line 2mm \r\nA,48,-6,12,-6\r\n*09b2,Long dashed double-short-dashed line 2mm \r\nA,48,-6,12,-6,12,-6\r\n*10b2,Dashed dotted line 2mm \r\nA,24,-6,1,-6\r\n*11b2,Double-dashed dotted line 2mm \r\nA,24,-6,24,-6,1,-6\r\n*12b2,Dashed double-dotted line 2mm \r\nA,24,-6,1,-6,1,-6\r\n*13b2,Double-dashed double-dotted line 2mm \r\nA,24,-6,24,-6,1,-6,1,-6\r\n*14b2,Dashed triplicate-dotted line 2mm \r\nA,24,-6,1,-6,1,-6,1,-6\r\n*15b2,Double-dashed triplicate-dotted line 2mm \r\nA,24,-6,24,-6,1,-6,1,-6,1,-6\r\n;; Complex linetypes using standard style\r\n*FENCELINE1,Fenceline_circle ----0-----0----0-----0----0-----0--\r\nA,.25,-.1,[CIRC1,ltypeshp.shx,x=-.1,s=.1],-.1,.1\r\n*FENCELINE2,Fenceline square ----[]-----[]----[]-----[]----[]---\r\nA,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1\r\n*TRACKS,Tracks -|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-\r\nA,.15,[TRACK1,ltypeshp.shx,s=.25],.15\r\n*MAP,Map line ----MAP----MAP----MAP----MAP----MAP---- \r\nA,.5,-.2,[\"MAP\",STANDARD,S=.1,R=0.0,X=-0.1,Y=-.05],-.25\r\n*BATTING,Batting SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS\r\nA,.0001,-.1,[BAT,ltypeshp.shx,x=-.1,s=.1],-.2,[BAT,ltypeshp.shx,r=180,x=.1,s=.1],-.1\r\n*ZIGZAG,Zig zag /\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\r\nA,.0001,-.2,[ZIG,ltypeshp.shx,x=-.2,s=.2],-.4,[ZIG,ltypeshp.shx,r=180,x=.2,s=.2],-.2\r\n*HOT_WATER_SUPPLY,Hot water supply ---- HW ---- HW ---- HW ----\r\nA,12.7,-5.08,[\"HW\",STANDARD,S=2.54,R=0.0,X=-2.54,Y=-1.27],-5.08\r\n*GAS_LINE,Gas line ----GAS----GAS----GAS----GAS----GAS----GAS--\r\nA,.5,-.2,[\"GAS\",STANDARD,S=.1,U=0.0,X=-0.1,Y=-.05],-.25\r\n*SMILEYS,Smiley line ---- :^) ---- 8) ---- :^) ----\r\nA,12.7,-5.08,[\":^)\",STANDARD,S=2.54,R=0.0,X=-2.54,Y=-1.27],-5.08,12.7,-5.08,[\"8)\",STANDARD,S=2.54,R=0.0,X=-2.54,Y=-1.27],-5.08\r\n\r\n;------------Slope--------------\r\n*SLOPE,1B 1S |-|-|-|-|-|-|-|-|-|-|-|-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=0.52,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE-D, 1B 1S Double |-|-|-|-|-|-|-|-|-|-|-|-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=1.04,R=0.0,X=0,Y=1.04],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE2, 1B 2S ||-||-||-||-||-||-||-||-||-||-||-||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=0.52,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE2-D, 1B 2S Double ||-||-||-||-||-||-||-||-||-||-||-||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=1.04,R=0.0,X=0,Y=1.04],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE3, 1B 3S |||-|||-|||-|||-|||-|||-|||-|||-|||-|||-|||-|||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=0.52,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE3-D, 1B 3S Double |||-|||-|||-|||-|||-|||-|||-|||-|||-|||-|||-|||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=1.04,R=0.0,X=0,Y=1.04],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE4, 1B 4S ||||-||||-||||-||||-||||-||||-||||-||||-||||-||||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=0.52,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE4-D, 1B 4S Double ||||-||||-||||-||||-||||-||||-||||-||||-||||-||||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=1.04,R=0.0,X=0,Y=1.04],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE5, 1B 5S |||||-|||||-|||||-|||||-|||||-|||||-|||||-|||||-|||||-|||||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=0.52,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n*SLOPE5-D, 1B 5S Double |||||-|||||-|||||-|||||-|||||-|||||-|||||-|||||-|||||-|||||-\r\nA,0.13,[TRACK1,ltypeshp.shx,S=1.04,R=0.0,X=0,Y=1.04],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.52],0.26,[TRACK1,ltypeshp.shx,S=0.26,R=0.0,X=0,Y=0.26],0.13\r\n\r\n;------------Alphabets--------------\r\n*Letter A, ---- A ---- A ---- A ---- A ---- A ----\r\nA,1.0,-0.5,[\"A\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter B, ---- B ---- B ---- B ---- B ---- B ----\r\nA,1.0,-0.5,[\"B\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter C,---- C ---- C ---- C ---- C ---- C ----\r\nA,1.0,-0.5,[\"C\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter D, ---- D ---- D ---- D ---- D ---- D ----\r\nA,1.0,-0.5,[\"D\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter E, ---- E ---- E ---- E ---- E ---- E ----\r\nA,1.0,-0.5,[\"E\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter F, ---- F ---- F ---- F ---- F ---- F ----\r\nA,1.0,-0.5,[\"F\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter G, ---- G ---- G ---- G ---- G ---- G ----\r\nA,1.0,-0.5,[\"G\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter H, ---- H ---- H ---- H ---- H ---- H ----\r\nA,1.0,-0.5,[\"H\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter I, ---- I ---- I ---- I ---- I ---- I ----\r\nA,1.0,-0.5,[\"I\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter J, ---- J ---- J ---- J ---- J ---- J ----\r\nA,1.0,-0.5,[\"J\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter K, ---- K ---- K ---- K ---- K ---- K ----\r\nA,1.0,-0.5,[\"K\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter L, ---- L ---- L ---- L ---- L ---- L ----\r\nA,1.0,-0.5,[\"L\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter M, ---- M ---- M ---- M ---- M ---- M ----\r\nA,1.0,-0.5,[\"M\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter N, ---- N ---- N ---- N ---- N ---- N ----\r\nA,1.0,-0.5,[\"N\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter O, ---- O ---- O ---- O ---- O ---- O ----\r\nA,1.0,-0.5,[\"O\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter P, ---- P ---- P ---- P ---- P ---- P ----\r\nA,1.0,-0.5,[\"P\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter Q, ---- Q ---- Q ---- Q ---- Q ---- Q ----\r\nA,1.0,-0.5,[\"Q\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter R, ---- R ---- R ---- R ---- R ---- R ----\r\nA,1.0,-0.5,[\"R\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter S, ---- S ---- S ---- S ---- S ---- S ----\r\nA,1.0,-0.5,[\"S\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter T, ---- T ---- T ---- T ---- T ---- T ----\r\nA,1.0,-0.5,[\"T\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter U, ---- U ---- U ---- U ---- U ---- U ----\r\nA,1.0,-0.5,[\"U\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter V, ---- V ---- V ---- V ---- V ---- V ----\r\nA,1.0,-0.5,[\"V\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter W, ---- W ---- W ---- W ---- W ---- W ----\r\nA,1.0,-0.5,[\"W\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter X, ---- X ---- X ---- X ---- X ---- X ----\r\nA,1.0,-0.5,[\"X\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter Y, ---- Y ---- Y ---- Y ---- Y ---- Y ----\r\nA,1.0,-0.5,[\"Y\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Letter Z, ---- Z ---- Z ---- Z ---- Z ---- Z ----\r\nA,1.0,-0.5,[\"Z\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n\r\n;------------Numericals--------------\r\n*Numbers 0, ---- 0 ---- 0 ---- 0 ---- 0 ---- 0 ----\r\nA,1.0,-0.5,[\"0\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 1, ---- 1 ---- 1 ---- 1 ---- 1 ---- 1 ----\r\nA,1.0,-0.5,[\"1\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 2, ---- 2 ---- 2 ---- 2 ---- 2 ---- 2 ----\r\nA,1.0,-0.5,[\"2\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 3, ---- 3 ---- 3 ---- 3 ---- 3 ---- 3 ----\r\nA,1.0,-0.5,[\"3\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 4, ---- 4 ---- 4 ---- 4 ---- 4 ---- 4 ----\r\nA,1.0,-0.5,[\"4\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 5, ---- 5 ---- 5 ---- 5 ---- 5 ---- 5 ----\r\nA,1.0,-0.5,[\"5\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 6, ---- 6 ---- 6 ---- 6 ---- 6 ---- 6 ----\r\nA,1.0,-0.5,[\"6\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 7, ---- 7 ---- 7 ---- 7 ---- 7 ---- 7 ----\r\nA,1.0,-0.5,[\"7\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 8, ---- 8 ---- 8 ---- 8 ---- 8 ---- 8 ----\r\nA,1.0,-0.5,[\"8\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n*Numbers 9, ---- 9 ---- 9 ---- 9 ---- 9 ---- 9 ----\r\nA,1.0,-0.5,[\"9\",standard,S=1,R=0,X=0,Y=-0.5],-1.5\r\n\r\n\r\n"; +export const KOSDIC_LIN = ";; ------------------------------------�� �� �� ��------------------------------------\r\n;; �� �� ���� ���� ����(KOSDIC.lin)�� �Ǽ�CALS/EC ���ڵ��� �ۼ�ǥ���� �� ��Ͽ� ���ǵ� �� ������\r\n;; AutoCAD ����ڿ��� �����ϱ� ���� ���۵Ǿ����ϴ�.\r\n;; \r\n;; ǥ�ع��� : �Ǽ�CALS/EC ���ڵ��� �ۼ�ǥ�� V1.1(2006.12)\r\n;; ������� : �Ǽ�CALS/EC ������ �ѱ��Ǽ����������\r\n;;\r\n;; �����Ģ : 1/1 ��ô�� ����ũ�� 2.0mm�� ��� ������ ũ�⸦ ������\r\n;; LIB ��Ģ : AutoCAD�� ANSI����(���� ��������� ����)�� ����Ű ���� 1/10�� ������\r\n;; (��) ���� �� ���� ������ 2mm�̸� 0.2unit�� ����\r\n;; ����Ģ : �ǹ����鿡 ����� ��� �������� ��Ģ�� ���յǷ��� ��������ô�� 10�踦 ������\r\n;; (��) LTSCALE=10 (���� 8~10��), ������ô�� 1/5 �̸� LTSCALE=5x10��=50\r\n;;\r\n;; ISO���� �԰� 15��\r\n// ���� AutoCAD ISO�� 25.4����� ������ �� ����\r\n*A-ISONS01,ISO �����԰� 01 Continuous ________________________________\r\nA,1,1\r\n*A-ISONS02,ISO �����԰� 02 dash __ __ __ __ __ __ __ __ __ __ __ __ __\r\nA,.47244,-.118111\r\n*A-ISONS03,ISO �����԰� 03 dash space __ __ __ __ __ __\r\nA,.47244,-.7086614\r\n*A-ISONS04,ISO �����԰� 04 long-dash dot ____ . ____ . ____ . ____ . _\r\nA,.94488,-.11811,0,-.11811\r\n*A-ISONS05,ISO �����԰� 05 long-dash double-dot ____ .. ____ .. ____ . \r\nA,.94488,-.11811,0,-.11811,0,-.11811\r\n*A-ISONS06,ISO �����԰� 06 long-dash triple-dot ____ ... ____ ... ____\r\nA,.94488,-.11811,0,-.11811,0,-.11811,0,-.11811\r\n*A-ISONS07,ISO �����԰� 07 dot . . . . . . . . . . . . . . . . . . . . \r\nA,0,-.11811\r\n*A-ISONS08,ISO �����԰� 08 long-dash short-dash ____ __ ____ __ ____ _\r\nA,.94488,-.11811,.94488,-.11811\r\n*A-ISONS09,ISO �����԰� 09 long-dash double-short-dash ____ __ __ ____\r\nA,.94488,-.11811,.94488,-.11811,.94488,-.11811\r\n*A-ISONS10,ISO �����԰� 10 dash dot __ . __ . __ . __ . __ . __ . __ . \r\nA,.47244,-.11811,0,-.11811\r\n*A-ISONS11,ISO �����԰� 11 double-dash dot __ __ . __ __ . __ __ . __ _\r\nA,.47244,-.11811,.47244,-.11811,0,-.11811\r\n*A-ISONS12,ISO �����԰� 12 dash double-dot __ . . __ . . __ . . __ . . \r\nA,.47244,-.11811,0,-.11811,0,-.11811\r\n*A-ISONS13,ISO �����԰� 13 double-dash double-dot __ __ . . __ __ . . _\r\nA,.47244,-.11811,.47244,-.11811,0,-.11811,0,-.11811\r\n*A-ISONS14,ISO �����԰� 14 dash triple-dot __ . . . __ . . . __ . . . _\r\nA,.47244,-.11811,0,-.11811,0,-.11811,0,-.11811\r\n*A-ISONS15,ISO �����԰� 15 double-dash triple-dot __ __ . . . __ __ . .\r\nA,.47244,-.11811,.47244,-.11811,0,-.11811,0,-.11811,0,-.11811\r\n\r\n;; ���CAD ����\r\n*A-BORDR01,�ܰ���(�Ϲ�) Border __ __ . __ __ . __ __ . __ __ . __ __ .\r\nA,.5,-.25,.5,-.25,0,-.25\r\n*A-BORDR01H,�ܰ���(�ݹ��) Border (.5x) __.__.__.__.__.__.__.__.__.__.__.\r\nA,.25,-.125,.25,-.125,0,-.125\r\n*A-BORDR01X,�ܰ���(���) Border (2x) ____ ____ . ____ ____ . ___\r\nA,1.0,-.5,1.0,-.5,0,-.5\r\n*A-CENTR01,�߽ɼ�(�Ϲ�) Center ____ _ ____ _ ____ _ ____ _ ____ _ ____\r\nA,1.25,-.25,.25,-.25\r\n*A-CENTR01H,�߽ɼ�(�ݹ��) Center (.5x) ___ _ ___ _ ___ _ ___ _ ___ _ ___\r\nA,.75,-.125,.125,-.125\r\n*A-CENTR01X,�߽ɼ�(���) Center (2x) ________ __ ________ __ _____\r\nA,2.5,-.5,.5,-.5\r\n*A-DASDT01,�Ĵ�����(�Ϲ�) Dash dot __ . __ . __ . __ . __ . __ . __ . __\r\nA,.5,-.25,0,-.25\r\n*A-DASDT01H,�Ĵ�����(�ݹ��) Dash dot (.5x) _._._._._._._._._._._._._._._.\r\nA,.25,-.125,0,-.125\r\n*A-DASDT01X,�Ĵ�����(���) Dash dot (2x) ____ . ____ . ____ . ___\r\nA,1.0,-.5,0,-.5\r\n*A-DASHD01,�ļ�(�Ϲ�) Dashed __ __ __ __ __ __ __ __ __ __ __ __ __ _\r\nA,.5,-.25\r\n*A-DASHD01H,�ļ�(�ݹ��) Dashed (.5x) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \r\nA,.25,-.125\r\n*A-DASHD01X,�ļ�(���) Dashed (2x) ____ ____ ____ ____ ____ ___\r\nA,1.0,-.5\r\n*A-DIVID01,���Ҽ�(�Ϲ�) Divide ____ . . ____ . . ____ . . ____ . . ____\r\nA,.5,-.25,0,-.25,0,-.25\r\n*A-DIVID01H,���Ҽ�(�ݹ��) Divide (.5x) __..__..__..__..__..__..__..__.._\r\nA,.25,-.125,0,-.125,0,-.125\r\n*A-DIVID01X,���Ҽ�(���) Divide (2x) ________ . . ________ . . _\r\nA,1.0,-.5,0,-.5,0,-.5\r\n*A-DOTLN01,����(�Ϲ�) Dot . . . . . . . . . . . . . . . . . . . . . . . .\r\nA,0,-.25\r\n*A-DOTLN01H,����(�ݹ��) Dot (.5x) ........................................\r\nA,0,-.125\r\n*A-DOTLN01X,����(���) Dot (2x) . . . . . . . . . . . . . .\r\nA,0,-.5\r\n*A-HIDDN01,������(�Ϲ�) Hidden __ __ __ __ __ __ __ __ __ __ __ __ __ __\r\nA,.25,-.125\r\n*A-HIDDN01H,������(�ݹ��) Hidden (.5x) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \r\nA,.125,-.0625\r\n*A-HIDDN01X,������(���) Hidden (2x) ____ ____ ____ ____ ____ ____ ____ \r\nA,.5,-.25\r\n*A-PHATM01,����(�Ϲ�) Phantom ______ __ __ ______ __ __ ______ \r\nA,1.25,-.25,.25,-.25,.25,-.25\r\n*A-PHATM01H,����(�ݹ��) Phantom (.5x) ___ _ _ ___ _ _ ___ _ _ ___ _ _\r\nA,.625,-.125,.125,-.125,.125,-.125\r\n*A-PHATM01X,����(���) Phantom (2x) ____________ ____ ____ _\r\nA,2.5,-.5,.5,-.5,.5,-.5\r\n\r\n;; �潺�ü�\r\n*E-FENCE01,�潺(X �ļ�)\r\nA,.85,-.15,[KSC11,kosdic.shx,s=.1],-.15,.85\r\n*E-FENCE02,�潺(X �Ǽ�)\r\nA,1.0,[KSC11,kosdic.shx,s=.1],1.0\r\n*E-FENCE03,�潺(/\\ �ļ�)\r\nA,.875,-.05,[KSC12,kosdic.shx,s=.1],-.2,.875\r\n*E-FENCE04,�潺(+ �ļ�)\r\nA,.85,-.15,[KSC15,kosdic.shx,s=.1],-.15,.85\r\n\r\n;; �������\r\n*E-UTIL01,����(| �Ǽ�)\r\nA,1.0,[KSC01,kosdic.shx,s=.1],1.0\r\n*E-UTIL02,����(|| �ļ�)\r\nA,.05,[KSC01,kosdic.shx,s=.1],-.1,[KSC01,kosdic.shx,s=.1],1.85\r\n*E-UTIL03,����(|| �Ǽ�)\r\nA,.95,[KSC01,kosdic.shx,s=.1],.1,[KSC01,kosdic.shx,s=.1],.95\r\n*E-UTIL04,����(||| �ļ�)\r\nA,.9,[KSC01,kosdic.shx,s=.1],-.1,[KSC01,kosdic.shx,s=.1],-.1,[KSC01,kosdic.shx,s=.1],.9\r\n*E-UTIL05,����(||| �Ǽ�)\r\nA,.9,[KSC01,kosdic.shx,s=.1],.1,[KSC01,kosdic.shx,s=.1],.1,[KSC01,kosdic.shx,s=.1],.9\r\n*E-UTIL06,����(/ �ļ�)\r\nA,.9,-.1,[KSC02,kosdic.shx,s=.1],-.1,.9\r\n*E-UTIL07,����(/ �Ǽ�)\r\nA,1.0,[KSC02,kosdic.shx,s=.1],1.0\r\n*E-UTIL08,����(// �ļ�)\r\nA,.85,-.15,[KSC05,kosdic.shx,s=.1],-.15,.85\r\n*E-UTIL09,����(// �Ǽ�)\r\nA,1.0,[KSC05,kosdic.shx,s=.1],1.0\r\n*E-UTIL10,����(/// �ļ�)\r\nA,.8,-.2,[KSC06,kosdic.shx,s=.1],-.2,.8\r\n*E-UTIL11,����(/// �Ǽ�)\r\nA,1.0,[KSC06,kosdic.shx,s=.1],1.0\r\n\r\n;; �ü�����\r\n*E-FACLN01,�ü�(- ���༱)\r\nA,.25,[KSC01,kosdic.shx,y=-.05,s=.1,r=270],.25\r\n*E-FACLN02,�ü�(= ���༱)\r\nA,.25,[KSC04,kosdic.shx,y=-.05,s=.1],.25\r\n*E-FACLN03,�ü�(O �ļ�)\r\nA,.85,[KSC31,kosdic.shx,x=.075,s=.075],-.15,1.0\r\n*E-FACLN04,�ü�(O �Ǽ�)\r\nA,1.0,[KSC31,kosdic.shx,s=.075],1.0\r\n*E-FACLN05,�ü�(��,����Ʈ)\r\nA,1.0,[KSC32,kosdic.shx,s=.075],1.0\r\n\r\n;; ��������\r\n*E-TOPOG01,����(�� �ļ�)\r\nA,.000002,[KSC16,kosdic.shx,s=.1],-.25\r\n*E-TOPOG02,����(�� �Ǽ�)\r\nA,.000002,[KSC16,kosdic.shx,s=.1],-.2\r\n*E-TOPOG03,����(| �Ǽ�)\r\nA,0,-.1,[KSC15,kosdic.shx,s=.1],-.1\r\n*E-TOPOG04,����(| ����)\r\nA,0,[KSC00,kosdic.shx,y=.1,s=.1],.2,[KSC00,kosdic.shx,s=.1],.2\r\n*E-TOPOG05,����(~. ����)\r\nA,.000005,[KSC1C,kosdic.shx,s=.1],-.45,[KSC30,kosdic.shx,s=.1],-.05\r\n\r\n;; ������� ����\r\n*B-ZIGZG01,�������(��)\r\nA,.000002,[KSC13,kosdic.shx,s=.1],-.2\r\n*B-ZIGZG02,�������(ȣ)\r\nA,.000002,[KSC1B,kosdic.shx,s=.1],-.2\r\n*C-ZIGZG01,�������(�Ǽ�,��)\r\nA,.1,[KSC13,kosdic.shx,s=.1],.1\r\n*C-ZIGZG02,�������(�Ǽ�,ȣ)\r\nA,.1,[KSC1B,kosdic.shx,s=.1],.1\r\n\r\n;; ���� �� ����\r\n*D-SWALL01,����(�ű�)\r\nA,0,[KSC61,kosdic.shx,y=-.1,s=.1],-.2\r\n*X-SWALL01,����(����)\r\nA,0,[KSC71,kosdic.shx,s=.1],-.4\r\n*D-BWALL01,���ϴ�(����)\r\nA,0,[KSC18,kosdic.shx,s=.1],-.25\r\n*D-BWALL02,���ϴ�(�ű�)\r\nA,.000005,[KSC3A,kosdic.shx,s=.05],-.1,[KSC3A,kosdic.shx,s=.05],-.1,[KSC04,kosdic.shx,y=-.025,s=.05],-.1,[KSC04,kosdic.shx,y=-.025,s=.05],-.1,[KSC04,kosdic.shx,y=-.025,s=.05],-.05\r\n\r\n;; ��/�����迭\r\n*E-DOTLN01,���ҹ迭\r\nA,0,[KSC30,kosdic.shx,s=.3],-.15\r\n*E-DOTLN02,���ҹ迭(��)\r\nA,.075,[KSC30,kosdic.shx,s=.3],.075\r\n*E-CIRCL01,�����������Ϲ迭\r\nA,0,[KSC31,kosdic.shx,s=.1],-.2\r\n*E-CIRCL02,ä��������Ϲ迭\r\nA,0,[KSC32,kosdic.shx,s=.1],-.2\r\n*D-RROAD01,ö��(����)\r\nA,0,[KSC3A,kosdic.shx,s=.1],-.2,[KSC04,kosdic.shx,y=-.05,s=.1],-.2\r\n\r\n;; ������\r\n*E-DIGLN01,������\r\nA,.9,-.2,[KSC19,kosdic.shx,s=.1,r=180],.9\r\n*E-DIGLN01H,������(�ݹ��)\r\nA,.45,-.1,[KSC19,kosdic.shx,s=.05,r=180],.45\r\n*E-DIGLN01X,������(���)\r\nA,1.8,-.4,[KSC19,kosdic.shx,s=.2,r=180],1.8\r\n\r\n;; ���\r\n*B-GREEN01,����/�����\r\nA,.000002,[KSC08,kosdic.shx,s=.1],-.2\r\n*B-GREEN02,�����\r\nA,0,[KSC21,kosdic.shx,s=.1],-.45\r\n*E-BOUND01,�������\r\nA,.875,[KSC60,kosdic.shx,s=.1],-.25,.875\r\n\r\n;; ��Ÿ\r\n*B-RANDM01,�ұ�Ģ��\r\nA,0,[KSC1A,kosdic.shx,s=.1],-.2\r\n*E-DEMOL01,ö�Ŵ��\r\nA,.075,[KSC01,kosdic.shx,s=.1,r=30],.075\r\n*X-EARTH01,���ݼ�\r\nA,0,[KSC20,kosdic.shx,s=.1,x=.25,r=180],.25\r\n\r\n;; ���ܼ�\r\n*E-CUTLN01,���ܼ�(��)\r\nA,.95,[KSC14,kosdic.shx,s=.1],-.1,.95\r\n*E-CUTLN02,���ܼ�(ȣ)\r\nA,.95,[KSC22,kosdic.shx,s=.1],-.1,.95\r\n\r\n;; ���帧 ����\r\n*E-WFLOW01,���帧(��, �ļ�)\r\nA,.6,[KSC36,kosdic.shx,s=.1],-.65\r\n*E-WFLOW02,���帧(��, �Ǽ�)\r\nA,1.5,[KSC36,kosdic.shx,s=.1],0\r\n*E-WFLOW03,���帧(ȭ��, �ļ�)\r\nA,.6,[KSC35,kosdic.shx,s=.1],-.65\r\n*E-WFLOW04,���帧(ȭ��, �Ǽ�)\r\nA,1.5,[KSC35,kosdic.shx,s=.1],0\r\n\r\n;; ���������\r\n*G-WATER01,���������(W ����)\r\nA,2.933333,[\"W\",Standard,y=.05,s=.2],.066667\r\n*G-WATER02,���������(W �ļ�)\r\nA,2.75,-.05,[\"W\",Standard,y=-.1,s=.2,r=360],-.2\r\n\r\n;; �¼�����\r\n*G-HWATR01,�¼�����(HWL ����)\r\nA,2.783629,[\"HWL\",Standard,y=.05,s=.2],.216371\r\n*G-HWATR02,�¼�����(HWL �ļ�)\r\nA,2.35,-.05,[\"HWL\",Standard,y=-.1,s=.2],-.6\r\n\r\n;; �ü�����\r\n*G-CWATR01,�ü�����(CWL ����)\r\nA,2.783629,[\"CWL\",Standard,y=.05,s=.2],.216371\r\n*G-CWATR02,�ü�����(CWL �ļ�)\r\nA,2.35,-.05,[\"CWL\",Standard,y=-.1,s=.2],-.6\r\n\r\n;; ��������\r\n*E-SEWER01,��������(����)\r\nA,.95,-.1,[KSC09,kosdic.shx,s=.1,r=180],.95\r\n*E-SEWER01H,��������(����,�ݹ��)\r\nA,.475,-.05,[KSC09,kosdic.shx,s=.05,r=180],.475\r\n*E-SEWER01X,��������(����,���)\r\nA,1.9,-.2,[KSC09,kosdic.shx,s=.2,r=180],1.9\r\n*G-SEWER01,��������(S ����)\r\nA,2.933333,[\"S\",Standard,y=.05,s=.2],.066667\r\n*G-SEWER02,��������(S �ļ�)\r\nA,2.75,-.05,[\"S\",Standard,y=-.1,s=.2,r=360],-.2\r\n\r\n;; ��ż���\r\n*G-TELCM01,��ż���(T ����)\r\nA,2.933333,[\"T\",Standard,y=.05,s=.2],.066667\r\n*G-TELCM02,��ż���(T �ļ�)\r\nA,2.75,-.05,[\"T\",Standard,y=-.1,s=.2,r=360],-.2\r\n\r\n;; ���⼱��\r\n*G-ELECT01,���⼱��(E ����)\r\nA,2.933333,[\"E\",Standard,y=.05,s=.2],.066667\r\n*G-ELECT02,���⼱��(E �ļ�)\r\nA,2.75,-.05,[\"E\",Standard,y=-.1,s=.2,r=360],-.2\r\n\r\n;; ��������\r\n*G-GASLN01,��������(G ����)\r\nA,2.933333,[\"G\",Standard,y=.05,s=.2],.066667\r\n*G-GASLN02,��������(G �ļ�)\r\nA,2.75,-.05,[\"G\",Standard,y=-.1,s=.2,r=360],-.2\r\n*G-GASLN03,��������(GAS ����)\r\nA,2.783629,[\"GAS\",Standard,y=.05,s=.2],.216371\r\n*G-GASLN04,��������(GAS �ļ�)\r\nA,2.35,-.05,[\"GAS\",Standard,y=-.1,s=.2],-.6\r\n\r\n;; �Ϲ�������\r\n;; *CAL421,PIPE\r\n;; A,2.733333,[\"PIPE\",Standard,y=.05,s=.2],.266667\r\n*G-PIPEL01,�Ϲ� ������(PIPE �ļ�)\r\nA,3.25,-.05,[\"PIPE\",Standard,y=-.1,s=.2],-.7\r\n\r\n;; �������\r\n*H-DICHT00,�������\r\nA,1.1,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"T\",Standard,y=-.1,s=.2,r=360],-.216667\r\n*H-DICHT00R,�������(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"T\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; L������\r\n*H-DICHL00,L������ ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.333333,[\"L\",Standard,y=-.1,s=.2],-.216667\r\n*H-DICHL00R,L������ ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"L\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL01,L������ ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.366667,[\"L1\",Standard,y=-.1,s=.2,r=360],-.383333\r\n*H-DICHL01R,L������ ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.633333,[\"L1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHL02,L������ ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"L2\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHL02R,L������ ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"L2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL03,L������ ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"L3\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHL03R,L������ ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"L3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL04,L������ ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"L4\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHL04R,L������ ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"L4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL05,L������ ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"L5\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHL05R,L������ ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"L5\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL06,L������ ����06\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"L6\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHL06R,L������ ����06(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"L6\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL07,L������ ����07\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"L7\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHL07R,L������ ����07(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"L7\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL08,L������ ����08\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"L8\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHL08R,L������ ����08(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"L8\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL09,L������ ����09\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"L9\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHL09R,L������ ����09(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"L9\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHL10,L������ ����10\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"L10\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-DICHL10R,L������ ����10(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"L10\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-DICHL11,L������ ����11\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.4,[\"L11\",Standard,y=-.1,s=.2],-.55\r\n*H-DICHL11R,L������ ����11(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.8,[\"L11\",Standard,y=.1,s=.2,r=180],-.15\r\n*H-DICHL12,L������ ����12\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.366667,[\"L12\",Standard,y=-.1,s=.2],-.583333\r\n*H-DICHL12R,L������ ����12(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.833333,[\"L12\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHL13,L������ ����13\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.366667,[\"L13\",Standard,y=-.1,s=.2,r=360],-.583333\r\n*H-DICHL13R,L������ ����13(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.833333,[\"L13\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHL14,L������ ����14\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.366667,[\"L14\",Standard,y=-.1,s=.2],-.583333\r\n*H-DICHL14R,L������ ����14(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.833333,[\"L14\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHL15,L������ ����15\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.366667,[\"L15\",Standard,y=-.1,s=.2],-.583333\r\n*H-DICHL15R,L������ ����15(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.833333,[\"L15\",Standard,y=.1,s=.2,r=180],-.116667\r\n\r\n;; U������\r\n*H-DICHU00,U������ ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.333333,[\"U\",Standard,y=-.1,s=.2],-.216667\r\n*H-DICHU00R,U������ ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"U\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU01,U������ ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.366667,[\"U1\",Standard,y=-.1,s=.2],-.383333\r\n*H-DICHU01R,U������ ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.633333,[\"U1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHU02,U������ ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"U2\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHU02R,U������ ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU03,U������ ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"U3\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHU03R,U������ ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU04,U������ ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"U4\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHU04R,U������ ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU05,U������ ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"U5\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHU05R,U������ ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U5\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU06,U������ ����06\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"U6\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHU06R,U������ ����06(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U6\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU07,U������ ����07\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"U7\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHU07R,U������ ����07(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U7\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU08,U������ ����08\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"U8\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHU08R,U������ ����08(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U8\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU09,U������ ����09\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"U9\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHU09R,U������ ����09(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"U9\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHU10,U������ ����10\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.383333,[\"U10\",Standard,y=-.1,s=.2],-.566667\r\n*H-DICHU10R,U������ ����10(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"U10\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-DICHU11,U������ ����11\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.4,[\"U11\",Standard,y=-.1,s=.2],-.55\r\n*H-DICHU11R,U������ ����11(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.8,[\"U11\",Standard,y=.1,s=.2,r=180],-.15\r\n*H-DICHU12,U������ ����12\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.366667,[\"U12\",Standard,y=-.1,s=.2],-.583333\r\n*H-DICHU12R,U������ ����12(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.833333,[\"U12\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHU13,U������ ����13\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.366667,[\"U13\",Standard,y=-.1,s=.2,r=360],-.583333\r\n*H-DICHU13R,U������ ����13(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.833333,[\"U13\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHU14,U������ ����14\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.366667,[\"U14\",Standard,y=-.1,s=.2],-.583333\r\n*H-DICHU14R,U������ ����14(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.833333,[\"U14\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHU15,U������ ����15\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.366667,[\"U15\",Standard,y=-.1,s=.2,r=360],-.583333\r\n*H-DICHU15R,U������ ����15(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.833333,[\"U15\",Standard,y=.1,s=.2,r=180],-.116667\r\n\r\n;; V������\r\n*H-DICHV00,V������ ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V\",Standard,y=-.1,s=.2,r=360],-.25\r\n*H-DICHV00R,V������ ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1,r=360],-.5,[\"V\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV01,V������ ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"V1\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHV01R,V������ ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"V1\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHV02,V������ ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.3,[\"V2\",Standard,y=-.1,s=.2],-.45\r\n*H-DICHV02R,V������ ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.7,[\"V2\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV03,V������ ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V3\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV03R,V������ ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.7,[\"V3\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV04,V������ ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V4\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV04R,V������ ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.7,[\"V4\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV05,V������ ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V5\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV05R,V������ ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.7,[\"V5\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV06,V������ ����06\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V6\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV06R,V������ ����06(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.25,0,-.45,[\"V6\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV07,V������ ����07\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V7\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV07R,V������ ����07(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.7,[\"V7\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV08,V������ ����08\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V8\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV08R,V������ ����08(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.7,[\"V8\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV09,V������ ����09\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.3,[\"V9\",Standard,y=-.1,s=.2,r=360],-.45\r\n*H-DICHV09R,V������ ����09(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.7,[\"V9\",Standard,y=.1,s=.2,r=180],-.05\r\n*H-DICHV10,V������ ����10\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.35,[\"V10\",Standard,y=-.1,s=.2],-.6\r\n*H-DICHV10R,V������ ����10(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.85,[\"V10\",Standard,y=.1,s=.2,r=180],-.1\r\n*H-DICHV11,V������ ����11\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.366667,[\"V11\",Standard,y=-.1,s=.2],-.583333\r\n*H-DICHV11R,V������ ����11(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.833333,[\"V11\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHV12,V������ ����12\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"V12\",Standard,y=-.1,s=.2,r=360],-.616667\r\n*H-DICHV12R,V������ ����12(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.866667,[\"V12\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHV13,V������ ����13\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.333333,[\"V13\",Standard,y=-.1,s=.2],-.616667\r\n*H-DICHV13R,V������ ����13(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.866667,[\"V13\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHV14,V������ ����14\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.333333,[\"V14\",Standard,y=-.1,s=.2],-.616667\r\n*H-DICHV14R,V������ ����14(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.866667,[\"V14\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHV15,V������ ����15\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.333333,[\"V15\",Standard,y=-.1,s=.2],-.616667\r\n*H-DICHV15R,V������ ����15(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.866667,[\"V15\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; ����ũ\r\n*H-DICHD00,����ũ ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"D\",Standard,y=-.1,s=.2,r=360],-.216667\r\n*H-DICHD00R,����ũ ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"D\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHD01,����ũ ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.366667,[\"D1\",Standard,y=-.1,s=.2,r=360],-.383333\r\n*H-DICHD01R,����ũ ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.633333,[\"D1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHD02,����ũ ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"D2\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHD02R,����ũ ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"D2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHD03,����ũ ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"D3\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHD03R,����ũ ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"D3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHD04,����ũ ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"D4\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHD04R,����ũ ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"D4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHD05,����ũ ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"D5\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHD05R,����ũ ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"D5\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; �긶�� ����\r\n*H-DICHB00,�긶������ ����00\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.633333,[\"B\",Standard,y=-.1,s=.2],-.216667\r\n*H-DICHB00R,�긶������ ����00(����)\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.766667,[\"B\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHB01,�긶������ ����01\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.666667,[\"B1\",Standard,y=-.1,s=.2],-.383333\r\n*H-DICHB01R,�긶������ ����01(����)\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.933333,[\"B1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHB02,�긶������ ����02\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.633333,[\"B2\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHB02R,�긶������ ����02(����)\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.966667,[\"B2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHB03,�긶������ ����03\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.633333,[\"B3\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHB03R,�긶������ ����03(����)\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.966667,[\"B3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHB04,�긶������ ����04\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.633333,[\"B4\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHB04R,�긶������ ����04(����)\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.966667,[\"B4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHB05,�긶������ ����05\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.633333,[\"B5\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHB05R,�긶������ ����05(����)\r\nA,.00001,[KSC52,kosdic.shx,s=.1],-.966667,[\"B5\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; �;ϰ�\r\n*H-DICHM00,�;ϰ� ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.333333,[\"M\",Standard,y=-.1,s=.2],-.216667\r\n*H-DICHM00R,�;ϰ� ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"M\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHM01,�;ϰ� ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.366667,[\"M1\",Standard,y=-.1,s=.2,r=360],-.383333\r\n*H-DICHM01R,�;ϰ� ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.633333,[\"M1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHM02,�;ϰ� ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"M2\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHM02R,�;ϰ� ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"M2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHM03,�;ϰ� ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"M3\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHM03R,�;ϰ� ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"M3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHM04,�;ϰ� ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"M4\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHM04R,�;ϰ� ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"M4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHM05,�;ϰ� ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"M5\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHM05R,�;ϰ� ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"M5\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; �������\r\n*H-DICHO00,������� ����00\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.333333,[\"L/O\",Standard,y=-.1,s=.2],-.616667\r\n*H-DICHO00R,������� ����00(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.866667,[\"L/O\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHO01,������� ����01\r\nA,.5,[KSC35,kosdic.shx,s=.1],-.366667,[\"L1/O\",Standard,y=-.1,s=.2],-.783333\r\n*H-DICHO01R,������� ����01(����)\r\nA,.5,[KSC35,kosdic.shx,s=.1],-1.033333,[\"L1/O\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHO02,������� ����02\r\nA,.5,[KSC35,kosdic.shx,s=.1],-.333333,[\"L2/O\",Standard,y=-.1,s=.2],-.816667\r\n*H-DICHO02R,������� ����02(����)\r\nA,.5,[KSC35,kosdic.shx,s=.1,r=360],-1.066667,[\"L2/O\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHO03,������� ����03\r\nA,.5,[KSC35,kosdic.shx,s=.1],-.333333,[\"L3/O\",Standard,y=-.1,s=.2],-.816667\r\n*H-DICHO03R,������� ����03(����)\r\nA,.5,[KSC35,kosdic.shx,s=.1],-1.066667,[\"L3/O\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHO04,������� ����04\r\nA,.5,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"L4/O\",Standard,y=-.1,s=.2,r=360],-.816667\r\n*H-DICHO04R,������� ����04(����)\r\nA,.5,[KSC35,kosdic.shx,s=.1],-1.066667,[\"L4/O\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHO05,������� ����05\r\nA,.5,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"L5/O\",Standard,y=-.1,s=.2,r=360],-.816667\r\n*H-DICHO05R,������� ����05(����)\r\nA,.5,[KSC35,kosdic.shx,s=.1],-1.066667,[\"L5/O\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; �Ҵ�����\r\n*H-DICHG00,�Ҵ����� ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.333333,[\"G\",Standard,y=-.1,s=.2],-.216667\r\n*H-DICHG00R,�Ҵ����� ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"G\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHG01,�Ҵ����� ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.366667,[\"G1\",Standard,y=-.1,s=.2],-.383333\r\n*H-DICHG01R,�Ҵ����� ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.633333,[\"G1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHG02,�Ҵ����� ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"G2\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHG02R,�Ҵ����� ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"G2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHG03,�Ҵ����� ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"G3\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHG03R,�Ҵ����� ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"G3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHG04,�Ҵ����� ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"G4\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHG04R,�Ҵ����� ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"G4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHG05,�Ҵ����� ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"G5\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHG05R,�Ҵ����� ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"G5\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; J������\r\n*H-DICHJ00,J������ ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.333333,[\"J\",Standard,y=-.1,s=.2],-.216667\r\n*H-DICHJ00R,J������ ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"J\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHJ01,J������ ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.366667,[\"J1\",Standard,y=-.1,s=.2],-.383333\r\n*H-DICHJ01R,J������ ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.633333,[\"J1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-DICHJ02,J������ ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"J2\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHJ02R,J������ ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"J2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHJ03,J������ ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"J3\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHJ03R,J������ ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"J3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHJ04,J������ ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"J4\",Standard,y=-.1,s=.2],-.416667\r\n*H-DICHJ04R,J������ ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"J4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-DICHJ05,J������ ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"J5\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-DICHJ05R,J������ ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"J5\",Standard,y=.1,s=.2,r=180],-.083333\r\n\r\n;; �����\r\n*H-CANAL00,����� ����00\r\nA,1.1,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"Y\",Standard,y=-.1,s=.2,r=360],-.216667\r\n*H-CANAL00R,����� ����00(����)\r\nA,1.1,[KSC35,kosdic.shx,s=.1],-.466667,[\"Y\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL01,����� ����01\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.366667,[\"Y1\",Standard,y=-.1,s=.2],-.383333\r\n*H-CANAL01R,����� ����01(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.633333,[\"Y1\",Standard,y=.1,s=.2,r=180],-.116667\r\n*H-CANAL02,����� ����02\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"Y2\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-CANAL02R,����� ����02(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"Y2\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL03,����� ����03\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"Y3\",Standard,y=-.1,s=.2],-.416667\r\n*H-CANAL03R,����� ����03(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"Y3\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL04,����� ����04\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"Y4\",Standard,y=-.1,s=.2],-.416667\r\n*H-CANAL04R,����� ����04(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"Y4\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL05,����� ����05\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.333333,[\"Y5\",Standard,y=-.1,s=.2,r=360],-.416667\r\n*H-CANAL05R,����� ����05(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1,r=360],-.666667,[\"Y5\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL06,����� ����06\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"Y6\",Standard,y=-.1,s=.2],-.416667\r\n*H-CANAL06R,����� ����06(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"Y6\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL07,����� ����07\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"Y7\",Standard,y=-.1,s=.2],-.416667\r\n*H-CANAL07R,����� ����07(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"Y7\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL08,����� ����08\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"Y8\",Standard,y=-.1,s=.2],-.416667\r\n*H-CANAL08R,����� ����08(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"Y8\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL09,����� ����09\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.333333,[\"Y9\",Standard,y=-.1,s=.2],-.416667\r\n*H-CANAL09R,����� ����09(����)\r\nA,.9,[KSC35,kosdic.shx,s=.1],-.666667,[\"Y9\",Standard,y=.1,s=.2,r=180],-.083333\r\n*H-CANAL10,����� ����10\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y10\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL10R,����� ����10(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y10\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL11,����� ����11\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y11\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL11R,����� ����11(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y11\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL12,����� ����12\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y12\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL12R,����� ����12(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y12\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL13,����� ����13\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y13\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL13R,����� ����13(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y13\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL14,����� ����14\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y14\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL14R,����� ����14(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y14\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL15,����� ����15\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y15\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL15R,����� ����15(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y15\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL16,����� ����16\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y16\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL16R,����� ����16(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y16\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL17,����� ����17\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y17\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL17R,����� ����17(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y17\",Standard,y=.1,s=.2,r=180],-.133333\r\n*H-CANAL18,����� ����18\r\nA,.7,[KSC35,kosdic.shx,s=.1,r=360],-.383333,[\"Y18\",Standard,y=-.1,s=.2,r=360],-.566667\r\n*H-CANAL18R,����� ����18(����)\r\nA,.7,[KSC35,kosdic.shx,s=.1],-.816667,[\"Y18\",Standard,y=.1,s=.2,r=180],-.133333\r\n\r\n"; diff --git a/src/viewer2d/linParser.ts b/src/viewer2d/linParser.ts new file mode 100644 index 0000000..97d7a11 --- /dev/null +++ b/src/viewer2d/linParser.ts @@ -0,0 +1,390 @@ +/** + * LIN File Parser & Complex Linetype Registry + * Parses AutoCAD LIN file definitions (including text and shape descriptors). + */ + +import { ICAD_LIN, KOSDIC_LIN } from './linData'; + +export interface LinElement { + type: 'dash' | 'gap' | 'shape' | 'text'; + val?: number; // dash length (+) or gap length (-) + shapeName?: string; // e.g. KSC35, CIRC1, BOX, TRACK1, BAT, ZIG + shxFile?: string; // e.g. kosdic.shx, ltypeshp.shx + text?: string; // e.g. "L2", "GAS", "HW" + style?: string; // e.g. Standard + scale: number; // s=... (default 1.0) + rotation: number; // r=... (angle in degrees) + isAbsoluteAngle: boolean;// a=... + isUprightAngle: boolean; // u=... + xOffset: number; // x=... + yOffset: number; // y=... +} + +export interface LinPattern { + name: string; + description: string; + elements: LinElement[]; + patternLength: number; // total length sum of dashes + abs(gaps) +} + +function parseBracketElement(inner: string): LinElement { + const parts: string[] = []; + let cur = ''; + let inQuote = false; + for (let i = 0; i < inner.length; i++) { + const c = inner[i]; + if (c === '"') inQuote = !inQuote; + if (c === ',' && !inQuote) { + parts.push(cur.trim()); + cur = ''; + } else { + cur += c; + } + } + if (cur.trim()) parts.push(cur.trim()); + + let type: 'shape' | 'text' = 'shape'; + let shapeName: string | undefined; + let shxFile: string | undefined; + let text: string | undefined; + let style: string | undefined; + + let scale = 1.0; + let rotation = 0; + let isAbsoluteAngle = false; + let isUprightAngle = false; + let xOffset = 0; + let yOffset = 0; + + if (parts.length > 0) { + const first = parts[0]; + if (first.startsWith('"') && first.endsWith('"')) { + type = 'text'; + text = first.slice(1, -1); + if (parts.length > 1 && !parts[1].includes('=')) style = parts[1]; + } else { + type = 'shape'; + shapeName = first; + if (parts.length > 1 && !parts[1].includes('=')) shxFile = parts[1]; + } + + for (let i = 1; i < parts.length; i++) { + const part = parts[i]; + const eqIdx = part.indexOf('='); + if (eqIdx > 0) { + const key = part.substring(0, eqIdx).trim().toLowerCase(); + const valStr = part.substring(eqIdx + 1).trim(); + const val = parseFloat(valStr); + if (!isNaN(val)) { + if (key === 's') scale = val; + else if (key === 'r') { rotation = val; } + else if (key === 'a') { rotation = val; isAbsoluteAngle = true; } + else if (key === 'u') { rotation = val; isUprightAngle = true; } + else if (key === 'x') xOffset = val; + else if (key === 'y') yOffset = val; + } + } + } + } + + return { + type, + shapeName, + shxFile, + text, + style, + scale, + rotation, + isAbsoluteAngle, + isUprightAngle, + xOffset, + yOffset, + }; +} + +function tokenizeLinPattern(patternStr: string): string[] { + const s = patternStr.replace(/^A\s*,\s*/i, '').trim(); + const tokens: string[] = []; + let current = ''; + let inBracket = false; + let inQuote = false; + + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (ch === '"') inQuote = !inQuote; + if (ch === '[' && !inQuote) inBracket = true; + if (ch === ']' && !inQuote) inBracket = false; + + if (ch === ',' && !inBracket && !inQuote) { + if (current.trim()) tokens.push(current.trim()); + current = ''; + } else { + current += ch; + } + } + if (current.trim()) tokens.push(current.trim()); + return tokens; +} + +export function parseLinContent(content: string): Map { + const result = new Map(); + const lines = content.split(/\r?\n/); + let currentName: string | null = null; + let currentDesc = ''; + + for (let line of lines) { + line = line.trim(); + if (!line || line.startsWith(';')) continue; + if (line.startsWith('*')) { + const commaIdx = line.indexOf(','); + if (commaIdx > 0) { + currentName = line.substring(1, commaIdx).trim(); + currentDesc = line.substring(commaIdx + 1).trim(); + } else { + currentName = line.substring(1).trim(); + currentDesc = ''; + } + } else if (currentName && line.toUpperCase().startsWith('A,')) { + const tokens = tokenizeLinPattern(line); + const elements: LinElement[] = []; + let totalLength = 0; + + for (const tok of tokens) { + if (tok.startsWith('[') && tok.endsWith(']')) { + const elem = parseBracketElement(tok.slice(1, -1)); + elements.push(elem); + } else { + const val = parseFloat(tok); + if (!isNaN(val)) { + if (val > 1e-6) { + elements.push({ + type: 'dash', + val, + scale: 1, + rotation: 0, + isAbsoluteAngle: false, + isUprightAngle: false, + xOffset: 0, + yOffset: 0, + }); + totalLength += val; + } else if (val < -1e-6) { + const absVal = Math.abs(val); + elements.push({ + type: 'gap', + val: -absVal, + scale: 1, + rotation: 0, + isAbsoluteAngle: false, + isUprightAngle: false, + xOffset: 0, + yOffset: 0, + }); + totalLength += absVal; + } else { + // Dot (val === 0) + elements.push({ + type: 'dash', + val: 0.05, + scale: 1, + rotation: 0, + isAbsoluteAngle: false, + isUprightAngle: false, + xOffset: 0, + yOffset: 0, + }); + totalLength += 0.05; + } + } + } + } + + if (elements.length > 0) { + result.set(currentName.toUpperCase(), { + name: currentName, + description: currentDesc, + elements, + patternLength: Math.max(totalLength, 0.1), + }); + } + currentName = null; + } + } + + return result; +} + +// Global Linetype Registry initialized with Icad.lin and KOSDIC.lin +const globalRegistry = new Map(); + +export function registerLinContent(content: string) { + const parsed = parseLinContent(content); + for (const [key, pattern] of parsed.entries()) { + globalRegistry.set(key, pattern); + } +} + +// Warm up registry with bundled LIN files +registerLinContent(ICAD_LIN); +registerLinContent(KOSDIC_LIN); + +const CIVIL_LT_TEXT_MAP: Record = { + 'H-DICHL02': 'L2-2/0', + 'H-DICHL02R': 'L2-2', + 'H-DICHL01': 'L2-3/0', + 'H-DICHL01R': 'L2-3', + 'H-DICHL06': 'L5-1', + 'H-DICHL06R': 'L5-1', + 'H-DICHL05': 'L5', + 'H-DICHL05R': 'L5', + 'H-DICHU05': 'U5', + 'H-DICHU05R': 'U5', + 'H-DICHB01': 'B1', + 'H-DICHB01R': 'B1', + 'H-DICHM03': 'M3', + 'H-DICHM03R': 'M3', + 'H-DICHV01': 'V1', + 'H-DICHV01R': 'V1', + 'H-DICHO01': 'O1', + 'H-DICHO01R': 'O1', + 'H-DICHL03': 'L3', + 'H-DICHL03R': 'L3', + 'H-DICHL04': 'L4', + 'H-DICHL04R': 'L4', + 'H-DICHL08': 'L8', + 'H-DICHL08R': 'L8', +}; + +const NO_SHAPE_LINETYPES = new Set([ + 'H-DICHL05', + 'H-DICHL05R', + 'H-DICHU05', + 'H-DICHU05R', + 'H-DICHB01', + 'H-DICHB01R', + 'H-DICHM03', + 'H-DICHM03R', + 'H-DICHV01', + 'H-DICHV01R', + 'H-DICHO01', + 'H-DICHO01R', +]); + +export function getLinPattern(name: string): LinPattern | undefined { + if (!name) return undefined; + const key = name.toUpperCase().trim(); + const pattern = globalRegistry.get(key); + if (!pattern) return undefined; + + let elements = pattern.elements; + + // Filter out shape/arrowhead elements for L5, U5, B1, etc. as specified in official CAD spec + if (NO_SHAPE_LINETYPES.has(key)) { + elements = elements.filter((elem) => elem.type !== 'shape'); + } + + const overrideText = CIVIL_LT_TEXT_MAP[key]; + if (overrideText) { + elements = elements.map((elem) => + elem.type === 'text' ? { ...elem, text: overrideText } : elem + ); + } + + return { + ...pattern, + elements, + }; +} + +export function getAllRegisteredPatterns(): Map { + return globalRegistry; +} + +export function mergeDwgLinetypePattern( + ltName: string, + dwgPattern: number[], + description?: string +): LinPattern | undefined { + if (!ltName) return undefined; + const base = getLinPattern(ltName); + if (!dwgPattern || !dwgPattern.length) return base; + + if (!base) { + const elements: LinElement[] = []; + let totalLength = 0; + for (const val of dwgPattern) { + if (val > 1e-6) { + elements.push({ + type: 'dash', + val, + scale: 1, + rotation: 0, + isAbsoluteAngle: false, + isUprightAngle: false, + xOffset: 0, + yOffset: 0, + }); + totalLength += val; + } else if (val < -1e-6) { + const absVal = Math.abs(val); + elements.push({ + type: 'gap', + val: -absVal, + scale: 1, + rotation: 0, + isAbsoluteAngle: false, + isUprightAngle: false, + xOffset: 0, + yOffset: 0, + }); + totalLength += absVal; + } + } + return { + name: ltName, + description: description || '', + elements, + patternLength: Math.max(totalLength, 0.1), + }; + } + + // Merge DWG numeric dash/gap values into base pattern elements + const newElements: LinElement[] = []; + let dwgValIdx = 0; + let totalLength = 0; + + for (const elem of base.elements) { + if (elem.type === 'dash') { + const dwgVal = + dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] > 0 + ? dwgPattern[dwgValIdx] + : elem.val || 0.05; + if (dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] > 0) { + dwgValIdx++; + } + newElements.push({ ...elem, val: dwgVal }); + totalLength += dwgVal; + } else if (elem.type === 'gap') { + const dwgVal = + dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] < 0 + ? dwgPattern[dwgValIdx] + : elem.val; + if (dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] < 0) { + dwgValIdx++; + } + const absVal = Math.abs(dwgVal || 0.1); + newElements.push({ ...elem, val: -absVal }); + totalLength += absVal; + } else { + newElements.push({ ...elem }); + } + } + + return { + ...base, + description: description || base.description, + elements: newElements, + patternLength: Math.max(totalLength, 0.1), + }; +} + diff --git a/src/viewer2d/parseDxf.ts b/src/viewer2d/parseDxf.ts new file mode 100644 index 0000000..798e473 --- /dev/null +++ b/src/viewer2d/parseDxf.ts @@ -0,0 +1,24 @@ +/** + * Parse DXF text/buffer into the same CadParseResult shape as DWG. + */ +import DxfParser from 'dxf-parser'; +import HatchHandler from './dxfHatchHandler.js'; +import { dxfToParseResult } from './dxfAdapter.js'; +import { decodeDxf } from './decodeDxf'; +import type { CadParseResult } from './dwgParser'; + +/** Parse a DXF ArrayBuffer (encoding-aware) → CadParseResult. */ +export function parseDxfBuffer(buf: ArrayBuffer): CadParseResult { + return parseDxfText(decodeDxf(buf)); +} + +/** Parse DXF text already decoded as a string. */ +export function parseDxfText(text: string): CadParseResult { + const parser = new DxfParser(); + // dxf-parser ships no HATCH handler; its EntityName union excludes 'HATCH'. + parser.registerEntityHandler( + HatchHandler as unknown as Parameters[0], + ); + const dxf = parser.parseSync(text); + return dxfToParseResult(dxf); +} diff --git a/src/viewer2d/shims.d.ts b/src/viewer2d/shims.d.ts new file mode 100644 index 0000000..ed70828 --- /dev/null +++ b/src/viewer2d/shims.d.ts @@ -0,0 +1,20 @@ +declare module 'dxf-parser' { + export default class DxfParser { + parseSync(text: string): unknown; + registerEntityHandler(handler: { ForEntityName: string; parseEntity: (...args: unknown[]) => unknown }): void; + } +} + +declare module 'opentype.js' { + export function parse(buffer: ArrayBuffer): unknown; +} + +declare module '*.wasm?url' { + const url: string; + export default url; +} + +declare module '*.wasm' { + const url: string; + export default url; +} diff --git a/src/viewer2d/slugText.ts b/src/viewer2d/slugText.ts new file mode 100644 index 0000000..2c331ba --- /dev/null +++ b/src/viewer2d/slugText.ts @@ -0,0 +1,403 @@ +/** + * slugText — GPU-resident vector text for the 2D viewer, after Eric Lengyel's + * Slug algorithm ("GPU-Centered Font Rendering Directly from Glyph Outlines", + * JCGT 2017; reference shaders at github.com/EricLengyel/Slug — patent + * dedicated to the public domain, attribution retained here). + * + * Replaces the per-string CanvasTexture sprites: TrueType quadratic Bézier + * outlines are uploaded once per UNIQUE GLYPH into a float data texture, and + * the fragment shader computes an antialiased winding number per pixel by + * intersecting horizontal + vertical rays with the glyph's curves (curves + * pre-sorted into 8 bands per axis to bound the per-pixel work). Text stays + * razor sharp at any zoom and memory scales with glyph count, not text count. + * + * Font: public/fonts/NanumGothic-Regular.ttf (OFL) — TrueType glyf outlines + * (quadratic only), Latin + full Hangul. + */ +import * as THREE from 'three'; +import { parse as parseFont } from 'opentype.js'; + +const TEX_W = 1024; // data texture width in texels (RGBA32F) +const BANDS = 8; // bands per axis per glyph +const PAD = 0.08; // em padding around glyph quads (AA spill room) + +interface Curve { x0: number; y0: number; cx: number; cy: number; x1: number; y1: number } + +interface GlyphRec { + base: number; // texel index of the glyph header (-1 when no ink, e.g. space) + advance: number; // em units +} + +/** Default TrueType font URL (host must serve Hangul-capable TTF). */ +let defaultFontUrl = '/fonts/NanumGothic-Regular.ttf'; + +/** Override the default font path before creating CadViewer / Viewer2D. */ +export function setDefaultFontUrl(url: string): void { + defaultFontUrl = url; + // reset shared engine so the next shared() uses the new URL + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (SlugTextEngine as any)._shared = null; +} + +/** One shared font engine (font fetch + parse happens once). */ +export class SlugTextEngine { + private static _shared: Promise | null = null; + + static shared(url: string = defaultFontUrl): Promise { + if (!this._shared) { + this._shared = (async () => { + const buf = await (await fetch(url)).arrayBuffer(); + return new SlugTextEngine(parseFont(buf)); + })(); + // allow a retry on transient fetch failure instead of caching the rejection + this._shared.catch(() => { (SlugTextEngine as any)._shared = null; }); + } + return this._shared; + } + + readonly capHeightEm: number; + readonly ascentEm: number; + readonly descentEm: number; + + private font: any; + private upem: number; + private glyphs = new Map(); + private data = new Float32Array(TEX_W * 4 * 64); // grows ×2 as needed + private used = 0; // texels consumed + private _texture: THREE.DataTexture | null = null; + private _texelsUploaded = 0; + + private constructor(font: any) { + this.font = font; + this.upem = font.unitsPerEm; + this.capHeightEm = (font.tables?.os2?.sCapHeight || font.ascender * 0.88) / this.upem; + this.ascentEm = font.ascender / this.upem; + this.descentEm = font.descender / this.upem; // negative + } + + /** Layout width of one line, in em units. */ + measureEm(line: string): number { + let w = 0; + for (const ch of line) w += this.ensureGlyph(ch.codePointAt(0)!).advance; + return w; + } + + ensureGlyph(cp: number): GlyphRec { + let rec = this.glyphs.get(cp); + if (rec) return rec; + + const glyph = this.font.charToGlyph(String.fromCodePoint(cp)); + const advance = (glyph.advanceWidth ?? this.upem * 0.5) / this.upem; + // getPath(0,0,1) → coordinates scaled to em units but y-DOWN; flip y. + const path = glyph.getPath(0, 0, 1); + const curves: Curve[] = []; + let sx = 0, sy = 0, px = 0, py = 0; + for (const c of path.commands) { + switch (c.type) { + case 'M': px = sx = c.x; py = sy = -c.y; break; + case 'L': curves.push(lineCurve(px, py, c.x, -c.y)); px = c.x; py = -c.y; break; + case 'Q': curves.push({ x0: px, y0: py, cx: c.x1, cy: -c.y1, x1: c.x, y1: -c.y }); px = c.x; py = -c.y; break; + case 'C': { // glyf fonts shouldn't emit cubics; approximate defensively + const mx = (c.x1 + c.x2) / 2, my = (-c.y1 + -c.y2) / 2; + curves.push({ x0: px, y0: py, cx: mx, cy: my, x1: c.x, y1: -c.y }); + px = c.x; py = -c.y; break; + } + case 'Z': if (px !== sx || py !== sy) curves.push(lineCurve(px, py, sx, sy)); px = sx; py = sy; break; + } + } + + if (!curves.length) { + rec = { base: -1, advance }; + this.glyphs.set(cp, rec); + return rec; + } + + // Conservative bbox from all points (control points included). + let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; + for (const q of curves) { + x0 = Math.min(x0, q.x0, q.cx, q.x1); x1 = Math.max(x1, q.x0, q.cx, q.x1); + y0 = Math.min(y0, q.y0, q.cy, q.y1); y1 = Math.max(y1, q.y0, q.cy, q.y1); + } + const w = Math.max(x1 - x0, 1e-6), h = Math.max(y1 - y0, 1e-6); + + // Band membership (conservative, via control-point extents). + const hBands: number[][] = Array.from({ length: BANDS }, () => []); + const vBands: number[][] = Array.from({ length: BANDS }, () => []); + curves.forEach((q, i) => { + const cy0 = Math.min(q.y0, q.cy, q.y1), cy1 = Math.max(q.y0, q.cy, q.y1); + const cx0 = Math.min(q.x0, q.cx, q.x1), cx1 = Math.max(q.x0, q.cx, q.x1); + const hb0 = clampBand((cy0 - y0) / h), hb1 = clampBand((cy1 - y0) / h); + for (let b = hb0; b <= hb1; b++) hBands[b].push(i); + const vb0 = clampBand((cx0 - x0) / w), vb1 = clampBand((cx1 - x0) / w); + for (let b = vb0; b <= vb1; b++) vBands[b].push(i); + }); + + // ── Serialize: header(2) + bands(16) + index lists + curves(2/curve) ── + const listTexels = (l: number[]) => Math.ceil(l.length / 4); + const idxTexels = hBands.reduce((s, l) => s + listTexels(l), 0) + + vBands.reduce((s, l) => s + listTexels(l), 0); + const total = 2 + BANDS * 2 + idxTexels + curves.length * 2; + const base = this.alloc(total); + const d = this.data; + const put = (t: number, a: number, b: number, c: number, e: number) => { + d[t * 4] = a; d[t * 4 + 1] = b; d[t * 4 + 2] = c; d[t * 4 + 3] = e; + }; + + const curveBase = base + 2 + BANDS * 2 + idxTexels; + put(base, x0, y0, x1, y1); + put(base + 1, base + 2, base + 2 + BANDS, 0, 0); // hBandBase, vBandBase + + let listCursor = base + 2 + BANDS * 2; + const writeBands = (bands: number[][], texel: number) => { + for (let b = 0; b < BANDS; b++) { + put(texel + b, listCursor, bands[b].length, 0, 0); + const l = bands[b]; + for (let j = 0; j < l.length; j += 4) { + put(listCursor++, + curveBase + l[j] * 2, + j + 1 < l.length ? curveBase + l[j + 1] * 2 : 0, + j + 2 < l.length ? curveBase + l[j + 2] * 2 : 0, + j + 3 < l.length ? curveBase + l[j + 3] * 2 : 0); + } + } + }; + writeBands(hBands, base + 2); + writeBands(vBands, base + 2 + BANDS); + + curves.forEach((q, i) => { + put(curveBase + i * 2, q.x0, q.y0, q.cx, q.cy); + put(curveBase + i * 2 + 1, q.x1, q.y1, 0, 0); + }); + + rec = { base, advance }; + this.glyphs.set(cp, rec); + return rec; + } + + bboxOf(rec: GlyphRec): [number, number, number, number] { + const t = rec.base * 4; + return [this.data[t], this.data[t + 1], this.data[t + 2], this.data[t + 3]]; + } + + private alloc(texels: number): number { + const need = (this.used + texels) * 4; + if (need > this.data.length) { + let cap = this.data.length; + while (cap < need) cap *= 2; + const next = new Float32Array(cap); + next.set(this.data); + this.data = next; + } + const at = this.used; + this.used += texels; + return at; + } + + /** Data texture with all glyph data uploaded (recreated when it grew). */ + texture(): THREE.DataTexture { + const rows = Math.max(1, Math.ceil(this.used / TEX_W)); + if (!this._texture || this._texelsUploaded < this.used) { + this._texture?.dispose(); + const buf = new Float32Array(TEX_W * rows * 4); + buf.set(this.data.subarray(0, Math.min(this.data.length, TEX_W * rows * 4))); + const tex = new THREE.DataTexture(buf, TEX_W, rows, THREE.RGBAFormat, THREE.FloatType); + tex.minFilter = THREE.NearestFilter; + tex.magFilter = THREE.NearestFilter; + tex.generateMipmaps = false; + tex.needsUpdate = true; + this._texture = tex; + this._texelsUploaded = this.used; + } + return this._texture; + } +} + +function lineCurve(x0: number, y0: number, x1: number, y1: number): Curve { + return { x0, y0, cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, x1, y1 }; +} + +function clampBand(f: number): number { + return Math.min(BANDS - 1, Math.max(0, Math.floor(f * BANDS))); +} + +// ── Shader (classic syntax; three's GLSL3 prefix maps attribute/varying/gl_FragColor) ── + +const VERT = /* glsl */` + attribute vec2 emuv; + attribute float gbase; + attribute vec3 tcolor; + varying vec2 vEm; + flat varying int vBase; + varying vec3 vColor; + void main() { + vEm = emuv; + vBase = int(gbase); + vColor = tcolor; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const FRAG = /* glsl */` + precision highp float; + uniform sampler2D dataTex; + varying vec2 vEm; + flat varying int vBase; + varying vec3 vColor; + layout(location = 0) out vec4 fragOut; + + vec4 T(int i) { return texelFetch(dataTex, ivec2(i % ${TEX_W}, i / ${TEX_W}), 0); } + + // Lengyel's banded winding-number coverage for one ray along +x from p. + // Curves are fetched via the band's index list; 'swap' mirrors x/y for the + // vertical (+y) ray, whose winding sign flips (handedness reversal). + float rayCoverage(vec2 p, float ppem, int bandTexel, bool swap) { + vec4 band = T(bandTexel); + int count = int(band.y); + float cov = 0.0; + for (int j = 0; j < 256; j++) { + if (j >= count) break; + vec4 idx4 = T(int(band.x) + (j >> 2)); + int cb = int(j % 4 == 0 ? idx4.x : j % 4 == 1 ? idx4.y : j % 4 == 2 ? idx4.z : idx4.w); + vec4 A = T(cb); + vec2 e = T(cb + 1).xy; + // Swap the CURVE points before subtracting: p is already in swapped + // coordinates for the vertical ray ((A.xy - p).yx would be wrong). + vec2 p1 = (swap ? A.yx : A.xy) - p; + vec2 p2 = (swap ? A.wz : A.zw) - p; + vec2 p3 = (swap ? e.yx : e.xy) - p; + + uint code = (0x2E74u >> ((p1.y > 0.0 ? 2u : 0u) + (p2.y > 0.0 ? 4u : 0u) + (p3.y > 0.0 ? 8u : 0u))) & 3u; + if (code != 0u) { + vec2 a = p1 - p2 * 2.0 + p3; + vec2 b = p1 - p2; + float t1, t2; + if (abs(a.y) < 1e-6) { + float t = p1.y / (2.0 * b.y); + t1 = t; t2 = t; + } else { + float d = sqrt(max(b.y * b.y - a.y * p1.y, 0.0)); + t1 = (b.y - d) / a.y; + t2 = (b.y + d) / a.y; + } + if ((code & 1u) != 0u) { + float x = (a.x * t1 - b.x * 2.0) * t1 + p1.x; + cov += clamp(x * ppem + 0.5, 0.0, 1.0); + } + if (code > 1u) { + float x = (a.x * t2 - b.x * 2.0) * t2 + p1.x; + cov -= clamp(x * ppem + 0.5, 0.0, 1.0); + } + } + } + return cov; + } + + void main() { + vec4 bbox = T(vBase); + vec4 bases = T(vBase + 1); + vec2 fw = fwidth(vEm); + + vec2 span = max(bbox.zw - bbox.xy, vec2(1e-6)); + int hb = clamp(int((vEm.y - bbox.y) / span.y * float(${BANDS})), 0, ${BANDS - 1}); + int vb = clamp(int((vEm.x - bbox.x) / span.x * float(${BANDS})), 0, ${BANDS - 1}); + + float covH = rayCoverage(vEm, 1.0 / max(fw.x, 1e-9), int(bases.x) + hb, false); + float covV = -rayCoverage(vEm.yx, 1.0 / max(fw.y, 1e-9), int(bases.y) + vb, true); + float alpha = clamp((covH + covV) * 0.5, 0.0, 1.0); + if (alpha < 0.004) discard; + fragOut = vec4(vColor, alpha); + } +`; + +/** Accumulates positioned strings and builds one merged mesh (single draw call). */ +export class SlugTextBatch { + private pos: number[] = []; + private emuv: number[] = []; + private gbase: number[] = []; + private color: number[] = []; + private index: number[] = []; + + constructor(private engine: SlugTextEngine) {} + + /** + * alignH: 0/3/5=left datum, 1/4=center, 2=right. + * alignV: 0=baseline, 1=bottom, 2=middle, 3=top (cap-height datum, matching + * the previous canvas-sprite behaviour). Multi-line via '\n'. + */ + add(text: string, pos: { x: number; y: number }, height: number, rotation: number, + colorInt: number, alignH: number, alignV: number): void { + const eng = this.engine; + const lines = String(text).split('\n'); + // Glyph outlines are em-scaled (getPath(0,0,1)), so raw cap ink height is + // capHeightEm em, not 1em. Divide by it so the rendered cap height equals + // the DWG `height` field, matching the canvas-sprite fallback's ascent fit. + const scale = height / eng.capHeightEm; + const lineStep = height * 5 / 3; // CAD default MTEXT line spacing + const blockH = (lines.length - 1) * lineStep + height; + + // DXF group 72 = 4 ("Middle") centers both axes on the alignment point; + // group 73 (vertical) is ignored. alignH already centers horizontally (below); + // force vertical to middle here too, else vA=0 text (block-internal plain + // TEXT, e.g. titleblock labels) renders half a line too high. + if (alignH === 4) alignV = 2; + + let baseline0: number; + if (alignV === 3) baseline0 = -height; // top datum + else if (alignV === 2) baseline0 = blockH / 2 - height; // middle + else baseline0 = (lines.length - 1) * lineStep; // baseline/bottom + + const cos = Math.cos(rotation), sin = Math.sin(rotation); + const r = ((colorInt >> 16) & 0xFF) / 255, g = ((colorInt >> 8) & 0xFF) / 255, b = (colorInt & 0xFF) / 255; + + for (let li = 0; li < lines.length; li++) { + const line = lines[li]; + if (!line) continue; + const wEm = eng.measureEm(line); + const ox = (alignH === 1 || alignH === 4) ? -wEm * scale / 2 : alignH === 2 ? -wEm * scale : 0; + const oy = baseline0 - li * lineStep; + let pen = 0; + for (const ch of line) { + const rec = eng.ensureGlyph(ch.codePointAt(0)!); + if (rec.base >= 0) { + const [x0, y0, x1, y1] = this.engine.bboxOf(rec); + const qx0 = x0 - PAD, qy0 = y0 - PAD, qx1 = x1 + PAD, qy1 = y1 + PAD; + const v = this.pos.length / 3; + for (const [ex, ey] of [[qx0, qy0], [qx1, qy0], [qx1, qy1], [qx0, qy1]] as const) { + const lx = ox + (pen + ex) * scale; + const ly = oy + ey * scale; + this.pos.push(pos.x + lx * cos - ly * sin, pos.y + lx * sin + ly * cos, 1); + this.emuv.push(ex, ey); + this.gbase.push(rec.base); + this.color.push(r, g, b); + } + this.index.push(v, v + 1, v + 2, v, v + 2, v + 3); + } + pen += rec.advance; + } + } + } + + /** Build the merged mesh; null when nothing was added. */ + build(): THREE.Mesh | null { + if (!this.index.length) return null; + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.Float32BufferAttribute(this.pos, 3)); + geo.setAttribute('emuv', new THREE.Float32BufferAttribute(this.emuv, 2)); + geo.setAttribute('gbase', new THREE.Float32BufferAttribute(this.gbase, 1)); + geo.setAttribute('tcolor', new THREE.Float32BufferAttribute(this.color, 3)); + geo.setIndex(this.index); + const mat = new THREE.ShaderMaterial({ + glslVersion: THREE.GLSL3, + vertexShader: VERT, + fragmentShader: FRAG, + uniforms: { dataTex: { value: this.engine.texture() } }, + transparent: true, + depthTest: false, + depthWrite: false, + side: THREE.DoubleSide, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.frustumCulled = true; + return mesh; + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a3fc3af --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedModules": true, + "allowJs": true + }, + "include": ["src/**/*", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..661447a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { port: 5173, strictPort: true }, + // HMDesign (git submodule) has its own example/export HTML entries with their + // own `@/...` path aliases — without pinning `entries`, Vite's dependency + // scanner crawls every *.html under the project root and trips over them. + optimizeDeps: { include: ['three', 'dxf-parser', 'opentype.js'], entries: ['index.html'] }, + assetsInclude: ['**/*.wasm'], +});