diff --git a/docs/development.md b/docs/development.md index f9a8302..998b9a7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -41,7 +41,7 @@ ttrpg-tools/ │ │ ├── md-pins.tsx # 标记组件 │ │ ├── md-commander/ # 命令追踪器 │ │ ├── md-yarn-spinner.tsx # 叙事线组件 -│ │ ├── md-token.tsx # 代币组件 + │ │ ├── Article.tsx # 文章组件 │ │ ├── Sidebar.tsx # 侧边栏 │ │ ├── FileTree.tsx # 文件树 @@ -77,7 +77,7 @@ ttrpg-tools/ | 样式 | Tailwind CSS 4 + @tailwindcss/typography | | Markdown | marked + marked-directive + marked-alert + marked-gfm-heading-id | | 图表 | mermaid | -| 3D | three.js | + | 测试 | Jest | | CLI | commander + chokidar | diff --git a/docs/markdown.md b/docs/markdown.md index 34d447f..95ee1e7 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -431,26 +431,7 @@ layers="字段:起始行,起始列 - 结束列,字体大小" :md-yarn-spinner[./story.yarn] ``` -### 🪙 代币组件 (md-token) -用于展示游戏代币/棋子。 - -```markdown -:md-token[./token.png] -``` - -### 🎨 代币预览组件 (md-token-viewer) - -用于 3D 预览 3MF 格式的代币模型。 - -```markdown -:md-token-viewer[./token.3mf] -``` - -**功能:** -- 使用 Three.js 渲染 3D 模型 -- 支持鼠标拖拽旋转 -- 自动旋转展示 ### 📋 命令追踪器 (md-commander) diff --git a/docs/mcp.md b/docs/mcp.md index 8a75013..04599bd 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -335,7 +335,7 @@ MCP 服务器提供以下 Resources,包含 TTRPG Tools 的文档和参考材 - `:md-table` - 表格组件 - `:md-deck` - 卡牌组件 - `:md-yarn-spinner` - 叙事线组件 - - `:md-token` - 代币组件 + - `:md-commander` - 命令追踪器 - YAML 标签 - Mermaid 图表 diff --git a/package-lock.json b/package-lock.json index 2b1f43a..3f05526 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@modelcontextprotocol/sdk": "^0.5.0", "@solidjs/router": "^0.15.0", "@thisbeyond/solid-dnd": "^0.7.5", - "@types/three": "^0.183.1", "aedes": "^1.1.1", "chokidar": "^5.0.0", "commander": "^14.0.3", @@ -27,18 +26,16 @@ "mqtt": "^5.15.1", "solid-element": "^1.9.1", "solid-js": "^1.9.3", - "three": "^0.183.2", - "three-3mf-exporter": "^45.1.0", "uuid": "^14.0.1", "ws": "^8.21.0", "yarn-spinner-loader": "^0.1.0", "zod": "^4.4.3" }, "bin": { - "ttrpg": "dist/cli/index.js" + "ttrpg": "dist/cli/index.js", + "ttt": "dist/cli/index.js" }, "devDependencies": { - "@image-tracer-ts/core": "^1.0.2", "@rsbuild/core": "^1.1.8", "@rsbuild/plugin-babel": "^1.1.0", "@rsbuild/plugin-solid": "^1.1.0", @@ -873,12 +870,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "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==", - "license": "Apache-2.0" - }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", @@ -927,13 +918,6 @@ "mlly": "^1.8.0" } }, - "node_modules/@image-tracer-ts/core": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@image-tracer-ts/core/-/core-1.0.2.tgz", - "integrity": "sha512-IY1/AMqvu6444dEwaFwJXwskp2fyKTFFVKvKHXBCT7hGz7OLRLHrWGHJrCsVw6HPSeucoHD5j/gtieWVSzocEw==", - "dev": true, - "license": "MIT" - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2944,12 +2928,6 @@ "dev": true, "license": "MIT" }, - "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==", - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3356,27 +3334,6 @@ "dev": true, "license": "MIT" }, - "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==", - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.183.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", - "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "@webgpu/types": "*", - "fflate": "~0.8.2", - "meshoptimizer": "~1.0.1" - } - }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -3398,12 +3355,6 @@ "dev": true, "license": "MIT" }, - "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==", - "license": "MIT" - }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -3440,12 +3391,6 @@ "d3-transition": "^3.0.1" } }, - "node_modules/@webgpu/types": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", - "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", - "license": "BSD-3-Clause" - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -4450,12 +4395,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -5585,41 +5524,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.10.tgz", - "integrity": "sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.1", - "strnum": "^2.2.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastparallel": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", @@ -5649,12 +5553,6 @@ "bser": "2.1.1" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6091,12 +5989,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -6256,12 +6148,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7968,18 +7854,6 @@ "node": ">=6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, "node_modules/katex": { "version": "0.16.45", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", @@ -8053,15 +7927,6 @@ "node": ">=6" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -8536,12 +8401,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/meshoptimizer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", - "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8915,12 +8774,6 @@ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -8969,21 +8822,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.1.tgz", - "integrity": "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -9320,21 +9158,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -9589,12 +9412,6 @@ "seroval": "^1.0" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -9862,18 +9679,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -9949,25 +9754,6 @@ "node": ">=8" } }, - "node_modules/three": { - "version": "0.183.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", - "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", - "license": "MIT" - }, - "node_modules/three-3mf-exporter": { - "version": "45.2.0", - "resolved": "https://registry.npmjs.org/three-3mf-exporter/-/three-3mf-exporter-45.2.0.tgz", - "integrity": "sha512-Xwuciopn3ecMhf9muVIRTh/GrSdxUTrdz43fMfazPZdxQ1E0eOBIGEhQEUrGlvRLLcWeaHo5MvZQCClTAMmmog==", - "license": "MIT", - "dependencies": { - "fast-xml-parser": "^5.0.9", - "jszip": "^3.10.1" - }, - "peerDependencies": { - "three": "*" - } - }, "node_modules/tinyexec": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", diff --git a/package.json b/package.json index 40e2bd1..332604d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "ttrpg-tools", "version": "0.0.1", - "description": "A TTRPG toolbox based on solid.js and rsbuild", + "description": "A tabletop RPG toolbox based on solid.js and rsbuild", "type": "module", "bin": { - "ttrpg": "./dist/cli/index.js" + "ttrpg": "./dist/cli/index.js", + "ttt": "./dist/cli/index.js" }, "main": "./dist/index.js", "module": "./dist/index.js", @@ -35,7 +36,6 @@ "@modelcontextprotocol/sdk": "^0.5.0", "@solidjs/router": "^0.15.0", "@thisbeyond/solid-dnd": "^0.7.5", - "@types/three": "^0.183.1", "aedes": "^1.1.1", "chokidar": "^5.0.0", "commander": "^14.0.3", @@ -50,15 +50,12 @@ "mqtt": "^5.15.1", "solid-element": "^1.9.1", "solid-js": "^1.9.3", - "three": "^0.183.2", - "three-3mf-exporter": "^45.1.0", "uuid": "^14.0.1", "ws": "^8.21.0", "yarn-spinner-loader": "^0.1.0", "zod": "^4.4.3" }, "devDependencies": { - "@image-tracer-ts/core": "^1.0.2", "@rsbuild/core": "^1.1.8", "@rsbuild/plugin-babel": "^1.1.0", "@rsbuild/plugin-solid": "^1.1.0", diff --git a/src/App.tsx b/src/App.tsx index 4fc7c34..f952397 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -96,7 +96,7 @@ const App: Component = () => { > ☰ -

TTRPG Tools

+

Tabletop Tools

-
- - -
- {/* 整体尺寸设置 */} -
- - { - const newSize = parseInt(e.target.value); - element?.setAttribute("size", newSize.toString()); - }} - /> - - {props.size || 50}mm - -
- - {/* 图层列表 */} -
-

- 图层设置 -

-
- - {(layer) => ( -
- toggleLayer(layer.id)} - class="w-4 h-4" - /> -
- - {layer.name} - - - - updateLayerThickness( - layer.id, - parseFloat(e.target.value) || 2 - ) - } - /> - mm -
- )} - -
-
- - {/* 生成按钮 */} -
- - - - -
-
- -
- -
- - {/* 加载状态 */} - -
-
- 加载图片中... -
- -
- ); -}); diff --git a/src/components/utils/3mf-generator.ts b/src/components/utils/3mf-generator.ts deleted file mode 100644 index 0052601..0000000 --- a/src/components/utils/3mf-generator.ts +++ /dev/null @@ -1,119 +0,0 @@ -import * as THREE from "three"; -import { exportTo3MF } from "three-3mf-exporter"; -import type { TraceResult } from "./image-tracer"; -import {Vector3} from "three"; - -export interface ExtrusionSettings { - size: number; // 模型整体尺寸 (mm) - layers: Array<{ - id: string; - thickness: number; // 图层厚度 (mm) - }>; -} - -export interface LayerMesh { - id: string; - mesh: THREE.Mesh; - thickness: number; - color: number; -} - -/** - * 从图层索引生成颜色(使用黄金角确保颜色分散) - */ -function generateLayerColor(index: number): number { - const hue = (index * 137.508) % 360; // 黄金角 - return new THREE.Color(`hsl(${hue}, 70%, 50%)`).getHex(); -} - -/** - * 将矢量路径生成 3MF 文件 - * @param image - 原始图片 - * @param traceResult - 矢量追踪结果 - * @param settings - 挤压设置 - * @returns 3MF Blob - */ -export async function generate3MF( - image: HTMLImageElement, - traceResult: TraceResult, - settings: ExtrusionSettings -): Promise { - // 创建 Three.js 场景 - const scene = new THREE.Scene(); - - // 计算缩放比例,使模型适应指定尺寸 - const maxDimension = Math.max(traceResult.width, traceResult.height); - const scale = settings.size / maxDimension; - - // 中心偏移 - const offsetX = -traceResult.width / 2; - const offsetY = -traceResult.height / 2; - - // 为每个启用的图层创建网格 - let currentHeight = 0; - const meshes: LayerMesh[] = []; - let layerIndex = 0; - - let boundingBox = new THREE.Box3(); - for (const layerSetting of settings.layers) { - const layer = traceResult.layers.find((l) => l.id === layerSetting.id); - if (!layer) continue; - - // 创建挤压几何体 - const extrudeSettings: THREE.ExtrudeGeometryOptions = { - depth: layerSetting.thickness, - curveSegments: 36, - bevelEnabled: true, - bevelSize: 0.4 - }; - const geometry: THREE.ExtrudeGeometry = new THREE.ExtrudeGeometry(layer.paths, extrudeSettings); - geometry.computeBoundingBox(); - - // 为该图层生成颜色 - const color = generateLayerColor(layerIndex); - const material = new THREE.MeshStandardMaterial({ - color, - metalness: 0.3, - roughness: 0.7, - }); - - const mesh = new THREE.Mesh(geometry, material); - boundingBox.expandByObject(mesh); - - // 设置图层高度(堆叠) - mesh.position.y = currentHeight; - - scene.add(mesh); - meshes.push({ - id: layerSetting.id, - mesh, - thickness: layerSetting.thickness, - color, - }); - - currentHeight += layerSetting.thickness; - layerIndex++; - } - - const center = boundingBox.getCenter(new Vector3()); - for(const mesh of meshes){ - mesh.mesh.position.sub(center); - } - - if (meshes.length === 0) { - throw new Error("没有可生成的图层"); - } - - // 导出为 3MF - const data = await exportTo3MF(scene); - const blob = new Blob([data], { type: "model/3mf" }); - - // 清理 - scene.clear(); - meshes.forEach(({ mesh }) => { - mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); - }); - - return blob; -} \ No newline at end of file diff --git a/src/components/utils/image-tracer.ts b/src/components/utils/image-tracer.ts deleted file mode 100644 index 6bedfa1..0000000 --- a/src/components/utils/image-tracer.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ImageTracer, Options } from "@image-tracer-ts/core"; -//@ts-ignore -import {SVGLoader, SVGResult} from "three/examples/jsm/loaders/SVGLoader"; -import {Color, ShapePath, Shape} from "three"; - -export interface TracedLayer { - id: string; - name: string; - color: Color; - paths: Shape[]; -} - -export interface PathData { - points: Point[]; - isClosed: boolean; -} - -export interface Point { - x: number; - y: number; -} - -export interface TraceResult { - width: number; - height: number; - layers: TracedLayer[]; -} - -/** - * 将图像转换为矢量路径 - * @param image - 要追踪的图片元素 - * @param options - 追踪选项 - * @returns 追踪结果 - */ -export async function traceImage( - image: HTMLImageElement, - options?: Partial -): Promise { - // 创建 canvas 来获取图片数据 - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - - if (!ctx) { - throw new Error("无法创建 canvas 上下文"); - } - - // 设置 canvas 尺寸为图片原始尺寸 - canvas.width = image.naturalWidth || image.width; - canvas.height = image.naturalHeight || image.height; - - // 绘制图片到 canvas - ctx.drawImage(image, 0, 0); - - // 获取图片数据 - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - - // 默认配置 - 使用 detailed preset 作为基础 - const defaultOptions: Partial = { - ...Options.Presets.default, - numberOfColors: 8, // 限制颜色数量以控制图层数 - minColorQuota: 0.01, // 降低最小颜色占比阈值 - strokeWidth: 0, // 不需要描边 - lineFilter: true, - ...options, - }; - - // 创建追踪器 - const tracer = new ImageTracer(defaultOptions); - - // 执行追踪,返回 SVG 字符串 - const svgString = tracer.traceImage(imageData); - - // 解析 SVG 字符串 - const loader = new SVGLoader(); - const result = loader.parse(svgString) as SVGResult; - const paths: ShapePath[] = result.paths; - - const layers: TracedLayer[] = paths.map((path, i,) => { - return { - id: `layer-${i}`, - name: `颜色层 ${i + 1}`, - color: path.color, - paths: SVGLoader.createShapes(path), - }; - }); - - return { - width: canvas.width, - height: canvas.height, - layers, - }; -} \ No newline at end of file diff --git a/src/components/utils/stl-generator.ts b/src/components/utils/stl-generator.ts deleted file mode 100644 index eba285c..0000000 --- a/src/components/utils/stl-generator.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as THREE from "three"; -import type { TraceResult, PathData, Point } from "./image-tracer"; - -export interface ExtrusionSettings { - size: number; // 模型整体尺寸 (mm) - layers: Array<{ - id: string; - thickness: number; // 图层厚度 (mm) - }>; -} - -export interface LayerMesh { - id: string; - mesh: THREE.Mesh; - thickness: number; -} - -/** - * 将矢量路径生成 STL 文件 - * @param image - 原始图片 - * @param traceResult - 矢量追踪结果 - * @param settings - 挤压设置 - * @returns STL Blob - */ -export async function generateSTL( - image: HTMLImageElement, - traceResult: TraceResult, - settings: ExtrusionSettings -): Promise { - // 创建 Three.js 场景 - const scene = new THREE.Scene(); - - // 计算缩放比例,使模型适应指定尺寸 - const maxDimension = Math.max(traceResult.width, traceResult.height); - const scale = settings.size / maxDimension; - - // 中心偏移 - const offsetX = -traceResult.width / 2; - const offsetY = -traceResult.height / 2; - - // 为每个启用的图层创建网格 - let currentHeight = 0; - const meshes: LayerMesh[] = []; - - for (const layerSetting of settings.layers) { - const layer = traceResult.layers.find((l) => l.id === layerSetting.id); - if (!layer) continue; - - // 创建挤压几何体 - Three.js 支持直接传入 shape 数组 - const extrudeSettings: THREE.ExtrudeGeometryOptions = { - depth: layerSetting.thickness, - curveSegments: 12, - bevelEnabled: false, - }; - - // 直接将所有 shape 传给 ExtrudeGeometry,它会自动处理多个 shape - const geometry = new THREE.ExtrudeGeometry(layer.paths, extrudeSettings); - - // 创建网格并设置位置 - const material = new THREE.MeshBasicMaterial({ color: 0x808080 }); - const mesh = new THREE.Mesh(geometry, material); - - // 设置图层高度(堆叠) - mesh.position.y = currentHeight; - - scene.add(mesh); - meshes.push({ - id: layerSetting.id, - mesh, - thickness: layerSetting.thickness, - }); - - currentHeight += layerSetting.thickness; - } - - if (meshes.length === 0) { - throw new Error("没有可生成的图层"); - } - - // 导出为 STL - const stlString = exportToSTL(scene); - const blob = new Blob([stlString], { type: "model/stl" }); - - // 清理 - scene.clear(); - meshes.forEach(({ mesh }) => { - mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); - }); - - return blob; -} - -/** - * 从路径数据创建 Three.js 形状 - */ -function createShapeFromPath( - path: PathData, - scale: number, - offsetX: number, - offsetY: number -): THREE.Shape | null { - if (path.points.length < 2) return null; - - const shape = new THREE.Shape(); - - // 移动到起点 - const startPoint = path.points[0]; - shape.moveTo( - (startPoint.x + offsetX) * scale, - (startPoint.y + offsetY) * scale - ); - - // 绘制线段到后续点 - for (let i = 1; i < path.points.length; i++) { - const point = path.points[i]; - shape.lineTo( - (point.x + offsetX) * scale, - (point.y + offsetY) * scale - ); - } - - // 如果是闭合路径,闭合形状 - if (path.isClosed) { - shape.closePath(); - } - - return shape; -} - -/** - * 将 Three.js 场景导出为 ASCII STL 格式 - */ -function exportToSTL(scene: THREE.Scene): string { - let output = "solid token\n"; - - scene.traverse((object) => { - if (object instanceof THREE.Mesh && object.geometry) { - const geometry = object.geometry as THREE.BufferGeometry; - const positions = geometry.getAttribute("position") as THREE.BufferAttribute; - const normals = geometry.getAttribute("normal") as THREE.BufferAttribute; - - for (let i = 0; i < positions.count; i += 3) { - // 获取法线 - let normalStr = ""; - if (normals) { - const nx = normals.getX(i); - const ny = normals.getY(i); - const nz = normals.getZ(i); - normalStr = ` normal ${nx.toFixed(6)} ${ny.toFixed(6)} ${nz.toFixed(6)}`; - } - - output += ` facet${normalStr}\n`; - output += " outer loop\n"; - - // 获取三个顶点 - for (let j = 0; j < 3; j++) { - const x = positions.getX(i + j); - const y = positions.getY(i + j); - const z = positions.getZ(i + j); - output += ` vertex ${x.toFixed(6)} ${y.toFixed(6)} ${z.toFixed(6)}\n`; - } - - output += " endloop\n"; - output += " endfacet\n"; - } - } - }); - - output += "endsolid token\n"; - return output; -} diff --git a/src/doc-entries/md-token-viewer.md b/src/doc-entries/md-token-viewer.md deleted file mode 100644 index e7408ac..0000000 --- a/src/doc-entries/md-token-viewer.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -tag: md-token-viewer -icon: 🎨 -title: 代币预览组件 -description: 使用 Three.js 在浏览器中 3D 渲染 3MF 格式的代币模型,支持旋转查看。 -syntax: ':md-token-viewer[./token.3mf]' -props: [] ---- - -**3D 预览模型:** -:md-token-viewer[./dragon.3mf] - -支持鼠标拖拽旋转和自动旋转,从多角度查看模型细节。 - -## 使用场景 - -用于 3D 打印前的代币模型预览。 \ No newline at end of file diff --git a/src/doc-entries/md-token.md b/src/doc-entries/md-token.md deleted file mode 100644 index 7fe7f8d..0000000 --- a/src/doc-entries/md-token.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -tag: md-token -icon: 🪙 -title: 代币组件 -description: 展示游戏代币或棋子的图片。 -syntax: ':md-token[./token.png]' -props: [] ---- - -**展示代币:** -:md-token[./goblin.png] - -代币图片自动渲染为圆形,适合战棋或卡牌中的棋子展示。 - -## 使用场景 - -用于展示怪物代币、角色棋子、道具图标等。 \ No newline at end of file diff --git a/src/index.html b/src/index.html index 1d29f58..45a952a 100644 --- a/src/index.html +++ b/src/index.html @@ -3,7 +3,7 @@ - TTRPG Tools + Tabletop Tools