@things-factory/figure-ui 10.1.208 → 10.1.211
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/data-interface.ts +5 -5
- package/client/modeller/editor.ts +97 -88
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +87 -157
- package/client/modeller/figure-size-editor.ts +233 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +87 -36
- package/client/modeller/live.ts +33 -0
- package/client/modeller/motion-ends.ts +10 -6
- package/client/modeller/size-response.ts +95 -0
- package/dist-client/modeller/data-interface.d.ts +1 -1
- package/dist-client/modeller/data-interface.js +5 -5
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +14 -1
- package/dist-client/modeller/editor.js +103 -89
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.js +1 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -4
- package/dist-client/modeller/figure-links.js +91 -145
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +29 -0
- package/dist-client/modeller/figure-size-editor.js +262 -0
- package/dist-client/modeller/figure-size-editor.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +1 -1
- package/dist-client/modeller/joint-drag.js +27 -22
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/joint-fan.js +4 -3
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +21 -9
- package/dist-client/modeller/links.js +85 -35
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +14 -0
- package/dist-client/modeller/live.js +31 -0
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +4 -1
- package/dist-client/modeller/motion-ends.js +10 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +29 -0
- package/dist-client/modeller/size-response.js +99 -0
- package/dist-client/modeller/size-response.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
- package/docs/evidence/2026-10-24/integrated-size.png +0 -0
- package/docs/evidence/2026-10-24/multipart-response.png +0 -0
- package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
- package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
- package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/width-preview.png +0 -0
- package/docs/multipart-response-review-2026-10-24.md +77 -0
- package/docs/response-authoring-2026-10-24.md +46 -0
- package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
- package/package.json +7 -7
- package/test/attachment-size-contract.test.ts +47 -0
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +66 -14
- package/test/browser/links.check.mjs +41 -22
- package/test/browser/modeller-page.js +1 -1
- package/test/browser/modeller-revision.check.mjs +21 -7
- package/test/browser/modeller.check.mjs +106 -39
- package/test/browser/multipart-response-page.js +8 -0
- package/test/browser/parts.check.mjs +2 -1
- package/test/browser/product-page.js +1 -1
- package/test/browser/props.check.mjs +17 -14
- package/test/browser/response-page.js +12 -0
- package/test/browser/row-preview.check.mjs +3 -3
- package/test/browser/screen.check.mjs +57 -13
- package/test/browser/serve-response.mjs +24 -0
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +2 -2
- package/test/command-preview.test.ts +7 -7
- package/test/joint-drag.test.ts +6 -6
- package/test/joint-frames.test.ts +33 -32
- package/test/joints.ts +1 -6
- package/test/links.test.ts +60 -20
- package/test/multipart-response-fixture.js +17 -0
- package/test/multipart-response.test.ts +37 -0
- package/test/refusals.test.ts +2 -2
- package/test/resize-with-placement.test.ts +23 -1
- package/test/size-response.test.ts +49 -0
- package/test/size-source-pairing.test.ts +66 -0
- package/translations/en.json +47 -10
- package/translations/es.json +47 -10
- package/translations/ja.json +47 -10
- package/translations/ko.json +52 -15
- package/translations/ms.json +47 -10
- package/translations/zh.json +47 -10
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* of data is things-scene's; this only lists what there is to choose, drafts a binding the kernel then checks, and edits
|
|
5
5
|
* the schema as a tree. Kept apart from the element so it is tested without a browser.
|
|
6
6
|
*/
|
|
7
|
-
import { FIGURE_PART_ID, FIGURE_MOTION_SPEED_MAX, FigureContractError, figureDataRows, parseFigureAccessor } from '@hatiolab/figure-model'
|
|
7
|
+
import { FIGURE_PART_ID, FIGURE_MOTION_SPEED_MAX, FigureContractError, figureAxesOf, figureDataRows, motionStateIdOf, parseFigureAccessor } from '@hatiolab/figure-model'
|
|
8
8
|
import type { FigureAsset, FigureDataBehavior, FigureDataBinding, FigureDataField, FigureDataNode, FigureDataRow, FigureDataValue, FigureDataValueIssue, FigureProposalStep, FigureStatusPort } from '@hatiolab/figure-model'
|
|
9
9
|
import { FigureInputs, type FigureDataResult } from '@hatiolab/things-scene'
|
|
10
10
|
|
|
@@ -286,12 +286,12 @@ export interface EndlessJoint {
|
|
|
286
286
|
unit: 'deg' | 'rad'
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
/** The
|
|
289
|
+
/** The axes that turn without end: revolute, with no limits, following no one (ADR-0159: every axis of every joint). */
|
|
290
290
|
export function endlessJointsOf(asset: FigureAsset): EndlessJoint[] {
|
|
291
291
|
const out: EndlessJoint[] = []
|
|
292
|
-
for (const
|
|
293
|
-
if (
|
|
294
|
-
const input =
|
|
292
|
+
for (const { motion: a } of figureAxesOf(asset.joints)) {
|
|
293
|
+
if (a.type !== 'revolute' || a.lowerLimit !== undefined || a.upperLimit !== undefined || a.mimic) continue
|
|
294
|
+
const input = motionStateIdOf(a)
|
|
295
295
|
const unit = asset.document.model.inputs.find(i => i.id === input)?.unit
|
|
296
296
|
if (unit !== 'deg' && unit !== 'rad') continue
|
|
297
297
|
if (!out.some(e => e.input === input)) out.push({ input, label: asset.stateInputs?.[input]?.label ?? input, unit })
|
|
@@ -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 } from '@hatiolab/figure-model'
|
|
9
|
+
import { figureFastenAxesOf, 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'
|
|
@@ -63,12 +63,13 @@ import { editRefusalOf, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, place
|
|
|
63
63
|
import type { FigureRenderPart } from './renderer.js'
|
|
64
64
|
import type { FigureBudgetLine } from './maturity.js'
|
|
65
65
|
import { HelpToggles } from './help.js'
|
|
66
|
-
import {
|
|
66
|
+
import { sizeInputOf } from './size-response.js'
|
|
67
|
+
import { canFollow, followableAxes, endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
67
68
|
import { jointFanOf } from './joint-fan.js'
|
|
68
69
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
69
70
|
import { jointDragOf } from './joint-drag.js'
|
|
70
71
|
import type { FirstSave } from './first-save.js'
|
|
71
|
-
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE,
|
|
72
|
+
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE, scrubLabel, type LiveValues } from './live.js'
|
|
72
73
|
import { startingShapeKindOfBody } from './starting-shape.js'
|
|
73
74
|
import { groupMenu, GroupMenuStyles } from './group-menu.js'
|
|
74
75
|
|
|
@@ -360,10 +361,14 @@ export class FigureEditor extends LitElement {
|
|
|
360
361
|
}
|
|
361
362
|
/**
|
|
362
363
|
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
363
|
-
* (the network, the server) as it came.
|
|
364
|
+
* (the network, the server) as it came. A loop is said as a loop whichever way it was made: a command, or the size
|
|
365
|
+
* editor's steps (a measure made to follow what follows it, 2026-10-24 -- said as a bare refused step before).
|
|
364
366
|
*/
|
|
365
367
|
private said(error: unknown): string {
|
|
366
|
-
|
|
368
|
+
const code = (error as { code?: string })?.code
|
|
369
|
+
if (code === 'DIMENSION_CYCLE') return i18next.t('figure.text.follow-would-loop')
|
|
370
|
+
if (code === 'FASTEN_CYCLE' || code === 'ATTACH_CYCLE') return i18next.t('figure.text.fasten-would-loop')
|
|
371
|
+
return code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error)
|
|
367
372
|
}
|
|
368
373
|
private run(action: () => unknown) {
|
|
369
374
|
try {
|
|
@@ -818,6 +823,10 @@ export class FigureEditor extends LitElement {
|
|
|
818
823
|
.partId=${this.selected}
|
|
819
824
|
?locked=${unavailable}
|
|
820
825
|
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
826
|
+
@size-recipe=${(e: CustomEvent) => this.applySizeSteps(e.detail.steps)}
|
|
827
|
+
@size-input=${(e: CustomEvent) => this.writeField({ id: e.detail.input, unit: 'mm' }, e.detail.value)}
|
|
828
|
+
@size-typing=${(e: CustomEvent) => this.hold(this.heldOf({ id: e.detail.input, unit: 'mm' }, e.detail.value))}
|
|
829
|
+
@size-let-go=${() => (this.live = NOTHING_LIVE)}
|
|
821
830
|
@fasten-open=${() => this.openFastening()}
|
|
822
831
|
.previewWords=${this.commandPreview.words()}
|
|
823
832
|
.detachWords=${this.commandPreview.detachWords(shown)}
|
|
@@ -837,21 +846,25 @@ export class FigureEditor extends LitElement {
|
|
|
837
846
|
*/
|
|
838
847
|
private renderPartGroups(shown: FigureRevision | undefined, geometry: readonly { id: string }[], fields: FieldLike[], unavailable: boolean, held?: HeldValues) {
|
|
839
848
|
const groups = this.withSizeRow(groupFields(fields, this.selected), held)
|
|
840
|
-
const
|
|
849
|
+
const integrated = !!shown && canFollow(shown.asset, this.selected)
|
|
850
|
+
const dimensionInputs = new Set(integrated ? followableAxes(shown!.asset, this.selected).map(axis => sizeInputOf(shown!.asset, this.selected, axis)) : [])
|
|
851
|
+
const singles = groups.singles.filter(g => g.kind !== 'single' || !dimensionInputs.has(g.field.id))
|
|
852
|
+
const size = integrated ? [] : groups.vectors.filter(g => g.kind === 'vector' && g.name === 'size')
|
|
841
853
|
const place = groups.vectors.filter(g => g.kind === 'vector' && g.name !== 'size')
|
|
842
854
|
const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(this.selected))
|
|
843
855
|
const placeLocked = fastened && place.every(g => g.kind === 'vector' && (['x', 'y', 'z'] as const).every(axis => !g.cells[axis]))
|
|
844
856
|
return this.renderLinks(
|
|
845
857
|
unavailable,
|
|
846
858
|
'shape',
|
|
847
|
-
html`${size.length ||
|
|
848
|
-
? html`<
|
|
859
|
+
html`${size.length || singles.length
|
|
860
|
+
? html`<section slot="size" group-fields>
|
|
861
|
+
<h3>${i18next.t(integrated ? 'figure.label.group-shape' : 'figure.label.group-size')}</h3>
|
|
849
862
|
${size.map(g => this.renderVector(g, unavailable, held))}
|
|
850
|
-
${
|
|
851
|
-
? html`<div form>${
|
|
863
|
+
${singles.length
|
|
864
|
+
? html`<div form>${singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>`
|
|
852
865
|
: nothing}
|
|
853
|
-
${this.renderSizeKeeps()}
|
|
854
|
-
</
|
|
866
|
+
${!integrated ? this.renderSizeKeeps() : nothing}
|
|
867
|
+
</section>`
|
|
855
868
|
: nothing}
|
|
856
869
|
${place.length && !placeLocked
|
|
857
870
|
? html`<div slot="place" group-fields>${place.map(g => this.renderVector(g, unavailable, held))}${this.renderHeldAxes(place)}</div>`
|
|
@@ -860,7 +873,7 @@ export class FigureEditor extends LitElement {
|
|
|
860
873
|
}
|
|
861
874
|
/** The 「반응형」 tab of a part: the links' response groups, its row's own section put in by slot. */
|
|
862
875
|
private renderResponse(shown: FigureRevision | undefined, geometry: readonly { id: string }[], unavailable: boolean) {
|
|
863
|
-
return this.renderLinks(unavailable, 'response', this.renderRepeat(shown, geometry, unavailable))
|
|
876
|
+
return this.renderLinks(unavailable, 'response', html`<div slot="size-note">${this.renderSizeKeeps()}</div>${this.renderRepeat(shown, geometry, unavailable)}`)
|
|
864
877
|
}
|
|
865
878
|
/**
|
|
866
879
|
* 「더 보기」: what few parts need, folded by default and left as this person last left it (chief architect
|
|
@@ -967,6 +980,24 @@ export class FigureEditor extends LitElement {
|
|
|
967
980
|
const done = this.linkCommand({ ...held.action, remap } as FigureAuthoringAction, held.from)
|
|
968
981
|
if (done && held.from === 'section-editor') this.closeSectionEditor()
|
|
969
982
|
}
|
|
983
|
+
/** The figure's size, declared for a part to follow, in one line (ADR ruling Q1). */
|
|
984
|
+
private sizeDeclaredWords() {
|
|
985
|
+
const size = this.designValues()
|
|
986
|
+
return i18next.t('figure.text.size-declared-to-follow', {
|
|
987
|
+
x: Math.round(size['size.x'] ?? 0).toLocaleString(),
|
|
988
|
+
y: Math.round(size['size.y'] ?? 0).toLocaleString(),
|
|
989
|
+
z: Math.round(size['size.z'] ?? 0).toLocaleString()
|
|
990
|
+
})
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* The size editor's steps, one step of the history. A measure made to follow the figure on a figure with no size
|
|
994
|
+
* declares it on the way; that is said in one line, as follow-size said it (ADR ruling Q1, 2026-10-24).
|
|
995
|
+
*/
|
|
996
|
+
private applySizeSteps(steps: readonly FigureProposalStep[]) {
|
|
997
|
+
const hadSize = this.hasSize()
|
|
998
|
+
this.run(() => this.session!.applyRecipe(steps))
|
|
999
|
+
if (!hadSize && !this.error && this.hasSize()) this.notice = this.sizeDeclaredWords()
|
|
1000
|
+
}
|
|
970
1001
|
/** Runs a command; true when it was taken. */
|
|
971
1002
|
private linkCommand(action: FigureAuthoringAction, from: 'section-editor' | 'panel' = 'panel'): boolean {
|
|
972
1003
|
/* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
|
|
@@ -978,14 +1009,7 @@ export class FigureEditor extends LitElement {
|
|
|
978
1009
|
this.error = ''
|
|
979
1010
|
this.faceMapping = undefined
|
|
980
1011
|
/* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
|
|
981
|
-
if (action.kind === 'follow-size' && !hadSize)
|
|
982
|
-
const size = this.designValues()
|
|
983
|
-
this.notice = i18next.t('figure.text.size-declared-to-follow', {
|
|
984
|
-
x: Math.round(size['size.x'] ?? 0).toLocaleString(),
|
|
985
|
-
y: Math.round(size['size.y'] ?? 0).toLocaleString(),
|
|
986
|
-
z: Math.round(size['size.z'] ?? 0).toLocaleString()
|
|
987
|
-
})
|
|
988
|
-
}
|
|
1012
|
+
if (action.kind === 'follow-size' && !hadSize) this.notice = this.sizeDeclaredWords()
|
|
989
1013
|
this.changed()
|
|
990
1014
|
return true
|
|
991
1015
|
} catch (error) {
|
|
@@ -998,19 +1022,20 @@ export class FigureEditor extends LitElement {
|
|
|
998
1022
|
this.error = ''
|
|
999
1023
|
return false
|
|
1000
1024
|
}
|
|
1001
|
-
this.error =
|
|
1002
|
-
code === 'DIMENSION_CYCLE'
|
|
1003
|
-
? i18next.t('figure.text.follow-would-loop')
|
|
1004
|
-
: code === 'FASTEN_CYCLE' || code === 'ATTACH_CYCLE'
|
|
1005
|
-
? i18next.t('figure.text.fasten-would-loop')
|
|
1006
|
-
: this.said(error)
|
|
1025
|
+
this.error = this.said(error)
|
|
1007
1026
|
return false
|
|
1008
1027
|
}
|
|
1009
1028
|
}
|
|
1010
1029
|
private changeField(field: { id: string; unit: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
1011
|
-
const input = event.target as HTMLInputElement
|
|
1012
|
-
|
|
1013
|
-
|
|
1030
|
+
const input = event.target as HTMLInputElement
|
|
1031
|
+
// Rejected edits must also restore the displayed numeric value.
|
|
1032
|
+
input.value = String(this.writeField(field, input.valueAsNumber))
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* A number written to an input, from a field, a scrub or the size editor's rows: a size keeps the part's middle, or its
|
|
1036
|
+
* fastening -- figure-model's one size function (ADR-0140 4.1). Gives the value the input holds after, refused or not.
|
|
1037
|
+
*/
|
|
1038
|
+
private writeField(field: { id: string; unit: FigureInputChange['unit']; role?: string }, value: number): number {
|
|
1014
1039
|
const part = this.selected
|
|
1015
1040
|
const change: FigureInputChange = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }
|
|
1016
1041
|
const asset = this.shown()?.asset
|
|
@@ -1019,8 +1044,7 @@ export class FigureEditor extends LitElement {
|
|
|
1019
1044
|
/* Typed past the mounting plane: said, with the way to fasten it there (ADR-0140 5). */
|
|
1020
1045
|
const after = asset && part && field.role !== 'state' ? throughPlaneOf(this.shown()!.asset, drawableFigureParts(this.session!.read().geometry ?? []), part) : 0
|
|
1021
1046
|
if (after > before + 0.5) this.tellThroughPlane(part, after)
|
|
1022
|
-
|
|
1023
|
-
input.value = String(this.session!.read().fields.find(i => i.id === field.id)!.value)
|
|
1047
|
+
return this.session!.read().fields.find(i => i.id === field.id)!.value
|
|
1024
1048
|
}
|
|
1025
1049
|
/** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
|
|
1026
1050
|
private typing(field: { id: string; unit?: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
@@ -1048,39 +1072,24 @@ export class FigureEditor extends LitElement {
|
|
|
1048
1072
|
* pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
|
|
1049
1073
|
*/
|
|
1050
1074
|
private scrub(field: { id: string; unit: FigureInputChange['unit']; role?: string; value: number }, down: PointerEvent, unavailable: boolean) {
|
|
1051
|
-
if (unavailable
|
|
1075
|
+
if (unavailable) return
|
|
1052
1076
|
const label = down.currentTarget as HTMLElement
|
|
1053
1077
|
const input = (label.closest('div[cell]')?.querySelector('input') ?? label.nextElementSibling) as HTMLInputElement | null
|
|
1054
1078
|
if (!input) return
|
|
1055
|
-
down.preventDefault()
|
|
1056
|
-
label.setPointerCapture(down.pointerId)
|
|
1057
|
-
const start = field.value
|
|
1058
|
-
let value = start
|
|
1059
1079
|
/* Prepared once for the scrub: each move is a size through the same function its release writes with. */
|
|
1060
1080
|
const asset = this.shown()?.asset
|
|
1061
1081
|
const sized = asset && this.selected && field.role !== 'state' ? (() => { try { return figureSizeEditorOf(asset, this.selected) } catch { return undefined } })() : undefined
|
|
1062
|
-
|
|
1063
|
-
value
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
label.removeEventListener('pointercancel', up)
|
|
1071
|
-
/* A drag the system took away is not the author's value: the field goes back to where it was. */
|
|
1072
|
-
if (e.type === 'pointercancel') value = start
|
|
1073
|
-
if (value === start) {
|
|
1074
|
-
input.value = String(start)
|
|
1082
|
+
scrubLabel(down, field, {
|
|
1083
|
+
move: value => {
|
|
1084
|
+
input.value = String(value)
|
|
1085
|
+
this.hold(this.heldOf(field, value, sized))
|
|
1086
|
+
},
|
|
1087
|
+
release: value => (input.value = String(this.writeField(field, value))),
|
|
1088
|
+
cancel: () => {
|
|
1089
|
+
input.value = String(field.value)
|
|
1075
1090
|
this.live = NOTHING_LIVE
|
|
1076
|
-
return
|
|
1077
1091
|
}
|
|
1078
|
-
|
|
1079
|
-
this.changeField(field, { target: input } as unknown as Event)
|
|
1080
|
-
}
|
|
1081
|
-
label.addEventListener('pointermove', move)
|
|
1082
|
-
label.addEventListener('pointerup', up)
|
|
1083
|
-
label.addEventListener('pointercancel', up)
|
|
1092
|
+
})
|
|
1084
1093
|
}
|
|
1085
1094
|
/** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
|
|
1086
1095
|
private renderInstanceRange(shown: FigureRevision | undefined, unavailable: boolean) {
|
|
@@ -1243,45 +1252,45 @@ export class FigureEditor extends LitElement {
|
|
|
1243
1252
|
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?'
|
|
1244
1253
|
const own = `${this.selected}.repeat.pitch`
|
|
1245
1254
|
/* The pitch as it stands: the part's own number, a constant, or what a pitch that follows a length comes to. */
|
|
1246
|
-
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(shown, layout?.args[4])
|
|
1247
1255
|
const fits = layout?.op === 'fit-pitch@1'
|
|
1248
|
-
|
|
1249
|
-
const lengthRef = layout?.args[0] ?? ''
|
|
1250
|
-
const length = asset.designInputs[lengthRef]
|
|
1251
|
-
const longest = model.inputs.find(i => i.id === lengthRef)?.max
|
|
1252
|
-
const limit = model.constants.find(c => c.id === layout?.args[6])?.value
|
|
1256
|
+
const pitch = fits ? asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(shown, layout?.args[4]) : this.rowNow(shown)?.pitch
|
|
1253
1257
|
const field = { id: own, unit: 'mm' as const, role: 'design', value: pitch ?? 0 }
|
|
1254
|
-
const
|
|
1258
|
+
const record = asset.joints?.find(j => j.body1.part === this.selected && j.repeat)
|
|
1259
|
+
const parent = record && 'part' in record.body0 ? record.body0.part : undefined
|
|
1260
|
+
const removable = !!record
|
|
1261
|
+
const axes = record && 'part' in record.body0 && 'face' in record.body0 ? figureFastenAxesOf(asset, record.body0) : []
|
|
1262
|
+
const update = (change: Partial<NonNullable<typeof record>['repeat']>) => {
|
|
1263
|
+
if (!record?.repeat || unavailable) return
|
|
1264
|
+
this.run(() => this.session!.command({ kind: 'set-joint', joint: { ...record, repeat: { ...record.repeat!, ...change } } }))
|
|
1265
|
+
}
|
|
1255
1266
|
return html`<section slot="row" row>
|
|
1256
1267
|
<h3>
|
|
1257
1268
|
${i18next.t('figure.label.repeat')}
|
|
1258
|
-
|
|
1269
|
+
<span section-state active>${i18next.t('figure.label.repeat-active')}</span>
|
|
1259
1270
|
</h3>
|
|
1260
|
-
<div
|
|
1261
|
-
|
|
1271
|
+
<div choice-row role="radiogroup" aria-label=${i18next.t('figure.label.row-basis')}>
|
|
1272
|
+
<label toggle><input type="radio" name="applied-repeat-mode" .checked=${live(fits)} ?disabled=${unavailable || !record} @change=${() => update({ count: undefined, pitch: pitch && pitch > 0 ? pitch : 200 })}>${i18next.t('figure.label.repeat-fills')}</label>
|
|
1273
|
+
<label toggle><input type="radio" name="applied-repeat-mode" .checked=${live(!fits)} ?disabled=${unavailable || !record} @change=${() => update({ count: repeat.count })}>${i18next.t('figure.label.row-by-count')}</label>
|
|
1274
|
+
</div>
|
|
1275
|
+
<p hint>${i18next.t(fits ? 'figure.text.repeat-mode-pitch' : 'figure.text.repeat-mode-count')}</p>
|
|
1276
|
+
<div inline-field><label>${i18next.t('figure.label.size-source')}</label><span value>${parent ?? '—'}</span></div>
|
|
1277
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-axis')}</label>${axes.length ? html`<select aria-label=${i18next.t('figure.label.repeat-axis')} ?disabled=${unavailable} .value=${live(axis)} @change=${(e: Event) => update({ axis: (e.target as HTMLSelectElement).value.toUpperCase() as 'X' | 'Y' | 'Z' })}>${axes.map(a => html`<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}</select>` : html`<span value>${axis.toUpperCase()}</span>`}</div>
|
|
1278
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-count')}</label>${!fits && record ? html`<input type="number" min="1" step="1" aria-label=${i18next.t('figure.label.repeat-count')} .value=${live(String(repeat.count))} ?disabled=${unavailable} @change=${(e: Event) => update({ count: (e.target as HTMLInputElement).valueAsNumber })}>` : html`<span value>${repeat.count}</span>`}</div>
|
|
1262
1279
|
<div inline-field>
|
|
1263
1280
|
<label>${i18next.t('figure.label.repeat-gap')} <span unit>mm</span></label>
|
|
1264
|
-
|
|
1281
|
+
${fits ? html`<input
|
|
1265
1282
|
type="number"
|
|
1266
1283
|
step="any"
|
|
1267
|
-
aria-label=${
|
|
1268
|
-
.value=${String(pitch ?? '')}
|
|
1269
|
-
?disabled=${unavailable
|
|
1284
|
+
aria-label=${i18next.t('figure.label.repeat-gap')}
|
|
1285
|
+
.value=${live(String(pitch ?? ''))}
|
|
1286
|
+
?disabled=${unavailable}
|
|
1270
1287
|
@change=${(e: Event) => this.setPitch(field, e)}
|
|
1271
|
-
|
|
1288
|
+
/>` : html`<span value>${pitch === undefined ? '—' : Number(pitch.toFixed(3))}</span>`}
|
|
1272
1289
|
</div>
|
|
1273
|
-
<ul row-reading>${rowWordsOf(this.rowNow(shown), (key, params) => i18next.t(key, params)).map(line => html`<li>${line}</li>`)}</ul>
|
|
1274
|
-
<
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
length: length.toLocaleString(),
|
|
1278
|
-
count: repeat.count,
|
|
1279
|
-
longest: longest.toLocaleString(),
|
|
1280
|
-
limit
|
|
1281
|
-
})
|
|
1282
|
-
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1283
|
-
</p>
|
|
1284
|
-
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1290
|
+
<ul row-reading>${rowWordsOf(this.rowNow(shown), (key, params) => i18next.t(key, params)).slice(1).map(line => html`<li>${line}</li>`)}</ul>
|
|
1291
|
+
<button quiet ?disabled=${unavailable || !removable} @click=${() => this.run(() => this.session!.command({ kind: 'remove-repeat', part: this.selected }))}>${i18next.t('figure.button.remove-repeat')}</button>
|
|
1292
|
+
<p hint>${i18next.t('figure.text.repeat-remove-explained')}</p>
|
|
1293
|
+
${fits ? this.renderPublicPitch(asset, pitch ?? 0, unavailable) : nothing}
|
|
1285
1294
|
</section>`
|
|
1286
1295
|
}
|
|
1287
1296
|
/** The selected row as it is drawn now, read by figure-model: nothing when it cannot be read. */
|
|
@@ -1589,8 +1598,8 @@ export class FigureEditor extends LitElement {
|
|
|
1589
1598
|
/** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
|
|
1590
1599
|
private renderTab(tab: SideTab, icon: string, label: string) {
|
|
1591
1600
|
/* Its accessible name is the name it shows (designer 2026-09-27); the longer title stays as its tooltip. */
|
|
1592
|
-
const name = i18next.t('figure.label.tab-short-' + tab)
|
|
1593
|
-
return html`<button tab ?on=${this.tab === tab} title=${label} aria-label=${name} @click=${() => (this.tab = tab)}>
|
|
1601
|
+
const name = i18next.t(tab === 'response' && this.selected ? 'figure.label.tab-arrangement' : 'figure.label.tab-short-' + tab)
|
|
1602
|
+
return html`<button tab ?on=${this.tab === tab} title=${tab === 'response' && this.selected ? name : label} aria-label=${name} @click=${() => (this.tab = tab)}>
|
|
1594
1603
|
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span>
|
|
1595
1604
|
</button>`
|
|
1596
1605
|
}
|
|
@@ -1717,8 +1726,8 @@ export class FigureEditor extends LitElement {
|
|
|
1717
1726
|
if (!summary) return nothing
|
|
1718
1727
|
return html`<div subject>
|
|
1719
1728
|
<div subject-summary>
|
|
1720
|
-
|
|
1721
|
-
${summary.
|
|
1729
|
+
<b>${ownerOf(this.selected)}</b>
|
|
1730
|
+
${summary.kind ? html`<small>${summary.kind}</small>` : nothing}
|
|
1722
1731
|
</div>
|
|
1723
1732
|
</div>`
|
|
1724
1733
|
}
|
|
@@ -126,7 +126,8 @@ export function overhangOf(view: FaceView, at: Record<Axis, number>): { touching
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */
|
|
129
|
-
export const turnsInPlace = (asset: FigureAsset, part: string) =>
|
|
129
|
+
export const turnsInPlace = (asset: FigureAsset, part: string) =>
|
|
130
|
+
(asset.joints ?? []).some(j => j.body1.part === part && ((j.motion ?? []).some(a => a.type === 'revolute') || j.origin?.facing === 'centre'))
|
|
130
131
|
|
|
131
132
|
/**
|
|
132
133
|
* How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up
|