@faicad/faijs-fcstd 0.17.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 (87) hide show
  1. package/LICENSE +29 -0
  2. package/NOTICE +20 -0
  3. package/dist/attachment.d.ts +72 -0
  4. package/dist/attachment.d.ts.map +1 -0
  5. package/dist/attachment.js +93 -0
  6. package/dist/attachment.js.map +1 -0
  7. package/dist/bspline.d.ts +58 -0
  8. package/dist/bspline.d.ts.map +1 -0
  9. package/dist/bspline.js +93 -0
  10. package/dist/bspline.js.map +1 -0
  11. package/dist/build-fai-zip.d.ts +40 -0
  12. package/dist/build-fai-zip.d.ts.map +1 -0
  13. package/dist/build-fai-zip.js +111 -0
  14. package/dist/build-fai-zip.js.map +1 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +37 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/codegen.d.ts +83 -0
  20. package/dist/codegen.d.ts.map +1 -0
  21. package/dist/codegen.js +618 -0
  22. package/dist/codegen.js.map +1 -0
  23. package/dist/container.d.ts +75 -0
  24. package/dist/container.d.ts.map +1 -0
  25. package/dist/container.js +36 -0
  26. package/dist/container.js.map +1 -0
  27. package/dist/contour.d.ts +43 -0
  28. package/dist/contour.d.ts.map +1 -0
  29. package/dist/contour.js +132 -0
  30. package/dist/contour.js.map +1 -0
  31. package/dist/convert.d.ts +69 -0
  32. package/dist/convert.d.ts.map +1 -0
  33. package/dist/convert.js +303 -0
  34. package/dist/convert.js.map +1 -0
  35. package/dist/document.d.ts +48 -0
  36. package/dist/document.d.ts.map +1 -0
  37. package/dist/document.js +140 -0
  38. package/dist/document.js.map +1 -0
  39. package/dist/expressions.d.ts +79 -0
  40. package/dist/expressions.d.ts.map +1 -0
  41. package/dist/expressions.js +162 -0
  42. package/dist/expressions.js.map +1 -0
  43. package/dist/external-geo.d.ts +49 -0
  44. package/dist/external-geo.d.ts.map +1 -0
  45. package/dist/external-geo.js +140 -0
  46. package/dist/external-geo.js.map +1 -0
  47. package/dist/feature-translate.d.ts +193 -0
  48. package/dist/feature-translate.d.ts.map +1 -0
  49. package/dist/feature-translate.js +1441 -0
  50. package/dist/feature-translate.js.map +1 -0
  51. package/dist/fillet-edges.d.ts +26 -0
  52. package/dist/fillet-edges.d.ts.map +1 -0
  53. package/dist/fillet-edges.js +36 -0
  54. package/dist/fillet-edges.js.map +1 -0
  55. package/dist/index.d.ts +23 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +19 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/placement.d.ts +93 -0
  60. package/dist/placement.d.ts.map +1 -0
  61. package/dist/placement.js +117 -0
  62. package/dist/placement.js.map +1 -0
  63. package/dist/planegcs-backend.d.ts +34 -0
  64. package/dist/planegcs-backend.d.ts.map +1 -0
  65. package/dist/planegcs-backend.js +665 -0
  66. package/dist/planegcs-backend.js.map +1 -0
  67. package/dist/sketch-parse.d.ts +199 -0
  68. package/dist/sketch-parse.d.ts.map +1 -0
  69. package/dist/sketch-parse.js +263 -0
  70. package/dist/sketch-parse.js.map +1 -0
  71. package/dist/sketch-solver.d.ts +63 -0
  72. package/dist/sketch-solver.d.ts.map +1 -0
  73. package/dist/sketch-solver.js +28 -0
  74. package/dist/sketch-solver.js.map +1 -0
  75. package/dist/sketch-verify.d.ts +53 -0
  76. package/dist/sketch-verify.d.ts.map +1 -0
  77. package/dist/sketch-verify.js +71 -0
  78. package/dist/sketch-verify.js.map +1 -0
  79. package/dist/structural-types.d.ts +96 -0
  80. package/dist/structural-types.d.ts.map +1 -0
  81. package/dist/structural-types.js +155 -0
  82. package/dist/structural-types.js.map +1 -0
  83. package/dist/unpack.d.ts +44 -0
  84. package/dist/unpack.d.ts.map +1 -0
  85. package/dist/unpack.js +64 -0
  86. package/dist/unpack.js.map +1 -0
  87. package/package.json +65 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * M6.2 — expression downgrade: <ExpressionEngine> → concrete values.
3
+ *
4
+ * Format (verified on TwoLengthsPadWithExpression.FCStd):
5
+ * <ExpressionEngine count="N">
6
+ * <Expression path="Length" expression="10 mm" />
7
+ * </ExpressionEngine>
8
+ *
9
+ * Scope: constant expressions only — number literals with optional unit
10
+ * (mm/m/cm/in/deg/rad). Anything else (object references like Sketch.Constraints[3],
11
+ * arithmetic with identifiers, functions) is NOT evaluated: the consumer must
12
+ * treat the property as unknown and degrade (no heuristic fallback, plan §12).
13
+ */
14
+ import type { FcstdProperty } from './document.js';
15
+ /** Evaluated expression result in mm (angles keep their own unit): a number, or undefined when unsupported. */
16
+ export type ExprValue = number | undefined;
17
+ /**
18
+ * Evaluate a constant expression. Returns undefined when the expression is
19
+ * not a bare constant (reference/arithmetic/function) — explicit unsupported,
20
+ * no guessing.
21
+ *
22
+ * @param expr raw expression string (e.g. "10 mm")
23
+ * @returns the value in mm, or undefined when not a constant expression
24
+ */
25
+ export declare function evalConstantExpression(expr: string): ExprValue;
26
+ import type { FcstdObject } from './document.js';
27
+ /**
28
+ * 三跳解析:`<<Label>>.Alias` / `Object.Alias` → 数据源对象 → 别名单元格 → 去掉
29
+ * 前导 `=` 后交给 evalConstantExpression。同表地址(`=B2*2` 里的 `B2`)再跳一次。
30
+ *
31
+ * @param docObjects 全文档对象(按 Label 或 name 匹配数据源)
32
+ * @param label 数据源标识(`<<Label>>` 内的 Label,或裸对象名)
33
+ * @param alias 单元格别名
34
+ * @returns 单元格值(mm),不可解析 → undefined
35
+ */
36
+ export declare function spreadsheetAliasValue(docObjects: readonly FcstdObject[], label: string, alias: string): ExprValue;
37
+ /**
38
+ * 带文档上下文的表达式求值:引用(`<<L>>.A` / `L.A` / 同表地址 `B2`)替换为
39
+ * 数值后,求值仅含常数与 + - * / ( ) 的算术。任何残留标识符 / 函数 → undefined
40
+ * (no heuristic fallback)。返回值单位跟随单元格(mm 语境,角度单元格调用方解释)。
41
+ *
42
+ * @param expr 待求值表达式原文。
43
+ * @param docObjects 文档对象表,用于解析引用(`<<L>>.A` / `L.A`)。
44
+ * @param self 调用方对象(Spreadsheet::Sheet 时启用同表地址 `B2` 解析)。可选。
45
+ * @returns 求值结果数值;含残留标识符 / 无法解析引用时返回 undefined。
46
+ */
47
+ /**
48
+ * Evaluate a FreeCAD expression against the document objects.
49
+ * @param expr - the expression text (without the leading `=`).
50
+ * @param docObjects - the document objects visible to the expression.
51
+ * @param self - the object the expression is evaluated on, if any.
52
+ * @returns the evaluated expression value.
53
+ */
54
+ export declare function evalWithDoc(expr: string, docObjects: readonly FcstdObject[], self?: FcstdObject): ExprValue;
55
+ /** One <Expression path=... expression=...> binding with its optional constant value. */
56
+ export interface ExpressionBinding {
57
+ /** property path the expression drives (e.g. "Length") */
58
+ path: string;
59
+ /** raw expression string */
60
+ expression: string;
61
+ /** evaluated constant; undefined when not a constant expression */
62
+ value?: number;
63
+ }
64
+ /**
65
+ * Extract expression bindings from a parsed ExpressionEngine property.
66
+ * `bindable` = every expression evaluated to a constant (then the property
67
+ * values can be overridden); otherwise the engine is only partially readable.
68
+ *
69
+ * @param prop the parsed <ExpressionEngine> property (undefined → no bindings)
70
+ * @returns one binding per <Expression> child, values evaluated
71
+ */
72
+ export declare function parseExpressionEngine(prop: FcstdProperty | undefined): ExpressionBinding[];
73
+ /**
74
+ * True when all bindings are constants → safe to override property values.
75
+ * @param bindings bindings extracted by parseExpressionEngine
76
+ * @returns true when there is at least one binding and every value is defined
77
+ */
78
+ export declare function allConstant(bindings: ExpressionBinding[]): boolean;
79
+ //# sourceMappingURL=expressions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expressions.d.ts","sourceRoot":"","sources":["../src/expressions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,+GAA+G;AAC/G,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAa3C;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAY9D;AAUD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,SAAS,WAAW,EAAE,EAClC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,SAAS,CAiBX;AAQD;;;;;;;;;GASG;AACH;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,WAAW,EAAE,EAClC,IAAI,CAAC,EAAE,WAAW,GACjB,SAAS,CAuBX;AA6BD,yFAAyF;AACzF,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,GAAG,SAAS,GAC9B,iBAAiB,EAAE,CAWrB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAElE"}
@@ -0,0 +1,162 @@
1
+ const UNIT_TO_MM = {
2
+ mm: 1, millimeter: 1,
3
+ cm: 10, centimeter: 10,
4
+ m: 1000, meter: 1000,
5
+ in: 25.4, inch: 25.4, '"': 25.4,
6
+ ft: 304.8, foot: 304.8,
7
+ // angles: value kept in the expression's own unit; callers interpret
8
+ deg: 1, degree: 1, '°': 1,
9
+ rad: 1, radian: 1,
10
+ };
11
+ /**
12
+ * Evaluate a constant expression. Returns undefined when the expression is
13
+ * not a bare constant (reference/arithmetic/function) — explicit unsupported,
14
+ * no guessing.
15
+ *
16
+ * @param expr raw expression string (e.g. "10 mm")
17
+ * @returns the value in mm, or undefined when not a constant expression
18
+ */
19
+ export function evalConstantExpression(expr) {
20
+ const s = expr.trim();
21
+ // number [unit]
22
+ const m = /^([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\s*([A-Za-z°"]*)$/.exec(s);
23
+ if (!m)
24
+ return undefined;
25
+ const value = Number(m[1]);
26
+ if (!Number.isFinite(value))
27
+ return undefined;
28
+ const unit = m[2];
29
+ if (unit === '')
30
+ return value;
31
+ const factor = UNIT_TO_MM[unit.toLowerCase()];
32
+ if (factor === undefined)
33
+ return undefined; // unknown unit → unsupported
34
+ return value * factor;
35
+ }
36
+ /**
37
+ * 三跳解析:`<<Label>>.Alias` / `Object.Alias` → 数据源对象 → 别名单元格 → 去掉
38
+ * 前导 `=` 后交给 evalConstantExpression。同表地址(`=B2*2` 里的 `B2`)再跳一次。
39
+ *
40
+ * @param docObjects 全文档对象(按 Label 或 name 匹配数据源)
41
+ * @param label 数据源标识(`<<Label>>` 内的 Label,或裸对象名)
42
+ * @param alias 单元格别名
43
+ * @returns 单元格值(mm),不可解析 → undefined
44
+ */
45
+ export function spreadsheetAliasValue(docObjects, label, alias) {
46
+ const src = docObjects.find((o) => o.type === 'Spreadsheet::Sheet' &&
47
+ (objectLabel(o) === label || o.name === label));
48
+ if (!src)
49
+ return undefined;
50
+ // cells 属性(Spreadsheet::PropertySheet)的 <Cell address alias content/> 子元素
51
+ const cells = src.properties.get('cells') ?? [...src.properties.values()].find((p) => p.children.some((c) => c.tagName === 'Cell' || c.children.some((g) => g.tagName === 'Cell')));
52
+ if (!cells)
53
+ return undefined;
54
+ const cell = [...cells.children].flatMap((c) => (c.tagName === 'Cell' ? [c] : c.children))
55
+ .find((c) => c.tagName === 'Cell' && c.attributes['alias'] === alias);
56
+ if (!cell)
57
+ return undefined;
58
+ const content = cell.attributes['content'] ?? '';
59
+ // 去掉前导 =(同表地址/算术再走一次带上下文的求值,一跳深度足够语料头部)
60
+ return evalWithDoc(content.startsWith('=') ? content.slice(1) : content, docObjects, src);
61
+ }
62
+ /** 对象的 Label 属性(FreeCAD 引用 `<<Label>>` 用的是它,不是 name)。 */
63
+ function objectLabel(o) {
64
+ const el = o.properties.get('Label')?.children[0];
65
+ return el?.attributes['value'] ?? (el?.valueText || undefined);
66
+ }
67
+ /**
68
+ * 带文档上下文的表达式求值:引用(`<<L>>.A` / `L.A` / 同表地址 `B2`)替换为
69
+ * 数值后,求值仅含常数与 + - * / ( ) 的算术。任何残留标识符 / 函数 → undefined
70
+ * (no heuristic fallback)。返回值单位跟随单元格(mm 语境,角度单元格调用方解释)。
71
+ *
72
+ * @param expr 待求值表达式原文。
73
+ * @param docObjects 文档对象表,用于解析引用(`<<L>>.A` / `L.A`)。
74
+ * @param self 调用方对象(Spreadsheet::Sheet 时启用同表地址 `B2` 解析)。可选。
75
+ * @returns 求值结果数值;含残留标识符 / 无法解析引用时返回 undefined。
76
+ */
77
+ /**
78
+ * Evaluate a FreeCAD expression against the document objects.
79
+ * @param expr - the expression text (without the leading `=`).
80
+ * @param docObjects - the document objects visible to the expression.
81
+ * @param self - the object the expression is evaluated on, if any.
82
+ * @returns the evaluated expression value.
83
+ */
84
+ export function evalWithDoc(expr, docObjects, self) {
85
+ let s = expr.trim();
86
+ // 引用替换:<<Label>>.Alias | Label.Alias | Object.Alias(标识符.标识符)
87
+ s = s.replace(/<<([^>]+)>>\.([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)/g, (whole, brLabel, brAlias, plLabel, plAlias) => {
88
+ const label = brLabel ?? plLabel;
89
+ const alias = brAlias ?? plAlias;
90
+ const v = spreadsheetAliasValue(docObjects, label, alias);
91
+ return v === undefined ? whole : `(${v})`;
92
+ });
93
+ // 同表地址(self 表内 address→alias 值,如 `B2` / `B2*2`):仅当 self 是表
94
+ if (self?.type === 'Spreadsheet::Sheet') {
95
+ s = s.replace(/\b([A-Z]+[0-9]+)\b/g, (whole, addr) => {
96
+ const v = spreadsheetAddressValue(self, addr, docObjects);
97
+ return v === undefined ? whole : `(${v})`;
98
+ });
99
+ }
100
+ // 单元格值自带单位(=5.9mm)——先按「常数+单位」求值(evalConstantExpression
101
+ // 的既有口径),再进纯算术;单位记号在算术前剥离(返回值跟随 mm 语境)。
102
+ const constant = evalConstantExpression(s);
103
+ if (constant !== undefined)
104
+ return constant;
105
+ s = s.replace(/\b(mm|millimeter|cm|centimeter|m|meter|in|inch|"|ft|foot|deg|degree|°|rad|radian)\b/g, '');
106
+ return evalArithmetic(s);
107
+ }
108
+ /** 同表地址取值:address → 该单元格 content(递归经 evalWithDoc,一跳深度)。 */
109
+ function spreadsheetAddressValue(sheet, address, docObjects) {
110
+ const cells = sheet.properties.get('cells');
111
+ if (!cells)
112
+ return undefined;
113
+ const cell = [...cells.children].flatMap((c) => (c.tagName === 'Cell' ? [c] : c.children))
114
+ .find((c) => c.tagName === 'Cell' && c.attributes['address'] === address);
115
+ if (!cell)
116
+ return undefined;
117
+ const content = cell.attributes['content'] ?? '';
118
+ return evalWithDoc(content.startsWith('=') ? content.slice(1) : content, docObjects, sheet);
119
+ }
120
+ /** 仅含数字与 + - * / ( ) 空格的算术求值;任何其它字符 → undefined。 */
121
+ function evalArithmetic(s) {
122
+ if (!/^[-+*/(). 0-9eE]*$/.test(s))
123
+ return undefined;
124
+ try {
125
+ const v = Function(`"use strict"; return (${s});`)();
126
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ }
132
+ /**
133
+ * Extract expression bindings from a parsed ExpressionEngine property.
134
+ * `bindable` = every expression evaluated to a constant (then the property
135
+ * values can be overridden); otherwise the engine is only partially readable.
136
+ *
137
+ * @param prop the parsed <ExpressionEngine> property (undefined → no bindings)
138
+ * @returns one binding per <Expression> child, values evaluated
139
+ */
140
+ export function parseExpressionEngine(prop) {
141
+ const engineEl = prop?.children[0];
142
+ if (!engineEl)
143
+ return [];
144
+ const out = [];
145
+ for (const el of engineEl.children) {
146
+ if (el.tagName !== 'Expression')
147
+ continue;
148
+ const path = el.attributes['path'] ?? '';
149
+ const expression = el.attributes['expression'] ?? '';
150
+ out.push({ path, expression, value: evalConstantExpression(expression) });
151
+ }
152
+ return out;
153
+ }
154
+ /**
155
+ * True when all bindings are constants → safe to override property values.
156
+ * @param bindings bindings extracted by parseExpressionEngine
157
+ * @returns true when there is at least one binding and every value is defined
158
+ */
159
+ export function allConstant(bindings) {
160
+ return bindings.length > 0 && bindings.every((b) => b.value !== undefined);
161
+ }
162
+ //# sourceMappingURL=expressions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expressions.js","sourceRoot":"","sources":["../src/expressions.ts"],"names":[],"mappings":"AAkBA,MAAM,UAAU,GAA2B;IACzC,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC;IACpB,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE;IACtB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACpB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI;IAC/B,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK;IACtB,qEAAqE;IACrE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC;IACzB,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACtB,gBAAgB;IAChB,MAAM,CAAC,GAAG,+DAA+D,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClF,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACnB,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC,CAAC,6BAA6B;IACzE,OAAO,KAAK,GAAG,MAAM,CAAC;AACxB,CAAC;AAYD;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAkC,EAClC,KAAa,EACb,KAAa;IAEb,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CACzB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,oBAAoB;QACpC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,CACjD,CAAC;IACF,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,0EAA0E;IAC1E,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CACnF,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,CAC7F,CAAC;IACF,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;SACvF,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,CAAC;IACxE,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IACjD,wCAAwC;IACxC,OAAO,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;AAC5F,CAAC;AAED,yDAAyD;AACzD,SAAS,WAAW,CAAC,CAAc;IACjC,MAAM,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAClD,OAAO,EAAE,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,IAAI,SAAS,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;;GASG;AACH;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,UAAkC,EAClC,IAAkB;IAElB,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,6DAA6D;IAC7D,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,2FAA2F,EACvG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;QAC5C,MAAM,KAAK,GAAG,OAAO,IAAI,OAAO,CAAC;QACjC,MAAM,KAAK,GAAG,OAAO,IAAI,OAAO,CAAC;QACjC,MAAM,CAAC,GAAG,qBAAqB,CAAC,UAAU,EAAE,KAAM,EAAE,KAAM,CAAC,CAAC;QAC5D,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;IAC5C,CAAC,CAAC,CAAC;IACL,2DAA2D;IAC3D,IAAI,IAAI,EAAE,IAAI,KAAK,oBAAoB,EAAE,CAAC;QACxC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;YAC3D,MAAM,CAAC,GAAG,uBAAuB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;YAC1D,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC;IACD,uDAAuD;IACvD,wCAAwC;IACxC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAC3C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC5C,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,sFAAsF,EAAE,EAAE,CAAC,CAAC;IAC1G,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED,2DAA2D;AAC3D,SAAS,uBAAuB,CAC9B,KAAkB,EAClB,OAAe,EACf,UAAkC;IAElC,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;SACvF,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,CAAC;IAC5E,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;AAC9F,CAAC;AAED,oDAAoD;AACpD,SAAS,cAAc,CAAC,CAAS;IAC/B,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACpD,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,QAAQ,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAa,CAAC;QAChE,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAaD;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAA+B;IAE/B,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzB,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC,OAAO,KAAK,YAAY;YAAE,SAAS;QAC1C,MAAM,IAAI,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QACrD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,sBAAsB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,QAA6B;IACvD,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["/**\n * M6.2 — expression downgrade: <ExpressionEngine> → concrete values.\n *\n * Format (verified on TwoLengthsPadWithExpression.FCStd):\n * <ExpressionEngine count=\"N\">\n * <Expression path=\"Length\" expression=\"10 mm\" />\n * </ExpressionEngine>\n *\n * Scope: constant expressions only — number literals with optional unit\n * (mm/m/cm/in/deg/rad). Anything else (object references like Sketch.Constraints[3],\n * arithmetic with identifiers, functions) is NOT evaluated: the consumer must\n * treat the property as unknown and degrade (no heuristic fallback, plan §12).\n */\nimport type { FcstdProperty } from './document.js';\n\n/** Evaluated expression result in mm (angles keep their own unit): a number, or undefined when unsupported. */\nexport type ExprValue = number | undefined;\n\nconst UNIT_TO_MM: Record<string, number> = {\n mm: 1, millimeter: 1,\n cm: 10, centimeter: 10,\n m: 1000, meter: 1000,\n in: 25.4, inch: 25.4, '\"': 25.4,\n ft: 304.8, foot: 304.8,\n // angles: value kept in the expression's own unit; callers interpret\n deg: 1, degree: 1, '°': 1,\n rad: 1, radian: 1,\n};\n\n/**\n * Evaluate a constant expression. Returns undefined when the expression is\n * not a bare constant (reference/arithmetic/function) — explicit unsupported,\n * no guessing.\n *\n * @param expr raw expression string (e.g. \"10 mm\")\n * @returns the value in mm, or undefined when not a constant expression\n */\nexport function evalConstantExpression(expr: string): ExprValue {\n const s = expr.trim();\n // number [unit]\n const m = /^([-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?)\\s*([A-Za-z°\"]*)$/.exec(s);\n if (!m) return undefined;\n const value = Number(m[1]);\n if (!Number.isFinite(value)) return undefined;\n const unit = m[2]!;\n if (unit === '') return value;\n const factor = UNIT_TO_MM[unit.toLowerCase()];\n if (factor === undefined) return undefined; // unknown unit → unsupported\n return value * factor;\n}\n\n// ── P1-1 参数载体(2026-09-23):Spreadsheet 别名三跳解析 + 引用算术 ──\n//\n// 语料实测(B2 / probe-param-cells,见 2026-09-21 方案 §3.9):<Expression> 的\n// 绝对头部是 `<<Label>>.Alias`(1,377 条)与引用参与的算术(657 条),而带别名\n// 的参数单元格 84.8% 就是「=5.9mm」形态——现有 evalConstantExpression 已能算值,\n// 缺的只是三跳:<<Data>> → 按 Label 找对象 → 按 alias 找单元格 → 去掉前导 =。\n// 函数族 / cells[...] 区间仍不猜(no heuristic fallback,方案 §12)。\n\nimport type { FcstdObject } from './document.js';\n\n/**\n * 三跳解析:`<<Label>>.Alias` / `Object.Alias` → 数据源对象 → 别名单元格 → 去掉\n * 前导 `=` 后交给 evalConstantExpression。同表地址(`=B2*2` 里的 `B2`)再跳一次。\n *\n * @param docObjects 全文档对象(按 Label 或 name 匹配数据源)\n * @param label 数据源标识(`<<Label>>` 内的 Label,或裸对象名)\n * @param alias 单元格别名\n * @returns 单元格值(mm),不可解析 → undefined\n */\nexport function spreadsheetAliasValue(\n docObjects: readonly FcstdObject[],\n label: string,\n alias: string,\n): ExprValue {\n const src = docObjects.find(\n (o) => o.type === 'Spreadsheet::Sheet' &&\n (objectLabel(o) === label || o.name === label),\n );\n if (!src) return undefined;\n // cells 属性(Spreadsheet::PropertySheet)的 <Cell address alias content/> 子元素\n const cells = src.properties.get('cells') ?? [...src.properties.values()].find((p) =>\n p.children.some((c) => c.tagName === 'Cell' || c.children.some((g) => g.tagName === 'Cell')),\n );\n if (!cells) return undefined;\n const cell = [...cells.children].flatMap((c) => (c.tagName === 'Cell' ? [c] : c.children))\n .find((c) => c.tagName === 'Cell' && c.attributes['alias'] === alias);\n if (!cell) return undefined;\n const content = cell.attributes['content'] ?? '';\n // 去掉前导 =(同表地址/算术再走一次带上下文的求值,一跳深度足够语料头部)\n return evalWithDoc(content.startsWith('=') ? content.slice(1) : content, docObjects, src);\n}\n\n/** 对象的 Label 属性(FreeCAD 引用 `<<Label>>` 用的是它,不是 name)。 */\nfunction objectLabel(o: FcstdObject): string | undefined {\n const el = o.properties.get('Label')?.children[0];\n return el?.attributes['value'] ?? (el?.valueText || undefined);\n}\n\n/**\n * 带文档上下文的表达式求值:引用(`<<L>>.A` / `L.A` / 同表地址 `B2`)替换为\n * 数值后,求值仅含常数与 + - * / ( ) 的算术。任何残留标识符 / 函数 → undefined\n * (no heuristic fallback)。返回值单位跟随单元格(mm 语境,角度单元格调用方解释)。\n *\n * @param expr 待求值表达式原文。\n * @param docObjects 文档对象表,用于解析引用(`<<L>>.A` / `L.A`)。\n * @param self 调用方对象(Spreadsheet::Sheet 时启用同表地址 `B2` 解析)。可选。\n * @returns 求值结果数值;含残留标识符 / 无法解析引用时返回 undefined。\n */\n/**\n * Evaluate a FreeCAD expression against the document objects.\n * @param expr - the expression text (without the leading `=`).\n * @param docObjects - the document objects visible to the expression.\n * @param self - the object the expression is evaluated on, if any.\n * @returns the evaluated expression value.\n */\nexport function evalWithDoc(\n expr: string,\n docObjects: readonly FcstdObject[],\n self?: FcstdObject,\n): ExprValue {\n let s = expr.trim();\n // 引用替换:<<Label>>.Alias | Label.Alias | Object.Alias(标识符.标识符)\n s = s.replace(/<<([^>]+)>>\\.([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)/g,\n (whole, brLabel, brAlias, plLabel, plAlias) => {\n const label = brLabel ?? plLabel;\n const alias = brAlias ?? plAlias;\n const v = spreadsheetAliasValue(docObjects, label!, alias!);\n return v === undefined ? whole : `(${v})`;\n });\n // 同表地址(self 表内 address→alias 值,如 `B2` / `B2*2`):仅当 self 是表\n if (self?.type === 'Spreadsheet::Sheet') {\n s = s.replace(/\\b([A-Z]+[0-9]+)\\b/g, (whole, addr: string) => {\n const v = spreadsheetAddressValue(self, addr, docObjects);\n return v === undefined ? whole : `(${v})`;\n });\n }\n // 单元格值自带单位(=5.9mm)——先按「常数+单位」求值(evalConstantExpression\n // 的既有口径),再进纯算术;单位记号在算术前剥离(返回值跟随 mm 语境)。\n const constant = evalConstantExpression(s);\n if (constant !== undefined) return constant;\n s = s.replace(/\\b(mm|millimeter|cm|centimeter|m|meter|in|inch|\"|ft|foot|deg|degree|°|rad|radian)\\b/g, '');\n return evalArithmetic(s);\n}\n\n/** 同表地址取值:address → 该单元格 content(递归经 evalWithDoc,一跳深度)。 */\nfunction spreadsheetAddressValue(\n sheet: FcstdObject,\n address: string,\n docObjects: readonly FcstdObject[],\n): ExprValue {\n const cells = sheet.properties.get('cells');\n if (!cells) return undefined;\n const cell = [...cells.children].flatMap((c) => (c.tagName === 'Cell' ? [c] : c.children))\n .find((c) => c.tagName === 'Cell' && c.attributes['address'] === address);\n if (!cell) return undefined;\n const content = cell.attributes['content'] ?? '';\n return evalWithDoc(content.startsWith('=') ? content.slice(1) : content, docObjects, sheet);\n}\n\n/** 仅含数字与 + - * / ( ) 空格的算术求值;任何其它字符 → undefined。 */\nfunction evalArithmetic(s: string): ExprValue {\n if (!/^[-+*/(). 0-9eE]*$/.test(s)) return undefined;\n try {\n const v = Function(`\"use strict\"; return (${s});`)() as unknown;\n return typeof v === 'number' && Number.isFinite(v) ? v : undefined;\n } catch {\n return undefined;\n }\n}\n\n\n/** One <Expression path=... expression=...> binding with its optional constant value. */\nexport interface ExpressionBinding {\n /** property path the expression drives (e.g. \"Length\") */\n path: string;\n /** raw expression string */\n expression: string;\n /** evaluated constant; undefined when not a constant expression */\n value?: number;\n}\n\n/**\n * Extract expression bindings from a parsed ExpressionEngine property.\n * `bindable` = every expression evaluated to a constant (then the property\n * values can be overridden); otherwise the engine is only partially readable.\n *\n * @param prop the parsed <ExpressionEngine> property (undefined → no bindings)\n * @returns one binding per <Expression> child, values evaluated\n */\nexport function parseExpressionEngine(\n prop: FcstdProperty | undefined,\n): ExpressionBinding[] {\n const engineEl = prop?.children[0];\n if (!engineEl) return [];\n const out: ExpressionBinding[] = [];\n for (const el of engineEl.children) {\n if (el.tagName !== 'Expression') continue;\n const path = el.attributes['path'] ?? '';\n const expression = el.attributes['expression'] ?? '';\n out.push({ path, expression, value: evalConstantExpression(expression) });\n }\n return out;\n}\n\n/**\n * True when all bindings are constants → safe to override property values.\n * @param bindings bindings extracted by parseExpressionEngine\n * @returns true when there is at least one binding and every value is defined\n */\nexport function allConstant(bindings: ExpressionBinding[]): boolean {\n return bindings.length > 0 && bindings.every((b) => b.value !== undefined);\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { type FcstdArchive } from './unpack.js';
2
+ import type { FcstdDocument, FcstdObject, FcstdProperty } from './document.js';
3
+ /**
4
+ * One resolved external-geometry link of a sketch.
5
+ */
6
+ export interface ExternalLink {
7
+ /** source object name, e.g. "Chamfer002" */
8
+ obj: string;
9
+ /** sub-element name, e.g. "Edge13" */
10
+ sub: string;
11
+ /** resolved sketch-local 2D polyline */
12
+ polyline: [number, number][];
13
+ }
14
+ /**
15
+ * Result of external-geometry resolution: successfully projected links plus
16
+ * per-link failures (with a human-readable reason) for the caller to report.
17
+ */
18
+ export interface ExternalGeoResult {
19
+ links: ExternalLink[];
20
+ failures: {
21
+ obj: string;
22
+ sub: string;
23
+ reason: string;
24
+ }[];
25
+ }
26
+ /**
27
+ * The `.brp` member holding an object's shape: the `Shape` property when
28
+ * present, else `SubShape` (GOTCHA, hole_puzzle corpus 2026-09-20: PartDesign
29
+ * features like Chamfer002/Pocket002 store their result cache in SubShape and
30
+ * have NO Shape property — external links pointing at them failed with
31
+ * "source shape not loadable" before the fallback existed).
32
+ *
33
+ * @param obj - the FCStd object to inspect.
34
+ * @returns the .brp member file name, or undefined when neither Shape nor SubShape carries one.
35
+ */
36
+ export declare function shapeBrpFile(obj: FcstdObject): string | undefined;
37
+ /**
38
+ * Resolve every external link of a sketch to sketch-local 2D polylines.
39
+ *
40
+ * @param externalGeoProp - the sketch's ExternalGeometry property.
41
+ * @param doc - the parsed FCStd document, used to locate source objects and their shapes.
42
+ * @param archive - the FCStd container, used to read the source `.brp` shape files.
43
+ * @param sketchPlacement - the sketch's Placement property, whose inverse maps
44
+ * source geometry into sketch-local coordinates.
45
+ * @returns the resolved sketch-local polylines per link, plus a failure entry
46
+ * (with reason) for every link that could not be resolved.
47
+ */
48
+ export declare function resolveExternalGeometry(externalGeoProp: FcstdProperty | undefined, doc: FcstdDocument, archive: FcstdArchive, sketchPlacement: FcstdProperty | undefined): Promise<ExternalGeoResult>;
49
+ //# sourceMappingURL=external-geo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-geo.d.ts","sourceRoot":"","sources":["../src/external-geo.ts"],"names":[],"mappings":"AAWA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1D;AAiCD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAC3C,eAAe,EAAE,aAAa,GAAG,SAAS,EAC1C,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,YAAY,EACrB,eAAe,EAAE,aAAa,GAAG,SAAS,GACzC,OAAO,CAAC,iBAAiB,CAAC,CA4E5B"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * M6.3 — external geometry resolution: decode ExternalGeometry links
3
+ * (App::PropertyLinkSubList), load the source object's .brp via OCCT,
4
+ * extract the named edge polyline (TopExp::MapShapes + IndexedMap order —
5
+ * Edge13 = wireframe edgeGroups[12], verified against hole_puzzle.fcstd),
6
+ * and project it into sketch-local 2D via the sketch Placement inverse.
7
+ *
8
+ * Result: fixed 2D segments/points that the solver treats as immutable
9
+ * constraints targets (geoId -3, -4, ... in link order).
10
+ */
11
+ import { initOcctWasm } from '@faicad/faijs/occt-kernel/occtKernel';
12
+ import { memberText } from './unpack.js';
13
+ /** Quaternion (x,y,z,w) inverse-rotate + translate into sketch-local frame. */
14
+ function makeInverseTransformer(q, p) {
15
+ const n = Math.hypot(q[0], q[1], q[2], q[3]);
16
+ const [qx, qy, qz, qw] = [-q[0] / n, -q[1] / n, -q[2] / n, q[3] / n]; // conjugate
17
+ return (w) => {
18
+ // local = R⁻¹ · (world − P): subtract the translation FIRST, then rotate
19
+ // by the conjugate quaternion. GOTCHA (probe-hole-ext.ts): rotating first
20
+ // and subtracting after (R⁻¹·v − P) is only correct for identity rotation
21
+ // — with Sketch005's 90° placement it shifted projected points by the
22
+ // rotated translation (−140,30 instead of (−40,40)) and the solver chased
23
+ // the wrong frame (delta 1.0e2).
24
+ const [vx, vy, vz] = [w[0] - p[0], w[1] - p[1], w[2] - p[2]];
25
+ const tx = 2 * (qy * vz - qz * vy);
26
+ const ty = 2 * (qz * vx - qx * vz);
27
+ const tz = 2 * (qx * vy - qy * vx);
28
+ const rx = vx + qw * tx + (qy * tz - qz * ty);
29
+ const ry = vy + qw * ty + (qz * tx - qx * tz);
30
+ const rz = vz + qw * tz + (qx * ty - qy * tx);
31
+ return [rx, ry, rz];
32
+ };
33
+ }
34
+ function placementOf(obj) {
35
+ const el = obj.properties.get('Placement')?.children[0];
36
+ const a = el?.attributes ?? {};
37
+ return {
38
+ p: [Number(a['Px'] ?? 0), Number(a['Py'] ?? 0), Number(a['Pz'] ?? 0)],
39
+ q: [Number(a['Q0'] ?? 0), Number(a['Q1'] ?? 0), Number(a['Q2'] ?? 0), Number(a['Q3'] ?? 1)],
40
+ };
41
+ }
42
+ /**
43
+ * The `.brp` member holding an object's shape: the `Shape` property when
44
+ * present, else `SubShape` (GOTCHA, hole_puzzle corpus 2026-09-20: PartDesign
45
+ * features like Chamfer002/Pocket002 store their result cache in SubShape and
46
+ * have NO Shape property — external links pointing at them failed with
47
+ * "source shape not loadable" before the fallback existed).
48
+ *
49
+ * @param obj - the FCStd object to inspect.
50
+ * @returns the .brp member file name, or undefined when neither Shape nor SubShape carries one.
51
+ */
52
+ export function shapeBrpFile(obj) {
53
+ return obj.properties.get('Shape')?.children[0]?.attributes['file']
54
+ ?? obj.properties.get('SubShape')?.children[0]?.attributes['file'];
55
+ }
56
+ /**
57
+ * Resolve every external link of a sketch to sketch-local 2D polylines.
58
+ *
59
+ * @param externalGeoProp - the sketch's ExternalGeometry property.
60
+ * @param doc - the parsed FCStd document, used to locate source objects and their shapes.
61
+ * @param archive - the FCStd container, used to read the source `.brp` shape files.
62
+ * @param sketchPlacement - the sketch's Placement property, whose inverse maps
63
+ * source geometry into sketch-local coordinates.
64
+ * @returns the resolved sketch-local polylines per link, plus a failure entry
65
+ * (with reason) for every link that could not be resolved.
66
+ */
67
+ export async function resolveExternalGeometry(externalGeoProp, doc, archive, sketchPlacement) {
68
+ const links = [];
69
+ const failures = [];
70
+ const listEl = externalGeoProp?.children[0];
71
+ if (!listEl)
72
+ return { links, failures };
73
+ const skPl = placementOf({ name: '', type: '', properties: new Map([['Placement', sketchPlacement ?? { name: 'Placement', type: '', tagName: 'Property', children: [], valueXml: '', valueText: '', attributes: {} }]]) });
74
+ const toLocal = makeInverseTransformer(skPl.q, skPl.p);
75
+ const kernel = (await initOcctWasm());
76
+ const brpCache = new Map();
77
+ const wireframeOf = (objName) => {
78
+ if (brpCache.has(objName))
79
+ return brpCache.get(objName);
80
+ const src = doc.objects.find((o) => o.name === objName);
81
+ const brpFile = src ? shapeBrpFile(src) : undefined;
82
+ if (!brpFile) {
83
+ brpCache.set(objName, undefined);
84
+ return undefined;
85
+ }
86
+ const brp = memberText(archive, brpFile);
87
+ if (!brp) {
88
+ brpCache.set(objName, undefined);
89
+ return undefined;
90
+ }
91
+ try {
92
+ const shape = kernel.fromBREP(brp);
93
+ const wf = kernel.wireframe(shape, 0.01);
94
+ brpCache.set(objName, wf);
95
+ return wf;
96
+ }
97
+ catch {
98
+ brpCache.set(objName, undefined);
99
+ return undefined;
100
+ }
101
+ };
102
+ for (const link of listEl.children) {
103
+ const obj = link.attributes['obj'] ?? '';
104
+ const sub = link.attributes['sub'] ?? '';
105
+ const ord = Number(sub.replace('Edge', ''));
106
+ if (!obj || !sub.startsWith('Edge') || !Number.isInteger(ord) || ord < 1) {
107
+ failures.push({ obj, sub, reason: `unsupported sub-element: ${sub}` });
108
+ continue;
109
+ }
110
+ const wf = wireframeOf(obj);
111
+ if (!wf) {
112
+ failures.push({ obj, sub, reason: 'source shape not loadable' });
113
+ continue;
114
+ }
115
+ const idx = ord - 1; // Edge13 → edgeGroups[12]
116
+ const g0 = wf.edgeGroups[idx * 3];
117
+ const g1 = wf.edgeGroups[idx * 3 + 1];
118
+ if (g0 === undefined || g1 === undefined) {
119
+ failures.push({ obj, sub, reason: `edge ordinal out of range (${wf.edgeGroups.length / 3} edges)` });
120
+ continue;
121
+ }
122
+ const n = Math.floor(g1 / 3);
123
+ const polyline = [];
124
+ for (let p = 0; p < n; p++) {
125
+ const w = [
126
+ wf.points[g0 + p * 3], wf.points[g0 + p * 3 + 1], wf.points[g0 + p * 3 + 2],
127
+ ];
128
+ const l = toLocal(w);
129
+ polyline.push([l[0], l[1]]);
130
+ }
131
+ // drop consecutive duplicates
132
+ const deduped = polyline.filter((pt, i) => i === 0 || Math.hypot(pt[0] - polyline[i - 1][0], pt[1] - polyline[i - 1][1]) > 1e-9);
133
+ if (deduped.length >= 2)
134
+ links.push({ obj, sub, polyline: deduped });
135
+ else
136
+ failures.push({ obj, sub, reason: 'degenerate polyline' });
137
+ }
138
+ return { links, failures };
139
+ }
140
+ //# sourceMappingURL=external-geo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-geo.js","sourceRoot":"","sources":["../src/external-geo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * M6.3 — external geometry resolution: decode ExternalGeometry links\n * (App::PropertyLinkSubList), load the source object's .brp via OCCT,\n * extract the named edge polyline (TopExp::MapShapes + IndexedMap order —\n * Edge13 = wireframe edgeGroups[12], verified against hole_puzzle.fcstd),\n * and project it into sketch-local 2D via the sketch Placement inverse.\n *\n * Result: fixed 2D segments/points that the solver treats as immutable\n * constraints targets (geoId -3, -4, ... in link order).\n */\nimport { initOcctWasm } from '@faicad/faijs/occt-kernel/occtKernel';\nimport { memberText, type FcstdArchive } from './unpack.js';\nimport type { FcstdDocument, FcstdObject, FcstdProperty } from './document.js';\n\n/**\n * One resolved external-geometry link of a sketch.\n */\nexport interface ExternalLink {\n /** source object name, e.g. \"Chamfer002\" */\n obj: string;\n /** sub-element name, e.g. \"Edge13\" */\n sub: string;\n /** resolved sketch-local 2D polyline */\n polyline: [number, number][];\n}\n\n/**\n * Result of external-geometry resolution: successfully projected links plus\n * per-link failures (with a human-readable reason) for the caller to report.\n */\nexport interface ExternalGeoResult {\n links: ExternalLink[];\n failures: { obj: string; sub: string; reason: string }[];\n}\n\n/** Quaternion (x,y,z,w) inverse-rotate + translate into sketch-local frame. */\nfunction makeInverseTransformer(q: [number, number, number, number], p: [number, number, number]) {\n const n = Math.hypot(q[0], q[1], q[2], q[3]);\n const [qx, qy, qz, qw] = [-q[0] / n, -q[1] / n, -q[2] / n, q[3] / n]; // conjugate\n return (w: [number, number, number]): [number, number, number] => {\n // local = R⁻¹ · (world − P): subtract the translation FIRST, then rotate\n // by the conjugate quaternion. GOTCHA (probe-hole-ext.ts): rotating first\n // and subtracting after (R⁻¹·v − P) is only correct for identity rotation\n // — with Sketch005's 90° placement it shifted projected points by the\n // rotated translation (−140,30 instead of (−40,40)) and the solver chased\n // the wrong frame (delta 1.0e2).\n const [vx, vy, vz] = [w[0] - p[0], w[1] - p[1], w[2] - p[2]];\n const tx = 2 * (qy * vz - qz * vy);\n const ty = 2 * (qz * vx - qx * vz);\n const tz = 2 * (qx * vy - qy * vx);\n const rx = vx + qw * tx + (qy * tz - qz * ty);\n const ry = vy + qw * ty + (qz * tx - qx * tz);\n const rz = vz + qw * tz + (qx * ty - qy * tx);\n return [rx, ry, rz];\n };\n}\n\nfunction placementOf(obj: FcstdObject): { q: [number, number, number, number]; p: [number, number, number] } {\n const el = obj.properties.get('Placement')?.children[0];\n const a = el?.attributes ?? {};\n return {\n p: [Number(a['Px'] ?? 0), Number(a['Py'] ?? 0), Number(a['Pz'] ?? 0)],\n q: [Number(a['Q0'] ?? 0), Number(a['Q1'] ?? 0), Number(a['Q2'] ?? 0), Number(a['Q3'] ?? 1)],\n };\n}\n\n/**\n * The `.brp` member holding an object's shape: the `Shape` property when\n * present, else `SubShape` (GOTCHA, hole_puzzle corpus 2026-09-20: PartDesign\n * features like Chamfer002/Pocket002 store their result cache in SubShape and\n * have NO Shape property — external links pointing at them failed with\n * \"source shape not loadable\" before the fallback existed).\n *\n * @param obj - the FCStd object to inspect.\n * @returns the .brp member file name, or undefined when neither Shape nor SubShape carries one.\n */\nexport function shapeBrpFile(obj: FcstdObject): string | undefined {\n return obj.properties.get('Shape')?.children[0]?.attributes['file']\n ?? obj.properties.get('SubShape')?.children[0]?.attributes['file'];\n}\n\n/**\n * Resolve every external link of a sketch to sketch-local 2D polylines.\n *\n * @param externalGeoProp - the sketch's ExternalGeometry property.\n * @param doc - the parsed FCStd document, used to locate source objects and their shapes.\n * @param archive - the FCStd container, used to read the source `.brp` shape files.\n * @param sketchPlacement - the sketch's Placement property, whose inverse maps\n * source geometry into sketch-local coordinates.\n * @returns the resolved sketch-local polylines per link, plus a failure entry\n * (with reason) for every link that could not be resolved.\n */\nexport async function resolveExternalGeometry(\n externalGeoProp: FcstdProperty | undefined,\n doc: FcstdDocument,\n archive: FcstdArchive,\n sketchPlacement: FcstdProperty | undefined,\n): Promise<ExternalGeoResult> {\n const links: ExternalLink[] = [];\n const failures: ExternalGeoResult['failures'] = [];\n const listEl = externalGeoProp?.children[0];\n if (!listEl) return { links, failures };\n\n const skPl = placementOf({ name: '', type: '', properties: new Map([['Placement', sketchPlacement ?? { name: 'Placement', type: '', tagName: 'Property', children: [], valueXml: '', valueText: '', attributes: {} } as FcstdProperty]]) as never });\n const toLocal = makeInverseTransformer(skPl.q, skPl.p);\n\n const kernel: {\n fromBREP: (s: string) => unknown;\n wireframe: (s: unknown, deflection: number) => { points: Float32Array; edgeGroups: number[] };\n } = (await initOcctWasm()) as never;\n\n const brpCache = new Map<string, { points: Float32Array; edgeGroups: number[] } | undefined>();\n const wireframeOf = (objName: string) => {\n if (brpCache.has(objName)) return brpCache.get(objName);\n const src = doc.objects.find((o) => o.name === objName);\n const brpFile = src ? shapeBrpFile(src) : undefined;\n if (!brpFile) {\n brpCache.set(objName, undefined);\n return undefined;\n }\n const brp = memberText(archive, brpFile);\n if (!brp) {\n brpCache.set(objName, undefined);\n return undefined;\n }\n try {\n const shape = kernel.fromBREP(brp);\n const wf = kernel.wireframe(shape, 0.01);\n brpCache.set(objName, wf);\n return wf;\n } catch {\n brpCache.set(objName, undefined);\n return undefined;\n }\n };\n\n for (const link of listEl.children) {\n const obj = link.attributes['obj'] ?? '';\n const sub = link.attributes['sub'] ?? '';\n const ord = Number(sub.replace('Edge', ''));\n if (!obj || !sub.startsWith('Edge') || !Number.isInteger(ord) || ord < 1) {\n failures.push({ obj, sub, reason: `unsupported sub-element: ${sub}` });\n continue;\n }\n const wf = wireframeOf(obj);\n if (!wf) {\n failures.push({ obj, sub, reason: 'source shape not loadable' });\n continue;\n }\n const idx = ord - 1; // Edge13 → edgeGroups[12]\n const g0 = wf.edgeGroups[idx * 3];\n const g1 = wf.edgeGroups[idx * 3 + 1];\n if (g0 === undefined || g1 === undefined) {\n failures.push({ obj, sub, reason: `edge ordinal out of range (${wf.edgeGroups.length / 3} edges)` });\n continue;\n }\n const n = Math.floor(g1 / 3);\n const polyline: [number, number][] = [];\n for (let p = 0; p < n; p++) {\n const w: [number, number, number] = [\n wf.points[g0 + p * 3]!, wf.points[g0 + p * 3 + 1]!, wf.points[g0 + p * 3 + 2]!,\n ];\n const l = toLocal(w);\n polyline.push([l[0], l[1]]);\n }\n // drop consecutive duplicates\n const deduped = polyline.filter(\n (pt, i) => i === 0 || Math.hypot(pt[0] - polyline[i - 1]![0], pt[1] - polyline[i - 1]![1]) > 1e-9,\n );\n if (deduped.length >= 2) links.push({ obj, sub, polyline: deduped });\n else failures.push({ obj, sub, reason: 'degenerate polyline' });\n }\n return { links, failures };\n}\n"]}