@things-factory/figure-ui 10.1.75 → 10.1.76

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 (235) hide show
  1. package/client/figure-assistant.ts +3 -3
  2. package/client/graphql/{v3-store.ts → store.ts} +6 -6
  3. package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
  4. package/client/modeller/{v3-align.ts → align.ts} +14 -14
  5. package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
  6. package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
  7. package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
  8. package/client/modeller/{v3-editor.ts → editor.ts} +117 -117
  9. package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
  10. package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
  11. package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
  12. package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
  13. package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
  14. package/client/modeller/figure-history-panel.ts +1 -1
  15. package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
  16. package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
  17. package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
  18. package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
  19. package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
  20. package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
  21. package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
  22. package/client/modeller/gate-findings.ts +2 -2
  23. package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
  24. package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
  25. package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
  26. package/client/modeller/{v3-links.ts → links.ts} +59 -59
  27. package/client/modeller/{v3-live.ts → live.ts} +9 -9
  28. package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
  29. package/client/modeller/modeller-route.ts +1 -1
  30. package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
  31. package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
  32. package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
  33. package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
  34. package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
  35. package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
  36. package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
  37. package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
  38. package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
  39. package/client/pages/figure-list-page.ts +4 -4
  40. package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
  41. package/client/route.ts +1 -5
  42. package/dist-client/figure-assistant.js +3 -3
  43. package/dist-client/figure-assistant.js.map +1 -1
  44. package/dist-client/graphql/store.d.ts +3 -0
  45. package/dist-client/graphql/store.js +39 -0
  46. package/dist-client/graphql/store.js.map +1 -0
  47. package/dist-client/modeller/ai-proposal.d.ts +51 -0
  48. package/dist-client/modeller/ai-proposal.js +64 -0
  49. package/dist-client/modeller/ai-proposal.js.map +1 -0
  50. package/dist-client/modeller/align.d.ts +30 -0
  51. package/dist-client/modeller/align.js +75 -0
  52. package/dist-client/modeller/align.js.map +1 -0
  53. package/dist-client/modeller/canvas.d.ts +72 -0
  54. package/dist-client/modeller/canvas.js +379 -0
  55. package/dist-client/modeller/canvas.js.map +1 -0
  56. package/dist-client/modeller/clips.d.ts +39 -0
  57. package/dist-client/modeller/clips.js +58 -0
  58. package/dist-client/modeller/clips.js.map +1 -0
  59. package/dist-client/modeller/controls.d.ts +28 -0
  60. package/dist-client/modeller/controls.js +28 -0
  61. package/dist-client/modeller/controls.js.map +1 -0
  62. package/dist-client/modeller/editor.d.ts +203 -0
  63. package/dist-client/modeller/editor.js +1526 -0
  64. package/dist-client/modeller/editor.js.map +1 -0
  65. package/dist-client/modeller/fields.d.ts +45 -0
  66. package/dist-client/modeller/fields.js +83 -0
  67. package/dist-client/modeller/fields.js.map +1 -0
  68. package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
  69. package/dist-client/modeller/figure-capability-panel.js +141 -0
  70. package/dist-client/modeller/figure-capability-panel.js.map +1 -0
  71. package/dist-client/modeller/figure-clips.d.ts +31 -0
  72. package/dist-client/modeller/figure-clips.js +268 -0
  73. package/dist-client/modeller/figure-clips.js.map +1 -0
  74. package/dist-client/modeller/figure-controls.d.ts +36 -0
  75. package/dist-client/modeller/figure-controls.js +226 -0
  76. package/dist-client/modeller/figure-controls.js.map +1 -0
  77. package/dist-client/modeller/figure-facts.d.ts +35 -0
  78. package/dist-client/modeller/figure-facts.js +55 -0
  79. package/dist-client/modeller/figure-facts.js.map +1 -0
  80. package/dist-client/modeller/figure-first-save.d.ts +20 -0
  81. package/dist-client/modeller/figure-first-save.js +118 -0
  82. package/dist-client/modeller/figure-first-save.js.map +1 -0
  83. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  84. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  85. package/dist-client/modeller/figure-links.d.ts +77 -0
  86. package/dist-client/modeller/figure-links.js +649 -0
  87. package/dist-client/modeller/figure-links.js.map +1 -0
  88. package/dist-client/modeller/figure-material-panel.d.ts +39 -0
  89. package/dist-client/modeller/figure-material-panel.js +323 -0
  90. package/dist-client/modeller/figure-material-panel.js.map +1 -0
  91. package/dist-client/modeller/figure-parts.d.ts +69 -0
  92. package/dist-client/modeller/figure-parts.js +559 -0
  93. package/dist-client/modeller/figure-parts.js.map +1 -0
  94. package/dist-client/modeller/figure-preview.d.ts +34 -0
  95. package/dist-client/modeller/figure-preview.js +346 -0
  96. package/dist-client/modeller/figure-preview.js.map +1 -0
  97. package/dist-client/modeller/figure-properties.d.ts +45 -0
  98. package/dist-client/modeller/figure-properties.js +239 -0
  99. package/dist-client/modeller/figure-properties.js.map +1 -0
  100. package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
  101. package/dist-client/modeller/figure-recipe-catalog.js +399 -0
  102. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
  103. package/dist-client/modeller/figure-verdict.d.ts +64 -0
  104. package/dist-client/modeller/figure-verdict.js +787 -0
  105. package/dist-client/modeller/figure-verdict.js.map +1 -0
  106. package/dist-client/modeller/first-save.d.ts +17 -0
  107. package/dist-client/modeller/first-save.js +26 -0
  108. package/dist-client/modeller/first-save.js.map +1 -0
  109. package/dist-client/modeller/gate-findings.d.ts +2 -2
  110. package/dist-client/modeller/gate-findings.js +2 -2
  111. package/dist-client/modeller/gate-findings.js.map +1 -1
  112. package/dist-client/modeller/gizmo.d.ts +72 -0
  113. package/dist-client/modeller/gizmo.js +111 -0
  114. package/dist-client/modeller/gizmo.js.map +1 -0
  115. package/dist-client/modeller/help.d.ts +18 -0
  116. package/dist-client/modeller/help.js +37 -0
  117. package/dist-client/modeller/help.js.map +1 -0
  118. package/dist-client/modeller/joint-drag.d.ts +19 -0
  119. package/dist-client/modeller/joint-drag.js +97 -0
  120. package/dist-client/modeller/joint-drag.js.map +1 -0
  121. package/dist-client/modeller/joint-fan.d.ts +9 -0
  122. package/dist-client/modeller/joint-fan.js +276 -0
  123. package/dist-client/modeller/joint-fan.js.map +1 -0
  124. package/dist-client/modeller/keys.d.ts +22 -0
  125. package/dist-client/modeller/keys.js +26 -0
  126. package/dist-client/modeller/keys.js.map +1 -0
  127. package/dist-client/modeller/links.d.ts +267 -0
  128. package/dist-client/modeller/links.js +480 -0
  129. package/dist-client/modeller/links.js.map +1 -0
  130. package/dist-client/modeller/live.d.ts +37 -0
  131. package/dist-client/modeller/live.js +68 -0
  132. package/dist-client/modeller/live.js.map +1 -0
  133. package/dist-client/modeller/maturity.d.ts +120 -0
  134. package/dist-client/modeller/maturity.js +313 -0
  135. package/dist-client/modeller/maturity.js.map +1 -0
  136. package/dist-client/modeller/modeller-route.d.ts +1 -1
  137. package/dist-client/modeller/modeller-route.js +1 -1
  138. package/dist-client/modeller/modeller-route.js.map +1 -1
  139. package/dist-client/modeller/occupancy.d.ts +18 -0
  140. package/dist-client/modeller/occupancy.js +24 -0
  141. package/dist-client/modeller/occupancy.js.map +1 -0
  142. package/dist-client/modeller/palette.d.ts +7 -0
  143. package/dist-client/modeller/palette.js +46 -0
  144. package/dist-client/modeller/palette.js.map +1 -0
  145. package/dist-client/modeller/preview-model.d.ts +47 -0
  146. package/dist-client/modeller/preview-model.js +68 -0
  147. package/dist-client/modeller/preview-model.js.map +1 -0
  148. package/dist-client/modeller/product-session.d.ts +510 -0
  149. package/dist-client/modeller/product-session.js +157 -0
  150. package/dist-client/modeller/product-session.js.map +1 -0
  151. package/dist-client/modeller/recipe-source.d.ts +8 -0
  152. package/dist-client/modeller/recipe-source.js +14 -0
  153. package/dist-client/modeller/recipe-source.js.map +1 -0
  154. package/dist-client/modeller/recipes.d.ts +37 -0
  155. package/dist-client/modeller/recipes.js +103 -0
  156. package/dist-client/modeller/recipes.js.map +1 -0
  157. package/dist-client/modeller/refusals.d.ts +68 -0
  158. package/dist-client/modeller/refusals.js +142 -0
  159. package/dist-client/modeller/refusals.js.map +1 -0
  160. package/dist-client/modeller/renderer.d.ts +40 -0
  161. package/dist-client/modeller/renderer.js +73 -0
  162. package/dist-client/modeller/renderer.js.map +1 -0
  163. package/dist-client/modeller/stage-model.d.ts +39 -0
  164. package/dist-client/modeller/stage-model.js +147 -0
  165. package/dist-client/modeller/stage-model.js.map +1 -0
  166. package/dist-client/modeller/thumbnail.d.ts +3 -0
  167. package/dist-client/modeller/thumbnail.js +75 -0
  168. package/dist-client/modeller/thumbnail.js.map +1 -0
  169. package/dist-client/pages/figure-list-page.d.ts +1 -1
  170. package/dist-client/pages/figure-list-page.js +4 -4
  171. package/dist-client/pages/figure-list-page.js.map +1 -1
  172. package/dist-client/pages/figure-modeller-page.d.ts +102 -0
  173. package/dist-client/pages/figure-modeller-page.js +548 -0
  174. package/dist-client/pages/figure-modeller-page.js.map +1 -0
  175. package/dist-client/route.d.ts +1 -1
  176. package/dist-client/route.js +1 -5
  177. package/dist-client/route.js.map +1 -1
  178. package/dist-client/tsconfig.tsbuildinfo +1 -1
  179. package/docs/product-cutover.md +25 -0
  180. package/package.json +5 -5
  181. package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
  182. package/test/{v3-align.test.ts → align.test.ts} +8 -8
  183. package/test/browser/modeller-page.js +30 -30
  184. package/test/browser/modeller.check.mjs +14 -14
  185. package/test/browser/serve.mjs +1 -1
  186. package/test/browser/thumbnail-page.js +6 -6
  187. package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
  188. package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
  189. package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
  190. package/test/figure-assistant-surface.test.ts +3 -3
  191. package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
  192. package/test/fixtures/joint-column.json +2 -2
  193. package/test/fixtures/joint-roller.json +2 -2
  194. package/test/fixtures/robot-arm-6dof.json +2 -2
  195. package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
  196. package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
  197. package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
  198. package/test/joints.ts +6 -6
  199. package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
  200. package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
  201. package/test/{v3-links.test.ts → links.test.ts} +201 -201
  202. package/test/{v3-live.test.ts → live.test.ts} +18 -18
  203. package/test/{v3-material.test.ts → material.test.ts} +12 -12
  204. package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
  205. package/test/modeller-route.test.ts +5 -6
  206. package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
  207. package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +5 -5
  208. package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
  209. package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
  210. package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
  211. package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
  212. package/test/{v3-recipes.test.ts → recipes.test.ts} +9 -9
  213. package/test/{v3-refusals.test.ts → refusals.test.ts} +43 -43
  214. package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
  215. package/test/renderer.test.ts +24 -0
  216. package/test/resolve-ts-siblings.mjs +1 -1
  217. package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
  218. package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
  219. package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
  220. package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
  221. package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
  222. package/test/{v3-view.test.ts → view.test.ts} +2 -2
  223. package/things-factory.config.js +2 -3
  224. package/translations/en.json +2 -2
  225. package/translations/ja.json +2 -2
  226. package/translations/ko.json +2 -2
  227. package/translations/ms.json +2 -2
  228. package/translations/zh.json +2 -2
  229. package/docs/v3-product-cutover.md +0 -25
  230. package/test/v3-renderer.test.ts +0 -24
  231. /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
  232. /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
  233. /package/client/modeller/{v3-help.ts → help.ts} +0 -0
  234. /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
  235. /package/client/modeller/{v3-refusals.ts → refusals.ts} +0 -0
@@ -0,0 +1,8 @@
1
+ import { type RecipeTemplate } from './recipes.js';
2
+ /**
3
+ * Where the modeller gets its recipe templates: the server's static files (ADR-0095, amended 2026-09-26). The one place
4
+ * they are read -- the rules in `recipes.ts` stay free of the network so they are tested without it.
5
+ */
6
+ export declare function availableRecipes(lang: string): Promise<RecipeTemplate[]>;
7
+ /** One template's guide, or '' while none is written -- the screen then shows no guide and no word about one. */
8
+ export declare function recipeGuide(name: string, lang: string): Promise<string>;
@@ -0,0 +1,14 @@
1
+ import { fetchRecipeGuide, fetchRecipeTemplates } from '../graphql/index.js';
2
+ import { templatesOf } from './recipes.js';
3
+ /**
4
+ * Where the modeller gets its recipe templates: the server's static files (ADR-0095, amended 2026-09-26). The one place
5
+ * they are read -- the rules in `recipes.ts` stay free of the network so they are tested without it.
6
+ */
7
+ export async function availableRecipes(lang) {
8
+ return templatesOf(await fetchRecipeTemplates(lang));
9
+ }
10
+ /** One template's guide, or '' while none is written -- the screen then shows no guide and no word about one. */
11
+ export function recipeGuide(name, lang) {
12
+ return fetchRecipeGuide(name, lang);
13
+ }
14
+ //# sourceMappingURL=recipe-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipe-source.js","sourceRoot":"","sources":["../../client/modeller/recipe-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAuB,MAAM,cAAc,CAAA;AAE/D;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,OAAO,WAAW,CAAC,MAAM,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAA;AACtD,CAAC;AAED,iHAAiH;AACjH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY;IACpD,OAAO,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AACrC,CAAC","sourcesContent":["import { fetchRecipeGuide, fetchRecipeTemplates } from '../graphql/index.js'\nimport { templatesOf, type RecipeTemplate } from './recipes.js'\n\n/**\n * Where the modeller gets its recipe templates: the server's static files (ADR-0095, amended 2026-09-26). The one place\n * they are read -- the rules in `recipes.ts` stay free of the network so they are tested without it.\n */\nexport async function availableRecipes(lang: string): Promise<RecipeTemplate[]> {\n return templatesOf(await fetchRecipeTemplates(lang))\n}\n\n/** One template's guide, or '' while none is written -- the screen then shows no guide and no word about one. */\nexport function recipeGuide(name: string, lang: string): Promise<string> {\n return fetchRecipeGuide(name, lang)\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import type { FigureRecipe, FigureRecipeParam, FigureRecipeValue } from '@hatiolab/figure-model';
2
+ import type { RecipeTemplateRow } from '../graphql/index.js';
3
+ /** A template in the catalog: the server's row, its recipe parsed. */
4
+ export interface RecipeTemplate extends Omit<RecipeTemplateRow, 'source'> {
5
+ recipe: FigureRecipe;
6
+ }
7
+ /** The server's rows as templates. A row that is not a recipe is left out rather than breaking the catalog. */
8
+ export declare function templatesOf(rows: ReadonlyArray<RecipeTemplateRow>): RecipeTemplate[];
9
+ /** A part the recipe makes, as the catalog names it: its own name, the placeholder left off. */
10
+ export declare const recipePartName: (part: string) => string;
11
+ /**
12
+ * How a template works, in lines the catalog shows -- worked out from its steps (figure-model `describeFigureRecipe`), each
13
+ * a translation key and its words.
14
+ */
15
+ export declare function recipeWorkings(recipe: FigureRecipe): Array<{
16
+ key: string;
17
+ words: Record<string, string | number>;
18
+ }>;
19
+ /** A recipe's words in the language on screen: that language, else English, else the first it has. */
20
+ export declare function recipeWord(words: Record<string, string> | undefined, language: string): string;
21
+ /** Whether a recipe can be put in now: one for a part needs a part chosen. */
22
+ export declare function recipeReady(recipe: Pick<FigureRecipe, 'target'>, selected: string): boolean;
23
+ /**
24
+ * A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a
25
+ * person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.
26
+ */
27
+ export declare function recipeValueOf(param: FigureRecipeParam, typed: string | boolean): FigureRecipeValue;
28
+ /** What a field shows for a value: a ratio as a percentage. */
29
+ export declare function recipeFieldOf(param: FigureRecipeParam, value: FigureRecipeValue): string;
30
+ /** The templates a category and words find: a word matches the title, the description, the name or a tag. */
31
+ export declare function recipesMatching<T extends {
32
+ category: string;
33
+ title: string;
34
+ description: string;
35
+ name: string;
36
+ tags: string[];
37
+ }>(templates: readonly T[], category: string, query: string): T[];
@@ -0,0 +1,103 @@
1
+ import { describeFigureRecipe } from '@hatiolab/figure-model';
2
+ /*
3
+ * The recipe section's rules (ADR-0095), kept apart from the element so they can be tested without a browser.
4
+ */
5
+ /** The server's rows as templates. A row that is not a recipe is left out rather than breaking the catalog. */
6
+ export function templatesOf(rows) {
7
+ const out = [];
8
+ for (const { source, ...row } of rows) {
9
+ try {
10
+ const recipe = JSON.parse(source);
11
+ if (recipe && typeof recipe.name === 'string' && Array.isArray(recipe.steps))
12
+ out.push({ ...row, recipe });
13
+ }
14
+ catch {
15
+ /* not a recipe */
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+ /** A part the recipe makes, as the catalog names it: its own name, the placeholder left off. */
21
+ export const recipePartName = (part) => part.replace(/^\{id\}-/, '');
22
+ /**
23
+ * How a template works, in lines the catalog shows -- worked out from its steps (figure-model `describeFigureRecipe`), each
24
+ * a translation key and its words.
25
+ */
26
+ export function recipeWorkings(recipe) {
27
+ const d = describeFigureRecipe(recipe);
28
+ const lines = [];
29
+ for (const m of d.moves)
30
+ lines.push(m.follows
31
+ ? { key: 'figure.text.recipe-moves-with', words: { part: recipePartName(m.part), leader: recipePartName(m.follows) } }
32
+ : { key: m.kind === 'slide' ? 'figure.text.recipe-slides' : 'figure.text.recipe-turns', words: { part: recipePartName(m.part) } });
33
+ if (d.controls.length)
34
+ lines.push({ key: 'figure.text.recipe-controls', words: { count: d.controls.length } });
35
+ if (d.shownByControl.length)
36
+ lines.push({ key: 'figure.text.recipe-shown-by-control', words: { parts: d.shownByControl.map(recipePartName).join(', ') } });
37
+ for (const row of d.rows)
38
+ lines.push({ key: 'figure.text.recipe-row', words: { part: recipePartName(row) } });
39
+ return lines;
40
+ }
41
+ /** A recipe's words in the language on screen: that language, else English, else the first it has. */
42
+ export function recipeWord(words, language) {
43
+ if (!words)
44
+ return '';
45
+ return words[language] ?? words[language.split('-')[0]] ?? words.en ?? Object.values(words)[0] ?? '';
46
+ }
47
+ /** Whether a recipe can be put in now: one for a part needs a part chosen. */
48
+ export function recipeReady(recipe, selected) {
49
+ return recipe.target === 'figure' || !!selected;
50
+ }
51
+ /**
52
+ * A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a
53
+ * person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.
54
+ */
55
+ export function recipeValueOf(param, typed) {
56
+ if (param.kind === 'flag')
57
+ return typed === true || typed === 'true';
58
+ if (param.kind === 'ratio' || param.kind === 'mm') {
59
+ const n = typeof typed === 'string' && typed.trim() !== '' ? Number(typed) : NaN;
60
+ if (!Number.isFinite(n))
61
+ return param.default;
62
+ return param.kind === 'ratio' ? Math.round(n * 1e4) / 1e6 : n;
63
+ }
64
+ const option = (param.options ?? []).find(o => String(o) === String(typed));
65
+ return option ?? param.default;
66
+ }
67
+ /** What a field shows for a value: a ratio as a percentage. */
68
+ export function recipeFieldOf(param, value) {
69
+ if (param.kind === 'ratio' && typeof value === 'number')
70
+ return String(Math.round(value * 1e6) / 1e4);
71
+ return String(value);
72
+ }
73
+ /** The templates a category and words find: a word matches the title, the description, the name or a tag. */
74
+ export function recipesMatching(templates, category, query) {
75
+ const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
76
+ const found = templates.filter(t => {
77
+ if (category && t.category !== category)
78
+ return false;
79
+ const text = [t.title, t.description, t.name, ...t.tags].join(' ').toLowerCase();
80
+ return words.every(w => text.includes(w));
81
+ });
82
+ if (!words.length)
83
+ return found;
84
+ /*
85
+ What the search names comes first: a title that is the words, then one that starts with them, then a title word that
86
+ does, then the rest in their order. 「다리」 listed 사다리 before 다리 넷 (walk 1, 2026-09-26).
87
+ */
88
+ const rank = (t) => {
89
+ const title = t.title.toLowerCase();
90
+ const query = words.join(' ');
91
+ if (title === query)
92
+ return 0;
93
+ if (title.startsWith(query))
94
+ return 1;
95
+ if (words.every(w => title.split(/\s+/).some(part => part.startsWith(w))))
96
+ return 2;
97
+ if (words.every(w => title.includes(w)))
98
+ return 3;
99
+ return 4;
100
+ };
101
+ return found.map((t, i) => ({ t, i, r: rank(t) })).sort((a, b) => a.r - b.r || a.i - b.i).map(x => x.t);
102
+ }
103
+ //# sourceMappingURL=recipes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../client/modeller/recipes.ts"],"names":[],"mappings":"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","sourcesContent":["import { describeFigureRecipe } from '@hatiolab/figure-model'\nimport type { FigureRecipe, FigureRecipeParam, FigureRecipeValue } from '@hatiolab/figure-model'\n\nimport type { RecipeTemplateRow } from '../graphql/index.js'\n\n/** A template in the catalog: the server's row, its recipe parsed. */\nexport interface RecipeTemplate extends Omit<RecipeTemplateRow, 'source'> {\n recipe: FigureRecipe\n}\n\n/*\n * The recipe section's rules (ADR-0095), kept apart from the element so they can be tested without a browser.\n */\n\n/** The server's rows as templates. A row that is not a recipe is left out rather than breaking the catalog. */\nexport function templatesOf(rows: ReadonlyArray<RecipeTemplateRow>): RecipeTemplate[] {\n const out: RecipeTemplate[] = []\n for (const { source, ...row } of rows) {\n try {\n const recipe = JSON.parse(source)\n if (recipe && typeof recipe.name === 'string' && Array.isArray(recipe.steps)) out.push({ ...row, recipe })\n } catch {\n /* not a recipe */\n }\n }\n return out\n}\n\n/** A part the recipe makes, as the catalog names it: its own name, the placeholder left off. */\nexport const recipePartName = (part: string) => part.replace(/^\\{id\\}-/, '')\n\n/**\n * How a template works, in lines the catalog shows -- worked out from its steps (figure-model `describeFigureRecipe`), each\n * a translation key and its words.\n */\nexport function recipeWorkings(recipe: FigureRecipe): Array<{ key: string; words: Record<string, string | number> }> {\n const d = describeFigureRecipe(recipe)\n const lines: Array<{ key: string; words: Record<string, string | number> }> = []\n for (const m of d.moves)\n lines.push(\n m.follows\n ? { key: 'figure.text.recipe-moves-with', words: { part: recipePartName(m.part), leader: recipePartName(m.follows) } }\n : { key: m.kind === 'slide' ? 'figure.text.recipe-slides' : 'figure.text.recipe-turns', words: { part: recipePartName(m.part) } }\n )\n if (d.controls.length) lines.push({ key: 'figure.text.recipe-controls', words: { count: d.controls.length } })\n if (d.shownByControl.length) lines.push({ key: 'figure.text.recipe-shown-by-control', words: { parts: d.shownByControl.map(recipePartName).join(', ') } })\n for (const row of d.rows) lines.push({ key: 'figure.text.recipe-row', words: { part: recipePartName(row) } })\n return lines\n}\n\n/** A recipe's words in the language on screen: that language, else English, else the first it has. */\nexport function recipeWord(words: Record<string, string> | undefined, language: string): string {\n if (!words) return ''\n return words[language] ?? words[language.split('-')[0]!] ?? words.en ?? Object.values(words)[0] ?? ''\n}\n\n/** Whether a recipe can be put in now: one for a part needs a part chosen. */\nexport function recipeReady(recipe: Pick<FigureRecipe, 'target'>, selected: string): boolean {\n return recipe.target === 'figure' || !!selected\n}\n\n/**\n * A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a\n * person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.\n */\nexport function recipeValueOf(param: FigureRecipeParam, typed: string | boolean): FigureRecipeValue {\n if (param.kind === 'flag') return typed === true || typed === 'true'\n if (param.kind === 'ratio' || param.kind === 'mm') {\n const n = typeof typed === 'string' && typed.trim() !== '' ? Number(typed) : NaN\n if (!Number.isFinite(n)) return param.default\n return param.kind === 'ratio' ? Math.round(n * 1e4) / 1e6 : n\n }\n const option = (param.options ?? []).find(o => String(o) === String(typed))\n return option ?? param.default\n}\n\n/** What a field shows for a value: a ratio as a percentage. */\nexport function recipeFieldOf(param: FigureRecipeParam, value: FigureRecipeValue): string {\n if (param.kind === 'ratio' && typeof value === 'number') return String(Math.round(value * 1e6) / 1e4)\n return String(value)\n}\n\n\n/** The templates a category and words find: a word matches the title, the description, the name or a tag. */\nexport function recipesMatching<T extends { category: string; title: string; description: string; name: string; tags: string[] }>(templates: readonly T[], category: string, query: string): T[] {\n const words = query.trim().toLowerCase().split(/\\s+/).filter(Boolean)\n const found = templates.filter(t => {\n if (category && t.category !== category) return false\n const text = [t.title, t.description, t.name, ...t.tags].join(' ').toLowerCase()\n return words.every(w => text.includes(w))\n })\n if (!words.length) return found\n /*\n What the search names comes first: a title that is the words, then one that starts with them, then a title word that\n does, then the rest in their order. 「다리」 listed 사다리 before 다리 넷 (walk 1, 2026-09-26).\n */\n const rank = (t: T) => {\n const title = t.title.toLowerCase()\n const query = words.join(' ')\n if (title === query) return 0\n if (title.startsWith(query)) return 1\n if (words.every(w => title.split(/\\s+/).some(part => part.startsWith(w)))) return 2\n if (words.every(w => title.includes(w))) return 3\n return 4\n }\n return found.map((t, i) => ({ t, i, r: rank(t) })).sort((a, b) => a.r - b.r || a.i - b.i).map(x => x.t)\n}\n"]}
@@ -0,0 +1,68 @@
1
+ type Say = (key: string, params: Record<string, string>) => string;
2
+ /** The refusal in words, or nothing for any other refusal. */
3
+ export declare function sizeRangeRefusalOf(error: unknown, say: Say): string | null;
4
+ /**
5
+ * A link refused because the size range it would leave cannot hold: measuring again cannot help (the link is not there
6
+ * to measure), so the author is offered the same link that narrows the range to what the figure then allows.
7
+ */
8
+ export declare function narrowingRetryOf<A extends {
9
+ kind: string;
10
+ }>(action: A, error: unknown, say: Say): {
11
+ message: string;
12
+ label: string;
13
+ retry: A & {
14
+ narrowRange: true;
15
+ };
16
+ } | null;
17
+ /**
18
+ * A placement refused because a part is fastened to the mounting plane by a face the new plane does not take (a bottom
19
+ * on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
20
+ */
21
+ export declare function placementRefusalOf(error: unknown, say: Say): string | null;
22
+ /** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
23
+ export declare function inDeclaredRoom(error: unknown): boolean;
24
+ /**
25
+ * Why a part cannot be deleted, in the author's words. Since the chief architect's ruling of 2026-09-27 a part that
26
+ * holds others, leads a motion or is followed goes, and what depended on it is let go of (`removalNoticeOf`); what is
27
+ * left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.
28
+ */
29
+ export declare function partRefusalOf(error: unknown, part: string, say: Say): string | null;
30
+ /** What removing a part let go of (figure-model's `RemovalNotice`), said in one line, or nothing when it let go of nothing. */
31
+ export declare function removalNoticeOf(notice: {
32
+ released: Array<{
33
+ part: string;
34
+ control?: string;
35
+ }>;
36
+ fixed: Array<{
37
+ part: string;
38
+ }>;
39
+ controls: Array<{
40
+ part: string;
41
+ control: string;
42
+ min: number;
43
+ max: number;
44
+ }>;
45
+ mountingFace?: boolean;
46
+ bindings?: Array<{
47
+ id: string;
48
+ label?: string;
49
+ }>;
50
+ } | undefined, say: Say): string | null;
51
+ /**
52
+ * A recipe refused at one of its steps: which step, and why -- the particular sentence when there is one (a size
53
+ * range, a value's range, a placement), otherwise the kernel's code. The kernel says 「steps[2]: attach: …」.
54
+ */
55
+ export declare function recipeRefusalOf(error: unknown, say: Say): string | null;
56
+ /**
57
+ * A refusal no sentence above knows, said as a refusal with its code -- never the kernel's English on the screen (the
58
+ * duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」, 2026-09-27). The figure's node limit
59
+ * is said with its number.
60
+ */
61
+ export declare function editRefusalOf(error: unknown, say: Say): string;
62
+ /**
63
+ * A value outside the range its input declares, with the range and the value given -- the kernel says
64
+ * 「size.x: 6021100 outside [300,1200]」. The figure's own size is named by its axis; any other input by its range only.
65
+ * The repeat pitch's refusal shares the code and has its own words (`figure.text.repeat-pitch-too-small`).
66
+ */
67
+ export declare function inputRangeRefusalOf(error: unknown, say: Say): string | null;
68
+ export {};
@@ -0,0 +1,142 @@
1
+ /*
2
+ * A command refused because the figure could not be drawn at an end of its declared size range, in the author's words
3
+ * with the numbers and where to put it right -- not the kernel's English (user, 2026-09-24: 「what is this warning
4
+ * message means?」). Kept apart from the element so it is tested without a browser.
5
+ */
6
+ /** The refusal in words, or nothing for any other refusal. */
7
+ export function sizeRangeRefusalOf(error, say) {
8
+ if (error?.code !== 'INSTANCE_SIZE_RANGE')
9
+ return null;
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ const found = /size\.([xyz]):\s*(below|above) ([\d.]+) mm [^(;]*(?:\(([^)]+)\))?;/.exec(message);
12
+ if (!found)
13
+ return null;
14
+ const [, axis, side, mm, part] = found;
15
+ return say(side === 'below' ? 'figure.text.size-range-too-small' : 'figure.text.size-range-too-large', {
16
+ axis: axis.toUpperCase(),
17
+ mm: Number(mm).toLocaleString(undefined, { maximumFractionDigits: 1 }),
18
+ part: part ?? ''
19
+ });
20
+ }
21
+ /**
22
+ * A link refused because the size range it would leave cannot hold: measuring again cannot help (the link is not there
23
+ * to measure), so the author is offered the same link that narrows the range to what the figure then allows.
24
+ */
25
+ export function narrowingRetryOf(action, error, say) {
26
+ if (action.kind !== 'link-dimension' || error?.code !== 'INSTANCE_SIZE_RANGE')
27
+ return null;
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ const found = /size\.([xyz]):\s*(below|above) ([\d.]+) mm [^(;]*(?:\(([^)]+)\))?;/.exec(message);
30
+ if (!found)
31
+ return null;
32
+ const [, axis, side, raw, part] = found;
33
+ const mm = Number(raw).toLocaleString(undefined, { maximumFractionDigits: 1 });
34
+ const words = { axis: axis.toUpperCase(), mm, part: part ?? '' };
35
+ return {
36
+ message: say(side === 'below' ? 'figure.text.link-needs-smaller-range' : 'figure.text.link-needs-larger-range', words),
37
+ label: say(side === 'below' ? 'figure.button.narrow-range-from' : 'figure.button.narrow-range-to', words),
38
+ retry: { ...action, narrowRange: true }
39
+ };
40
+ }
41
+ /**
42
+ * A placement refused because a part is fastened to the mounting plane by a face the new plane does not take (a bottom
43
+ * on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
44
+ */
45
+ export function placementRefusalOf(error, say) {
46
+ if (error?.code !== 'PLACEMENT_PLANE')
47
+ return null;
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ const found = /: (.+?) (?:is|are) fastened to the mounting plane by a face an? (\w+) does not take \(([^)]+)\)/.exec(message);
50
+ if (!found)
51
+ return null;
52
+ return say('figure.text.placement-plane-refused', { parts: found[1], placement: say('figure.label.placement-' + found[2], {}), faces: found[3] });
53
+ }
54
+ /** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
55
+ export function inDeclaredRoom(error) {
56
+ return error?.code === 'PART_REFERENCED' && /is in the space the figure declares/.test(error instanceof Error ? error.message : String(error));
57
+ }
58
+ /**
59
+ * Why a part cannot be deleted, in the author's words. Since the chief architect's ruling of 2026-09-27 a part that
60
+ * holds others, leads a motion or is followed goes, and what depended on it is let go of (`removalNoticeOf`); what is
61
+ * left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.
62
+ */
63
+ export function partRefusalOf(error, part, say) {
64
+ const code = error?.code;
65
+ if (code !== 'PART_REFERENCED' && code !== 'PART_ROW')
66
+ return null;
67
+ /* figure-model's sentence after its code and path: 「PART_ROW: bed: roller is a row laid along bed; …」. */
68
+ const message = (error instanceof Error ? error.message : String(error)).replace(/^[A-Z_]+: [^:]+: /, '');
69
+ if (/is in the space the figure declares/.test(message))
70
+ return say('figure.text.part-in-declared-space', { part });
71
+ const along = /^(.+?) (?:is a row|are rows) laid along /.exec(message);
72
+ if (along)
73
+ return say('figure.text.part-row-along', { part, rows: along[1] });
74
+ const carried = /^(.+?) hangs? in the row /.exec(message);
75
+ if (carried)
76
+ return say('figure.text.part-row-carries', { part, others: carried[1] });
77
+ return say('figure.text.part-is-referenced', { part });
78
+ }
79
+ /** What removing a part let go of (figure-model's `RemovalNotice`), said in one line, or nothing when it let go of nothing. */
80
+ export function removalNoticeOf(notice, say) {
81
+ if (!notice)
82
+ return null;
83
+ const number = (v) => v.toLocaleString(undefined, { maximumFractionDigits: 3 });
84
+ const lines = [];
85
+ const still = notice.released.filter(r => !r.control).map(r => r.part);
86
+ if (still.length)
87
+ lines.push(say('figure.text.removal-released', { parts: still.join(', ') }));
88
+ for (const r of notice.released.filter(r => r.control))
89
+ lines.push(say('figure.text.removal-released-motion', { part: r.part, control: r.control }));
90
+ for (const c of notice.controls)
91
+ lines.push(say('figure.text.removal-own-control', { part: c.part, control: c.control, min: number(c.min), max: number(c.max) }));
92
+ const fixed = [...new Set(notice.fixed.map(f => f.part))];
93
+ if (fixed.length)
94
+ lines.push(say('figure.text.removal-fixed', { parts: fixed.join(', ') }));
95
+ if (notice.bindings?.length)
96
+ lines.push(say('figure.text.removal-bindings', { places: notice.bindings.map(b => b.label ?? b.id).join(', ') }));
97
+ if (notice.mountingFace)
98
+ lines.push(say('figure.text.removal-mounting-face', {}));
99
+ return lines.length ? lines.join(' ') : null;
100
+ }
101
+ /**
102
+ * A recipe refused at one of its steps: which step, and why -- the particular sentence when there is one (a size
103
+ * range, a value's range, a placement), otherwise the kernel's code. The kernel says 「steps[2]: attach: …」.
104
+ */
105
+ export function recipeRefusalOf(error, say) {
106
+ const step = /^steps\[(\d+)\]$/.exec(error?.path ?? '');
107
+ if (!step)
108
+ return null;
109
+ const why = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? placementRefusalOf(error, say);
110
+ return say('figure.text.recipe-step-refused', { step: String(Number(step[1]) + 1), why: why ?? '', code: error?.code ?? '' });
111
+ }
112
+ /**
113
+ * A refusal no sentence above knows, said as a refusal with its code -- never the kernel's English on the screen (the
114
+ * duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」, 2026-09-27). The figure's node limit
115
+ * is said with its number.
116
+ */
117
+ export function editRefusalOf(error, say) {
118
+ const code = error?.code;
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ if (code === 'DOCUMENT_LIMIT')
121
+ return say('figure.text.edit-refused-node-limit', { max: /maximum (\d+) nodes/.exec(message)?.[1] ?? '' });
122
+ return say('figure.text.edit-refused', { code: code ?? '' });
123
+ }
124
+ /**
125
+ * A value outside the range its input declares, with the range and the value given -- the kernel says
126
+ * 「size.x: 6021100 outside [300,1200]」. The figure's own size is named by its axis; any other input by its range only.
127
+ * The repeat pitch's refusal shares the code and has its own words (`figure.text.repeat-pitch-too-small`).
128
+ */
129
+ export function inputRangeRefusalOf(error, say) {
130
+ if (error?.code !== 'INPUT_RANGE')
131
+ return null;
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ const found = /([\w.-]+): (-?[\d.e+]+) outside \[(-?[\d.e+]+),\s*(-?[\d.e+]+)\]/.exec(message);
134
+ if (!found)
135
+ return null;
136
+ const [, input, value, min, max] = found;
137
+ const number = (text) => Number(text).toLocaleString(undefined, { maximumFractionDigits: 3 });
138
+ const axis = /^size\.([xyz])$/.exec(input)?.[1];
139
+ const params = { value: number(value), min: number(min), max: number(max) };
140
+ return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params);
141
+ }
142
+ //# sourceMappingURL=refusals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusals.js","sourceRoot":"","sources":["../../client/modeller/refusals.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A command refused because the figure could not be drawn at an end of its declared size range, in the author's words\n * with the numbers and where to put it right -- not the kernel's English (user, 2026-09-24: 「what is this warning\n * message means?」). Kept apart from the element so it is tested without a browser.\n */\n\ntype Say = (key: string, params: Record<string, string>) => string\n\n/** The refusal in words, or nothing for any other refusal. */\nexport function sizeRangeRefusalOf(error: unknown, say: Say): string | null {\n if ((error as { code?: string })?.code !== 'INSTANCE_SIZE_RANGE') return null\n const message = error instanceof Error ? error.message : String(error)\n const found = /size\\.([xyz]):\\s*(below|above) ([\\d.]+) mm [^(;]*(?:\\(([^)]+)\\))?;/.exec(message)\n if (!found) return null\n const [, axis, side, mm, part] = found\n return say(side === 'below' ? 'figure.text.size-range-too-small' : 'figure.text.size-range-too-large', {\n axis: axis!.toUpperCase(),\n mm: Number(mm).toLocaleString(undefined, { maximumFractionDigits: 1 }),\n part: part ?? ''\n })\n}\n\n/**\n * A link refused because the size range it would leave cannot hold: measuring again cannot help (the link is not there\n * to measure), so the author is offered the same link that narrows the range to what the figure then allows.\n */\nexport function narrowingRetryOf<A extends { kind: string }>(action: A, error: unknown, say: Say): { message: string; label: string; retry: A & { narrowRange: true } } | null {\n if (action.kind !== 'link-dimension' || (error as { code?: string })?.code !== 'INSTANCE_SIZE_RANGE') return null\n const message = error instanceof Error ? error.message : String(error)\n const found = /size\\.([xyz]):\\s*(below|above) ([\\d.]+) mm [^(;]*(?:\\(([^)]+)\\))?;/.exec(message)\n if (!found) return null\n const [, axis, side, raw, part] = found\n const mm = Number(raw).toLocaleString(undefined, { maximumFractionDigits: 1 })\n const words = { axis: axis!.toUpperCase(), mm, part: part ?? '' }\n return {\n message: say(side === 'below' ? 'figure.text.link-needs-smaller-range' : 'figure.text.link-needs-larger-range', words),\n label: say(side === 'below' ? 'figure.button.narrow-range-from' : 'figure.button.narrow-range-to', words),\n retry: { ...action, narrowRange: true }\n }\n}\n\n/**\n * A placement refused because a part is fastened to the mounting plane by a face the new plane does not take (a bottom\n * on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.\n */\nexport function placementRefusalOf(error: unknown, say: Say): string | null {\n if ((error as { code?: string })?.code !== 'PLACEMENT_PLANE') return null\n const message = error instanceof Error ? error.message : String(error)\n const found = /: (.+?) (?:is|are) fastened to the mounting plane by a face an? (\\w+) does not take \\(([^)]+)\\)/.exec(message)\n if (!found) return null\n return say('figure.text.placement-plane-refused', { parts: found[1]!, placement: say('figure.label.placement-' + found[2]!, {}), faces: found[3]! })\n}\n\n/** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */\nexport function inDeclaredRoom(error: unknown): boolean {\n return (error as { code?: string })?.code === 'PART_REFERENCED' && /is in the space the figure declares/.test(error instanceof Error ? error.message : String(error))\n}\n\n/**\n * Why a part cannot be deleted, in the author's words. Since the chief architect's ruling of 2026-09-27 a part that\n * holds others, leads a motion or is followed goes, and what depended on it is let go of (`removalNoticeOf`); what is\n * left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.\n */\nexport function partRefusalOf(error: unknown, part: string, say: Say): string | null {\n const code = (error as { code?: string })?.code\n if (code !== 'PART_REFERENCED' && code !== 'PART_ROW') return null\n /* figure-model's sentence after its code and path: 「PART_ROW: bed: roller is a row laid along bed; …」. */\n const message = (error instanceof Error ? error.message : String(error)).replace(/^[A-Z_]+: [^:]+: /, '')\n if (/is in the space the figure declares/.test(message)) return say('figure.text.part-in-declared-space', { part })\n const along = /^(.+?) (?:is a row|are rows) laid along /.exec(message)\n if (along) return say('figure.text.part-row-along', { part, rows: along[1]! })\n const carried = /^(.+?) hangs? in the row /.exec(message)\n if (carried) return say('figure.text.part-row-carries', { part, others: carried[1]! })\n return say('figure.text.part-is-referenced', { part })\n}\n\n/** What removing a part let go of (figure-model's `RemovalNotice`), said in one line, or nothing when it let go of nothing. */\nexport function removalNoticeOf(\n notice:\n | {\n released: Array<{ part: string; control?: string }>\n fixed: Array<{ part: string }>\n controls: Array<{ part: string; control: string; min: number; max: number }>\n mountingFace?: boolean\n bindings?: Array<{ id: string; label?: string }>\n }\n | undefined,\n say: Say\n): string | null {\n if (!notice) return null\n const number = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 3 })\n const lines: string[] = []\n const still = notice.released.filter(r => !r.control).map(r => r.part)\n if (still.length) lines.push(say('figure.text.removal-released', { parts: still.join(', ') }))\n for (const r of notice.released.filter(r => r.control)) lines.push(say('figure.text.removal-released-motion', { part: r.part, control: r.control! }))\n for (const c of notice.controls) lines.push(say('figure.text.removal-own-control', { part: c.part, control: c.control, min: number(c.min), max: number(c.max) }))\n const fixed = [...new Set(notice.fixed.map(f => f.part))]\n if (fixed.length) lines.push(say('figure.text.removal-fixed', { parts: fixed.join(', ') }))\n if (notice.bindings?.length) lines.push(say('figure.text.removal-bindings', { places: notice.bindings.map(b => b.label ?? b.id).join(', ') }))\n if (notice.mountingFace) lines.push(say('figure.text.removal-mounting-face', {}))\n return lines.length ? lines.join(' ') : null\n}\n\n/**\n * A recipe refused at one of its steps: which step, and why -- the particular sentence when there is one (a size\n * range, a value's range, a placement), otherwise the kernel's code. The kernel says 「steps[2]: attach: …」.\n */\nexport function recipeRefusalOf(error: unknown, say: Say): string | null {\n const step = /^steps\\[(\\d+)\\]$/.exec((error as { path?: string })?.path ?? '')\n if (!step) return null\n const why = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? placementRefusalOf(error, say)\n return say('figure.text.recipe-step-refused', { step: String(Number(step[1]) + 1), why: why ?? '', code: (error as { code?: string })?.code ?? '' })\n}\n\n/**\n * A refusal no sentence above knows, said as a refusal with its code -- never the kernel's English on the screen (the\n * duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」, 2026-09-27). The figure's node limit\n * is said with its number.\n */\nexport function editRefusalOf(error: unknown, say: Say): string {\n const code = (error as { code?: string })?.code\n const message = error instanceof Error ? error.message : String(error)\n if (code === 'DOCUMENT_LIMIT') return say('figure.text.edit-refused-node-limit', { max: /maximum (\\d+) nodes/.exec(message)?.[1] ?? '' })\n return say('figure.text.edit-refused', { code: code ?? '' })\n}\n\n/**\n * A value outside the range its input declares, with the range and the value given -- the kernel says\n * 「size.x: 6021100 outside [300,1200]」. The figure's own size is named by its axis; any other input by its range only.\n * The repeat pitch's refusal shares the code and has its own words (`figure.text.repeat-pitch-too-small`).\n */\nexport function inputRangeRefusalOf(error: unknown, say: Say): string | null {\n if ((error as { code?: string })?.code !== 'INPUT_RANGE') return null\n const message = error instanceof Error ? error.message : String(error)\n const found = /([\\w.-]+): (-?[\\d.e+]+) outside \\[(-?[\\d.e+]+),\\s*(-?[\\d.e+]+)\\]/.exec(message)\n if (!found) return null\n const [, input, value, min, max] = found\n const number = (text: string) => Number(text).toLocaleString(undefined, { maximumFractionDigits: 3 })\n const axis = /^size\\.([xyz])$/.exec(input!)?.[1]\n const params = { value: number(value!), min: number(min!), max: number(max!) }\n return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params)\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import * as THREE from 'three';
2
+ /** Evaluated boundary: graph values are resolved before reaching the renderer. */
3
+ export interface FigureRenderPart {
4
+ id: string;
5
+ shape: {
6
+ provider: string;
7
+ dimensions: Record<string, number>;
8
+ profile?: Array<{
9
+ x: number;
10
+ y: number;
11
+ }>;
12
+ };
13
+ pose: {
14
+ r: number[][];
15
+ t: number[];
16
+ };
17
+ material: {
18
+ token: string;
19
+ preset?: string;
20
+ transparent?: boolean;
21
+ emissive?: {
22
+ token: string;
23
+ intensity: number;
24
+ on: boolean;
25
+ };
26
+ };
27
+ segments?: number;
28
+ }
29
+ /**
30
+ * The bodies a canvas draws, out of everything the asset evaluated.
31
+ *
32
+ * Two are left out. A scalar body carries no shape or pose — it is a number the graph produced, not something
33
+ * on screen. A body the asset hides is evaluated, placed and gated exactly as a shown one, and excluded from
34
+ * rendering, from shadows and from pointer picking (designer's ruling 2026-09-22); drawing it would make
35
+ * "hidden" mean nothing in the modeller while it means something on a board.
36
+ */
37
+ export declare function drawableFigureParts(geometry: readonly unknown[]): FigureRenderPart[];
38
+ export declare function disposeFigureMeshes(group: THREE.Group): void;
39
+ /** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */
40
+ export declare function replaceFigureMeshes(group: THREE.Group, parts: readonly FigureRenderPart[], palette: Record<string, string>): void;
@@ -0,0 +1,73 @@
1
+ import * as THREE from 'three';
2
+ import { buildFigureGeometry } from '@hatiolab/things-scene';
3
+ /**
4
+ * The bodies a canvas draws, out of everything the asset evaluated.
5
+ *
6
+ * Two are left out. A scalar body carries no shape or pose — it is a number the graph produced, not something
7
+ * on screen. A body the asset hides is evaluated, placed and gated exactly as a shown one, and excluded from
8
+ * rendering, from shadows and from pointer picking (designer's ruling 2026-09-22); drawing it would make
9
+ * "hidden" mean nothing in the modeller while it means something on a board.
10
+ */
11
+ export function drawableFigureParts(geometry) {
12
+ return geometry.filter((body) => !!body && typeof body === 'object' && 'shape' in body && 'pose' in body && body.visible !== false);
13
+ }
14
+ /**
15
+ * The mesh for one body — things-scene's builder, not a copy of it.
16
+ *
17
+ * The modeller kept its own provider switch and it knew four providers; figures use eleven. A body the
18
+ * board drew was refused here with "Unsupported provider: roundedBoxXZ" (2026-09-24, the first canvas check of
19
+ * ADR-0087 step 4). One builder, and the canvas an author looks at shows what a board will show.
20
+ */
21
+ function geometry(part) {
22
+ const built = buildFigureGeometry(part.shape, part.segments ?? 12);
23
+ if (!built)
24
+ throw Error('Unsupported provider: ' + part.shape.provider);
25
+ return built;
26
+ }
27
+ export function disposeFigureMeshes(group) {
28
+ for (const child of [...group.children]) {
29
+ if (child instanceof THREE.Mesh) {
30
+ child.geometry.dispose();
31
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
32
+ materials.forEach(m => m.dispose());
33
+ }
34
+ group.remove(child);
35
+ }
36
+ }
37
+ /** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */
38
+ export function replaceFigureMeshes(group, parts, palette) {
39
+ const staged = new THREE.Group(), ids = new Set();
40
+ try {
41
+ for (const part of parts) {
42
+ if (ids.has(part.id))
43
+ throw Error('Duplicate geometry ID: ' + part.id);
44
+ ids.add(part.id);
45
+ const color = palette[part.material.token];
46
+ if (!color || !/^#[0-9a-f]{6}$/i.test(color))
47
+ throw Error('Unresolved material token: ' + part.material.token);
48
+ const { r, t } = part.pose;
49
+ if (r.length !== 3 || r.some(row => row.length !== 3 || row.some(x => !Number.isFinite(x))) || t.length !== 3 || t.some(x => !Number.isFinite(x)))
50
+ throw Error('Invalid pose: ' + part.id);
51
+ const emissive = part.material.emissive;
52
+ if (emissive?.on && !/^#[0-9a-f]{6}$/i.test(palette[emissive.token] ?? ''))
53
+ throw Error('Unresolved emissive token');
54
+ const g = geometry(part);
55
+ const material = new THREE.MeshStandardMaterial({ color, metalness: part.material.preset === 'metal' ? 0.8 : 0, roughness: part.material.preset === 'metal' ? 0.3 : 0.65, transparent: !!part.material.transparent, opacity: part.material.transparent ? 0.35 : 1, emissive: emissive?.on ? palette[emissive.token] : 0, emissiveIntensity: emissive?.on ? emissive.intensity : 0 });
56
+ const mesh = new THREE.Mesh(g, material);
57
+ mesh.name = part.id;
58
+ mesh.userData.figurePartId = part.id;
59
+ mesh.matrixAutoUpdate = false;
60
+ mesh.matrix.set(r[0][0], r[0][1], r[0][2], t[0], r[1][0], r[1][1], r[1][2], t[1], r[2][0], r[2][1], r[2][2], t[2], 0, 0, 0, 1);
61
+ staged.add(mesh);
62
+ }
63
+ }
64
+ catch (error) {
65
+ disposeFigureMeshes(staged);
66
+ throw error;
67
+ }
68
+ disposeFigureMeshes(group);
69
+ for (const mesh of [...staged.children])
70
+ group.add(mesh);
71
+ group.updateMatrixWorld(true);
72
+ }
73
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../../client/modeller/renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import * as THREE from 'three'\nimport { buildFigureGeometry } from '@hatiolab/things-scene'\n\n/** Evaluated boundary: graph values are resolved before reaching the renderer. */\nexport interface FigureRenderPart {\n id: string\n shape: { provider: string; dimensions: Record<string, number>; profile?: Array<{x:number;y:number}> }\n pose: {r:number[][];t:number[]}\n material: {token:string;preset?:string;transparent?:boolean;emissive?:{token:string;intensity:number;on:boolean}}\n segments?:number\n}\n\n/**\n * The bodies a canvas draws, out of everything the asset evaluated.\n *\n * Two are left out. A scalar body carries no shape or pose — it is a number the graph produced, not something\n * on screen. A body the asset hides is evaluated, placed and gated exactly as a shown one, and excluded from\n * rendering, from shadows and from pointer picking (designer's ruling 2026-09-22); drawing it would make\n * \"hidden\" mean nothing in the modeller while it means something on a board.\n */\nexport function drawableFigureParts(geometry: readonly unknown[]): FigureRenderPart[] {\n return geometry.filter(\n (body): body is FigureRenderPart =>\n !!body && typeof body === 'object' && 'shape' in body && 'pose' in body && (body as { visible?: boolean }).visible !== false\n )\n}\n\n/**\n * The mesh for one body — things-scene's builder, not a copy of it.\n *\n * The modeller kept its own provider switch and it knew four providers; figures use eleven. A body the\n * board drew was refused here with \"Unsupported provider: roundedBoxXZ\" (2026-09-24, the first canvas check of\n * ADR-0087 step 4). One builder, and the canvas an author looks at shows what a board will show.\n */\nfunction geometry(part: FigureRenderPart): THREE.BufferGeometry {\n const built = buildFigureGeometry(part.shape as never, part.segments ?? 12) as THREE.BufferGeometry | undefined\n if (!built) throw Error('Unsupported provider: ' + part.shape.provider)\n return built\n}\n\nexport function disposeFigureMeshes(group:THREE.Group):void {\n for(const child of [...group.children]){\n if(child instanceof THREE.Mesh){child.geometry.dispose();const materials=Array.isArray(child.material)?child.material:[child.material];materials.forEach(m=>m.dispose())}\n group.remove(child)\n }\n}\n\n/** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */\nexport function replaceFigureMeshes(group:THREE.Group,parts:readonly FigureRenderPart[],palette:Record<string,string>):void {\n const staged=new THREE.Group(),ids=new Set<string>()\n try{\n for(const part of parts){\n if(ids.has(part.id))throw Error('Duplicate geometry ID: '+part.id);ids.add(part.id)\n const color=palette[part.material.token]\n if(!color||!/^#[0-9a-f]{6}$/i.test(color))throw Error('Unresolved material token: '+part.material.token)\n const {r,t}=part.pose\n if(r.length!==3||r.some(row=>row.length!==3||row.some(x=>!Number.isFinite(x)))||t.length!==3||t.some(x=>!Number.isFinite(x)))throw Error('Invalid pose: '+part.id)\n const emissive=part.material.emissive\n if(emissive?.on&&!/^#[0-9a-f]{6}$/i.test(palette[emissive.token]??''))throw Error('Unresolved emissive token')\n const g=geometry(part)\n const material=new THREE.MeshStandardMaterial({color,metalness:part.material.preset==='metal'?0.8:0,roughness:part.material.preset==='metal'?0.3:0.65,transparent:!!part.material.transparent,opacity:part.material.transparent?0.35:1,emissive:emissive?.on?palette[emissive.token]:0,emissiveIntensity:emissive?.on?emissive.intensity:0})\n const mesh=new THREE.Mesh(g,material);mesh.name=part.id;mesh.userData.figurePartId=part.id\n mesh.matrixAutoUpdate=false;mesh.matrix.set(r[0][0],r[0][1],r[0][2],t[0],r[1][0],r[1][1],r[1][2],t[1],r[2][0],r[2][1],r[2][2],t[2],0,0,0,1)\n staged.add(mesh)\n }\n }catch(error){disposeFigureMeshes(staged);throw error}\n disposeFigureMeshes(group)\n for(const mesh of [...staged.children])group.add(mesh)\n group.updateMatrixWorld(true)\n}\n"]}
@@ -0,0 +1,39 @@
1
+ import * as THREE from 'three';
2
+ import type { GizmoResult } from './gizmo.js';
3
+ import type { FigureRenderPart } from './renderer.js';
4
+ export interface Board {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface BodyFrame {
9
+ /** The body's box: its centre in the body's own frame, and its extent. */
10
+ centre: THREE.Vector3;
11
+ extent: THREE.Vector3;
12
+ rotation: THREE.Matrix4;
13
+ origin: THREE.Vector3;
14
+ }
15
+ export declare function frameOf(part: FigureRenderPart): BodyFrame | null;
16
+ /** The component state that draws a body where figure-model put it. */
17
+ export declare function componentOf(part: FigureRenderPart, palette: Record<string, string>, board: Board): Record<string, unknown> | null;
18
+ /**
19
+ * What the gizmo did, in the inputs' terms: where the body's origin is now, how it is turned (degrees, rigid@1's
20
+ * order) and how much each of its own axes grew. Only what changed is given; a component the gizmo did not move gives
21
+ * nothing.
22
+ */
23
+ export declare function gizmoResultOf(part: FigureRenderPart, after: Record<string, unknown>, board: Board): GizmoResult;
24
+ /**
25
+ * A drag as amounts, for a part whose place is worked out (`nudge-part`): how far its origin moved and how far each of
26
+ * its angles turned (rigid@1's order, degrees). Nothing on an axis that did not change.
27
+ */
28
+ export declare function nudgeOf(part: FigureRenderPart, result: GizmoResult): {
29
+ by: Partial<Record<'x' | 'y' | 'z', number>>;
30
+ turn: Partial<Record<'x' | 'y' | 'z', number>>;
31
+ };
32
+ /** The keys of `next` whose values differ from `current` -- only those are set, so the scene raises no echo. */
33
+ export declare function changedKeys(current: Record<string, unknown>, next: Record<string, unknown>): Record<string, unknown> | null;
34
+ /** A board wide and deep enough for the figure, with room around it; its size only grows while a figure is edited. */
35
+ export declare function boardFor(parts: readonly FigureRenderPart[], at?: Board & {
36
+ depth?: number;
37
+ }): Board & {
38
+ depth: number;
39
+ };