@things-factory/figure-ui 10.1.204 → 10.1.206

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 (116) hide show
  1. package/client/modeller/canvas.ts +91 -9
  2. package/client/modeller/editor-styles.ts +23 -22
  3. package/client/modeller/editor.ts +217 -184
  4. package/client/modeller/fasten-form.ts +130 -12
  5. package/client/modeller/figure-fasten-dialog.ts +45 -11
  6. package/client/modeller/figure-fasten-drawing.ts +233 -12
  7. package/client/modeller/figure-links.ts +2 -2
  8. package/client/modeller/figure-preview.ts +6 -6
  9. package/client/modeller/figure-section-editor.ts +6 -6
  10. package/client/modeller/figure-slot-layout-panel.ts +1 -1
  11. package/client/modeller/gizmo.ts +22 -1
  12. package/client/modeller/keys.ts +10 -1
  13. package/client/modeller/links.ts +79 -46
  14. package/client/modeller/palette.ts +1 -1
  15. package/client/modeller/product-session.ts +16 -4
  16. package/client/modeller/refusal-teller.ts +3 -25
  17. package/client/modeller/refusals.ts +19 -26
  18. package/client/modeller/section-drawing.ts +7 -7
  19. package/client/modeller/size-drag.ts +112 -0
  20. package/client/modeller/slot-layout-host.ts +3 -3
  21. package/client/modeller/stage-model.ts +6 -9
  22. package/client/pages/figure-modeller-page.ts +1 -4
  23. package/dist-client/modeller/canvas.d.ts +24 -3
  24. package/dist-client/modeller/canvas.js +96 -11
  25. package/dist-client/modeller/canvas.js.map +1 -1
  26. package/dist-client/modeller/editor-styles.js +23 -22
  27. package/dist-client/modeller/editor-styles.js.map +1 -1
  28. package/dist-client/modeller/editor.d.ts +35 -35
  29. package/dist-client/modeller/editor.js +221 -193
  30. package/dist-client/modeller/editor.js.map +1 -1
  31. package/dist-client/modeller/fasten-form.d.ts +46 -4
  32. package/dist-client/modeller/fasten-form.js +120 -11
  33. package/dist-client/modeller/fasten-form.js.map +1 -1
  34. package/dist-client/modeller/figure-fasten-dialog.d.ts +18 -4
  35. package/dist-client/modeller/figure-fasten-dialog.js +49 -12
  36. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  37. package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
  38. package/dist-client/modeller/figure-fasten-drawing.js +235 -11
  39. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  40. package/dist-client/modeller/figure-links.d.ts +1 -1
  41. package/dist-client/modeller/figure-links.js +2 -2
  42. package/dist-client/modeller/figure-links.js.map +1 -1
  43. package/dist-client/modeller/figure-preview.js +4 -4
  44. package/dist-client/modeller/figure-preview.js.map +1 -1
  45. package/dist-client/modeller/figure-section-editor.d.ts +3 -3
  46. package/dist-client/modeller/figure-section-editor.js +4 -4
  47. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  48. package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
  49. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
  50. package/dist-client/modeller/gizmo.d.ts +18 -1
  51. package/dist-client/modeller/gizmo.js +19 -0
  52. package/dist-client/modeller/gizmo.js.map +1 -1
  53. package/dist-client/modeller/keys.d.ts +1 -1
  54. package/dist-client/modeller/keys.js +6 -0
  55. package/dist-client/modeller/keys.js.map +1 -1
  56. package/dist-client/modeller/links.d.ts +30 -24
  57. package/dist-client/modeller/links.js +67 -42
  58. package/dist-client/modeller/links.js.map +1 -1
  59. package/dist-client/modeller/palette.js +1 -1
  60. package/dist-client/modeller/palette.js.map +1 -1
  61. package/dist-client/modeller/product-session.d.ts +15 -13
  62. package/dist-client/modeller/product-session.js +15 -5
  63. package/dist-client/modeller/product-session.js.map +1 -1
  64. package/dist-client/modeller/refusal-teller.d.ts +1 -12
  65. package/dist-client/modeller/refusal-teller.js +3 -22
  66. package/dist-client/modeller/refusal-teller.js.map +1 -1
  67. package/dist-client/modeller/refusals.d.ts +6 -14
  68. package/dist-client/modeller/refusals.js +16 -27
  69. package/dist-client/modeller/refusals.js.map +1 -1
  70. package/dist-client/modeller/section-drawing.d.ts +3 -4
  71. package/dist-client/modeller/section-drawing.js +10 -9
  72. package/dist-client/modeller/section-drawing.js.map +1 -1
  73. package/dist-client/modeller/size-drag.d.ts +38 -0
  74. package/dist-client/modeller/size-drag.js +85 -0
  75. package/dist-client/modeller/size-drag.js.map +1 -0
  76. package/dist-client/modeller/slot-layout-host.d.ts +1 -3
  77. package/dist-client/modeller/slot-layout-host.js +3 -3
  78. package/dist-client/modeller/slot-layout-host.js.map +1 -1
  79. package/dist-client/modeller/stage-model.d.ts +5 -7
  80. package/dist-client/modeller/stage-model.js +6 -6
  81. package/dist-client/modeller/stage-model.js.map +1 -1
  82. package/dist-client/pages/figure-modeller-page.d.ts +0 -2
  83. package/dist-client/pages/figure-modeller-page.js +1 -6
  84. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  85. package/dist-client/route.d.ts +1 -1
  86. package/dist-client/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +7 -7
  89. package/test/align.test.ts +7 -11
  90. package/test/browser/acceptance-page.js +11 -1
  91. package/test/browser/editing.check.mjs +17 -13
  92. package/test/browser/links-page.js +2 -2
  93. package/test/browser/links.check.mjs +124 -69
  94. package/test/browser/modeller-page.js +30 -18
  95. package/test/browser/modeller-revision.check.mjs +39 -0
  96. package/test/browser/modeller.check.mjs +77 -26
  97. package/test/browser/parts.check.mjs +49 -0
  98. package/test/browser/screen-page.js +0 -2
  99. package/test/browser/screen.check.mjs +11 -13
  100. package/test/browser/section.check.mjs +29 -2
  101. package/test/fasten-form.test.ts +102 -1
  102. package/test/gizmo.test.ts +25 -16
  103. package/test/keys.test.ts +11 -0
  104. package/test/links.test.ts +46 -36
  105. package/test/refusals.test.ts +17 -21
  106. package/test/resize-with-placement.test.ts +14 -13
  107. package/test/revision.test.ts +3 -2
  108. package/test/section-editor.test.ts +9 -10
  109. package/test/size-drag.test.ts +114 -0
  110. package/test/slot-layout-host.test.ts +13 -8
  111. package/translations/en.json +32 -34
  112. package/translations/es.json +32 -34
  113. package/translations/ja.json +32 -34
  114. package/translations/ko.json +32 -34
  115. package/translations/ms.json +32 -34
  116. package/translations/zh.json +32 -34
@@ -11,6 +11,12 @@ export function keyActionOf(e, selected) {
11
11
  return e.shiftKey ? 'redo' : 'undo';
12
12
  if (mod && key === 'y')
13
13
  return 'redo';
14
+ if (mod && key === 'c')
15
+ return selected ? 'copy' : 'hold';
16
+ if (mod && key === 'v')
17
+ return e.shiftKey ? 'hold' : 'paste';
18
+ if (mod && key === 'x')
19
+ return 'hold';
14
20
  if (!selected)
15
21
  return null;
16
22
  if (mod && key === 'd')
@@ -1 +1 @@
1
- {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../client/modeller/keys.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,GAA6B,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAA;AAkBxF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;AAEvD,MAAM,UAAU,WAAW,CAAC,CAAU,EAAE,QAAgB;IACtD,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/G,IAAI,CAAC,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACzB,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAA;IAC/B,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAC3D,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,MAAM,CAAA;IACrC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC1B,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,WAAW,CAAA;IAC1C,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,WAAW,CAAC;QAAE,OAAO,QAAQ,CAAA;IACtE,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAA;IAC/C,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,UAAU,CAAC,GAAG,CAAC,CAAA;IAChF,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["/*\n * The modeller's keys: undo and redo on Ctrl+Z / Ctrl+Y, and what a person reaches for next. Read apart\n * from the element so the rule is tested without a browser.\n *\n * Typing is never taken: a key pressed in a field belongs to the field -- Backspace in a name box deletes a letter,\n * not the part.\n */\nexport type KeyAction = 'undo' | 'redo' | 'duplicate' | 'remove' | 'deselect' | GizmoKey\n\n/** The gizmo's modes on W, E and R, as the scene has them. */\nexport type GizmoKey = 'translate' | 'rotate' | 'scale'\nconst GIZMO_KEYS: Record<string, GizmoKey> = { w: 'translate', e: 'rotate', r: 'scale' }\n\nexport interface KeyLike {\n key: string\n ctrlKey?: boolean\n metaKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n /** The element the key was pressed in -- the first of the event's composed path. */\n target?: { tagName?: string; isContentEditable?: boolean } | null\n /**\n * Pressed with the canvas focused. The scene takes W, E and R there itself; they are passed on only from elsewhere\n * -- a part picked in the parts list left the focus there, and the gizmo could not be had at all (2026-09-25).\n * Passing a key the scene already took would toggle the gizmo off again.\n */\n inScene?: boolean\n}\n\nconst TYPING = new Set(['INPUT', 'SELECT', 'TEXTAREA'])\n\nexport function keyActionOf(e: KeyLike, selected: string): KeyAction | null {\n if (e.target && (TYPING.has(String(e.target.tagName).toUpperCase()) || e.target.isContentEditable)) return null\n if (e.altKey) return null\n const mod = !!(e.ctrlKey || e.metaKey)\n const key = e.key.toLowerCase()\n if (mod && key === 'z') return e.shiftKey ? 'redo' : 'undo'\n if (mod && key === 'y') return 'redo'\n if (!selected) return null\n if (mod && key === 'd') return 'duplicate'\n if (!mod && (key === 'delete' || key === 'backspace')) return 'remove'\n if (!mod && key === 'escape') return 'deselect'\n if (!mod && !e.shiftKey && !e.inScene && GIZMO_KEYS[key]) return GIZMO_KEYS[key]\n return null\n}\n"]}
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../client/modeller/keys.ts"],"names":[],"mappings":"AAiBA,MAAM,UAAU,GAA6B,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAA;AAkBxF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;AAEvD,MAAM,UAAU,WAAW,CAAC,CAAU,EAAE,QAAgB;IACtD,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/G,IAAI,CAAC,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACzB,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAA;IAC/B,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAC3D,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,MAAM,CAAA;IACrC,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IACzD,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;IAC5D,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,MAAM,CAAA;IACrC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC1B,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,WAAW,CAAA;IAC1C,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,WAAW,CAAC;QAAE,OAAO,QAAQ,CAAA;IACtE,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAA;IAC/C,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,UAAU,CAAC,GAAG,CAAC,CAAA;IAChF,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["/*\n * The modeller's keys: undo and redo on Ctrl+Z / Ctrl+Y, and what a person reaches for next. Read apart\n * from the element so the rule is tested without a browser.\n *\n * Typing is never taken: a key pressed in a field belongs to the field -- Backspace in a name box deletes a letter,\n * not the part.\n */\n/*\n * `copy` · `paste` are the modeller's own: a part copied and pasted is the kernel's duplicate, with a name of its own.\n * Left to the scene, Ctrl+C / Ctrl+V copied the canvas object with the part's name -- two objects of one name, the copy\n * moving the original (user 2026-10-21). `hold` is a key kept from the scene that the modeller has no use for: the\n * scene's cut and bulk paste act on canvas objects alone and leave the figure out of step with them.\n */\nexport type KeyAction = 'undo' | 'redo' | 'duplicate' | 'remove' | 'deselect' | 'copy' | 'paste' | 'hold' | GizmoKey\n\n/** The gizmo's modes on W, E and R, as the scene has them. */\nexport type GizmoKey = 'translate' | 'rotate' | 'scale'\nconst GIZMO_KEYS: Record<string, GizmoKey> = { w: 'translate', e: 'rotate', r: 'scale' }\n\nexport interface KeyLike {\n key: string\n ctrlKey?: boolean\n metaKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n /** The element the key was pressed in -- the first of the event's composed path. */\n target?: { tagName?: string; isContentEditable?: boolean } | null\n /**\n * Pressed with the canvas focused. The scene takes W, E and R there itself; they are passed on only from elsewhere\n * -- a part picked in the parts list left the focus there, and the gizmo could not be had at all (2026-09-25).\n * Passing a key the scene already took would toggle the gizmo off again.\n */\n inScene?: boolean\n}\n\nconst TYPING = new Set(['INPUT', 'SELECT', 'TEXTAREA'])\n\nexport function keyActionOf(e: KeyLike, selected: string): KeyAction | null {\n if (e.target && (TYPING.has(String(e.target.tagName).toUpperCase()) || e.target.isContentEditable)) return null\n if (e.altKey) return null\n const mod = !!(e.ctrlKey || e.metaKey)\n const key = e.key.toLowerCase()\n if (mod && key === 'z') return e.shiftKey ? 'redo' : 'undo'\n if (mod && key === 'y') return 'redo'\n if (mod && key === 'c') return selected ? 'copy' : 'hold'\n if (mod && key === 'v') return e.shiftKey ? 'hold' : 'paste'\n if (mod && key === 'x') return 'hold'\n if (!selected) return null\n if (mod && key === 'd') return 'duplicate'\n if (!mod && (key === 'delete' || key === 'backspace')) return 'remove'\n if (!mod && key === 'escape') return 'deselect'\n if (!mod && !e.shiftKey && !e.inScene && GIZMO_KEYS[key]) return GIZMO_KEYS[key]\n return null\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { figureJointAsFixed } from '@hatiolab/figure-model';
2
- import type { FigureFollow, FigureSizeRange } from '@hatiolab/figure-model';
3
- import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureFace, FigureFacing, FigureJoint } from '@hatiolab/figure-model';
2
+ import type { FigureFollow, FigureInstanceSizeRange } from '@hatiolab/figure-model';
3
+ import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureFace, FigureFacing, FigureJoint, FigureSurfacePlace } from '@hatiolab/figure-model';
4
4
  import { type FigureRevision } from './revision.js';
5
5
  import { type JointAxisVector } from './joint-axis.js';
6
6
  export type Axis = 'x' | 'y' | 'z';
@@ -14,6 +14,11 @@ export type Follow = FigureFollow;
14
14
  */
15
15
  /** What one measure of a part is worked out from, read back from the graph (figure-model's reader, one copy). */
16
16
  export declare function followOf(asset: FigureAsset, part: string, axis: Axis): Follow;
17
+ /**
18
+ * The parts whose size follows the figure's size on some axis, in the model's order: the ones that change when the
19
+ * figure is resized on a drawing. Read by figure-model's own follow (`figureFollowOf`), not worked out again here.
20
+ */
21
+ export declare function sizeFollowersOf(asset: FigureAsset): string[];
17
22
  /** Whether the asset declares an instance size on an axis — only then can a dimension follow it. */
18
23
  export declare const hasInstanceSize: (asset: FigureAsset, axis: Axis) => boolean;
19
24
  /** The part this one is fastened to, or `null`. The mounting plane counts as fastened (the model names no part). */
@@ -91,6 +96,11 @@ export declare function measureNow(revision: FigureRevision, part: string, axis:
91
96
  * written (a portal's opening sides as well as its outer faces). A sphere has none.
92
97
  */
93
98
  export declare function facesOf(asset: FigureAsset, part: string): string[];
99
+ /** The curved surface a part offers to be fastened to at a point: a cylinder's side or a sphere's round (ADR-0140 3.9). */
100
+ export declare function surfaceOf(asset: FigureAsset, part: string): {
101
+ form: 'around' | 'toward';
102
+ axis: Axis;
103
+ } | undefined;
94
104
  /** The axis a face of a part faces along, from the same definitions. */
95
105
  export declare function faceAxisOf(asset: FigureAsset, part: string, face: string): Axis | undefined;
96
106
  /** Whether `attach` can take this part: any part whose shape has a flat face. */
@@ -105,7 +115,7 @@ export declare function rowAxesOf(asset: FigureAsset, part: string): Axis[];
105
115
  * Why a part cannot be made into a row here, in the terms `rowAxesOf` refuses it: said, not hidden (ADR-0097 §5.5).
106
116
  * Nothing when it can, or when it already is a row (that row is shown where it stands).
107
117
  */
108
- export declare function rowBlockOf(asset: FigureAsset, part: string): 'unfastened' | 'on-plane' | 'holds-parts' | 'at-origin' | undefined;
118
+ export declare function rowBlockOf(asset: FigureAsset, part: string): 'unfastened' | 'on-plane' | 'holds-parts' | 'at-origin' | 'on-surface' | undefined;
109
119
  /** The row a part is made into, as its fastening's record keeps it. */
110
120
  export declare const rowOf: (asset: FigureAsset, part: string) => {
111
121
  axis: import("@hatiolab/figure-model").FigureJointAxis;
@@ -187,6 +197,8 @@ export declare function fasteningOf(revision: FigureRevision, part: string): {
187
197
  alignMm: Partial<Record<Axis, number>>;
188
198
  /** Placed by a part's origin (ADR-0107 decision 4): no face to fasten by, and the form does not write it. */
189
199
  atOrigin?: true;
200
+ /** On a point of the target's curved surface (ADR-0140 3.9), as the record says it. */
201
+ surface?: FigureSurfacePlace;
190
202
  } | null;
191
203
  /**
192
204
  * The `gap` an attach command writes from the form: a worked-out gap as it was read, else the number, else none. Read as
@@ -215,7 +227,7 @@ export declare function placeKeptOf(revision: FigureRevision, part: string, targ
215
227
  export declare function alignOf(asset: FigureAsset, target: {
216
228
  part: string;
217
229
  face: string;
218
- } | 'plane', how: Partial<Record<Axis, Alignment>>, mm: Partial<Record<Axis, number>>, inputs?: Partial<Record<Axis, string>>): Partial<Record<Axis, "min" | "max" | {
230
+ } | 'plane', how: Partial<Record<Axis, Alignment>>, mm: Partial<Record<Axis, number>>, inputs?: Partial<Record<Axis, string>>): Partial<Record<Axis, "max" | "min" | {
219
231
  mm: number | {
220
232
  from: {
221
233
  of: "instance";
@@ -346,32 +358,27 @@ export declare const holdsThings: (asset: FigureAsset) => boolean;
346
358
  /** What `accepts` a person typed: names split on commas, trimmed, blanks and repeats dropped. */
347
359
  export declare const acceptsOf: (typed: string) => string[];
348
360
  /**
349
- * The sizes a board may give this figure: the range it declares now, and the range measured afresh -- the default
350
- * (1 mm to ten times the authored size) narrowed only where the figure cannot follow, by figure-model's own functions.
351
- * A range measured when a part filled what the others left (ROBOT_ARM: Y from 1,854 mm) stays narrow after the part is
352
- * made to grow with the figure; measuring again gives the figure its whole range back. Nothing when no size is declared.
361
+ * The sizes a board may give this figure, per axis: the ends its author narrowed, and the rest as far as its parts
362
+ * allow, computed by figure-model (`figureInstanceSizeRangeOf`, ADR-0087 decision 6) -- the screen does not measure
363
+ * on its own. Nothing when no size is declared.
353
364
  */
354
- export declare function instanceRangeOf(asset: FigureAsset): {
355
- declared: FigureSizeRange;
356
- measured: FigureSizeRange;
357
- } | undefined;
365
+ export declare function instanceRangeOf(asset: FigureAsset): Record<Axis, FigureInstanceSizeRange> | undefined;
358
366
  /**
359
- * The size range to declare for a figure that has none yet: its parts' extent now as the size it is authored at, and
360
- * as wide as its parts allow around it (the same measure 「다시 재기」 uses). Undefined when the figure already has a
361
- * size or has nothing to measure. Without a declared size a board cannot resize the figure, and no part is offered
362
- * to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
367
+ * The size to declare for a figure that has none yet: its parts' extent now, the size it is authored at. Undefined when
368
+ * the figure already has a size or has nothing to measure. Without a declared size a board cannot resize the figure,
369
+ * and no part is offered to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
363
370
  */
364
- export declare function instanceRangeToDeclare(asset: FigureAsset): {
365
- range: FigureSizeRange;
371
+ export declare function instanceSizeToDeclare(asset: FigureAsset): {
366
372
  at: Record<Axis, number>;
367
373
  } | undefined;
368
- /** Whether measuring again would change the declared range (to a micrometre). */
369
374
  /**
370
- * The declared range with one end typed by the author: the sizes a figure may be placed at are a claim about the
371
- * product (a gantry spans metres, not 30 cm to 29 m), and measuring only ever widens them. Nothing when the typed
372
- * value is not a number; whether the figure holds there is the command's to say.
375
+ * The ends the author narrowed, with one end typed or cleared: a number narrows that end, an empty field opens it
376
+ * again to what the parts allow. Whether the figure holds there is the command's to say.
373
377
  */
374
- export declare function rangeWith(declared: FigureSizeRange, axis: Axis, bound: 'min' | 'max', value: number): FigureSizeRange | undefined;
378
+ export declare function rangeWith(range: Record<Axis, FigureInstanceSizeRange>, axis: Axis, bound: 'min' | 'max', value: number): Partial<Record<Axis, {
379
+ min?: number;
380
+ max?: number;
381
+ }>>;
375
382
  /**
376
383
  * For a figure that grows by one ratio: how many of its lengths would follow the ratio, and which it leaves with their
377
384
  * own number (not those that follow another length -- they grow with it). Nothing for a figure of another sizing.
@@ -382,7 +389,6 @@ export declare function proportionToFollow(asset: FigureAsset): {
382
389
  } | undefined;
383
390
  /** A value of the figure as it stands, by its graph reference. */
384
391
  export declare function valueNow(revision: FigureRevision, ref: string | undefined): number | undefined;
385
- export declare function rangeDiffers(a: FigureSizeRange, b: FigureSizeRange): boolean;
386
392
  /**
387
393
  * Whether the figure is a mechanism -- a part turns about a joint. Such a figure should grow by one ratio (chief
388
394
  * architect's ruling 2026-09-24); the modeller proposes it, the author declares it.
@@ -1,4 +1,4 @@
1
- import { figureShapeFaces, figurePartsExtentOf, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion } from '@hatiolab/figure-model';
1
+ import { figureShapeFaces, figurePartsExtentOf, figureFollowOf, proposeFigureProportion, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeRangeOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion, figureSurfaceOf } from '@hatiolab/figure-model';
2
2
  import { startingShapeKindOf } from './starting-shape.js';
3
3
  import { boxOf } from './align.js';
4
4
  import { graphValuesOf } from './revision.js';
@@ -14,6 +14,21 @@ const modelOf = (asset) => asset.document.model;
14
14
  export function followOf(asset, part, axis) {
15
15
  return figureFollowOf(asset, part, axis);
16
16
  }
17
+ /**
18
+ * The parts whose size follows the figure's size on some axis, in the model's order: the ones that change when the
19
+ * figure is resized on a drawing. Read by figure-model's own follow (`figureFollowOf`), not worked out again here.
20
+ */
21
+ export function sizeFollowersOf(asset) {
22
+ const parts = modelOf(asset).nodes.filter(n => n.op === 'place@1').map(n => n.id);
23
+ return parts.filter(part => AXES.some(axis => {
24
+ try {
25
+ return figureFollowOf(asset, part, axis).of === 'instance';
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }));
31
+ }
17
32
  /** Whether the asset declares an instance size on an axis — only then can a dimension follow it. */
18
33
  export const hasInstanceSize = (asset, axis) => modelOf(asset).inputs.some(i => i.id === `size.${axis}` && i.role === 'design');
19
34
  /** The part this one is fastened to, or `null`. The mounting plane counts as fastened (the model names no part). */
@@ -202,7 +217,13 @@ export function measureNow(revision, part, axis) {
202
217
  */
203
218
  export function facesOf(asset, part) {
204
219
  const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`);
205
- return inScreenOrder(Object.keys((shape && figureShapeFaces(shape)) ?? {}));
220
+ const faces = (shape && figureShapeFaces(shape)) ?? {};
221
+ /* A cylinder's curved side is no face: a part is fastened to a point on it (ADR-0140 3.9, `surfaceOf`). */
222
+ return inScreenOrder(Object.keys(faces).filter(f => faces[f].contact !== 'line'));
223
+ }
224
+ /** The curved surface a part offers to be fastened to at a point: a cylinder's side or a sphere's round (ADR-0140 3.9). */
225
+ export function surfaceOf(asset, part) {
226
+ return figureSurfaceOf(asset, part);
206
227
  }
207
228
  /** Faces in the order the screen lists them: the six outer ones left to front, then the rest as the shape gives them. */
208
229
  const SCREEN_ORDER = ['left', 'right', 'bottom', 'top', 'back', 'front'];
@@ -217,7 +238,7 @@ export function faceAxisOf(asset, part, face) {
217
238
  }
218
239
  /** Whether `attach` can take this part: any part whose shape has a flat face. */
219
240
  export function canFasten(asset, part) {
220
- return facesOf(asset, part).length > 0;
241
+ return facesOf(asset, part).length > 0 || !!surfaceOf(asset, part);
221
242
  }
222
243
  const OUTER = { x: ['left', 'right'], y: ['bottom', 'top'], z: ['back', 'front'] };
223
244
  /**
@@ -253,6 +274,9 @@ export function rowBlockOf(asset, part) {
253
274
  return 'on-plane';
254
275
  if (joints.some(o => 'part' in o.body0 && o.body0.part === part))
255
276
  return 'holds-parts';
277
+ /* On a point of a curved surface a row has no face to run across (ADR-0140 3.9): the kernel refuses it. */
278
+ if ('surface' in j.body0)
279
+ return 'on-surface';
256
280
  if (!('face' in j.body0) || 'at' in j.body1)
257
281
  return 'at-origin';
258
282
  return undefined;
@@ -328,8 +352,22 @@ export function fasteningOf(revision, part) {
328
352
  return null;
329
353
  const toPlane = 'plane' in record.body0;
330
354
  const target = 'part' in record.body0 ? record.body0.part : '';
355
+ const surface = 'surface' in record.body0 ? record.body0.surface : undefined;
331
356
  const targetFace = 'face' in record.body0 ? record.body0.face : 'top';
332
- const atOrigin = !('plane' in record.body0) && (!('face' in record.body0) || 'at' in record.body1);
357
+ const atOrigin = !('plane' in record.body0) && ((!('face' in record.body0) && !surface) || 'at' in record.body1);
358
+ if (surface)
359
+ return {
360
+ toPlane,
361
+ target,
362
+ targetFace,
363
+ facing: record.origin?.facing ?? 'meet',
364
+ gap: typeof record.origin?.gap === 'number' ? record.origin.gap : 0,
365
+ ...(record.origin?.gap !== undefined && typeof record.origin.gap !== 'number' ? { gapKept: record.origin.gap } : {}),
366
+ align: {},
367
+ alignMm: {},
368
+ surface,
369
+ ...(atOrigin ? { atOrigin: true } : {})
370
+ };
333
371
  /* Where across the face: as the record says it, and where it says nothing, where the part stands now. */
334
372
  const kept = placeKeptOf(revision, part, toPlane ? 'plane' : { part: target, face: targetFace });
335
373
  for (const [axis, how] of Object.entries(record.origin?.align ?? {})) {
@@ -591,55 +629,45 @@ export const holdsThings = (asset) => (asset.document.capabilities.names ?? []).
591
629
  /** What `accepts` a person typed: names split on commas, trimmed, blanks and repeats dropped. */
592
630
  export const acceptsOf = (typed) => [...new Set(typed.split(',').map(one => one.trim()).filter(Boolean))];
593
631
  /**
594
- * The sizes a board may give this figure: the range it declares now, and the range measured afresh -- the default
595
- * (1 mm to ten times the authored size) narrowed only where the figure cannot follow, by figure-model's own functions.
596
- * A range measured when a part filled what the others left (ROBOT_ARM: Y from 1,854 mm) stays narrow after the part is
597
- * made to grow with the figure; measuring again gives the figure its whole range back. Nothing when no size is declared.
632
+ * The sizes a board may give this figure, per axis: the ends its author narrowed, and the rest as far as its parts
633
+ * allow, computed by figure-model (`figureInstanceSizeRangeOf`, ADR-0087 decision 6) -- the screen does not measure
634
+ * on its own. Nothing when no size is declared.
598
635
  */
599
636
  export function instanceRangeOf(asset) {
600
- const inputs = modelOf(asset).inputs;
601
- const declared = {};
602
- const authored = {};
603
- for (const axis of AXES) {
604
- const input = inputs.find(i => i.id === `size.${axis}` && i.role === 'design');
605
- const at = asset.designInputs[`size.${axis}`];
606
- if (!input || typeof at !== 'number')
607
- return undefined;
608
- declared[axis] = { min: input.min ?? 0, max: input.max ?? Infinity };
609
- authored[axis] = at;
610
- }
611
- const start = defaultInstanceSizeRange(authored);
612
- const measured = foldInstanceSizeRange(start, figureInstanceSizeLimitsOf(asset, start)).range;
613
- return { declared, measured };
637
+ const range = figureInstanceSizeRangeOf(asset);
638
+ return AXES.every(axis => range[axis]) ? range : undefined;
614
639
  }
615
640
  /**
616
- * The size range to declare for a figure that has none yet: its parts' extent now as the size it is authored at, and
617
- * as wide as its parts allow around it (the same measure 「다시 재기」 uses). Undefined when the figure already has a
618
- * size or has nothing to measure. Without a declared size a board cannot resize the figure, and no part is offered
619
- * to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
641
+ * The size to declare for a figure that has none yet: its parts' extent now, the size it is authored at. Undefined when
642
+ * the figure already has a size or has nothing to measure. Without a declared size a board cannot resize the figure,
643
+ * and no part is offered to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
620
644
  */
621
- export function instanceRangeToDeclare(asset) {
645
+ export function instanceSizeToDeclare(asset) {
622
646
  if (instanceRangeOf(asset) || !asset.appearance.length)
623
647
  return undefined;
624
648
  /* The parts as they rest -- not the room their joints swing them through, which declared a door 8208 mm every way. */
625
649
  const at = figurePartsExtentOf(asset);
626
650
  if (AXES.some(axis => !(at[axis] > 0)))
627
651
  return undefined;
628
- const start = defaultInstanceSizeRange(at);
629
- return { range: foldInstanceSizeRange(start, figureInstanceSizeLimitsOf(asset, start)).range, at };
652
+ return { at };
630
653
  }
631
- /** Whether measuring again would change the declared range (to a micrometre). */
632
654
  /**
633
- * The declared range with one end typed by the author: the sizes a figure may be placed at are a claim about the
634
- * product (a gantry spans metres, not 30 cm to 29 m), and measuring only ever widens them. Nothing when the typed
635
- * value is not a number; whether the figure holds there is the command's to say.
655
+ * The ends the author narrowed, with one end typed or cleared: a number narrows that end, an empty field opens it
656
+ * again to what the parts allow. Whether the figure holds there is the command's to say.
636
657
  */
637
- export function rangeWith(declared, axis, bound, value) {
638
- if (!Number.isFinite(value))
639
- return undefined;
640
- const next = structuredClone(declared);
641
- next[axis][bound] = value;
642
- return next;
658
+ export function rangeWith(range, axis, bound, value) {
659
+ const out = {};
660
+ for (const a of AXES) {
661
+ const ends = {};
662
+ for (const b of ['min', 'max']) {
663
+ const typed = a === axis && b === bound;
664
+ if (typed ? Number.isFinite(value) : range[a].narrowed[b])
665
+ ends[b] = typed ? value : range[a][b];
666
+ }
667
+ if (Object.keys(ends).length)
668
+ out[a] = ends;
669
+ }
670
+ return out;
643
671
  }
644
672
  /**
645
673
  * For a figure that grows by one ratio: how many of its lengths would follow the ratio, and which it leaves with their
@@ -668,9 +696,6 @@ export function valueNow(revision, ref) {
668
696
  return undefined;
669
697
  }
670
698
  }
671
- export function rangeDiffers(a, b) {
672
- return AXES.some(axis => Math.abs(a[axis].min - b[axis].min) > 1e-3 || Math.abs(a[axis].max - b[axis].max) > 1e-3);
673
- }
674
699
  /**
675
700
  * Whether the figure is a mechanism -- a part turns about a joint. Such a figure should grow by one ratio (chief
676
701
  * architect's ruling 2026-09-24); the modeller proposes it, the author declares it.