dsh-motion-ui 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +37 -1
  3. package/VERIFICATION.md +36 -0
  4. package/client.js +1856 -984
  5. package/examples/mechanism-assembly.json +50 -0
  6. package/examples/mechanism-crank.json +103 -0
  7. package/examples/mechanism-gears.json +103 -0
  8. package/index.js +115 -104
  9. package/lib/assets/standalone-runtime.js +3 -1
  10. package/lib/assets/visual.js +3 -1
  11. package/lib/author-expression.js +3 -2
  12. package/lib/authoring-guide.js +2 -1
  13. package/lib/build-info.js +1 -1
  14. package/lib/catalog.js +4 -4
  15. package/lib/components.js +2 -2
  16. package/lib/export-runtime-bundle.js +1 -1
  17. package/lib/expressive-examples.js +6 -0
  18. package/lib/fence-feedback.js +1 -1
  19. package/lib/freeform.js +4 -3
  20. package/lib/mechanism-catalog.js +24 -0
  21. package/lib/mechanism-css.js +13 -0
  22. package/lib/mechanism-entry.js +20 -0
  23. package/lib/mechanism-geometry.js +62 -0
  24. package/lib/mechanism-render.js +24 -0
  25. package/lib/mechanism-runtime-bundle.js +2 -0
  26. package/lib/mechanism-scenes.js +20 -0
  27. package/lib/motion-library.js +18 -0
  28. package/lib/normalize-authoring.js +6 -2
  29. package/lib/protocol.js +23 -8
  30. package/lib/sandbox.js +2 -0
  31. package/lib/style-system.js +28 -0
  32. package/lib/third-party/anatomy-LICENSE.txt +21 -0
  33. package/lib/third-party/anatomy-NOTICE.md +13 -0
  34. package/lib/third-party/anatomy-iso.mjs +674 -0
  35. package/lib/third-party/lucian-uxui-LICENSE.txt +21 -0
  36. package/lib/third-party/lucian-uxui-NOTICE.md +1 -0
  37. package/lib/tool-schema.js +5 -5
  38. package/lib/ui-intent.js +1 -1
  39. package/lib/ui-skill.js +3 -2
  40. package/lib/visual-styles.js +134 -22
  41. package/package.json +92 -92
  42. package/skills/intelligent-ui/SKILL.md +113 -100
@@ -0,0 +1,6 @@
1
+ // Original Motion UI documents applying the reviewed Lucian interaction patterns.
2
+ export const EXPRESSIVE_EXAMPLES=[
3
+ {id:'action',name:'渐变磁吸按钮',description:'实色主按钮、渐变扩散与文字换位;实际打开网页',document:{version:1,title:'继续探索',fallback:'点击按钮打开 GitHub 文档。',presentation:{preset:'brief'},nodes:[{id:'heading',type:'heading',text:'从一个可验证的结果开始',size:24},{id:'text',type:'text',text:'所有风格下,悬停或键盘聚焦可体验渐变扩散;点击实际打开网页。'},{id:'open',type:'link',text:'查看开发指南',href:'https://docs.github.com/',variant:'primary',icon:'arrow'}]}},
4
+ {id:'quantity',name:'数量步进与滚动',description:'原生数字输入与增减按钮,共享精确计算',document:{version:1,title:'数量与费用 · 示例',fallback:'示例单价19元,按数量计算。',presentation:{preset:'brief'},state:{quantity:{type:'number',default:2,min:1,max:10,step:1}},derived:{total:{op:'mul',args:[{const:19},{ref:'state.quantity'}]}},nodes:[{id:'quantity',type:'input',label:'数量',bind:'state.quantity'},{id:'sum',type:'metric',label:'示例总价',value:{ref:'derived.total'},unit:'元',decimals:0},{id:'note',type:'text',text:'示例价格,不是真实报价。所有风格均提供增减按钮与滚动强调。'}]}},
5
+ {id:'elastic',name:'弹性开关联动',description:'点击或拖拽,真实状态驱动形变与费用变化',document:{version:1,title:'配送费用 · 示例',fallback:'基础费用29元,加急费用5元。',presentation:{preset:'brief'},state:{urgent:{type:'boolean',default:false}},derived:{fee:{op:'add',args:[{const:29},{op:'if',args:[{ref:'state.urgent'},{const:5},{const:0}]}]}},nodes:[{id:'urgent',type:'toggle',label:'加急配送',bind:'state.urgent'},{id:'fee',type:'metric',label:'示例合计',value:{ref:'derived.fee'},unit:'元',decimals:0},{id:'note',type:'text',text:'仅是费用计算示例,不会创建订单或请求配送。'}]}}
6
+ ];
@@ -22,7 +22,7 @@ export function registerFenceFeedback(ctx,enabled=()=>true){
22
22
  if(event.type==='assistant/message'){state.lastText=textOf(event.data.message?.content).slice(0,262144);state.blocks.set(event.data.step??0,state.lastText)}
23
23
  if(event.type==='tool/call')state.seenToolCall=true;
24
24
  if(event.type==='user/message'&&event.data.source?.kind==='user'){state.request=textOf(event.data.content).slice(0,8192);state.silentRequested=/(?:不用|不必|无需|不要)回复|do not (?:respond|reply)|no (?:response|reply) (?:needed|required)/i.test(state.request)}
25
- if(event.type==='tool/call'&&['render_text','render_ui','render_canvas','update_ui','patch_ui'].includes(event.data.name))state.calls.set(event.data.callId,event.data.name);
25
+ if(event.type==='tool/call'&&['render_text','render_ui','render_canvas','render_mechanism','update_ui','patch_ui'].includes(event.data.name))state.calls.set(event.data.callId,event.data.name);
26
26
  if(event.type==='tool/result'){
27
27
  const message=event.data.message,name=state.calls.get(message?.toolCallId);
28
28
  if(name){if(message.isError){const diagnostic=name+': '+textOf(message.content).slice(0,1200);state.toolFailures.set(fingerprint(diagnostic),diagnostic)}else state.delivered=true}
package/lib/freeform.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { toolJson } from './tool-json.js';
2
2
  import { validateDocument } from './protocol.js';
3
3
  import { AUTHORING_GUIDE } from './authoring-guide.js';
4
- export function canvasDocument({title,fallback,html,css,script,height,state,data,derived}){
4
+ export function canvasDocument({title,fallback,html,css,script,height,libraries,state,data,derived}){
5
5
  const values={state:toolJson(state??{},'state'),data:toolJson(data??{},'data'),derived:toolJson(derived??{},'derived')};
6
6
  for(const [key,value] of Object.entries(values))if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('DSH UI: '+key+' must be an object');
7
- return validateDocument({version:1,title,fallback,...values,nodes:[{id:'canvas',type:'sandbox',title,fallback,html,...(css===undefined?{}:{css}),...(script===undefined?{}:{script}),...(height===undefined?{}:{height}),stateKeys:Object.keys(values.state),dataKeys:Object.keys(values.data),derivedKeys:Object.keys(values.derived)}]});
7
+ return validateDocument({version:1,title,fallback,...values,nodes:[{id:'canvas',type:'sandbox',title,fallback,html,...(css===undefined?{}:{css}),...(script===undefined?{}:{script}),...(height===undefined?{}:{height}),...(libraries===undefined?{}:{libraries}),stateKeys:Object.keys(values.state),dataKeys:Object.keys(values.data),derivedKeys:Object.keys(values.derived)}]});
8
8
  }
9
9
  export const FREE_UI_CONTRACT=AUTHORING_GUIDE+`\nYou have a freeform visual UI surface (static or interactive) in the conversation. Choose the graphics, structure, typography, colors, layout and animation yourself to suit the user's intent. No design template, card composition, accent palette, example UI or dashboard convention is prescribed. Use ordinary text when a UI would not help.
10
- Call render_canvas({title,fallback,html,css?,script?,height?,state?,data?,derived?}) directly, not inside PTC/run_code. A stable inert HTML/SVG preview appears progressively during generation; after the whole assistant turn ends, parameter restoration enables controls. Prefer title/fallback/css before html for early styled previews. Do not claim interaction is active merely because the tool succeeded. Source field order does not affect validation. Optional host .ui-* primitives exist but no composition is required. html is a body fragment supporting HTML/SVG; put all CSS in css and JavaScript in script, use addEventListener rather than HTML event attributes. Each source field max 49152 characters, whole document max128KiB, height160..1000. Code runs only after commit and parameter restoration. No external resources, imports, eval or host APIs; the isolated frame is not a CPU/memory hard limit. The existing frame is updated with inspect_ui/patch_ui, never arbitrary host access.
10
+ Call render_canvas({title,fallback,html,css?,script?,height?,libraries?,state?,data?,derived?}) directly, not inside PTC/run_code. A stable inert HTML/SVG preview appears progressively during generation; after the whole assistant turn ends, parameter restoration enables controls. Prefer title/fallback/css before html for early styled previews. Do not claim interaction is active merely because the tool succeeded. Source field order does not affect validation. Optional host .ui-* primitives exist but no composition is required. html is a body fragment supporting HTML/SVG; put all CSS in css and JavaScript in script, use addEventListener rather than HTML event attributes. Each source field max 49152 characters, whole document max128KiB, height160..1000. Code runs only after commit and parameter restoration. No external resources, imports, eval or host APIs; the isolated frame is not a CPU/memory hard limit. The existing frame is updated with inspect_ui/patch_ui, never arbitrary host access.
11
11
  The user-facing content and all controls may be authored inside your canvas. State important to the user belongs in state, e.g. {speed:{type:"number",default:1,min:0.5,max:3,step:0.5},running:{type:"boolean",default:false}}. Strings require type:"string",default; options:[string,...] is optional. Structured lists/objects use type:"json",default:[]/{} (16KiB per value). Bind custom controls through DSHUI.setState({speed:2}); use DSHUI.subscribe(snapshot=>...) to draw snapshot.state/data/derived. Do not write state from each animation frame or echo subscribe updates back into setState. State updates are validated and persisted. Reload may reset transient animation phase.
12
12
  Prefer DSHUI.animate(({time,delta,reducedMotion,visible})=>...) for motion; seconds, delta capped0.05, returns unsubscribe. Integrate delta only while running; reducedMotion needs a meaningful static view. Stop/replacement disposes the frame. DSHUI.formatNumber(value, decimals=2) formats finite numbers with 0..6 decimal places and comma grouping, exactly matching native metrics and fallback; use it for shared readouts instead of mixing toFixed rounding. Theme is in snapshot.theme and CSS variables --canvas-bg/soft/ink/muted/line/accent. Use readable text and named keyboard-operable controls, adapt to narrow and wide widths; ensure information remains available in a useful fallback. Be truthful about data, assumptions and simulation accuracy.
13
+ For common mechanisms the optional render_mechanism({scene:"assembly"|"gears"|"crank",title?,rpm?,teeth?,radius?,running?,angle?,explode?}) delivers a complete adjustable diagram without writing HTML. Fixed driver16 teeth / rod length70; source-free convenience is optional in free mode. For custom assemblies, exploded views, gears or crank mechanisms, an optional local helper avoids writing large SVG meshes: call ui_catalog({topic:"mechanism"}) for summaries or add scene:"assembly"/"gears"/"crank" for one complete example. A sandbox node (or render_canvas) enables it with libraries:["mechanism"], then DSHUI.mechanism.mountDemo("#diagram","gears",{bind:{rpm:"rpm",running:"running"}}) uses declared shared state. The helper also exposes mount, scenes, gearPair, sliderCrank and follow. This is explanatory geometry/kinematics, not engineering collision, force or manufacturing validation. Keep ordinary simple diagrams small; do not impose high part counts or idle animations.
13
14
  If native controls or mixed component layouts would help, render_ui and ui_catalog remain available; query the catalog for their exact contract when needed. They are optional, not the default layout.
14
15
  inspect_ui defaults to a compact summary; detail:"nodes",nodeIds:["canvas"] reads canvas source. patch_ui requires documentId,current documentRevision,changes:[{id:"canvas",props:{html/css/script/...}}]. It preserves declared state and other nodes; replacing source restarts that canvas. Observations are heuristic and scoped to recorded viewport/theme/revision, not a beauty or safety score. Empty reports mean unobserved, do not poll or invent success. Limit automatic correction to two rounds per user request, then simplify or explain unresolved problems. Explicit new user edits start a new request. This is a behavior budget, not a provider inference timeout.`;
@@ -0,0 +1,24 @@
1
+ import {assemblyScene,gearsScene,crankScene} from './mechanism-scenes.js';
2
+ import {sceneSvg} from './mechanism-render.js';
3
+ import {MECHANISM_CSS} from './mechanism-css.js';
4
+ import {validateDocument} from './protocol.js';
5
+ export const MECHANISM_GUIDE=`Mechanism illustrations: when internal structure, assembly order, power transfer or rotary-to-linear motion helps explain the question, prefer render_mechanism({scene:"assembly"/"gears"/"crank",rpm?:20,teeth?:32,radius?:20,running?:false,angle?:0,explode?:0,title?:string}) for the complete working diagram. No source code or catalog lookup is necessary for these three scenes. Use the optional local mechanism library for custom structures. ui_catalog({topic:"mechanism",scene:"assembly"/"gears"/"crank"}) returns a compact render_mechanism call; customize its initial parameters and explanation without copying source. Do not search installation source, reproduce a huge SVG engine, require dozens of parts or animate unrelated decoration. libraries:["mechanism"] enables DSHUI.mechanism inside a sandbox only. Keep informative inert HTML/SVG for previews, controls bound to declared state, and all continuous motion on DSHUI.animate. A moving picture must express a real relation: external gear RPM is negative inputRPM * driverTeeth / drivenTeeth; slider-crank keeps rod length constant. Explain units and limits; these are educational shapes and kinematics, not validated engineering drawings or force/contact/collision simulations.`;
6
+ export const MECHANISM_SCENES=[{id:'assembly',title:'分层装配与部件职责',description:'22个部件、按组选择、连续拆解、隐藏外壳。'},{id:'gears',title:'齿轮传动与速比',description:'改变从动齿数、输入转速,观察反向转动与精确速比。'},{id:'crank',title:'曲柄连杆与往复运动',description:'改变曲柄半径和角度,连杆长度保持不变,观察滑块位置与行程。'}];
7
+ const n=(value,min,max,step=1)=>({type:'number',default:value,min,max,step});
8
+ export function mechanismExample(name){
9
+ if(!MECHANISM_SCENES.some(s=>s.id===name))throw Error('Motion mechanism: scene must be assembly/gears/crank');
10
+ const scene=({assembly:assemblyScene,gears:gearsScene,crank:crankScene})[name]();
11
+ const state={explode:n(name==='assembly'?.45:0,0,1,.01),selected:{type:'string',default:'all'},cutaway:{type:'boolean',default:name==='gears'}};
12
+ const bind={explode:'explode',selected:'selected',cutaway:'cutaway'},nodes=[];
13
+ if(name!=='assembly'){
14
+ Object.assign(state,{rpm:n(20,0,90),running:{type:'boolean',default:false},angle:n(0,0,360)});Object.assign(bind,{rpm:'rpm',running:'running',angle:'angle'});
15
+ nodes.push({id:'rpm',type:'slider',label:'输入转速(rpm)',bind:'state.rpm'},{id:'angle',type:'slider',label:'手动角度(°,暂停时调节)',bind:'state.angle'},{id:'running',type:'toggle',label:'运行机械示意',bind:'state.running'});
16
+ }
17
+ if(name==='gears'){state.teeth=n(32,16,48);bind.teeth='teeth';nodes.unshift({id:'teeth',type:'slider',label:'从动轮齿数(主动轮16齿)',bind:'state.teeth'})}
18
+ if(name==='crank'){state.radius=n(20,10,28);bind.radius='radius';nodes.unshift({id:'radius',type:'slider',label:'曲柄半径(示意长度单位)',bind:'state.radius'})}
19
+ const fallback=name==='assembly'?'底座支撑功能板,处理模块上方为散热层,外壳保护内部。拆解表示装配关系,不表示受力运动。':name==='gears'?'外啮合齿轮反向转动。主动轮16齿、20 rpm,从动轮默认32齿、-10 rpm,中心距由模数和齿数确定。齿形为简化示意,不用于加工。':'旋转曲柄带动定长连杆,滑块沿导轨往复。默认半径20、杆长70,完整行程40,位置由几何约束计算。非动力学仿真。';
20
+ nodes.unshift({id:'mechanism',type:'sandbox',title:scene.title,fallback,height:560,libraries:['mechanism'],stateKeys:Object.keys(state),html:'<div id="diagram"><section class="mui-mechanism">'+sceneSvg(scene,{explode:state.explode.default,cutaway:state.cutaway.default,preview:true})+'<p>'+fallback+'</p></section></div>',css:MECHANISM_CSS,script:'const diagram=DSHUI.mechanism.mountDemo("#diagram",'+JSON.stringify(name)+',{title:"",bind:'+JSON.stringify(bind)+'});addEventListener("pagehide",()=>diagram.destroy(),{once:true});'});
21
+ nodes.push({id:'accuracy',type:'text',text:fallback});
22
+ return validateDocument({version:1,title:scene.title,fallback,state,nodes});
23
+ }
24
+ export function mechanismCatalog(scene){return{topic:'mechanism',guide:MECHANISM_GUIDE,scenes:MECHANISM_SCENES,...(scene?(MECHANISM_SCENES.some(s=>s.id===scene)?{tool:'render_mechanism',args:{scene},example:'For a custom scene enable libraries:["mechanism"] and call DSHUI.mechanism.mount/mountDemo in script; full standalone documents are shipped in examples/mechanism-*.json.'}:(()=>{throw Error('Motion mechanism: scene must be assembly/gears/crank')})()):{}),api:{enable:'sandbox.libraries:["mechanism"] or render_canvas libraries:["mechanism"]',mountDemo:'DSHUI.mechanism.mountDemo(target,"assembly"|"gears"|"crank",{bind:{explode,selected,cutaway,rpm,teeth,running,angle,radius},driverTeeth?:16,module?:1.5,rod?:70}) — bind values are declared state key names; omit unused bindings. Returns update/pose/stats/destroy.',mount:'DSHUI.mechanism.mount(target,{title?,parts:[...],groups?:[{id,label,description?}],bounds?:[minXYZ,maxXYZ],azimuth?,elevation?,bind?:{explode,selected,cutaway},onSelect?}) returns update({explode:0..1,selected:"all"|partId|groupId,cutaway:boolean,pose:{partId:{at?,rotation?,from?,to?}},readout?}), pose(), stats(), destroy(). Rotations in radians; bounds include full exploded positions.',parts:'1..96 unique identifiers. box:{id,type:"box",size:[x,y,z],at?,round?}; cylinder:{id,type:"cylinder",radius,height?,inner?,axis?:"x"|"y"|"z",at?}; gear:{id,type:"gear",radius,teeth:6..64,module?,height?,inner?,at?}; rod/spring:{id,type:"rod"|"spring",from:[x,y,z],to:[x,y,z],radius?,turns?}. All accept label?,description?,group?,material?:metal/dark/accent/warm/cool,explode?:[x,y,z],cover?:boolean,rotation?:radians.',math:'gearPair({driverTeeth,drivenTeeth,rpm,angle,module}) returns driverAngle/drivenAngle/driverRpm/drivenRpm/ratio/radii/centerDistance; sliderCrank({angle,radius,rod,z?}) returns crank/piston/position/stroke. follow(value,target,delta,tau?) returns exponential easing. Use DSHUI.animate for custom movement; do not write shared state per frame.',limits:'No external resources or extra permissions. 48Ki characters per model source; 128KiB document. Helpers inject only for opted-in frames. Six guided styles propagate from host; free mode has neutral helper defaults and allows custom CSS. Cover hide is an illustrative cutaway, not a solid clipping plane. Face depth sorting is approximate; not an engineering collision or occlusion audit.'}}}
@@ -0,0 +1,13 @@
1
+ export const MECHANISM_CSS=`
2
+ .mui-mechanism{font:15px/1.7 system-ui,sans-serif;color:var(--canvas-ink);background:var(--canvas-bg);border:1px solid var(--canvas-line);border-radius:24px;padding:20px;max-width:100%;box-sizing:border-box}.mui-mechanism *{box-sizing:border-box}.mui-mechanism h2{font-size:22px;line-height:1.35;margin:0 0 8px;font-weight:750}.mui-mechanism p{margin:0;color:var(--canvas-muted)}
3
+ .mui-mechanism-svg{display:block;width:100%;height:auto;max-height:420px;touch-action:pan-y}.mui-mechanism-svg path{stroke:color-mix(in srgb,var(--canvas-ink) 38%,var(--canvas-bg));stroke-width:.8;stroke-linejoin:round;vector-effect:non-scaling-stroke;cursor:pointer;fill:var(--mech-base,var(--canvas-soft))}.mui-mechanism-svg [data-material=metal]{--mech-base:color-mix(in srgb,var(--canvas-muted) 32%,var(--canvas-bg))}.mui-mechanism-svg [data-material=dark]{--mech-base:color-mix(in srgb,var(--canvas-ink) 78%,var(--canvas-bg))}.mui-mechanism-svg [data-material=accent]{--mech-base:var(--canvas-accent)}.mui-mechanism-svg [data-material=cool]{--mech-base:color-mix(in srgb,#22998a 70%,var(--canvas-bg))}.mui-mechanism-svg [data-material=warm]{--mech-base:color-mix(in srgb,#dd9c42 75%,var(--canvas-bg))}
4
+ .mui-mechanism-svg [data-tone="0"]{fill:color-mix(in srgb,var(--mech-base) 74%,var(--canvas-bg))}.mui-mechanism-svg [data-tone="1"]{fill:var(--mech-base)}.mui-mechanism-svg [data-tone="2"]{fill:color-mix(in srgb,var(--mech-base) 85%,var(--canvas-ink))}.mui-mechanism-svg [data-tone="3"]{fill:color-mix(in srgb,var(--mech-base) 68%,var(--canvas-ink))}.mui-mechanism-svg [data-line]{fill:none;stroke:var(--mech-base);stroke-width:2}.mui-mechanism-svg [data-active=false]{opacity:.2}.mui-mechanism-svg [data-active=true]{opacity:1}
5
+ .mui-mechanism-nav{display:flex;flex-wrap:wrap;gap:7px;margin:8px 0 12px}.mui-mechanism button{font:inherit;color:var(--canvas-ink);background:var(--canvas-bg);border:1px solid var(--canvas-line);border-radius:999px;padding:6px 13px;cursor:pointer;min-height:36px}.mui-mechanism button[aria-pressed=true]{color:var(--canvas-bg);background:var(--canvas-ink);border-color:var(--canvas-ink)}.mui-mechanism button:focus-visible,.mui-mechanism input:focus-visible{outline:2px solid var(--canvas-accent);outline-offset:3px}.mui-mechanism button:disabled{opacity:.5;cursor:default}
6
+ .mui-mechanism-controls{display:flex;align-items:center;flex-wrap:wrap;gap:14px;padding:12px 14px;border-radius:16px;background:var(--canvas-soft);margin:8px 0}.mui-mechanism-controls label{display:flex;align-items:center;gap:9px;flex:1 1 180px;min-width:0}.mui-mechanism-controls input[type=range]{flex:1;min-width:70px;accent-color:var(--canvas-accent)}.mui-mechanism-controls input[type=checkbox]{accent-color:var(--canvas-accent)}.mui-mechanism-controls output{font-variant-numeric:tabular-nums;min-width:40px;text-align:right;font-weight:650}.mui-mechanism-readout{display:block;padding:10px 0;font-weight:700;font-variant-numeric:tabular-nums;color:var(--canvas-ink)}.mui-mechanism-description{min-height:0;font-size:14px}
7
+ .mui-mechanism[data-style=editorial]{border:0;border-top:1px solid var(--canvas-line);border-radius:0;padding:16px 0}.mui-mechanism[data-style=editorial] h2{font-family:ui-serif,Georgia,serif;font-weight:650}.mui-mechanism[data-style=editorial] .mui-mechanism-controls{border-radius:0;background:transparent;border-block:1px solid var(--canvas-line)}
8
+ .mui-mechanism[data-style=technical]{border-radius:10px}.mui-mechanism[data-style=technical] button,.mui-mechanism[data-style=technical] .mui-mechanism-controls{border-radius:6px}.mui-mechanism[data-style=technical] .mui-mechanism-readout{font-family:ui-monospace,Consolas,monospace}
9
+ .mui-mechanism[data-style=soft]{border:0;background:var(--canvas-soft);border-radius:28px 28px 28px 10px}.mui-mechanism[data-style=soft] .mui-mechanism-controls{background:var(--canvas-bg);border-radius:22px}.mui-mechanism[data-style=expressive] h2{font-size:28px;font-weight:850;border-left:5px solid var(--canvas-accent);padding-left:14px}.mui-mechanism[data-style=expressive]{border:0;border-radius:0}.mui-mechanism[data-style=prismframe]{border:1.5px solid var(--canvas-line);box-shadow:4px 5px 0 color-mix(in srgb,var(--canvas-accent) 14%,var(--canvas-bg))}.mui-mechanism[data-style=aurora]{border-radius:28px;box-shadow:0 16px 30px -25px #30204d66}.mui-mechanism[data-style=aurora] .mui-mechanism-controls{border-radius:24px}
10
+ @media(max-width:500px){.mui-mechanism{padding:14px;border-radius:20px}.mui-mechanism h2{font-size:20px}.mui-mechanism-controls{padding:10px}.mui-mechanism-nav{gap:5px}.mui-mechanism button{padding:6px 10px;font-size:14px}}
11
+ @media(prefers-reduced-motion:reduce){.mui-mechanism *{animation:none!important;transition:none!important}}
12
+ @media(forced-colors:active){.mui-mechanism-svg path{fill:Canvas;stroke:CanvasText}.mui-mechanism button[aria-pressed=true]{background:Highlight;color:HighlightText}}
13
+ `;
@@ -0,0 +1,20 @@
1
+ import{mechanismExample}from'./mechanism-catalog.js';import{validateDocument}from'./protocol.js';import{assemblyScene,gearsScene,crankScene}from'./mechanism-scenes.js';import{sceneSvg}from'./mechanism-render.js';import{formatNumber}from'./format-number.js';
2
+ export function mechanismDocument(args={}){
3
+ for(const key of Object.keys(args))if(!['scene','title','rpm','teeth','radius','running','angle','explode'].includes(key))throw Error('DSH UI: unknown render_mechanism argument '+key);
4
+ for(const key of['teeth','radius','angle'])if(args[key]!==undefined&&!Number.isInteger(args[key]))throw Error('DSH UI: '+key+' must be an integer');
5
+ const doc=mechanismExample(args.scene),node=doc.nodes[0];
6
+ for(const key of['rpm','teeth','radius','running','angle','explode'])if(args[key]!==undefined){if(!doc.state[key])throw Error('DSH UI: '+key+' is not used by '+args.scene);doc.state[key].default=args[key]}
7
+ // Validate defaults before calculating or producing a preview; the same ranges
8
+ // also govern all subsequent native/iframe state updates.
9
+ validateDocument(doc);
10
+ if(args.title!==undefined){doc.title=args.title;node.title=args.title}
11
+ const s=Object.fromEntries(Object.entries(doc.state).map(([k,v])=>[k,v.default]));
12
+ const fallback=args.scene==='gears'?`外啮合齿轮反向转动。主动轮16齿、${s.rpm} rpm;从动轮${s.teeth}齿、${formatNumber(-s.rpm*16/s.teeth,2)} rpm。速度比由齿数确定,齿形为简化示意,不用于加工。`:args.scene==='crank'?`曲柄旋转带动定长连杆,滑块沿导轨往复。曲柄半径${s.radius}、连杆长度70,完整行程${s.radius*2},长度为示意单位,非动力学仿真。`:doc.fallback;
13
+ doc.fallback=node.fallback='初始参数示意:'+fallback;
14
+ doc.nodes.at(-1).text=args.scene==='gears'?'外啮合齿轮转向相反;从动转速 = −主动转速 × 主动齿数 ÷ 从动齿数。当前参数与数值见图中实时读数。简化齿形,不用于加工。':args.scene==='crank'?'连杆长度固定为70,滑块沿导轨往复;完整行程等于曲柄半径的两倍。当前半径、位置和行程见实时读数。长度为示意单位,非动力学仿真。':fallback;
15
+ const scene=args.scene==='assembly'?assemblyScene():args.scene==='gears'?gearsScene({drivenTeeth:s.teeth}):crankScene({radius:s.radius,angle:s.angle*Math.PI/180});
16
+ if(args.scene==='gears'){const phase=s.angle*Math.PI/180;scene.parts.find(p=>p.id==='driverGear').rotation=phase;scene.parts.find(p=>p.id==='drivenGear').rotation=-phase*16/s.teeth+Math.PI-Math.PI/s.teeth}
17
+ node.html='<div id="diagram"><section class="mui-mechanism">'+sceneSvg(scene,{explode:s.explode,cutaway:s.cutaway,preview:true})+'<p>'+fallback+'</p></section></div>';
18
+ return validateDocument(doc);
19
+ }
20
+ export function previewMechanism(raw){try{if(typeof raw!=='string'||raw.length>2048)return null;return mechanismDocument(JSON.parse(raw))}catch{return null}}
@@ -0,0 +1,62 @@
1
+ // Pure explanatory geometry and kinematics. Projection/contours use Anatomy (MIT).
2
+ import {iso,depthOf,fitProjection,roundedPlan,circleRing} from './third-party/anatomy-iso.mjs';
3
+ export {iso,depthOf,fitProjection};
4
+ export const MECHANISM_LIMITS=Object.freeze({parts:96,teeth:64,faces:8192});
5
+ const finite=v=>typeof v==='number'&&Number.isFinite(v);
6
+ const fail=s=>{throw Error('Motion mechanism: '+s)};
7
+ const num=(v,label,min=-10000,max=10000)=>{if(!finite(v)||v<min||v>max)fail(label+' must be a finite number in '+min+'..'+max);return v};
8
+ const vector=(v,label)=>{if(!Array.isArray(v)||v.length!==3)fail(label+' must be [x,y,z]');return v.map((x,i)=>num(x,label+'['+i+']'))};
9
+ const id=v=>typeof v==='string'&&/^[\p{L}_][\p{L}\p{M}\p{N}_]{0,63}$/u.test(v)&&!['constructor','prototype','__proto__'].includes(v);
10
+ const kinds=['box','cylinder','gear','rod','spring'];
11
+ export const add=(a,b)=>a.map((v,i)=>v+b[i]),sub=(a,b)=>a.map((v,i)=>v-b[i]),mul=(a,t)=>a.map(v=>v*t),dot=(a,b)=>a.reduce((s,v,i)=>s+v*b[i],0),cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];
12
+ const unit=a=>{const len=Math.hypot(...a);if(len<1e-6)fail('zero-length axis');return mul(a,1/len)};
13
+ export function validateScene(spec){
14
+ if(!spec||typeof spec!=='object'||Array.isArray(spec))fail('scene must be an object');
15
+ if(!Array.isArray(spec.parts)||!spec.parts.length||spec.parts.length>MECHANISM_LIMITS.parts)fail('parts requires 1..96 objects');
16
+ const seen=new Set(),groups=new Set();
17
+ for(const [i,p]of spec.parts.entries()){
18
+ const at='parts['+i+']';if(!p||typeof p!=='object'||!id(p.id)||seen.has(p.id))fail(at+'.id must be a unique identifier');seen.add(p.id);
19
+ if(!kinds.includes(p.type))fail(at+'.type must be '+kinds.join('/'));
20
+ if(p.group!==undefined){if(!id(p.group))fail(at+'.group must be an identifier');groups.add(p.group)}
21
+ for(const k of['label','description'])if(p[k]!==undefined&&(typeof p[k]!=='string'||p[k].length>1024))fail(at+'.'+k+' must be short text');
22
+ if(p.cover!==undefined&&typeof p.cover!=='boolean')fail(at+'.cover must be boolean');
23
+ if(p.material!==undefined&&!['metal','dark','accent','warm','cool'].includes(p.material))fail(at+'.material must be metal/dark/accent/warm/cool');
24
+ if(p.at!==undefined)vector(p.at,at+'.at');if(p.explode!==undefined)vector(p.explode,at+'.explode');if(p.rotation!==undefined)num(p.rotation,at+'.rotation',-1e9,1e9);
25
+ if(p.axis!==undefined&&!['x','y','z'].includes(p.axis))fail(at+'.axis must be x/y/z');
26
+ if(p.type==='box'){const size=vector(p.size,at+'.size');if(size.some(v=>v<=0||v>2000))fail(at+'.size must be positive and <=2000');if(p.round!==undefined)num(p.round,at+'.round',0,Math.min(size[0],size[1])/2)}
27
+ if(['cylinder','gear'].includes(p.type)){num(p.radius,at+'.radius',.1,1000);num(p.height??8,at+'.height',.1,1000);if(p.inner!==undefined)num(p.inner,at+'.inner',0,p.radius-.01)}
28
+ if(p.type==='gear'){const teeth=num(p.teeth,at+'.teeth',6,64);if(!Number.isInteger(teeth))fail(at+'.teeth must be an integer');if(p.module!==undefined)num(p.module,at+'.module',.1,p.radius/3)}
29
+ if(['rod','spring'].includes(p.type)){vector(p.from,at+'.from');vector(p.to,at+'.to');if(Math.hypot(...sub(p.to,p.from))<.1)fail(at+' endpoints must differ');num(p.radius??2,at+'.radius',.1,200)}
30
+ if(p.type==='spring'){const turns=num(p.turns??8,at+'.turns',2,24);if(!Number.isInteger(turns))fail(at+'.turns must be integer')}
31
+ }
32
+ if(spec.groups!==undefined){if(!Array.isArray(spec.groups)||spec.groups.length>12)fail('groups must be an array of <=12 labels');const ids=new Set();for(const g of spec.groups){if(!g||!id(g.id)||ids.has(g.id)||!groups.has(g.id)||typeof g.label!=='string'||g.label.length>128)fail('groups must name unique existing part groups');ids.add(g.id)}}
33
+ if(spec.bounds!==undefined){if(!Array.isArray(spec.bounds)||spec.bounds.length!==2)fail('bounds must be [minXYZ,maxXYZ]');const [a,b]=spec.bounds.map((v,i)=>vector(v,'bounds['+i+']'));if(a.some((v,i)=>v>=b[i]))fail('bounds min must be below max on every axis')}
34
+ if(spec.azimuth!==undefined)num(spec.azimuth,'azimuth',10,80);if(spec.elevation!==undefined)num(spec.elevation,'elevation',15,70);
35
+ if(spec.title!==undefined&&(typeof spec.title!=='string'||spec.title.length>256))fail('title must be short text');
36
+ if(spec.bind!==undefined){if(!spec.bind||typeof spec.bind!=='object'||Array.isArray(spec.bind))fail('bind must be an object');for(const[k,v]of Object.entries(spec.bind))if(!['explode','selected','cutaway'].includes(k)||!id(v))fail('bind only accepts declared state keys for explode/selected/cutaway')}
37
+ return spec;
38
+ }
39
+ function frame(axis){if(axis==='x')return [[0,1,0],[0,0,1],[1,0,0]];if(axis==='y')return [[0,0,1],[1,0,0],[0,1,0]];return [[1,0,0],[0,1,0],[0,0,1]]}
40
+ const point=(f,x,y,z)=>add(add(mul(f[0],x),mul(f[1],y)),mul(f[2],z));
41
+ function sweep(ring,height,f,inner){
42
+ const low=ring.map(([x,y])=>point(f,x,y,-height/2)),high=ring.map(([x,y])=>point(f,x,y,height/2)),faces=[];
43
+ const topInner=inner?.map(([x,y])=>point(f,x,y,height/2)),bottomInner=inner?.map(([x,y])=>point(f,x,y,-height/2));
44
+ faces.push({points:high,holes:topInner?[topInner.slice().reverse()]:[],normal:f[2]},{points:low.slice().reverse(),holes:bottomInner?[bottomInner]:[],normal:mul(f[2],-1)});
45
+ for(let i=0;i<ring.length;i++){const j=(i+1)%ring.length;if(Math.hypot(...sub(low[j],low[i]))<1e-6)continue;faces.push({points:[low[i],low[j],high[j],high[i]],normal:unit(cross(sub(low[j],low[i]),f[2]))});if(inner)faces.push({points:[bottomInner[j],bottomInner[i],topInner[i],topInner[j]],normal:unit(cross(sub(bottomInner[i],bottomInner[j]),f[2]))})}
46
+ return faces;
47
+ }
48
+ export function partGeometry(p){
49
+ let f=frame(p.axis),ring,height,faces;
50
+ if(p.type==='box'){ring=roundedPlan({x:-p.size[0]/2,y:-p.size[1]/2,w:p.size[0],d:p.size[1],r:p.round??Math.min(2,p.size[0]/2,p.size[1]/2)},3);height=p.size[2]}
51
+ if(p.type==='cylinder'){ring=circleRing(0,0,p.radius,24);height=p.height??8}
52
+ if(p.type==='gear'){const step=Math.PI*2/p.teeth,m=p.module??p.radius*2/p.teeth;ring=[];for(let i=0;i<p.teeth;i++)for(const[t,r]of[[-.5,p.radius-m*.85],[-.32,p.radius-m*.85],[-.17,p.radius+m*.7],[.17,p.radius+m*.7],[.32,p.radius-m*.85]]){const a=(i+t)*step;ring.push([Math.cos(a)*r,Math.sin(a)*r])}height=p.height??8}
53
+ if(p.type==='rod'){const w=unit(sub(p.to,p.from)),u=unit(cross(w,Math.abs(w[2])<.9?[0,0,1]:[0,1,0]));f=[u,cross(w,u),w];ring=circleRing(0,0,p.radius??2,16);height=Math.hypot(...sub(p.to,p.from))}
54
+ if(p.type==='spring'){const w=unit(sub(p.to,p.from)),u=unit(cross(w,Math.abs(w[2])<.9?[0,0,1]:[0,1,0])),v=cross(w,u),points=[];for(let i=0;i<=128;i++){const t=i/128,a=t*(p.turns??8)*Math.PI*2,fade=Math.min(1,t*12,(1-t)*12);points.push(add(add(p.from,mul(sub(p.to,p.from),t)),add(mul(u,Math.cos(a)*(p.radius??6)*fade),mul(v,Math.sin(a)*(p.radius??6)*fade))))}return {line:points,points}}
55
+ const rotation=p.rotation??0,c=Math.cos(rotation),s=Math.sin(rotation);ring=ring.map(([x,y])=>[x*c-y*s,x*s+y*c]);const inner=p.inner?ring.map(([x,y])=>{const a=Math.atan2(y,x);return [Math.cos(a)*p.inner,Math.sin(a)*p.inner]}):undefined;
56
+ faces=sweep(ring,height,f,inner);const center=p.type==='rod'?mul(add(p.from,p.to),.5):p.at??[0,0,0];faces=faces.map(face=>({...face,points:face.points.map(v=>add(v,center)),holes:face.holes?.map(h=>h.map(v=>add(v,center)))}));return {faces,points:faces.flatMap(f=>f.points)};
57
+ }
58
+ export function sceneBounds(spec){if(spec.bounds){const[a,b]=spec.bounds;return Array.from({length:8},(_,i)=>[i&1?b[0]:a[0],i&2?b[1]:a[1],i&4?b[2]:a[2]])}return spec.parts.flatMap(p=>{const geometry=partGeometry(p);return geometry.points.flatMap(v=>[v,add(v,p.explode??[0,0,0])])})}
59
+ export function gearPair({driverTeeth=16,drivenTeeth=32,rpm=30,angle=0,module=2}={}){for(const[k,v]of Object.entries({driverTeeth,drivenTeeth}))if(!Number.isInteger(v)||v<6||v>64)fail(k+' must be integer 6..64');num(rpm,'rpm',-600,600);num(angle,'angle',-1e9,1e9);num(module,'module',.1,10);return {driverRadius:driverTeeth*module/2,drivenRadius:drivenTeeth*module/2,centerDistance:(driverTeeth+drivenTeeth)*module/2,driverAngle:angle,drivenAngle:-angle*driverTeeth/drivenTeeth+Math.PI-Math.PI/drivenTeeth,driverRpm:rpm,drivenRpm:-rpm*driverTeeth/drivenTeeth,ratio:drivenTeeth/driverTeeth}}
60
+ export function sliderCrank({angle=0,radius=20,rod=70,z=20}={}){num(angle,'angle',-1e9,1e9);num(radius,'radius',.1,200);num(rod,'rod',.1,1000);num(z,'z');if(rod<=radius)fail('connecting rod length must exceed crank radius');const crank=[radius*Math.cos(angle),radius*Math.sin(angle),z],x=crank[0]+Math.sqrt(rod*rod-crank[1]*crank[1]);return {crank,piston:[x,0,z],position:x,stroke:radius*2,angle}}
61
+ export function follow(value,target,delta,tau=.16){num(value,'value',-1e9,1e9);num(target,'target',-1e9,1e9);num(delta,'delta',0,.1);num(tau,'tau',.01,10);return value+(target-value)*(1-Math.exp(-delta/tau))}
62
+
@@ -0,0 +1,24 @@
1
+ import {iso,depthOf,fitProjection,validateScene,sceneBounds,partGeometry,add,mul,dot} from './mechanism-geometry.js';
2
+ const TAU=Math.PI*2;
3
+ export function projectionFor(spec,options){validateScene(spec);const points=options&&!spec.bounds?spec.parts.filter(p=>!options.cutaway||!p.cover).flatMap(p=>partGeometry(p).points.map(v=>add(v,mul(p.explode??[0,0,0],options.explode??0)))):sceneBounds(spec);return fitProjection(points.length?points:[[0,0,0],[1,1,1]],600,340,{pad:24,azimuth:spec.azimuth??45,elevation:spec.elevation??30})}
4
+ const path=points=>points.map(([x,y],i)=>(i?'L':'M')+(Math.round(x*10)/10)+' '+(Math.round(y*10)/10)).join('')+'Z';
5
+ export function renderFaces(spec,projection,{explode=0,selected='all',cutaway=false,pose={}}={},cache=new Map()){
6
+ if(typeof explode!=='number'||!Number.isFinite(explode)||explode<0||explode>1)throw Error('Motion mechanism: explode must be 0..1');
7
+ if(typeof selected!=='string'||typeof cutaway!=='boolean')throw Error('Motion mechanism: selected must be text and cutaway boolean');
8
+ if(!pose||typeof pose!=='object'||Array.isArray(pose))throw Error('Motion mechanism: pose must be an object');
9
+ for(const id of Object.keys(pose))if(!spec.parts.some(p=>p.id===id))throw Error('Motion mechanism: unknown pose part '+id);
10
+ const a=(projection.azimuth??45)*TAU/360,e=(projection.elevation??30)*TAU/360,view=[Math.cos(a)*Math.cos(e),Math.sin(a)*Math.cos(e),Math.sin(e)],faces=[];
11
+ for(const part of spec.parts){
12
+ if(cutaway&&part.cover)continue;const override=pose[part.id]??{};for(const k of Object.keys(override))if(!['at','rotation','from','to'].includes(k))throw Error('Motion mechanism: unsupported pose field '+k);
13
+ const p={...part,...override},key=JSON.stringify(p);let geometry=cache.get(part.id);if(geometry?.key!==key){validateScene({parts:[p]});geometry={key,...partGeometry(p)};cache.set(part.id,geometry)}
14
+ const offset=mul(p.explode??[0,0,0],explode),floor=Math.min(...geometry.points.map(v=>v[2]))+offset[2],active=selected==='all'||selected===part.id||selected===part.group;
15
+ if(geometry.line){const points=geometry.line.map(v=>add(v,offset));faces.push({floor,key:p.id+'_line',part:p.id,group:p.group,d:points.map((v,i)=>(i?'L':'M')+iso(v,projection).join(' ')).join(''),depth:depthOf(points[Math.floor(points.length/2)],projection),line:true,tone:1,material:p.material??'metal',active});continue}
16
+ for(const [index,face]of geometry.faces.entries()){
17
+ const facing=dot(face.normal,view);if(facing<=.0001)continue;
18
+ const points=face.points.map(v=>add(v,offset)),holes=face.holes?.map(h=>h.map(v=>add(v,offset))),center=mul(points.reduce((s,v)=>add(s,v),[0,0,0]),1/points.length),light=dot(face.normal,[-.25,-.4,.88]);
19
+ faces.push({floor,key:p.id+'_'+index,part:p.id,group:p.group,d:path(points.map(v=>iso(v,projection)))+(holes??[]).map(h=>path(h.map(v=>iso(v,projection)))).join(''),depth:depthOf(center,projection),line:false,tone:light>.5?0:light>.1?1:light>-.4?2:3,material:p.material??'metal',active});
20
+ }
21
+ }
22
+ if(faces.length>8192)throw Error('Motion mechanism: face budget exceeded');return faces.sort((a,b)=>a.floor-b.floor||a.depth-b.depth||a.key.localeCompare(b.key));
23
+ }
24
+ export function sceneSvg(spec,options={}){const projection=projectionFor(spec,options),faces=renderFaces(spec,projection,options).filter(f=>!options.preview||f.tone===0||f.line);const runs=[];for(const f of faces){const last=runs.at(-1);if(last&&last.material===f.material&&last.tone===f.tone&&last.line===f.line)last.d+=f.d;else runs.push({...f})}const shapes=runs.map(f=>'<path data-material="'+f.material+'" data-tone="'+f.tone+'" d="'+f.d+'" fill-rule="evenodd"'+(f.line?' data-line=""':'')+'/>').join('');return '<svg class="mui-mechanism-svg" viewBox="0 0 600 340" role="img" aria-label="'+(options.preview?'结构轮廓预览;完成后显示立体细节':'结构图解预览')+'">'+shapes+'</svg>'}
@@ -0,0 +1,2 @@
1
+ // Generated from Motion UI integration and Anatomy MIT projection helpers.
2
+ export const MECHANISM_RUNTIME="var __MotionMechanism=(()=>{var it=Object.defineProperty;var Ot=Object.getOwnPropertyDescriptor;var Et=Object.getOwnPropertyNames;var It=Object.prototype.hasOwnProperty;var zt=(t,a)=>{for(var n in a)it(t,n,{get:a[n],enumerable:!0})},Pt=(t,a,n,o)=>{if(a&&typeof a==\"object\"||typeof a==\"function\")for(let e of Et(a))!It.call(t,e)&&e!==n&&it(t,e,{get:()=>a[e],enumerable:!(o=Ot(a,e))||o.enumerable});return t};var _t=t=>Pt(it({},\"__esModule\",{value:!0}),t);var Tt={};zt(Tt,{createMechanismAPI:()=>Ft});var st=Math.PI/180,Ct=Math.sqrt(1.6);function yt({scale:t,azimuth:a=45,elevation:n=30}){let o=a*st,e=n*st;return{sinA:Math.sin(o),cosA:Math.cos(o),sinE:Math.sin(e),cosE:Math.cos(e),k:t*Ct}}function q([t,a,n],o){let{origin:e}=o,{sinA:i,cosA:r,sinE:p,cosE:d,k:m}=yt(o);return[e[0]+(t*i-a*r)*m,e[1]+((t*r+a*i)*p-n*d)*m]}function tt([t,a,n],o){let{sinA:e,cosA:i,sinE:r,cosE:p}=yt(o);return(t*i+a*e)*p+n*r}function vt({x:t,y:a,w:n,d:o,r:e},i=8){let r=Math.max(0,Math.min(e,n/2,o/2)),p=[[t+n-r,a+r,-90],[t+n-r,a+o-r,0],[t+r,a+o-r,90],[t+r,a+r,180]],d=[];for(let[m,s,c]of p){let u=r>0?i:0;for(let f=0;f<=u;f++){let g=(c+(u?90*f/u:45))*st;d.push([m+r*Math.cos(g),s+r*Math.sin(g)])}}return d}function lt(t,a,n,o=40){return Array.from({length:o},(e,i)=>{let r=i/o*Math.PI*2;return[t+n*Math.cos(r),a+n*Math.sin(r)]})}function ct(t,a,n,{pad:o=24,headroom:e=0,azimuth:i=45,elevation:r=30}={}){let p={origin:[0,0],scale:1,azimuth:i,elevation:r},d=t.map(M=>q(M,p)),m=d.map(([M])=>M),s=d.map(([,M])=>M),c=Math.min(...m),u=Math.max(...m),f=Math.min(...s),g=Math.max(...s),h=Math.min((a-o*2)/(u-c),(n-o*2-e)/(g-f));return{origin:[a/2-(c+u)/2*h,e+(n-e)/2-(f+g)/2*h],scale:h,azimuth:i,elevation:r}}var Nt=Object.freeze({parts:96,teeth:64,faces:8192}),Ht=t=>typeof t==\"number\"&&Number.isFinite(t),y=t=>{throw Error(\"Motion mechanism: \"+t)},v=(t,a,n=-1e4,o=1e4)=>((!Ht(t)||t<n||t>o)&&y(a+\" must be a finite number in \"+n+\"..\"+o),t),U=(t,a)=>((!Array.isArray(t)||t.length!==3)&&y(a+\" must be [x,y,z]\"),t.map((n,o)=>v(n,a+\"[\"+o+\"]\"))),et=t=>typeof t==\"string\"&&/^[\\p{L}_][\\p{L}\\p{M}\\p{N}_]{0,63}$/u.test(t)&&![\"constructor\",\"prototype\",\"__proto__\"].includes(t),kt=[\"box\",\"cylinder\",\"gear\",\"rod\",\"spring\"],I=(t,a)=>t.map((n,o)=>n+a[o]),B=(t,a)=>t.map((n,o)=>n-a[o]),_=(t,a)=>t.map(n=>n*a),dt=(t,a)=>t.reduce((n,o,e)=>n+o*a[e],0),V=(t,a)=>[t[1]*a[2]-t[2]*a[1],t[2]*a[0]-t[0]*a[2],t[0]*a[1]-t[1]*a[0]],J=t=>{let a=Math.hypot(...t);return a<1e-6&&y(\"zero-length axis\"),_(t,1/a)};function Y(t){(!t||typeof t!=\"object\"||Array.isArray(t))&&y(\"scene must be an object\"),(!Array.isArray(t.parts)||!t.parts.length||t.parts.length>Nt.parts)&&y(\"parts requires 1..96 objects\");let a=new Set,n=new Set;for(let[o,e]of t.parts.entries()){let i=\"parts[\"+o+\"]\";(!e||typeof e!=\"object\"||!et(e.id)||a.has(e.id))&&y(i+\".id must be a unique identifier\"),a.add(e.id),kt.includes(e.type)||y(i+\".type must be \"+kt.join(\"/\")),e.group!==void 0&&(et(e.group)||y(i+\".group must be an identifier\"),n.add(e.group));for(let r of[\"label\",\"description\"])e[r]!==void 0&&(typeof e[r]!=\"string\"||e[r].length>1024)&&y(i+\".\"+r+\" must be short text\");if(e.cover!==void 0&&typeof e.cover!=\"boolean\"&&y(i+\".cover must be boolean\"),e.material!==void 0&&![\"metal\",\"dark\",\"accent\",\"warm\",\"cool\"].includes(e.material)&&y(i+\".material must be metal/dark/accent/warm/cool\"),e.at!==void 0&&U(e.at,i+\".at\"),e.explode!==void 0&&U(e.explode,i+\".explode\"),e.rotation!==void 0&&v(e.rotation,i+\".rotation\",-1e9,1e9),e.axis!==void 0&&![\"x\",\"y\",\"z\"].includes(e.axis)&&y(i+\".axis must be x/y/z\"),e.type===\"box\"){let r=U(e.size,i+\".size\");r.some(p=>p<=0||p>2e3)&&y(i+\".size must be positive and <=2000\"),e.round!==void 0&&v(e.round,i+\".round\",0,Math.min(r[0],r[1])/2)}if([\"cylinder\",\"gear\"].includes(e.type)&&(v(e.radius,i+\".radius\",.1,1e3),v(e.height??8,i+\".height\",.1,1e3),e.inner!==void 0&&v(e.inner,i+\".inner\",0,e.radius-.01)),e.type===\"gear\"){let r=v(e.teeth,i+\".teeth\",6,64);Number.isInteger(r)||y(i+\".teeth must be an integer\"),e.module!==void 0&&v(e.module,i+\".module\",.1,e.radius/3)}if([\"rod\",\"spring\"].includes(e.type)&&(U(e.from,i+\".from\"),U(e.to,i+\".to\"),Math.hypot(...B(e.to,e.from))<.1&&y(i+\" endpoints must differ\"),v(e.radius??2,i+\".radius\",.1,200)),e.type===\"spring\"){let r=v(e.turns??8,i+\".turns\",2,24);Number.isInteger(r)||y(i+\".turns must be integer\")}}if(t.groups!==void 0){(!Array.isArray(t.groups)||t.groups.length>12)&&y(\"groups must be an array of <=12 labels\");let o=new Set;for(let e of t.groups)(!e||!et(e.id)||o.has(e.id)||!n.has(e.id)||typeof e.label!=\"string\"||e.label.length>128)&&y(\"groups must name unique existing part groups\"),o.add(e.id)}if(t.bounds!==void 0){(!Array.isArray(t.bounds)||t.bounds.length!==2)&&y(\"bounds must be [minXYZ,maxXYZ]\");let[o,e]=t.bounds.map((i,r)=>U(i,\"bounds[\"+r+\"]\"));o.some((i,r)=>i>=e[r])&&y(\"bounds min must be below max on every axis\")}if(t.azimuth!==void 0&&v(t.azimuth,\"azimuth\",10,80),t.elevation!==void 0&&v(t.elevation,\"elevation\",15,70),t.title!==void 0&&(typeof t.title!=\"string\"||t.title.length>256)&&y(\"title must be short text\"),t.bind!==void 0){(!t.bind||typeof t.bind!=\"object\"||Array.isArray(t.bind))&&y(\"bind must be an object\");for(let[o,e]of Object.entries(t.bind))(![\"explode\",\"selected\",\"cutaway\"].includes(o)||!et(e))&&y(\"bind only accepts declared state keys for explode/selected/cutaway\")}return t}function Lt(t){return t===\"x\"?[[0,1,0],[0,0,1],[1,0,0]]:t===\"y\"?[[0,0,1],[1,0,0],[0,1,0]]:[[1,0,0],[0,1,0],[0,0,1]]}var at=(t,a,n,o)=>I(I(_(t[0],a),_(t[1],n)),_(t[2],o));function Rt(t,a,n,o){let e=t.map(([m,s])=>at(n,m,s,-a/2)),i=t.map(([m,s])=>at(n,m,s,a/2)),r=[],p=o?.map(([m,s])=>at(n,m,s,a/2)),d=o?.map(([m,s])=>at(n,m,s,-a/2));r.push({points:i,holes:p?[p.slice().reverse()]:[],normal:n[2]},{points:e.slice().reverse(),holes:d?[d]:[],normal:_(n[2],-1)});for(let m=0;m<t.length;m++){let s=(m+1)%t.length;Math.hypot(...B(e[s],e[m]))<1e-6||(r.push({points:[e[m],e[s],i[s],i[m]],normal:J(V(B(e[s],e[m]),n[2]))}),o&&r.push({points:[d[s],d[m],p[m],p[s]],normal:J(V(B(d[m],d[s]),n[2]))}))}return r}function ot(t){let a=Lt(t.axis),n,o,e;if(t.type===\"box\"&&(n=vt({x:-t.size[0]/2,y:-t.size[1]/2,w:t.size[0],d:t.size[1],r:t.round??Math.min(2,t.size[0]/2,t.size[1]/2)},3),o=t.size[2]),t.type===\"cylinder\"&&(n=lt(0,0,t.radius,24),o=t.height??8),t.type===\"gear\"){let s=Math.PI*2/t.teeth,c=t.module??t.radius*2/t.teeth;n=[];for(let u=0;u<t.teeth;u++)for(let[f,g]of[[-.5,t.radius-c*.85],[-.32,t.radius-c*.85],[-.17,t.radius+c*.7],[.17,t.radius+c*.7],[.32,t.radius-c*.85]]){let h=(u+f)*s;n.push([Math.cos(h)*g,Math.sin(h)*g])}o=t.height??8}if(t.type===\"rod\"){let s=J(B(t.to,t.from)),c=J(V(s,Math.abs(s[2])<.9?[0,0,1]:[0,1,0]));a=[c,V(s,c),s],n=lt(0,0,t.radius??2,16),o=Math.hypot(...B(t.to,t.from))}if(t.type===\"spring\"){let s=J(B(t.to,t.from)),c=J(V(s,Math.abs(s[2])<.9?[0,0,1]:[0,1,0])),u=V(s,c),f=[];for(let g=0;g<=128;g++){let h=g/128,M=h*(t.turns??8)*Math.PI*2,O=Math.min(1,h*12,(1-h)*12);f.push(I(I(t.from,_(B(t.to,t.from),h)),I(_(c,Math.cos(M)*(t.radius??6)*O),_(u,Math.sin(M)*(t.radius??6)*O))))}return{line:f,points:f}}let i=t.rotation??0,r=Math.cos(i),p=Math.sin(i);n=n.map(([s,c])=>[s*r-c*p,s*p+c*r]);let d=t.inner?n.map(([s,c])=>{let u=Math.atan2(c,s);return[Math.cos(u)*t.inner,Math.sin(u)*t.inner]}):void 0;e=Rt(n,o,a,d);let m=t.type===\"rod\"?_(I(t.from,t.to),.5):t.at??[0,0,0];return e=e.map(s=>({...s,points:s.points.map(c=>I(c,m)),holes:s.holes?.map(c=>c.map(u=>I(u,m)))})),{faces:e,points:e.flatMap(s=>s.points)}}function wt(t){if(t.bounds){let[a,n]=t.bounds;return Array.from({length:8},(o,e)=>[e&1?n[0]:a[0],e&2?n[1]:a[1],e&4?n[2]:a[2]])}return t.parts.flatMap(a=>ot(a).points.flatMap(o=>[o,I(o,a.explode??[0,0,0])]))}function W({driverTeeth:t=16,drivenTeeth:a=32,rpm:n=30,angle:o=0,module:e=2}={}){for(let[i,r]of Object.entries({driverTeeth:t,drivenTeeth:a}))(!Number.isInteger(r)||r<6||r>64)&&y(i+\" must be integer 6..64\");return v(n,\"rpm\",-600,600),v(o,\"angle\",-1e9,1e9),v(e,\"module\",.1,10),{driverRadius:t*e/2,drivenRadius:a*e/2,centerDistance:(t+a)*e/2,driverAngle:o,drivenAngle:-o*t/a+Math.PI-Math.PI/a,driverRpm:n,drivenRpm:-n*t/a,ratio:a/t}}function X({angle:t=0,radius:a=20,rod:n=70,z:o=20}={}){v(t,\"angle\",-1e9,1e9),v(a,\"radius\",.1,200),v(n,\"rod\",.1,1e3),v(o,\"z\"),n<=a&&y(\"connecting rod length must exceed crank radius\");let e=[a*Math.cos(t),a*Math.sin(t),o],i=e[0]+Math.sqrt(n*n-e[1]*e[1]);return{crank:e,piston:[i,0,o],position:i,stroke:a*2,angle:t}}function Mt(t,a,n,o=.16){return v(t,\"value\",-1e9,1e9),v(a,\"target\",-1e9,1e9),v(n,\"delta\",0,.1),v(o,\"tau\",.01,10),t+(a-t)*(1-Math.exp(-n/o))}var $t=Math.PI*2;function nt(t,a){Y(t);let n=a&&!t.bounds?t.parts.filter(o=>!a.cutaway||!o.cover).flatMap(o=>ot(o).points.map(e=>I(e,_(o.explode??[0,0,0],a.explode??0)))):wt(t);return ct(n.length?n:[[0,0,0],[1,1,1]],600,340,{pad:24,azimuth:t.azimuth??45,elevation:t.elevation??30})}var At=t=>t.map(([a,n],o)=>(o?\"L\":\"M\")+Math.round(a*10)/10+\" \"+Math.round(n*10)/10).join(\"\")+\"Z\";function mt(t,a,{explode:n=0,selected:o=\"all\",cutaway:e=!1,pose:i={}}={},r=new Map){if(typeof n!=\"number\"||!Number.isFinite(n)||n<0||n>1)throw Error(\"Motion mechanism: explode must be 0..1\");if(typeof o!=\"string\"||typeof e!=\"boolean\")throw Error(\"Motion mechanism: selected must be text and cutaway boolean\");if(!i||typeof i!=\"object\"||Array.isArray(i))throw Error(\"Motion mechanism: pose must be an object\");for(let c of Object.keys(i))if(!t.parts.some(u=>u.id===c))throw Error(\"Motion mechanism: unknown pose part \"+c);let p=(a.azimuth??45)*$t/360,d=(a.elevation??30)*$t/360,m=[Math.cos(p)*Math.cos(d),Math.sin(p)*Math.cos(d),Math.sin(d)],s=[];for(let c of t.parts){if(e&&c.cover)continue;let u=i[c.id]??{};for(let $ of Object.keys(u))if(![\"at\",\"rotation\",\"from\",\"to\"].includes($))throw Error(\"Motion mechanism: unsupported pose field \"+$);let f={...c,...u},g=JSON.stringify(f),h=r.get(c.id);h?.key!==g&&(Y({parts:[f]}),h={key:g,...ot(f)},r.set(c.id,h));let M=_(f.explode??[0,0,0],n),O=Math.min(...h.points.map($=>$[2]))+M[2],N=o===\"all\"||o===c.id||o===c.group;if(h.line){let $=h.line.map(P=>I(P,M));s.push({floor:O,key:f.id+\"_line\",part:f.id,group:f.group,d:$.map((P,A)=>(A?\"L\":\"M\")+q(P,a).join(\" \")).join(\"\"),depth:tt($[Math.floor($.length/2)],a),line:!0,tone:1,material:f.material??\"metal\",active:N});continue}for(let[$,P]of h.faces.entries()){if(dt(P.normal,m)<=1e-4)continue;let w=P.points.map(S=>I(S,M)),T=P.holes?.map(S=>S.map(H=>I(H,M))),D=_(w.reduce((S,H)=>I(S,H),[0,0,0]),1/w.length),G=dt(P.normal,[-.25,-.4,.88]);s.push({floor:O,key:f.id+\"_\"+$,part:f.id,group:f.group,d:At(w.map(S=>q(S,a)))+(T??[]).map(S=>At(S.map(H=>q(H,a)))).join(\"\"),depth:tt(D,a),line:!1,tone:G>.5?0:G>.1?1:G>-.4?2:3,material:f.material??\"metal\",active:N})}}if(s.length>8192)throw Error(\"Motion mechanism: face budget exceeded\");return s.sort((c,u)=>c.floor-u.floor||c.depth-u.depth||c.key.localeCompare(u.key))}var z=(t,a,n,o,e,i={})=>({id:t,type:\"box\",label:a,at:n,size:o,group:e,...i}),F=(t,a,n,o,e,i,r={})=>({id:t,type:\"cylinder\",label:a,at:n,radius:o,height:e,group:i,...r});function ut(t,a,n,o,e=3,i=18){return[[-1,-1],[-1,1],[1,-1],[1,1]].map(([r,p],d)=>F(o+\"_bolt_\"+d,\"\\u56FA\\u5B9A\\u87BA\\u6813\",[t+r*i,a+p*i,n],e,3,o,{material:\"dark\",explode:[0,0,26]}))}function ft(){return{title:\"\\u5206\\u5C42\\u7ED3\\u6784 \\xB7 \\u793A\\u610F\\u88C5\\u914D\",groups:[{id:\"base\",label:\"\\u5E95\\u5EA7\",description:\"\\u652F\\u6491\\u6574\\u4F53\\uFF0C\\u5E76\\u63D0\\u4F9B\\u5B89\\u88C5\\u4F4D\\u7F6E\\u3002\"},{id:\"board\",label:\"\\u529F\\u80FD\\u677F\",description:\"\\u8FDE\\u63A5\\u8F93\\u5165\\u4E0E\\u5904\\u7406\\u6A21\\u5757\\uFF1B\\u793A\\u610F\\u7ED3\\u6784\\uFF0C\\u4E0D\\u5BF9\\u5E94\\u7279\\u5B9A\\u4EA7\\u54C1\\u3002\"},{id:\"sink\",label:\"\\u6563\\u70ED\\u5C42\",description:\"\\u5C06\\u70ED\\u91CF\\u4F20\\u5230\\u66F4\\u5927\\u7684\\u8868\\u9762\\u79EF\\u3002\"},{id:\"cover\",label:\"\\u5916\\u58F3\",description:\"\\u4FDD\\u62A4\\u5185\\u90E8\\uFF1B\\u5256\\u5F00\\u9009\\u9879\\u901A\\u8FC7\\u9690\\u85CF\\u5916\\u58F3\\u5C55\\u793A\\u7ED3\\u6784\\u3002\"}],parts:[z(\"base\",\"\\u5B89\\u88C5\\u5E95\\u5EA7\",[0,0,0],[120,85,10],\"base\",{round:6,material:\"metal\"}),...[-1,1].flatMap((t,a)=>[-1,1].map((n,o)=>F(\"foot_\"+a+o,\"\\u9632\\u6ED1\\u811A\",[t*46,n*30,-8],6,7,\"base\",{material:\"dark\"}))),z(\"board\",\"\\u529F\\u80FD\\u677F\",[0,0,15],[96,62,4],\"board\",{round:3,material:\"cool\",explode:[0,0,35]}),z(\"chip\",\"\\u5904\\u7406\\u6A21\\u5757\",[-16,0,22],[25,24,10],\"board\",{round:2,material:\"dark\",explode:[0,0,35]}),z(\"connector\",\"\\u8FDE\\u63A5\\u7AEF\\u53E3\",[32,0,24],[16,36,14],\"board\",{round:2,material:\"warm\",explode:[0,0,35]}),...ut(0,0,20,\"board\",2,24),z(\"spreader\",\"\\u5BFC\\u70ED\\u5E95\\u677F\",[-16,0,32],[44,42,5],\"sink\",{round:2,material:\"metal\",explode:[-10,0,65]}),...Array.from({length:7},(t,a)=>z(\"fin_\"+a,\"\\u6563\\u70ED\\u9CCD\\u7247\",[-34+a*6,0,44],[2.4,40,20],\"sink\",{round:1,material:\"metal\",explode:[-10,0,65]})),z(\"top\",\"\\u9876\\u76D6\",[0,0,61],[124,89,5],\"cover\",{round:6,cover:!0,material:\"metal\",explode:[12,0,85]}),z(\"side\",\"\\u4FA7\\u677F\",[0,-44,32],[124,4,58],\"cover\",{round:2,cover:!0,material:\"metal\",explode:[12,-22,50]})]}}function pt({driverTeeth:t=16,drivenTeeth:a=32,module:n=1.5}={}){let o=W({driverTeeth:t,drivenTeeth:a,module:n}),e=o.centerDistance;return{title:\"\\u9F7F\\u8F6E\\u4F20\\u52A8 \\xB7 \\u7B80\\u5316\\u76F4\\u9F7F\\u8F6E\\u793A\\u610F\",azimuth:70,elevation:55,groups:[{id:\"frame\",label:\"\\u652F\\u67B6\",description:\"\\u56FA\\u5B9A\\u8F74\\u5FC3\\u4F4D\\u7F6E\\uFF1B\\u4E24\\u8F74\\u4E2D\\u5FC3\\u8DDD\\u7531\\u9F7F\\u6570\\u4E0E\\u6A21\\u6570\\u5171\\u540C\\u786E\\u5B9A\\u3002\"},{id:\"driver\",label:\"\\u4E3B\\u52A8\\u8F6E\",description:\"\\u8F93\\u5165\\u8F6C\\u901F\\uFF0C\\u7531\\u9F7F\\u6570\\u51B3\\u5B9A\\u8282\\u5706\\u534A\\u5F84\\u3002\"},{id:\"driven\",label:\"\\u4ECE\\u52A8\\u8F6E\",description:\"\\u5916\\u556E\\u5408\\u8F6C\\u5411\\u76F8\\u53CD\\uFF0C\\u8F6C\\u901F\\u6309\\u9F7F\\u6570\\u6BD4\\u53D8\\u5316\\u3002\"},{id:\"cover\",label:\"\\u5916\\u58F3\",description:\"\\u9690\\u85CF\\u76D6\\u677F\\u4EE5\\u89C2\\u5BDF\\u4F20\\u52A8\\uFF1B\\u4E0D\\u662F\\u52A0\\u5DE5\\u6216\\u53D7\\u529B\\u4EFF\\u771F\\u3002\"}],parts:[z(\"base\",\"\\u5B89\\u88C5\\u5E95\\u5EA7\",[30,0,-8],[140,90,10],\"frame\",{round:6,material:\"metal\"}),...ut(30,0,0,\"frame\",3,32),...[0,e].flatMap((i,r)=>[F(\"bearing_\"+r,\"\\u8F74\\u627F\\u5EA7\",[i,0,4],10,12,\"frame\",{inner:4,material:\"dark\"}),F(\"shaft_\"+r,\"\\u4F20\\u52A8\\u8F74\",[i,0,16],3.8,28,r?\"driven\":\"driver\",{material:\"metal\",explode:[r?24:-24,0,20]}),F(\"hub_\"+r,\"\\u8F6E\\u6BC2\",[i,0,27],6,6,r?\"driven\":\"driver\",{inner:3.8,material:\"warm\",explode:[r?24:-24,0,20]})]),{id:\"driverGear\",type:\"gear\",label:\"\\u4E3B\\u52A8\\u9F7F\\u8F6E\",group:\"driver\",at:[0,0,20],radius:o.driverRadius,teeth:t,module:n,height:7,inner:4,material:\"accent\",explode:[-24,0,20]},{id:\"drivenGear\",type:\"gear\",label:\"\\u4ECE\\u52A8\\u9F7F\\u8F6E\",group:\"driven\",at:[e,0,20],radius:o.drivenRadius,teeth:a,module:n,height:7,inner:4,material:\"cool\",rotation:o.drivenAngle,explode:[24,0,20]},z(\"cover\",\"\\u9632\\u62A4\\u76D6\\u677F\",[30,0,36],[140,90,4],\"cover\",{round:6,cover:!0,material:\"metal\",explode:[0,0,62]})]}}function ht({radius:t=20,rod:a=70,angle:n=0}={}){let o=X({radius:t,rod:a,angle:n,z:28});return{title:\"\\u66F2\\u67C4\\u8FDE\\u6746 \\xB7 \\u65CB\\u8F6C\\u8F6C\\u4E3A\\u5F80\\u590D\",groups:[{id:\"frame\",label:\"\\u652F\\u67B6\\u4E0E\\u5BFC\\u8F68\",description:\"\\u652F\\u6491\\u8F74\\u5FC3\\uFF0C\\u9650\\u5236\\u6ED1\\u5757\\u6CBF\\u540C\\u4E00\\u65B9\\u5411\\u8FD0\\u52A8\\u3002\"},{id:\"crank\",label:\"\\u66F2\\u67C4\",description:\"\\u66F2\\u67C4\\u7ED5\\u56FA\\u5B9A\\u8F74\\u5FC3\\u65CB\\u8F6C\\u3002\"},{id:\"rod\",label:\"\\u8FDE\\u6746\",description:\"\\u8FDE\\u63A5\\u66F2\\u67C4\\u9500\\u548C\\u6ED1\\u5757\\uFF0C\\u957F\\u5EA6\\u4FDD\\u6301\\u4E0D\\u53D8\\u3002\"},{id:\"piston\",label:\"\\u6ED1\\u5757\",description:\"\\u6CBF\\u5BFC\\u8F68\\u5F80\\u590D\\uFF1B\\u5B8C\\u6574\\u884C\\u7A0B\\u7B49\\u4E8E\\u66F2\\u67C4\\u534A\\u5F84\\u7684\\u4E24\\u500D\\u3002\"}],parts:[z(\"base\",\"\\u5B89\\u88C5\\u5E95\\u5EA7\",[35,0,-6],[175,80,10],\"frame\",{round:6,material:\"metal\"}),...ut(35,0,1,\"frame\",3,30),F(\"bearing\",\"\\u66F2\\u8F74\\u8F74\\u627F\",[0,0,7],11,15,\"frame\",{inner:4,material:\"dark\"}),F(\"axle\",\"\\u66F2\\u8F74\",[0,0,15],3.8,26,\"crank\",{material:\"metal\",explode:[0,-18,18]}),F(\"flywheel\",\"\\u66F2\\u67C4\\u76D8\",[0,0,20],t+6,6,\"crank\",{inner:4,material:\"accent\",explode:[0,-18,18]}),z(\"railA\",\"\\u5BFC\\u8F68\",[74,-16,13],[85,5,6],\"frame\",{round:1,material:\"dark\"}),z(\"railB\",\"\\u5BFC\\u8F68\",[74,16,13],[85,5,6],\"frame\",{round:1,material:\"dark\"}),F(\"crankPin\",\"\\u66F2\\u67C4\\u9500\",o.crank,3,12,\"crank\",{material:\"warm\",explode:[0,-18,18]}),{id:\"link\",type:\"rod\",label:\"\\u8FDE\\u6746\",from:o.crank,to:o.piston,radius:2.4,group:\"rod\",material:\"metal\",explode:[0,0,45]},z(\"piston\",\"\\u6ED1\\u5757\",[o.position,0,22],[18,24,16],\"piston\",{round:3,material:\"cool\",explode:[20,0,18]}),F(\"pistonPin\",\"\\u6ED1\\u5757\\u9500\",o.piston,3,10,\"piston\",{material:\"warm\",explode:[20,0,18]})]}}var St=`\n.mui-mechanism{font:15px/1.7 system-ui,sans-serif;color:var(--canvas-ink);background:var(--canvas-bg);border:1px solid var(--canvas-line);border-radius:24px;padding:20px;max-width:100%;box-sizing:border-box}.mui-mechanism *{box-sizing:border-box}.mui-mechanism h2{font-size:22px;line-height:1.35;margin:0 0 8px;font-weight:750}.mui-mechanism p{margin:0;color:var(--canvas-muted)}\n.mui-mechanism-svg{display:block;width:100%;height:auto;max-height:420px;touch-action:pan-y}.mui-mechanism-svg path{stroke:color-mix(in srgb,var(--canvas-ink) 38%,var(--canvas-bg));stroke-width:.8;stroke-linejoin:round;vector-effect:non-scaling-stroke;cursor:pointer;fill:var(--mech-base,var(--canvas-soft))}.mui-mechanism-svg [data-material=metal]{--mech-base:color-mix(in srgb,var(--canvas-muted) 32%,var(--canvas-bg))}.mui-mechanism-svg [data-material=dark]{--mech-base:color-mix(in srgb,var(--canvas-ink) 78%,var(--canvas-bg))}.mui-mechanism-svg [data-material=accent]{--mech-base:var(--canvas-accent)}.mui-mechanism-svg [data-material=cool]{--mech-base:color-mix(in srgb,#22998a 70%,var(--canvas-bg))}.mui-mechanism-svg [data-material=warm]{--mech-base:color-mix(in srgb,#dd9c42 75%,var(--canvas-bg))}\n.mui-mechanism-svg [data-tone=\"0\"]{fill:color-mix(in srgb,var(--mech-base) 74%,var(--canvas-bg))}.mui-mechanism-svg [data-tone=\"1\"]{fill:var(--mech-base)}.mui-mechanism-svg [data-tone=\"2\"]{fill:color-mix(in srgb,var(--mech-base) 85%,var(--canvas-ink))}.mui-mechanism-svg [data-tone=\"3\"]{fill:color-mix(in srgb,var(--mech-base) 68%,var(--canvas-ink))}.mui-mechanism-svg [data-line]{fill:none;stroke:var(--mech-base);stroke-width:2}.mui-mechanism-svg [data-active=false]{opacity:.2}.mui-mechanism-svg [data-active=true]{opacity:1}\n.mui-mechanism-nav{display:flex;flex-wrap:wrap;gap:7px;margin:8px 0 12px}.mui-mechanism button{font:inherit;color:var(--canvas-ink);background:var(--canvas-bg);border:1px solid var(--canvas-line);border-radius:999px;padding:6px 13px;cursor:pointer;min-height:36px}.mui-mechanism button[aria-pressed=true]{color:var(--canvas-bg);background:var(--canvas-ink);border-color:var(--canvas-ink)}.mui-mechanism button:focus-visible,.mui-mechanism input:focus-visible{outline:2px solid var(--canvas-accent);outline-offset:3px}.mui-mechanism button:disabled{opacity:.5;cursor:default}\n.mui-mechanism-controls{display:flex;align-items:center;flex-wrap:wrap;gap:14px;padding:12px 14px;border-radius:16px;background:var(--canvas-soft);margin:8px 0}.mui-mechanism-controls label{display:flex;align-items:center;gap:9px;flex:1 1 180px;min-width:0}.mui-mechanism-controls input[type=range]{flex:1;min-width:70px;accent-color:var(--canvas-accent)}.mui-mechanism-controls input[type=checkbox]{accent-color:var(--canvas-accent)}.mui-mechanism-controls output{font-variant-numeric:tabular-nums;min-width:40px;text-align:right;font-weight:650}.mui-mechanism-readout{display:block;padding:10px 0;font-weight:700;font-variant-numeric:tabular-nums;color:var(--canvas-ink)}.mui-mechanism-description{min-height:0;font-size:14px}\n.mui-mechanism[data-style=editorial]{border:0;border-top:1px solid var(--canvas-line);border-radius:0;padding:16px 0}.mui-mechanism[data-style=editorial] h2{font-family:ui-serif,Georgia,serif;font-weight:650}.mui-mechanism[data-style=editorial] .mui-mechanism-controls{border-radius:0;background:transparent;border-block:1px solid var(--canvas-line)}\n.mui-mechanism[data-style=technical]{border-radius:10px}.mui-mechanism[data-style=technical] button,.mui-mechanism[data-style=technical] .mui-mechanism-controls{border-radius:6px}.mui-mechanism[data-style=technical] .mui-mechanism-readout{font-family:ui-monospace,Consolas,monospace}\n.mui-mechanism[data-style=soft]{border:0;background:var(--canvas-soft);border-radius:28px 28px 28px 10px}.mui-mechanism[data-style=soft] .mui-mechanism-controls{background:var(--canvas-bg);border-radius:22px}.mui-mechanism[data-style=expressive] h2{font-size:28px;font-weight:850;border-left:5px solid var(--canvas-accent);padding-left:14px}.mui-mechanism[data-style=expressive]{border:0;border-radius:0}.mui-mechanism[data-style=prismframe]{border:1.5px solid var(--canvas-line);box-shadow:4px 5px 0 color-mix(in srgb,var(--canvas-accent) 14%,var(--canvas-bg))}.mui-mechanism[data-style=aurora]{border-radius:28px;box-shadow:0 16px 30px -25px #30204d66}.mui-mechanism[data-style=aurora] .mui-mechanism-controls{border-radius:24px}\n@media(max-width:500px){.mui-mechanism{padding:14px;border-radius:20px}.mui-mechanism h2{font-size:20px}.mui-mechanism-controls{padding:10px}.mui-mechanism-nav{gap:5px}.mui-mechanism button{padding:6px 10px;font-size:14px}}\n@media(prefers-reduced-motion:reduce){.mui-mechanism *{animation:none!important;transition:none!important}}\n@media(forced-colors:active){.mui-mechanism-svg path{fill:Canvas;stroke:CanvasText}.mui-mechanism button[aria-pressed=true]{background:Highlight;color:HighlightText}}\n`;var xt=\"http://www.w3.org/2000/svg\",C=(t,a,n)=>{let o=document.createElement(t);return a&&(o.className=a),n!==void 0&&(o.textContent=n),o};function Ft(t){function a(o,e){let i=typeof o==\"string\"?document.querySelector(o):o;if(!i||!i.replaceChildren)throw Error(\"Motion mechanism: target element not found\");Y(e);let r=structuredClone({...e,onSelect:void 0}),p=nt(r),d={explode:0,selected:\"all\",cutaway:!1,pose:{}},m=!1,s=\"\",c=\"\",u=0,f=new Map,g=new Map,h=new Map,M=[],O=C(\"section\",\"mui-mechanism\"),N=C(\"h2\",null,r.title??\"\\u7ED3\\u6784\\u56FE\\u89E3\"),$=document.createElementNS(xt,\"svg\"),P=document.createElementNS(xt,\"g\"),A=C(\"div\",\"mui-mechanism-nav\"),w=C(\"div\",\"mui-mechanism-controls\"),T=C(\"p\",\"mui-mechanism-description\"),D=C(\"output\",\"mui-mechanism-readout\"),G=C(\"style\");G.textContent=St,N.hidden=r.title===\"\",O.setAttribute(\"aria-label\",r.title??\"\\u7ED3\\u6784\\u56FE\\u89E3\"),$.setAttribute(\"class\",\"mui-mechanism-svg\"),$.setAttribute(\"viewBox\",\"0 0 600 340\"),$.setAttribute(\"role\",\"img\"),$.setAttribute(\"aria-label\",\"\\u53EF\\u62C6\\u89E3\\u7684\\u7ED3\\u6784\\u793A\\u610F\\u56FE\\uFF1B\\u4F7F\\u7528\\u4E0B\\u65B9\\u6309\\u94AE\\u9009\\u62E9\\u90E8\\u4EF6\"),$.append(P),O.append(G,N,$,A,w,D,T),i.replaceChildren(O);let S=()=>r.groups?.length?r.groups:r.parts.map(l=>({id:l.id,label:l.label??l.id,description:l.description??\"\"}));function H(l){r.bind?.selected?t.setState({[r.bind.selected]:l}):typeof e.onSelect==\"function\"?e.onSelect(l):Q({selected:l})}function K(){if(A.replaceChildren(),h.clear(),!r.bind?.selected&&typeof e.onSelect!=\"function\"){A.hidden=!0;return}A.hidden=!1;for(let l of[{id:\"all\",label:\"\\u5168\\u90E8\",description:\"\"},...S()]){let b=C(\"button\",null,l.label);b.type=\"button\",b.setAttribute(\"aria-pressed\",String(d.selected===l.id)),b.addEventListener(\"click\",()=>H(l.id)),h.set(l.id,b),A.append(b)}}let x,rt,L;if(r.bind?.explode){let l=C(\"label\",null,\"\\u62C6\\u89E3\\u8FDB\\u5EA6\");x=C(\"input\"),x.type=\"range\",x.min=\"0\",x.max=\"1\",x.step=\".01\",x.setAttribute(\"aria-label\",\"\\u62C6\\u89E3\\u8FDB\\u5EA6\"),rt=C(\"output\"),x.addEventListener(\"input\",()=>t.setState({[r.bind.explode]:Number(x.value)})),l.append(x,rt),w.append(l)}if(r.bind?.cutaway&&r.parts.some(l=>l.cover)){let l=C(\"label\",null,\"\\u5256\\u5F00\\u5916\\u58F3\");L=C(\"input\"),L.type=\"checkbox\",L.setAttribute(\"aria-label\",\"\\u5256\\u5F00\\u5916\\u58F3\"),L.addEventListener(\"change\",()=>t.setState({[r.bind.cutaway]:L.checked})),l.prepend(L),w.append(l)}w.hidden=!w.childNodes.length;function jt(l){let b=JSON.stringify([l.explode,l.cutaway]);b!==s&&(p=nt(r,l),s=b);let E=mt(r,p,l,f),k=new Set;for(let j of E){k.add(j.key);let R=g.get(j.key);R||(R=document.createElementNS(xt,\"path\"),R.setAttribute(\"fill-rule\",\"evenodd\"),g.set(j.key,R));for(let[gt,bt]of Object.entries({d:j.d,\"data-part\":j.part,\"data-material\":j.material,\"data-tone\":j.tone,\"data-active\":j.active}))R.getAttribute(gt)!==String(bt)&&R.setAttribute(gt,String(bt));j.line?R.setAttribute(\"data-line\",\"\"):R.removeAttribute(\"data-line\")}for(let[j,R]of g)k.has(j)||(R.remove(),g.delete(j));let Z=E.map(j=>j.key).join(\"|\");if(Z!==c){for(let j of E)P.append(g.get(j.key));c=Z}u++}function Q(l={}){if(m)return;for(let k of Object.keys(l))if(![\"explode\",\"selected\",\"cutaway\",\"pose\",\"parts\",\"readout\"].includes(k))throw Error(\"Motion mechanism: unknown update field \"+k);if(l.parts){let k={...r,parts:l.parts};Y(k);let Z=nt(k);mt(k,Z,{...d,...l},new Map),r=k,p=Z,f.clear(),c=\"\",s=\"\"}let b={...d,...l};l.pose&&(b.pose=l.pose),jt(b),d=b;for(let[k,Z]of h)Z.setAttribute(\"aria-pressed\",String(b.selected===k));let E=S().find(k=>k.id===b.selected)??r.parts.find(k=>k.id===b.selected);T.textContent=E?.description??(b.selected===\"all\"?\"\\u9009\\u62E9\\u4E00\\u4E2A\\u90E8\\u4EF6\\uFF0C\\u67E5\\u770B\\u5B83\\u7684\\u804C\\u8D23\\uFF1B\\u62C6\\u89E3\\u53EA\\u5C55\\u793A\\u88C5\\u914D\\u5173\\u7CFB\\u3002\":\"\"),x&&(x.value=String(b.explode),rt.textContent=Math.round(b.explode*100)+\"%\"),L&&(L.checked=b.cutaway),l.readout!==void 0&&(D.textContent=String(l.readout))}return $.addEventListener(\"click\",l=>{let b=l.target.closest(\"[data-part]\");if(!b)return;let E=r.parts.find(k=>k.id===b.getAttribute(\"data-part\"));E&&H(r.groups?.length?E.group??E.id:E.id)}),K(),M.push(t.subscribe(l=>{if(m)return;let b={};for(let[E,k]of Object.entries(r.bind??{})){if(!Object.hasOwn(l.state,k))throw Error(\"Motion mechanism: bind.\"+E+\" names undeclared state \"+k);b[E]=l.state[k]}O.dataset.style=l.theme?.visualStyle??\"\";for(let E of h.values())E.disabled=l.disabled;x&&(x.disabled=l.disabled),L&&(L.disabled=l.disabled),Q(b)})),Object.freeze({update:Q,pose:l=>Q({pose:l}),stats:()=>({frames:u,faces:g.size,parts:r.parts.length}),destroy(){m||(m=!0,M.forEach(l=>l()),g.clear(),f.clear(),i.replaceChildren())}})}function n(o,e,i={}){if(![\"assembly\",\"gears\",\"crank\"].includes(e))throw Error(\"Motion mechanism: demo must be assembly/gears/crank\");let r=i.bind??{},p=()=>t.snapshot.state,d=(A,w)=>Object.hasOwn(r,A)?p()[r[A]]:w;for(let[A,w]of Object.entries(r))if(![\"explode\",\"selected\",\"cutaway\",\"rpm\",\"teeth\",\"running\",\"angle\",\"radius\"].includes(A)||typeof w!=\"string\"||!Object.hasOwn(p(),w))throw Error(\"Motion mechanism: invalid demo binding \"+A);let m=d(\"angle\",0)*Math.PI/180,s=m,c=\"\",u=!1,f=Object.fromEntries(Object.entries(r).filter(([A])=>[\"explode\",\"selected\",\"cutaway\"].includes(A)));function g(){return e===\"assembly\"?ft():e===\"gears\"?pt({driverTeeth:i.driverTeeth??16,drivenTeeth:d(\"teeth\",32),module:i.module??1.5}):ht({radius:d(\"radius\",20),rod:i.rod??70})}let h=a(o,{...g(),...i.title===void 0?{}:{title:i.title},bind:f}),M=!1;function O(A=0,w=!1,T=!0){if(M)return;let D=d(\"rpm\",30),G=d(\"running\",!1),S=d(\"angle\",0)*Math.PI/180,H=s;S!==m?(H=S,m=S):G&&T&&!w&&(H+=D*Math.PI*2/60*A),s=H;let K=JSON.stringify([e,d(\"teeth\",32),d(\"radius\",20)]);if(K!==c&&(h.update({parts:g().parts}),c=K),e===\"gears\"){let x=W({driverTeeth:i.driverTeeth??16,drivenTeeth:d(\"teeth\",32),rpm:D,angle:s,module:i.module??1.5});h.update({pose:{driverGear:{rotation:x.driverAngle},drivenGear:{rotation:x.drivenAngle}},readout:\"\\u4E3B\\u52A8\\u8F6E \"+t.formatNumber(x.driverRpm,0)+\" rpm \\xB7 \\u4ECE\\u52A8\\u8F6E \"+t.formatNumber(x.drivenRpm,2)+\" rpm \\xB7 \\u9F7F\\u6570\\u6BD4 \"+t.formatNumber(x.ratio,2)+\"\\uFF08\\u8D1F\\u53F7\\u8868\\u793A\\u53CD\\u5411\\uFF09\"})}else if(e===\"crank\"){let x=X({angle:s,radius:d(\"radius\",20),rod:i.rod??70,z:28});h.update({pose:{crankPin:{at:x.crank},link:{from:x.crank,to:x.piston},piston:{at:[x.position,0,22]},pistonPin:{at:x.piston}},readout:\"\\u66F2\\u67C4\\u89D2 \"+t.formatNumber((s*180/Math.PI%360+360)%360,1)+\"\\xB0 \\xB7 \\u6ED1\\u5757\\u4F4D\\u7F6E \"+t.formatNumber(x.position,2)+\" \\xB7 \\u884C\\u7A0B \"+t.formatNumber(x.stroke,1)+\"\\uFF08\\u793A\\u610F\\u957F\\u5EA6\\u5355\\u4F4D\\uFF09\"})}u=w}let N=null,$=()=>{O(0,u,!0),e!==\"assembly\"&&(d(\"running\",!1)&&!N?N=t.animate(({delta:A,reducedMotion:w,visible:T})=>{T&&!w?O(A,!1,!0):u!==w&&O(0,w,T)}):!d(\"running\",!1)&&N&&(N(),N=null))},P=t.subscribe($);return O(),Object.freeze({...h,destroy(){M||(M=!0,N?.(),P(),h.destroy())}})}return Object.freeze({mount:a,mountDemo:n,gearPair:W,sliderCrank:X,follow:Mt,scenes:Object.freeze({assembly:ft,gears:pt,crank:ht})})}return _t(Tt);})();\n";
@@ -0,0 +1,20 @@
1
+ import {gearPair,sliderCrank} from './mechanism-geometry.js';
2
+ const box=(id,label,at,size,group,extra={})=>({id,type:'box',label,at,size,group,...extra});
3
+ const round=(id,label,at,radius,height,group,extra={})=>({id,type:'cylinder',label,at,radius,height,group,...extra});
4
+ function bolts(cx,cy,z,group,r=3,spread=18){return [[-1,-1],[-1,1],[1,-1],[1,1]].map(([x,y],i)=>round(group+'_bolt_'+i,'固定螺栓',[cx+x*spread,cy+y*spread,z],r,3,group,{material:'dark',explode:[0,0,26]}))}
5
+ export function assemblyScene(){return{title:'分层结构 · 示意装配',groups:[{id:'base',label:'底座',description:'支撑整体,并提供安装位置。'},{id:'board',label:'功能板',description:'连接输入与处理模块;示意结构,不对应特定产品。'},{id:'sink',label:'散热层',description:'将热量传到更大的表面积。'},{id:'cover',label:'外壳',description:'保护内部;剖开选项通过隐藏外壳展示结构。'}],parts:[
6
+ box('base','安装底座',[0,0,0],[120,85,10],'base',{round:6,material:'metal'}),...[-1,1].flatMap((x,i)=>[-1,1].map((y,j)=>round('foot_'+i+j,'防滑脚',[x*46,y*30,-8],6,7,'base',{material:'dark'}))),
7
+ box('board','功能板',[0,0,15],[96,62,4],'board',{round:3,material:'cool',explode:[0,0,35]}),box('chip','处理模块',[-16,0,22],[25,24,10],'board',{round:2,material:'dark',explode:[0,0,35]}),box('connector','连接端口',[32,0,24],[16,36,14],'board',{round:2,material:'warm',explode:[0,0,35]}),...bolts(0,0,20,'board',2,24),
8
+ box('spreader','导热底板',[-16,0,32],[44,42,5],'sink',{round:2,material:'metal',explode:[-10,0,65]}),...Array.from({length:7},(_,i)=>box('fin_'+i,'散热鳍片',[-34+i*6,0,44],[2.4,40,20],'sink',{round:1,material:'metal',explode:[-10,0,65]})),
9
+ box('top','顶盖',[0,0,61],[124,89,5],'cover',{round:6,cover:true,material:'metal',explode:[12,0,85]}),box('side','侧板',[0,-44,32],[124,4,58],'cover',{round:2,cover:true,material:'metal',explode:[12,-22,50]})]}}
10
+ export function gearsScene({driverTeeth=16,drivenTeeth=32,module=1.5}={}){const g=gearPair({driverTeeth,drivenTeeth,module}),x=g.centerDistance;return{title:'齿轮传动 · 简化直齿轮示意',azimuth:70,elevation:55,groups:[{id:'frame',label:'支架',description:'固定轴心位置;两轴中心距由齿数与模数共同确定。'},{id:'driver',label:'主动轮',description:'输入转速,由齿数决定节圆半径。'},{id:'driven',label:'从动轮',description:'外啮合转向相反,转速按齿数比变化。'},{id:'cover',label:'外壳',description:'隐藏盖板以观察传动;不是加工或受力仿真。'}],parts:[
11
+ box('base','安装底座',[30,0,-8],[140,90,10],'frame',{round:6,material:'metal'}),...bolts(30,0,0,'frame',3,32),
12
+ ...[0,x].flatMap((cx,i)=>[round('bearing_'+i,'轴承座',[cx,0,4],10,12,'frame',{inner:4,material:'dark'}),round('shaft_'+i,'传动轴',[cx,0,16],3.8,28,i?'driven':'driver',{material:'metal',explode:[i?24:-24,0,20]}),round('hub_'+i,'轮毂',[cx,0,27],6,6,i?'driven':'driver',{inner:3.8,material:'warm',explode:[i?24:-24,0,20]})]),
13
+ {id:'driverGear',type:'gear',label:'主动齿轮',group:'driver',at:[0,0,20],radius:g.driverRadius,teeth:driverTeeth,module,height:7,inner:4,material:'accent',explode:[-24,0,20]},
14
+ {id:'drivenGear',type:'gear',label:'从动齿轮',group:'driven',at:[x,0,20],radius:g.drivenRadius,teeth:drivenTeeth,module,height:7,inner:4,material:'cool',rotation:g.drivenAngle,explode:[24,0,20]},
15
+ box('cover','防护盖板',[30,0,36],[140,90,4],'cover',{round:6,cover:true,material:'metal',explode:[0,0,62]})]}}
16
+ export function crankScene({radius=20,rod=70,angle=0}={}){const k=sliderCrank({radius,rod,angle,z:28});return{title:'曲柄连杆 · 旋转转为往复',groups:[{id:'frame',label:'支架与导轨',description:'支撑轴心,限制滑块沿同一方向运动。'},{id:'crank',label:'曲柄',description:'曲柄绕固定轴心旋转。'},{id:'rod',label:'连杆',description:'连接曲柄销和滑块,长度保持不变。'},{id:'piston',label:'滑块',description:'沿导轨往复;完整行程等于曲柄半径的两倍。'}],parts:[
17
+ box('base','安装底座',[35,0,-6],[175,80,10],'frame',{round:6,material:'metal'}),...bolts(35,0,1,'frame',3,30),round('bearing','曲轴轴承',[0,0,7],11,15,'frame',{inner:4,material:'dark'}),round('axle','曲轴',[0,0,15],3.8,26,'crank',{material:'metal',explode:[0,-18,18]}),round('flywheel','曲柄盘',[0,0,20],radius+6,6,'crank',{inner:4,material:'accent',explode:[0,-18,18]}),
18
+ box('railA','导轨',[74,-16,13],[85,5,6],'frame',{round:1,material:'dark'}),box('railB','导轨',[74,16,13],[85,5,6],'frame',{round:1,material:'dark'}),
19
+ round('crankPin','曲柄销',k.crank,3,12,'crank',{material:'warm',explode:[0,-18,18]}),{id:'link',type:'rod',label:'连杆',from:k.crank,to:k.piston,radius:2.4,group:'rod',material:'metal',explode:[0,0,45]},
20
+ box('piston','滑块',[k.position,0,22],[18,24,16],'piston',{round:3,material:'cool',explode:[20,0,18]}),round('pistonPin','滑块销',k.piston,3,10,'piston',{material:'warm',explode:[20,0,18]})]}}
@@ -0,0 +1,18 @@
1
+ import {EXPRESSIVE_EXAMPLES} from './expressive-examples.js';
2
+ import {validateDocument} from './protocol.js';
3
+ const ref=key=>({ref:key}),constant=value=>({const:value});
4
+ export const MOTION_RECIPES=Object.freeze([
5
+ ...EXPRESSIVE_EXAMPLES,
6
+ {id:'plan',name:'方案切换',description:'分段选择、本地计算与精确指标',document:{version:1,title:'订阅预算 · 示例',fallback:'示例单价 29 元/人/月,按月数和人数计算,不是真实报价。',presentation:{preset:'brief'},state:{months:{type:'number',default:1,min:1,max:12,step:1,options:[1,3,12]},seats:{type:'number',default:2,min:1,max:8,step:1}},derived:{total:{op:'mul',args:[constant(29),ref('state.months'),ref('state.seats')]}},nodes:[{id:'period',type:'select',label:'购买月数',bind:'state.months',variant:'segmented'},{id:'seats',type:'slider',label:'使用人数',bind:'state.seats'},{id:'total',type:'metric',label:'示例合计',value:ref('derived.total'),unit:'元',decimals:0},{id:'budget',type:'progress',label:'占 3000 元预算',value:ref('derived.total'),max:3000,unit:'元'},{id:'note',type:'text',text:'示例单价 29 元/人/月,不是真实报价。'}]}},
7
+ {id:'checklist',name:'任务勾选',description:'真实勾选状态联动完成进度',document:{version:1,title:'发布前自查 · 示例',fallback:'勾选实际完成的检查项,进度在本地更新。',presentation:{preset:'brief'},state:{readme:{type:'boolean',default:false},test:{type:'boolean',default:false},license:{type:'boolean',default:false}},derived:{done:{op:'add',args:['readme','test','license'].map(k=>({op:'if',args:[ref('state.'+k),constant(1),constant(0)]}))}},nodes:[{id:'readme',type:'toggle',label:'说明文档已检查',bind:'state.readme'},{id:'test',type:'toggle',label:'实际测试已完成',bind:'state.test'},{id:'license',type:'toggle',label:'许可证已确认',bind:'state.license'},{id:'done',type:'progress',label:'已完成检查',value:ref('derived.done'),max:3,unit:'项'},{id:'note',type:'text',text:'仅记录用户勾选,不代表插件已执行这些检查。'}]}},
8
+ {id:'copy',name:'复制反馈',description:'复用文本/代码,成功或失败均有反馈',document:{version:1,title:'可直接使用的内容',fallback:'git status\ngit diff',nodes:[{id:'draft',type:'text',title:'提交说明',text:'补充使用说明并修复交互问题。',copyable:true},{id:'code',type:'code',title:'检查本地改动',lang:'bash',code:'git status\ngit diff'}]}},
9
+ {id:'plot',name:'参数图解',description:'安全函数表达式,本地实时重绘',document:{title:'振幅探索',items:[{type:'plot',title:'y = a sin(x)',xMin:-6.28,xMax:6.28,series:[{expr:'a*sin(x)',label:'正弦',params:[{name:'a',value:1,min:0,max:3,step:0.1}]}]},{type:'text',content:'调整 a 改变振幅。函数在浏览器本地绘制,无需再次调用模型。'}]}}
10
+ ]);
11
+ export function motionCatalog(id){
12
+ if(!id)return{recipes:MOTION_RECIPES.map(({id,name,description})=>({id,name,description})),usage:'ui_catalog({topic:"motion",scene:"plan"}) returns one complete example; adapt real content and selected guided style. Do not load every recipe.'};
13
+ const recipe=MOTION_RECIPES.find(r=>r.id===id);if(!recipe)throw Error('Unknown motion recipe; choose '+MOTION_RECIPES.map(r=>r.id).join(', '));
14
+ return{...recipe,document:validateDocument(recipe.document),guidance:'All feedback follows actual state/results. Native controls and graph transitions are built in; no extra JS is needed. Preview is read-only until turn end. Do not copy sample prices as real data or simulate success with a timer.'};
15
+ }
16
+ export const MOTION_GUIDE='Use motion to explain an actual change or confirm a user action, not to decorate every answer. Across ALL visual styles and free mode, native primary link/button uses gradient expansion, magnetic hover and rolling labels; numeric input shows a stepper, toggle stretches and can be dragged, and metric changes roll vertically. Retrieve motion scenes action/quantity/elastic for these patterns. Native select supports variant:"segmented" for 2..8 short options (more options use dropdown); sliders show filled value tracks, toggles and copy buttons have built-in feedback. Values are exact immediately, graphics may transition briefly; do not animate fake progress or declare success before completion. ui_catalog({topic:"motion",scene:"plan"|"checklist"|"copy"|"plot"}) retrieves one small working example when needed. Respect reduced motion and keep static text stable; no library or source checkout search is required.';
17
+
18
+ export const MOTION_INTERFACE='Native controls have built-in interaction feedback in every style and free mode: primary button/link hover fill and rolling label, draggable elastic toggle, numeric input stepper, exact metric update feedback, segmented select, slider fill and real copy confirmation. Native feedback respects reduced motion. Canvas animation is model-authored; the host does not rewrite its source. No visual palette or layout is imposed.';
@@ -6,7 +6,7 @@ import {COMPONENTS} from './components.js';
6
6
  const own = (v,k) => Object.hasOwn(v,k);
7
7
  const singleItem = v => object(v) && Object.keys(v).length === 1 && own(v,'item');
8
8
  const array = v => {let candidate=v;for(let i=0;i<8&&singleItem(candidate);i++)candidate=candidate.item;return Array.isArray(candidate)?candidate:v};
9
- const numericOps = new Set(['add','sub','mul','div','min','max','round','abs','lt','lte','gt','gte']);
9
+ const numericOps = new Set(['add','sub','mul','div','min','max','round','ceil','floor','abs','lt','lte','gt','gte']);
10
10
  function number(v) {
11
11
  if (typeof v !== 'string' || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(v.trim())) return v;
12
12
  const n = Number(v);
@@ -79,6 +79,10 @@ export function normalizeAuthoring(doc) {
79
79
  for(const k of Object.keys(node))delete node[k];
80
80
  Object.assign(node,{id,type:'visual',spec});
81
81
  }
82
+ // Positional cells map losslessly to an explicit ordered, unique column list.
83
+ if(node.type==='table'&&Array.isArray(node.columns)&&node.columns.length&&node.columns.every(c=>object(c)&&typeof c.key==='string')&&new Set(node.columns.map(c=>c.key)).size===node.columns.length&&Array.isArray(node.rows)&&node.rows.every(row=>Array.isArray(row)&&row.length===node.columns.length)){
84
+ node.rows=node.rows.map(row=>Object.fromEntries(node.columns.map((c,i)=>[c.key,row[i]])));
85
+ }
82
86
  if(node.type==='table'&&Array.isArray(node.columns)&&node.columns.length&&Array.isArray(node.rows)&&node.rows.every(Array.isArray)&&node.columns.every(c=>object(c)&&Number.isInteger(c.key)&&c.key>=0)&&new Set(node.columns.map(c=>c.key)).size===node.columns.length){
83
87
  const columns=node.columns.map(c=>({...c,key:'col_'+c.key}));
84
88
  node.rows=node.rows.map(row=>Object.fromEntries(node.columns.map(c=>['col_'+c.key,row[c.key]??null])));node.columns=columns;
@@ -91,7 +95,7 @@ export function normalizeAuthoring(doc) {
91
95
  if (!own(node,'title')) node.title = doc.title;
92
96
  if (!own(node,'fallback')) node.fallback = doc.fallback;
93
97
  }
94
- for (const field of ['children','items','columns','stateKeys','dataKeys','derivedKeys']) if (own(node,field)) node[field] = array(node[field]);
98
+ for (const field of ['children','items','columns','libraries','stateKeys','dataKeys','derivedKeys']) if (own(node,field)) node[field] = array(node[field]);
95
99
  if (['table','chart'].includes(node.type) && own(node,'rows')) node.rows = array(node.rows);
96
100
  for (const field of ['decimals','height']) if (own(node,field)) node[field] = number(node[field]);
97
101
  if(['row','column'].includes(node.type)){if(own(node,'gap'))node.gap=number(node.gap);if(node.type==='row'&&own(node,'columns'))node.columns=number(node.columns)}
package/lib/protocol.js CHANGED
@@ -56,7 +56,7 @@ export function validateState(doc, state) {
56
56
  }
57
57
  return result;
58
58
  }
59
- const ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };
59
+ const ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], ceil: [1, 1], floor: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };
60
60
  function reference(doc, ref) {
61
61
  if (typeof ref !== 'string' || !['state','data','derived'].includes(ref.split('.')[0]) || ref.split('.').length !== 2 || !key(ref.split('.')[1])) fail(`invalid reference ${JSON.stringify(ref)}; use state.key, data.key or derived.key with a letter/underscore identifier`);
62
62
  const [group, k] = ref.split('.');
@@ -100,7 +100,9 @@ export function evaluate(doc, state) {
100
100
  case 'div': if (a[1] === 0) fail('division by zero'); out = a[0] / a[1]; break;
101
101
  case 'min': out = Math.min(...a); break;
102
102
  case 'max': out = Math.max(...a); break;
103
- case 'round': out = Math.round(a[0]); break;
103
+ case 'round': out = Math.round(a[0]); break;
104
+ case 'ceil': out = Math.ceil(a[0]); break;
105
+ case 'floor': out = Math.floor(a[0]); break;
104
106
  case 'abs': out = Math.abs(a[0]); break;
105
107
  case 'lt': return a[0] < a[1];
106
108
  case 'lte': return a[0] <= a[1];
@@ -112,7 +114,9 @@ export function evaluate(doc, state) {
112
114
  };
113
115
  for (const k of Object.keys(doc.derived)) resolve(`derived.${k}`);
114
116
  if (new TextEncoder().encode(JSON.stringify(derived)).length > LIMITS.bytes) fail('derived output exceeds 128 KiB');
117
+ const resolveCell=cell=>{const out=expression(cell);if(!scalar(out))fail('inline row formula cells must resolve to a string, boolean or finite number');return out};
115
118
  const result = { derived, resolveValue: value => {
119
+ if (Array.isArray(value)) return value.map(row => object(row) ? Object.fromEntries(Object.entries(row).map(([k,cell])=>[k,object(cell)&&['const','ref','op'].some(field=>own(cell,field))?resolveCell(cell):cell])) : row);
116
120
  if (!object(value)) return value;
117
121
  return expression(value);
118
122
  } };
@@ -123,7 +127,10 @@ function checkViewValues(doc, evaluated) {
123
127
  if (n.type === 'table' || n.type === 'chart') {
124
128
  const rows = evaluated.resolveValue(n.rows);
125
129
  if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail('table/chart rows must reference an array of objects');
126
- if (n.type === 'chart' && rows.some(r => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail('chart requires labels and finite numeric values');
130
+ if (n.type === 'chart') {
131
+ const index=rows.findIndex(r=>!own(r,n.labelKey)||!scalar(r[n.labelKey])||!own(r,n.valueKey)||!finite(r[n.valueKey]));
132
+ if(index>=0){const row=rows[index],missing=[n.labelKey,n.valueKey].filter(k=>!own(row,k));fail('chart requires labels and finite numeric values. node.'+n.id+'.rows['+index+']: '+(missing.length?'missing field(s) '+missing.join(', '):n.valueKey+' must resolve to a finite number')+'. Available keys: '+Object.keys(row).slice(0,16).join(', ')+'. For live values use inline rows:[{'+n.labelKey+':"项目",'+n.valueKey+':{ref:"derived.amount"}}]; static data labels alone contain no amounts. Keep all nodes/state/derived when correcting the document');}
133
+ }
127
134
  if (n.type === 'chart' && n.xScale === 'linear' && rows.some(r => !finite(r[n.labelKey]))) fail('linear chart requires numeric x values');
128
135
  }
129
136
  if (n.type === 'metric' && !scalar(evaluated.resolveValue(n.value))) fail(`node.${n.id}.value must resolve to a number, string or boolean; use table for rows or a canvas for structured data`);
@@ -150,7 +157,7 @@ export function validateDocument(input) {
150
157
  for (const [k, allowed] of Object.entries({ layout: UI_LAYOUTS, accent: UI_ACCENTS, density: ['comfortable', 'compact'] }))
151
158
  if (doc.presentation[k] !== undefined && !allowed.includes(doc.presentation[k])) fail(`unknown presentation ${k}`);
152
159
  }
153
- if (doc.version !== 1) fail('unsupported protocol version');
160
+ if (doc.version !== 1) fail(own(doc,'version') ? 'document.version must be 1' : 'document.version is missing. render_ui creates a complete document, not a partial patch: send {version:1,title,fallback,nodes:[...]} for shared controls/formulas, or {title,items:[...]} for extended visuals. Include all content, not only data/derived');
154
161
  text(doc.title, 'title'); text(doc.fallback, 'fallback');
155
162
  if (!doc.title.trim() || !doc.fallback.trim()) fail('title and fallback cannot be empty');
156
163
  for (const group of ['state', 'data', 'derived', 'actions']) {
@@ -171,11 +178,13 @@ export function validateDocument(input) {
171
178
  } else if (['min', 'max', 'step'].some(p => own(s, p))) fail(`numeric limits on non-number ${k}`);
172
179
  if (s.options !== undefined && (!Array.isArray(s.options) || !s.options.length || s.options.length > 64 || !s.options.every(scalar))) fail(`state.${k}.options must be a direct array of 1..64 scalar values, e.g. options:["3","5","7"], not {item:[...]}`);
173
180
  }
174
- const defaults = validateState(doc, initialState(doc)); let expressions = 0;
181
+ const defaults = validateState(doc, initialState(doc)); let expressions = 0; const expressionShapes=new Set();
175
182
  for (const [k, spec] of Object.entries(doc.state))
176
183
  for (const option of spec.options ?? []) validateState(doc, { ...defaults, [k]: option });
177
184
  function validateExpression(e, depth = 0, itemRows, at = 'expression') {
178
- if (++expressions > LIMITS.expressions || depth > LIMITS.depth) fail('expression limit exceeded');
185
+ if (depth > LIMITS.depth) fail(at+': expression nesting exceeds '+LIMITS.depth);
186
+ const shape=JSON.stringify(e);
187
+ if(!expressionShapes.has(shape)){expressionShapes.add(shape);if(++expressions>LIMITS.expressions)fail(at+': expression limit exceeded (128 distinct expressions). Reuse derived references instead of expanding long formulas; keep the complete document when retrying');}
179
188
  if (!object(e)) fail(`${at}: expression must be an object`);
180
189
  if (own(e, 'map')) {
181
190
  if (depth !== 0) fail('map is only allowed at the root of a derived value');
@@ -219,7 +228,8 @@ export function validateDocument(input) {
219
228
  const ids = new Map(); let count = 0;
220
229
  const value = (v, at) => { if (object(v)) { if(own(v,'map'))fail('map belongs in derived');validateExpression(v); } else if (!scalar(v)) fail(`invalid value at ${at}`); };
221
230
  function nodes(list, depth = 0, path = 'nodes') {
222
- if (!Array.isArray(list) || depth > LIMITS.depth) fail('invalid node children or depth');
231
+ if (!Array.isArray(list)) fail(path==='nodes' ? 'document.nodes is missing or is not an array. State/derived define values but do not create visible UI. Include nodes:[{id:"heading",type:"heading",text:"..."},...] and actual controls in the SAME complete document; do not resend state/data/derived alone. Declared state keys: '+Object.keys(doc.state).join(', ') : path+' must be an array of child nodes; use children:[{id,type,...}]');
232
+ if (depth > LIMITS.depth) fail(path+' exceeds maximum node depth '+LIMITS.depth);
223
233
  for (const [index,n] of list.entries()) {
224
234
  const at = `${path}[${index}]`;
225
235
  if (++count > LIMITS.nodes) fail('too many nodes');
@@ -245,6 +255,7 @@ export function validateDocument(input) {
245
255
  if (!n.fallback.trim()) fail(`${at}.fallback must not be empty`);
246
256
  if (typeof n.html !== 'string' || !n.html.trim()) fail(`${at}.html must be a non-empty HTML string; script/css/fallback belong beside html on the sandbox node, not inside it`);
247
257
  for (const field of ['html', 'css', 'script']) if (n[field] !== undefined && (typeof n[field] !== 'string' || n[field].length > SANDBOX_LIMITS.source)) fail('canvas source exceeds 48 Ki characters');
258
+ if(n.libraries!==undefined&&(!Array.isArray(n.libraries)||n.libraries.length>1||n.libraries.some(v=>v!=='mechanism')))fail('canvas libraries supports only ["mechanism"] or []');
248
259
  if (n.height !== undefined && (!Number.isInteger(n.height) || n.height < 160 || n.height > SANDBOX_LIMITS.height)) fail('canvas height must be 160..1000');
249
260
  for (const [field, group] of [['stateKeys', 'state'], ['dataKeys', 'data'], ['derivedKeys', 'derived']]) if (n[field] !== undefined && (!Array.isArray(n[field]) || n[field].length > 64 || new Set(n[field]).size !== n[field].length || n[field].some(k => !own(doc[group], k)))) fail('invalid canvas ' + field);
250
261
  break;
@@ -289,7 +300,8 @@ export function validateDocument(input) {
289
300
  if (n.type === 'toggle' && spec.type !== 'boolean') fail('toggle requires boolean state');
290
301
  if (['input','select'].includes(n.type) && spec.type === 'json') fail(`node.${n.id}: structured state requires a sandbox with DSHUI.setState; use separate boolean states and toggle nodes for a native checklist`);
291
302
  if (n.type === 'input' && spec.type === 'boolean') fail('use toggle for boolean state');
292
- if (n.type === 'select' && !spec.options) fail('select requires options'); break;
303
+ if (n.type === 'select' && !spec.options) fail('select requires options');
304
+ if(n.type==='select'&&n.variant!==undefined&&!['dropdown','segmented'].includes(n.variant))fail(`node.${n.id}.variant must be dropdown or segmented`);break;
293
305
  }
294
306
  case 'code':
295
307
  if(typeof n.code!=='string'||n.code.length>SANDBOX_LIMITS.source)fail('code must be a string of at most 48 Ki characters');
@@ -310,6 +322,9 @@ export function validateDocument(input) {
310
322
  if (n.icon !== undefined && !UI_ICONS.includes(n.icon)) fail('unknown UI icon');
311
323
  if (!['button','link'].includes(n.type)) { if (n.tone !== undefined && !UI_TONES.includes(n.tone)) fail('unknown UI tone'); }
312
324
  }
325
+ if (['table','chart'].includes(n.type) && Array.isArray(n.rows)) {
326
+ for (const [i,row] of n.rows.entries()) if(object(row)) for(const [k,cell] of Object.entries(row)) if(object(cell)&&['const','ref','op'].some(field=>own(cell,field))) value(cell,at+'.rows['+i+'].'+k);
327
+ }
313
328
  fields(n, componentFields(n.type), `node.${n.id}`);
314
329
  if (['row', 'column', 'card', 'details'].includes(n.type)) nodes(n.children, depth + 1, `${at}.children`);
315
330
  }
package/lib/sandbox.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {MECHANISM_RUNTIME} from './mechanism-runtime-bundle.js';
1
2
  import { measureVisual } from './visual-audit.js';
2
3
  import { formatNumber } from './format-number.js';
3
4
  import { CANVAS_SYSTEM_CSS } from './canvas-system.js';
@@ -35,6 +36,7 @@ export function sandboxDocument(node, channel, nonce) {
35
36
  motion.addEventListener('change',refresh);document.addEventListener('visibilitychange',refresh);
36
37
  const formatNumber=${formatNumber.toString()};
37
38
  const api={formatNumber,get snapshot(){return structuredClone(snapshot)},subscribe(fn){if(typeof fn!=='function')throw Error('Listener required');listeners.add(fn);fn(api.snapshot);return()=>listeners.delete(fn)},setState(patch){if(!snapshot.disabled)send('patch',patch)},animate(fn){if(typeof fn!=='function')throw Error('Animation callback required');animations.add(fn);refresh();return()=>{animations.delete(fn);if(!animations.size){cancelAnimationFrame(raf);raf=0;last=0}}}};
39
+ ${node.libraries?.includes('mechanism')?MECHANISM_RUNTIME.replace(/<\/script/gi,'<\\/script')+';api.mechanism=__MotionMechanism.createMechanismAPI(api);':''}
38
40
  Object.defineProperty(window,'DSHUI',{value:Object.freeze(api),writable:false});
39
41
  for(const type of ['click','input','change','keydown','pointerdown'])document.addEventListener(type,e=>{if(snapshot.disabled){e.preventDefault();e.stopImmediatePropagation()}},true);
40
42
  addEventListener('message',event=>{
@@ -0,0 +1,28 @@
1
+ // Visual grammar, shared by native rendering, previews and model guidance.
2
+ // IDs are stable configuration values; no additional user switches are introduced.
3
+ export const STYLE_SYSTEM=Object.freeze({
4
+ editorial:{description:'开放正文 · 杂志层级 · 细线分组',grammar:'编辑式阅读',composition:'先给一个明确结论,再用开放正文、左右对齐比较和细线段落展开。用 column 组织文章,card 在此风格中是章节,不是封闭面板;不要给每段套框。指标嵌在解释旁,流程像文中的编号提纲。',typography:'标题有衬线阅读感,正文行距舒展;标题、正文、注释至少三个层级,强调依赖字重和位置。',surfaces:'透明正文、细分隔线、低阴影或无阴影;保持结构性留白,不加大面积渐变。',controls:'表单像文章中的设置行,标签与当前值左右对齐;按钮紧凑,代码像有边注的摘录。',avoid:'避免大彩色卡片、每个数字一个盒子、粗重边框和装饰性背景。',native:{title:27,weight:650,gap:24,radius:12,metric:32,feedback:210,gain:.25}},
5
+ soft:{description:'笔记色块 · 柔圆分组 · 舒缓节奏',grammar:'柔和彩色笔记',composition:'像清晰友好的笔记:把相关内容放在柔圆色块里,先提示主题,再给两三项要点。章节标题可以是小标签;比较项如并排便签,流程像一串有间距的笔记。',typography:'圆润标题和温和字重;正文清楚,数值与单位贴近,次要说明放在组内。',surfaces:'低饱和彩色便签、不同章节轻微交替色块、不对称的大圆角;使用实色底或少量柔和渐变。',controls:'控件内嵌在柔色分组,选择器像标签,操作有柔和回弹;避免每行都变成边框表单。',avoid:'避免工程网格、重黑海报、透明玻璃、泛白低对比度文字。',native:{title:25,weight:700,gap:20,radius:28,metric:36,feedback:390,gain:.7}},
6
+ technical:{description:'工程网格 · 数据条带 · 编号连接',grammar:'技术说明与工程图',composition:'像工程说明:按输入→机制→输出组织;优先对齐的数据条带、编号流程、表格和有意义的连接线。指标放成紧凑测量带而非一排装饰卡;比较围绕相同维度对齐。',typography:'中文正文保持正常字号,数值/单位/序号用等宽字体与 tabular-nums;标题简短精确,明确基准、单位和假设。',surfaces:'细线网格、规则的小圆角、清晰边界和表头、无浮动投影。网格仅在图解背景使用,文字区域保持干净。',controls:'标签和输入严格对齐,参数区像仪表设置行;按钮和分段选项采用规则矩形,保留键盘及全局反馈。',avoid:'避免胶囊堆叠、巨大海报数字、彩色便签、装饰箭头、靠小字硬塞信息。',native:{title:22,weight:750,gap:16,radius:10,metric:28,feedback:180,gain:.3}},
7
+ expressive:{description:'海报层级 · 非对称重点 · 醒目数字',grammar:'活力展示与重点海报',composition:'先选一个真正的视觉中心:大标题或关键数字,再用不对称分栏展开解释。主要结论占更大空间,次要比较退后;流程用醒目的序号与短句。不要把每一项都做成同等大小卡片。',typography:'短而有力的重标题、大数字、紧凑标题行距,正文仍易读。最多一个主要强调区,其余留白和细线辅助。',surfaces:'实色色块、粗细明确的边线、局部高对比强调;可用橙/珊瑚/暖黄,但不能整页争抢注意力。',controls:'主要操作醒目、次要操作轻量;表单和数据保持对齐,回弹比阅读风格明显但不空闲循环。',avoid:'避免六张等大的指标卡、满屏渐变、全屏大留白、无意义数字装饰。',native:{title:34,weight:850,gap:22,radius:20,metric:50,feedback:480,gain:1.1}},
8
+ prismframe:{description:'应用模块 · 实色控件 · 偏移底面',grammar:'明亮扁平的轻量应用',composition:'像可直接使用的轻量应用:任务标题、输入模块、结果模块、解释区职责清楚。模块使用稳定网格,操作靠近对应结果;比较、流程和数据看板各自采用合适结构,不固定成同一套卡片。',typography:'圆润清晰的系统字体、明确模块标题、中等大数字;数字绑定名称和单位,图形承担关系说明。',surfaces:'浅彩底、白色实心模块、细彩色边线和扁平偏移底面;局部多色用来区分职责,避免每块同色同形。',controls:'整齐且有面积的实色表单行、清晰状态标签,主要操作有轻微偏移底面;不是磨砂玻璃。',avoid:'避免文档式满屏细线、海报式超大字、透明层叠、视频页码和字幕、重复标题。',native:{title:26,weight:780,gap:20,radius:24,metric:36,feedback:320,gain:.65}},
9
+ aurora:{description:'产品层级 · 暖白曲面 · 局部渐彩',grammar:'柔光渐彩的精致产品界面',composition:'像精致的产品面板:暖白开放背景上安排少量实心表面,标题简洁、指标突出、控件顺手。用宽窄和疏密区分主要结果与次要说明;按钮、表单、表格、代码与文字要属于同一套语言。',typography:'近黑标题、清晰的字重差、大而利落的数字、柔和次要说明;不把文字都染成紫色。',surfaces:'暖白实色、24..30px 圆润表面、柔和投影、局部紫蓝青渐彩强调;不把整页铺成紫面板。',controls:'圆润内嵌输入、胶囊选择器、深色主操作与渐彩图标圆点;真实操作触发渐变扩散和弹性反馈。',avoid:'禁止玻璃/backdrop-filter、泛白低对比度、仅加一个演示按钮就算风格、空闲漂浮。',native:{title:28,weight:780,gap:24,radius:28,metric:42,feedback:460,gain:1}}
10
+ });
11
+ export function grammarPrompt(id){const g=STYLE_SYSTEM[id];return `视觉语言:${g.grammar}\n构图:${g.composition}\n文字层级:${g.typography}\n表面与边界:${g.surfaces}\n控件与反馈:${g.controls}\n避免:${g.avoid}\n同一任务在此风格下应具有可辨识的层级与构图,不能只换强调色。所有风格共用原生交互能力;动画只回应真实操作、支持 reduced motion。保持内容完整和 DOM 阅读顺序,不为了风格重排计算含义。用户明确指定的尺寸、颜色与布局优先。禁止玻璃材质。示例演示构图,不要求每次输出所有模块;简单需求用简短正文或一组数字,不能为了展示风格扩写内容。`;}
12
+ // Same facts, intentionally different content composition. Model examples are
13
+ // complete small documents; preview uses the same grammar without live controls.
14
+ export function grammarExample(id){
15
+ const heading={type:'heading',text:'先交付一个可用版本'},body={type:'text',text:'把目标缩小,把关键体验做好。'},note={type:'text',text:'先验证,再逐步完善。',color:'muted'};
16
+ const choice=(title,text)=>({type:'card',title,children:[{type:'text',text}]});
17
+ const choices=[choice('先做小版本','验证重点'),choice('先铺全功能','覆盖需求')];
18
+ const metrics=[{type:'metric',label:'推进阶段',value:3,unit:'步'},{type:'metric',label:'核心目标',value:1,unit:'个'}];
19
+ const flow={type:'steps',items:[{title:'明确目标',description:'先抓住重点。'},{title:'解释关系',description:'用图形说明。'},{title:'给出行动',description:'收成下一步。'}]};
20
+ let preset='brief',blocks;
21
+ if(id==='editorial'){preset='reading';blocks=[heading,body,{type:'row',columns:2,children:choices},{type:'row',columns:2,children:metrics},flow,note]}
22
+ else if(id==='soft'){blocks=[{type:'card',title:'今天的重点',children:[heading,body]},{type:'row',columns:2,children:choices},flow,{type:'row',columns:2,children:metrics},note]}
23
+ else if(id==='technical'){blocks=[heading,body,{type:'row',columns:2,children:metrics},flow,{type:'table',label:'推进方式',columns:[{key:'method',label:'方案'},{key:'focus',label:'重点'}],rows:[{method:'先做小版本',focus:'验证重点'},{method:'先铺全功能',focus:'覆盖需求'}]},note]}
24
+ else if(id==='expressive'){preset='minimal';blocks=[heading,{type:'row',columns:2,children:[{type:'column',children:[metrics[0],body]},{type:'column',children:[metrics[1],note]}]},{type:'row',columns:2,children:choices},flow]}
25
+ else if(id==='prismframe'){blocks=[heading,body,{type:'row',columns:2,children:choices},{type:'row',columns:2,children:metrics},{type:'card',title:'行动路径',children:[flow]},note]}
26
+ else{blocks=[heading,body,{type:'row',columns:2,children:metrics},{type:'row',columns:2,children:choices},flow,note]}
27
+ return {tool:'render_text',args:{title:'把复杂问题讲清楚',preset,blocks}};
28
+ }