@things-factory/figure-ui 10.1.205 → 10.1.207

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 (105) hide show
  1. package/client/modeller/editor-styles.ts +16 -0
  2. package/client/modeller/editor.ts +53 -137
  3. package/client/modeller/fasten-form.ts +50 -11
  4. package/client/modeller/figure-facts.ts +1 -1
  5. package/client/modeller/figure-fasten-dialog.ts +3 -2
  6. package/client/modeller/figure-history-panel.ts +1 -29
  7. package/client/modeller/figure-links.ts +2 -2
  8. package/client/modeller/figure-preview.ts +10 -12
  9. package/client/modeller/figure-verdict.ts +2 -17
  10. package/client/modeller/gizmo.ts +10 -0
  11. package/client/modeller/keys.ts +10 -1
  12. package/client/modeller/links.ts +32 -41
  13. package/client/modeller/maturity.ts +11 -27
  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 +20 -33
  18. package/client/pages/figure-modeller-page.ts +7 -22
  19. package/dist-client/modeller/editor-styles.js +16 -0
  20. package/dist-client/modeller/editor-styles.js.map +1 -1
  21. package/dist-client/modeller/editor.d.ts +8 -11
  22. package/dist-client/modeller/editor.js +57 -138
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/fasten-form.d.ts +16 -7
  25. package/dist-client/modeller/fasten-form.js +45 -11
  26. package/dist-client/modeller/fasten-form.js.map +1 -1
  27. package/dist-client/modeller/figure-facts.js +1 -1
  28. package/dist-client/modeller/figure-facts.js.map +1 -1
  29. package/dist-client/modeller/figure-fasten-dialog.js +3 -3
  30. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  31. package/dist-client/modeller/figure-history-panel.d.ts +0 -16
  32. package/dist-client/modeller/figure-history-panel.js +1 -27
  33. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  34. package/dist-client/modeller/figure-links.js +2 -2
  35. package/dist-client/modeller/figure-links.js.map +1 -1
  36. package/dist-client/modeller/figure-preview.js +8 -11
  37. package/dist-client/modeller/figure-preview.js.map +1 -1
  38. package/dist-client/modeller/figure-verdict.d.ts +0 -5
  39. package/dist-client/modeller/figure-verdict.js +2 -21
  40. package/dist-client/modeller/figure-verdict.js.map +1 -1
  41. package/dist-client/modeller/gizmo.d.ts +6 -0
  42. package/dist-client/modeller/gizmo.js +11 -0
  43. package/dist-client/modeller/gizmo.js.map +1 -1
  44. package/dist-client/modeller/keys.d.ts +1 -1
  45. package/dist-client/modeller/keys.js +6 -0
  46. package/dist-client/modeller/keys.js.map +1 -1
  47. package/dist-client/modeller/links.d.ts +16 -22
  48. package/dist-client/modeller/links.js +26 -39
  49. package/dist-client/modeller/links.js.map +1 -1
  50. package/dist-client/modeller/maturity.d.ts +2 -4
  51. package/dist-client/modeller/maturity.js +12 -26
  52. package/dist-client/modeller/maturity.js.map +1 -1
  53. package/dist-client/modeller/palette.js +1 -1
  54. package/dist-client/modeller/palette.js.map +1 -1
  55. package/dist-client/modeller/product-session.d.ts +15 -13
  56. package/dist-client/modeller/product-session.js +15 -5
  57. package/dist-client/modeller/product-session.js.map +1 -1
  58. package/dist-client/modeller/refusal-teller.d.ts +1 -12
  59. package/dist-client/modeller/refusal-teller.js +3 -22
  60. package/dist-client/modeller/refusal-teller.js.map +1 -1
  61. package/dist-client/modeller/refusals.d.ts +7 -17
  62. package/dist-client/modeller/refusals.js +17 -34
  63. package/dist-client/modeller/refusals.js.map +1 -1
  64. package/dist-client/pages/figure-modeller-page.d.ts +1 -3
  65. package/dist-client/pages/figure-modeller-page.js +7 -23
  66. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  67. package/dist-client/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +7 -7
  69. package/test/browser/acceptance-page.js +4 -4
  70. package/test/browser/detail-page.js +1 -3
  71. package/test/browser/detail-tabs-page.js +1 -2
  72. package/test/browser/links.check.mjs +32 -50
  73. package/test/browser/modeller.check.mjs +46 -19
  74. package/test/browser/parts-page.js +2 -2
  75. package/test/browser/parts.check.mjs +51 -12
  76. package/test/browser/product-page.check.mjs +6 -9
  77. package/test/browser/product-page.js +1 -2
  78. package/test/browser/screen-page.js +2 -6
  79. package/test/browser/screen.check.mjs +8 -65
  80. package/test/browser/section.check.mjs +27 -0
  81. package/test/fasten-form.test.ts +42 -2
  82. package/test/first-minute.test.ts +2 -3
  83. package/test/fixtures/joint-column.json +1 -66
  84. package/test/fixtures/joint-roller.json +1 -66
  85. package/test/fixtures/robot-arm-6dof.json +2 -67
  86. package/test/gizmo.test.ts +12 -1
  87. package/test/keys.test.ts +11 -0
  88. package/test/links.test.ts +39 -34
  89. package/test/parts-wiring.test.ts +1 -1
  90. package/test/properties.test.ts +9 -12
  91. package/test/refusals.test.ts +20 -42
  92. package/test/resize-with-placement.test.ts +13 -15
  93. package/test/revision.test.ts +1 -2
  94. package/test/verdict.test.ts +1 -15
  95. package/translations/en.json +8 -32
  96. package/translations/es.json +8 -32
  97. package/translations/ja.json +8 -32
  98. package/translations/ko.json +8 -32
  99. package/translations/ms.json +8 -32
  100. package/translations/zh.json +8 -32
  101. package/client/modeller/occupancy.ts +0 -51
  102. package/dist-client/modeller/occupancy.d.ts +0 -31
  103. package/dist-client/modeller/occupancy.js +0 -36
  104. package/dist-client/modeller/occupancy.js.map +0 -1
  105. package/test/release-panel.test.ts +0 -53
@@ -130,6 +130,22 @@ export const EditorStyles = css `
130
130
  min-width: 0;
131
131
  min-height: 0;
132
132
  }
133
+ /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */
134
+ p[same-measure] {
135
+ position: absolute;
136
+ bottom: 12px;
137
+ left: 50%;
138
+ z-index: 5;
139
+ transform: translateX(-50%);
140
+ margin: 0;
141
+ padding: 4px 10px;
142
+ border-radius: 999em;
143
+ background: var(--md-sys-color-surface-container-high);
144
+ color: var(--md-sys-color-on-surface-variant);
145
+ font-size: var(--ui-t-small, 11.5px);
146
+ pointer-events: none;
147
+ white-space: nowrap;
148
+ }
133
149
  /* A new figure's first question, over the empty canvas. */
134
150
  div[start] {
135
151
  position: absolute;
@@ -1 +1 @@
1
- {"version":3,"file":"editor-styles.js","sourceRoot":"","sources":["../../client/modeller/editor-styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4Z9B,CAAA","sourcesContent":["import { css } from 'lit'\n\n/** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */\nexport const EditorStyles = css`\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */\n container-type: inline-size;\n /* The app's one face (its typography theme's --theme-font) at the body size: a bare sans-serif here put a second\n face under everything the editor draws, beside the parts list and the shell in the app's own (2026-10-16). */\n font-family: var(--theme-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n }\n /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and\n fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */\n ox-canvas-bar button[mode] {\n height: 28px;\n border: 1px solid transparent;\n padding: 0 10px;\n border-radius: 999em;\n border-color: transparent;\n background: none;\n }\n ox-canvas-bar button[mode][aria-pressed='true'] {\n border-color: var(--md-sys-color-primary);\n }\n /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */\n ox-canvas-bar .gizmo-tools {\n display: inline-flex;\n gap: 2px;\n padding-left: 4px;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ox-canvas-bar button[tool] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n ox-canvas-bar button[tool]:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n ox-canvas-bar button[tool][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n ox-canvas-bar button[tool]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n /*\n Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they\n showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.\n */\n ox-canvas-bar button[tool] md-icon {\n --md-icon-size: 20px;\n }\n ox-canvas-bar button[tool]:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n /* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */\n div[middle][hidden] {\n display: none;\n }\n /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */\n figure-preview[hidden] {\n display: none;\n }\n ox-canvas-bar .status {\n padding: 0 8px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):\n round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,\n a promise to whoever draws the figure, is outlined.\n */\n ox-canvas-bar button[save],\n ox-canvas-bar button[publish] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 14px 0 10px;\n border-radius: 999em;\n font-weight: 600;\n }\n ox-canvas-bar button[save] md-icon,\n ox-canvas-bar button[publish] md-icon {\n --md-icon-size: 18px;\n }\n ox-canvas-bar button[save] {\n border: 1px solid transparent;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n }\n /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under\n 860px and one column with the middle first under 560px are the frame's, as are the dividers. */\n ox-three-pane {\n flex: 1;\n min-height: 0;\n }\n figure-parts {\n min-height: 0;\n }\n figure-canvas {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n div[middle] {\n position: relative;\n display: flex;\n min-width: 0;\n min-height: 0;\n }\n /* A new figure's first question, over the empty canvas. */\n div[start] {\n position: absolute;\n inset: 50% auto auto 50%;\n z-index: 10;\n color: var(--md-sys-color-on-surface);\n transform: translate(-50%, -50%);\n width: min(440px, calc(100% - 32px));\n box-sizing: border-box;\n padding: 20px;\n border-radius: 12px;\n background: var(--md-sys-color-surface, #fff);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);\n }\n div[start] button[start-button] {\n width: auto;\n margin: 0;\n font-size: var(--ui-t-body);\n height: 36px;\n padding: 0 16px;\n border: 1px solid var(--md-sys-color-outline);\n border-radius: 999em;\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface, #fff);\n cursor: pointer;\n }\n div[start] button[start-button]:hover:not(:disabled) {\n background: var(--md-sys-color-primary-container);\n }\n div[start] button[start-button][pressed] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: var(--md-sys-color-primary);\n }\n div[start] button[start-button]:disabled {\n opacity: 0.5;\n cursor: default;\n }\n div[start] h2 {\n margin: 0 0 12px;\n font-size: var(--ui-t-title);\n }\n div[start-paths] {\n display: grid;\n gap: 14px;\n }\n div[start-paths] h3 {\n margin: 0 0 6px;\n font-size: var(--ui-t-body);\n }\n div[start-shapes] {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n div[start] small {\n display: block;\n margin-top: 4px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The side: tabs over one panel at a time, each using the whole height. */\n /*\n The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body\n for the bar, and the fields the editor drew itself -- the part's name -- stood a step larger than every other field.\n */\n aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n font-size: var(--ui-t-small, 11.5px);\n background: var(--md-sys-color-surface-container-lowest);\n }\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n min-width: 0;\n height: 48px;\n padding: 0;\n border: none;\n border-radius: 0;\n border-bottom: 2px solid transparent;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n button[tab]:hover:not([on]) {\n background: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n button[tab] span[tab-label] {\n max-width: 100%;\n overflow: hidden;\n font-size: var(--ui-t-micro, 11px);\n line-height: 1.2;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n /* A range's two ends read as one: smallest – largest. */\n div[pair][range] {\n align-items: center;\n }\n div[pair][range] > span[dash] {\n flex: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */\n div[panel] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0;\n }\n div[panel][flush] {\n padding: 0;\n }\n /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */\n [off] {\n display: none !important;\n }\n /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */\n div[group-fields] {\n display: grid;\n gap: 6px;\n }\n /* The folded 「더 보기」: its title is the fold's handle, at the title's size. */\n section[more] > details > summary {\n padding: 0;\n }\n section[more] > details > summary > h3 {\n margin: 0;\n }\n div[more-body] {\n display: grid;\n gap: 16px;\n margin-top: 12px;\n }\n /*\n The view settings and the release history, opened from the bar over the right of the work: neither is about the\n part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in\n the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).\n */\n :host {\n position: relative;\n }\n div[side-overlay] {\n position: absolute;\n top: var(--figure-bar-height, 44px);\n right: 8px;\n z-index: 5;\n display: flex;\n flex-direction: column;\n width: min(360px, calc(100% - 16px));\n max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);\n background: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);\n }\n div[overlay-head] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[overlay-head] h3 {\n margin: 0;\n }\n div[overlay-head] button[quiet] {\n margin-left: 0;\n }\n [overlay-body] {\n min-height: 0;\n overflow: auto;\n }\n /* Which part the panel speaks of. */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 1;\n display: grid;\n gap: 4px;\n padding: 10px 12px;\n background: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */\n div[subject-summary] {\n display: grid;\n gap: 2px;\n }\n div[subject-summary] b {\n font-size: var(--ui-t-body, 13px);\n font-weight: 600;\n }\n div[subject-summary] b span {\n font-weight: 400;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[subject-summary] small {\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one\n height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도\n 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).\n */\n button {\n font: inherit;\n box-sizing: border-box;\n color: inherit;\n min-width: 0;\n cursor: pointer;\n }\n /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */\n ox-canvas-bar button[release-blocked] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n padding: 0 14px 0 10px;\n border: none;\n /* Round as save and release beside it: one shape for the bar's end. */\n border-radius: 999em;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n }\n ox-canvas-bar button[release-blocked] md-icon {\n --md-icon-size: 18px;\n }\n div[release-blockers] {\n position: absolute;\n top: 46px;\n right: 8px;\n z-index: 6;\n width: min(340px, calc(100% - 16px));\n padding: 8px 10px 10px;\n box-sizing: border-box;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);\n }\n ox-canvas-bar button[publish] {\n border: 1px solid var(--md-sys-color-outline);\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface);\n }\n ox-canvas-bar button[publish][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n .hint {\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n`\n"]}
1
+ {"version":3,"file":"editor-styles.js","sourceRoot":"","sources":["../../client/modeller/editor-styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4a9B,CAAA","sourcesContent":["import { css } from 'lit'\n\n/** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */\nexport const EditorStyles = css`\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */\n container-type: inline-size;\n /* The app's one face (its typography theme's --theme-font) at the body size: a bare sans-serif here put a second\n face under everything the editor draws, beside the parts list and the shell in the app's own (2026-10-16). */\n font-family: var(--theme-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n }\n /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and\n fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */\n ox-canvas-bar button[mode] {\n height: 28px;\n border: 1px solid transparent;\n padding: 0 10px;\n border-radius: 999em;\n border-color: transparent;\n background: none;\n }\n ox-canvas-bar button[mode][aria-pressed='true'] {\n border-color: var(--md-sys-color-primary);\n }\n /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */\n ox-canvas-bar .gizmo-tools {\n display: inline-flex;\n gap: 2px;\n padding-left: 4px;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ox-canvas-bar button[tool] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n ox-canvas-bar button[tool]:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n ox-canvas-bar button[tool][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n ox-canvas-bar button[tool]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n /*\n Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they\n showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.\n */\n ox-canvas-bar button[tool] md-icon {\n --md-icon-size: 20px;\n }\n ox-canvas-bar button[tool]:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n /* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */\n div[middle][hidden] {\n display: none;\n }\n /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */\n figure-preview[hidden] {\n display: none;\n }\n ox-canvas-bar .status {\n padding: 0 8px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):\n round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,\n a promise to whoever draws the figure, is outlined.\n */\n ox-canvas-bar button[save],\n ox-canvas-bar button[publish] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 14px 0 10px;\n border-radius: 999em;\n font-weight: 600;\n }\n ox-canvas-bar button[save] md-icon,\n ox-canvas-bar button[publish] md-icon {\n --md-icon-size: 18px;\n }\n ox-canvas-bar button[save] {\n border: 1px solid transparent;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n }\n /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under\n 860px and one column with the middle first under 560px are the frame's, as are the dividers. */\n ox-three-pane {\n flex: 1;\n min-height: 0;\n }\n figure-parts {\n min-height: 0;\n }\n figure-canvas {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n div[middle] {\n position: relative;\n display: flex;\n min-width: 0;\n min-height: 0;\n }\n /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */\n p[same-measure] {\n position: absolute;\n bottom: 12px;\n left: 50%;\n z-index: 5;\n transform: translateX(-50%);\n margin: 0;\n padding: 4px 10px;\n border-radius: 999em;\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-small, 11.5px);\n pointer-events: none;\n white-space: nowrap;\n }\n /* A new figure's first question, over the empty canvas. */\n div[start] {\n position: absolute;\n inset: 50% auto auto 50%;\n z-index: 10;\n color: var(--md-sys-color-on-surface);\n transform: translate(-50%, -50%);\n width: min(440px, calc(100% - 32px));\n box-sizing: border-box;\n padding: 20px;\n border-radius: 12px;\n background: var(--md-sys-color-surface, #fff);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);\n }\n div[start] button[start-button] {\n width: auto;\n margin: 0;\n font-size: var(--ui-t-body);\n height: 36px;\n padding: 0 16px;\n border: 1px solid var(--md-sys-color-outline);\n border-radius: 999em;\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface, #fff);\n cursor: pointer;\n }\n div[start] button[start-button]:hover:not(:disabled) {\n background: var(--md-sys-color-primary-container);\n }\n div[start] button[start-button][pressed] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: var(--md-sys-color-primary);\n }\n div[start] button[start-button]:disabled {\n opacity: 0.5;\n cursor: default;\n }\n div[start] h2 {\n margin: 0 0 12px;\n font-size: var(--ui-t-title);\n }\n div[start-paths] {\n display: grid;\n gap: 14px;\n }\n div[start-paths] h3 {\n margin: 0 0 6px;\n font-size: var(--ui-t-body);\n }\n div[start-shapes] {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n div[start] small {\n display: block;\n margin-top: 4px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The side: tabs over one panel at a time, each using the whole height. */\n /*\n The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body\n for the bar, and the fields the editor drew itself -- the part's name -- stood a step larger than every other field.\n */\n aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n font-size: var(--ui-t-small, 11.5px);\n background: var(--md-sys-color-surface-container-lowest);\n }\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n min-width: 0;\n height: 48px;\n padding: 0;\n border: none;\n border-radius: 0;\n border-bottom: 2px solid transparent;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n button[tab]:hover:not([on]) {\n background: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n button[tab] span[tab-label] {\n max-width: 100%;\n overflow: hidden;\n font-size: var(--ui-t-micro, 11px);\n line-height: 1.2;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n /* A range's two ends read as one: smallest – largest. */\n div[pair][range] {\n align-items: center;\n }\n div[pair][range] > span[dash] {\n flex: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */\n div[panel] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0;\n }\n div[panel][flush] {\n padding: 0;\n }\n /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */\n [off] {\n display: none !important;\n }\n /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */\n div[group-fields] {\n display: grid;\n gap: 6px;\n }\n /* The folded 「더 보기」: its title is the fold's handle, at the title's size. */\n section[more] > details > summary {\n padding: 0;\n }\n section[more] > details > summary > h3 {\n margin: 0;\n }\n div[more-body] {\n display: grid;\n gap: 16px;\n margin-top: 12px;\n }\n /*\n The view settings and the release history, opened from the bar over the right of the work: neither is about the\n part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in\n the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).\n */\n :host {\n position: relative;\n }\n div[side-overlay] {\n position: absolute;\n top: var(--figure-bar-height, 44px);\n right: 8px;\n z-index: 5;\n display: flex;\n flex-direction: column;\n width: min(360px, calc(100% - 16px));\n max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);\n background: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);\n }\n div[overlay-head] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[overlay-head] h3 {\n margin: 0;\n }\n div[overlay-head] button[quiet] {\n margin-left: 0;\n }\n [overlay-body] {\n min-height: 0;\n overflow: auto;\n }\n /* Which part the panel speaks of. */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 1;\n display: grid;\n gap: 4px;\n padding: 10px 12px;\n background: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */\n div[subject-summary] {\n display: grid;\n gap: 2px;\n }\n div[subject-summary] b {\n font-size: var(--ui-t-body, 13px);\n font-weight: 600;\n }\n div[subject-summary] b span {\n font-weight: 400;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[subject-summary] small {\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one\n height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도\n 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).\n */\n button {\n font: inherit;\n box-sizing: border-box;\n color: inherit;\n min-width: 0;\n cursor: pointer;\n }\n /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */\n ox-canvas-bar button[release-blocked] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n padding: 0 14px 0 10px;\n border: none;\n /* Round as save and release beside it: one shape for the bar's end. */\n border-radius: 999em;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n }\n ox-canvas-bar button[release-blocked] md-icon {\n --md-icon-size: 18px;\n }\n div[release-blockers] {\n position: absolute;\n top: 46px;\n right: 8px;\n z-index: 6;\n width: min(340px, calc(100% - 16px));\n padding: 8px 10px 10px;\n box-sizing: border-box;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);\n }\n ox-canvas-bar button[publish] {\n border: 1px solid var(--md-sys-color-outline);\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface);\n }\n ox-canvas-bar button[publish][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n .hint {\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n`\n"]}
@@ -59,8 +59,6 @@ export declare class FigureEditor extends LitElement {
59
59
  /** How far the GLB being brought in has gone up (0‒1), while it does. */
60
60
  private meshProgress?;
61
61
  private faceMapping?;
62
- /** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
63
- private retry?;
64
62
  private tab;
65
63
  /** The view settings or the release history, open over the work from the bar. */
66
64
  private overlay?;
@@ -118,6 +116,10 @@ export declare class FigureEditor extends LitElement {
118
116
  */
119
117
  private renderGizmoTools;
120
118
  private keys;
119
+ /** The part Ctrl+C took: pasted as the kernel's duplicate. Not kept past the editor, nor written anywhere. */
120
+ private copied;
121
+ /** A copied part still in the figure is duplicated -- a part of its own, named by figure-model; one gone, nothing. */
122
+ private pasteCopied;
121
123
  private keyAction;
122
124
  private paneFold;
123
125
  private refusals;
@@ -131,6 +133,8 @@ export declare class FigureEditor extends LitElement {
131
133
  * gone or another figure opened has nothing to go back to: the editor closes.
132
134
  */
133
135
  private leaveSectionEditor;
136
+ /** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
137
+ private renderSameMeasure;
134
138
  /** The scene toggles: the mode already shown takes the gizmo away. */
135
139
  private useGizmo;
136
140
  /**
@@ -270,7 +274,7 @@ export declare class FigureEditor extends LitElement {
270
274
  * pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
271
275
  */
272
276
  private scrub;
273
- /** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
277
+ /** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
274
278
  private renderInstanceRange;
275
279
  /**
276
280
  * How the figure grows. One ratio (`sizing: 'uniform'`) is said as it is; a figure with joints that has not declared
@@ -307,7 +311,7 @@ export declare class FigureEditor extends LitElement {
307
311
  private setPitch;
308
312
  /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
309
313
  private releaseWords;
310
- /** The findings that block release, under the bar beside 「발행」, each with the way out the verdict tab offers for it. */
314
+ /** The findings that block release, under the bar beside 「발행」. */
311
315
  private renderReleaseBlockers;
312
316
  private goRelease;
313
317
  private overhangOf;
@@ -391,12 +395,5 @@ export declare class FigureEditor extends LitElement {
391
395
  /** What a size typed keeps, under the size fields: the part's middle, where it is fastened, or only its size (ADR-0140 4.1). */
392
396
  private renderSizeKeeps;
393
397
  private renderHeldAxes;
394
- /**
395
- * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
396
- * typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.
397
- */
398
- private renderRoomOnly;
399
- /** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
400
- private askRoomRefit;
401
398
  private renderSingle;
402
399
  }
@@ -4,10 +4,9 @@ import { LitElement, html, nothing } from 'lit';
4
4
  import { i18next } from '@operato/i18n';
5
5
  import '@operato/layout';
6
6
  import { openPopup } from '@operato/layout';
7
- import { OxPrompt } from '@operato/popup';
8
7
  import { customElement, property, state } from 'lit/decorators.js';
9
8
  import { live } from 'lit/directives/live.js';
10
- import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
9
+ import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf } from '@hatiolab/figure-model';
11
10
  import './figure-recipe-catalog.js';
12
11
  import { captureFigureThumbnail } from './thumbnail.js';
13
12
  import { drawableFigureParts } from './renderer.js';
@@ -28,7 +27,7 @@ import './figure-preview.js';
28
27
  import './figure-settings.js';
29
28
  import { DEFAULT_VIEW } from '@things-factory/figure-viewer/dist-client/figure-view.js';
30
29
  import './figure-capability-panel.js';
31
- import { gizmoChanges, gizmoReachOf, sizeKeepOf } from './gizmo.js';
30
+ import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf, sizeKeepOf } from './gizmo.js';
32
31
  import { keptBySizeOf, partsAtFrame, SIZE_KEEPS_WORDS, startSizeDrag, throughPlaneOf } from './size-drag.js';
33
32
  import { ModellerKeys } from './modeller-keys.js';
34
33
  import { PaneFold } from './pane-fold.js';
@@ -48,11 +47,10 @@ const NO_MARKS = [];
48
47
  import { apexOver, drawnOf } from './section-drawing.js';
49
48
  import { END_FACE } from './section-controls.js';
50
49
  import { nudgeOf, placementOf } from './stage-model.js';
51
- import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js';
52
50
  import { gateFindingStyles, renderGateFinding } from './gate-findings.js';
53
- import { editRefusalOf, inDeclaredRoom, narrowingRetryOf, partNameRefusalOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js';
51
+ import { editRefusalOf, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js';
54
52
  import { HelpToggles } from './help.js';
55
- import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
53
+ import { endPlaceKeyOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
56
54
  import { jointFanOf } from './joint-fan.js';
57
55
  import { derivedOf } from './revision.js';
58
56
  import { jointDragOf } from './joint-drag.js';
@@ -137,14 +135,14 @@ let FigureEditor = class FigureEditor extends LitElement {
137
135
  selected: () => this.selected,
138
136
  act: action => this.keyAction(action)
139
137
  });
138
+ /** The part Ctrl+C took: pasted as the kernel's duplicate. Not kept past the editor, nor written anywhere. */
139
+ this.copied = '';
140
140
  this.paneFold = new PaneFold(this, () => this.panes);
141
141
  this.refusals = new RefusalTeller({
142
142
  session: () => this.session,
143
143
  error: () => this.error,
144
144
  notice: () => this.notice,
145
- retry: () => this.retry,
146
- refuse: message => (this.error = message),
147
- command: action => this.linkCommand(action)
145
+ refuse: message => (this.error = message)
148
146
  });
149
147
  this.panelMemory = new PanelMemory(() => this.renderRoot);
150
148
  /** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
@@ -231,6 +229,13 @@ let FigureEditor = class FigureEditor extends LitElement {
231
229
  </button>`)}
232
230
  </span>`;
233
231
  }
232
+ /** A copied part still in the figure is duplicated -- a part of its own, named by figure-model; one gone, nothing. */
233
+ pasteCopied() {
234
+ const asset = this.session?.revisionNow()?.asset;
235
+ if (!this.copied || !asset?.document.model.nodes.some(n => n.op === 'place@1' && n.id === this.copied))
236
+ return;
237
+ this.duplicatePart(this.copied);
238
+ }
234
239
  keyAction(action) {
235
240
  const view = this.session.read();
236
241
  const canvas = this.renderRoot.querySelector('figure-canvas');
@@ -242,6 +247,12 @@ let FigureEditor = class FigureEditor extends LitElement {
242
247
  view.canRedo && this.run(() => this.session.redo());
243
248
  else if (action === 'duplicate')
244
249
  this.duplicatePart(this.selected);
250
+ else if (action === 'copy')
251
+ this.copied = this.selected;
252
+ else if (action === 'paste')
253
+ this.pasteCopied();
254
+ else if (action === 'hold')
255
+ return;
245
256
  else if (action === 'remove')
246
257
  this.removePart(this.selected);
247
258
  else
@@ -295,6 +306,15 @@ let FigureEditor = class FigureEditor extends LitElement {
295
306
  this.selected = next;
296
307
  });
297
308
  }
309
+ /** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
310
+ renderSameMeasure(unavailable) {
311
+ if (unavailable || this.gizmoMode !== 'scale' || !this.selected)
312
+ return nothing;
313
+ const axes = sameMeasureAxesOf(this.reachOf(this.selected));
314
+ return axes
315
+ ? html `<p same-measure role="status">${i18next.t('figure.text.gizmo-same-measure', { axes: axes.map(a => a.toUpperCase()).join(' · ') })}</p>`
316
+ : nothing;
317
+ }
298
318
  /** The scene toggles: the mode already shown takes the gizmo away. */
299
319
  useGizmo(mode) {
300
320
  /*
@@ -380,6 +400,10 @@ let FigureEditor = class FigureEditor extends LitElement {
380
400
  this.live = action ? { ...rest, action } : rest;
381
401
  }
382
402
  changed() {
403
+ /* What the step did to the size ends the author typed, said with whatever else it said. */
404
+ const fits = this.session?.takeSizeRangeFits() ?? [];
405
+ if (fits.length)
406
+ this.notice = [this.notice, sizeRangeFitsOf(fits, (key, params) => i18next.t(key, params))].filter(Boolean).join(' ');
383
407
  this.requestUpdate();
384
408
  this.dispatchEvent(new CustomEvent('changed', { bubbles: true, composed: true }));
385
409
  }
@@ -482,10 +506,9 @@ let FigureEditor = class FigureEditor extends LitElement {
482
506
  movePart(id, to) {
483
507
  this.run(() => this.session.edit({ kind: 'move-part', target: id, to }));
484
508
  }
485
- removePart(id, refitRoom = false) {
486
- this.retry = undefined;
509
+ removePart(id) {
487
510
  try {
488
- const view = this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
511
+ const view = this.session.edit({ kind: 'remove-part', target: id });
489
512
  this.error = '';
490
513
  /* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
491
514
  this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? '';
@@ -494,19 +517,8 @@ let FigureEditor = class FigureEditor extends LitElement {
494
517
  this.changed();
495
518
  }
496
519
  catch (error) {
497
- if (this.refusals.askClipLoss(error, () => this.removePart(id, refitRoom)))
520
+ if (this.refusals.askClipLoss(error, () => this.removePart(id)))
498
521
  return;
499
- /*
500
- A part in the declared space goes, and the space is fitted to the parts left, in the same click: every space
501
- this product declares is fitted to the parts (「부품에 맞추기」, the AI's fit-room -- no padding), so fitting it
502
- again keeps what it says, and there is nothing to ask (user, 2026-09-27: 「이 확인이 꼭 필요한가 ?」).
503
- */
504
- if (!refitRoom && inDeclaredRoom(error)) {
505
- this.removePart(id, true);
506
- if (!this.error)
507
- this.notice = [this.notice, i18next.t('figure.text.removal-space-refitted')].filter(Boolean).join(' ');
508
- return;
509
- }
510
522
  this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error);
511
523
  }
512
524
  }
@@ -962,10 +974,8 @@ let FigureEditor = class FigureEditor extends LitElement {
962
974
  linkCommand(action, from = 'panel') {
963
975
  /* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
964
976
  this.holdAction(null);
965
- this.retry = undefined;
966
977
  this.notice = '';
967
978
  const hadSize = this.hasSize();
968
- const hadSpace = !!this.shown()?.asset.occupancy;
969
979
  try {
970
980
  this.session.command(action);
971
981
  this.error = '';
@@ -973,9 +983,7 @@ let FigureEditor = class FigureEditor extends LitElement {
973
983
  /* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
974
984
  if (action.kind === 'follow-size' && !hadSize) {
975
985
  const size = this.designValues();
976
- /* The same step may have fitted the space the figure takes to its parts (figure-model, ruling 2026-10-16 Q1 A): one line says both. */
977
- const spaceToo = !hadSpace && !!this.shown()?.asset.occupancy;
978
- this.notice = i18next.t(spaceToo ? 'figure.text.size-and-space-declared-to-follow' : 'figure.text.size-declared-to-follow', {
986
+ this.notice = i18next.t('figure.text.size-declared-to-follow', {
979
987
  x: Math.round(size['size.x'] ?? 0).toLocaleString(),
980
988
  y: Math.round(size['size.y'] ?? 0).toLocaleString(),
981
989
  z: Math.round(size['size.z'] ?? 0).toLocaleString()
@@ -995,12 +1003,6 @@ let FigureEditor = class FigureEditor extends LitElement {
995
1003
  this.error = '';
996
1004
  return false;
997
1005
  }
998
- const narrowing = narrowingRetryOf(action, error, (key, params) => i18next.t(key, params));
999
- if (narrowing) {
1000
- this.error = narrowing.message;
1001
- this.retry = { label: narrowing.label, action: narrowing.retry };
1002
- return false;
1003
- }
1004
1006
  this.error =
1005
1007
  code === 'DIMENSION_CYCLE'
1006
1008
  ? i18next.t('figure.text.follow-would-loop')
@@ -1096,12 +1098,12 @@ let FigureEditor = class FigureEditor extends LitElement {
1096
1098
  label.addEventListener('pointerup', up);
1097
1099
  label.addEventListener('pointercancel', up);
1098
1100
  }
1099
- /** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
1101
+ /** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
1100
1102
  renderInstanceRange(shown, unavailable) {
1101
1103
  if (!shown)
1102
1104
  return nothing;
1103
1105
  const asset = shown.asset;
1104
- /* Measured once per version: measuring evaluates the figure at its edges. */
1106
+ /* Computed once per version: figure-model evaluates the figure at its edges. */
1105
1107
  const measured = derivedOf(shown, 'instance-range', () => {
1106
1108
  let of;
1107
1109
  let toDeclare;
@@ -1112,7 +1114,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1112
1114
  of = undefined;
1113
1115
  }
1114
1116
  try {
1115
- toDeclare = of ? undefined : instanceRangeToDeclare(asset);
1117
+ toDeclare = of ? undefined : instanceSizeToDeclare(asset);
1116
1118
  }
1117
1119
  catch {
1118
1120
  toDeclare = undefined;
@@ -1133,14 +1135,18 @@ let FigureEditor = class FigureEditor extends LitElement {
1133
1135
  return html `<section board-size>
1134
1136
  ${title('figure.text.board-size-fixed-explained')}
1135
1137
  <p fixed-size>${i18next.t('figure.text.board-size-fixed', { x: mm(toDeclare.at.x), y: mm(toDeclare.at.y), z: mm(toDeclare.at.z) })}</p>
1136
- <button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })}>
1138
+ <button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', at: toDeclare.at })}>
1137
1139
  <md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
1138
1140
  </button>
1139
1141
  </section>`;
1140
1142
  if (!range)
1141
1143
  return nothing;
1142
- const differs = rangeDiffers(range.declared, range.measured);
1143
1144
  const followers = sizeFollowersOf(asset);
1145
+ /*
1146
+ An end the author typed shows its number; an open end is empty, with what the parts allow written faintly in it
1147
+ (ADR-0087 decision 6). Typing narrows that end, emptying the field opens it again.
1148
+ */
1149
+ const shownEnd = (r, bound) => (bound === 'max' && !Number.isFinite(r.max) ? '∞' : mm(r[bound]));
1144
1150
  return html `<section board-size>
1145
1151
  ${title('figure.text.board-size-range-explained')}
1146
1152
  ${['x', 'y', 'z'].map(axis => html `<div inline-field>
@@ -1150,12 +1156,12 @@ let FigureEditor = class FigureEditor extends LitElement {
1150
1156
  aria-label=${`size.${axis}.${bound}`}
1151
1157
  type="number"
1152
1158
  step="any"
1153
- .value=${live(String(Math.round(range.declared[axis][bound])))}
1159
+ .value=${live(range[axis].narrowed[bound] ? String(Math.round(range[axis][bound])) : '')}
1160
+ placeholder=${shownEnd(range[axis], bound)}
1154
1161
  ?disabled=${unavailable}
1155
1162
  @change=${(e) => {
1156
- const next = rangeWith(range.declared, axis, bound, e.target.valueAsNumber);
1157
- if (next)
1158
- this.linkCommand({ kind: 'declare-instance-size', range: next, replace: true });
1163
+ const typed = e.target.valueAsNumber;
1164
+ this.linkCommand({ kind: 'declare-instance-size', range: rangeWith(range, axis, bound, typed), replace: true });
1159
1165
  }}
1160
1166
  />`)}
1161
1167
  </div>
@@ -1167,19 +1173,6 @@ let FigureEditor = class FigureEditor extends LitElement {
1167
1173
  })
1168
1174
  : i18next.t('figure.text.board-size-no-followers')}
1169
1175
  </p>
1170
- ${differs
1171
- ? html `<p hint>
1172
- ${i18next.t('figure.text.instance-range-can-widen', {
1173
- ranges: ['x', 'y', 'z']
1174
- .filter(axis => rangeDiffers(range.declared, { ...range.declared, [axis]: range.measured[axis] }))
1175
- .map(axis => `${axis.toUpperCase()} ${mm(range.measured[axis].min)} – ${mm(range.measured[axis].max)}`)
1176
- .join(' · ')
1177
- })}
1178
- </p>
1179
- <button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })}>
1180
- <md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
1181
- </button>`
1182
- : nothing}
1183
1176
  ${this.renderSizingRule(asset, unavailable)}
1184
1177
  </section>`;
1185
1178
  }
@@ -1334,7 +1327,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1334
1327
  const form = this.publishing?.part === part
1335
1328
  ? this.publishing
1336
1329
  : listed && input
1337
- ? { part, label: listed.label, min: input.min, max: input.max }
1330
+ ? /* A published input has both ends (figure-model checkPublicInput). */ { part, label: listed.label, min: input.min, max: input.max }
1338
1331
  : undefined;
1339
1332
  const open = !!form;
1340
1333
  const edit = (key) => (e) => {
@@ -1431,18 +1424,13 @@ let FigureEditor = class FigureEditor extends LitElement {
1431
1424
  return i18next.t('figure.text.release-state-released', { version: String(version) });
1432
1425
  return version > 0 ? i18next.t('figure.text.release-state-draft-over', { version: String(version) }) : i18next.t('figure.text.release-state-draft-new');
1433
1426
  }
1434
- /** The findings that block release, under the bar beside 「발행」, each with the way out the verdict tab offers for it. */
1435
- renderReleaseBlockers(blockers, asset, unavailable) {
1427
+ /** The findings that block release, under the bar beside 「발행」. */
1428
+ renderReleaseBlockers(blockers, asset) {
1436
1429
  const parts = (asset?.appearance ?? []).map(look => look.target);
1437
- const way = (code) => (code === 'occupancy-undeclared' || code === 'part-outside-occupancy') && asset && parts.length
1438
- ? html `<button act ?disabled=${unavailable} @click=${() => this.linkCommand(occupancyFromParts(asset))}>
1439
- <md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
1440
- </button>`
1441
- : nothing;
1442
1430
  return html `<div release-blockers role="dialog" aria-label=${i18next.t('figure.text.must-fix-to-release')} @keydown=${(e) => e.key === 'Escape' && (this.blockersOpen = false)}>
1443
1431
  <ul gate>
1444
1432
  <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
1445
- ${blockers.map(finding => renderGateFinding(finding, true, way(finding.code), parts))}
1433
+ ${blockers.map(finding => renderGateFinding(finding, true, nothing, parts))}
1446
1434
  </ul>
1447
1435
  </div>`;
1448
1436
  }
@@ -1777,20 +1765,9 @@ let FigureEditor = class FigureEditor extends LitElement {
1777
1765
  /* A repeat's own row values (its pitch) have their own section, not the shape's. */
1778
1766
  const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.') && !i.id.startsWith(this.selected + '.repeat.'));
1779
1767
  const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(ownerOf(g.id) + '.')));
1780
- /*
1781
- A part's field only the declared room still reads -- its old height position once the floor holds it -- moves
1782
- nothing; it is shown apart as what it is, not as a position (ADR-0097, designer 2026-09-27).
1783
- */
1784
1768
  const shown = this.shown();
1785
1769
  const asset = shown?.asset;
1786
- let roomOnly = [];
1787
- try {
1788
- roomOnly = this.selected && asset ? figureRoomOnlyInputsOf(asset, this.selected) : [];
1789
- }
1790
- catch {
1791
- roomOnly = [];
1792
- }
1793
- const partFields = own.filter(i => !roomOnly.includes(i.id));
1770
+ const partFields = own;
1794
1771
  /* The numbers a rule gives the axes that have no field of their own -- shown read-only, never as a blank. */
1795
1772
  let held;
1796
1773
  try {
@@ -1808,7 +1785,6 @@ let FigureEditor = class FigureEditor extends LitElement {
1808
1785
  catch {
1809
1786
  held = undefined;
1810
1787
  }
1811
- const roomFields = own.filter(i => roomOnly.includes(i.id));
1812
1788
  /*
1813
1789
  Only what is under way is said. Saved or not is the save button's: filled and pressable when there is something
1814
1790
  to save, dimmed when not -- the saved and unsaved words beside it said the same thing again in the primary
@@ -1859,7 +1835,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1859
1835
  </button>
1860
1836
  <button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}><md-icon>save</md-icon>${i18next.t('figure.button.save')}</button>
1861
1837
  </ox-canvas-bar>
1862
- ${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset, unavailable) : nothing}
1838
+ ${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset) : nothing}
1863
1839
  ${this.catalogOpen
1864
1840
  ? html `<figure-recipe-catalog
1865
1841
  .templates=${this.templates}
@@ -1938,7 +1914,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1938
1914
  @render-error=${(e) => {
1939
1915
  this.error = e.detail.message;
1940
1916
  }}
1941
- ></figure-canvas></div>
1917
+ ></figure-canvas>${this.renderSameMeasure(unavailable)}</div>
1942
1918
  <aside slot="end">
1943
1919
  <div tabs>
1944
1920
  ${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
@@ -1967,7 +1943,6 @@ let FigureEditor = class FigureEditor extends LitElement {
1967
1943
  ${this.renderRepeatNote(view.geometry)}
1968
1944
  </section>
1969
1945
  ${this.renderPartGroups(shown, view.geometry, partFields, unavailable, held)}
1970
- ${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
1971
1946
  <figure-material-panel
1972
1947
  .body=${{ ...part, id: this.selected }}
1973
1948
  .palette=${view.palette ?? {}}
@@ -2109,58 +2084,6 @@ let FigureEditor = class FigureEditor extends LitElement {
2109
2084
  return html `<p hint held-axes>${i18next.t('figure.text.held-by-fastening')}</p>`;
2110
2085
  return html `<p hint held-axes>${i18next.t('figure.text.held-worked-out', { group: held.map(g => i18next.t('figure.label.group-' + g.name)).join(' · ') })}</p>`;
2111
2086
  }
2112
- /**
2113
- * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
2114
- * typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.
2115
- */
2116
- renderRoomOnly(fields, unavailable) {
2117
- return html `<section room-only>
2118
- <h3>${i18next.t('figure.label.room-only-inputs')}</h3>
2119
- <p hint>${i18next.t('figure.text.room-only-inputs')}</p>
2120
- <div form>
2121
- ${fields.map(f => html `<span>${fieldWordsOf(f.id, key => i18next.t(key))} (${f.unit})</span><input aria-label=${f.id} type="number" .value=${String(f.value)} disabled />`)}
2122
- </div>
2123
- <button act room-refit ?disabled=${unavailable} @click=${() => this.askRoomRefit()}><md-icon>fit_screen</md-icon>${i18next.t('figure.button.room-refit')}</button>
2124
- </section>`;
2125
- }
2126
- /** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
2127
- async askRoomRefit() {
2128
- let refit;
2129
- try {
2130
- refit = figureRoomRefitOf(this.shown().asset);
2131
- }
2132
- catch (error) {
2133
- this.error = this.said(error);
2134
- return;
2135
- }
2136
- if (!refit)
2137
- return;
2138
- const rows = roomRefitRowsOf(refit);
2139
- const outside = (list) => list.length
2140
- ? list.map(o => i18next.t('figure.text.room-part-outside', { part: partLabelOf(o.part), axis: o.axis.toUpperCase(), mm: String(o.mm) })).join(' · ')
2141
- : i18next.t('figure.text.room-nothing-outside');
2142
- const agreed = await OxPrompt.open({
2143
- type: 'question',
2144
- title: i18next.t('figure.label.room-refit'),
2145
- template: html `<table room-refit>
2146
- <tr><th></th><th>${i18next.t('figure.label.room-declared')}</th><th>${i18next.t('figure.label.room-proposed')}</th></tr>
2147
- ${rows.map(r => html `<tr>
2148
- <td>${i18next.t(`figure.label.room-at-${r.at}`)} ${r.axis.toUpperCase()}</td>
2149
- <td>${r.declared}</td>
2150
- <td>${r.same ? i18next.t('figure.text.room-same') : r.proposed}</td>
2151
- </tr>`)}
2152
- </table>
2153
- <p>${i18next.t('figure.label.room-declared')}: ${outside(refit.outside.declared)}</p>
2154
- <p>${i18next.t('figure.label.room-proposed')}: ${outside(refit.outside.proposed)}</p>
2155
- ${refit.drops.length
2156
- ? html `<p>${i18next.t('figure.text.room-drops', { fields: [...new Set(refit.drops.map(id => `${partLabelOf(id.split('.')[0])} ${fieldWordsOf(id, key => i18next.t(key))}`))].join(' · ') })}</p>`
2157
- : nothing}`,
2158
- confirmButton: { text: i18next.t('figure.button.room-refit-confirm') },
2159
- cancelButton: { text: i18next.t('figure.button.keep-editing') }
2160
- });
2161
- if (agreed)
2162
- this.linkCommand(refit.step);
2163
- }
2164
2087
  renderSingle(name, field, unavailable) {
2165
2088
  const words = name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }));
2166
2089
  return html `<span ?scrub=${!unavailable} @pointerdown=${(e) => this.scrub(field, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
@@ -2243,10 +2166,6 @@ __decorate([
2243
2166
  state(),
2244
2167
  __metadata("design:type", Object)
2245
2168
  ], FigureEditor.prototype, "faceMapping", void 0);
2246
- __decorate([
2247
- state(),
2248
- __metadata("design:type", Object)
2249
- ], FigureEditor.prototype, "retry", void 0);
2250
2169
  __decorate([
2251
2170
  state(),
2252
2171
  __metadata("design:type", String)