@things-factory/figure-ui 10.1.168 → 10.1.170
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/figure-assistant.ts +10 -1
- package/client/modeller/affected-parts.ts +11 -0
- package/client/modeller/ai-proposal.ts +40 -9
- package/client/modeller/canvas.ts +15 -1
- package/client/modeller/editor-styles.ts +98 -6
- package/client/modeller/editor.ts +212 -53
- package/client/modeller/fasten-geometry.ts +19 -0
- package/client/modeller/figure-fasten-drawing.ts +4 -3
- package/client/modeller/figure-links.ts +80 -37
- package/client/modeller/figure-material-panel.ts +20 -1
- package/client/modeller/figure-preview.ts +117 -8
- package/client/modeller/figure-properties.ts +1 -2
- package/client/modeller/figure-section-panel.ts +5 -0
- package/client/modeller/figure-verdict.ts +9 -2
- package/client/modeller/gate-findings.ts +12 -4
- package/client/modeller/group-menu.ts +57 -0
- package/client/modeller/links.ts +22 -1
- package/client/modeller/modeller-keys.ts +6 -0
- package/client/modeller/preview-size.ts +32 -0
- package/client/modeller/product-session.ts +23 -9
- package/client/modeller/refusals.ts +14 -0
- package/client/modeller/stage-model.ts +11 -0
- package/client/pages/figure-modeller-page.ts +15 -6
- package/dist-client/figure-assistant.d.ts +6 -0
- package/dist-client/figure-assistant.js +9 -1
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/modeller/affected-parts.d.ts +6 -0
- package/dist-client/modeller/affected-parts.js +12 -0
- package/dist-client/modeller/affected-parts.js.map +1 -0
- package/dist-client/modeller/ai-proposal.d.ts +15 -5
- package/dist-client/modeller/ai-proposal.js +39 -10
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +3 -0
- package/dist-client/modeller/canvas.js +12 -1
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +98 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +42 -1
- package/dist-client/modeller/editor.js +220 -51
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +27 -0
- package/dist-client/modeller/fasten-geometry.js +14 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +4 -3
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -1
- package/dist-client/modeller/figure-links.js +84 -38
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +8 -1
- package/dist-client/modeller/figure-material-panel.js +24 -1
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +12 -0
- package/dist-client/modeller/figure-preview.js +116 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +1 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.d.ts +1 -1
- package/dist-client/modeller/figure-section-panel.js +5 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +1 -0
- package/dist-client/modeller/figure-verdict.js +8 -2
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gate-findings.d.ts +3 -1
- package/dist-client/modeller/gate-findings.js +8 -4
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/group-menu.d.ts +18 -0
- package/dist-client/modeller/group-menu.js +52 -0
- package/dist-client/modeller/group-menu.js.map +1 -0
- package/dist-client/modeller/links.d.ts +5 -0
- package/dist-client/modeller/links.js +21 -1
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +7 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/modeller/preview-size.d.ts +23 -0
- package/dist-client/modeller/preview-size.js +32 -0
- package/dist-client/modeller/preview-size.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +0 -1
- package/dist-client/modeller/product-session.js +22 -9
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -5
- package/dist-client/modeller/refusals.js +14 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +11 -0
- package/dist-client/modeller/stage-model.js +11 -0
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -0
- package/dist-client/pages/figure-modeller-page.js +18 -5
- 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 +8 -8
- package/test/ai-proposal.test.ts +43 -4
- package/test/browser/editing-page.js +3 -1
- package/test/browser/editing.check.mjs +2 -1
- package/test/browser/links-page.js +1 -0
- package/test/browser/links.check.mjs +20 -5
- package/test/browser/modeller.check.mjs +6 -0
- package/test/browser/more-open.js +11 -0
- package/test/browser/parts.check.mjs +11 -1
- package/test/browser/places-mesh.check.mjs +9 -3
- package/test/browser/product-page.check.mjs +9 -4
- package/test/browser/props.check.mjs +224 -0
- package/test/browser/screen-page.js +17 -0
- package/test/browser/screen.check.mjs +171 -29
- package/test/browser/section-page.js +1 -0
- package/test/browser/section.check.mjs +6 -0
- package/test/command-preview.test.ts +2 -2
- package/test/fasten-geometry.test.ts +15 -0
- package/test/fields.test.ts +4 -2
- package/test/figure-assistant-surface.test.ts +38 -0
- package/test/first-minute.test.ts +15 -2
- package/test/layout.test.ts +1 -1
- package/test/preview-size.test.ts +33 -0
- package/test/refusals.test.ts +1 -1
- package/test/resize-with-placement.test.ts +1 -1
- package/test/stage-model.test.ts +11 -1
- package/test/verdict.test.ts +7 -0
- package/translations/en.json +43 -27
- package/translations/es.json +24 -8
- package/translations/ja.json +25 -9
- package/translations/ko.json +56 -40
- package/translations/ms.json +25 -9
- package/translations/zh.json +25 -9
|
@@ -55,7 +55,9 @@ import { nudgeOf, placementOf, type ScaleAnchor } from './stage-model.js'
|
|
|
55
55
|
|
|
56
56
|
/** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
|
|
57
57
|
type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
|
|
58
|
-
import { roomRefitRowsOf } from './occupancy.js'
|
|
58
|
+
import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js'
|
|
59
|
+
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
60
|
+
import type { FigureGateFinding } from '../types.js'
|
|
59
61
|
import { editRefusalOf, inDeclaredRoom, narrowingRetryOf, partNameRefusalOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
|
|
60
62
|
import type { FigureRenderPart } from './renderer.js'
|
|
61
63
|
import type { FigureBudgetLine } from './maturity.js'
|
|
@@ -66,13 +68,22 @@ import { jointDragOf } from './joint-drag.js'
|
|
|
66
68
|
import type { FirstSave } from './first-save.js'
|
|
67
69
|
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE, scrubbed, scrubStep, type LiveValues } from './live.js'
|
|
68
70
|
import { startingShapeKindOfBody } from './starting-shape.js'
|
|
71
|
+
import { groupMenu, GroupMenuStyles } from './group-menu.js'
|
|
69
72
|
|
|
70
73
|
|
|
71
74
|
/**
|
|
72
|
-
* The side's tabs
|
|
75
|
+
* The side's four tabs (chief architect 2026-10-16): properties, motion, its data (ADR-0051), the verdict. What it does
|
|
76
|
+
* (its places) is a group of the properties; the view is a menu over the canvas and the release history opens from the
|
|
77
|
+
* bar, since neither is about the part chosen. The verdict gives way to properties when a part is chosen; a tab about
|
|
73
78
|
* parts keeps showing the newly chosen part.
|
|
74
79
|
*/
|
|
75
|
-
type SideTab = 'properties' | '
|
|
80
|
+
type SideTab = 'properties' | 'motion' | 'data' | 'verdict'
|
|
81
|
+
|
|
82
|
+
/** What opens over the work from the bar: the view settings or the release history, one at a time. */
|
|
83
|
+
type BarOverlay = 'view' | 'history'
|
|
84
|
+
|
|
85
|
+
/** The person's own fold of the properties' 「더 보기」, kept in their browser. */
|
|
86
|
+
const MORE_OPEN_KEY = 'figure-properties-more-open'
|
|
76
87
|
|
|
77
88
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
78
89
|
const NO_OVERHANG = { words: NO_WORDS, note: NO_WORDS }
|
|
@@ -80,10 +91,16 @@ const NO_OVERHANG = { words: NO_WORDS, note: NO_WORDS }
|
|
|
80
91
|
@customElement('figure-editor')
|
|
81
92
|
export class FigureEditor extends LitElement {
|
|
82
93
|
/* The operato scrollbar, as every scrolling panel of the app has it -- the side panel scrolled with the browser's own. */
|
|
83
|
-
static styles = [ScrollbarStyles, SidePanelStyles, EditorStyles]
|
|
94
|
+
static styles = [ScrollbarStyles, SidePanelStyles, gateFindingStyles, GroupMenuStyles, EditorStyles]
|
|
84
95
|
@property({ attribute: false }) session?: FigureProductSession
|
|
85
96
|
/** The saved figure's state and version, as the server gave them (the page reads the row). */
|
|
86
97
|
@property({ attribute: false }) releaseState?: { state?: 'draft' | 'released'; version: number }
|
|
98
|
+
/**
|
|
99
|
+
* What blocks release now, as the page's verdict heard it: said beside 「발행」 with the way out at hand, so an author
|
|
100
|
+
* does not find it at the foot of the verdict tab (first 15 minutes, 2026-10-16; chief architect, Q1 B).
|
|
101
|
+
*/
|
|
102
|
+
@property({ attribute: false }) releaseBlockers: FigureGateFinding[] = []
|
|
103
|
+
@state() private blockersOpen = false
|
|
87
104
|
/** The recipe templates (ADR-0095), read by the page from the server. */
|
|
88
105
|
@property({ attribute: false }) templates: RecipeTemplate[] = []
|
|
89
106
|
/** Reads a template's guide; given by the page. */
|
|
@@ -115,6 +132,28 @@ export class FigureEditor extends LitElement {
|
|
|
115
132
|
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
116
133
|
@state() private retry?: Retry
|
|
117
134
|
@state() private tab: SideTab = 'properties'
|
|
135
|
+
/** The view settings or the release history, open over the work from the bar. */
|
|
136
|
+
@state() private overlay?: BarOverlay
|
|
137
|
+
/** The capability group open: the canvas marks the places only while their group is in sight. */
|
|
138
|
+
@state() private capabilityOpen = false
|
|
139
|
+
/** 「더 보기」 open, as this person left it last (chief architect 2026-10-16: kept per person). */
|
|
140
|
+
@state() private moreOpen = FigureEditor.storedMoreOpen()
|
|
141
|
+
private static storedMoreOpen(): boolean {
|
|
142
|
+
try {
|
|
143
|
+
return localStorage.getItem(MORE_OPEN_KEY) === '1'
|
|
144
|
+
} catch {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
private setMoreOpen(open: boolean) {
|
|
149
|
+
if (open === this.moreOpen) return
|
|
150
|
+
this.moreOpen = open
|
|
151
|
+
try {
|
|
152
|
+
localStorage.setItem(MORE_OPEN_KEY, open ? '1' : '0')
|
|
153
|
+
} catch {
|
|
154
|
+
/* A browser that keeps nothing folds it again next time. */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
118
157
|
/** The frame the bar folds - found after the first render, since the bar is drawn before it. */
|
|
119
158
|
@state() private panes?: OxThreePane
|
|
120
159
|
/** What the middle shows: the parts being edited, or the figure placed on a board at eight sizes. */
|
|
@@ -713,7 +752,7 @@ export class FigureEditor extends LitElement {
|
|
|
713
752
|
}
|
|
714
753
|
}
|
|
715
754
|
/** Size response, fastening and motion for the selected part, as authoring commands. */
|
|
716
|
-
private renderLinks(unavailable: boolean, section: 'shape' | 'places' | 'motion') {
|
|
755
|
+
private renderLinks(unavailable: boolean, section: 'shape' | 'places' | 'motion', slotted: unknown = nothing) {
|
|
717
756
|
const shown = this.shown()
|
|
718
757
|
if (!shown) return null
|
|
719
758
|
return html`<figure-links
|
|
@@ -730,7 +769,126 @@ export class FigureEditor extends LitElement {
|
|
|
730
769
|
@go-fasten=${() => this.goFasten()}
|
|
731
770
|
@preview-command=${(e: CustomEvent) => this.commandPreview.ask(e.detail)}
|
|
732
771
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
733
|
-
|
|
772
|
+
>${slotted}</figure-links>`
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* A part's groups in one order whatever its state (chief architect 2026-10-16): its size, its place, its row. The links
|
|
776
|
+
* draw the three groups and their rules; the part's own numbers go into them by slot. A fastened part's place and turn
|
|
777
|
+
* are its fastening's: the place group says it in one line and the locked cells are not drawn (user 2026-10-16).
|
|
778
|
+
*/
|
|
779
|
+
private renderPartGroups(shown: FigureRevision | undefined, geometry: readonly { id: string }[], fields: FieldLike[], unavailable: boolean, held?: HeldValues) {
|
|
780
|
+
const groups = this.withSizeRow(groupFields(fields, this.selected), held)
|
|
781
|
+
const size = groups.vectors.filter(g => g.kind === 'vector' && g.name === 'size')
|
|
782
|
+
const place = groups.vectors.filter(g => g.kind === 'vector' && g.name !== 'size')
|
|
783
|
+
const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(this.selected))
|
|
784
|
+
const placeLocked = fastened && place.every(g => g.kind === 'vector' && (['x', 'y', 'z'] as const).every(axis => !g.cells[axis]))
|
|
785
|
+
return this.renderLinks(
|
|
786
|
+
unavailable,
|
|
787
|
+
'shape',
|
|
788
|
+
html`${size.length || groups.singles.length
|
|
789
|
+
? html`<div slot="size" group-fields>
|
|
790
|
+
${size.map(g => this.renderVector(g, unavailable, held))}
|
|
791
|
+
${groups.singles.length
|
|
792
|
+
? html`<div form>${groups.singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>`
|
|
793
|
+
: nothing}
|
|
794
|
+
</div>`
|
|
795
|
+
: nothing}
|
|
796
|
+
${place.length && !placeLocked
|
|
797
|
+
? html`<div slot="place" group-fields>${place.map(g => this.renderVector(g, unavailable, held))}${this.renderHeldAxes(place)}</div>`
|
|
798
|
+
: nothing}
|
|
799
|
+
${this.renderRepeat(shown, geometry, unavailable)}`
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* What the figure does -- its places (ADR-0118) -- as a group of the properties, folded: a declaration made once,
|
|
804
|
+
* whose marks on the canvas show while it is open. A verdict that finds it empty still says where it is.
|
|
805
|
+
*/
|
|
806
|
+
private renderCapability(asset: FigureAsset | undefined, unavailable: boolean) {
|
|
807
|
+
return html`<section capability>
|
|
808
|
+
<details capability-fold ?open=${this.capabilityOpen} @toggle=${(e: Event) => (this.capabilityOpen = (e.target as HTMLDetailsElement).open)}>
|
|
809
|
+
<summary><h3>${i18next.t('figure.label.tab-capability')}</h3></summary>
|
|
810
|
+
${this.selected ? this.renderLinks(unavailable, 'places') : nothing}
|
|
811
|
+
<figure-capability-panel
|
|
812
|
+
.asset=${asset}
|
|
813
|
+
.selected=${this.selected}
|
|
814
|
+
?locked=${unavailable}
|
|
815
|
+
@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}
|
|
816
|
+
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
817
|
+
></figure-capability-panel>
|
|
818
|
+
</details>
|
|
819
|
+
</section>`
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* 「더 보기」: what few parts need, folded by default and left as this person last left it (chief architect
|
|
823
|
+
* 2026-10-16) -- the section, its corners and inside; transparency, glow and segments; the mirrored copy and the
|
|
824
|
+
* alignment tools.
|
|
825
|
+
*/
|
|
826
|
+
private renderMore(part: object, palette: Record<string, string>, unavailable: boolean) {
|
|
827
|
+
const tools = this.renderTools(unavailable)
|
|
828
|
+
return html`<section more>
|
|
829
|
+
<details more-fold ?open=${this.moreOpen} @toggle=${(e: Event) => this.setMoreOpen((e.target as HTMLDetailsElement).open)}>
|
|
830
|
+
<summary><h3>${i18next.t('figure.label.group-more')}</h3></summary>
|
|
831
|
+
<div more-body>
|
|
832
|
+
${tools ? html`<div group-fields>${tools}</div>` : nothing}
|
|
833
|
+
<figure-section-panel
|
|
834
|
+
embedded
|
|
835
|
+
.body=${{ ...part, id: this.selected }}
|
|
836
|
+
?locked=${unavailable}
|
|
837
|
+
.faceMapping=${this.faceMapping?.from === 'panel' ? this.faceMapping.need : undefined}
|
|
838
|
+
@face-mapped=${(e: CustomEvent) => this.mapFaces(e.detail.remap)}
|
|
839
|
+
@face-mapping-cancel=${() => (this.faceMapping = undefined)}
|
|
840
|
+
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
841
|
+
@command-live=${(e: CustomEvent) => this.holdAction(e.detail.action)}
|
|
842
|
+
@section-editor-open=${() => (this.sectionEditing = this.selected)}
|
|
843
|
+
></figure-section-panel>
|
|
844
|
+
<figure-material-panel
|
|
845
|
+
show="more"
|
|
846
|
+
.body=${{ ...part, id: this.selected }}
|
|
847
|
+
.palette=${palette}
|
|
848
|
+
?locked=${unavailable}
|
|
849
|
+
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
850
|
+
@part-live=${(e: CustomEvent) => this.hold({}, e.detail.edit)}
|
|
851
|
+
></figure-material-panel>
|
|
852
|
+
</div>
|
|
853
|
+
</details>
|
|
854
|
+
</section>`
|
|
855
|
+
}
|
|
856
|
+
/** A bar button that opens the view settings or the release history over the work; pressed while it is open. */
|
|
857
|
+
private renderOverlayButton(which: BarOverlay, icon: string, label: string, slot?: string) {
|
|
858
|
+
const open = this.overlay === which
|
|
859
|
+
return html`<button
|
|
860
|
+
slot=${slot ?? nothing}
|
|
861
|
+
tool
|
|
862
|
+
bar-overlay=${which}
|
|
863
|
+
aria-pressed=${String(open)}
|
|
864
|
+
aria-label=${i18next.t(label)}
|
|
865
|
+
title=${i18next.t(label)}
|
|
866
|
+
@click=${() => (this.overlay = open ? undefined : which)}
|
|
867
|
+
>
|
|
868
|
+
<md-icon>${icon}</md-icon>
|
|
869
|
+
</button>`
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* The view settings or the release history, over the work under the bar. Both stay drawn, hidden when closed: the
|
|
873
|
+
* history keeps what it read, as the tabs kept it. Esc closes it.
|
|
874
|
+
*/
|
|
875
|
+
private renderOverlay() {
|
|
876
|
+
const close = () => (this.overlay = undefined)
|
|
877
|
+
const panel = (which: BarOverlay, label: string, body: unknown) => html`<div
|
|
878
|
+
bar-overlay-panel=${which}
|
|
879
|
+
role="dialog"
|
|
880
|
+
aria-label=${i18next.t(label)}
|
|
881
|
+
?off=${this.overlay !== which}
|
|
882
|
+
@keydown=${(e: KeyboardEvent) => e.key === 'Escape' && (e.stopPropagation(), close())}
|
|
883
|
+
>
|
|
884
|
+
<div overlay-head>
|
|
885
|
+
<h3>${i18next.t(label)}</h3>
|
|
886
|
+
<button quiet aria-label=${i18next.t('figure.button.close')} title=${i18next.t('figure.button.close')} @click=${close}><md-icon>close</md-icon></button>
|
|
887
|
+
</div>
|
|
888
|
+
<div overlay-body>${body}</div>
|
|
889
|
+
</div>`
|
|
890
|
+
return html`${panel('view', 'figure.label.tab-environment', html`<figure-settings .view=${this.view} @update-view=${(e: CustomEvent) => this.setView(e.detail.view)}></figure-settings>`)}
|
|
891
|
+
${panel('history', 'figure.label.history', html`<slot name="history"></slot>`)}`
|
|
734
892
|
}
|
|
735
893
|
/**
|
|
736
894
|
* The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
|
|
@@ -789,6 +947,7 @@ export class FigureEditor extends LitElement {
|
|
|
789
947
|
this.retry = undefined
|
|
790
948
|
this.notice = ''
|
|
791
949
|
const hadSize = this.hasSize()
|
|
950
|
+
const hadSpace = !!this.shown()?.asset.occupancy
|
|
792
951
|
try {
|
|
793
952
|
this.session!.command(action)
|
|
794
953
|
this.error = ''
|
|
@@ -796,7 +955,9 @@ export class FigureEditor extends LitElement {
|
|
|
796
955
|
/* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
|
|
797
956
|
if (action.kind === 'follow-size' && !hadSize) {
|
|
798
957
|
const size = this.designValues()
|
|
799
|
-
|
|
958
|
+
/* 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. */
|
|
959
|
+
const spaceToo = !hadSpace && !!this.shown()?.asset.occupancy
|
|
960
|
+
this.notice = i18next.t(spaceToo ? 'figure.text.size-and-space-declared-to-follow' : 'figure.text.size-declared-to-follow', {
|
|
800
961
|
x: Math.round(size['size.x'] ?? 0).toLocaleString(),
|
|
801
962
|
y: Math.round(size['size.y'] ?? 0).toLocaleString(),
|
|
802
963
|
z: Math.round(size['size.z'] ?? 0).toLocaleString()
|
|
@@ -1042,8 +1203,12 @@ export class FigureEditor extends LitElement {
|
|
|
1042
1203
|
const longest = model.inputs.find(i => i.id === lengthRef)?.max
|
|
1043
1204
|
const limit = model.constants.find(c => c.id === layout?.args[6])?.value
|
|
1044
1205
|
const field = { id: own, unit: 'mm' as const, role: 'design', value: pitch ?? 0 }
|
|
1045
|
-
|
|
1046
|
-
|
|
1206
|
+
const removable = !!asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
1207
|
+
return html`<section slot="row" row>
|
|
1208
|
+
<h3>
|
|
1209
|
+
${i18next.t('figure.label.repeat')}
|
|
1210
|
+
${groupMenu([{ label: i18next.t('figure.button.remove-repeat'), icon: 'close', disabled: unavailable || !removable, run: () => this.run(() => this.session!.command({ kind: 'remove-repeat', part: this.selected })) }])}
|
|
1211
|
+
</h3>
|
|
1047
1212
|
<div inline-field><label>${i18next.t('figure.label.repeat-axis')}</label><span value>${axis.toUpperCase()}</span></div>
|
|
1048
1213
|
<div inline-field><label>${i18next.t('figure.label.repeat-count')}</label><span value>${repeat.count}</span></div>
|
|
1049
1214
|
<div inline-field>
|
|
@@ -1069,11 +1234,6 @@ export class FigureEditor extends LitElement {
|
|
|
1069
1234
|
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1070
1235
|
</p>
|
|
1071
1236
|
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1072
|
-
${asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
1073
|
-
? html`<button act ?disabled=${unavailable} @click=${() => this.run(() => this.session!.command({ kind: 'remove-repeat', part: this.selected }))}>
|
|
1074
|
-
<md-icon>close</md-icon>${i18next.t('figure.button.remove-repeat')}
|
|
1075
|
-
</button>`
|
|
1076
|
-
: nothing}
|
|
1077
1237
|
</section>`
|
|
1078
1238
|
}
|
|
1079
1239
|
/** The selected row as it is drawn now, read by figure-model: nothing when it cannot be read. */
|
|
@@ -1197,9 +1357,25 @@ export class FigureEditor extends LitElement {
|
|
|
1197
1357
|
if (state === 'released') return i18next.t('figure.text.release-state-released', { version: String(version) })
|
|
1198
1358
|
return version > 0 ? i18next.t('figure.text.release-state-draft-over', { version: String(version) }) : i18next.t('figure.text.release-state-draft-new')
|
|
1199
1359
|
}
|
|
1200
|
-
/**
|
|
1360
|
+
/** The findings that block release, under the bar beside 「발행」, each with the way out the verdict tab offers for it. */
|
|
1361
|
+
private renderReleaseBlockers(asset: FigureAsset | undefined, unavailable: boolean) {
|
|
1362
|
+
const parts = (asset?.appearance ?? []).map(look => look.target)
|
|
1363
|
+
const way = (code: string) =>
|
|
1364
|
+
(code === 'occupancy-undeclared' || code === 'part-outside-occupancy') && asset && parts.length
|
|
1365
|
+
? html`<button act ?disabled=${unavailable} @click=${() => this.linkCommand(occupancyFromParts(asset))}>
|
|
1366
|
+
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
1367
|
+
</button>`
|
|
1368
|
+
: nothing
|
|
1369
|
+
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)}>
|
|
1370
|
+
<ul gate>
|
|
1371
|
+
<li head>${i18next.t('figure.text.must-fix-to-release')}</li>
|
|
1372
|
+
${this.releaseBlockers.map(finding => renderGateFinding(finding, true, way(finding.code), parts))}
|
|
1373
|
+
</ul>
|
|
1374
|
+
</div>`
|
|
1375
|
+
}
|
|
1376
|
+
/** To the release that exists: the history's release section, opened from the bar, not a second one here. */
|
|
1201
1377
|
private goRelease = async () => {
|
|
1202
|
-
this.
|
|
1378
|
+
this.overlay = 'history'
|
|
1203
1379
|
await this.updateComplete
|
|
1204
1380
|
await this.querySelector<HTMLElement & { showRelease?: () => Promise<void> }>('figure-history-panel')?.showRelease?.()
|
|
1205
1381
|
}
|
|
@@ -1289,7 +1465,7 @@ export class FigureEditor extends LitElement {
|
|
|
1289
1465
|
/* A copy on the canvas selects its repeat: the part the author made. */
|
|
1290
1466
|
this.selected = id ? ownerOf(id) : id
|
|
1291
1467
|
this.faceMapping = undefined
|
|
1292
|
-
if (id &&
|
|
1468
|
+
if (id && this.tab === 'verdict') this.tab = 'properties'
|
|
1293
1469
|
}
|
|
1294
1470
|
private asset(): FigureAsset | undefined {
|
|
1295
1471
|
return this.shown()?.asset
|
|
@@ -1304,6 +1480,8 @@ export class FigureEditor extends LitElement {
|
|
|
1304
1480
|
}
|
|
1305
1481
|
updated(changed: PropertyValues<this>) {
|
|
1306
1482
|
super.updated?.(changed)
|
|
1483
|
+
/* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
|
|
1484
|
+
if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen) this.blockersOpen = false
|
|
1307
1485
|
this.refusals.tell(changed)
|
|
1308
1486
|
this.panelMemory.restore(() => this.selected)
|
|
1309
1487
|
const panes = this.renderRoot.querySelector<OxThreePane>('ox-three-pane') ?? undefined
|
|
@@ -1381,7 +1559,6 @@ export class FigureEditor extends LitElement {
|
|
|
1381
1559
|
</button>`
|
|
1382
1560
|
)}
|
|
1383
1561
|
</div>
|
|
1384
|
-
<small>${i18next.t('figure.text.placement-is-intent')}</small>
|
|
1385
1562
|
</section>
|
|
1386
1563
|
<section>
|
|
1387
1564
|
<h3>${i18next.t('figure.label.start-shape')}</h3>
|
|
@@ -1497,6 +1674,7 @@ export class FigureEditor extends LitElement {
|
|
|
1497
1674
|
@canvas-fit=${() => this.renderRoot.querySelector<FigureCanvas>('figure-canvas')?.fit()}
|
|
1498
1675
|
>
|
|
1499
1676
|
${this.renderGizmoTools(!unavailable && this.middle === 'edit')}
|
|
1677
|
+
${this.renderOverlayButton('view', 'wb_sunny', 'figure.label.tab-environment')}
|
|
1500
1678
|
${(['edit', 'preview'] as const).map(
|
|
1501
1679
|
mode => html`<button slot="mode" mode aria-pressed=${String(this.middle === mode)} title=${i18next.t('figure.text.mode-' + mode + '-explained')} @click=${() => (this.middle = mode)}>
|
|
1502
1680
|
${i18next.t('figure.label.mode-' + mode)}
|
|
@@ -1504,9 +1682,16 @@ export class FigureEditor extends LitElement {
|
|
|
1504
1682
|
)}
|
|
1505
1683
|
<span slot="status" class="status" role="status" ?dirty=${status === 'figure.text.unsaved-changes'}>${i18next.t(status)}</span>
|
|
1506
1684
|
<span slot="status" release-state>${this.releaseWords(view)}</span>
|
|
1685
|
+
${this.renderOverlayButton('history', 'history', 'figure.label.history', 'commit')}
|
|
1686
|
+
${this.releaseBlockers.length
|
|
1687
|
+
? html`<button slot="commit" release-blocked aria-expanded=${String(this.blockersOpen)} @click=${() => (this.blockersOpen = !this.blockersOpen)}>
|
|
1688
|
+
<md-icon>error</md-icon>${i18next.t('figure.button.release-blocked', { count: this.releaseBlockers.length })}
|
|
1689
|
+
</button>`
|
|
1690
|
+
: nothing}
|
|
1507
1691
|
<button slot="commit" publish ?disabled=${!view.id} title=${view.id ? '' : i18next.t('figure.text.save-the-figure-before-releasing')} @click=${this.goRelease}>${i18next.t('figure.button.release')}</button>
|
|
1508
1692
|
<button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>${i18next.t('figure.button.save')}</button>
|
|
1509
1693
|
</ox-canvas-bar>
|
|
1694
|
+
${this.blockersOpen && this.releaseBlockers.length ? this.renderReleaseBlockers(asset, unavailable) : nothing}
|
|
1510
1695
|
${this.catalogOpen
|
|
1511
1696
|
? html`<figure-recipe-catalog
|
|
1512
1697
|
.templates=${this.templates}
|
|
@@ -1573,7 +1758,7 @@ export class FigureEditor extends LitElement {
|
|
|
1573
1758
|
.fan=${this.fanOf(shown, live)}
|
|
1574
1759
|
.ghost=${this.previewShown().ghost}
|
|
1575
1760
|
.highlight=${this.previewShown().highlight}
|
|
1576
|
-
.marks=${this.tab === '
|
|
1761
|
+
.marks=${this.tab === 'properties' && this.capabilityOpen ? this.slotMarks(view, asset) : NO_MARKS}
|
|
1577
1762
|
.palette=${view.palette ?? {}}
|
|
1578
1763
|
.context=${view.context ?? ''}
|
|
1579
1764
|
.selected=${this.selected}
|
|
@@ -1587,12 +1772,9 @@ export class FigureEditor extends LitElement {
|
|
|
1587
1772
|
<aside slot="end">
|
|
1588
1773
|
<div tabs>
|
|
1589
1774
|
${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
|
|
1590
|
-
${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
|
|
1591
1775
|
${this.renderTab('motion', 'animation', i18next.t('figure.label.motion'))}
|
|
1592
1776
|
${this.renderTab('data', 'sensors', i18next.t('figure.label.tab-data'))}
|
|
1593
1777
|
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}
|
|
1594
|
-
${this.renderTab('view', 'wb_sunny', i18next.t('figure.label.tab-environment'))}
|
|
1595
|
-
${this.renderTab('history', 'history', i18next.t('figure.label.history'))}
|
|
1596
1778
|
</div>
|
|
1597
1779
|
<slot name="notice"></slot>
|
|
1598
1780
|
<div panel ?off=${tab !== 'properties'}>
|
|
@@ -1612,27 +1794,17 @@ export class FigureEditor extends LitElement {
|
|
|
1612
1794
|
</div>
|
|
1613
1795
|
${this.renderRepeatNote(view.geometry)}
|
|
1614
1796
|
</section>
|
|
1615
|
-
${this.
|
|
1616
|
-
${this.renderGroups(this.withSizeRow(groupFields(partFields, this.selected), held), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable), held)}
|
|
1797
|
+
${this.renderPartGroups(shown, view.geometry, partFields, unavailable, held)}
|
|
1617
1798
|
${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
|
|
1618
|
-
${this.renderLinks(unavailable, 'shape')}
|
|
1619
|
-
<figure-section-panel
|
|
1620
|
-
.body=${{ ...part, id: this.selected }}
|
|
1621
|
-
?locked=${unavailable}
|
|
1622
|
-
.faceMapping=${this.faceMapping?.from === 'panel' ? this.faceMapping.need : undefined}
|
|
1623
|
-
@face-mapped=${(e: CustomEvent) => this.mapFaces(e.detail.remap)}
|
|
1624
|
-
@face-mapping-cancel=${() => (this.faceMapping = undefined)}
|
|
1625
|
-
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
1626
|
-
@command-live=${(e: CustomEvent) => this.holdAction(e.detail.action)}
|
|
1627
|
-
@section-editor-open=${() => (this.sectionEditing = this.selected)}
|
|
1628
|
-
></figure-section-panel>
|
|
1629
1799
|
<figure-material-panel
|
|
1630
1800
|
.body=${{ ...part, id: this.selected }}
|
|
1631
1801
|
.palette=${view.palette ?? {}}
|
|
1632
1802
|
?locked=${unavailable}
|
|
1633
1803
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1634
1804
|
@part-live=${(e: CustomEvent) => this.hold({}, e.detail.edit)}
|
|
1635
|
-
></figure-material-panel
|
|
1805
|
+
></figure-material-panel>
|
|
1806
|
+
${this.renderCapability(asset, unavailable)}
|
|
1807
|
+
${this.renderMore(part, view.palette ?? {}, unavailable)}`
|
|
1636
1808
|
: view.geometry.length === 0
|
|
1637
1809
|
? html`${this.renderSubject()}<section empty-figure>
|
|
1638
1810
|
<p>${i18next.t('figure.text.empty-figure-next')}</p>
|
|
@@ -1643,18 +1815,8 @@ export class FigureEditor extends LitElement {
|
|
|
1643
1815
|
</section>`
|
|
1644
1816
|
: html`${this.renderSubject()}<slot name="figure"></slot>
|
|
1645
1817
|
${this.renderGroups(groupFields(common), 'figure.label.group-figure-size', 'figure.label.design-inputs', unavailable)}
|
|
1646
|
-
${this.renderInstanceRange(shown, unavailable)}
|
|
1647
|
-
|
|
1648
|
-
<div panel ?off=${tab !== 'capability'}>
|
|
1649
|
-
${this.renderSubject()}
|
|
1650
|
-
${this.selected && part ? this.renderLinks(unavailable, 'places') : null}
|
|
1651
|
-
<figure-capability-panel
|
|
1652
|
-
.asset=${asset}
|
|
1653
|
-
.selected=${this.selected}
|
|
1654
|
-
?locked=${unavailable}
|
|
1655
|
-
@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}
|
|
1656
|
-
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1657
|
-
></figure-capability-panel>
|
|
1818
|
+
${this.renderInstanceRange(shown, unavailable)}
|
|
1819
|
+
${this.renderCapability(asset, unavailable)}`}
|
|
1658
1820
|
</div>
|
|
1659
1821
|
<div panel ?off=${tab !== 'motion'}>
|
|
1660
1822
|
${this.renderSubject()}
|
|
@@ -1672,12 +1834,9 @@ export class FigureEditor extends LitElement {
|
|
|
1672
1834
|
></figure-data-panel>
|
|
1673
1835
|
</div>
|
|
1674
1836
|
<div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
|
|
1675
|
-
<div panel flush ?off=${tab !== 'view'}>
|
|
1676
|
-
<figure-settings .view=${this.view} @update-view=${(e: CustomEvent) => this.setView(e.detail.view)}></figure-settings>
|
|
1677
|
-
</div>
|
|
1678
|
-
<div panel flush ?off=${tab !== 'history'}><slot name="history"></slot></div>
|
|
1679
1837
|
</aside>
|
|
1680
|
-
</ox-three-pane
|
|
1838
|
+
</ox-three-pane>
|
|
1839
|
+
${this.renderOverlay()}`
|
|
1681
1840
|
}
|
|
1682
1841
|
/** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
|
|
1683
1842
|
private renderGroups(
|
|
@@ -185,3 +185,22 @@ export function screenOf(axes: readonly [Axis, Axis]): { h: Axis; v: Axis; sign:
|
|
|
185
185
|
const v = axes.find(a => a !== h)!
|
|
186
186
|
return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } as Record<Axis, 1 | -1> }
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the
|
|
191
|
+
* roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge
|
|
192
|
+
* only when the face fills the drawing. The part's name stands in the part when the part is wide and tall enough for it,
|
|
193
|
+
* else just under it -- above it when the drawing ends first. Widths are estimated: 7 px a letter at 11-12 px.
|
|
194
|
+
*/
|
|
195
|
+
export function fastenLabelsOf(
|
|
196
|
+
face: { x: number; y: number; w: number; h: number },
|
|
197
|
+
part: { x: number; y: number; w: number; h: number },
|
|
198
|
+
partName: string,
|
|
199
|
+
height: number
|
|
200
|
+
): { face: { x: number; y: number }; part: { x: number; y: number; inside: boolean } } {
|
|
201
|
+
const facePlace = face.y >= 16 ? { x: face.x, y: face.y - 5 } : { x: face.x + 4, y: face.y + 14 }
|
|
202
|
+
const fits = part.w >= partName.length * 7 + 8 && part.h >= 16
|
|
203
|
+
if (fits) return { face: facePlace, part: { x: part.x + part.w / 2, y: part.y + part.h / 2 + 4, inside: true } }
|
|
204
|
+
const below = part.y + part.h + 14
|
|
205
|
+
return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } }
|
|
206
|
+
}
|
|
@@ -4,7 +4,7 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import type { FigureFace } from '@hatiolab/figure-model'
|
|
6
6
|
|
|
7
|
-
import { alignmentAt, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
7
|
+
import { alignmentAt, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
8
8
|
import type { Alignment } from './links.js'
|
|
9
9
|
|
|
10
10
|
type Axis = 'x' | 'y' | 'z'
|
|
@@ -299,6 +299,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
299
299
|
}
|
|
300
300
|
const faceName = this.target ? `${this.target} ${i18next.t('figure.label.face-' + this.face)}` : this.planeWord
|
|
301
301
|
const sizeWords = faceSized ? ` ${Math.round(view.face[h]!)} × ${Math.round(view.face[v]!)}` : ''
|
|
302
|
+
const labels = fastenLabelsOf(faceSized ? { x: fx, y: fy, w: fw, h: fh } : { x: 6, y: 4, w: W - 12, h: H - 8 }, { x: px, y: py, w: pw, h: ph }, this.partName, H)
|
|
302
303
|
return html`<div where tabindex="0" @keydown=${(ev: KeyboardEvent) => this.keys(ev, view)}>
|
|
303
304
|
${svg`<svg viewBox="0 0 ${W} ${H}" role="img" aria-label=${faceName}
|
|
304
305
|
@pointermove=${(ev: PointerEvent) => this.drag(ev, view, cx, cy)}
|
|
@@ -306,7 +307,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
306
307
|
${faceSized
|
|
307
308
|
? svg`<rect x=${fx} y=${fy} width=${fw} height=${fh} rx="4" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`
|
|
308
309
|
: svg`<rect x="1" y="1" width=${W - 2} height=${H - 2} rx="8" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`}
|
|
309
|
-
<text x=${
|
|
310
|
+
<text face-name x=${labels.face.x} y=${labels.face.y} font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${faceName}${sizeWords}</text>
|
|
310
311
|
${guide(h)}${guide(v)}
|
|
311
312
|
<g footprint style=${`cursor:${this.locked ? 'default' : 'grab'}`} @pointerdown=${(ev: PointerEvent) => this.grab(ev, at, caught)}>
|
|
312
313
|
<rect x=${px} y=${py} width=${pw} height=${ph} rx=${round ? pw / 2 : 2} style="fill:var(--md-sys-color-tertiary-container)" />
|
|
@@ -317,7 +318,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
317
318
|
: nothing}
|
|
318
319
|
<rect x=${px} y=${py} width=${pw} height=${ph} rx=${round ? pw / 2 : 2}
|
|
319
320
|
style=${`fill:none;stroke:var(--md-sys-color-on-surface);stroke-width:1.5;stroke-dasharray:${touching ? '' : '4 3'}`} />
|
|
320
|
-
<text x=${
|
|
321
|
+
<text part-name x=${labels.part.x} y=${labels.part.y} text-anchor="middle" font-size="12" pointer-events="none" style="fill:var(--md-sys-color-on-surface)">${this.partName}</text>
|
|
321
322
|
</g>
|
|
322
323
|
${dim(h)}${dim(v)}
|
|
323
324
|
</svg>`}
|