@things-factory/figure-ui 10.1.205 → 10.1.206

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/client/modeller/editor-styles.ts +16 -0
  2. package/client/modeller/editor.ts +43 -41
  3. package/client/modeller/fasten-form.ts +50 -11
  4. package/client/modeller/figure-fasten-dialog.ts +3 -2
  5. package/client/modeller/figure-links.ts +2 -2
  6. package/client/modeller/figure-preview.ts +6 -6
  7. package/client/modeller/gizmo.ts +10 -0
  8. package/client/modeller/keys.ts +10 -1
  9. package/client/modeller/links.ts +32 -41
  10. package/client/modeller/palette.ts +1 -1
  11. package/client/modeller/product-session.ts +16 -4
  12. package/client/modeller/refusal-teller.ts +3 -25
  13. package/client/modeller/refusals.ts +19 -26
  14. package/dist-client/modeller/editor-styles.js +16 -0
  15. package/dist-client/modeller/editor-styles.js.map +1 -1
  16. package/dist-client/modeller/editor.d.ts +7 -3
  17. package/dist-client/modeller/editor.js +47 -42
  18. package/dist-client/modeller/editor.js.map +1 -1
  19. package/dist-client/modeller/fasten-form.d.ts +16 -7
  20. package/dist-client/modeller/fasten-form.js +45 -11
  21. package/dist-client/modeller/fasten-form.js.map +1 -1
  22. package/dist-client/modeller/figure-fasten-dialog.d.ts +1 -1
  23. package/dist-client/modeller/figure-fasten-dialog.js +3 -3
  24. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  25. package/dist-client/modeller/figure-links.d.ts +1 -1
  26. package/dist-client/modeller/figure-links.js +2 -2
  27. package/dist-client/modeller/figure-links.js.map +1 -1
  28. package/dist-client/modeller/figure-preview.js +4 -4
  29. package/dist-client/modeller/figure-preview.js.map +1 -1
  30. package/dist-client/modeller/gizmo.d.ts +6 -0
  31. package/dist-client/modeller/gizmo.js +11 -0
  32. package/dist-client/modeller/gizmo.js.map +1 -1
  33. package/dist-client/modeller/keys.d.ts +1 -1
  34. package/dist-client/modeller/keys.js +6 -0
  35. package/dist-client/modeller/keys.js.map +1 -1
  36. package/dist-client/modeller/links.d.ts +16 -22
  37. package/dist-client/modeller/links.js +26 -39
  38. package/dist-client/modeller/links.js.map +1 -1
  39. package/dist-client/modeller/palette.js +1 -1
  40. package/dist-client/modeller/palette.js.map +1 -1
  41. package/dist-client/modeller/product-session.d.ts +15 -13
  42. package/dist-client/modeller/product-session.js +15 -5
  43. package/dist-client/modeller/product-session.js.map +1 -1
  44. package/dist-client/modeller/refusal-teller.d.ts +1 -12
  45. package/dist-client/modeller/refusal-teller.js +3 -22
  46. package/dist-client/modeller/refusal-teller.js.map +1 -1
  47. package/dist-client/modeller/refusals.d.ts +6 -14
  48. package/dist-client/modeller/refusals.js +16 -27
  49. package/dist-client/modeller/refusals.js.map +1 -1
  50. package/dist-client/route.d.ts +1 -1
  51. package/dist-client/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +7 -7
  53. package/test/browser/acceptance-page.js +1 -1
  54. package/test/browser/links.check.mjs +30 -13
  55. package/test/browser/modeller.check.mjs +45 -16
  56. package/test/browser/parts.check.mjs +49 -0
  57. package/test/browser/section.check.mjs +27 -0
  58. package/test/fasten-form.test.ts +42 -2
  59. package/test/gizmo.test.ts +12 -1
  60. package/test/keys.test.ts +11 -0
  61. package/test/links.test.ts +38 -33
  62. package/test/refusals.test.ts +17 -21
  63. package/translations/en.json +8 -13
  64. package/translations/es.json +8 -13
  65. package/translations/ja.json +8 -13
  66. package/translations/ko.json +8 -13
  67. package/translations/ms.json +8 -13
  68. package/translations/zh.json +8 -13
@@ -5,6 +5,8 @@ import {
5
5
  createFigureEditorWorkspace,
6
6
  serializeFigureAsset,
7
7
  parseFigureAsset,
8
+ figureSizeRangeFitsOf,
9
+ type FigureSizeRangeFit,
8
10
  type FigureInputChange,
9
11
  type FigurePrimitiveCreation,
10
12
  type FigurePartEdit,
@@ -71,10 +73,14 @@ export function createFigureProductSession(store: FigureProductStore) {
71
73
  */
72
74
  let done: Array<string | undefined> = [],
73
75
  undone: Array<string | undefined> = []
74
- function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string): T {
75
- const before = workspace.read().revision
76
+ /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */
77
+ let fitted: FigureSizeRangeFit[] = []
78
+ function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {
79
+ const was = workspace.read()
76
80
  const result = run()
77
- if (workspace.read().revision === before) return result
81
+ const now = workspace.read()
82
+ if (now.revision === was.revision) return result
83
+ fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []
78
84
  if (kind === 'do') {
79
85
  done.push(tag)
80
86
  undone = []
@@ -250,7 +256,13 @@ export function createFigureProductSession(store: FigureProductStore) {
250
256
  },
251
257
  /** One authoring command — what a person means, not a graph edit — as one step of the same history. */
252
258
  command(action: FigureAuthoringAction) {
253
- return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }))
259
+ return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')
260
+ },
261
+ /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */
262
+ takeSizeRangeFits(): FigureSizeRangeFit[] {
263
+ const out = fitted
264
+ fitted = []
265
+ return out
254
266
  },
255
267
  /**
256
268
  * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and
@@ -2,29 +2,23 @@ import type { PropertyValues } from 'lit'
2
2
  import { i18next } from '@operato/i18n'
3
3
  import { notify } from '@operato/layout'
4
4
  import { OxPrompt } from '@operato/popup'
5
- import type { FigureAuthoringAction, FigureClipLoss } from '@hatiolab/figure-model'
5
+ import type { FigureClipLoss } from '@hatiolab/figure-model'
6
6
  import type { FigureProductSession } from './product-session.js'
7
7
  import { clipLossOf } from './refusals.js'
8
8
 
9
- /** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
10
- export type Retry = { label: string; action?: FigureAuthoringAction; run?: () => void }
11
-
12
9
  /** What the teller reads of the editor, and what it asks of it. */
13
10
  export interface RefusalHost {
14
11
  session(): FigureProductSession | undefined
15
12
  error(): string
16
13
  notice(): string
17
- retry(): Retry | undefined
18
14
  /** Said as the editor's refusal. */
19
15
  refuse(message: string): void
20
- /** A narrowed link, confirmed: run as the editor runs a command. */
21
- command(action: FigureAuthoringAction): void
22
16
  }
23
17
 
24
18
  /**
25
19
  * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
26
20
  * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
27
- * delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
21
+ * delete and fit the space again, let a clip go -- asks in the app's prompt and goes on when confirmed; any
28
22
  * other refusal is the app's notification, which stays until it is closed.
29
23
  */
30
24
  export class RefusalTeller {
@@ -35,11 +29,7 @@ export class RefusalTeller {
35
29
  /** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */
36
30
  tell(changed: PropertyValues) {
37
31
  const error = this.of.error()
38
- if (changed.has('error') && error) {
39
- const retry = this.of.retry()
40
- if (retry) this.ask(error, retry)
41
- else notify({ level: 'error', message: error })
42
- }
32
+ if (changed.has('error') && error) notify({ level: 'error', message: error })
43
33
  const notice = this.of.notice()
44
34
  if (changed.has('notice') && notice) notify({ level: 'info', message: notice })
45
35
  const viewError = this.of.session()?.read()?.error ?? ''
@@ -91,16 +81,4 @@ export class RefusalTeller {
91
81
  }
92
82
  asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again)
93
83
  }
94
-
95
- private async ask(message: string, retry: Retry) {
96
- const agreed = await OxPrompt.open({
97
- type: 'question',
98
- text: message,
99
- confirmButton: { text: retry.label },
100
- cancelButton: { text: i18next.t('figure.button.keep-editing') }
101
- })
102
- if (!agreed) return
103
- if (retry.run) retry.run()
104
- else this.of.command(retry.action!)
105
- }
106
84
  }
@@ -7,7 +7,7 @@
7
7
  * English is for logs and readers of the kernel, and a screen that read it went quiet whenever a sentence changed or a
8
8
  * new one came under the same code (ADR-0054). A refusal with no details the screen knows is said with its code.
9
9
  */
10
- import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureClipLoss, type FigureRefusalDetails } from '@hatiolab/figure-model'
10
+ import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureClipLoss, type FigureRefusalDetails, type FigureSizeRangeFit } from '@hatiolab/figure-model'
11
11
 
12
12
  type Say = (key: string, params: Record<string, string>) => string
13
13
  type Reason = FigureRefusalDetails['reason']
@@ -23,36 +23,29 @@ const numberOf = (value: number) => value.toLocaleString(undefined, { maximumFra
23
23
  /** Bytes in KiB, rounded up: a figure a few bytes over its limit is not said to be at it. */
24
24
  const kibOf = (bytes: number) => Math.ceil(bytes / 1024).toLocaleString()
25
25
 
26
- /** A size range the figure cannot be drawn over, or one that leaves out its own size, in words; nothing for any other refusal. */
26
+ /**
27
+ * The ends an author typed for the size on a drawing, refused: they leave out the size the figure is drawn at, or start
28
+ * at zero. The only refusals the size range has left (ADR-0087 decision 6); nothing for any other refusal.
29
+ */
27
30
  export function sizeRangeRefusalOf(error: unknown, say: Say): string | null {
28
31
  if (codeOf(error) !== 'INSTANCE_SIZE_RANGE') return null
29
- const reach = detailsOf(error, 'size-cannot-reach')
30
- if (reach)
31
- return say(reach.bound === 'min' ? 'figure.text.size-range-too-small' : 'figure.text.size-range-too-large', { axis: reach.axis.toUpperCase(), mm: mmOf(reach.need), part: reach.part ?? '' })
32
32
  const outside = detailsOf(error, 'size-outside-range')
33
- if (outside) return say('figure.text.size-range-leaves-out-size', { axis: outside.axis.toUpperCase(), at: mmOf(outside.at), min: mmOf(outside.min), max: mmOf(outside.max) })
33
+ if (outside)
34
+ return say('figure.text.size-range-leaves-out-size', {
35
+ axis: say(`figure.label.resize-${outside.axis}`, {}),
36
+ range: `${mmOf(outside.min ?? 0)} – ${outside.max === undefined ? '∞' : mmOf(outside.max)}`,
37
+ at: mmOf(outside.at)
38
+ })
34
39
  const zero = detailsOf(error, 'size-min-not-positive')
35
- if (zero) return say('figure.text.size-range-from-zero', { axis: zero.axis.toUpperCase() })
36
- const gone = detailsOf(error, 'size-already-gone')
37
- if (gone) return say('figure.text.size-range-part-gone', { axis: gone.axis.toUpperCase(), at: mmOf(gone.at) })
40
+ if (zero) return say('figure.text.size-range-from-zero', { axis: say(`figure.label.resize-${zero.axis}`, {}) })
38
41
  return null
39
42
  }
40
43
 
41
- /**
42
- * A link refused because the size range it would leave cannot hold: measuring again cannot help (the link is not there
43
- * to measure), so the author is offered the same link that narrows the range to what the figure then allows.
44
- */
45
- export function narrowingRetryOf<A extends { kind: string }>(action: A, error: unknown, say: Say): { message: string; label: string; retry: A & { narrowRange: true } } | null {
46
- if (action.kind !== 'link-dimension' || codeOf(error) !== 'INSTANCE_SIZE_RANGE') return null
47
- const reach = detailsOf(error, 'size-cannot-reach')
48
- if (!reach) return null
49
- const words = { axis: reach.axis.toUpperCase(), mm: mmOf(reach.need), part: reach.part ?? '' }
50
- const below = reach.bound === 'min'
51
- return {
52
- message: say(below ? 'figure.text.link-needs-smaller-range' : 'figure.text.link-needs-larger-range', words),
53
- label: say(below ? 'figure.button.narrow-range-from' : 'figure.button.narrow-range-to', words),
54
- retry: { ...action, narrowRange: true }
55
- }
44
+ /** What a command did to the ends the author typed, in one line each: what moved, from what to what. */
45
+ export function sizeRangeFitsOf(fits: readonly FigureSizeRangeFit[], say: Say): string {
46
+ return fits
47
+ .map(f => say(`figure.text.size-range-moved-${f.bound}`, { axis: say(`figure.label.resize-${f.axis}`, {}), from: mmOf(f.from), to: mmOf(f.to) }))
48
+ .join(' ')
56
49
  }
57
50
 
58
51
  /**
@@ -297,7 +290,7 @@ export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
297
290
  const setting = detailsOf(error, 'motion-setting')
298
291
  if (setting) return say('figure.text.motion-setting-' + setting.why, { group: setting.group, clip: setting.clip ?? '' })
299
292
  const key = codeOf(error) === 'MOTION_GROUP' ? detailsOf(error, 'value-outside-range') : undefined
300
- if (key) return say('figure.text.motion-key-outside-range', { input: key.input, value: numberOf(key.value), min: numberOf(key.min), max: numberOf(key.max) })
293
+ if (key) return say('figure.text.motion-key-outside-range', { input: key.input, value: numberOf(key.value), min: numberOf(key.min!), max: numberOf(key.max!) })
301
294
  return null
302
295
  }
303
296
 
@@ -310,7 +303,7 @@ export function inputRangeRefusalOf(error: unknown, say: Say): string | null {
310
303
  const range = detailsOf(error, 'value-outside-range')
311
304
  if (!range) return null
312
305
  const axis = (Object.entries(FIGURE_INSTANCE_SIZE_INPUTS) as Array<[string, string]>).find(([, id]) => id === range.input)?.[0]
313
- const params = { value: numberOf(range.value), min: numberOf(range.min), max: numberOf(range.max) }
306
+ const params = { value: numberOf(range.value), min: range.min === undefined ? '−∞' : numberOf(range.min), max: range.max === undefined ? '∞' : numberOf(range.max) }
314
307
  return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params)
315
308
  }
316
309
 
@@ -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
@@ -28,7 +28,7 @@ import './figure-preview.js';
28
28
  import './figure-settings.js';
29
29
  import { DEFAULT_VIEW } from '@things-factory/figure-viewer/dist-client/figure-view.js';
30
30
  import './figure-capability-panel.js';
31
- import { gizmoChanges, gizmoReachOf, sizeKeepOf } from './gizmo.js';
31
+ import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf, sizeKeepOf } from './gizmo.js';
32
32
  import { keptBySizeOf, partsAtFrame, SIZE_KEEPS_WORDS, startSizeDrag, throughPlaneOf } from './size-drag.js';
33
33
  import { ModellerKeys } from './modeller-keys.js';
34
34
  import { PaneFold } from './pane-fold.js';
@@ -50,9 +50,9 @@ import { END_FACE } from './section-controls.js';
50
50
  import { nudgeOf, placementOf } from './stage-model.js';
51
51
  import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js';
52
52
  import { gateFindingStyles, renderGateFinding } from './gate-findings.js';
53
- import { editRefusalOf, inDeclaredRoom, narrowingRetryOf, partNameRefusalOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js';
53
+ import { editRefusalOf, inDeclaredRoom, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js';
54
54
  import { HelpToggles } from './help.js';
55
- import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
55
+ import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
56
56
  import { jointFanOf } from './joint-fan.js';
57
57
  import { derivedOf } from './revision.js';
58
58
  import { jointDragOf } from './joint-drag.js';
@@ -137,14 +137,14 @@ let FigureEditor = class FigureEditor extends LitElement {
137
137
  selected: () => this.selected,
138
138
  act: action => this.keyAction(action)
139
139
  });
140
+ /** The part Ctrl+C took: pasted as the kernel's duplicate. Not kept past the editor, nor written anywhere. */
141
+ this.copied = '';
140
142
  this.paneFold = new PaneFold(this, () => this.panes);
141
143
  this.refusals = new RefusalTeller({
142
144
  session: () => this.session,
143
145
  error: () => this.error,
144
146
  notice: () => this.notice,
145
- retry: () => this.retry,
146
- refuse: message => (this.error = message),
147
- command: action => this.linkCommand(action)
147
+ refuse: message => (this.error = message)
148
148
  });
149
149
  this.panelMemory = new PanelMemory(() => this.renderRoot);
150
150
  /** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
@@ -231,6 +231,13 @@ let FigureEditor = class FigureEditor extends LitElement {
231
231
  </button>`)}
232
232
  </span>`;
233
233
  }
234
+ /** A copied part still in the figure is duplicated -- a part of its own, named by figure-model; one gone, nothing. */
235
+ pasteCopied() {
236
+ const asset = this.session?.revisionNow()?.asset;
237
+ if (!this.copied || !asset?.document.model.nodes.some(n => n.op === 'place@1' && n.id === this.copied))
238
+ return;
239
+ this.duplicatePart(this.copied);
240
+ }
234
241
  keyAction(action) {
235
242
  const view = this.session.read();
236
243
  const canvas = this.renderRoot.querySelector('figure-canvas');
@@ -242,6 +249,12 @@ let FigureEditor = class FigureEditor extends LitElement {
242
249
  view.canRedo && this.run(() => this.session.redo());
243
250
  else if (action === 'duplicate')
244
251
  this.duplicatePart(this.selected);
252
+ else if (action === 'copy')
253
+ this.copied = this.selected;
254
+ else if (action === 'paste')
255
+ this.pasteCopied();
256
+ else if (action === 'hold')
257
+ return;
245
258
  else if (action === 'remove')
246
259
  this.removePart(this.selected);
247
260
  else
@@ -295,6 +308,15 @@ let FigureEditor = class FigureEditor extends LitElement {
295
308
  this.selected = next;
296
309
  });
297
310
  }
311
+ /** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
312
+ renderSameMeasure(unavailable) {
313
+ if (unavailable || this.gizmoMode !== 'scale' || !this.selected)
314
+ return nothing;
315
+ const axes = sameMeasureAxesOf(this.reachOf(this.selected));
316
+ return axes
317
+ ? html `<p same-measure role="status">${i18next.t('figure.text.gizmo-same-measure', { axes: axes.map(a => a.toUpperCase()).join(' · ') })}</p>`
318
+ : nothing;
319
+ }
298
320
  /** The scene toggles: the mode already shown takes the gizmo away. */
299
321
  useGizmo(mode) {
300
322
  /*
@@ -380,6 +402,10 @@ let FigureEditor = class FigureEditor extends LitElement {
380
402
  this.live = action ? { ...rest, action } : rest;
381
403
  }
382
404
  changed() {
405
+ /* What the step did to the size ends the author typed, said with whatever else it said. */
406
+ const fits = this.session?.takeSizeRangeFits() ?? [];
407
+ if (fits.length)
408
+ this.notice = [this.notice, sizeRangeFitsOf(fits, (key, params) => i18next.t(key, params))].filter(Boolean).join(' ');
383
409
  this.requestUpdate();
384
410
  this.dispatchEvent(new CustomEvent('changed', { bubbles: true, composed: true }));
385
411
  }
@@ -483,7 +509,6 @@ let FigureEditor = class FigureEditor extends LitElement {
483
509
  this.run(() => this.session.edit({ kind: 'move-part', target: id, to }));
484
510
  }
485
511
  removePart(id, refitRoom = false) {
486
- this.retry = undefined;
487
512
  try {
488
513
  const view = this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
489
514
  this.error = '';
@@ -962,7 +987,6 @@ let FigureEditor = class FigureEditor extends LitElement {
962
987
  linkCommand(action, from = 'panel') {
963
988
  /* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
964
989
  this.holdAction(null);
965
- this.retry = undefined;
966
990
  this.notice = '';
967
991
  const hadSize = this.hasSize();
968
992
  const hadSpace = !!this.shown()?.asset.occupancy;
@@ -995,12 +1019,6 @@ let FigureEditor = class FigureEditor extends LitElement {
995
1019
  this.error = '';
996
1020
  return false;
997
1021
  }
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
1022
  this.error =
1005
1023
  code === 'DIMENSION_CYCLE'
1006
1024
  ? i18next.t('figure.text.follow-would-loop')
@@ -1096,12 +1114,12 @@ let FigureEditor = class FigureEditor extends LitElement {
1096
1114
  label.addEventListener('pointerup', up);
1097
1115
  label.addEventListener('pointercancel', up);
1098
1116
  }
1099
- /** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
1117
+ /** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
1100
1118
  renderInstanceRange(shown, unavailable) {
1101
1119
  if (!shown)
1102
1120
  return nothing;
1103
1121
  const asset = shown.asset;
1104
- /* Measured once per version: measuring evaluates the figure at its edges. */
1122
+ /* Computed once per version: figure-model evaluates the figure at its edges. */
1105
1123
  const measured = derivedOf(shown, 'instance-range', () => {
1106
1124
  let of;
1107
1125
  let toDeclare;
@@ -1112,7 +1130,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1112
1130
  of = undefined;
1113
1131
  }
1114
1132
  try {
1115
- toDeclare = of ? undefined : instanceRangeToDeclare(asset);
1133
+ toDeclare = of ? undefined : instanceSizeToDeclare(asset);
1116
1134
  }
1117
1135
  catch {
1118
1136
  toDeclare = undefined;
@@ -1133,14 +1151,18 @@ let FigureEditor = class FigureEditor extends LitElement {
1133
1151
  return html `<section board-size>
1134
1152
  ${title('figure.text.board-size-fixed-explained')}
1135
1153
  <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 })}>
1154
+ <button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', at: toDeclare.at })}>
1137
1155
  <md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
1138
1156
  </button>
1139
1157
  </section>`;
1140
1158
  if (!range)
1141
1159
  return nothing;
1142
- const differs = rangeDiffers(range.declared, range.measured);
1143
1160
  const followers = sizeFollowersOf(asset);
1161
+ /*
1162
+ An end the author typed shows its number; an open end is empty, with what the parts allow written faintly in it
1163
+ (ADR-0087 decision 6). Typing narrows that end, emptying the field opens it again.
1164
+ */
1165
+ const shownEnd = (r, bound) => (bound === 'max' && !Number.isFinite(r.max) ? '∞' : mm(r[bound]));
1144
1166
  return html `<section board-size>
1145
1167
  ${title('figure.text.board-size-range-explained')}
1146
1168
  ${['x', 'y', 'z'].map(axis => html `<div inline-field>
@@ -1150,12 +1172,12 @@ let FigureEditor = class FigureEditor extends LitElement {
1150
1172
  aria-label=${`size.${axis}.${bound}`}
1151
1173
  type="number"
1152
1174
  step="any"
1153
- .value=${live(String(Math.round(range.declared[axis][bound])))}
1175
+ .value=${live(range[axis].narrowed[bound] ? String(Math.round(range[axis][bound])) : '')}
1176
+ placeholder=${shownEnd(range[axis], bound)}
1154
1177
  ?disabled=${unavailable}
1155
1178
  @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 });
1179
+ const typed = e.target.valueAsNumber;
1180
+ this.linkCommand({ kind: 'declare-instance-size', range: rangeWith(range, axis, bound, typed), replace: true });
1159
1181
  }}
1160
1182
  />`)}
1161
1183
  </div>
@@ -1167,19 +1189,6 @@ let FigureEditor = class FigureEditor extends LitElement {
1167
1189
  })
1168
1190
  : i18next.t('figure.text.board-size-no-followers')}
1169
1191
  </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
1192
  ${this.renderSizingRule(asset, unavailable)}
1184
1193
  </section>`;
1185
1194
  }
@@ -1334,7 +1343,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1334
1343
  const form = this.publishing?.part === part
1335
1344
  ? this.publishing
1336
1345
  : listed && input
1337
- ? { part, label: listed.label, min: input.min, max: input.max }
1346
+ ? /* A published input has both ends (figure-model checkPublicInput). */ { part, label: listed.label, min: input.min, max: input.max }
1338
1347
  : undefined;
1339
1348
  const open = !!form;
1340
1349
  const edit = (key) => (e) => {
@@ -1938,7 +1947,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1938
1947
  @render-error=${(e) => {
1939
1948
  this.error = e.detail.message;
1940
1949
  }}
1941
- ></figure-canvas></div>
1950
+ ></figure-canvas>${this.renderSameMeasure(unavailable)}</div>
1942
1951
  <aside slot="end">
1943
1952
  <div tabs>
1944
1953
  ${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
@@ -2243,10 +2252,6 @@ __decorate([
2243
2252
  state(),
2244
2253
  __metadata("design:type", Object)
2245
2254
  ], FigureEditor.prototype, "faceMapping", void 0);
2246
- __decorate([
2247
- state(),
2248
- __metadata("design:type", Object)
2249
- ], FigureEditor.prototype, "retry", void 0);
2250
2255
  __decorate([
2251
2256
  state(),
2252
2257
  __metadata("design:type", String)