import { customElement, noShadowDOM } from "solid-element"; import { Show, For, createResource, createMemo, createSignal, onCleanup, } from "solid-js"; import { resolvePath } from "./utils/path"; import { traceImage, type TracedLayer } from "./utils/image-tracer"; import { generate3MF, type ExtrusionSettings } from "./utils/3mf-generator"; import MdTokenViewer from "./md-token-viewer"; export interface TokenProps { size?: number; // 模型整体尺寸 (mm), 默认 50 defaultThickness?: number; // 默认图层厚度 (mm), 默认 2 } interface LayerSettings { id: string; name: string; enabled: boolean; thickness: number; color: string; } customElement("md-token", { size: 50, defaultThickness: 2 }, (props, { element }) => { noShadowDOM(); const [showEditor, setShowEditor] = createSignal(false); const [layers, setLayers] = createSignal([]); const [modelUrl, setModelUrl] = createSignal(null); const [isGenerating, setIsGenerating] = createSignal(false); const [error, setError] = createSignal(null); // 从 element 的 textContent 获取图片路径 const rawSrc = element?.textContent?.trim() || ""; // 隐藏原始文本内容 if (element) { element.textContent = ""; } // 从父节点 article 的 data-src 获取当前 markdown 文件完整路径 const articleEl = element?.closest("article[data-src]"); const articlePath = articleEl?.getAttribute("data-src") || ""; // 解析相对路径 const resolvedSrc = resolvePath(articlePath, rawSrc); // 加载图片 const loadImage = (src: string): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); }; const [image] = createResource(resolvedSrc, loadImage); // 图像加载完成后进行矢量追踪 const [traceResult] = createResource( () => image(), async (img) => { if (!img) return null; try { const traced = await traceImage(img); return traced; } catch (e) { setError(e instanceof Error ? e.message : "矢量追踪失败"); return null; } } ); // 初始化图层设置 createMemo(() => { const result = traceResult(); if (result && result.layers.length > 0) { const layerSettings: LayerSettings[] = result.layers.map((layer, index) => ({ id: layer.id, name: layer.name || `图层 ${index + 1}`, enabled: true, thickness: props.defaultThickness || 2, color: `#${layer.color.getHexString()}` || `hsl(${(index * 60) % 360}, 70%, 50%)`, })); setLayers(layerSettings); } }); // 生成 3MF 模型 const generateModel = async () => { if (!image()) return; setIsGenerating(true); setError(null); try { const enabledLayers = layers().filter((l) => l.enabled); if (enabledLayers.length === 0) { setError("请至少选择一个图层"); setIsGenerating(false); return; } const settings: ExtrusionSettings = { size: props.size || 50, layers: enabledLayers.map((l) => ({ id: l.id, thickness: l.thickness, })), }; const modelBlob = await generate3MF(image()!, traceResult()!, settings); const url = URL.createObjectURL(modelBlob); setModelUrl(url); } catch (e) { setError(e instanceof Error ? e.message : "生成模型失败"); } finally { setIsGenerating(false); } }; // 更新图层厚度 const updateLayerThickness = (layerId: string, thickness: number) => { setLayers((prev) => prev.map((l) => (l.id === layerId ? { ...l, thickness } : l)) ); }; // 切换图层启用状态 const toggleLayer = (layerId: string) => { setLayers((prev) => prev.map((l) => (l.id === layerId ? { ...l, enabled: !l.enabled } : l)) ); }; // 下载 3MF 文件 const downloadModel = () => { const url = modelUrl(); if (!url) return; const a = document.createElement("a"); a.href = url; a.download = `token-${Date.now()}.3mf`; a.click(); }; // 清理 URL onCleanup(() => { const url = modelUrl(); if (url) { URL.revokeObjectURL(url); } }); const visible = createMemo(() => !image.loading && !!image()); return (
{/* 预览区域 */}
{/* 原始图片预览 */}

原始图片

Token source
{/* 3D 模型预览 */}

3D 模型预览

{/* 错误信息 */}
⚠️ {error()}
{/* 控制面板 */}

🎯 Token 设置

{/* 整体尺寸设置 */}
{ 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
)}
{/* 生成按钮 */}
{/* 加载状态 */}
加载图片中...
); });