@things-factory/figure-ui 10.1.95 → 10.1.97

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.
@@ -1631,14 +1631,13 @@ export class FigureEditor extends LitElement {
1631
1631
  const key = `${view.context}|${view.revision}|${this.selected}`
1632
1632
  if (this.detachSaid?.key === key) return this.detachSaid.words
1633
1633
  const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)
1634
- const from = 'plane' in joint.body0 ? say('figure.label.mounting-plane') : partLabelOf(joint.body0.part)
1635
1634
  let words: string[]
1636
1635
  try {
1637
1636
  const seen = this.session.previewCommand({ kind: 'detach', part: this.selected })
1638
1637
  const own = seen.moves.some(m => m.body === this.selected)
1639
1638
  words = own
1640
1639
  ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key, moves: seen.moves }, this.selected, say)]
1641
- : [say('figure.text.detach-stays', { from })]
1640
+ : /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []
1642
1641
  const children = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === this.selected).map(j => partLabelOf(j.body1.part))
1643
1642
  if (children.length) words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }))
1644
1643
  if (seen.clipsWouldGo.length) words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }))
@@ -1864,14 +1863,15 @@ export class FigureEditor extends LitElement {
1864
1863
  </div>`
1865
1864
  }
1866
1865
  /**
1867
- * The part the panel speaks of, held at its top: its kind, how its size answers, what holds it (designer 2026-09-27,
1868
- * B). No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
1866
+ * The part the panel speaks of, held at its top: its kind, and what its size follows when it follows something. No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
1869
1867
  * list's root is the way back to the model's settings.
1870
1868
  */
1871
1869
  private renderSubject() {
1872
- let summary: { kind: string; size: string; relation: string } | undefined
1870
+ let summary: { kind: string; size: string } | undefined
1873
1871
  try {
1874
- summary = this.selected ? partSummaryOf(parseFigureAsset(this.session!.source()), ownerOf(this.selected), (key, params) => i18next.t(key, params)) : undefined
1872
+ summary = this.selected
1873
+ ? partSummaryOf(parseFigureAsset(this.session!.source()), ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
1874
+ : undefined
1875
1875
  } catch {
1876
1876
  summary = undefined
1877
1877
  }
@@ -1879,7 +1879,7 @@ export class FigureEditor extends LitElement {
1879
1879
  return html`<div subject>
1880
1880
  <div subject-summary>
1881
1881
  ${summary.kind ? html`<b>${summary.kind}</b>` : nothing}
1882
- <small>${summary.size}</small><small>${summary.relation}</small>
1882
+ ${summary.size ? html`<small>${summary.size}</small>` : nothing}
1883
1883
  </div>
1884
1884
  </div>`
1885
1885
  }
@@ -2185,19 +2185,17 @@ export class FigureEditor extends LitElement {
2185
2185
  `
2186
2186
  }
2187
2187
  /**
2188
- * Which axes a rule sets, said once under the rows (position and rotation X · Y · Z set by a rule), not a line under each row
2189
- * saying the same (user 2026-10-01, the panel checked for balance). The cells themselves are dashed and read-only.
2188
+ * Why a row's numbers cannot be typed, said once under the rows by what decides them (user 2026-10-01: "set by a
2189
+ * rule" named nothing): a fastened part's place and turn are its fastening's; other numbers are worked out from
2190
+ * something else. A size that follows something is said at the panel's head, so not again here.
2190
2191
  */
2191
2192
  private renderHeldAxes(vectors: FieldGroup[]) {
2192
- const byAxes = new Map<string, string[]>()
2193
- for (const g of vectors) {
2194
- if (g.kind !== 'vector') continue
2195
- const ruled = (['x', 'y', 'z'] as const).filter(axis => !g.cells[axis])
2196
- if (!ruled.length) continue
2197
- const axes = ruled.map(a => a.toUpperCase()).join(' · ')
2198
- byAxes.set(axes, [...(byAxes.get(axes) ?? []), i18next.t('figure.label.group-' + g.name)])
2199
- }
2200
- return [...byAxes].map(([axes, groups]) => html`<p hint held-axes>${i18next.t('figure.text.axes-held-by-rule', { group: groups.join(' · '), axes })}</p>`)
2193
+ const fastened = !!(this.asset() as FigureAsset | undefined)?.joints?.some(j => j.body1.part === ownerOf(this.selected))
2194
+ const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && (['x', 'y', 'z'] as const).some(axis => !g.cells[axis]))
2195
+ if (!held.length) return nothing
2196
+ const placed = held.every(g => g.name === 'position' || g.name === 'rotation')
2197
+ if (fastened && placed) return html`<p hint held-axes>${i18next.t('figure.text.held-by-fastening')}</p>`
2198
+ 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>`
2201
2199
  }
2202
2200
  /**
2203
2201
  * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
@@ -325,9 +325,10 @@ export class FigureLinks extends localize(i18next)(LitElement) {
325
325
  const word = (axis: Axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
326
326
  const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
327
327
  const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance')
328
- return html`<p hint>${i18next.t('figure.text.resize-intent')}</p>
329
- ${asset.sizing === 'uniform' ? html`<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
330
- <div resize-now>${editable.map(axis => {
328
+ /* A size of its own on every axis is the usual: nothing to read, only the way to make it follow (user 2026-10-01). */
329
+ const allOwn = editable.every(axis => followOf(asset, this.partId, axis).of === 'own')
330
+ return html`${asset.sizing === 'uniform' ? html`<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
331
+ ${allOwn ? nothing : html`<div resize-now>${editable.map(axis => {
331
332
  const now = followOf(asset, this.partId, axis)
332
333
  const following = now.of === 'instance' && now.axis === axis
333
334
  const custom = now.of !== 'own' && !following
@@ -343,7 +344,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
343
344
  }}>${i18next.t('figure.button.resize-unfollow')}</button>`
344
345
  : nothing}
345
346
  </div>`
346
- })}</div>
347
+ })}</div>`}
347
348
  ${followsAll
348
349
  ? html`<p hint>${i18next.t('figure.text.resize-all-following')}</p>`
349
350
  : html`<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
@@ -114,11 +114,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
114
114
  </div>`
115
115
  : nothing}
116
116
  ${kind === 'rectangle' ? this.renderCorner(body) : nothing}
117
- ${kind === 'rectangle' || kind === 'ellipse'
118
- ? this.renderCavity(body, e.axis)
119
- : html`<div field>
120
- <small>${i18next.t('figure.text.section-cavity-unavailable')}</small>
121
- </div>`}
117
+ ${/* No inside for a drawn section: the control is not there, rather than a line saying so. */ kind === 'rectangle' || kind === 'ellipse' ? this.renderCavity(body, e.axis) : nothing}
122
118
  </section>`
123
119
  }
124
120
 
@@ -89,36 +89,62 @@ export function resizeAxesOf(asset: FigureAsset, part: string, pending?: readonl
89
89
  }
90
90
 
91
91
  /**
92
- * The selected part in three short lines, for the head of its panel (designer 2026-09-27, B): what it is, how its
93
- * size answers the figure's, and what holds it -- 「box · 상자 / 너비는 도형을 따라감 / 아랫면을 바닥에 유지 중」.
92
+ * Korean particles for a word the sentence is built around: the topic (은/는) and "and/with" (과/와), by whether the
93
+ * word's last syllable ends in a consonant. Other languages need none.
94
94
  */
95
- export function partSummaryOf(asset: FigureAsset, part: string, say: (key: string, params?: Record<string, string>) => string): { kind: string; size: string; relation: string } {
95
+ export function particlesOf(word: string, lang: string): { topic: string; with: string } {
96
+ if (!lang.startsWith('ko')) return { topic: word, with: word }
97
+ const last = word.trim().slice(-1).charCodeAt(0)
98
+ const closed = last >= 0xac00 && last <= 0xd7a3 && (last - 0xac00) % 28 !== 0
99
+ return { topic: word + (closed ? '은' : '는'), with: word + (closed ? '과' : '와') }
100
+ }
101
+
102
+ /**
103
+ * The selected part at the head of its panel: what it is, and -- only when a measure changes by itself -- what it
104
+ * follows, as a sentence a person reads without knowing the model's words: the model's size, a share of it, another
105
+ * part's measure, a field, a calculation (`figureFollowOf`). A size of its own and a part fastened or not say nothing
106
+ * here; the fastening is drawn in its own section (user 2026-10-01: 「규칙을 따름: 높이 (Y)」 -- "who would know what that
107
+ * means?", and "why say anything when all is as usual?").
108
+ */
109
+ export function partSummaryOf(
110
+ asset: FigureAsset,
111
+ part: string,
112
+ say: (key: string, params?: Record<string, string>) => string,
113
+ lang = 'ko'
114
+ ): { kind: string; size: string } {
96
115
  const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
97
116
  const named = startingShapeKindOf(shape as never)
98
117
  const kind = named ? say(`figure.label.primitive-${named}`) : ''
99
118
  const editable = followableAxes(asset, part)
100
119
  const round = editable.length === 2
101
- const word = (axis: Axis) => say(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
102
- const following = editable.filter(axis => {
103
- const now = followOf(asset, part, axis)
104
- return now.of === 'instance' && now.axis === axis
105
- })
106
- const ruled = editable.filter(axis => followOf(asset, part, axis).of !== 'own' && !following.includes(axis))
107
- const size = [
108
- following.length ? say('figure.text.summary-follows', { axes: following.map(word).join(' · ') }) : '',
109
- ruled.length ? say('figure.text.summary-ruled', { axes: ruled.map(word).join(' · ') }) : ''
110
- ].filter(Boolean).join(' / ') || say('figure.text.summary-size-fixed')
111
- const joint = asset.joints?.find(j => j.body1.part === part)
112
- let relation = say('figure.text.summary-free')
113
- if (joint) {
114
- const face = say(`figure.label.face-${joint.body1.face}`)
115
- const gap = typeof joint.origin?.gap === 'number' ? joint.origin.gap : 0
116
- relation =
117
- 'plane' in joint.body0
118
- ? say('figure.text.held-on-plane', { face, place: say(`figure.label.resize-place-${asset.placement ?? 'floor'}`), gap: String(gap) })
119
- : say('figure.text.held-on-part', { face, other: joint.body0.part, otherFace: say(`figure.label.face-${joint.body0.face}`) })
120
+ const word = (axis: Axis) => say(round && axis === 'x' ? 'figure.label.measure-diameter' : `figure.label.measure-${axis}`)
121
+ const pct = (times: number) => String(Math.round(times * 1000) / 10)
122
+ /* One sentence per way of following; the measures that follow the same way are said together. */
123
+ const lines = new Map<string, { key: string; params: Record<string, string>; axes: Axis[] }>()
124
+ for (const axis of editable) {
125
+ const f = followOf(asset, part, axis)
126
+ let key: string
127
+ let params: Record<string, string> = {}
128
+ if (f.of === 'own') continue
129
+ if (f.of === 'instance') {
130
+ key = f.times === 1 ? 'figure.text.summary-size-follows' : 'figure.text.summary-size-share'
131
+ params = { pct: pct(f.times), whole: say(`figure.label.measure-${f.axis}`) }
132
+ if (f.plus) (key = key + '-plus'), (params.plus = String(f.plus))
133
+ } else if (f.of === 'part') {
134
+ const other = say(`figure.label.measure-${f.axis}`)
135
+ key = (f.times ?? 1) === 1 ? 'figure.text.summary-size-part' : 'figure.text.summary-size-part-share'
136
+ params = { other: f.part, measure: particlesOf(other, lang).with, measureWord: other, pct: pct(f.times ?? 1) }
137
+ if (f.plus) (key = key + '-plus'), (params.plus = String(f.plus))
138
+ } else if (f.of === 'input') {
139
+ key = 'figure.text.summary-size-input'
140
+ params = { field: fieldWordsOf(f.input, k => say(k)) }
141
+ } else key = 'figure.text.summary-size-worked-out'
142
+ const id = key + JSON.stringify(params)
143
+ lines.set(id, { key, params, axes: [...(lines.get(id)?.axes ?? []), axis] })
120
144
  }
121
- return { kind, size, relation }
145
+ /* Nothing to say when the size is its own: only a measure that changes by itself is news (user 2026-10-01). */
146
+ const size = [...lines.values()].map(l => say(l.key, { ...l.params, axes: particlesOf(l.axes.map(word).join(' · '), lang).topic })).join(' ')
147
+ return { kind, size }
122
148
  }
123
149
 
124
150
  /**
@@ -309,8 +309,7 @@ export declare class FigureEditor extends LitElement {
309
309
  private pointBlockedOf;
310
310
  private renderStart;
311
311
  /**
312
- * The part the panel speaks of, held at its top: its kind, how its size answers, what holds it (designer 2026-09-27,
313
- * B). No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
312
+ * The part the panel speaks of, held at its top: its kind, and what its size follows when it follows something. No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
314
313
  * list's root is the way back to the model's settings.
315
314
  */
316
315
  private renderSubject;
@@ -329,8 +328,9 @@ export declare class FigureEditor extends LitElement {
329
328
  private withSizeRow;
330
329
  private renderVector;
331
330
  /**
332
- * Which axes a rule sets, said once under the rows (position and rotation X · Y · Z set by a rule), not a line under each row
333
- * saying the same (user 2026-10-01, the panel checked for balance). The cells themselves are dashed and read-only.
331
+ * Why a row's numbers cannot be typed, said once under the rows by what decides them (user 2026-10-01: "set by a
332
+ * rule" named nothing): a fastened part's place and turn are its fastening's; other numbers are worked out from
333
+ * something else. A size that follows something is said at the panel's head, so not again here.
334
334
  */
335
335
  private renderHeldAxes;
336
336
  /**
@@ -1656,14 +1656,13 @@ let FigureEditor = class FigureEditor extends LitElement {
1656
1656
  if (this.detachSaid?.key === key)
1657
1657
  return this.detachSaid.words;
1658
1658
  const say = (k, params) => i18next.t(k, params);
1659
- const from = 'plane' in joint.body0 ? say('figure.label.mounting-plane') : partLabelOf(joint.body0.part);
1660
1659
  let words;
1661
1660
  try {
1662
1661
  const seen = this.session.previewCommand({ kind: 'detach', part: this.selected });
1663
1662
  const own = seen.moves.some(m => m.body === this.selected);
1664
1663
  words = own
1665
1664
  ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key, moves: seen.moves }, this.selected, say)]
1666
- : [say('figure.text.detach-stays', { from })];
1665
+ : /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ [];
1667
1666
  const children = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === this.selected).map(j => partLabelOf(j.body1.part));
1668
1667
  if (children.length)
1669
1668
  words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }));
@@ -1897,14 +1896,15 @@ let FigureEditor = class FigureEditor extends LitElement {
1897
1896
  </div>`;
1898
1897
  }
1899
1898
  /**
1900
- * The part the panel speaks of, held at its top: its kind, how its size answers, what holds it (designer 2026-09-27,
1901
- * B). No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
1899
+ * The part the panel speaks of, held at its top: its kind, and what its size follows when it follows something. No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
1902
1900
  * list's root is the way back to the model's settings.
1903
1901
  */
1904
1902
  renderSubject() {
1905
1903
  let summary;
1906
1904
  try {
1907
- summary = this.selected ? partSummaryOf(parseFigureAsset(this.session.source()), ownerOf(this.selected), (key, params) => i18next.t(key, params)) : undefined;
1905
+ summary = this.selected
1906
+ ? partSummaryOf(parseFigureAsset(this.session.source()), ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
1907
+ : undefined;
1908
1908
  }
1909
1909
  catch {
1910
1910
  summary = undefined;
@@ -1914,7 +1914,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1914
1914
  return html `<div subject>
1915
1915
  <div subject-summary>
1916
1916
  ${summary.kind ? html `<b>${summary.kind}</b>` : nothing}
1917
- <small>${summary.size}</small><small>${summary.relation}</small>
1917
+ ${summary.size ? html `<small>${summary.size}</small>` : nothing}
1918
1918
  </div>
1919
1919
  </div>`;
1920
1920
  }
@@ -2214,21 +2214,19 @@ let FigureEditor = class FigureEditor extends LitElement {
2214
2214
  `;
2215
2215
  }
2216
2216
  /**
2217
- * Which axes a rule sets, said once under the rows (position and rotation X · Y · Z set by a rule), not a line under each row
2218
- * saying the same (user 2026-10-01, the panel checked for balance). The cells themselves are dashed and read-only.
2217
+ * Why a row's numbers cannot be typed, said once under the rows by what decides them (user 2026-10-01: "set by a
2218
+ * rule" named nothing): a fastened part's place and turn are its fastening's; other numbers are worked out from
2219
+ * something else. A size that follows something is said at the panel's head, so not again here.
2219
2220
  */
2220
2221
  renderHeldAxes(vectors) {
2221
- const byAxes = new Map();
2222
- for (const g of vectors) {
2223
- if (g.kind !== 'vector')
2224
- continue;
2225
- const ruled = ['x', 'y', 'z'].filter(axis => !g.cells[axis]);
2226
- if (!ruled.length)
2227
- continue;
2228
- const axes = ruled.map(a => a.toUpperCase()).join(' · ');
2229
- byAxes.set(axes, [...(byAxes.get(axes) ?? []), i18next.t('figure.label.group-' + g.name)]);
2230
- }
2231
- return [...byAxes].map(([axes, groups]) => html `<p hint held-axes>${i18next.t('figure.text.axes-held-by-rule', { group: groups.join(' · '), axes })}</p>`);
2222
+ const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(this.selected));
2223
+ const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && ['x', 'y', 'z'].some(axis => !g.cells[axis]));
2224
+ if (!held.length)
2225
+ return nothing;
2226
+ const placed = held.every(g => g.name === 'position' || g.name === 'rotation');
2227
+ if (fastened && placed)
2228
+ return html `<p hint held-axes>${i18next.t('figure.text.held-by-fastening')}</p>`;
2229
+ 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>`;
2232
2230
  }
2233
2231
  /**
2234
2232
  * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not