@things-factory/figure-ui 10.1.206 → 10.1.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/editor.ts +11 -97
- package/client/modeller/figure-facts.ts +1 -1
- package/client/modeller/figure-history-panel.ts +1 -29
- package/client/modeller/figure-preview.ts +5 -7
- package/client/modeller/figure-verdict.ts +2 -17
- package/client/modeller/maturity.ts +11 -27
- package/client/modeller/refusals.ts +1 -7
- package/client/pages/figure-modeller-page.ts +7 -22
- package/dist-client/modeller/editor.d.ts +1 -8
- package/dist-client/modeller/editor.js +12 -98
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-facts.js +1 -1
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +0 -16
- package/dist-client/modeller/figure-history-panel.js +1 -27
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-preview.js +5 -8
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +0 -5
- package/dist-client/modeller/figure-verdict.js +2 -21
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/maturity.d.ts +2 -4
- package/dist-client/modeller/maturity.js +12 -26
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -3
- package/dist-client/modeller/refusals.js +1 -7
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +1 -3
- package/dist-client/pages/figure-modeller-page.js +7 -23
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/browser/acceptance-page.js +3 -3
- package/test/browser/detail-page.js +1 -3
- package/test/browser/detail-tabs-page.js +1 -2
- package/test/browser/links.check.mjs +2 -37
- package/test/browser/modeller.check.mjs +1 -3
- package/test/browser/parts-page.js +2 -2
- package/test/browser/parts.check.mjs +2 -12
- package/test/browser/product-page.check.mjs +6 -9
- package/test/browser/product-page.js +1 -2
- package/test/browser/screen-page.js +2 -6
- package/test/browser/screen.check.mjs +8 -65
- package/test/first-minute.test.ts +2 -3
- package/test/fixtures/joint-column.json +1 -66
- package/test/fixtures/joint-roller.json +1 -66
- package/test/fixtures/robot-arm-6dof.json +2 -67
- package/test/links.test.ts +1 -1
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +9 -12
- package/test/refusals.test.ts +3 -21
- package/test/resize-with-placement.test.ts +13 -15
- package/test/revision.test.ts +1 -2
- package/test/verdict.test.ts +1 -15
- package/translations/en.json +0 -19
- package/translations/es.json +0 -19
- package/translations/ja.json +0 -19
- package/translations/ko.json +0 -19
- package/translations/ms.json +0 -19
- package/translations/zh.json +0 -19
- package/client/modeller/occupancy.ts +0 -51
- package/dist-client/modeller/occupancy.d.ts +0 -31
- package/dist-client/modeller/occupancy.js +0 -36
- package/dist-client/modeller/occupancy.js.map +0 -1
- 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
|
|
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,
|
|
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
|
|
526
|
+
private removePart(id: string) {
|
|
528
527
|
try {
|
|
529
|
-
const view = this.session!.edit({ kind: 'remove-part', target: id
|
|
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
|
|
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
|
-
|
|
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
|
|
1423
|
-
private renderReleaseBlockers(blockers: FigureGateFinding[], asset: FigureAsset | undefined
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
368
|
-
const box =
|
|
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
|
|
604
|
-
const boxOf = (asked: { x: number; y: number; z: number }) =>
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
frame =
|
|
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,
|
|
421
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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
|
}
|