@hatiolab/figure-model 0.1.94 → 0.2.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 (102) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/asset.d.ts.map +1 -1
  3. package/dist/asset.js +3 -1
  4. package/dist/asset.js.map +1 -1
  5. package/dist/authoring-actions.d.ts +19 -0
  6. package/dist/authoring-actions.d.ts.map +1 -1
  7. package/dist/authoring-actions.js +109 -64
  8. package/dist/authoring-actions.js.map +1 -1
  9. package/dist/command-preview.d.ts +3 -1
  10. package/dist/command-preview.d.ts.map +1 -1
  11. package/dist/command-preview.js +52 -27
  12. package/dist/command-preview.js.map +1 -1
  13. package/dist/cost.d.ts +1 -1
  14. package/dist/cost.d.ts.map +1 -1
  15. package/dist/cost.js +30 -25
  16. package/dist/cost.js.map +1 -1
  17. package/dist/create.d.ts.map +1 -1
  18. package/dist/create.js +112 -31
  19. package/dist/create.js.map +1 -1
  20. package/dist/document.d.ts.map +1 -1
  21. package/dist/document.js +3 -2
  22. package/dist/document.js.map +1 -1
  23. package/dist/editor-workspace.d.ts +38 -0
  24. package/dist/editor-workspace.d.ts.map +1 -1
  25. package/dist/editor-workspace.js +26 -0
  26. package/dist/editor-workspace.js.map +1 -1
  27. package/dist/fixtures/shape-nodes.d.ts +12 -0
  28. package/dist/fixtures/shape-nodes.d.ts.map +1 -0
  29. package/dist/fixtures/shape-nodes.js +20 -0
  30. package/dist/fixtures/shape-nodes.js.map +1 -0
  31. package/dist/follow.d.ts.map +1 -1
  32. package/dist/follow.js +5 -2
  33. package/dist/follow.js.map +1 -1
  34. package/dist/gate.d.ts.map +1 -1
  35. package/dist/gate.js +31 -59
  36. package/dist/gate.js.map +1 -1
  37. package/dist/graph-types.d.ts +7 -6
  38. package/dist/graph-types.d.ts.map +1 -1
  39. package/dist/graph.d.ts +17 -12
  40. package/dist/graph.d.ts.map +1 -1
  41. package/dist/graph.js +24 -292
  42. package/dist/graph.js.map +1 -1
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/kernel-version.d.ts +1 -1
  48. package/dist/kernel-version.d.ts.map +1 -1
  49. package/dist/kernel-version.js +1 -1
  50. package/dist/kernel-version.js.map +1 -1
  51. package/dist/part-edit.d.ts.map +1 -1
  52. package/dist/part-edit.js +8 -6
  53. package/dist/part-edit.js.map +1 -1
  54. package/dist/proposal.d.ts +1 -1
  55. package/dist/proposal.d.ts.map +1 -1
  56. package/dist/proposal.js +14 -2
  57. package/dist/proposal.js.map +1 -1
  58. package/dist/recipe-from-parts.d.ts.map +1 -1
  59. package/dist/recipe-from-parts.js +107 -24
  60. package/dist/recipe-from-parts.js.map +1 -1
  61. package/dist/render-batches.d.ts.map +1 -1
  62. package/dist/render-batches.js +11 -2
  63. package/dist/render-batches.js.map +1 -1
  64. package/dist/resize-plan.d.ts.map +1 -1
  65. package/dist/resize-plan.js +22 -8
  66. package/dist/resize-plan.js.map +1 -1
  67. package/dist/section.d.ts +164 -0
  68. package/dist/section.d.ts.map +1 -0
  69. package/dist/section.js +277 -0
  70. package/dist/section.js.map +1 -0
  71. package/dist/shape-edit.d.ts +22 -0
  72. package/dist/shape-edit.d.ts.map +1 -0
  73. package/dist/shape-edit.js +225 -0
  74. package/dist/shape-edit.js.map +1 -0
  75. package/dist/shape-features.d.ts +83 -0
  76. package/dist/shape-features.d.ts.map +1 -0
  77. package/dist/shape-features.js +102 -0
  78. package/dist/shape-features.js.map +1 -0
  79. package/dist/shape-ops.d.ts +154 -0
  80. package/dist/shape-ops.d.ts.map +1 -0
  81. package/dist/shape-ops.js +571 -0
  82. package/dist/shape-ops.js.map +1 -0
  83. package/dist/shape-provider.d.ts +16 -23
  84. package/dist/shape-provider.d.ts.map +1 -1
  85. package/dist/shape-provider.js +18 -56
  86. package/dist/shape-provider.js.map +1 -1
  87. package/dist/shape-sampling.d.ts +0 -8
  88. package/dist/shape-sampling.d.ts.map +1 -1
  89. package/dist/shape-sampling.js +67 -101
  90. package/dist/shape-sampling.js.map +1 -1
  91. package/dist/size-edit.js +2 -2
  92. package/dist/size-edit.js.map +1 -1
  93. package/dist/surface.d.ts.map +1 -1
  94. package/dist/surface.js +10 -17
  95. package/dist/surface.js.map +1 -1
  96. package/dist/words.d.ts +7 -0
  97. package/dist/words.d.ts.map +1 -1
  98. package/dist/words.js +7 -0
  99. package/dist/words.js.map +1 -1
  100. package/docs/document-contract.md +1 -1
  101. package/docs/shape-contract.md +439 -0
  102. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"words.d.ts","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,IAAI,0BAA2B,CAAA;AAC5C,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;AAExC,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,+DAAgE,CAAA;AAChG,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC,CAEjF,CAAA;AAEF,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,gFAAiF,CAAA;AAC9G,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,0BAA2B,CAAA;AACrD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD,2CAA2C;AAC3C,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAyB,CAAA;AAE5E;;;GAGG;AACH,eAAO,MAAM,IAAI,MAAM,CAAA;AAEvB,MAAM,MAAM,WAAW,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAE/C;;;GAGG;AACH,eAAO,MAAM,MAAM;IACjB,2GAA2G;;IAE3G,qEAAqE;;IAErE,0EAA0E;;IAE1E;;;;OAIG;;IAEH;;;OAGG;;IAEH;;;;OAIG;;IAEH,mDAAmD;;IAEnD,mGAAmG;;IAEnG,iDAAiD;;CAEzC,CAAA;AAEV;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAA4B,CAAA;AAEzE,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAA4D,CAAA;AAE1H;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAkC,CAAA;AAC3D,eAAO,MAAM,WAAW;;;CAA+B,CAAA;AAEvD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAmB,GAAG,MAAM,CAEhE;AAID,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD"}
1
+ {"version":3,"file":"words.d.ts","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,IAAI,0BAA2B,CAAA;AAC5C,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;AAExC,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,+DAAgE,CAAA;AAChG,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC,CAEjF,CAAA;AAEF,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,gFAAiF,CAAA;AAC9G,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,0BAA2B,CAAA;AACrD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD,2CAA2C;AAC3C,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAyB,CAAA;AAE5E;;;GAGG;AACH,eAAO,MAAM,IAAI,MAAM,CAAA;AAEvB,MAAM,MAAM,WAAW,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAE/C;;;GAGG;AACH,eAAO,MAAM,MAAM;IACjB,2GAA2G;;IAE3G,qEAAqE;;IAErE,0EAA0E;;IAE1E;;;;OAIG;;IAEH;;;OAGG;;IAEH;;;;OAIG;;IAEH,mDAAmD;;IAEnD,mGAAmG;;IAEnG,iDAAiD;;CAEzC,CAAA;AAEV;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAA4B,CAAA;AAEzE,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAA4D,CAAA;AAE1H;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAkC,CAAA;AAC3D,eAAO,MAAM,WAAW;;;CAA+B,CAAA;AAEvD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAmB,GAAG,MAAM,CAEhE;AAID,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAA"}
package/dist/words.js CHANGED
@@ -113,4 +113,11 @@ const PLACEHOLDER_TYPE = /^FIGURE_[0-9A-Z]{8}$/;
113
113
  export function isPlaceholderType(type) {
114
114
  return PLACEHOLDER_TYPE.test(type);
115
115
  }
116
+ /**
117
+ * The most nodes one figure's graph may have: an experimental resource limit, not a product capacity policy
118
+ * (docs/document-contract.md). Raised from 512 on 2026-09-29, provisionally, pending the designer's ruling on
119
+ * docs/shape-contract.md Q10: writing a push as its two ends (Q1) gives every part a start node, and two stored figures
120
+ * of 493 and 497 nodes went past 512 when rewritten.
121
+ */
122
+ export const FIGURE_DOCUMENT_NODE_LIMIT = 1024;
116
123
  //# sourceMappingURL=words.js.map
package/dist/words.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"words.js","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AAS5C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,CAAU,CAAA;AAGhG;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAA0D,MAAM,CAAC,MAAM,CAAC;IACnG,YAAY,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC;CACvC,CAAC,CAAA;AAEF,gFAAgF;AAChF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAU,CAAA;AAG9G;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;AAE/B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AAGrD,2CAA2C;AAC3C,MAAM,CAAC,MAAM,UAAU,GAAgC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAA;AAE5E;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAA;AAIvB;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,2GAA2G;IAC3G,iBAAiB,EAAE,CAAC;IACpB,qEAAqE;IACrE,cAAc,EAAE,IAAI;IACpB,0EAA0E;IAC1E,iBAAiB,EAAE,OAAO;IAC1B;;;;OAIG;IACH,eAAe,EAAE,GAAG;IACpB;;;OAGG;IACH,gBAAgB,EAAE,EAAE;IACpB;;;;OAIG;IACH,oBAAoB,EAAE,MAAM;IAC5B,mDAAmD;IACnD,oBAAoB,EAAE,CAAC;IACvB,mGAAmG;IACnG,eAAe,EAAE,CAAC;IAClB,iDAAiD;IACjD,WAAW,EAAE,CAAC;CACN,CAAA;AAEV;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAEzE,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAmC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;AAE1H;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,+BAA+B,CAAA;AAC3D,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAW,CAAA;AAEvD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACtD,OAAO,UAAU,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAA;AACnD,CAAC;AAED,MAAM,gBAAgB,GAAG,sBAAsB,CAAA;AAE/C,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACpC,CAAC"}
1
+ {"version":3,"file":"words.js","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AAS5C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,CAAU,CAAA;AAGhG;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAA0D,MAAM,CAAC,MAAM,CAAC;IACnG,YAAY,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC;CACvC,CAAC,CAAA;AAEF,gFAAgF;AAChF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAU,CAAA;AAG9G;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;AAE/B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AAGrD,2CAA2C;AAC3C,MAAM,CAAC,MAAM,UAAU,GAAgC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAA;AAE5E;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAA;AAIvB;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,2GAA2G;IAC3G,iBAAiB,EAAE,CAAC;IACpB,qEAAqE;IACrE,cAAc,EAAE,IAAI;IACpB,0EAA0E;IAC1E,iBAAiB,EAAE,OAAO;IAC1B;;;;OAIG;IACH,eAAe,EAAE,GAAG;IACpB;;;OAGG;IACH,gBAAgB,EAAE,EAAE;IACpB;;;;OAIG;IACH,oBAAoB,EAAE,MAAM;IAC5B,mDAAmD;IACnD,oBAAoB,EAAE,CAAC;IACvB,mGAAmG;IACnG,eAAe,EAAE,CAAC;IAClB,iDAAiD;IACjD,WAAW,EAAE,CAAC;CACN,CAAA;AAEV;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAEzE,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAmC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;AAE1H;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,+BAA+B,CAAA;AAC3D,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAW,CAAA;AAEvD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACtD,OAAO,UAAU,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAA;AACnD,CAAC;AAED,MAAM,gBAAgB,GAAG,sBAAsB,CAAA;AAE/C,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAA"}
@@ -35,7 +35,7 @@ JSON Schema는 필드 구조·자료형·기본 enum·버전을 검사한다. op
35
35
  - 알 수 없는 문서/계약 버전과 추가 필드를 거부한다. 현재 capability V3는 transform 또는 표면 참조 pose를 허용한다. V1/V2를 추측 변환하지 않는다.
36
36
  - JSON 키 중복은 마지막 값으로 덮지 않고 거부한다. Unicode escape로 같은 키가 표현된 경우도 포함한다.
37
37
  - 잘린 JSON, 비유한 수, undefined, 함수, Date, 순환, sparse array, accessor·숨겨진 속성 등 JSON으로 손실되는 입력을 거부한다.
38
- - 실험 자원 제한: 1MiB UTF-8, 객체 깊이64, 그래프 node512. 제품 용량 정책으로 확정된 값이 아니다.
38
+ - 실험 자원 제한: 1MiB UTF-8, 객체 깊이64, 그래프 node 1024(`FIGURE_DOCUMENT_NODE_LIMIT`, 2026-09-29 512에서 잠정 상향 — shape-contract Q10). 제품 용량 정책으로 확정된 값이 아니다.
39
39
  - 선언 공간(occupancy)의 경계는 `affine-bounds@1` 한 노드의 출력 여섯이다. 압축 인자 모양(`layout` = `L` + 여섯 묶음 + 후보별 항 수)은 [연산 계약 §4](operator-contracts.md#4-연산-catalog)에 적었다 — AI 저작·내보내기는 거기서 풀어 읽는다.
40
40
  - round-trip은 의미·수치·선언 동일성을 대상으로 한다. 원본 JSON의 공백·키 표기·서식은 보존하지 않는다.
41
41
  - 함수는 파일/DB에 직접 쓰지 않는다. 저장 API의 원자성, 권한, 버전 충돌, 마이그레이션은 제품 통합 시 별도로 구현해야 한다.
@@ -0,0 +1,439 @@
1
+ # Shape contract — sections, pushes, cavities and their named features
2
+
3
+ Status: **contract, Q1–Q9 ruled by the designer as recommended (2026-09-29, 「추천대로 진행하라」).** Stage 1 is being
4
+ built against it. The 2026-09-22 Bézier-corner ruling is replaced by Q4: a corner is an elliptic quarter.
5
+
6
+ Stage 0 record: Nothing here is wired into the asset, the compiler,
7
+ the renderer or the modeller yet. What is code today:
8
+
9
+ - `src/section.ts`: a section's meaning. Validity, the one written form, side ids, topology change, area and bounds,
10
+ and the inside section of a cavity.
11
+ - `src/shape-features.ts`: the named features of a pushed section and its cavity.
12
+
13
+ Both are tested against figures worked out by hand (`src/section.test.ts`, `src/shape-features.test.ts`). Every
14
+ choice this document still needs from the designer is marked **Qn** and listed in §12. The Korean question sheet for
15
+ the user is `operato-application/packages/operato-figure/docs/shape-editing-stage0-2026-09-29.md`.
16
+
17
+ This contract replaces the eleven shape operators (`box-shape@1` … `polygon-shape@1`) and their provider names. It
18
+ does not sit beside them. Old assets are rewritten once, and nothing reads the old operators after the switch
19
+ (ADR-0100 decision 9, §10).
20
+
21
+ ## What a person sees
22
+
23
+ An author adds a **상자** (box). They type a width, a height and a depth, and the box is done. They drag the corner
24
+ radius from 0 to 10. The four corners round off, the box keeps its outer size, and the lid fastened to its top stays
25
+ where it is. They turn on **속 공간** (inside space) with a 10 mm wall, a 15 mm floor and the top open. The box
26
+ becomes a tote. The lid now sits on a rim, and the rim highlights as a ring, not as a filled square. A divider can be
27
+ fastened to the tote's inside floor. Then they open both ends: the floor goes and the tote becomes a duct. Nothing
28
+ is left behind at the bottom.
29
+
30
+ A second author starts from **단면 그려 만들기** (draw a section) and gets a triangle. They click a side to add a
31
+ point and drag it out, and the triangle becomes a trapezoid. They drag one point across the opposite side. The two
32
+ crossing sides turn red, the message reads 「두 선이 교차합니다. 표시된 점을 옮겨 주세요」, and `적용` (apply) is
33
+ disabled. The last valid shape stays drawn, faded, labelled 「마지막으로 유효했던 모양」. They press Esc, and the
34
+ part is exactly what it was before.
35
+
36
+ A third author deletes a point of a section whose side has a bracket fastened to it. Before anything changes, the
37
+ screen names the bracket and the side it is fastened to. It asks which new side to use, or offers to leave the
38
+ bracket where it is, unfastened. It never picks the nearest side on its own.
39
+
40
+ ## 0. As built (stage 1, 2026-09-29)
41
+
42
+ What the kernel does now, where it differs from the text below, and why. Branch `shape-extrude`, unpublished.
43
+
44
+ - **The node is the record.** A shape is one graph node, `extrude@1` or `revolve@1`. Its params say what it is: the
45
+ axis, the section kind, the point names, and whether it has an inside and which ends are open. Its arguments are the
46
+ values that size it, in an order the params fix (`figureExtrudeLayoutOf`). There is no second record beside it, so
47
+ there is no drift to check. Every reader asks `figureShapeDescriptionOf(node)` which argument is which, and never
48
+ reads an argument by its place.
49
+ - **A push is `start` and `length`**, and its end is `start + length` (Q1's two ends). A new part's start is one node,
50
+ `<start> = <length> × −0.5`, so it is centred on its origin as parts always were. Given the graph, such a push's ends
51
+ are written as ±half its length, which keeps the joints of a rewritten figure made of the same nodes. A later
52
+ "keep this face" edit writes an explicit start instead (`set-extrusion`, not built yet).
53
+ - **Section kinds:** `rectangle` (with `corner: round`), `ellipse`, `polygon`, and **`portal`**. A portal is a door
54
+ frame's Π section (width, height, jamb, head) with its faces `opening-left`, `opening-right` and `opening-top`. It is
55
+ the one section Q5 ruled, named so that DRY_ROOM's doors keep the faces they hang on and follow its size.
56
+ - **Faces keep the modeller's words in stage 1:** the six outer faces by axis, `inside-*` for an inside and
57
+ `opening-*` for a portal. Renaming them to feature ids (`start-face`, `side-face:<id>` … §4) is its own step, done
58
+ once for every joint and binding reference. A plane touching a curved side is marked `contact: 'line'` (Q3): it can
59
+ be fastened to, it is not a surface a binding holds things on, and its preview is a line.
60
+ - **Commands built:** `set-section-corner` and `set-cavity` (§7). An edit that takes away a face something refers to
61
+ is refused with `FEATURE_MAPPING_REQUIRED`. Mapping to another face is not built yet.
62
+ - **The one-time rewrite:** `scripts/rewrite-shapes.ts` and `scripts/rewrite-shapes-files.ts`. A file is written only
63
+ when every body's world box and pose agree with the old kernel's (0.1.95) at the drawn values, at half and twice
64
+ the size, and at each state input's ends. All 24 fixtures agree.
65
+ - **Q10 (new, provisional):** the experimental node limit was raised from 512 to 1024 (`FIGURE_DOCUMENT_NODE_LIMIT`).
66
+ A start node per part took two stored figures, of 493 and 497 nodes, past 512. The alternative is a `start` left out
67
+ for a centred push, which is a stored shorthand and a change of format.
68
+
69
+ ## 1. The record
70
+
71
+ The shape of a part is a record in the asset, **the canon**. Its graph nodes are generated from it, the way a joint's
72
+ nodes are generated from its record (ADR-0093 decision 2). No command edits the nodes directly, and an asset whose
73
+ nodes disagree with its records is refused when read.
74
+
75
+ Every length in a record is a `FigureMeasure`, the same type a joint's gap uses. It is a number, or
76
+ `{ from, times, plus }` read from the instance's size, a design input or another part's dimension. So a section's
77
+ size and a drawn section's corner can follow the figure exactly as a box's width does today.
78
+
79
+ ```ts
80
+ interface FigurePartShape {
81
+ part: string // the part id (the place node's id, as today)
82
+ body: FigureExtrude | FigureRevolve | FigureLoft
83
+ cavities?: FigureCavityRecord[] // at most one, and only on an extrude, in the first contract (§3)
84
+ }
85
+
86
+ interface FigureExtrude {
87
+ op: 'extrude'
88
+ axis: 'x' | 'y' | 'z' // the part's own axis the section is pushed along
89
+ section: FigureSectionRecord
90
+ start: FigureMeasure // E(S, a, b): a and b, measured from the part's origin along axis
91
+ end: FigureMeasure // start < end over the whole declared range
92
+ }
93
+
94
+ type FigureSectionRecord =
95
+ | { kind: 'rectangle'; size: PlaneRecord<FigureMeasure>; corner?: PlaneRecord<FigureMeasure> }
96
+ | { kind: 'ellipse'; radius: PlaneRecord<FigureMeasure> }
97
+ | { kind: 'polygon'; points: Array<{ id: string } & PlaneRecord<FigureMeasure>> }
98
+
99
+ interface FigureCavityRecord {
100
+ id: string
101
+ wall: Record<'<a>-' | '<a>+' | '<b>-' | '<b>+', FigureMeasure> // one per flat side of the outer section
102
+ start: 'open' | { closed: FigureMeasure } // thickness left at the start end
103
+ end: 'open' | { closed: FigureMeasure }
104
+ }
105
+ ```
106
+
107
+ `PlaneRecord<T>` holds the two plane axes by name, for example `{ x, z }` for a push along Y (§2).
108
+
109
+ **Q1 — extent as two ends, not a length and an anchor.** ADR-0100 §4.1 writes the push as `E(S, a, b)` and says the
110
+ origin is never recentred by a shape edit. Storing `start` and `end` says exactly that. A centred box is
111
+ `start = -h/2, end = h/2`. A box standing on its origin is `start = 0, end = h`. The box's height is `end - start`
112
+ and is shown as one value (ADR-0100 §3.1). When a length is edited, the edit command decides which end stays, and it
113
+ reads that choice from the part's current fastening (§7). The choice is not stored on the shape. A height that
114
+ follows the instance links both ends, for example `start = {from: instance.y, times: -0.5}` and
115
+ `end = {from: instance.y, times: 0.5}`, and the linking command writes that pair.
116
+
117
+ The part's record replaces its `<part>.shape` node and nothing else. The pose (`<part>.pose`), the place
118
+ (`<part>`), the appearance, the joints and the bindings keep their ids.
119
+
120
+ ## 2. The section
121
+
122
+ ### 2.1 Kinds
123
+
124
+ | kind | written | round when | first stage |
125
+ | ----------- | --------------------------------------------------------------------------------- | ----------- | ----------- |
126
+ | `rectangle` | size along each plane axis, centred on the part's origin; `corner` per plane axis | — | 1 |
127
+ | `ellipse` | radius along each plane axis, centred | radii equal | 1 |
128
+ | `polygon` | one closed loop of straight sides, points by id, not recentred | — | 2 |
129
+
130
+ A rectangle and an ellipse are never stored as a list of points (ADR-0100 §4.1). Turning a rectangle into points is
131
+ an explicit command, `convert-section`, which the author runs (「점으로 편집」). A polygon becomes a rectangle again only
132
+ when it is one: four points, axis-aligned, centred.
133
+
134
+ **Corners are per plane axis.** The designer's rule of 2026-09-22 (shape-dimension contract §1) is that every round
135
+ thing has per-axis dimensions. A corner therefore has a reach along each plane axis. Equal reaches make a round
136
+ corner, and unequal reaches an elliptic one. The screen shows one radius and writes it to both axes. The per-axis
137
+ value appears in the detail view only when the two differ. A corner rounded along one axis and not the other is
138
+ refused (`CORNER_ONE_AXIS`).
139
+
140
+ **Q4 — which curve is a corner.** The 2026-09-22 ruling also says a corner is the quadratic Bézier with the square
141
+ corner as its control point, which is what things-scene's `roundedRect` draws. That ruling was made so V2's
142
+ drawings would match. The new canon is free to choose again, and the choice matters for cavities. The inward offset
143
+ of a quarter circle by `t` is a quarter circle of `r - t`, so an even wall is exact. The inward offset of a Bézier
144
+ corner is not a Bézier. **Recommended: a quarter of the ellipse the two reaches span.** `figureSectionAreaOf` is
145
+ written that way now, and it is the one function to change if the Bézier is kept.
146
+
147
+ ### 2.2 The plane and the one written form
148
+
149
+ A section pushed along `n` lies across the two other local axes, `(u, v)`, chosen so that `(u, v, n)` is
150
+ right-handed:
151
+
152
+ | push along | plane axes (u, v) |
153
+ | ---------- | ----------------- |
154
+ | X | (Y, Z) |
155
+ | Y | (Z, X) |
156
+ | Z | (X, Y) |
157
+
158
+ **Q2 — points keep the axes' own names.** A point for a push along Y is written `{ id, x, z }`, not `{ u, v }`. A
159
+ reader of the JSON never has to know which axis is u, and today's drawn sections, which are X–Z pushed along Y, keep
160
+ their numbers. Only the winding test uses `(u, v)`.
161
+
162
+ The kernel keeps a drawn loop one way: counter-clockwise seen from `+n`, which means a positive signed area over
163
+ `(u, v)`. Seen from +Y, X runs right and Z runs down the page, as in the modeller's top view. Normalising reverses
164
+ the loop and keeps its first point first. It never moves, adds or removes a point, so every point and side keeps its
165
+ id (ADR-0100 §4.2).
166
+
167
+ ### 2.3 Validity (ADR-0100 §4.2)
168
+
169
+ `checkFigureSection(section, axis)` returns every fault by the point, side or field that has it. It returns an empty
170
+ list when the section is valid. Nothing is clamped, snapped, reordered or thinned to pass.
171
+
172
+ | code | fault |
173
+ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
174
+ | `NON_FINITE` | a coordinate, size, radius or corner that is missing or not a finite number |
175
+ | `BAD_POINT_ID`, `DUPLICATE_POINT_ID` | a point's name is not an identifier, or is used twice |
176
+ | `TOO_FEW_POINTS`, `TOO_MANY_POINTS` | fewer than 3 points, or more than `FIGURE_SECTION_POINT_LIMIT` |
177
+ | `ZERO_LENGTH_SIDE` | two points in a row at one place |
178
+ | `SIDES_FOLD` | a side runs back over the one before it |
179
+ | `SIDES_CROSS` | two sides pass through each other |
180
+ | `SIDES_TOUCH` | two sides that are not neighbours meet at a point, or run along each other, without crossing |
181
+ | `ZERO_AREA` | the loop encloses nothing |
182
+ | `SIZE_NOT_POSITIVE` | a rectangle's size or an ellipse's radius is 0 or less |
183
+ | `CORNER_NEGATIVE`, `CORNER_ONE_AXIS`, `CORNER_TOO_LARGE` | a corner reach below 0, on one axis only, or past half its side (`max` given) |
184
+
185
+ A point added on a side before it is moved is valid: sides may meet at a straight angle. This lets "add a point,
186
+ then drag it" be two steps.
187
+
188
+ ## 3. Cavities (ADR-0100 §4.3)
189
+
190
+ A cavity is `C = E(I, c, d)` inside `E(S, a, b)`, and the body is `E(S, a, b) \ C`. Its record does not store `I`,
191
+ `c` or `d`. It stores what the author sets:
192
+
193
+ - **`wall`**: how far in from each flat side of the outer section the inside begins. The screen shows one thickness
194
+ and writes it to all four sides. It shows the four sides separately only when the author asks for different ones
195
+ (ADR-0100 §3.3).
196
+ - **`start` / `end`**: `open`, or `{ closed: t }` for a thickness `t` left at that end. Whether an end is open is
197
+ written in the record. It is never inferred from two numbers happening to be equal. Open at both ends makes a
198
+ pipe, open at one end a container, and closed at both a sealed space. These are three results of one rule, not
199
+ three shapes. The floor thickness is the `closed` value, not a separate kind of shape.
200
+
201
+ `figureCavitySectionOf(outer, axis, wall)` gives the inside section:
202
+
203
+ - **Rectangle.** The size less the two walls on each axis. The inside is off-centre by half the difference when the
204
+ walls are uneven. Each corner reach goes in by the thicker of the two walls on its axis, `max(r - t, 0)`, which is
205
+ the rule the renderer draws today. With a round corner and even walls this is the exact inward offset. Otherwise it
206
+ is a declared rule, and it does not claim an even thickness around the bend. A corner that goes in to nothing on
207
+ one axis leaves the inside sharp.
208
+ - **Ellipse.** Each radius less the walls on its axis. This is not an inward offset of an ellipse and is not called
209
+ one. For a circle with even walls it is exact, which covers a pipe.
210
+ - **Polygon.** Refused (`SECTION_KIND`). A drawn loop's inside needs an inward offset with its own contract for
211
+ concave corners, narrow necks and regions that vanish or split. That is ADR-0100 stage 4. Until then the screen
212
+ shows 「이 윤곽에는 속 공간을 만들 수 없습니다」 with the reason, and offers no button.
213
+
214
+ Refused, never thinned: `WALL_NOT_POSITIVE` (with the side), `NO_ROOM_INSIDE`, `CAVITY_END_NOT_POSITIVE`,
215
+ `CAVITY_NO_LENGTH`.
216
+
217
+ **One cavity per part** in the first contract (`CAVITY_COUNT`). Two cavities raise questions about whether they
218
+ overlap and what wall lies between them, which nothing asks for yet. A part with two compartments is two parts, or a
219
+ divider fastened to the inside floor.
220
+
221
+ The body is one closed mesh. The renderer builds the outer walls, the inner walls, the rim and the closing faces as
222
+ one surface. It does not overlay a slab and a ring. Today's `hollowBoxXZ` does that (things-scene
223
+ `figure-geometry.ts:95-111`): the ring of walls stands on the full floor slab, so the walls' bottom and the slab's top lie
224
+ on each other inside the body.
225
+
226
+ A thing placed inside is a separate part with an `inside` relation (ADR-0100 §4.3). That relation is **not part of
227
+ this stage**. Until it has its own contract, a divider is fastened to `cavity-start:<id>` by an ordinary joint.
228
+
229
+ ## 4. Named features (ADR-0100 §4.1.1)
230
+
231
+ `figureExtrusionFeaturesOf(extrusion, cavities)` lists what a pushed section offers:
232
+
233
+ | id | kind | what it is |
234
+ | ---------------------------------------------- | --------------- | ----------------------------------------------------------------------------------- |
235
+ | `start-face`, `end-face` | plane | the two ends; `open: true` when the cavity reaches through, so only the rim is left |
236
+ | `side-face:<side>` | plane or curved | each side of the section pushed along the axis |
237
+ | `edge:start:<side>`, `edge:end:<side>` | edge | where a side meets an end |
238
+ | `edge:<point>` | edge | a drawn section's corner, along the axis |
239
+ | `edge:<a>±<b>±` | edge | a sharp rectangle's corner, along the axis |
240
+ | `cavity-wall:<cavity>:<side>` | plane or curved | the inside of each wall, facing into the cavity |
241
+ | `cavity-start:<cavity>`, `cavity-end:<cavity>` | plane | the face closing the cavity at that end, when it is closed |
242
+ | `centre` | point | the middle of the span, at the section's origin |
243
+
244
+ **Side ids.**
245
+
246
+ - **Rectangle.** Each side is named by the plane axis it faces along and the direction: `x-`, `x+`, `z-`, `z+` for
247
+ a push along Y. A rounded corner is named by the two sides it joins, for example `corner:x-z+`, and exists only
248
+ while the corner is rounded.
249
+ - **Ellipse.** One curved side, `round`.
250
+ - **Drawn section.** A side is named by **the pair of points it joins**, in name order: `a~b`. So the name does not
251
+ depend on the loop's direction or on its first point. Moving a point keeps every side. Adding a point on side
252
+ `c~a` removes that side and adds `c~d` and `a~d`; neither half inherits the old name. Removing a point removes its
253
+ two sides and adds the side that joins its neighbours. **Every split, merge or deletion is a topology change, and
254
+ it is reported, not resolved**: `figureSectionTopologyChange(before, after)` lists the sides kept, removed and
255
+ added (ADR-0100 §5).
256
+
257
+ **Q9 — what a point is called, and what a screen shows.** The prototype first numbered points by their place in the
258
+ loop. Deleting a point then renumbered the others, and the side the author picked as 「옆면 1–3」 in the editor showed
259
+ as 「옆면 1–2」 once applied. **Recommended:** a drawn point's id is `p<n>`, and a new point takes the highest `n` in
260
+ the loop plus one. Every screen shows `n`, so point 3 is 「3」 before and after any edit, and deleting 2 leaves 1, 3
261
+ and 4. The kernel still accepts any identifier; the rule is for the commands that make points.
262
+
263
+ **What a box's six faces become** (the modeller's current words):
264
+
265
+ | today (`FIGURE_FACES`) | box pushed along Y |
266
+ | ---------------------- | ------------------------------- |
267
+ | `bottom` / `top` | `start-face` / `end-face` |
268
+ | `left` / `right` | `side-face:x-` / `side-face:x+` |
269
+ | `back` / `front` | `side-face:z-` / `side-face:z+` |
270
+ | `centre` | `centre` |
271
+
272
+ This is a one-time rewrite of the joint and binding references. It is not an alias kept at runtime.
273
+
274
+ **Q3 — curved sides and the 22 joints that use them.** Today a cylinder offers six box faces, and four of them lie
275
+ on its curved side (`cylinder-shape@1` uses `centredFaces`). In the development database, **22 joint ends are
276
+ fastened to a cylinder's `left`, `right`, `front` or `back`** (§11). ADR-0100 §5 says a curved face is never taken
277
+ for a plane. **Recommended:** a curved side offers **tangent planes**, `tangent:<axis>±`, defined as the plane that
278
+ touches the curve at its extreme along that axis, with the normal along it. Fastening to one is a line contact, and
279
+ the screen says so (「곡면에 닿는 선으로 붙습니다」). The alternative is to allow only `centre` on round parts and to
280
+ re-author the 22.
281
+
282
+ ## 5. The other operations
283
+
284
+ ADR-0100 names six operations. Their contracts differ in how settled they are. **None of the four below is offered on
285
+ screen until its row in §9 says so.**
286
+
287
+ - **`revolve`**: a half-section in `(r, h)` about the part's axis, a full turn only in the first contract. It has
288
+ three profile kinds, each keeping its dimensions as they are named:
289
+ - `half-ellipse { radial, axial }` makes a sphere or a spheroid.
290
+ - `taper { start: {at, radius}, end: {at, radius} }` makes a cone or a frustum.
291
+ - `polygon` of `(r, h)` points with `r ≥ 0` makes a turned profile.
292
+
293
+ A cylinder is `extrude(ellipse)`, never a taper with equal radii. The taper allows equal radii for a moment while
294
+ it is edited, and does not become a cylinder on its own.
295
+
296
+ - **`loft`**: stations in order along the axis. Each station is a section of the same kind and the same point ids,
297
+ and the stations are matched by id. The last station may be a `point`. A **tetrahedron** is a triangle and one
298
+ point, and a pyramid is a rectangle and one point. There is no tetrahedron shape. The contract still needs: loop
299
+ correspondence when the kinds differ, twist between stations, the normal at a degenerate point, and
300
+ self-intersection between stations. So loft stays off until those are written.
301
+ - **`sweep`**: a section along a path. The path frame, twist, curvature limit and self-intersection are not
302
+ contracted. It stays off. This is the "curved shapes" item already ranked after render batching in the app status.
303
+ - **`assembly`**: several parts as one product structure, held by fixed joints. It keeps each child's meaning,
304
+ relations and material, and it is never a merged mesh. **Q5** asks whether the door frame, the one assembly in
305
+ ADR-0100's representative list, needs it. A door frame is also one `extrude` of a drawn Π section along Z. That is
306
+ one body with named inside faces, and it needs no assembly contract.
307
+
308
+ **Q6 — an ellipsoid with three different radii.** `sphere-shape@1` takes three radii. `revolve` makes only
309
+ spheroids, with the two radial radii equal. Three live figures use spheres (여의봉 and 작업자 by expression, 화성
310
+ 충방전기 at 85/85/85). **Recommended:** rewrite them as `revolve(half-ellipse)`. If an evaluated pair of radial radii
311
+ differs at any size in the declared range, the rewrite reports that asset and does not convert it. The ellipsoid
312
+ does not become a seventh operation without a new ADR.
313
+
314
+ ## 6. Numbers
315
+
316
+ | name | value | what it decides | status |
317
+ | ---------------------------- | ------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------- |
318
+ | `FIGURE_SECTION_TOLERANCE` | 1e-6 mm | whether two points coincide, a side has length, two sides touch; topology only | proposed |
319
+ | `FIGURE_SECTION_POINT_LIMIT` | 256 | points in one drawn loop | proposed, to be measured (below) |
320
+ | tessellation | `appearance.segments` as today | how many segments a curve is drawn with | unchanged; no segment choice on the first screen (ADR-0100 §7) |
321
+
322
+ The tolerance is not a snapping distance, not the magnet's pixel tolerance, and not a tolerance for comparing
323
+ drawings (ADR-0100 §4.2). The point limit is to be confirmed by measuring the validity check (O(n²) over the sides),
324
+ triangulation and a full re-evaluation of the figure at 64, 256 and 1024 points on the modeller's machine class.
325
+
326
+ ## 7. Commands
327
+
328
+ All of them go through the authoring session, which checks the model and revision, applies atomically, and undoes
329
+ in one step. The modeller, the AI and the templates use them and nothing else (ADR-0100 decision 6).
330
+
331
+ | command | what it changes |
332
+ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
333
+ | `edit-section { part, section, mapping? }` | the whole section, as one step: what the 2D editor's `적용` sends |
334
+ | `set-extrusion { part, length, keep: 'start' \| 'centre' \| 'end' }` | `end - start`; `keep` defaults to what the part's fastening holds |
335
+ | `set-section-corner { part, corner }` | the corner reaches of a rectangle |
336
+ | `set-cavity { part, cavity \| null, mapping? }` | the part's inside space: add, change, remove |
337
+ | `convert-section { part, to, mapping? }` | rectangle to points, points to rectangle (only when it is one), ellipse to rectangle |
338
+
339
+ `mapping` names, for each removed feature something refers to, the feature that takes its place, or `release`.
340
+ `release` unfastens the part and leaves it at its current rest pose, under ADR-0093 decision 7. A command without
341
+ the mapping it needs is refused with `FEATURE_MAPPING_REQUIRED` and the list of references. Nothing is mapped in
342
+ passing.
343
+
344
+ **The plan.** `planFigureShapeEdit(asset, command)` returns, without changing anything:
345
+
346
+ - the context (model, revision)
347
+ - the section's validity issues
348
+ - the topology change (§4)
349
+ - every reference to a removed feature, with its owner (joint, binding, occupancy contact, material, capability)
350
+ - the parts that will move, and by how much
351
+ - the clips that would be lost, which are refused under ADR-0099
352
+ - the evaluated result, for the preview
353
+
354
+ The preview and the apply use the same plan. A plan made against another revision is refused when applied.
355
+
356
+ ## 8. What an edit keeps (ADR-0100 §5)
357
+
358
+ - The part's id, origin and rest pose never change with its shape. A fastened child is evaluated again against the
359
+ changed feature, and the plan shows the move before it is applied.
360
+ - A dimension linked to something stays linked. Setting a linked value to a number is `unlink-dimension`, shown
361
+ first.
362
+ - State may change a length, never the topology. No state input may change the number of points or cavities, or
363
+ whether an end is open. The compiler refuses such a dependence under the operator's `state` policy.
364
+ - A reference is to a feature id, never to a side's index, a triangle or a point's place in a list.
365
+
366
+ ## 9. What is on when
367
+
368
+ | row | stage 1 (rectangle family) | stage 2 (drawn section) | stage 3 (product path) | later |
369
+ | --------------------------------------------------- | -------------------------- | ----------------------- | ---------------------- | -------------- |
370
+ | extrude · rectangle / ellipse | on | | | |
371
+ | corner (per axis) | on | | | |
372
+ | cavity on rectangle / ellipse | on | | | |
373
+ | extrude · polygon, 2D editor | | on | | |
374
+ | `convert-section` | rectangle ↔ points | on | | |
375
+ | revolve (half-ellipse, taper) | on, no editor beyond sizes | | | |
376
+ | revolve · polygon profile | | | | contract first |
377
+ | loft (point station: pyramid, tetrahedron) | | | on, after its contract | |
378
+ | loft · section to section | | | | contract first |
379
+ | sweep | | | | contract first |
380
+ | assembly | | | Q5 | |
381
+ | cavity on a drawn section | | | | stage 4 |
382
+ | AI grammar, templates, board, rewrite of all assets | | | on | |
383
+
384
+ ## 10. What changes where
385
+
386
+ Surveyed 2026-09-29 (read-only):
387
+
388
+ - **figure-model.** It gains the record and its compiler, the gate reading features, the commands above, and the
389
+ proposal grammar. It loses the eleven shape operators, `feature@1`'s provider switch (`graph.ts` 863–), the
390
+ provider table (`shape-provider.ts`), `FOLLOWS_NOW`, `create.ts`'s recipe table, and `FIGURE_FACES` as the words
391
+ for a fastening.
392
+ - **things-scene** (about 150 lines). The provider switch in `figure-geometry.ts:73-134` becomes one mesh builder per
393
+ operation. The geometry cache key (`keyOf`) becomes the record's content. The top view (`figure-top-view.ts:87-108`)
394
+ reads the section instead of the provider. There is no per-face picking today, so features need picking added,
395
+ not moved.
396
+ - **figure-ui** (about 220 lines in about a dozen tables). The palette (`palette.ts`, `figure-parts.ts`) moves to
397
+ starting shapes. The gizmo's `BOXES` and round-scaling tables (`gizmo.ts:33`, `:68-76`) move to the record's own
398
+ sizes. `links.ts` `KIND_OF_SHAPE` and `placeFacesOf` move to features. The icons, the material panel's `ROUND`,
399
+ maturity's copy of the geometry key and `ROUND_FOOTPRINT`, and `faceHighlightOf` also change. The face `<select>`
400
+ lists of six names become feature lists. The 2D section editor is new.
401
+ - **figure-service** (no shape code on the server). The proposal grammar comes from figure-model. The 101 recipes'
402
+ `build.mts` use `kind` primitives through `addFigurePrimitive`, so the recipes are rebuilt and the 123 generated
403
+ models regenerated.
404
+ - **operato-figure.** Two landing scripts push raw `box-shape@1` nodes. The maturity row "polygon/shape editing:
405
+ planned" changes.
406
+
407
+ ## 11. The assets to rewrite
408
+
409
+ The development database, read-only on 2026-09-29, has 58 live figures:
410
+
411
+ | operator | nodes | figures | becomes |
412
+ | ------------------ | ----- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
413
+ | `box-shape@1` | 257 | 47 | extrude · rectangle |
414
+ | `cylinder-shape@1` | 117 | 34 | extrude · ellipse |
415
+ | `rounded-box@2` | 52 | 11 | extrude · rectangle with corner per axis (in 7 one reach is a number and the other an expression; whether they differ is evaluated at the rewrite) |
416
+ | `rounded-box@1` | 25 | 8 | extrude · rectangle with corner |
417
+ | `cylinder-shape@2` | 10 | 3 | extrude · ellipse |
418
+ | `sphere-shape@1` | 5 | 3 | revolve · half-ellipse (Q6) |
419
+ | `hollow-box@2` | 2 | 2 | extrude · rectangle + cavity, end open |
420
+ | `polygon-shape@1` | 2 | 1 | extrude · polygon; point ids are given at the rewrite |
421
+
422
+ Joint ends on a cylinder's side faces: `left` 6, `right` 6, `front` 5, `back` 5 (Q3). Joint ends on a cylinder's
423
+ `bottom` 23 and `top` 26 become `start-face` and `end-face`. The 101 templates use box 177, cylinder 36, hollow-box
424
+ 10, portal 6, rounded-box 3 and frustum 2 steps, and are rebuilt from their `build.mts`.
425
+
426
+ ## 12. Questions for the designer
427
+
428
+ | | question | recommended |
429
+ | --- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
430
+ | Q1 | Store a push as its two ends, not a length and an anchor | two ends (§1) |
431
+ | Q2 | Points keep the axes' own names; `(u, v, n)` right-handed | yes (§2.2) |
432
+ | Q3 | Curved sides: tangent planes for the 22 joints, or `centre` only | tangent planes, said as line contact (§4) |
433
+ | Q4 | A corner's curve: elliptic quarter, or keep the Bézier of 2026-09-22 | elliptic quarter (§2.1) |
434
+ | Q5 | Door frame: one Π-section extrude, or an assembly | one extrude; assembly after (§5) |
435
+ | Q6 | Three-radius ellipsoid: rewrite as spheroid and report the rest, or keep it | rewrite and report (§5) |
436
+ | Q7 | One cavity per part in the first contract | yes (§3) |
437
+ | Q8 | Tolerance 1e-6 mm and 256 points, pending the measurement in §6 | yes |
438
+ | Q9 | A drawn point's id is `p<n>`, a new point takes the highest n + 1, and screens show n, never the point's place in the loop | yes (§4) |
439
+ | Q10 | The node limit, 1024 (raised from 512, provisionally), or a centred push written without its start | 1024; the shorthand changes the format (§0) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatiolab/figure-model",
3
- "version": "0.1.94",
3
+ "version": "0.2.0",
4
4
  "description": "The figure format (figure-asset-1): a kernel that evaluates a figure from its declared relations, the authoring commands a modeller and an AI share, the release gate and the score.",
5
5
  "keywords": [
6
6
  "figure",