@things-factory/figure-ui 10.1.206 → 10.1.208

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 (70) hide show
  1. package/client/modeller/editor.ts +11 -97
  2. package/client/modeller/figure-facts.ts +1 -1
  3. package/client/modeller/figure-history-panel.ts +1 -29
  4. package/client/modeller/figure-preview.ts +5 -7
  5. package/client/modeller/figure-verdict.ts +2 -17
  6. package/client/modeller/maturity.ts +11 -27
  7. package/client/modeller/refusals.ts +1 -7
  8. package/client/pages/figure-modeller-page.ts +7 -22
  9. package/dist-client/modeller/editor.d.ts +1 -8
  10. package/dist-client/modeller/editor.js +12 -98
  11. package/dist-client/modeller/editor.js.map +1 -1
  12. package/dist-client/modeller/figure-facts.js +1 -1
  13. package/dist-client/modeller/figure-facts.js.map +1 -1
  14. package/dist-client/modeller/figure-fasten-dialog.d.ts +1 -1
  15. package/dist-client/modeller/figure-history-panel.d.ts +0 -16
  16. package/dist-client/modeller/figure-history-panel.js +1 -27
  17. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  18. package/dist-client/modeller/figure-links.d.ts +1 -1
  19. package/dist-client/modeller/figure-preview.js +5 -8
  20. package/dist-client/modeller/figure-preview.js.map +1 -1
  21. package/dist-client/modeller/figure-verdict.d.ts +0 -5
  22. package/dist-client/modeller/figure-verdict.js +2 -21
  23. package/dist-client/modeller/figure-verdict.js.map +1 -1
  24. package/dist-client/modeller/maturity.d.ts +2 -4
  25. package/dist-client/modeller/maturity.js +12 -26
  26. package/dist-client/modeller/maturity.js.map +1 -1
  27. package/dist-client/modeller/refusals.d.ts +1 -3
  28. package/dist-client/modeller/refusals.js +1 -7
  29. package/dist-client/modeller/refusals.js.map +1 -1
  30. package/dist-client/pages/figure-modeller-page.d.ts +1 -3
  31. package/dist-client/pages/figure-modeller-page.js +7 -23
  32. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  33. package/dist-client/route.d.ts +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +8 -8
  36. package/test/browser/acceptance-page.js +3 -3
  37. package/test/browser/detail-page.js +1 -3
  38. package/test/browser/detail-tabs-page.js +1 -2
  39. package/test/browser/links.check.mjs +2 -37
  40. package/test/browser/modeller.check.mjs +1 -3
  41. package/test/browser/parts-page.js +2 -2
  42. package/test/browser/parts.check.mjs +2 -12
  43. package/test/browser/product-page.check.mjs +6 -9
  44. package/test/browser/product-page.js +1 -2
  45. package/test/browser/screen-page.js +2 -6
  46. package/test/browser/screen.check.mjs +8 -65
  47. package/test/first-minute.test.ts +2 -3
  48. package/test/fixtures/joint-column.json +1 -66
  49. package/test/fixtures/joint-roller.json +1 -66
  50. package/test/fixtures/robot-arm-6dof.json +2 -67
  51. package/test/harness-serve.test.ts +1 -1
  52. package/test/heading-scale-guard.test.ts +3 -3
  53. package/test/links.test.ts +1 -1
  54. package/test/parts-wiring.test.ts +1 -1
  55. package/test/properties.test.ts +9 -12
  56. package/test/refusals.test.ts +3 -21
  57. package/test/resize-with-placement.test.ts +13 -15
  58. package/test/revision.test.ts +1 -2
  59. package/test/verdict.test.ts +1 -15
  60. package/translations/en.json +0 -19
  61. package/translations/es.json +0 -19
  62. package/translations/ja.json +0 -19
  63. package/translations/ko.json +9 -28
  64. package/translations/ms.json +0 -19
  65. package/translations/zh.json +0 -19
  66. package/client/modeller/occupancy.ts +0 -51
  67. package/dist-client/modeller/occupancy.d.ts +0 -31
  68. package/dist-client/modeller/occupancy.js +0 -36
  69. package/dist-client/modeller/occupancy.js.map +0 -1
  70. package/test/release-panel.test.ts +0 -53
@@ -6,7 +6,7 @@ import { openPopup } from '@operato/layout'
6
6
  import { OxPrompt } from '@operato/popup'
7
7
  import { customElement, property, state } from 'lit/decorators.js'
8
8
  import { live } from 'lit/directives/live.js'
9
- import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model'
9
+ import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang } from '@hatiolab/figure-model'
10
10
  import type { RecipeTemplate } from './recipes.js'
11
11
  import './figure-recipe-catalog.js'
12
12
  import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement, FigureFaceMappingNeed, FigureFaceRemap } from '@hatiolab/figure-model'
@@ -57,10 +57,9 @@ import { nudgeOf, placementOf } from './stage-model.js'
57
57
 
58
58
  /** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
59
59
  type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
60
- import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js'
61
60
  import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
62
61
  import type { FigureGateFinding } from '../types.js'
63
- import { editRefusalOf, inDeclaredRoom, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
62
+ import { editRefusalOf, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
64
63
  import type { FigureRenderPart } from './renderer.js'
65
64
  import type { FigureBudgetLine } from './maturity.js'
66
65
  import { HelpToggles } from './help.js'
@@ -524,26 +523,16 @@ export class FigureEditor extends LitElement {
524
523
  private movePart(id: string, to: number) {
525
524
  this.run(() => this.session!.edit({ kind: 'move-part', target: id, to }))
526
525
  }
527
- private removePart(id: string, refitRoom = false) {
526
+ private removePart(id: string) {
528
527
  try {
529
- const view = this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
528
+ const view = this.session!.edit({ kind: 'remove-part', target: id }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
530
529
  this.error = ''
531
530
  /* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
532
531
  this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? ''
533
532
  if (this.selected === id) this.selected = ''
534
533
  this.changed()
535
534
  } catch (error) {
536
- if (this.refusals.askClipLoss(error, () => this.removePart(id, refitRoom))) return
537
- /*
538
- A part in the declared space goes, and the space is fitted to the parts left, in the same click: every space
539
- this product declares is fitted to the parts (「부품에 맞추기」, the AI's fit-room -- no padding), so fitting it
540
- again keeps what it says, and there is nothing to ask (user, 2026-09-27: 「이 확인이 꼭 필요한가 ?」).
541
- */
542
- if (!refitRoom && inDeclaredRoom(error)) {
543
- this.removePart(id, true)
544
- if (!this.error) this.notice = [this.notice, i18next.t('figure.text.removal-space-refitted')].filter(Boolean).join(' ')
545
- return
546
- }
535
+ if (this.refusals.askClipLoss(error, () => this.removePart(id))) return
547
536
  this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error)
548
537
  }
549
538
  }
@@ -984,7 +973,6 @@ export class FigureEditor extends LitElement {
984
973
  this.holdAction(null)
985
974
  this.notice = ''
986
975
  const hadSize = this.hasSize()
987
- const hadSpace = !!this.shown()?.asset.occupancy
988
976
  try {
989
977
  this.session!.command(action)
990
978
  this.error = ''
@@ -992,9 +980,7 @@ export class FigureEditor extends LitElement {
992
980
  /* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
993
981
  if (action.kind === 'follow-size' && !hadSize) {
994
982
  const size = this.designValues()
995
- /* The same step may have fitted the space the figure takes to its parts (figure-model, ruling 2026-10-16 Q1 A): one line says both. */
996
- const spaceToo = !hadSpace && !!this.shown()?.asset.occupancy
997
- this.notice = i18next.t(spaceToo ? 'figure.text.size-and-space-declared-to-follow' : 'figure.text.size-declared-to-follow', {
983
+ this.notice = i18next.t('figure.text.size-declared-to-follow', {
998
984
  x: Math.round(size['size.x'] ?? 0).toLocaleString(),
999
985
  y: Math.round(size['size.y'] ?? 0).toLocaleString(),
1000
986
  z: Math.round(size['size.z'] ?? 0).toLocaleString()
@@ -1419,19 +1405,13 @@ export class FigureEditor extends LitElement {
1419
1405
  if (state === 'released') return i18next.t('figure.text.release-state-released', { version: String(version) })
1420
1406
  return version > 0 ? i18next.t('figure.text.release-state-draft-over', { version: String(version) }) : i18next.t('figure.text.release-state-draft-new')
1421
1407
  }
1422
- /** The findings that block release, under the bar beside 「발행」, each with the way out the verdict tab offers for it. */
1423
- private renderReleaseBlockers(blockers: FigureGateFinding[], asset: FigureAsset | undefined, unavailable: boolean) {
1408
+ /** The findings that block release, under the bar beside 「발행」. */
1409
+ private renderReleaseBlockers(blockers: FigureGateFinding[], asset: FigureAsset | undefined) {
1424
1410
  const parts = (asset?.appearance ?? []).map(look => look.target)
1425
- const way = (code: string) =>
1426
- (code === 'occupancy-undeclared' || code === 'part-outside-occupancy') && asset && parts.length
1427
- ? html`<button act ?disabled=${unavailable} @click=${() => this.linkCommand(occupancyFromParts(asset))}>
1428
- <md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
1429
- </button>`
1430
- : nothing
1431
1411
  return html`<div release-blockers role="dialog" aria-label=${i18next.t('figure.text.must-fix-to-release')} @keydown=${(e: KeyboardEvent) => e.key === 'Escape' && (this.blockersOpen = false)}>
1432
1412
  <ul gate>
1433
1413
  <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
1434
- ${blockers.map(finding => renderGateFinding(finding, true, way(finding.code), parts))}
1414
+ ${blockers.map(finding => renderGateFinding(finding, true, nothing, parts))}
1435
1415
  </ul>
1436
1416
  </div>`
1437
1417
  }
@@ -1760,19 +1740,9 @@ export class FigureEditor extends LitElement {
1760
1740
  /* A repeat's own row values (its pitch) have their own section, not the shape's. */
1761
1741
  const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.') && !i.id.startsWith(this.selected + '.repeat.'))
1762
1742
  const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(ownerOf(g.id) + '.')))
1763
- /*
1764
- A part's field only the declared room still reads -- its old height position once the floor holds it -- moves
1765
- nothing; it is shown apart as what it is, not as a position (ADR-0097, designer 2026-09-27).
1766
- */
1767
1743
  const shown = this.shown()
1768
1744
  const asset = shown?.asset
1769
- let roomOnly: string[] = []
1770
- try {
1771
- roomOnly = this.selected && asset ? figureRoomOnlyInputsOf(asset, this.selected) : []
1772
- } catch {
1773
- roomOnly = []
1774
- }
1775
- const partFields = own.filter(i => !roomOnly.includes(i.id))
1745
+ const partFields = own
1776
1746
  /* The numbers a rule gives the axes that have no field of their own -- shown read-only, never as a blank. */
1777
1747
  let held: HeldValues | undefined
1778
1748
  try {
@@ -1789,7 +1759,6 @@ export class FigureEditor extends LitElement {
1789
1759
  } catch {
1790
1760
  held = undefined
1791
1761
  }
1792
- const roomFields = own.filter(i => roomOnly.includes(i.id))
1793
1762
  /*
1794
1763
  Only what is under way is said. Saved or not is the save button's: filled and pressable when there is something
1795
1764
  to save, dimmed when not -- the saved and unsaved words beside it said the same thing again in the primary
@@ -1842,7 +1811,7 @@ export class FigureEditor extends LitElement {
1842
1811
  </button>
1843
1812
  <button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}><md-icon>save</md-icon>${i18next.t('figure.button.save')}</button>
1844
1813
  </ox-canvas-bar>
1845
- ${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset, unavailable) : nothing}
1814
+ ${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset) : nothing}
1846
1815
  ${this.catalogOpen
1847
1816
  ? html`<figure-recipe-catalog
1848
1817
  .templates=${this.templates}
@@ -1952,7 +1921,6 @@ export class FigureEditor extends LitElement {
1952
1921
  ${this.renderRepeatNote(view.geometry)}
1953
1922
  </section>
1954
1923
  ${this.renderPartGroups(shown, view.geometry, partFields, unavailable, held)}
1955
- ${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
1956
1924
  <figure-material-panel
1957
1925
  .body=${{ ...part, id: this.selected }}
1958
1926
  .palette=${view.palette ?? {}}
@@ -2098,60 +2066,6 @@ export class FigureEditor extends LitElement {
2098
2066
  if (fastened && placed) return html`<p hint held-axes>${i18next.t('figure.text.held-by-fastening')}</p>`
2099
2067
  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>`
2100
2068
  }
2101
- /**
2102
- * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
2103
- * typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.
2104
- */
2105
- private renderRoomOnly(fields: FieldLike[], unavailable: boolean) {
2106
- return html`<section room-only>
2107
- <h3>${i18next.t('figure.label.room-only-inputs')}</h3>
2108
- <p hint>${i18next.t('figure.text.room-only-inputs')}</p>
2109
- <div form>
2110
- ${fields.map(
2111
- f => html`<span>${fieldWordsOf(f.id, key => i18next.t(key))} (${f.unit})</span><input aria-label=${f.id} type="number" .value=${String(f.value)} disabled />`
2112
- )}
2113
- </div>
2114
- <button act room-refit ?disabled=${unavailable} @click=${() => this.askRoomRefit()}><md-icon>fit_screen</md-icon>${i18next.t('figure.button.room-refit')}</button>
2115
- </section>`
2116
- }
2117
- /** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
2118
- private async askRoomRefit() {
2119
- let refit
2120
- try {
2121
- refit = figureRoomRefitOf(this.shown()!.asset)
2122
- } catch (error) {
2123
- this.error = this.said(error)
2124
- return
2125
- }
2126
- if (!refit) return
2127
- const rows = roomRefitRowsOf(refit)
2128
- const outside = (list: typeof refit.outside.declared) =>
2129
- list.length
2130
- ? list.map(o => i18next.t('figure.text.room-part-outside', { part: partLabelOf(o.part), axis: o.axis.toUpperCase(), mm: String(o.mm) })).join(' · ')
2131
- : i18next.t('figure.text.room-nothing-outside')
2132
- const agreed = await OxPrompt.open({
2133
- type: 'question',
2134
- title: i18next.t('figure.label.room-refit'),
2135
- template: html`<table room-refit>
2136
- <tr><th></th><th>${i18next.t('figure.label.room-declared')}</th><th>${i18next.t('figure.label.room-proposed')}</th></tr>
2137
- ${rows.map(
2138
- r => html`<tr>
2139
- <td>${i18next.t(`figure.label.room-at-${r.at}`)} ${r.axis.toUpperCase()}</td>
2140
- <td>${r.declared}</td>
2141
- <td>${r.same ? i18next.t('figure.text.room-same') : r.proposed}</td>
2142
- </tr>`
2143
- )}
2144
- </table>
2145
- <p>${i18next.t('figure.label.room-declared')}: ${outside(refit.outside.declared)}</p>
2146
- <p>${i18next.t('figure.label.room-proposed')}: ${outside(refit.outside.proposed)}</p>
2147
- ${refit.drops.length
2148
- ? html`<p>${i18next.t('figure.text.room-drops', { fields: [...new Set(refit.drops.map(id => `${partLabelOf(id.split('.')[0]!)} ${fieldWordsOf(id, key => i18next.t(key))}`))].join(' · ') })}</p>`
2149
- : nothing}`,
2150
- confirmButton: { text: i18next.t('figure.button.room-refit-confirm') },
2151
- cancelButton: { text: i18next.t('figure.button.keep-editing') }
2152
- })
2153
- if (agreed) this.linkCommand(refit.step)
2154
- }
2155
2069
  private renderSingle(name: string, field: FieldLike, unavailable: boolean) {
2156
2070
  const words = name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }))
2157
2071
  return html`<span ?scrub=${!unavailable} @pointerdown=${(e: PointerEvent) => this.scrub(field as never, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
@@ -27,7 +27,7 @@ export function figureFigureFacts(revision: FigureRevision): FigureFigureFacts {
27
27
  }
28
28
  function figureFactsOf(revision: FigureRevision): FigureFigureFacts {
29
29
  const asset = revision.asset as FigureAsset
30
- const feature = (asset.occupancy?.contact as { surface?: { feature?: string } } | undefined)?.surface?.feature
30
+ const feature = asset.mountingFace?.surface.feature
31
31
  const found = feature?.match(/^(.+)\.mount\.(left|right|bottom|top|back|front)\.value$/)
32
32
  return {
33
33
  name: asset.name,
@@ -401,35 +401,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
401
401
  * 준다 — 화면이 문장을 지으면 서버가 거절할 때와 다른 말을 하게 된다.
402
402
  */
403
403
  private renderFinding(finding: FigureGateFinding, blocking: boolean) {
404
- return renderGateFinding(finding, blocking, html`
405
- ${this.offersFit(finding.code)
406
- ? html`<button fit-base @click=${() => this.fitBase()}>
407
- ${i18next.t('figure.button.fit-base-to-parts')}
408
- </button>`
409
- : nothing}
410
- `)
411
- }
412
-
413
- /**
414
- * 넘친 것을 담는 길 하나를 여기서도 내민다 — **같은 동작이 속성 패널에도 있다**(「부품에 맞추기」).
415
- *
416
- * 그 단추를 아는 저작자는 거기서 누른다. 발행이 막혔다는 말을 여기서 읽은 사람은 그 단추가 있는
417
- * 것을 모르고, 문구는 그것을 가리킬 수 없다 — 문장은 형식이 쓰고 형식은 화면의 단추를 모른다.
418
- * 그래서 화면이 자기 자리에서 같은 동작을 낸다. 누르면 상자만 커지고 부품은 제자리이며, 저장은
419
- * 여전히 저작자가 따로 한다.
420
- */
421
- /**
422
- * 「부품에 맞추기」를 내미는 판정: 공간을 부품에서 다시 선언한다(`declare-occupancy`) — 부품은 그대로 두고 선언만
423
- * 바꾼다. 공간을 아직 선언하지 않은 도형도 같은 단추로 선언한다: 새로 만든 도형은 전부 그 판정으로 막히고, 화면에
424
- * 다른 길이 없다. 장착면이 떠 있는 것은 여기 없다 — 부품 하나를 평면에 붙여도 거기 붙지 않은 것은 따라오지 않는다.
425
- * 판정의 「어떻게」가 할 일을 말한다.
426
- */
427
- private offersFit(code: string): boolean {
428
- return code === 'part-outside-occupancy' || code === 'occupancy-undeclared'
429
- }
430
-
431
- private fitBase() {
432
- this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }))
404
+ return renderGateFinding(finding, blocking, nothing)
433
405
  }
434
406
 
435
407
  /** Where one released version downloads as a package file, or undefined before the figure has a full name. */
@@ -10,7 +10,7 @@ import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSetting
10
10
  import { dialShape } from './angle-dial.js'
11
11
  import { domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
12
12
  import { derivedOf, type FigureRevision } from './revision.js'
13
- import { authoredSizeOf, occupancyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
13
+ import { authoredSizeOf, bodyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
14
14
  import { previewSizeWordsOf } from './preview-size.js'
15
15
 
16
16
  /**
@@ -331,8 +331,6 @@ export class FigurePreview extends localize(i18next)(LitElement) {
331
331
  /** The one figure's size: a slider per axis the figure lets change, and what it is at the size tried. */
332
332
  private renderSizes(asset: FigureAsset, size: { x: number; y: number; z: number }) {
333
333
  const say = (key: string, params?: Record<string, string>) => i18next.t(key, params)
334
- /* A board sizes a figure by the space it takes: with none declared, its box folds and no size can be tried. */
335
- if (!asset.occupancy) return html`<div sizes><p said>${i18next.t('figure.text.preview-needs-space')}</p></div>`
336
334
  return html`<div sizes>
337
335
  ${(['x', 'y', 'z'] as const).map(axis => {
338
336
  const range = sizeSpanOf(asset, axis)
@@ -364,8 +362,8 @@ export class FigurePreview extends localize(i18next)(LitElement) {
364
362
  if (used === undefined || !this.trying) return
365
363
  this.trying = { ...this.trying, [axis]: used }
366
364
  const instance = this.scene?.root?.components?.[0] as { model?: { width?: number; height?: number }; set(props: Record<string, unknown>): void } | undefined
367
- /* The instance's box is the space the figure takes at the sizes tried, not the sizes (occupancyAtSize). */
368
- const box = occupancyAtSize(asset!, this.trying) ?? this.trying
365
+ /* The instance's box is the body at the sizes tried, not the sizes (bodyAtSize). */
366
+ const box = bodyAtSize(asset!, this.trying) ?? this.trying
369
367
  instance?.set({ width: box.x, depth: box.y, height: box.z })
370
368
  const { width = 0, height = 0 } = instance?.model ?? {}
371
369
  instance?.set({ left: this.board.width / 2 - width / 2, top: this.board.height / 2 - height / 2 })
@@ -600,8 +598,8 @@ export class FigurePreview extends localize(i18next)(LitElement) {
600
598
  this.valuing = {}
601
599
  registerFigure(type, asset)
602
600
  this.registered = type
603
- /* Each instance stands in the space the figure takes at the size it is asked (occupancyAtSize), as a board reads it. */
604
- const boxOf = (asked: { x: number; y: number; z: number }) => occupancyAtSize(asset, asked) ?? asked
601
+ /* Each instance stands in its body at the size it is asked (bodyAtSize), as a board reads it. */
602
+ const boxOf = (asked: { x: number; y: number; z: number }) => bodyAtSize(asset, asked) ?? asked
605
603
  const { components, board } = this.way === 'one' ? previewOneInstance(type, boxOf(size)) : previewInstances(type, size, boxOf)
606
604
  this.board = board
607
605
  this.trying = this.way === 'one' ? { ...size } : undefined
@@ -392,8 +392,6 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
392
392
 
393
393
  /** The report over the source being edited. Absent when nothing is drawn, or the format refuses the source. */
394
394
  @state() private report?: FigureReport
395
- /** Bodies in the asset, drawn or hidden. With none there is nothing to fit the space to, so the button is not offered. */
396
- @state() private bodies = 0
397
395
  /** Whether the trend chart is open at its own size. */
398
396
  @state() private expanded = false
399
397
  @state() private inspection?: FigureInspection
@@ -428,9 +426,8 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
428
426
  }
429
427
 
430
428
  private measure() {
431
- const { report, bodies, told } = verdictMeasureOf(this.revision?.asset)
429
+ const { report, told } = verdictMeasureOf(this.revision?.asset)
432
430
  this.report = report
433
- this.bodies = bodies
434
431
  if (!report) this.expanded = false
435
432
  this.dispatchEvent(new CustomEvent('capacity-changed', { detail: told }))
436
433
  }
@@ -766,7 +763,7 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
766
763
  ${blocking.length
767
764
  ? html`<ul gate>
768
765
  <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
769
- ${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code), this.partNames()))}
766
+ ${blocking.map(finding => renderGateFinding(finding, true, nothing, this.partNames()))}
770
767
  </ul>`
771
768
  : nothing}
772
769
  ${advisory.length
@@ -782,16 +779,4 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
782
779
  private partNames(): string[] {
783
780
  return (this.revision?.asset.appearance ?? []).map(look => look.target)
784
781
  }
785
-
786
- /** The same way out the release panel offers for the same two findings — declare the space from the parts. */
787
- private actionsFor(code: string) {
788
- if (code !== 'part-outside-occupancy' && code !== 'occupancy-undeclared') return nothing
789
- /* A new figure with no parts is refused as undeclared too, but fitting needs parts to measure (seen 2026-09-24). */
790
- if (!this.bodies) return nothing
791
- return html`<button fit @click=${this.fit}>${i18next.t('figure.button.fit-base-to-parts')}</button>`
792
- }
793
-
794
- private fit = () => {
795
- this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }))
796
- }
797
782
  }
@@ -137,7 +137,7 @@ export function figureReportOf(asset: FigureAsset): FigureReport | undefined {
137
137
  },
138
138
  { key: 'detail', blocked: 'figure.text.maturity-blocked-no-detail-level' },
139
139
  { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' },
140
- readable(asset, plan, drawn, writers)
140
+ readable(drawn, writers)
141
141
  ]
142
142
 
143
143
  return {
@@ -229,13 +229,10 @@ const roundFootprint = (shape: { extrusion?: { axis: string; section: { kind: st
229
229
  * finer segment count does not raise it: H = −Σ p·log p over each material's share of the footprint plus the
230
230
  * background, divided by log(`LIMITS.distinctMaterials` + 1), the most this measure can reach.
231
231
  *
232
- * The footprint is the base box: the space the asset declares when it declares one (ADR-0044's box is the model's
233
- * occupancy), otherwise the drawn bodies' own extent. Bodies are their axis-aligned world boxes
232
+ * The footprint is the drawn bodies' own extent, the box a board gives it (ADR-0145). Bodies are their axis-aligned world boxes
234
233
  * (`figureWorldBoxesOf`, the gate's reading); the higher one covers the lower.
235
234
  */
236
235
  function readable(
237
- asset: FigureAsset,
238
- plan: ReturnType<typeof compileFigureDocument>,
239
236
  drawn: FigureAssetGeometry[],
240
237
  writers: Map<string, string>
241
238
  ): FigureMaturityAxis {
@@ -245,20 +242,12 @@ function readable(
245
242
  .map(body => ({ body, box: figureWorldBoxesOf([body], writers)[0]! }))
246
243
  if (placed.length === 0) return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }
247
244
 
248
- let frame: { x0: number; x1: number; z0: number; z1: number }
249
- if (asset.occupancy) {
250
- const values = plan.evaluate({ ...asset.designInputs, ...asset.stateDefaults }).values
251
- const at = (ref: string) => values[ref] as number
252
- const b = asset.occupancy.bounds
253
- frame = { x0: at(b.x.min), x1: at(b.x.max), z0: at(b.z.min), z1: at(b.z.max) }
254
- } else {
255
- frame = { x0: Infinity, x1: -Infinity, z0: Infinity, z1: -Infinity }
256
- for (const { box } of placed) {
257
- frame.x0 = Math.min(frame.x0, box.centre.x - box.extent.x / 2)
258
- frame.x1 = Math.max(frame.x1, box.centre.x + box.extent.x / 2)
259
- frame.z0 = Math.min(frame.z0, box.centre.z - box.extent.z / 2)
260
- frame.z1 = Math.max(frame.z1, box.centre.z + box.extent.z / 2)
261
- }
245
+ const frame = { x0: Infinity, x1: -Infinity, z0: Infinity, z1: -Infinity }
246
+ for (const { box } of placed) {
247
+ frame.x0 = Math.min(frame.x0, box.centre.x - box.extent.x / 2)
248
+ frame.x1 = Math.max(frame.x1, box.centre.x + box.extent.x / 2)
249
+ frame.z0 = Math.min(frame.z0, box.centre.z - box.extent.z / 2)
250
+ frame.z1 = Math.max(frame.z1, box.centre.z + box.extent.z / 2)
262
251
  }
263
252
 
264
253
  const regions = topViewRegions(placed, frame)
@@ -417,26 +406,21 @@ export interface FigureBudgetLine {
417
406
  }
418
407
 
419
408
  /**
420
- * What the verdict tab measures of the figure on screen once it settles: the report, the bodies there are to fit the
421
- * space to (drawn or hidden -- a hidden part is still a part to measure), and what it tells the page: the count for its
422
- * tab and the parts list's line, from the same report. A source the format refuses has no report.
409
+ * What the verdict tab measures of the figure on screen once it settles: the report, and what it tells the page: the
410
+ * count for its tab and the parts list's line, from the same report. A source the format refuses has no report.
423
411
  */
424
412
  export function verdictMeasureOf(asset: FigureAsset | undefined): {
425
413
  report?: FigureReport
426
- bodies: number
427
414
  told: { instances?: number; budget?: FigureBudgetLine }
428
415
  } {
429
416
  let report: FigureReport | undefined
430
- let bodies = 0
431
417
  try {
432
- const cost = asset ? figureCostOf(asset) : undefined
433
- bodies = cost ? cost.groups + cost.hidden : 0
434
418
  report = asset ? figureReportOf(asset) : undefined
435
419
  } catch {
436
420
  /* A source the format refuses has no report; the server's finding says why. */
437
421
  report = undefined
438
422
  }
439
- return { report, bodies, told: { instances: report?.capacity.instances, budget: budgetOf(report) } }
423
+ return { report, told: { instances: report?.capacity.instances, budget: budgetOf(report) } }
440
424
  }
441
425
 
442
426
  export function budgetOf(report: FigureReport | undefined): FigureBudgetLine | undefined {
@@ -76,20 +76,14 @@ export function clipLossOf(error: unknown): FigureClipLoss[] | null {
76
76
  return codeOf(error) === 'CLIPS_DROPPED' ? (detailsOf(error, 'clips-dropped')?.clips ?? []) : null
77
77
  }
78
78
 
79
- /** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
80
- export function inDeclaredRoom(error: unknown): boolean {
81
- return codeOf(error) === 'PART_REFERENCED' && !!detailsOf(error, 'in-declared-space')
82
- }
83
-
84
79
  /**
85
80
  * Why a part cannot be deleted, in the author's words. Since the chief architect's ruling of 2026-09-27 a part that
86
81
  * holds others, leads a motion or is followed goes, and what depended on it is let go of (`removalNoticeOf`); what is
87
- * left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.
82
+ * left is a row -- one laid along the part, or hanging in its row.
88
83
  */
89
84
  export function partRefusalOf(error: unknown, part: string, say: Say): string | null {
90
85
  const code = codeOf(error)
91
86
  if (code !== 'PART_REFERENCED' && code !== 'PART_ROW') return null
92
- if (detailsOf(error, 'in-declared-space')) return say('figure.text.part-in-declared-space', { part })
93
87
  const along = detailsOf(error, 'row-along')
94
88
  if (along) return say('figure.text.part-row-along', { part, rows: along.rows.join(', ') })
95
89
  const carried = detailsOf(error, 'hangs-in-row')
@@ -10,7 +10,6 @@ import { fetchFigure, fetchFigureCategories, fetchFigureNamespaces, fetchFigureT
10
10
  import { figureProposalOutcome } from '../figure-assistant.js'
11
11
  import { availableRecipes, recipeGuide } from '../modeller/recipe-source.js'
12
12
  import type { RecipeTemplate } from '../modeller/recipes.js'
13
- import { occupancyFromParts } from '../modeller/occupancy.js'
14
13
  import type { FigureAuthoringAction } from '@hatiolab/figure-model'
15
14
  import type { FigureGateFinding } from '../types.js'
16
15
  import '../modeller/editor.js'
@@ -290,13 +289,7 @@ export class FigureModellerPage extends PageView {
290
289
  input.value = this.row.type ?? ''
291
290
  }
292
291
  }
293
- /** 「부품에 맞추기」 — 공간을 지금 부품에서 다시 선언한다. 저장은 저작자가 따로 한다. */
294
- private fitToParts = () => {
295
- const shown = this.session.revisionNow()
296
- /* Nothing open, nothing to fit: the button stands only beside an open figure. */
297
- if (shown) this.runCommand(() => occupancyFromParts(shown.asset))
298
- }
299
- /** A command from the properties tab — rename, declare the room, the mounting face. */
292
+ /** A command from the properties tab — rename, the mounting face. */
300
293
  private onCommand = (event: Event) => {
301
294
  const action = (event as CustomEvent).detail?.action as FigureAuthoringAction | undefined
302
295
  if (action) this.runCommand(() => action)
@@ -311,18 +304,12 @@ export class FigureModellerPage extends PageView {
311
304
  this.commandError = ''
312
305
  this.changed()
313
306
  } catch (error) {
314
- /*
315
- This used to set the page error, which replaces the whole editor with one raw line — for a figure with no
316
- parts that line was 「OCCUPANCY_NO_PARTS: occupancy: no part could be measured」 (2026-09-24).
317
- */
318
- this.commandError =
319
- (error as { code?: string })?.code === 'OCCUPANCY_NO_PARTS'
320
- ? i18next.t('figure.text.fit-needs-parts')
321
- : (error as { code?: string })?.code
322
- ? editRefusalOf(error, (key, params) => i18next.t(key, params))
323
- : error instanceof Error
324
- ? error.message
325
- : String(error)
307
+ /* Said in the author's words beside the side panel, not as the page error that replaces the whole editor. */
308
+ this.commandError = (error as { code?: string })?.code
309
+ ? editRefusalOf(error, (key, params) => i18next.t(key, params))
310
+ : error instanceof Error
311
+ ? error.message
312
+ : String(error)
326
313
  }
327
314
  }
328
315
  /**
@@ -562,7 +549,6 @@ export class FigureModellerPage extends PageView {
562
549
  @capacity-changed=${(event: CustomEvent) => {
563
550
  this.budget = event.detail.budget
564
551
  }}
565
- @fit-base-to-parts=${this.fitToParts}
566
552
  @release-verdict=${(event: CustomEvent<{ blocking: FigureGateFinding[] }>) => (this.releaseBlockers = event.detail.blocking)}
567
553
  ></figure-verdict>
568
554
  <figure-history-panel
@@ -575,7 +561,6 @@ export class FigureModellerPage extends PageView {
575
561
  .dirty=${!!view.dirty}
576
562
  .saveFirst=${this.saveBeforeRevert}
577
563
  .serverSource=${() => this.session.confirmedSource()}
578
- @fit-base-to-parts=${this.fitToParts}
579
564
  @figure-released=${this.released}
580
565
  @figure-reverted=${this.reverted}
581
566
  ></figure-history-panel>
@@ -311,7 +311,7 @@ export declare class FigureEditor extends LitElement {
311
311
  private setPitch;
312
312
  /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
313
313
  private releaseWords;
314
- /** The findings that block release, under the bar beside 「발행」, each with the way out the verdict tab offers for it. */
314
+ /** The findings that block release, under the bar beside 「발행」. */
315
315
  private renderReleaseBlockers;
316
316
  private goRelease;
317
317
  private overhangOf;
@@ -395,12 +395,5 @@ export declare class FigureEditor extends LitElement {
395
395
  /** What a size typed keeps, under the size fields: the part's middle, where it is fastened, or only its size (ADR-0140 4.1). */
396
396
  private renderSizeKeeps;
397
397
  private renderHeldAxes;
398
- /**
399
- * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
400
- * typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.
401
- */
402
- private renderRoomOnly;
403
- /** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
404
- private askRoomRefit;
405
398
  private renderSingle;
406
399
  }