@things-factory/figure-ui 10.1.82 → 10.1.83
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 +3 -28
- package/client/modeller/figure-links.ts +47 -101
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/editor.d.ts +0 -6
- package/dist-client/modeller/editor.js +2 -28
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +1 -1
- package/dist-client/modeller/figure-clips.d.ts +1 -1
- package/dist-client/modeller/figure-controls.d.ts +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -3
- package/dist-client/modeller/figure-links.js +45 -96
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +1 -1
- package/dist-client/modeller/figure-properties.d.ts +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/test/links.test.ts +2 -1
- package/test/resize-with-placement.test.ts +12 -11
- package/translations/en.json +8 -2
- package/translations/ja.json +7 -1
- package/translations/ko.json +9 -3
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
|
@@ -6,10 +6,10 @@ import { notify } 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 { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor,
|
|
9
|
+
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, parseFigureAsset, figureSizeEditChanges, figureScaleAnchorOf, figurePlaneAlignChanges, type FigurePlaneContact, type FigureClipLoss, figurePartFaceOf, type FigureFaceArea, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model'
|
|
10
10
|
import type { RecipeTemplate } from './recipes.js'
|
|
11
11
|
import './figure-recipe-catalog.js'
|
|
12
|
-
import type { FigureAsset, FigureAuthoringAction, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue,
|
|
12
|
+
import type { FigureAsset, FigureAuthoringAction, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement } from '@hatiolab/figure-model'
|
|
13
13
|
import type { FigureProductSession } from './product-session.js'
|
|
14
14
|
import { captureFigureThumbnail } from './thumbnail.js'
|
|
15
15
|
import { drawableFigureParts } from './renderer.js'
|
|
@@ -39,7 +39,7 @@ import { nudgeOf, placementOf, type ScaleAnchor } from './stage-model.js'
|
|
|
39
39
|
/** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
|
|
40
40
|
type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
|
|
41
41
|
import { roomRefitRowsOf } from './occupancy.js'
|
|
42
|
-
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf,
|
|
42
|
+
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './refusals.js'
|
|
43
43
|
import type { FigureRenderPart } from './renderer.js'
|
|
44
44
|
import type { FigureBudgetLine } from './maturity.js'
|
|
45
45
|
import { fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
@@ -1089,7 +1089,6 @@ export class FigureEditor extends LitElement {
|
|
|
1089
1089
|
.motionEnds=${section === 'motion' ? this.motionEnds() : null}
|
|
1090
1090
|
@go-fasten=${() => this.goFasten()}
|
|
1091
1091
|
@preview-command=${(e: CustomEvent) => this.previewCommand(e.detail)}
|
|
1092
|
-
@resize-with-placement=${(e: CustomEvent) => this.resizeWithPlacement(e.detail.request)}
|
|
1093
1092
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1094
1093
|
></figure-links>`
|
|
1095
1094
|
}
|
|
@@ -1137,30 +1136,6 @@ export class FigureEditor extends LitElement {
|
|
|
1137
1136
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1138
1137
|
></figure-clips>`
|
|
1139
1138
|
}
|
|
1140
|
-
/**
|
|
1141
|
-
* "Follow the size and keep where the figure is placed" (ADR-0097): figure-model plans it from the figure as it is now,
|
|
1142
|
-
* and the whole plan is one step of the history. A refusal with a way on -- move the part onto the plane, replace how
|
|
1143
|
-
* its position is worked out -- asks in the app's prompt and plans again with that choice; any other says why.
|
|
1144
|
-
*/
|
|
1145
|
-
private resizeWithPlacement(request: FigureResizeRequest) {
|
|
1146
|
-
this.retry = undefined
|
|
1147
|
-
this.notice = ''
|
|
1148
|
-
const plan = planFigureResize(parseFigureAsset(this.session!.source()), request)
|
|
1149
|
-
const refusal = plan.refusal
|
|
1150
|
-
if (refusal) {
|
|
1151
|
-
const choice = refusal.choice
|
|
1152
|
-
/* The gap is chosen on the screen beside the button, with its distance; only replacing a position rule is asked here. */
|
|
1153
|
-
this.retry = choice === 'replacePosition' ? { label: i18next.t('figure.button.resize-choice-replace-position'), run: () => this.resizeWithPlacement({ ...request, replacePosition: true }) } : undefined
|
|
1154
|
-
this.error = resizeRefusalOf(plan, request.part, (key, params) => i18next.t(key, params))
|
|
1155
|
-
return
|
|
1156
|
-
}
|
|
1157
|
-
this.run(() => this.session!.applyRecipe(plan.steps))
|
|
1158
|
-
/*
|
|
1159
|
-
The declared room was measured from the part's old numbers and is not changed: said as a fact, not as a claim that
|
|
1160
|
-
the part leaves it -- that is measured where the room is declared again (designer 2026-09-27).
|
|
1161
|
-
*/
|
|
1162
|
-
if (!this.error && plan.roomReads.length) this.notice = i18next.t('figure.text.resize-room-unchanged')
|
|
1163
|
-
}
|
|
1164
1139
|
/** One command, one step of the history. A refusal a person can meet here is said in their words. */
|
|
1165
1140
|
private linkCommand(action: FigureAuthoringAction) {
|
|
1166
1141
|
this.retry = undefined
|
|
@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from 'lit'
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
|
-
import { FIGURE_FACES
|
|
7
|
+
import { FIGURE_FACES } from '@hatiolab/figure-model'
|
|
8
8
|
import type { FigureAsset, FigureAuthoringAction, FigureDimensionSource, FigureFace } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -42,7 +42,6 @@ import {
|
|
|
42
42
|
fixedJointOf,
|
|
43
43
|
rowOf,
|
|
44
44
|
positionsWorkedOut,
|
|
45
|
-
resizeAxesOf,
|
|
46
45
|
fieldWordsOf,
|
|
47
46
|
measureNow,
|
|
48
47
|
type Alignment,
|
|
@@ -109,10 +108,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
109
108
|
div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
|
|
110
109
|
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
111
110
|
button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
|
|
112
|
-
/*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
fieldset[resize-pending] small { opacity: 0.7; }
|
|
111
|
+
/* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
|
|
112
|
+
details[resize-choice] { margin: 12px 0; padding: 0; border: 0; }
|
|
113
|
+
details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
|
|
116
114
|
div[resize-held] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
|
|
117
115
|
div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
|
|
118
116
|
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
@@ -170,8 +168,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
170
168
|
private fastenCandidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
171
169
|
private rowCandidate?: FigureAuthoringAction
|
|
172
170
|
|
|
173
|
-
/**
|
|
174
|
-
@state() private
|
|
171
|
+
/** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
|
|
172
|
+
@state() private choosingFastening = false
|
|
175
173
|
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
176
174
|
@state() private spotlit = false
|
|
177
175
|
@state() private fasten = freshFasten()
|
|
@@ -198,6 +196,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
198
196
|
this.place = freshPlace()
|
|
199
197
|
this.row = freshRow()
|
|
200
198
|
this.editingMotion = false
|
|
199
|
+
this.choosingFastening = false
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
|
|
@@ -317,41 +316,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
317
316
|
}
|
|
318
317
|
|
|
319
318
|
private renderDimensions(asset: FigureAsset) {
|
|
320
|
-
/*
|
|
321
|
-
The kernel's plan for "follow the size and keep where the figure is placed" (ADR-0097), with the directions the
|
|
322
|
-
author ticked below -- a pending choice, drawn apart from the rows above, which only say what each direction does
|
|
323
|
-
now (designer 2026-09-27: the immediate and the pending are not mixed in one look).
|
|
324
|
-
*/
|
|
325
319
|
const editable = followableAxes(asset, this.partId)
|
|
326
|
-
const choice = resizeAxesOf(asset, this.partId, this.pendingAxes[this.partId])
|
|
327
|
-
const axes = choice.axes
|
|
328
|
-
const plan = planFigureResize(asset, { part: this.partId, axes })
|
|
329
|
-
const mode = plan.mode
|
|
330
320
|
const round = editable.length === 2
|
|
331
321
|
const word = (axis: Axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
|
|
332
|
-
const scope =
|
|
333
|
-
axes.length && axes.length === editable.length
|
|
334
|
-
? i18next.t(`figure.text.resize-scope-all-${editable.length}`)
|
|
335
|
-
: i18next.t('figure.text.resize-scope-only', { axes: axes.map(word).join(' · ') })
|
|
336
|
-
const ask = (gap?: 'keep' | 'plane') =>
|
|
337
|
-
this.dispatchEvent(new CustomEvent('resize-with-placement', { detail: { request: { part: this.partId, axes, ...(gap ? { gap } : {}) } }, bubbles: true, composed: true }))
|
|
338
|
-
const gapChoice = plan.refusal?.code === 'GAP_CHOICE' ? plan.refusal : undefined
|
|
339
|
-
/* Brought onto the plane, where it moves -- said on the button before it is pressed. */
|
|
340
|
-
const onto = gapChoice ? planFigureResize(asset, { part: this.partId, axes, gap: 'plane' }).moves[0] : undefined
|
|
341
|
-
const place = i18next.t(`figure.label.resize-place-${mode}`)
|
|
342
|
-
const tick = (axis: Axis, on: boolean) => {
|
|
343
|
-
const next = on ? [...new Set([...axes, axis])] : axes.filter(one => one !== axis)
|
|
344
|
-
this.pendingAxes = { ...this.pendingAxes, [this.partId]: next }
|
|
345
|
-
}
|
|
346
322
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
|
|
347
|
-
/* One step for the whole part (figure-model follow-size): the size is declared from the parts if there is none. */
|
|
348
323
|
const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance')
|
|
349
|
-
const whole = followsAll
|
|
350
|
-
? nothing
|
|
351
|
-
: html`<button act follow-size ?disabled=${this.locked} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
352
|
-
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
353
|
-
</button>`
|
|
354
|
-
const held = plan.refusal?.code === 'HELD'
|
|
355
324
|
return html`<p hint>${i18next.t('figure.text.resize-intent')}</p>
|
|
356
325
|
${asset.sizing === 'uniform' ? html`<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
|
|
357
326
|
<div resize-now>${editable.map(axis => {
|
|
@@ -366,46 +335,17 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
366
335
|
${following
|
|
367
336
|
? html`<button link resize-unfollow=${axis} ?disabled=${this.locked}
|
|
368
337
|
@click=${() => {
|
|
369
|
-
/* Set back to fixed: the author's choice for this part from now on, not a blank to fill with "all". */
|
|
370
|
-
this.pendingAxes = { ...this.pendingAxes, [this.partId]: axes.filter(one => one !== axis) }
|
|
371
338
|
this.follow(asset, axis, 'own', now)
|
|
372
339
|
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
373
340
|
: nothing}
|
|
374
341
|
</div>`
|
|
375
342
|
})}</div>
|
|
376
|
-
${
|
|
377
|
-
?
|
|
378
|
-
: html`<
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const following = now.of === 'instance' && now.axis === axis
|
|
383
|
-
const custom = now.of !== 'own' && !following
|
|
384
|
-
return html`<label>
|
|
385
|
-
<input type="checkbox" resize-tick=${axis} .checked=${axes.includes(axis)} ?disabled=${following || custom}
|
|
386
|
-
@change=${(e: Event) => tick(axis, (e.target as HTMLInputElement).checked)} />
|
|
387
|
-
${word(axis)}${following ? html` <small>${i18next.t('figure.text.resize-already-following')}</small>` : nothing}
|
|
388
|
-
</label>`
|
|
389
|
-
})}
|
|
390
|
-
<p hint resize-basis>${i18next.t(`figure.text.resize-basis-${choice.basis}`)}</p>
|
|
391
|
-
<p hint>${i18next.t('figure.text.placement-intent-' + mode)}</p>
|
|
392
|
-
${gapChoice
|
|
393
|
-
? html`<p hint gap-choice>${i18next.t(gapChoice.mm! > 0 ? 'figure.text.resize-gap-off' : 'figure.text.resize-gap-through', { place, mm: String(Math.abs(Math.round(gapChoice.mm! * 10) / 10)) })}</p>
|
|
394
|
-
<button act resize-keep-gap @click=${() => ask('keep')}>
|
|
395
|
-
${i18next.t(gapChoice.mm! > 0 ? 'figure.button.resize-keep-gap' : 'figure.button.resize-keep-through', { scope, mm: String(Math.abs(Math.round(gapChoice.mm! * 10) / 10)) })}
|
|
396
|
-
</button>
|
|
397
|
-
<button act resize-onto-plane @click=${() => ask('plane')}>
|
|
398
|
-
${i18next.t('figure.button.resize-onto-plane-' + mode, { scope })}
|
|
399
|
-
${onto ? html`<small>${i18next.t('figure.text.resize-move', { axis: onto.axis.toUpperCase(), mm: String(Math.round((onto.to - onto.from) * 10) / 10) })}</small>` : nothing}
|
|
400
|
-
</button>`
|
|
401
|
-
: html`<button act resize-with-placement ?disabled=${choice.basis === 'none' || plan.refusal?.code === 'UNIFORM_POLICY'} @click=${() => ask()}>
|
|
402
|
-
${i18next.t('figure.button.resize-scoped-' + mode, { scope })}
|
|
403
|
-
</button>`}
|
|
404
|
-
${!plan.refusal && plan.replaces.length
|
|
405
|
-
? html`<p hint resize-replaces>${i18next.t('figure.text.resize-replaces', { fields: [...new Set(plan.replaces.map(id => fieldWordsOf(id, key => i18next.t(key))))].join(' · '), place })}</p>`
|
|
406
|
-
: nothing}
|
|
407
|
-
</fieldset>`}
|
|
408
|
-
<details><summary>${i18next.t('figure.label.resize-advanced')}</summary>${whole}${followableAxes(asset, this.partId).map(axis => {
|
|
343
|
+
${followsAll
|
|
344
|
+
? html`<p hint>${i18next.t('figure.text.resize-all-following')}</p>`
|
|
345
|
+
: html`<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
346
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
347
|
+
</button>`}
|
|
348
|
+
<details resize-choice><summary>${i18next.t('figure.label.resize-advanced')}</summary><div>${followableAxes(asset, this.partId).map(axis => {
|
|
409
349
|
const now = followOf(asset, this.partId, axis)
|
|
410
350
|
const value = choiceOf(now)
|
|
411
351
|
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
@@ -445,7 +385,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
445
385
|
</label>`
|
|
446
386
|
: nothing}
|
|
447
387
|
</div>`
|
|
448
|
-
})}</details>`
|
|
388
|
+
})}</div></details>`
|
|
449
389
|
}
|
|
450
390
|
|
|
451
391
|
/**
|
|
@@ -631,6 +571,14 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
631
571
|
target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
|
|
632
572
|
})
|
|
633
573
|
: i18next.t('figure.text.fasten-new')
|
|
574
|
+
/* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
|
|
575
|
+
if (!current && !this.choosingFastening && !f.target)
|
|
576
|
+
return html`
|
|
577
|
+
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
578
|
+
<button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
|
|
579
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
580
|
+
</button>
|
|
581
|
+
`
|
|
634
582
|
return html`
|
|
635
583
|
<p fasten-mode ?editing=${!!current}>${editing}</p>
|
|
636
584
|
${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
@@ -651,34 +599,32 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
651
599
|
</div>
|
|
652
600
|
${this.renderDetachWords()}`
|
|
653
601
|
: nothing}
|
|
654
|
-
<div form>
|
|
655
|
-
<span>${i18next.t('figure.label.my-face')}</span>
|
|
656
|
-
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as FigureFace | 'centre' })}>
|
|
657
|
-
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
658
|
-
</select>
|
|
659
|
-
</div>
|
|
660
|
-
<label toggle>
|
|
661
|
-
<input type="checkbox" .checked=${f.toPlane} @change=${(e: Event) => this.aimAt(asset, { ...f, toPlane: (e.target as HTMLInputElement).checked })} />
|
|
662
|
-
${i18next.t('figure.label.stands-on-plane')}
|
|
663
|
-
</label>
|
|
664
602
|
${f.toPlane
|
|
665
|
-
?
|
|
603
|
+
? html`<p hint>${i18next.t('figure.text.fasten-edit-on-plane')}</p>`
|
|
666
604
|
: html`<div form>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
605
|
+
<span>${i18next.t('figure.label.target-part')}</span>
|
|
606
|
+
<select @change=${(e: Event) => this.aimAt(asset, { ...f, target: (e.target as HTMLSelectElement).value })}>
|
|
607
|
+
<option value="" ?selected=${!f.target}>${i18next.t('figure.label.target-part-placeholder')}</option>
|
|
608
|
+
${targets.map(one =>
|
|
609
|
+
loops(one)
|
|
610
|
+
? html`<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
611
|
+
: html`<option value=${one} ?selected=${one === f.target}>${one}</option>`
|
|
612
|
+
)}
|
|
613
|
+
</select>
|
|
614
|
+
</div>
|
|
615
|
+
${f.target
|
|
616
|
+
? html`<div form>
|
|
617
|
+
<span>${i18next.t('figure.label.my-face')}</span>
|
|
618
|
+
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as FigureFace | 'centre' })}>
|
|
619
|
+
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
620
|
+
</select>
|
|
621
|
+
<span>${i18next.t('figure.label.target-face')}</span>
|
|
622
|
+
<select @change=${(e: Event) => this.aimAt(asset, { ...f, targetFace: (e.target as HTMLSelectElement).value as FigureFace })}>
|
|
623
|
+
${theirFaces.map(face => html`<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
624
|
+
</select>
|
|
625
|
+
</div>`
|
|
626
|
+
: html`<p hint>${i18next.t('figure.text.fasten-target-first')}</p>`}`}
|
|
627
|
+
${(f.toPlane || f.target) ? html`<details fasten-detail>
|
|
682
628
|
<summary>${i18next.t('figure.label.fasten-detail')} · <span fasten-summary>${detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')}</span></summary>
|
|
683
629
|
${f.toPlane
|
|
684
630
|
? nothing
|
|
@@ -700,7 +646,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
700
646
|
${action && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
|
|
701
647
|
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
702
648
|
<md-icon>link</md-icon>${i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')}
|
|
703
|
-
</button
|
|
649
|
+
</button>` : nothing}
|
|
704
650
|
`
|
|
705
651
|
}
|
|
706
652
|
|
|
@@ -196,12 +196,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
196
196
|
private lastAnchors;
|
|
197
197
|
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
|
|
198
198
|
private renderControls;
|
|
199
|
-
/**
|
|
200
|
-
* "Follow the size and keep where the figure is placed" (ADR-0097): figure-model plans it from the figure as it is now,
|
|
201
|
-
* and the whole plan is one step of the history. A refusal with a way on -- move the part onto the plane, replace how
|
|
202
|
-
* its position is worked out -- asks in the app's prompt and plans again with that choice; any other says why.
|
|
203
|
-
*/
|
|
204
|
-
private resizeWithPlacement;
|
|
205
199
|
/** One command, one step of the history. A refusal a person can meet here is said in their words. */
|
|
206
200
|
private linkCommand;
|
|
207
201
|
private changeField;
|
|
@@ -7,7 +7,7 @@ import { notify } from '@operato/layout';
|
|
|
7
7
|
import { OxPrompt } from '@operato/popup';
|
|
8
8
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
9
9
|
import { live } from 'lit/directives/live.js';
|
|
10
|
-
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor,
|
|
10
|
+
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, parseFigureAsset, figureSizeEditChanges, figureScaleAnchorOf, figurePlaneAlignChanges, figurePartFaceOf, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
|
|
11
11
|
import './figure-recipe-catalog.js';
|
|
12
12
|
import { captureFigureThumbnail } from './thumbnail.js';
|
|
13
13
|
import { drawableFigureParts } from './renderer.js';
|
|
@@ -32,7 +32,7 @@ import { alignChanges, boxOf, canMirror, mirrorChanges } from './align.js';
|
|
|
32
32
|
import { paletteRecipe } from './palette.js';
|
|
33
33
|
import { nudgeOf, placementOf } from './stage-model.js';
|
|
34
34
|
import { roomRefitRowsOf } from './occupancy.js';
|
|
35
|
-
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf,
|
|
35
|
+
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './refusals.js';
|
|
36
36
|
import { fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
|
|
37
37
|
import { jointFanOf } from './joint-fan.js';
|
|
38
38
|
import { jointDragOf } from './joint-drag.js';
|
|
@@ -1110,7 +1110,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1110
1110
|
.motionEnds=${section === 'motion' ? this.motionEnds() : null}
|
|
1111
1111
|
@go-fasten=${() => this.goFasten()}
|
|
1112
1112
|
@preview-command=${(e) => this.previewCommand(e.detail)}
|
|
1113
|
-
@resize-with-placement=${(e) => this.resizeWithPlacement(e.detail.request)}
|
|
1114
1113
|
@part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
|
|
1115
1114
|
></figure-links>`;
|
|
1116
1115
|
}
|
|
@@ -1161,31 +1160,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1161
1160
|
@part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
|
|
1162
1161
|
></figure-clips>`;
|
|
1163
1162
|
}
|
|
1164
|
-
/**
|
|
1165
|
-
* "Follow the size and keep where the figure is placed" (ADR-0097): figure-model plans it from the figure as it is now,
|
|
1166
|
-
* and the whole plan is one step of the history. A refusal with a way on -- move the part onto the plane, replace how
|
|
1167
|
-
* its position is worked out -- asks in the app's prompt and plans again with that choice; any other says why.
|
|
1168
|
-
*/
|
|
1169
|
-
resizeWithPlacement(request) {
|
|
1170
|
-
this.retry = undefined;
|
|
1171
|
-
this.notice = '';
|
|
1172
|
-
const plan = planFigureResize(parseFigureAsset(this.session.source()), request);
|
|
1173
|
-
const refusal = plan.refusal;
|
|
1174
|
-
if (refusal) {
|
|
1175
|
-
const choice = refusal.choice;
|
|
1176
|
-
/* The gap is chosen on the screen beside the button, with its distance; only replacing a position rule is asked here. */
|
|
1177
|
-
this.retry = choice === 'replacePosition' ? { label: i18next.t('figure.button.resize-choice-replace-position'), run: () => this.resizeWithPlacement({ ...request, replacePosition: true }) } : undefined;
|
|
1178
|
-
this.error = resizeRefusalOf(plan, request.part, (key, params) => i18next.t(key, params));
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
this.run(() => this.session.applyRecipe(plan.steps));
|
|
1182
|
-
/*
|
|
1183
|
-
The declared room was measured from the part's old numbers and is not changed: said as a fact, not as a claim that
|
|
1184
|
-
the part leaves it -- that is measured where the room is declared again (designer 2026-09-27).
|
|
1185
|
-
*/
|
|
1186
|
-
if (!this.error && plan.roomReads.length)
|
|
1187
|
-
this.notice = i18next.t('figure.text.resize-room-unchanged');
|
|
1188
|
-
}
|
|
1189
1163
|
/** One command, one step of the history. A refusal a person can meet here is said in their words. */
|
|
1190
1164
|
linkCommand(action) {
|
|
1191
1165
|
this.retry = undefined;
|