parapoly-runtime 1.0.1 → 1.0.3

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.
package/README.md CHANGED
@@ -41,6 +41,9 @@ node node_modules/parapoly-runtime/cli/code3d-build.js ./my-project
41
41
 
42
42
  ```bash
43
43
  code3d-install-instructions
44
+
45
+ # 或通过 node 直接运行
46
+ node node_modules/parapoly-runtime/cli/code3d-install-instructions.js
44
47
  ```
45
48
 
46
49
  或指定目标目录:
@@ -41,11 +41,11 @@
41
41
  *
42
42
  * 编译流程:
43
43
  * 1. 读取 code3d-workspace.json
44
- * 2. 递归收集所有 .code3d.js .part3d 文件(跳过 node_modules/dist/.git)
45
- * 3. 语法检查所有 .code3d.js 模块
44
+ * 2. 递归收集所有 .code3d.js / .part3d.js / .sketch3d.js / .part3d 文件(跳过 node_modules/dist/.git)
45
+ * 3. 语法检查所有 .code3d.js / .part3d.js / .sketch3d.js 模块
46
46
  * 4. 从 main 入口构建依赖图(支持远程 URL 和绝对路径)
47
47
  * 5. 拓扑排序确定编译顺序
48
48
  * 6. 生成 bundle(所有模块打包为单文件)
49
49
  * 7. 根据模式输出 .bin 或 .js 文件
50
50
  */
51
- const fs=require("fs"),path=require("path"),https=require("https"),http=require("http"),zlib=require("zlib"),vm=require("vm");function fetchUrl(e){return new Promise((t,n)=>{(e.startsWith("https")?https:http).get(e,s=>{if(s.statusCode>=300&&s.statusCode<400&&s.headers.location)return void fetchUrl(s.headers.location).then(t,n);if(200!==s.statusCode)return void n(new Error(`HTTP ${s.statusCode} for ${e}`));let r="";s.setEncoding("utf-8"),s.on("data",e=>r+=e),s.on("end",()=>t(r)),s.on("error",n)}).on("error",n)})}function isUrl(e){return e.startsWith("http://")||e.startsWith("https://")}function extractDependencies(e){let t=[],n=e.split("\n");for(let e of n){if(e.trim().startsWith("//"))continue;let n,s=/use\s*\(\s*["']([^"']+)["']/g;for(;null!==(n=s.exec(e));)t.indexOf(n[1])<0&&t.push(n[1])}return t}function isAbsolutePath(e){return!!/^[a-zA-Z]:[/\\]/.test(e)||!!e.startsWith("/")}function normalizePath(e){return e.replace(/\\/g,"/")}function resolvePath(e,t){if(isUrl(t))return t;if(isAbsolutePath(t))return normalizePath(t);if(t.startsWith("./")||t.startsWith("../")){let n=e.replace(/\\/g,"/").split("/");n.pop();let s=t.split("/");for(let e of s)"."!==e&&""!==e&&(".."===e?n.pop():n.push(e));return"./"+n.filter(e=>""!==e&&"."!==e).join("/")}return t}async function buildDependencyGraph(e,t){let n=new Map,s=[t],r=new Set;for(;s.length>0;){let t=s.shift();if(r.has(t))continue;if(r.add(t),!e[t]&&isUrl(t)){console.log(` Fetching: ${t}`);let n=await fetchUrl(t);65279===n.charCodeAt(0)&&(n=n.slice(1)),e[t]=n}if(!e[t]&&isAbsolutePath(t)){let n=t.replace(/\//g,path.sep);if(fs.existsSync(n)){let s=fs.readFileSync(n,"utf-8");65279===s.charCodeAt(0)&&(s=s.slice(1)),e[t]=s}}let o=e[t];if(!o)throw new Error("File not found: "+t);if(t.endsWith(".part3d")){n.set(t,[]);continue}let i=extractDependencies(o),l=[];for(let e of i){let n=resolvePath(t,e);l.push(n),r.has(n)||s.push(n)}n.set(t,l)}return n}function topologicalSort(e){let t=[],n=new Set,s=new Set;function r(o){if(n.has(o))return;if(s.has(o))throw new Error("Circular dependency detected involving: "+o);s.add(o);let i=e.get(o)||[];for(let e of i)r(e);s.delete(o),n.add(o),t.push(o)}for(let t of e.keys())r(t);return t}function rewriteUsePaths(e,t){return e.replace(/use\s*\(\s*["']([^"']+)["']/g,(e,n)=>{let s=resolvePath(t,n);return`use(${JSON.stringify(s)}`})}function extractFunctionBody(e){let t=e.match(/let\s+main\s*=\s*function\s*\([^)]*\)\s*\{/);if(!t)return e;let n=t.index+t[0].length,s=1,r=n;for(let t=n;t<e.length;t++)if("{"===e[t])s++;else if("}"===e[t]&&(s--,0===s)){r=t;break}return e.substring(n,r).trim()}function checkSyntax(e,t){let n=[];if(!/let\s+main\s*=\s*function/.test(e))return n.push({line:1,column:1,message:"缺少 main 函数定义 (需要 'let main = function(...) { ... }')"}),n;let s=extractFunctionBody(e);/return\s+/.test(s)||n.push({line:null,column:null,message:"main 函数没有 return 语句"});try{let e=`(function(params, use, PartBlock3dDocument) { ${s} })`;new vm.Script(e,{filename:t})}catch(e){let t=e.stack?.match(/:(\d+)/)?.[1],s=e.message;n.push({line:t?parseInt(t):null,column:null,message:s})}return n}async function bundle(e,t){let n="./"+e.main,s=topologicalSort(await buildDependencyGraph(t,n)),r=!1;for(let e in t){if(!e.endsWith(".code3d.js"))continue;let n=checkSyntax(t[e],e);for(let t of n){r=!0;let n=t.line?` (行 ${t.line})`:"";console.error(` ✗ ${e}${n}: ${t.message}`)}}if(r)throw new Error("语法检查未通过,编译中止");console.log(" 语法检查通过");let o=[];o.push("let main = function() {"),o.push(" const __modules = {};"),o.push(" const __cache = {};"),o.push("");for(let e in t)if(e.endsWith(".part3d")){let n=JSON.stringify(t[e]),s=e.replace(/\\/g,"/");o.push(` __modules[${JSON.stringify(s)}] = function(params) {`),o.push(" let recompile = params?.recompile !== undefined ? params.recompile : false;"),o.push(" let color = params?.color;"),o.push(` return PartBlock3dDocument.create_doc_from_part3d(${n}, recompile, color);`),o.push(" };"),o.push("")}for(let e of s){if(e===n)continue;if(e.endsWith(".part3d"))continue;let s=extractFunctionBody(rewriteUsePaths(t[e],e)).replace(/\\/g,"\\\\").replace(/`/g,"\\`").replace(/\$\{/g,"\\${");o.push(` __modules[${JSON.stringify(e)}] = new Function("params", "use", "PartBlock3dDocument", \`${s}\`);`),o.push("")}o.push(" function use(path, params) {"),o.push(' let key = path + "|" + JSON.stringify(params || {});'),o.push(" if (__cache[key]) return __cache[key];"),o.push(' if (!__modules[path]) throw new Error("Module not found: " + path);'),o.push(" if (typeof postMessage === 'function') {"),o.push(" let label = path.endsWith('.part3d') ? '加载资源: ' : '编译模块: ';"),o.push(" try { postMessage({ cmd: 'log', log_type: 'debug', message: label + path }); } catch(e) {}"),o.push(" }"),o.push(" try {"),o.push(" __cache[key] = __modules[path](params, use, PartBlock3dDocument);"),o.push(" } catch(e) {"),o.push(" let lineInfo = '';"),o.push(" var stack = e.stack || '';"),o.push(" var m = stack.match(/<anonymous>:(\\d+):(\\d+)/);"),o.push(" if (m) lineInfo = ' (行 ' + (parseInt(m[1]) - 1) + ', 列 ' + m[2] + ')';"),o.push(" throw new Error('[' + path + lineInfo + '] ' + e.message);"),o.push(" }"),o.push(" return __cache[key];"),o.push(" }"),o.push("");let i=extractFunctionBody(rewriteUsePaths(t[n],n));return o.push(" // --- entry ---"),o.push(` ${i}`),o.push("}"),o.join("\n")}function computeOutputFilename(e,t){let n=e.build?.filename||e.name;return n=n.replace(/\.code3d\.js\.bin$/,"").replace(/\.code3d\.js$/,"").replace(/\.code3d$/,""),n+(t?".code3d.js.bin":".code3d.js")}async function main_cli(){let e=process.argv.slice(2),t=e.includes("--no-bin");e=e.filter(e=>"--bin"!==e&&"--no-bin"!==e);let n=e[0]||process.cwd();n=path.resolve(n);let s=path.join(n,"package.json");fs.existsSync(s)||(console.error("Error: package.json not found in "+n),process.exit(1));let r=JSON.parse(fs.readFileSync(s,"utf-8"));r.name&&r.main||(console.error("Error: package.json must contain 'name' and 'main' fields"),process.exit(1));let o={},i=path.join(n,"code3d-workspace-config.json");fs.existsSync(i)&&(o=JSON.parse(fs.readFileSync(i,"utf-8")));let l={name:r.name,version:r.version||"1.0.0",main:r.main,...o},a=!1!==l.build?.bin;t&&(a=!1),console.log(`Building project: ${l.name} v${l.version}`),console.log(`Entry: ${l.main}`);let c={};collectFiles(n,n,c),console.log(`Found ${Object.keys(c).length} module(s)`);let u=await bundle(l,c),p=(l.build?.output||"dist")+"/"+computeOutputFilename(l,a),h=path.join(n,p),f=path.dirname(h);fs.existsSync(f)||fs.mkdirSync(f,{recursive:!0});let d=Buffer.byteLength(u,"utf-8");if(a){let e=zlib.gzipSync(Buffer.from(u,"utf-8"),{level:9});fs.writeFileSync(h,e);let t=(100*(1-e.length/d)).toFixed(1);console.log(`Binary written to: ${p} (${(e.length/1024).toFixed(1)} KB, -${t}% from ${(d/1024).toFixed(1)} KB source)`)}else fs.writeFileSync(h,u,"utf-8"),console.log(`Bundle written to: ${p} (${(d/1024).toFixed(1)} KB)`);console.log("Done.")}function collectFiles(e,t,n){let s=fs.readdirSync(t);for(let r of s){let s=path.join(t,r);if(fs.statSync(s).isDirectory()){if("node_modules"===r||"dist"===r||".git"===r)continue;collectFiles(e,s,n)}else if(r.endsWith(".code3d.js")||r.endsWith(".part3d")){let t="./"+path.relative(e,s).replace(/\\/g,"/"),r=fs.readFileSync(s,"utf-8");65279===r.charCodeAt(0)&&(r=r.slice(1)),n[t]=r}}}main_cli().catch(e=>{console.error("Build failed:",e.message),process.exit(1)});
51
+ const fs=require("fs"),path=require("path"),https=require("https"),http=require("http"),zlib=require("zlib"),vm=require("vm");function fetchUrl(e){return new Promise((t,n)=>{(e.startsWith("https")?https:http).get(e,s=>{if(s.statusCode>=300&&s.statusCode<400&&s.headers.location)return void fetchUrl(s.headers.location).then(t,n);if(200!==s.statusCode)return void n(new Error(`HTTP ${s.statusCode} for ${e}`));let r="";s.setEncoding("utf-8"),s.on("data",e=>r+=e),s.on("end",()=>t(r)),s.on("error",n)}).on("error",n)})}function isUrl(e){return e.startsWith("http://")||e.startsWith("https://")}function extractDependencies(e){let t=[],n=e.split("\n");for(let e of n){if(e.trim().startsWith("//"))continue;let n,s=/use\s*\(\s*["']([^"']+)["']/g;for(;null!==(n=s.exec(e));)t.indexOf(n[1])<0&&t.push(n[1])}return t}function isAbsolutePath(e){return!!/^[a-zA-Z]:[/\\]/.test(e)||!!e.startsWith("/")}function normalizePath(e){return e.replace(/\\/g,"/")}function resolvePath(e,t){if(isUrl(t))return t;if(isAbsolutePath(t))return normalizePath(t);if(t.startsWith("./")||t.startsWith("../")){let n=e.replace(/\\/g,"/").split("/");n.pop();let s=t.split("/");for(let e of s)"."!==e&&""!==e&&(".."===e?n.pop():n.push(e));return"./"+n.filter(e=>""!==e&&"."!==e).join("/")}return t}async function buildDependencyGraph(e,t){let n=new Map,s=[t],r=new Set;for(;s.length>0;){let t=s.shift();if(r.has(t))continue;if(r.add(t),!e[t]&&isUrl(t)){console.log(` Fetching: ${t}`);let n=await fetchUrl(t);65279===n.charCodeAt(0)&&(n=n.slice(1)),e[t]=n}if(!e[t]&&isAbsolutePath(t)){let n=t.replace(/\//g,path.sep);if(fs.existsSync(n)){let s=fs.readFileSync(n,"utf-8");65279===s.charCodeAt(0)&&(s=s.slice(1)),e[t]=s}}let o=e[t];if(!o)throw new Error("File not found: "+t);if(t.endsWith(".part3d")||t.endsWith(".block3d")||t.endsWith(".part3d.js")||t.endsWith(".sketch3d.js")){n.set(t,[]);continue}let i=extractDependencies(o),l=[];for(let e of i){let n=resolvePath(t,e);l.push(n),r.has(n)||s.push(n)}n.set(t,l)}return n}function topologicalSort(e){let t=[],n=new Set,s=new Set;function r(o){if(n.has(o))return;if(s.has(o))throw new Error("Circular dependency detected involving: "+o);s.add(o);let i=e.get(o)||[];for(let e of i)r(e);s.delete(o),n.add(o),t.push(o)}for(let t of e.keys())r(t);return t}function rewriteUsePaths(e,t){return e.replace(/use\s*\(\s*["']([^"']+)["']/g,(e,n)=>{let s=resolvePath(t,n);return`use(${JSON.stringify(s)}`})}function extractFunctionBody(e){let t=e.match(/let\s+main\s*=\s*function\s*\([^)]*\)\s*\{/);if(!t)return e;let n=t.index+t[0].length,s=1,r=n;for(let t=n;t<e.length;t++)if("{"===e[t])s++;else if("}"===e[t]&&(s--,0===s)){r=t;break}return e.substring(n,r).trim()}function checkSyntax(e,t){let n=[];if(!/let\s+main\s*=\s*function/.test(e))return n.push({line:1,column:1,message:"缺少 main 函数定义 (需要 'let main = function(...) { ... }')"}),n;let s=extractFunctionBody(e);/return\s+/.test(s)||n.push({line:null,column:null,message:"main 函数没有 return 语句"});try{let e=`(function(params, use, PartBlock3dDocument) { ${s} })`;new vm.Script(e,{filename:t})}catch(e){let t=e.stack?.match(/:(\d+)/)?.[1],s=e.message;n.push({line:t?parseInt(t):null,column:null,message:s})}return n}async function bundle(e,t){let n="./"+e.main,s=topologicalSort(await buildDependencyGraph(t,n)),r=!1;for(let e of s){if(!e.endsWith(".code3d.js")&&!e.endsWith(".part3d.js")&&!e.endsWith(".sketch3d.js"))continue;let n=checkSyntax(t[e],e);for(let t of n){r=!0;let n=t.line?` (行 ${t.line})`:"";console.error(` ✗ ${e}${n}: ${t.message}`)}}if(r)throw new Error("语法检查未通过,编译中止");console.log(" 语法检查通过");let o=[];o.push("let main = function() {"),o.push(" const __modules = {};"),o.push(" const __cache = {};"),o.push("");for(let e in t)if(e.endsWith(".part3d")){let n=JSON.stringify(t[e]),s=e.replace(/\\/g,"/");o.push(` __modules[${JSON.stringify(s)}] = function(params) {`),o.push(" let recompile = params?.recompile !== undefined ? params.recompile : false;"),o.push(" let color = params?.color;"),o.push(` return PartBlock3dDocument.create_doc_from_part3d(${n}, recompile, color);`),o.push(" };"),o.push("")}for(let e in t)if(e.endsWith(".block3d")){let n=JSON.stringify(t[e]),s=e.replace(/\\/g,"/");o.push(` __modules[${JSON.stringify(s)}] = function(params) {`),o.push(` return PartBlock3dDocument.create_doc_from_block3d(${n});`),o.push(" };"),o.push("")}for(let e of s){if(e===n)continue;if(e.endsWith(".part3d"))continue;if(e.endsWith(".block3d"))continue;let s=extractFunctionBody(rewriteUsePaths(t[e],e)).replace(/\\/g,"\\\\").replace(/`/g,"\\`").replace(/\$\{/g,"\\${");o.push(` __modules[${JSON.stringify(e)}] = new Function("params", "use", "PartBlock3dDocument", \`${s}\`);`),o.push("")}o.push(" function use(path, params) {"),o.push(' let key = path + "|" + JSON.stringify(params || {});'),o.push(" if (__cache[key]) return __cache[key];"),o.push(' if (!__modules[path]) throw new Error("Module not found: " + path);'),o.push(" if (typeof postMessage === 'function') {"),o.push(" let label = (path.endsWith('.part3d') || path.endsWith('.block3d')) ? '加载资源: ' : '编译模块: ';"),o.push(" try { postMessage({ cmd: 'log', log_type: 'debug', message: label + path }); } catch(e) {}"),o.push(" }"),o.push(" try {"),o.push(" __cache[key] = __modules[path](params, use, PartBlock3dDocument);"),o.push(" } catch(e) {"),o.push(" let lineInfo = '';"),o.push(" var stack = e.stack || '';"),o.push(" var m = stack.match(/<anonymous>:(\\d+):(\\d+)/);"),o.push(" if (m) lineInfo = ' (行 ' + (parseInt(m[1]) - 1) + ', 列 ' + m[2] + ')';"),o.push(" throw new Error('[' + path + lineInfo + '] ' + e.message);"),o.push(" }"),o.push(" return __cache[key];"),o.push(" }"),o.push("");let i=extractFunctionBody(rewriteUsePaths(t[n],n));return o.push(" // --- entry ---"),o.push(` ${i}`),o.push("}"),o.join("\n")}function computeOutputFilename(e,t){let n=e.build?.filename||e.name;return n=n.replace(/\.code3d\.js\.bin$/,"").replace(/\.code3d\.js$/,"").replace(/\.code3d$/,""),n+(t?".code3d.js.bin":".code3d.js")}async function main_cli(){let e=process.argv.slice(2),t=e.includes("--no-bin");e=e.filter(e=>"--bin"!==e&&"--no-bin"!==e);let n=e[0]||process.cwd();n=path.resolve(n);let s=path.join(n,"package.json");fs.existsSync(s)||(console.error("Error: package.json not found in "+n),process.exit(1));let r=JSON.parse(fs.readFileSync(s,"utf-8"));r.name&&r.main||(console.error("Error: package.json must contain 'name' and 'main' fields"),process.exit(1));let o={},i=path.join(n,"code3d-workspace-config.json");fs.existsSync(i)&&(o=JSON.parse(fs.readFileSync(i,"utf-8")));let l={name:r.name,version:r.version||"1.0.0",main:r.main,...o},a=!1!==l.build?.bin;t&&(a=!1),console.log(`Building project: ${l.name} v${l.version}`),console.log(`Entry: ${l.main}`);let c={},u=l.build?.output||"dist";collectFiles(n,n,c,u),console.log(`Found ${Object.keys(c).length} module(s)`);let h=await bundle(l,c),p=u+"/"+computeOutputFilename(l,a),d=path.join(n,p),f=path.dirname(d);fs.existsSync(f)||fs.mkdirSync(f,{recursive:!0});let m=Buffer.byteLength(h,"utf-8");if(a){let e=zlib.gzipSync(Buffer.from(h,"utf-8"),{level:9});fs.writeFileSync(d,e);let t=(100*(1-e.length/m)).toFixed(1);console.log(`Binary written to: ${p} (${(e.length/1024).toFixed(1)} KB, -${t}% from ${(m/1024).toFixed(1)} KB source)`)}else fs.writeFileSync(d,h,"utf-8"),console.log(`Bundle written to: ${p} (${(m/1024).toFixed(1)} KB)`);console.log("Done.")}function collectFiles(e,t,n,s){let r=fs.readdirSync(t);for(let o of r){let r=path.join(t,o);if(fs.statSync(r).isDirectory()){if("node_modules"===o||o===s||".git"===o)continue;collectFiles(e,r,n,s)}else if(o.endsWith(".code3d.js")||o.endsWith(".part3d.js")||o.endsWith(".sketch3d.js")||o.endsWith(".part3d")||o.endsWith(".block3d")){let t="./"+path.relative(e,r).replace(/\\/g,"/"),s=fs.readFileSync(r,"utf-8");65279===s.charCodeAt(0)&&(s=s.slice(1)),n[t]=s}}}main_cli().catch(e=>{console.error("Build failed:",e.message),process.exit(1)});
@@ -14,8 +14,10 @@
14
14
  * 从而在编写 .code3d.js 文件时提供更准确的代码建议。
15
15
  *
16
16
  * 安装的文件:
17
- * .github/instructions/code3d-project-workspace.instructions.md
18
- * .github/instructions/code3d.instructions.md
17
+ * .github/instructions/parapoly/code3d-project-workspace.instructions.md
18
+ * .github/instructions/parapoly/code3d.instructions.md
19
+ * .github/instructions/parapoly/part3d.instructions.md
20
+ * .github/instructions/parapoly/sketch3d.instructions.md
19
21
  */
20
22
 
21
23
  const fs = require("fs");
@@ -23,7 +25,7 @@ const path = require("path");
23
25
 
24
26
  const targetDir = path.resolve(process.argv[2] || ".");
25
27
  const instructionsSource = path.join(__dirname, "..", "instructions");
26
- const instructionsTarget = path.join(targetDir, ".github", "instructions");
28
+ const instructionsTarget = path.join(targetDir, ".github", "instructions", "parapoly");
27
29
 
28
30
  // Validate target is a project directory
29
31
  if (!fs.existsSync(path.join(targetDir, "package.json"))) {
@@ -26,6 +26,9 @@ my-project/
26
26
  │ ├── wall.code3d.js
27
27
  │ └── details/
28
28
  │ └── window.code3d.js
29
+ ├── exports/ # 从编辑器导出的文件
30
+ │ ├── gear.part3d.js # Part3D 导出的可执行代码
31
+ │ └── profile.sketch3d.js # Sketch3D 导出的可执行代码
29
32
  ├── assets/ # .part3d 资源文件
30
33
  │ └── model.part3d
31
34
  └── dist/ # 编译输出(只生成一个文件)
@@ -112,6 +115,25 @@ use("./model.part3d", { recompile: true }) // 强制重新编译
112
115
  use("./model.part3d", { color: "#ff0000" }) // 覆盖颜色
113
116
  ```
114
117
 
118
+ ### .part3d.js / .sketch3d.js 模块
119
+
120
+ `.part3d.js` 和 `.sketch3d.js` 是从 Part3D/Sketch3D 编辑器导出的可执行 JS 文件,结构与 `.code3d.js` 相同(`let main = function() { ... }`),可直接通过 `use()` 引用为子模块:
121
+
122
+ ```javascript
123
+ // 引用导出的 Part3D 模型
124
+ let part = use("./exports/gear.part3d.js")
125
+ block3d_doc.add_part(part, "union", "gear")
126
+ block3d_doc.translate(0, 5, 0)
127
+
128
+ // 引用导出的 Sketch3D 草图(返回包含 sketch 的 PartBlock3dDocument)
129
+ let sketch = use("./sketches/profile.sketch3d.js")
130
+ ```
131
+
132
+ 与 `.part3d` 的区别:
133
+ - `.part3d` — 序列化的 JSON 数据文件,需要运行时解析/重编译
134
+ - `.part3d.js` — 已导出的可执行代码,直接运行即可,无需 `recompile` 参数
135
+ - `.sketch3d.js` — 导出的草图代码,含几何和约束定义
136
+
115
137
  ## add_part() — 组合子文档
116
138
 
117
139
  将 `use()` 返回的文档组合到当前文档:
@@ -146,9 +168,9 @@ node .parapoly/parapoly-runtime/cli/code3d-build.js --no-bin
146
168
  ### 编译流程
147
169
 
148
170
  1. 读取 `package.json` 和 `code3d-workspace-config.json`
149
- 2. 收集所有 `.code3d.js` 和 `.part3d` 文件
150
- 3. **语法检查**:验证 main 函数、return 语句、JS 语法
151
- 4. 分析 `use()` 依赖,构建依赖图,检测循环依赖
171
+ 2. 收集所有 `.code3d.js`、`.part3d.js`、`.sketch3d.js` 和 `.part3d` 文件
172
+ 3. **语法检查**:验证所有 JS 模块的 main 函数、return 语句、JS 语法
173
+ 4. 分析 `use()` 依赖,构建依赖图,检测循环依赖(`.part3d`/`.part3d.js`/`.sketch3d.js` 为叶节点)
152
174
  5. 下载远程文件,读取绝对路径文件
153
175
  6. 拓扑排序,生成单文件 bundle
154
176
  7. 可选输出 `.bin`(gzip 压缩,压缩率约 85-90%)
@@ -634,4 +656,388 @@ let remote = use("https://poly.keepwork.com/parapoly-projects/part3d/机械模
634
656
  ```
635
657
  packages/parapoly-ai-to-3d/code3d-project-workspace-example/ ← 含 .part3d 引用注释示例
636
658
  packages/parapoly-ai-to-3d/code3d-project-workspace-example2/ ← 小方机器人多零件装配
637
- ```
659
+ ```
660
+
661
+ ## Sketch 约束 API(code3d/block3d)
662
+
663
+ ### 概述
664
+
665
+ 在 `start_sketch()` / `end_sketch()` 块内,几何绘制方法返回 `geom_id`(number),可用于添加约束。约束方法在 `PartBlock3dDocument` 上调用。
666
+
667
+ ### PointPos 枚举
668
+
669
+ 约束中引用几何体上的特定位置:
670
+ - `0` — none(整条边/线,用于线段约束如 horizontal)
671
+ - `1` — start(起点)
672
+ - `2` — end(终点)
673
+ - `3` — mid(中点)
674
+
675
+ ### 几何方法(返回 geom_id)
676
+
677
+ ```javascript
678
+ block3d_doc.start_sketch("XY")
679
+ let g1 = block3d_doc.point(x, y)
680
+ let g2 = block3d_doc.line_segment(x1, y1, x2, y2)
681
+ let g3 = block3d_doc.circle(cx, cy, r)
682
+ let g4 = block3d_doc.arc(cx, cy, r, start_angle, end_angle)
683
+ let g5 = block3d_doc.ellipse(cx, cy, rx, ry)
684
+ ```
685
+
686
+ ### 约束方法
687
+
688
+ ```javascript
689
+ // 几何约束
690
+ block3d_doc.constraint_coincident(from_geom_id, from_pos, to_geom_id, to_pos)
691
+ block3d_doc.constraint_horizontal(geom_id) // 线段水平
692
+ block3d_doc.constraint_vertical(geom_id) // 线段垂直
693
+ block3d_doc.constraint_horizontal_points(g1, pos1, g2, pos2) // 两点水平对齐
694
+ block3d_doc.constraint_vertical_points(g1, pos1, g2, pos2) // 两点垂直对齐
695
+ block3d_doc.constraint_parallel(geom_id_1, geom_id_2)
696
+ block3d_doc.constraint_perpendicular(geom_id_1, geom_id_2)
697
+ block3d_doc.constraint_equal(geom_id_1, geom_id_2)
698
+ block3d_doc.constraint_tangent(geom_id_1, geom_id_2)
699
+ block3d_doc.constraint_point_on_object(point_geom_id, point_pos, obj_geom_id)
700
+ block3d_doc.constraint_symmetric(geom_id_1, pos_1, line_geom_id, geom_id_2, pos_2)
701
+
702
+ // 尺寸约束
703
+ block3d_doc.constraint_distance(geom_id, value) // 线段长度
704
+ block3d_doc.constraint_distance_points(g1, pos1, g2, pos2, value) // 两点距离
705
+ block3d_doc.constraint_distance_h(geom_id, value) // 线段水平分量
706
+ block3d_doc.constraint_distance_h_points(g1, pos1, g2, pos2, value)
707
+ block3d_doc.constraint_distance_v(geom_id, value) // 线段垂直分量
708
+ block3d_doc.constraint_distance_v_points(g1, pos1, g2, pos2, value)
709
+ block3d_doc.constraint_distance_point_to_line(point_geom_id, point_pos, line_geom_id, value)
710
+ block3d_doc.constraint_radius(geom_id, value)
711
+ block3d_doc.constraint_diameter(geom_id, value)
712
+ block3d_doc.constraint_angle(geom_id, value) // 单线段与X轴的角度
713
+ block3d_doc.constraint_angle_lines(geom_id_1, geom_id_2, value) // 两线夹角
714
+ ```
715
+
716
+ ### 完整示例
717
+
718
+ ```javascript
719
+ block3d_doc.start_sketch("XY")
720
+
721
+ // 绘制矩形轮廓
722
+ let l1 = block3d_doc.line_segment(0, 0, 10, 0)
723
+ let l2 = block3d_doc.line_segment(10, 0, 10, 5)
724
+ let l3 = block3d_doc.line_segment(10, 5, 0, 5)
725
+ let l4 = block3d_doc.line_segment(0, 5, 0, 0)
726
+
727
+ // 添加约束
728
+ block3d_doc.constraint_horizontal(l1)
729
+ block3d_doc.constraint_horizontal(l3)
730
+ block3d_doc.constraint_vertical(l2)
731
+ block3d_doc.constraint_vertical(l4)
732
+ block3d_doc.constraint_coincident(l1, 2, l2, 1) // l1.end = l2.start
733
+ block3d_doc.constraint_coincident(l2, 2, l3, 1) // l2.end = l3.start
734
+ block3d_doc.constraint_coincident(l3, 2, l4, 1) // l3.end = l4.start
735
+ block3d_doc.constraint_coincident(l4, 2, l1, 1) // l4.end = l1.start
736
+ block3d_doc.constraint_distance(l1, 10) // 底边长=10
737
+ block3d_doc.constraint_distance(l2, 5) // 右边高=5
738
+
739
+ block3d_doc.end_sketch()
740
+ block3d_doc.extrude(3) // 拉伸 3mm
741
+ ```
742
+
743
+ ### 注意事项
744
+
745
+ - 约束只在 `start_sketch()` / `end_sketch()` 块内有效
746
+ - `geom_id` 从 0 开始递增,按绘制顺序分配
747
+ - PointPos `0`(none)用于引用整条线/圆,不指定端点
748
+ - 圆和椭圆通常用 PointPos `3`(mid)引用圆心
749
+
750
+ ## Part3D → code3d 导出经验(Part3dCodeExporter 维护指南)
751
+
752
+ 源文件:`packages/parapoly-editor/src/io/Part3dCodeExporter.ts`
753
+
754
+ 当特征节点(`FeatureNode*`)的 `run()` 逻辑变更时,需要同步更新 Exporter 中对应特征的导出代码。
755
+
756
+ ### 总体架构
757
+
758
+ 导出代码结构:
759
+ ```javascript
760
+ let main = function() {
761
+ let block3d_doc = new PartBlock3dDocument();
762
+ // helpers
763
+ let __id_to_name = {}; // feature_id → 节点名
764
+ let __saved_shapes = {}; // 被 Boolean 删除前保存的 shape
765
+ let __record_name = function(feature_id) { ... };
766
+ let __select_feature = function(feature_id) { ... };
767
+
768
+ // Feature summary(注释段)
769
+ // Executable export(可执行段)
770
+
771
+ return block3d_doc;
772
+ };
773
+ ```
774
+
775
+ ### 导出模式分类
776
+
777
+ #### 模式 A:基础体(直接 API 调用)
778
+
779
+ 适用于:Box, Cylinder, Sphere, Cone, Torus, Prism, Ellipsoid, Wedge, Trapezoid, Step
780
+
781
+ ```javascript
782
+ block3d_doc.box(op, x, y, z, color);
783
+ block3d_doc.translate(px, py, pz); // 来自 transform_component
784
+ block3d_doc.rotate_by_quaternion(rx, ry, rz, rw);
785
+ __record_name(feature_id);
786
+ ```
787
+
788
+ 颜色值必须带 `#` 前缀(如 `"#ffffff"`)。
789
+
790
+ #### 模式 B:IIFE + ParaPolyShapeMaker(操作型特征)
791
+
792
+ 适用于:Extrude, Revolve, Sweep, Helix, Fillet, Chamfer, Shell, Draft, Mirror, Boolean, Transform
793
+
794
+ 统一模式:
795
+ ```javascript
796
+ (function() {
797
+ // 1. 查找目标节点
798
+ let target_name = __id_to_name[target_id];
799
+ let node = root.getChildByName(target_name, true);
800
+ // 2. 获取/生成形体
801
+ // 3. 调用 ParaPolyShapeMaker.xxx()
802
+ // 4. 创建结果节点
803
+ let result_node = block3d_doc.push_node(op, name, color, false);
804
+ result_node.set_topo_shape(result_shape);
805
+ block3d_doc.pop_node();
806
+ // 5. 归档原始节点
807
+ node.set_archived(true);
808
+ node.set_op_feature_node_id(feature_id);
809
+ __record_name(feature_id);
810
+ })();
811
+ ```
812
+
813
+ ### 各特征导出规则(对应 run() 逻辑)
814
+
815
+ #### FeatureNodeExtrude
816
+
817
+ **运行时逻辑**(`FeatureNodeExtrude.run()`):
818
+ - 目标为 SkContainerNode → `to_wires_shape()` → `extrude_shape(wires, value, 0, 0, 1, solid)` → `transform_native_shape(result, sk_node)`
819
+ - 目标为 FeatureNodeBase → `get_topo_shape()` → `extrude(shape, value, face_index, dx, dy, dz, solid, true)`
820
+
821
+ **导出时**:通过 `sketch_nodes.has(target_id)` 判断走哪条路径。
822
+
823
+ **Sketch 路径关键调用序列**:
824
+ ```javascript
825
+ node.set_archived(false); node.set_op_feature_node_id(null);
826
+ let wires_shape = node.to_wires_shape();
827
+ let extrude_shape = parapoly_engine.ParaPolyShapeMaker.extrude_shape(wires_shape, value, 0, 0, 1, solid);
828
+ extrude_shape = node.constructor.transform_native_shape(extrude_shape, node); // 变换到世界坐标
829
+ node.set_archived(true); node.set_op_feature_node_id(feature_id); // 归档
830
+ ```
831
+
832
+ **Face 路径关键调用序列**:
833
+ ```javascript
834
+ let extrude_shape = parapoly_engine.ParaPolyShapeMaker.extrude(topo_shape, value, face_index, dx, dy, dz, solid, true);
835
+ ```
836
+
837
+ #### FeatureNodeRevolve
838
+
839
+ **运行时逻辑**:仅支持 SkContainerNode。
840
+ - `to_wires_shape()` → `transform_native_shape(wires, sk_node)` → `revolve_shape(wires, value, cx, cy, cz, dx, dy, dz, solid)`
841
+
842
+ **注意**:Revolve 先变换 wires 再旋转(与 Extrude 不同,Extrude 是先拉伸再变换)。
843
+
844
+ ```javascript
845
+ let wires_shape = node.to_wires_shape();
846
+ wires_shape = node.constructor.transform_native_shape(wires_shape, node);
847
+ let revolve_shape = parapoly_engine.ParaPolyShapeMaker.revolve_shape(wires_shape, value, cx, cy, cz, dx, dy, dz, solid);
848
+ ```
849
+
850
+ #### FeatureNodeSweep
851
+
852
+ **运行时逻辑**:需要 profile + path 两个 SkContainerNode。
853
+ - 分别 `to_wires_shape()` → 分别 `transform_native_shape` → `sweep_shape(path_wires, profile_wires, solid)`
854
+
855
+ ```javascript
856
+ let profile_wires = profile_node.to_wires_shape();
857
+ let path_wires = path_node.to_wires_shape();
858
+ profile_wires = profile_node.constructor.transform_native_shape(profile_wires, profile_node);
859
+ path_wires = path_node.constructor.transform_native_shape(path_wires, path_node);
860
+ let sweep_shape = parapoly_engine.ParaPolyShapeMaker.sweep_shape(path_wires, profile_wires, solid);
861
+ ```
862
+
863
+ #### FeatureNodeHelix
864
+
865
+ **运行时逻辑**:目标为 SkContainerNode。
866
+ - `to_wires_shape()` → `helix_shape(wires, axis, mode, pitch, height, angle, left_hand, reversed, solid)` → `transform_native_shape(result, sk_node)`
867
+
868
+ **注意**:Helix 先做螺旋再变换(与 Extrude 相同,与 Revolve 不同)。
869
+
870
+ ```javascript
871
+ let wires_shape = node.to_wires_shape();
872
+ let helix_shape = parapoly_engine.ParaPolyShapeMaker.helix_shape(wires_shape, axis, mode, pitch, height, angle, left_hand, reversed, solid);
873
+ helix_shape = node.constructor.transform_native_shape(helix_shape, node);
874
+ ```
875
+
876
+ #### FeatureNodeFillet / FeatureNodeChamfer
877
+
878
+ **运行时逻辑**:
879
+ - 获取目标节点 shape + edge 索引数组
880
+ - 构造 `ParaPolyIntArray` 填入 edge 索引
881
+ - 调用 `fillet(shape, value, edge_arr)` 或 `chamfer(shape, value, edge_arr)`
882
+ - 归档目标节点
883
+
884
+ ```javascript
885
+ node.set_archived(false); node.set_op_feature_node_id(null);
886
+ let topo_shape = node.get_topo_shape();
887
+ let edge_arr = new parapoly_engine.ParaPolyIntArray();
888
+ edge_arr.pushValue(edge_index_0);
889
+ edge_arr.pushValue(edge_index_1);
890
+ let result_shape = parapoly_engine.ParaPolyShapeMaker.fillet(topo_shape, value, edge_arr);
891
+ // 或 chamfer(topo_shape, value, edge_arr)
892
+ node.set_archived(true); node.set_op_feature_node_id(feature_id);
893
+ ```
894
+
895
+ #### FeatureNodeShell
896
+
897
+ **运行时逻辑**:
898
+ - 获取目标节点 shape + face_index
899
+ - 调用 `shell_shape(shape, value, face_index, inward)`
900
+
901
+ ```javascript
902
+ let result_shape = parapoly_engine.ParaPolyShapeMaker.shell_shape(topo_shape, value, face_index, inward);
903
+ ```
904
+
905
+ #### FeatureNodeDraft
906
+
907
+ **运行时逻辑**:
908
+ - 获取目标节点 shape + face 索引数组
909
+ - 构造 `ParaPolyIntArray` 填入 face 索引
910
+ - 调用 `draft_shape(shape, value, cx, cy, cz, dx, dy, dz, face_arr, reverse)`
911
+
912
+ ```javascript
913
+ let face_arr = new parapoly_engine.ParaPolyIntArray();
914
+ face_arr.pushValue(face_index);
915
+ let result_shape = parapoly_engine.ParaPolyShapeMaker.draft_shape(topo_shape, value, cx, cy, cz, dx, dy, dz, face_arr, reverse);
916
+ ```
917
+
918
+ #### FeatureNodeMirror
919
+
920
+ **运行时逻辑**:
921
+ - 获取目标 shape(可能来自 `__saved_shapes`)
922
+ - 调用 `mirror_shape(shape, cx, cy, cz, dx, dy, dz)`
923
+
924
+ #### FeatureNodeBoolean
925
+
926
+ **运行时逻辑**:
927
+ - 获取多个 target + tool 节点的 shape(`.clone()`)
928
+ - 合并 targets/tools(多个时先 `fuse`)
929
+ - 最终操作:`fuse` / `cut` / `common`
930
+ - 保存原始 shape 到 `__saved_shapes` → 删除原始节点
931
+
932
+ #### FeatureNodeTransform
933
+
934
+ **运行时逻辑**:
935
+ - 获取目标 shape `.clone()`
936
+ - 按顺序:`scale(sx, sy, sz)` → `rotate(rx, ry, rz, rw)` → `translate(px, py, pz)`
937
+ - 直接 `set_topo_shape()` 覆盖原始
938
+
939
+ ### 修改 Exporter 的流程
940
+
941
+ 当某个 `FeatureNode*.run()` 逻辑变更时:
942
+
943
+ 1. **定位源码**:`packages/parapoly-runtime/src/part/feature_node/op/FeatureNode*.ts` 的 `run()` 方法
944
+ 2. **识别关键调用**:找到 `ParaPolyShapeMaker.xxx()` 调用及其参数来源
945
+ 3. **注意变换顺序**:`transform_native_shape` 在操作前还是操作后(各特征不同)
946
+ 4. **更新导出代码**:在 `Part3dCodeExporter.ts` 的 `export_executable_calls()` 中找到对应的 `class_name.indexOf("FeatureNode*")` 分支
947
+ 5. **保持 IIFE 模式**:所有操作型特征使用 `(function() { ... })();` 包裹
948
+ 6. **保持归档逻辑**:`set_archived(false) → 操作 → set_archived(true)` 配对
949
+ 7. **保持错误日志格式**:`[part3d.js #序号 操作类型 特征ID前8位] 消息`
950
+
951
+ ### Sketch 复用与重发射
952
+
953
+ 当同一个 Sketch 被多次引用(如被两个 Extrude 使用)时,第二次引用前必须**重新发射**整个 Sketch 代码。
954
+
955
+ Exporter 使用 `consumed_sketches: Set<string>` 追踪。仅 Extrude/Revolve 会消耗 sketch。
956
+
957
+ ### SketchCodeExporter 架构
958
+
959
+ 源文件:`packages/parapoly-editor/src/io/SketchCodeExporter.ts`
960
+
961
+ 将 SkContainerNode 导出为独立 `.code3d.js`:
962
+
963
+ ```javascript
964
+ let main = function() {
965
+ let block3d_doc = new PartBlock3dDocument();
966
+ block3d_doc.start_sketch("sketch", "z");
967
+ let g1 = block3d_doc.line_segment(...); // 返回 geom_id
968
+ block3d_doc.constraint_horizontal(g1);
969
+ block3d_doc.end_sketch();
970
+ return block3d_doc;
971
+ };
972
+ ```
973
+
974
+ #### 几何类型映射
975
+
976
+ | 节点类 | 导出调用 |
977
+ |---|---|
978
+ | SkGeomPointNode | `block3d_doc.point(x, y, z)` |
979
+ | SkGeomLineNode | `block3d_doc.line_segment(x1,y1,z1, x2,y2,z2)` |
980
+ | SkGeomCircleNode | `block3d_doc.circle(cx, cy, cz, r)` |
981
+ | SkGeomEllipseNode | `block3d_doc.ellipse(cx, cy, cz, major_r, minor_r)` |
982
+ | SkGeomArcNode | `block3d_doc.arc(cx, cy, cz, r, start_deg, end_deg)` |
983
+ | SkGeomBSplineNode | `start_bspline(closed)` + `add_pole(x,y,z)` × N + `end_bspline()` |
984
+
985
+ **注意**:Arc 角度从弧度转为角度(`× 180 / Math.PI`)。
986
+
987
+ #### 约束导出规则
988
+
989
+ - 跳过 `InternalAlignment` 约束和 `is_internal` 几何体
990
+ - `geom_id` 映射为变量名 `g{id}`
991
+ - BSpline 不返回 geom_id,不参与约束引用
992
+
993
+ ### 数值精度
994
+
995
+ `n()` 函数直接用 `String(num)` 输出完整浮点精度,会产生 `9.999999999999998` 等噪声。
996
+
997
+ AI 生成 code3d 代码时**应使用简洁数值**(`10`、`0.3`),不要模仿 Exporter 的浮点噪声。
998
+
999
+ ### 颜色格式
1000
+
1001
+ 导出颜色必须带 `#` 前缀:`"#ffffff"`。`get_color()` 可能返回不带 `#` 的值,Exporter 需要手动补全。
1002
+
1003
+ ### Sketch 可见度保持
1004
+
1005
+ 导出的代码必须保持 Sketch 在原始 part3d 中的可见状态。Sketch 有两种独立的隐藏机制:
1006
+
1007
+ 1. **`archived`(系统隐藏)**— Sketch 被 Extrude/Revolve 等操作消费后,系统自动设置 `set_archived(true)`
1008
+ 2. **`visible`(用户隐藏)**— 用户通过编辑器的眼睛图标手动切换 `set_visible(false)`
1009
+
1010
+ 渲染时,`archived === true` 的节点会被跳过(`SpriteToNativeNode` 和 `Text2CADFacade.build()` 中均检查此标志)。
1011
+
1012
+ **Exporter 中的处理逻辑**(`SkContainerNode` 分支):
1013
+
1014
+ ```typescript
1015
+ // 1. 导出 sketch 几何
1016
+ lines.push(...Part3dCodeExporter.export_sketch_lines(node));
1017
+ lines.push(`__record_name(${JSON.stringify(feature_id)});`);
1018
+
1019
+ // 2. 保持可见度
1020
+ let is_archived = node.get_archived ? node.get_archived() : false;
1021
+ let op_id = node.get_op_feature_node_id ? node.get_op_feature_node_id() : null;
1022
+ let is_visible = node.get_visible ? node.get_visible() : true;
1023
+ if (is_archived || op_id) {
1024
+ lines.push(`block3d_doc.selected_node.set_archived(true);`);
1025
+ }
1026
+ if (!is_visible) {
1027
+ lines.push(`block3d_doc.selected_node.set_visible(false);`);
1028
+ }
1029
+ ```
1030
+
1031
+ **判断依据**:
1032
+
1033
+ | 属性 | 含义 | 可靠性 |
1034
+ |---|---|---|
1035
+ | `get_archived()` | 当前是否被归档 | 依赖 `update_feature_node_archived()` 是否被调用过 |
1036
+ | `get_op_feature_node_id()` | 消费此 sketch 的操作节点 ID | 持久化属性,最可靠 |
1037
+ | `get_visible()` | 用户是否手动隐藏 | 持久化属性,可靠 |
1038
+
1039
+ **为什么要同时检查 `op_id`**:`get_archived()` 是由 `update_feature_node_archived()` 在编辑器 build 流程中计算设置的,依赖 `op_feature_node.getMemoryVisible()` 状态。如果导出时这个计算没有及时执行,`archived` 可能为 `false`。而 `op_feature_node_id` 是 sketch 被消费时直接持久化的属性,不依赖外部调用。
1040
+
1041
+ **与 Extrude/Revolve IIFE 的配合**:Extrude/Revolve IIFE 中会先 `node.set_archived(false)` 取消归档(用于 `to_wires_shape()`),操作完成后再 `node.set_archived(true)` 重新归档。即使在 sketch 创建时已设置 `archived=true`,操作 IIFE 的归档流程仍然正确执行。
1042
+
1043
+ **`end_sketch()` 后的 `selected_node`**:`PartBlock3dDocument.end_sketch()` 将 `this.selected_node = node`(sketch 节点),因此 `block3d_doc.selected_node.set_archived(true)` 可以正确定位到刚创建的 sketch。