@things-factory/figure-ui 10.1.104 → 10.1.106

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 (50) hide show
  1. package/client/modeller/canvas.ts +6 -13
  2. package/client/modeller/editor.ts +15 -4
  3. package/client/modeller/fields.ts +7 -1
  4. package/client/modeller/figure-cavity-drawing.ts +80 -18
  5. package/client/modeller/figure-links.ts +49 -35
  6. package/client/modeller/figure-material-panel.ts +0 -1
  7. package/client/modeller/figure-parts.ts +3 -3
  8. package/client/modeller/figure-section-editor.ts +106 -11
  9. package/client/modeller/figure-section-panel.ts +15 -103
  10. package/client/modeller/section-drawing.ts +27 -4
  11. package/dist-client/modeller/canvas.d.ts +0 -2
  12. package/dist-client/modeller/canvas.js +6 -17
  13. package/dist-client/modeller/canvas.js.map +1 -1
  14. package/dist-client/modeller/editor.js +17 -4
  15. package/dist-client/modeller/editor.js.map +1 -1
  16. package/dist-client/modeller/fields.js +7 -1
  17. package/dist-client/modeller/fields.js.map +1 -1
  18. package/dist-client/modeller/figure-cavity-drawing.d.ts +6 -0
  19. package/dist-client/modeller/figure-cavity-drawing.js +81 -16
  20. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  21. package/dist-client/modeller/figure-links.d.ts +2 -2
  22. package/dist-client/modeller/figure-links.js +49 -34
  23. package/dist-client/modeller/figure-links.js.map +1 -1
  24. package/dist-client/modeller/figure-material-panel.js +0 -1
  25. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  26. package/dist-client/modeller/figure-parts.d.ts +2 -1
  27. package/dist-client/modeller/figure-parts.js +2 -2
  28. package/dist-client/modeller/figure-parts.js.map +1 -1
  29. package/dist-client/modeller/figure-section-editor.d.ts +13 -0
  30. package/dist-client/modeller/figure-section-editor.js +109 -9
  31. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  32. package/dist-client/modeller/figure-section-panel.d.ts +0 -1
  33. package/dist-client/modeller/figure-section-panel.js +14 -73
  34. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  35. package/dist-client/modeller/section-drawing.d.ts +12 -1
  36. package/dist-client/modeller/section-drawing.js +23 -3
  37. package/dist-client/modeller/section-drawing.js.map +1 -1
  38. package/dist-client/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +6 -6
  40. package/test/cavity-drawing.test.ts +10 -1
  41. package/test/command-preview.test.ts +11 -4
  42. package/test/first-minute.test.ts +6 -2
  43. package/test/layout.test.ts +11 -0
  44. package/test/section-editor.test.ts +32 -3
  45. package/test/section-panel.test.ts +5 -3
  46. package/translations/en.json +2 -10
  47. package/translations/ja.json +2 -10
  48. package/translations/ko.json +2 -10
  49. package/translations/ms.json +2 -10
  50. package/translations/zh.json +2 -10
@@ -6,8 +6,9 @@ import { i18next, localize } from '@operato/i18n'
6
6
  import { figureCavitySectionOf, figureSectionSidesOf } from '@hatiolab/figure-model'
7
7
  import type { FigureAuthoringAction, FigureEvaluatedShape, FigureSection, FigureSectionMeasure, FigureSectionPoint } from '@hatiolab/figure-model'
8
8
  import { SidePanelStyles } from './side-panel-styles.js'
9
- import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, type SectionWorking } from './section-drawing.js'
10
- import { END_FACE } from './section-controls.js'
9
+ import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, workingShapeOf, type SectionWorking } from './section-drawing.js'
10
+ import { cavityDepthOf, cavityRequestOf, cavityResultOf, END_FACE, sameOf, sidesOf } from './section-controls.js'
11
+ import './figure-cavity-drawing.js'
11
12
  import { openPopup } from '@operato/layout'
12
13
  import './figure-section-close-choice.js'
13
14
  import type { SectionCloseChoice } from './figure-section-close-choice.js'
@@ -17,6 +18,10 @@ type Axis = 'x' | 'y' | 'z'
17
18
  /** How the section is laid on the screen, by the axis it is pushed along: across to the right, then down. */
18
19
  const SCREEN: Record<Axis, [Axis, Axis]> = { y: ['x', 'z'], x: ['z', 'y'], z: ['x', 'y'] }
19
20
 
21
+ /** The inside said so two can be compared: its walls by side, its ends. */
22
+ const cavityKey = (w: SectionWorking) =>
23
+ w.cavity ? [Object.entries(w.cavity.wall).sort(), w.cavity.start, w.cavity.end] : null
24
+
20
25
  /** The editor's width and room for the solid beside it: below this the two take turns. */
21
26
  export const NARROW_BELOW = 900
22
27
 
@@ -35,6 +40,10 @@ export const NARROW_BELOW = 900
35
40
  * apex is dragged or typed like a point, and is not offered while something is fastened to the end it takes away (the
36
41
  * kernel's own refusal, `pointBlocked`).
37
42
  *
43
+ * A rectangle's or ellipse's inside is made, changed or taken away here too, by the same drawing the section panel has
44
+ * (figure-cavity-drawing), drawn and judged on the widths being typed: walls that fit only a wider section are taken
45
+ * with it, in the same command (figure-model `set-section`'s `cavity`).
46
+ *
38
47
  * What the kernel would refuse is marked on the outline and said in the list; apply is held back, and the solid shows
39
48
  * the last valid state, which the caption names. Apply sends one `set-section` command (one step of the history); cancel
40
49
  * or Esc sends nothing and lets the preview go -- after asking, when something was changed: apply it, let it go, or keep
@@ -192,6 +201,26 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
192
201
  min-width: 0;
193
202
  font: inherit;
194
203
  }
204
+ div[cavity] {
205
+ display: grid;
206
+ gap: 6px;
207
+ margin-top: 10px;
208
+ padding-top: 8px;
209
+ border-top: 1px solid var(--md-sys-color-outline-variant);
210
+ }
211
+ /* The panel's width: wider, the drawing pushed the outline out of view. */
212
+ div[cavity] figure-cavity-drawing {
213
+ max-width: 340px;
214
+ }
215
+ label[toggle] {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 6px;
219
+ flex-wrap: wrap;
220
+ }
221
+ label[toggle] small[result] {
222
+ margin-left: auto;
223
+ }
195
224
  ul[issues] {
196
225
  margin: 8px 0 0;
197
226
  padding: 0 0 0 16px;
@@ -256,7 +285,9 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
256
285
  if (!this.narrow) this.show = 'outline'
257
286
  })
258
287
 
259
- @state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null }
288
+ @state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null, cavity: null }
289
+ /** Walls one per side in the inside's drawing; on when they open differing. */
290
+ @state() private perSide = false
260
291
  @state() private selected: string | null = null
261
292
  @state() private note = ''
262
293
  private opened!: SectionWorking
@@ -274,6 +305,10 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
274
305
  private get kind() {
275
306
  return this.body && drawnOf(this.body.shape)?.section.kind
276
307
  }
308
+ /** A pushed rectangle or ellipse can have an inside; a drawn section's is a later stage. */
309
+ private get hollowable() {
310
+ return !!this.body?.shape.extrusion && (this.kind === 'rectangle' || this.kind === 'ellipse')
311
+ }
277
312
 
278
313
  connectedCallback() {
279
314
  super.connectedCallback()
@@ -282,6 +317,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
282
317
  this.lastValid = this.working
283
318
  this.openedAt = this.revision
284
319
  this.extent = this.extentOf(this.working)
320
+ this.perSide = !!this.working.cavity && sameOf(Object.values(this.working.cavity.wall)) === undefined
285
321
  window.addEventListener('keydown', this.keys, true)
286
322
  if (this.parentElement) this.room.observe(this.parentElement)
287
323
  }
@@ -310,6 +346,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
310
346
  */
311
347
  private keys = (e: KeyboardEvent) => {
312
348
  const inField = (e.composedPath()[0] as HTMLElement)?.tagName === 'INPUT'
349
+ /* A number being typed in the inside's drawing: Esc there puts the number back, not the editor away. */
350
+ if (inField && e.composedPath().some(el => (el as HTMLElement).tagName === 'FIGURE-CAVITY-DRAWING')) return
313
351
  /* While the close is being asked, the question has the keys: Esc there is 계속 편집. */
314
352
  if (this.asking) return
315
353
  const take = () => (e.preventDefault(), e.stopImmediatePropagation())
@@ -351,10 +389,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
351
389
  }
352
390
 
353
391
  /** The solid behind shows the working state, or the last valid one while the working one is refused. */
354
- private preview() {
355
- this.dispatchEvent(
356
- new CustomEvent('command-live', { detail: { action: this.action(this.lastValid) }, bubbles: true, composed: true })
357
- )
392
+ private preview(w = this.lastValid) {
393
+ this.dispatchEvent(new CustomEvent('command-live', { detail: { action: this.action(w) }, bubbles: true, composed: true }))
358
394
  }
359
395
 
360
396
  /** One command for everything changed since the editor opened; a measure not changed is not sent (it may follow another). */
@@ -363,13 +399,15 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
363
399
  const changed = (a: number | undefined, b: number | undefined) => Math.abs((a ?? 0) - (b ?? 0)) > 1e-9
364
400
  const across = Object.fromEntries(Object.entries(w.across).filter(([k, v]) => changed(v, o.across[k as Axis])))
365
401
  const apexMoved = !w.apex !== !o.apex || (!!w.apex && Object.entries(w.apex).some(([k, v]) => changed(v, o.apex![k as Axis])))
402
+ const cavityMoved = JSON.stringify(cavityKey(w)) !== JSON.stringify(cavityKey(o))
366
403
  return {
367
404
  kind: 'set-section',
368
405
  part: this.body!.id,
369
406
  ...(this.kind === 'polygon' ? { points: w.points.map(p => ({ ...p })) } : {}),
370
407
  ...(Object.keys(across).length ? { across } : {}),
371
408
  ...(changed(w.length, o.length) ? { length: w.length } : {}),
372
- ...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {})
409
+ ...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {}),
410
+ ...(cavityMoved ? { cavity: w.cavity ? { wall: { ...w.cavity.wall }, start: w.cavity.start, end: w.cavity.end } : null } : {})
373
411
  }
374
412
  }
375
413
 
@@ -403,7 +441,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
403
441
  w.points.map(p => [p.id, p.x ?? 0, p.y ?? 0, p.z ?? 0]),
404
442
  Object.entries(w.across).sort(),
405
443
  w.length,
406
- w.apex ? Object.entries(w.apex).sort() : null
444
+ w.apex ? Object.entries(w.apex).sort() : null,
445
+ cavityKey(w)
407
446
  ])
408
447
  return !!this.opened && key(this.working) !== key(this.opened)
409
448
  }
@@ -529,8 +568,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
529
568
  const grid = Math.max(10, Math.pow(10, Math.floor(Math.log10(e))) / 2)
530
569
  const lines: number[] = []
531
570
  for (let g = -Math.floor(e / grid) * grid; g <= e; g += grid) lines.push(g)
532
- const cavity = this.body!.shape.cavity
533
- const inside = cavity && !invalid ? figureCavitySectionOf(section, this.axis, cavity.wall) : undefined
571
+ const inside = w.cavity && !invalid ? figureCavitySectionOf(section, this.axis, w.cavity.wall) : undefined
534
572
  const sized = (axis: Axis) => this.measures?.measures[axis] === 'editable'
535
573
  return html`<div caption ?last-valid=${invalid}>
536
574
  ${invalid ? i18next.t('figure.text.section-editor-last-valid') : i18next.t('figure.text.section-editor-units', { h: h.toUpperCase(), v: v.toUpperCase() })}
@@ -710,6 +748,62 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
710
748
  : nothing}`
711
749
  }
712
750
 
751
+ /** The inside, as the section panel shows it, on the working section: on or off, what it makes, its drawing. */
752
+ private cavityBlock(invalid: boolean) {
753
+ const w = this.working
754
+ const shape = workingShapeOf(this.body!.shape, w)
755
+ return html`<div cavity>
756
+ <label toggle>
757
+ <input
758
+ type="checkbox"
759
+ .checked=${live(!!w.cavity)}
760
+ @change=${(ev: Event) => {
761
+ const on = (ev.target as HTMLInputElement).checked
762
+ const ask = cavityRequestOf(shape, {})
763
+ const wall = typeof ask.wall === 'number' ? Object.fromEntries(sidesOf(this.axis).map(side => [side, ask.wall as number])) : (ask.wall as Record<string, number>)
764
+ this.change({ ...w, cavity: on ? { wall, start: ask.start, end: ask.end } : null })
765
+ }}
766
+ />
767
+ ${i18next.t('figure.label.section-cavity')}
768
+ ${w.cavity && !invalid
769
+ ? html`<small result>
770
+ ${i18next.t('figure.text.section-depth', {
771
+ result: i18next.t(`figure.label.section-result-${cavityResultOf(shape.cavity!)}`),
772
+ depth: String(cavityDepthOf(shape))
773
+ })}
774
+ </small>`
775
+ : nothing}
776
+ </label>
777
+ ${w.cavity
778
+ ? html`<figure-cavity-drawing
779
+ .body=${{ id: this.body!.id, shape }}
780
+ .perSide=${this.perSide}
781
+ @command-live=${(ev: CustomEvent) => {
782
+ ev.stopPropagation()
783
+ const c = this.cavityOf(ev.detail.action)
784
+ this.preview(c && this.valid({ ...this.working, cavity: c }) ? { ...this.working, cavity: c } : this.lastValid)
785
+ }}
786
+ @command=${(ev: CustomEvent) => {
787
+ ev.stopPropagation()
788
+ const c = this.cavityOf(ev.detail.action)
789
+ if (c) this.change({ ...this.working, cavity: c })
790
+ }}
791
+ ></figure-cavity-drawing>
792
+ <label toggle>
793
+ <input type="checkbox" .checked=${live(this.perSide)} @change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)} />
794
+ ${i18next.t('figure.label.section-wall-per-side')}
795
+ </label>`
796
+ : nothing}
797
+ </div>`
798
+ }
799
+
800
+ /** The inside a drawing's set-cavity asks for, every wall written out; the drawing's commands stay in the editor. */
801
+ private cavityOf(action: FigureAuthoringAction | null): SectionWorking['cavity'] | undefined {
802
+ if (action?.kind !== 'set-cavity' || !action.cavity) return undefined
803
+ const { wall, start, end } = action.cavity
804
+ return { wall: typeof wall === 'number' ? Object.fromEntries(sidesOf(this.axis).map(side => [side, wall])) : ({ ...wall } as Record<string, number>), start, end }
805
+ }
806
+
713
807
  private list(issues: ReturnType<typeof workingIssuesOf>['issues'], words: ReturnType<typeof workingIssuesOf>['words']) {
714
808
  const [h, v] = SCREEN[this.axis]
715
809
  const marked = markedBy(issues)
@@ -773,6 +867,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
773
867
  )}
774
868
  </ol>`
775
869
  : nothing}
870
+ ${this.hollowable ? this.cavityBlock(words.length > 0) : nothing}
776
871
  ${this.note ? html`<small role="status">${this.note}</small>` : nothing}
777
872
  ${words.length
778
873
  ? html`<ul issues role="alert">
@@ -4,30 +4,12 @@ import { customElement, property, state } from 'lit/decorators.js'
4
4
  import { live } from 'lit/directives/live.js'
5
5
 
6
6
  import { i18next, localize } from '@operato/i18n'
7
- import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'
7
+ import type { FigureAuthoringAction, FigureEvaluatedShape } from '@hatiolab/figure-model'
8
8
  import { SidePanelStyles } from './side-panel-styles.js'
9
9
  import { drawnOf } from './section-drawing.js'
10
10
  import './figure-cavity-drawing.js'
11
- import {
12
- END_FACE,
13
- SIDE_FACE,
14
- cavityDepthOf,
15
- cavityRefusalOf,
16
- cavityRequestOf,
17
- cavityResultOf,
18
- cornerLimitOf,
19
- cornerRefusalOf,
20
- defaultWallOf,
21
- mm,
22
- sameOf,
23
- sidesOf,
24
- sideWallLimitOf,
25
- wallLimitOf,
26
- type FigureCavityAsk
27
- } from './section-controls.js'
11
+ import { cavityDepthOf, cavityRequestOf, cavityResultOf, cornerLimitOf, cornerRefusalOf, mm, sameOf } from './section-controls.js'
28
12
 
29
- type Axis = 'x' | 'y' | 'z'
30
- type End = 'start' | 'end'
31
13
  type Body = { id: string; shape: FigureEvaluatedShape }
32
14
 
33
15
  /**
@@ -114,7 +96,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
114
96
  </div>`
115
97
  : nothing}
116
98
  ${kind === 'rectangle' ? this.renderCorner(body) : nothing}
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}
99
+ ${/* No inside for a drawn section: the control is not there, rather than a line saying so. */ kind === 'rectangle' || kind === 'ellipse' ? this.renderCavity(body) : nothing}
118
100
  </section>`
119
101
  }
120
102
 
@@ -142,29 +124,13 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
142
124
  </div>`
143
125
  }
144
126
 
145
- private renderCavity(body: Body, axis: Axis) {
127
+ /*
128
+ * The inside: made or taken away here, and drawn -- walls dragged or their number pressed and typed, ends pressed
129
+ * open or closed (figure-cavity-drawing). The number fields under the drawing went (user 2026-10-01): they said the
130
+ * drawing again.
131
+ */
132
+ private renderCavity(body: Body) {
146
133
  const cavity = body.shape.cavity
147
- const request = (patch: Partial<FigureCavityRequest>) => cavityRequestOf(body.shape, patch)
148
- const said = (ask: FigureCavityAsk | null) =>
149
- !ask
150
- ? ''
151
- : ask.code === 'not-positive'
152
- ? i18next.t('figure.text.section-not-positive')
153
- : i18next.t(`figure.text.section-${ask.code === 'wall-too-thick' ? 'wall' : 'ends'}-over`, { max: String(ask.max) })
154
- const cavityField = (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) =>
155
- this.number(key, now, {
156
- min: 0,
157
- ask: value => {
158
- if (!(value > 0)) return i18next.t('figure.text.section-not-positive')
159
- const ask = cavityRefusalOf(body.shape, request(patch(value)))
160
- /* One side too thick: the most it can be is the width across less the wall opposite, not the even limit. */
161
- const side = key.startsWith('wall.') ? key.slice(5) : undefined
162
- if (ask?.code === 'wall-too-thick' && side && cavity)
163
- return said({ code: 'wall-too-thick', max: mm(sideWallLimitOf(body.shape, cavity.wall, side)) })
164
- return said(ask)
165
- },
166
- action: value => ({ kind: 'set-cavity', part: body.id, cavity: request(patch(value)) })
167
- })
168
134
  return html`<div field>
169
135
  <label toggle>
170
136
  <input
@@ -174,7 +140,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
174
140
  @change=${(ev: Event) => {
175
141
  const on = (ev.target as HTMLInputElement).checked
176
142
  ;(ev.target as HTMLInputElement).checked = !!cavity
177
- this.command({ kind: 'set-cavity', part: body.id, cavity: on ? request({}) : null })
143
+ this.command({ kind: 'set-cavity', part: body.id, cavity: on ? cavityRequestOf(body.shape, {}) : null })
178
144
  }}
179
145
  />
180
146
  ${i18next.t('figure.label.section-cavity')}
@@ -188,69 +154,15 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
188
154
  </small>`
189
155
  : nothing}
190
156
  </div>
191
- ${cavity ? html`<figure-cavity-drawing .body=${body} .perSide=${this.perSide} ?locked=${this.locked}></figure-cavity-drawing>` : nothing}
192
157
  ${cavity
193
- ? html`<div form>
194
- <label>${i18next.t('figure.label.section-wall')}</label>
195
- <div>
196
- ${this.perSide
197
- ? html`<div sides>
198
- ${[...sidesOf(axis)].sort((a, b) => a[0]!.localeCompare(b[0]!) || (a[1] === '-' ? -1 : 1)).map(
199
- side => html`<label>
200
- <span>${i18next.t('figure.label.face-' + SIDE_FACE[side])}</span>
201
- ${cavityField(`wall.${side}`, cavity.wall[side], value => ({ wall: { ...cavity.wall, [side]: value } }))}
202
- </label>`
203
- )}
204
- </div>`
205
- : cavityField('wall', sameOf(Object.values(cavity.wall)), value => ({ wall: value }))}
206
- ${this.perSide || this.refusals.wall
207
- ? nothing
208
- : html`<small>${i18next.t('figure.text.section-wall-at-most', { max: String(mm(wallLimitOf(body.shape))) })}</small>`}
209
- <label toggle>
210
- <input
211
- type="checkbox"
212
- .checked=${live(this.perSide)}
213
- ?disabled=${this.locked}
214
- @change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)}
215
- />
216
- ${i18next.t('figure.label.section-wall-per-side')}
217
- </label>
218
- </div>
219
- </div>
220
- ${(['start', 'end'] as const).map(end => this.renderEnd(body, axis, end, cavity[end], request, cavityField))}`
158
+ ? html`<figure-cavity-drawing .body=${body} .perSide=${this.perSide} ?locked=${this.locked}></figure-cavity-drawing>
159
+ <label toggle>
160
+ <input type="checkbox" .checked=${live(this.perSide)} ?disabled=${this.locked} @change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)} />
161
+ ${i18next.t('figure.label.section-wall-per-side')}
162
+ </label>`
221
163
  : nothing}`
222
164
  }
223
165
 
224
- private renderEnd(
225
- body: Body,
226
- axis: Axis,
227
- end: End,
228
- now: 'open' | { closed: number },
229
- request: (patch: Partial<FigureCavityRequest>) => FigureCavityRequest,
230
- cavityField: (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) => unknown
231
- ) {
232
- const closed = now !== 'open'
233
- return html`<div form>
234
- <label>${i18next.t('figure.label.face-' + END_FACE(axis, end))}</label>
235
- <div row>
236
- <select
237
- .value=${live(closed ? 'closed' : 'open')}
238
- ?disabled=${this.locked}
239
- @change=${(ev: Event) => {
240
- const select = ev.target as HTMLSelectElement
241
- const want = select.value
242
- select.value = closed ? 'closed' : 'open'
243
- this.command({ kind: 'set-cavity', part: body.id, cavity: request({ [end]: want === 'open' ? 'open' : { closed: defaultWallOf(body.shape) } }) })
244
- }}
245
- >
246
- <option value="closed">${i18next.t('figure.label.section-end-closed')}</option>
247
- <option value="open">${i18next.t('figure.label.section-end-open')}</option>
248
- </select>
249
- ${closed ? cavityField(`closed.${end}`, now.closed, value => ({ [end]: { closed: value } })) : html`<span></span>`}
250
- </div>
251
- </div>`
252
- }
253
-
254
166
  /**
255
167
  * A millimetre field. Typing draws the value (or, if the kernel would refuse it, says why under the field and draws the
256
168
  * figure as it stands); Enter or leaving sends it once; Esc puts the value back.
@@ -1,5 +1,5 @@
1
1
  import { FIGURE_SECTION_PLANE, FIGURE_SECTION_POINT_LIMIT, checkFigureSection, figureExtrusionFeaturesOf, figureSectionSidesOf } from '@hatiolab/figure-model'
2
- import type { FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionPoint } from '@hatiolab/figure-model'
2
+ import type { FigureCavityRequest, FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionPoint } from '@hatiolab/figure-model'
3
3
 
4
4
  /*
5
5
  * What the section editor does to a drawn section's points, and how it says what is wrong with them (ADR-0100 §3.2).
@@ -106,6 +106,8 @@ export interface SectionWorking {
106
106
  length: number
107
107
  /** A drawn section's end drawn to one point (a pyramid), across the plane axes; null for a push. */
108
108
  apex: Partial<Record<Axis, number>> | null
109
+ /** A pushed rectangle's or ellipse's inside, every side's wall written out; null for none or a drawn section. */
110
+ cavity: { wall: Record<string, number>; start: FigureCavityRequest['start']; end: FigureCavityRequest['end'] } | null
109
111
  }
110
112
 
111
113
  /** A pushed part's section, axis and ends -- or a pyramid's, whose end is its apex (figure-model `loft@1`). */
@@ -135,7 +137,28 @@ export function workingOf(shape: FigureEvaluatedShape): SectionWorking {
135
137
  : s.kind === 'ellipse'
136
138
  ? { [u]: 2 * s.radius[u]!, [v]: 2 * s.radius[v]! }
137
139
  : {}
138
- return { points: s.kind === 'polygon' ? s.points.map(p => ({ ...p })) : [], across, length: e.end - e.start, apex: e.apex ? { ...e.apex } : null }
140
+ const c = shape.extrusion && shape.cavity
141
+ return {
142
+ points: s.kind === 'polygon' ? s.points.map(p => ({ ...p })) : [],
143
+ across,
144
+ length: e.end - e.start,
145
+ apex: e.apex ? { ...e.apex } : null,
146
+ cavity: c ? { wall: { ...c.wall }, start: c.start, end: c.end } : null
147
+ }
148
+ }
149
+
150
+ /**
151
+ * The part as the working state would make it: its section, its length from the same start, its inside -- what the
152
+ * inside's drawing reads, so its walls are judged against the widths being typed and not the ones the editor opened with.
153
+ */
154
+ export function workingShapeOf(shape: FigureEvaluatedShape, w: SectionWorking): FigureEvaluatedShape {
155
+ const e = shape.extrusion!
156
+ const { cavity: _, ...rest } = shape
157
+ return {
158
+ ...rest,
159
+ extrusion: { ...e, section: sectionOf(shape, w), end: e.start + w.length },
160
+ ...(w.cavity ? { cavity: { id: shape.cavity?.id ?? 'inside', wall: { ...w.cavity.wall }, start: w.cavity.start, end: w.cavity.end } } : {})
161
+ } as FigureEvaluatedShape
139
162
  }
140
163
 
141
164
  /** The section the working state makes, of the part's own kind: its corners kept, its points or widths the working ones. */
@@ -159,8 +182,8 @@ export function workingIssuesOf(
159
182
  const words = issues.map(i => issueWords(i, w.points, e.axis))
160
183
  if (!(Number.isFinite(w.length) && w.length > 0)) words.push({ key: 'figure.text.section-issue-length', params: {} })
161
184
  if (w.apex && !Object.values(w.apex).every(n => Number.isFinite(n))) words.push({ key: 'figure.text.section-issue-number', params: {} })
162
- if (!issues.length && words.length === 0 && shape.cavity) {
163
- const made = figureExtrusionFeaturesOf({ axis: e.axis, section, start: e.start, end: e.start + w.length }, [shape.cavity])
185
+ if (!issues.length && words.length === 0 && w.cavity) {
186
+ const made = figureExtrusionFeaturesOf({ axis: e.axis, section, start: e.start, end: e.start + w.length }, [{ id: 'inside', ...w.cavity }])
164
187
  if ('refused' in made) {
165
188
  const r = made.refused
166
189
  words.push({ key: r.code === 'CAVITY_NO_LENGTH' ? 'figure.text.section-issue-cavity-length' : 'figure.text.section-issue-cavity-walls', params: {} })
@@ -21,8 +21,6 @@ export declare class FigureCanvas extends LitElement {
21
21
  * would reach the editor as an edit.
22
22
  */
23
23
  viewOnly: boolean;
24
- /** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */
25
- startShown: boolean;
26
24
  /** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */
27
25
  fan: FigureRenderPart | null;
28
26
  /**
@@ -1,7 +1,6 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import { LitElement, html, css, nothing, unsafeCSS } from 'lit';
2
+ import { LitElement, html, css, unsafeCSS } from 'lit';
3
3
  import { customElement, property, query } from 'lit/decorators.js';
4
- import { i18next } from '@operato/i18n';
5
4
  import { create, Model, niceGridStep } from '@hatiolab/things-scene';
6
5
  import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js';
7
6
  import { boardFor, changedKeys, componentOf, gizmoResultOf } from './stage-model.js';
@@ -31,8 +30,6 @@ let FigureCanvas = class FigureCanvas extends LitElement {
31
30
  * would reach the editor as an edit.
32
31
  */
33
32
  this.viewOnly = false;
34
- /** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */
35
- this.startShown = false;
36
33
  /** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */
37
34
  this.fan = null;
38
35
  /**
@@ -72,17 +69,13 @@ let FigureCanvas = class FigureCanvas extends LitElement {
72
69
  width: 100%;
73
70
  height: 100%;
74
71
  }
75
- p {
76
- position: absolute;
77
- inset: 45% 1rem auto;
78
- margin: 0;
79
- text-align: center;
80
- pointer-events: none;
81
- color: #f4f4f4;
82
- }
83
72
  `; }
84
73
  render() {
85
- return html `<div stage></div>${this.parts.length || this.startShown ? nothing : html `<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`;
74
+ /*
75
+ * No line of its own over an empty stage: a new figure's start card says how to begin, and an empty stage otherwise
76
+ * is one loading, or with every part hidden -- where 「도형을 추가해…」 flashed and said the wrong thing (2026-10-02).
77
+ */
78
+ return html `<div stage></div>`;
86
79
  }
87
80
  firstUpdated() {
88
81
  this.standUp();
@@ -381,10 +374,6 @@ __decorate([
381
374
  property({ type: Boolean }),
382
375
  __metadata("design:type", Object)
383
376
  ], FigureCanvas.prototype, "viewOnly", void 0);
384
- __decorate([
385
- property({ type: Boolean }),
386
- __metadata("design:type", Object)
387
- ], FigureCanvas.prototype, "startShown", void 0);
388
377
  __decorate([
389
378
  property({ attribute: false }),
390
379
  __metadata("design:type", Object)