@things-factory/figure-ui 10.1.210 → 10.1.212
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/graphql/index.ts +1 -1
- package/client/modeller/angle-dial.ts +1 -1
- package/client/modeller/canvas.ts +1 -1
- package/client/modeller/editor.ts +24 -32
- package/client/modeller/figure-history-panel.ts +1 -1
- package/client/modeller/figure-links.ts +61 -37
- package/client/modeller/figure-parts.ts +3 -3
- package/client/modeller/figure-settings.ts +1 -1
- package/client/modeller/figure-size-editor.ts +41 -4
- package/client/modeller/links.ts +63 -24
- package/client/modeller/live.ts +33 -0
- package/client/modeller/scene-budget.ts +3 -3
- package/client/modeller/thumbnail.ts +1 -1
- package/client/pages/figure-list-page.ts +2 -2
- package/client/pages/figure-modeller-page.ts +4 -4
- package/client/types.ts +2 -2
- package/client/viewparts/figure-thumb.ts +1 -1
- package/dist-client/graphql/index.d.ts +1 -1
- package/dist-client/graphql/index.js +1 -1
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/angle-dial.d.ts +1 -1
- package/dist-client/modeller/angle-dial.js.map +1 -1
- package/dist-client/modeller/canvas.js +1 -1
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +5 -0
- package/dist-client/modeller/editor.js +24 -32
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +1 -1
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +2 -0
- package/dist-client/modeller/figure-links.js +66 -39
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +3 -3
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +1 -1
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +3 -1
- package/dist-client/modeller/figure-size-editor.js +45 -4
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/links.d.ts +15 -6
- package/dist-client/modeller/links.js +63 -25
- 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/scene-budget.d.ts +3 -3
- package/dist-client/modeller/scene-budget.js +3 -3
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/modeller/thumbnail.js +1 -1
- package/dist-client/modeller/thumbnail.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -1
- package/dist-client/pages/figure-list-page.js +2 -2
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -3
- package/dist-client/pages/figure-modeller-page.js +4 -4
- 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/dist-client/types.d.ts +2 -2
- package/dist-client/types.js +1 -1
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.js +1 -1
- package/dist-client/viewparts/figure-thumb.js.map +1 -1
- package/docs/multipart-response-review-2026-10-24.md +1 -1
- package/docs/response-authoring-2026-10-24.md +4 -4
- package/package.json +6 -6
- package/test/ai-proposal.test.ts +1 -1
- package/test/align.test.ts +1 -1
- package/test/browser/editing.check.mjs +38 -0
- package/test/browser/modeller.check.mjs +55 -0
- package/test/command-preview.test.ts +3 -3
- package/test/fields.test.ts +2 -2
- package/test/first-minute.test.ts +5 -5
- package/test/first-save.test.ts +3 -3
- package/test/gizmo.test.ts +3 -3
- package/test/keys.test.ts +2 -2
- package/test/layout.test.ts +1 -1
- package/test/links.test.ts +53 -14
- package/test/live.test.ts +4 -4
- package/test/material.test.ts +1 -1
- package/test/properties.test.ts +1 -1
- package/test/proposal-record.test.ts +1 -1
- package/test/refusals.test.ts +1 -1
- package/test/resize-with-placement.test.ts +1 -1
- package/test/scene-budget.test.ts +4 -4
- package/test/verdict.test.ts +1 -1
- package/translations/en.json +3 -0
- package/translations/es.json +3 -0
- package/translations/ja.json +3 -0
- package/translations/ko.json +109 -106
- package/translations/ms.json +3 -0
- package/translations/zh.json +3 -0
package/client/graphql/index.ts
CHANGED
|
@@ -399,7 +399,7 @@ export interface ProposalOutcome {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
/**
|
|
402
|
-
* 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간
|
|
402
|
+
* 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 피규어들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,
|
|
403
403
|
* 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.
|
|
404
404
|
*/
|
|
405
405
|
export async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가
|
|
14
14
|
* 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.
|
|
15
15
|
*
|
|
16
|
-
* 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고,
|
|
16
|
+
* 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 피규어가 아무리
|
|
17
17
|
* 작게 서 있어도 같은 크기로 읽힌다.
|
|
18
18
|
*/
|
|
19
19
|
|
|
@@ -121,7 +121,7 @@ export class FigureCanvas extends LitElement {
|
|
|
121
121
|
render() {
|
|
122
122
|
/*
|
|
123
123
|
* No line of its own over an empty stage: a new figure's start card says how to begin, and an empty stage otherwise
|
|
124
|
-
* is one loading, or with every part hidden -- where
|
|
124
|
+
* is one loading, or with every part hidden -- where the empty stage's hint flashed and said the wrong thing (2026-10-02).
|
|
125
125
|
*/
|
|
126
126
|
return html`<div stage></div>`
|
|
127
127
|
}
|
|
@@ -69,7 +69,7 @@ import { jointFanOf } from './joint-fan.js'
|
|
|
69
69
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
70
70
|
import { jointDragOf } from './joint-drag.js'
|
|
71
71
|
import type { FirstSave } from './first-save.js'
|
|
72
|
-
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE,
|
|
72
|
+
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE, scrubLabel, type LiveValues } from './live.js'
|
|
73
73
|
import { startingShapeKindOfBody } from './starting-shape.js'
|
|
74
74
|
import { groupMenu, GroupMenuStyles } from './group-menu.js'
|
|
75
75
|
|
|
@@ -824,7 +824,9 @@ export class FigureEditor extends LitElement {
|
|
|
824
824
|
?locked=${unavailable}
|
|
825
825
|
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
826
826
|
@size-recipe=${(e: CustomEvent) => this.applySizeSteps(e.detail.steps)}
|
|
827
|
-
@size-input=${(e: CustomEvent) => this.
|
|
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)}
|
|
828
830
|
@fasten-open=${() => this.openFastening()}
|
|
829
831
|
.previewWords=${this.commandPreview.words()}
|
|
830
832
|
.detachWords=${this.commandPreview.detachWords(shown)}
|
|
@@ -1025,9 +1027,15 @@ export class FigureEditor extends LitElement {
|
|
|
1025
1027
|
}
|
|
1026
1028
|
}
|
|
1027
1029
|
private changeField(field: { id: string; unit: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
1028
|
-
const input = event.target as HTMLInputElement
|
|
1029
|
-
|
|
1030
|
-
|
|
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 {
|
|
1031
1039
|
const part = this.selected
|
|
1032
1040
|
const change: FigureInputChange = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }
|
|
1033
1041
|
const asset = this.shown()?.asset
|
|
@@ -1036,8 +1044,7 @@ export class FigureEditor extends LitElement {
|
|
|
1036
1044
|
/* Typed past the mounting plane: said, with the way to fasten it there (ADR-0140 5). */
|
|
1037
1045
|
const after = asset && part && field.role !== 'state' ? throughPlaneOf(this.shown()!.asset, drawableFigureParts(this.session!.read().geometry ?? []), part) : 0
|
|
1038
1046
|
if (after > before + 0.5) this.tellThroughPlane(part, after)
|
|
1039
|
-
|
|
1040
|
-
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
|
|
1041
1048
|
}
|
|
1042
1049
|
/** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
|
|
1043
1050
|
private typing(field: { id: string; unit?: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
@@ -1065,39 +1072,24 @@ export class FigureEditor extends LitElement {
|
|
|
1065
1072
|
* pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
|
|
1066
1073
|
*/
|
|
1067
1074
|
private scrub(field: { id: string; unit: FigureInputChange['unit']; role?: string; value: number }, down: PointerEvent, unavailable: boolean) {
|
|
1068
|
-
if (unavailable
|
|
1075
|
+
if (unavailable) return
|
|
1069
1076
|
const label = down.currentTarget as HTMLElement
|
|
1070
1077
|
const input = (label.closest('div[cell]')?.querySelector('input') ?? label.nextElementSibling) as HTMLInputElement | null
|
|
1071
1078
|
if (!input) return
|
|
1072
|
-
down.preventDefault()
|
|
1073
|
-
label.setPointerCapture(down.pointerId)
|
|
1074
|
-
const start = field.value
|
|
1075
|
-
let value = start
|
|
1076
1079
|
/* Prepared once for the scrub: each move is a size through the same function its release writes with. */
|
|
1077
1080
|
const asset = this.shown()?.asset
|
|
1078
1081
|
const sized = asset && this.selected && field.role !== 'state' ? (() => { try { return figureSizeEditorOf(asset, this.selected) } catch { return undefined } })() : undefined
|
|
1079
|
-
|
|
1080
|
-
value
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
label.removeEventListener('pointercancel', up)
|
|
1088
|
-
/* A drag the system took away is not the author's value: the field goes back to where it was. */
|
|
1089
|
-
if (e.type === 'pointercancel') value = start
|
|
1090
|
-
if (value === start) {
|
|
1091
|
-
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)
|
|
1092
1090
|
this.live = NOTHING_LIVE
|
|
1093
|
-
return
|
|
1094
1091
|
}
|
|
1095
|
-
|
|
1096
|
-
this.changeField(field, { target: input } as unknown as Event)
|
|
1097
|
-
}
|
|
1098
|
-
label.addEventListener('pointermove', move)
|
|
1099
|
-
label.addEventListener('pointerup', up)
|
|
1100
|
-
label.addEventListener('pointercancel', up)
|
|
1092
|
+
})
|
|
1101
1093
|
}
|
|
1102
1094
|
/** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
|
|
1103
1095
|
private renderInstanceRange(shown: FigureRevision | undefined, unavailable: boolean) {
|
|
@@ -223,7 +223,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
223
223
|
`
|
|
224
224
|
]
|
|
225
225
|
|
|
226
|
-
/** 지금 열려 있는
|
|
226
|
+
/** 지금 열려 있는 피규어. 없으면(새로 만드는 중) 발행할 것이 없다. */
|
|
227
227
|
@property({ type: String }) figureId?: string
|
|
228
228
|
@property({ type: String }) figureState?: 'draft' | 'released'
|
|
229
229
|
@property({ type: Number }) figureVersion = 0
|
|
@@ -7,7 +7,7 @@ import './figure-size-editor.js'
|
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
8
8
|
|
|
9
9
|
import { i18next, localize } from '@operato/i18n'
|
|
10
|
-
import { figureFaceOverhangOf } from '@hatiolab/figure-model'
|
|
10
|
+
import { figureFaceOverhangOf, figureMotionsOf, motionIdOf } from '@hatiolab/figure-model'
|
|
11
11
|
import type { FigureAsset, FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
13
|
import {
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
motionFormRefusalOf,
|
|
29
29
|
endPlaceKeyOf,
|
|
30
30
|
motionJoint,
|
|
31
|
+
controlIdFor,
|
|
32
|
+
withoutAxis,
|
|
31
33
|
placeFacesOf,
|
|
32
34
|
placeIdFor,
|
|
33
35
|
placesOn,
|
|
@@ -183,6 +185,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
183
185
|
@state() private row = freshRow()
|
|
184
186
|
/** The motion form is open over the part's own motion, to change it where it is. */
|
|
185
187
|
@state() private editingMotion = false
|
|
188
|
+
/** Which of the joint's axes the form is over (ADR-0159); one past the last is a new axis. */
|
|
189
|
+
@state() private motionAxis = 0
|
|
186
190
|
|
|
187
191
|
/*
|
|
188
192
|
The forms describe the next command for the selected part, so another part starts from the defaults: carrying the
|
|
@@ -204,6 +208,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
204
208
|
this.rowOpen = false
|
|
205
209
|
this.rowCandidate = undefined
|
|
206
210
|
this.editingMotion = false
|
|
211
|
+
this.motionAxis = 0
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
214
|
|
|
@@ -623,6 +628,43 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
623
628
|
/* Which part moves, against what, which way, how far -- by name (ADR-0097 §5.5). */
|
|
624
629
|
const against = joint ? ('plane' in joint.body0 ? i18next.t('figure.label.mounting-plane') : joint.body0.part) : ''
|
|
625
630
|
const ends = this.motionEnds
|
|
631
|
+
/* One row an axis (ADR-0159): how it moves and by what, changed or taken away where it is; more added under them. */
|
|
632
|
+
const count = joint ? figureMotionsOf(joint).length : 1
|
|
633
|
+
const edit = (k: number) => {
|
|
634
|
+
if (!joint) return
|
|
635
|
+
this.motionAxis = k
|
|
636
|
+
this.motion = motionFormOf(joint, k)
|
|
637
|
+
this.editingMotion = true
|
|
638
|
+
}
|
|
639
|
+
const row = (k: number) => {
|
|
640
|
+
const it = k === 0 ? now : motionOf(asset, this.partId, k)
|
|
641
|
+
if (!it) return nothing
|
|
642
|
+
return html`<div pair motion-axis=${k}>
|
|
643
|
+
<span>
|
|
644
|
+
${count > 1 ? html`<b>${i18next.t('figure.label.motion-axis-n', { n: k + 1 })}</b> ` : nothing}${i18next.t('figure.label.motion-' + it.kind)} · ${it.vector ? jointAxisWord(it.vector) : String(it.axis).toUpperCase()}
|
|
645
|
+
${it.travel
|
|
646
|
+
? html`<small>${i18next.t('figure.label.motion-travel-is', { part: it.travel.part, axis: it.travel.axis.toUpperCase(), plus: it.travel.plus ?? 0 })}</small>`
|
|
647
|
+
: nothing}
|
|
648
|
+
${it.follows?.sum.length === 1
|
|
649
|
+
? html`<small>${i18next.t('figure.label.motion-follows', { joint: it.follows.sum[0]!.joint, multiplier: it.follows.sum[0]!.multiplier, offset: it.follows.offset })}</small>`
|
|
650
|
+
: it.follows
|
|
651
|
+
? html`<small>${i18next.t('figure.label.motion-follow')}: ${it.follows.sum.map(t => `${t.joint} × ${t.multiplier}`).join(' + ')} + ${it.follows.offset}</small>`
|
|
652
|
+
: it.control
|
|
653
|
+
? html`<small>${it.control.id} (${it.control.unit}) ${it.control.min} … ${it.control.max}</small>`
|
|
654
|
+
: nothing}
|
|
655
|
+
</span>
|
|
656
|
+
${count > 1
|
|
657
|
+
? html`<button act remove-axis ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: withoutAxis(joint, k) })}>
|
|
658
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-axis')}
|
|
659
|
+
</button>`
|
|
660
|
+
: html`<button act ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
|
|
661
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
662
|
+
</button>`}
|
|
663
|
+
<button act ?disabled=${this.locked || !joint} @click=${() => edit(k)}>
|
|
664
|
+
<md-icon>edit</md-icon>${i18next.t('figure.button.change-motion')}
|
|
665
|
+
</button>
|
|
666
|
+
</div>`
|
|
667
|
+
}
|
|
626
668
|
return html`<p motion-summary>
|
|
627
669
|
${i18next.t('figure.text.motion-summary', {
|
|
628
670
|
part: this.partId,
|
|
@@ -634,39 +676,17 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
634
676
|
${ends
|
|
635
677
|
? html`<p hint motion-ends>${i18next.t('figure.text.motion-ends-shown', { min: String(ends.min), max: String(ends.max), unit: ends.unit })}</p>`
|
|
636
678
|
: nothing}
|
|
679
|
+
${Array.from({ length: count }, (_, k) => row(k))}
|
|
637
680
|
<div pair>
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
681
|
+
<button act add-axis ?disabled=${this.locked || !joint} @click=${() => edit(count)}>
|
|
682
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-axis')}
|
|
683
|
+
</button>
|
|
684
|
+
${count > 1
|
|
685
|
+
? html`<button act ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
|
|
686
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
687
|
+
</button>`
|
|
642
688
|
: nothing}
|
|
643
|
-
|
|
644
|
-
? html`<small>${i18next.t('figure.label.motion-follows', { joint: now.follows.sum[0]!.joint, multiplier: now.follows.sum[0]!.multiplier, offset: now.follows.offset })}</small>`
|
|
645
|
-
: now.follows
|
|
646
|
-
? html`<small>${i18next.t('figure.label.motion-follow')}: ${now.follows.sum.map(t => `${t.joint} × ${t.multiplier}`).join(' + ')} + ${now.follows.offset}</small>`
|
|
647
|
-
: now.control
|
|
648
|
-
? html`<small>${now.control.id} (${now.control.unit}) ${now.control.min} … ${now.control.max}</small>`
|
|
649
|
-
: nothing}
|
|
650
|
-
</span>
|
|
651
|
-
<button
|
|
652
|
-
act
|
|
653
|
-
?disabled=${this.locked || !joint}
|
|
654
|
-
@click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}
|
|
655
|
-
>
|
|
656
|
-
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
657
|
-
</button>
|
|
658
|
-
<button
|
|
659
|
-
act
|
|
660
|
-
?disabled=${this.locked || !joint}
|
|
661
|
-
@click=${() => {
|
|
662
|
-
if (!joint) return
|
|
663
|
-
this.motion = motionFormOf(joint)
|
|
664
|
-
this.editingMotion = true
|
|
665
|
-
}}
|
|
666
|
-
>
|
|
667
|
-
<md-icon>edit</md-icon>${i18next.t('figure.button.change-motion')}
|
|
668
|
-
</button>
|
|
669
|
-
</div>`
|
|
689
|
+
</div>`
|
|
670
690
|
}
|
|
671
691
|
/* A joint needs a parent (URDF): a part fastened to nothing is fastened first. */
|
|
672
692
|
if (!joint)
|
|
@@ -677,10 +697,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
677
697
|
const m = this.motion
|
|
678
698
|
const unit = m.kind === 'slide' ? 'mm' : 'deg'
|
|
679
699
|
/* A follower follows a joint that moves the same way; a choice the kind no longer allows is dropped. */
|
|
680
|
-
const
|
|
700
|
+
const k = this.motionAxis
|
|
701
|
+
const leaders = leadersFor(asset, this.partId, m.kind, motionIdOf(joint, k))
|
|
702
|
+
const adding = k > 0 && k >= figureMotionsOf(joint).length
|
|
681
703
|
const follow = leaders.some(j => j.id === m.follow) ? m.follow : ''
|
|
682
704
|
const refusal = motionFormRefusalOf(m, follow)
|
|
683
705
|
return html`
|
|
706
|
+
${k > 0 ? html`<p motion-axis-heading><b>${i18next.t('figure.label.motion-axis-n', { n: k + 1 })}</b></p>` : nothing}
|
|
684
707
|
<div pair>
|
|
685
708
|
<select @change=${(e: Event) => this.setKind((e.target as HTMLSelectElement).value as 'slide' | 'turn' | 'continuous')}>
|
|
686
709
|
<option value="slide" ?selected=${m.kind === 'slide'}>${i18next.t('figure.label.motion-slide')}</option>
|
|
@@ -736,7 +759,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
736
759
|
: nothing}`
|
|
737
760
|
: nothing}
|
|
738
761
|
<span>${i18next.t('figure.label.control-id')}</span>
|
|
739
|
-
<input .value=${m.id} placeholder=${
|
|
762
|
+
<input .value=${m.id} placeholder=${controlIdFor(this.partId, m.kind, k)} @change=${(e: Event) => (this.motion = { ...m, id: (e.target as HTMLInputElement).value.trim() })} />
|
|
740
763
|
${m.kind === 'continuous' || (m.kind === 'slide' && (m.travel || m.travelSum))
|
|
741
764
|
? nothing
|
|
742
765
|
: html`<span>${i18next.t('figure.label.control-range')} (${unit})</span>
|
|
@@ -750,14 +773,15 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
750
773
|
?disabled=${this.locked || !!refusal}
|
|
751
774
|
title=${refusal === 'range' ? i18next.t('figure.text.range-needs-room') : ''}
|
|
752
775
|
@click=${() => {
|
|
753
|
-
this.command({ kind: 'set-joint', joint: motionJoint(joint, { ...m, follow }, this.partId) })
|
|
776
|
+
this.command({ kind: 'set-joint', joint: motionJoint(joint, { ...m, follow }, this.partId, k) })
|
|
754
777
|
this.editingMotion = false
|
|
778
|
+
this.motionAxis = 0
|
|
755
779
|
}}
|
|
756
780
|
>
|
|
757
|
-
<md-icon>animation</md-icon>${i18next.t(this.editingMotion ? 'figure.button.apply-motion-change' : 'figure.button.add-motion')}
|
|
781
|
+
<md-icon>animation</md-icon>${i18next.t(adding ? 'figure.button.add-axis' : this.editingMotion ? 'figure.button.apply-motion-change' : 'figure.button.add-motion')}
|
|
758
782
|
</button>
|
|
759
783
|
${this.editingMotion
|
|
760
|
-
? html`<button act @click=${() => (this.editingMotion = false)}><md-icon>undo</md-icon>${i18next.t('figure.button.cancel-motion-change')}</button>`
|
|
784
|
+
? html`<button act @click=${() => ((this.editingMotion = false), (this.motionAxis = 0))}><md-icon>undo</md-icon>${i18next.t('figure.button.cancel-motion-change')}</button>`
|
|
761
785
|
: nothing}
|
|
762
786
|
`
|
|
763
787
|
}
|
|
@@ -10,11 +10,11 @@ import type { FigureBudgetLine } from './maturity.js'
|
|
|
10
10
|
import { partLabelOf } from './fields.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* 부품 팔레트와 부품 목록 — 자산의 것.
|
|
14
14
|
*
|
|
15
15
|
* 팔레트는 목록 위에, 눈 단추는 늘 자리를 차지하고, 조작은 고른 줄에서만 보인다.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* 부품은 평가가 준 형상에서 읽은 시작 모양 이름(`kind`)으로 보인다. 「숨김」은 여기서도 **작업대의 것**이다 — 저장되지 않고, 자산의 `visibility`(상태에 따라
|
|
18
18
|
* 보이고 안 보이는 것)와는 다른 것이다. 그것은 속성 패널의 몫이다.
|
|
19
19
|
*
|
|
20
20
|
* 이 컴포넌트는 정본을 만지지 않는다. 무엇을 하겠다는 것만 알린다.
|
|
@@ -475,7 +475,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
475
475
|
/** 편집할 수 없을 때(불러오는 중 · 저장 중) 팔레트와 조작을 잠근다. */
|
|
476
476
|
@property({ type: Boolean }) locked = false
|
|
477
477
|
/**
|
|
478
|
-
* 이
|
|
478
|
+
* 이 피규어가 치르는 값 — 삼각형 · 묶음 · 한 화면에 몇 대 · 등급. 부품을 더하는 바로 그 자리에서 보여야 로우폴리의
|
|
479
479
|
* 기준이 저작을 이끈다. 등급이 D 면 무엇이 넘쳤는지를 적는다. 누르면 성숙도 탭이 열린다.
|
|
480
480
|
*/
|
|
481
481
|
@property({ attribute: false }) budget?: FigureBudgetLine
|
|
@@ -166,7 +166,7 @@ export class FigureSettings extends localize(i18next)(LitElement) {
|
|
|
166
166
|
있나」이기 때문이다. 미리보기가 도면에 놓인 모습을 말하는 자리이므로, 어떤 도면을
|
|
167
167
|
가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
피규어 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 피규어가 어느 면에
|
|
170
170
|
붙는가(저장된다), 이쪽은 그 면을 가진 도면이 어느 쪽을 기준으로 재는가(저장 안 된다).
|
|
171
171
|
-->
|
|
172
172
|
<section>
|
|
@@ -6,6 +6,7 @@ import type { FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
|
6
6
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
7
7
|
import type { FigureRevision } from './revision.js'
|
|
8
8
|
import { AXES, followOf, followableAxes, measureNow, type Axis } from './links.js'
|
|
9
|
+
import { scrubLabel } from './live.js'
|
|
9
10
|
import {
|
|
10
11
|
measureFactor,
|
|
11
12
|
sameSizeSource,
|
|
@@ -22,7 +23,8 @@ import {
|
|
|
22
23
|
* A part's measures and what each follows, one row an axis (ADR-0159 3). Made to follow, a measure takes what the part
|
|
23
24
|
* hangs from, along the axis its own axis lies on as it stands -- the author turns it on, nothing more. Another source
|
|
24
25
|
* is there when asked for, or when there is no such axis (the part lies at an angle) or the measure already follows
|
|
25
|
-
* something else; each way of following says what it will do, in millimetres.
|
|
26
|
+
* something else; each way of following says what it will do, in millimetres. A measure of the part's own is a value in
|
|
27
|
+
* hand as on any field (user 2026-09-24): drawn while it is typed or its label is dragged, written once on the release.
|
|
26
28
|
*/
|
|
27
29
|
@customElement('figure-size-editor')
|
|
28
30
|
export class FigureSizeEditor extends localize(i18next)(LitElement) {
|
|
@@ -50,6 +52,9 @@ export class FigureSizeEditor extends localize(i18next)(LitElement) {
|
|
|
50
52
|
.result { margin: 0 0 6px 30px; }
|
|
51
53
|
[role=alert] { color: var(--md-sys-color-error); }
|
|
52
54
|
`]
|
|
55
|
+
private tell(name: 'size-input' | 'size-typing' | 'size-let-go', detail?: object) {
|
|
56
|
+
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }))
|
|
57
|
+
}
|
|
53
58
|
private apply(steps: FigureAuthoringAction[]) {
|
|
54
59
|
if (this.locked) return
|
|
55
60
|
this.error = ''
|
|
@@ -116,6 +121,38 @@ export class FigureSizeEditor extends localize(i18next)(LitElement) {
|
|
|
116
121
|
const choosing = relation.variable && (this.choosing.has(key) || !sameSizeSource(source, suggested))
|
|
117
122
|
const selectedSource = source ? (source.of === 'instance' ? '@figure' : source.part) : '@custom'
|
|
118
123
|
const sourceAxes = source?.of === 'part' ? followableAxes(asset, source.part) : AXES
|
|
124
|
+
const factor = measureFactor(asset, this.partId, axis)
|
|
125
|
+
/* Its own measure, no rule over it: in hand while typed or scrubbed, as the input it is (a diameter, its radius). */
|
|
126
|
+
const own = !relation.variable && !!input && !this.locked
|
|
127
|
+
const typingSize = (e: Event) => {
|
|
128
|
+
const value = (e.target as HTMLInputElement).valueAsNumber
|
|
129
|
+
if (own && Number.isFinite(value) && value > 0) this.tell('size-typing', { input, value: value / factor })
|
|
130
|
+
}
|
|
131
|
+
const scrubSize = (down: PointerEvent) => {
|
|
132
|
+
if (!own || mm === undefined) return
|
|
133
|
+
const field = (down.currentTarget as HTMLElement).closest('.fields')?.querySelector<HTMLInputElement>('input[type=number]')
|
|
134
|
+
if (!field) return
|
|
135
|
+
scrubLabel(down, { value: mm, unit: 'mm' }, {
|
|
136
|
+
move: value => {
|
|
137
|
+
field.value = String(value)
|
|
138
|
+
if (value > 0) this.tell('size-typing', { input, value: value / factor })
|
|
139
|
+
},
|
|
140
|
+
release: value => {
|
|
141
|
+
if (!(value > 0)) {
|
|
142
|
+
field.value = String(mm)
|
|
143
|
+
this.tell('size-let-go')
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
this.error = ''
|
|
147
|
+
this.tell('size-input', { input, value: value / factor, part: this.partId })
|
|
148
|
+
this.requestUpdate()
|
|
149
|
+
},
|
|
150
|
+
cancel: () => {
|
|
151
|
+
field.value = String(mm)
|
|
152
|
+
this.tell('size-let-go')
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
119
156
|
const changeSize = (e: Event) => {
|
|
120
157
|
const value = (e.target as HTMLInputElement).valueAsNumber
|
|
121
158
|
if (!(Number.isFinite(value) && value > 0)) {
|
|
@@ -126,7 +163,7 @@ export class FigureSizeEditor extends localize(i18next)(LitElement) {
|
|
|
126
163
|
if (relation.variable && source && mode !== 'custom') this.configure(axis, source, mode, value)
|
|
127
164
|
else if (!relation.variable && input) {
|
|
128
165
|
this.error = ''
|
|
129
|
-
this.
|
|
166
|
+
this.tell('size-input', { input, value: value / factor, part: this.partId })
|
|
130
167
|
this.requestUpdate()
|
|
131
168
|
}
|
|
132
169
|
}
|
|
@@ -145,9 +182,9 @@ export class FigureSizeEditor extends localize(i18next)(LitElement) {
|
|
|
145
182
|
}
|
|
146
183
|
return html`<div class="dimension" data-axis=${axis}>
|
|
147
184
|
<div class="fields">
|
|
148
|
-
<span>${label}</span>
|
|
185
|
+
<span ?scrub=${own} @pointerdown=${scrubSize}>${label}</span>
|
|
149
186
|
<input type="number" step="any" min="0" aria-label=${`${this.partId} ${label} ${i18next.t('figure.label.size-measure')}`} .value=${live(mm === undefined ? '' : String(Number(mm.toFixed(6))))}
|
|
150
|
-
?disabled=${this.locked} ?readonly=${relation.variable ? mode === 'custom' || uniform : !input} @change=${changeSize}>
|
|
187
|
+
?disabled=${this.locked} ?readonly=${relation.variable ? mode === 'custom' || uniform : !input} @input=${typingSize} @change=${changeSize}>
|
|
151
188
|
<label class="variable"><input type="checkbox" aria-label=${`${this.partId} ${label} ${i18next.t('figure.label.size-follows')}`} .checked=${live(relation.variable || this.choosing.has(key))} ?disabled=${this.locked || uniform}
|
|
152
189
|
@change=${follows}></label>
|
|
153
190
|
${choosing || (this.choosing.has(key) && !relation.variable)
|
package/client/modeller/links.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeRangeOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion, figureSurfaceOf, figureAxesOf, figureMotionsOf } from '@hatiolab/figure-model'
|
|
1
|
+
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeRangeOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion, figureSurfaceOf, figureAxesOf, figureMotionsOf, motionIdOf, motionStateIdOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureFollow, FigureInstanceSizeRange } from '@hatiolab/figure-model'
|
|
3
3
|
import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureDimensionSource, FigureFace, FigureFacing, FigureJoint, FigureJointMotion, FigureNamedMotion, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
4
4
|
import { startingShapeKindOf } from './starting-shape.js'
|
|
@@ -152,7 +152,7 @@ export function partSummaryOf(
|
|
|
152
152
|
if (f.of === 'instance') {
|
|
153
153
|
key = f.times === 1 ? 'figure.text.summary-size-follows' : 'figure.text.summary-size-share'
|
|
154
154
|
/* A measure following the figure's own size on its own axis says no measure word, so the axes that do so are one
|
|
155
|
-
sentence -- 「너비 · 높이 · 깊이는
|
|
155
|
+
sentence -- 「너비 · 높이 · 깊이는 피규어 크기를 따라 바뀝니다」, not three (user 2026-10-16). */
|
|
156
156
|
params = f.times === 1 && f.axis === axis && !f.plus ? {} : { pct: pct(f.times), whole: say(`figure.label.measure-${f.axis}`) }
|
|
157
157
|
if (f.plus) (key = key + '-plus'), (params.plus = String(f.plus))
|
|
158
158
|
} else if (f.of === 'part') {
|
|
@@ -531,16 +531,32 @@ export interface Motion {
|
|
|
531
531
|
travel?: { part: string; axis: string; plus?: number }
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
export function motionOf(asset: FigureAsset, part: string): Motion | undefined {
|
|
534
|
+
export function motionOf(asset: FigureAsset, part: string, k = 0): Motion | undefined {
|
|
535
535
|
const m = modelOf(asset)
|
|
536
|
-
const node = m.nodes.find(n => n.id === `${part}.motion`)
|
|
537
|
-
if (!node) return undefined
|
|
538
536
|
const joint = asset.joints?.find(j => j.body1.part === part)
|
|
539
|
-
|
|
540
|
-
const record = joint && figureMotionsOf(joint)[0]
|
|
537
|
+
const record = joint && figureMotionsOf(joint)[k]
|
|
541
538
|
/* A follower has no limits of its own: its range is its leaders', not a turn without end. */
|
|
542
539
|
const endless = record?.type === 'revolute' && record.lowerLimit === undefined && record.upperLimit === undefined && !record.mimic
|
|
543
540
|
const vector = jointAxisVectorOf(record?.axis)
|
|
541
|
+
const one = record?.travel && 'source' in record.travel ? record.travel : undefined
|
|
542
|
+
const travel = one?.source && 'part' in one.source ? { part: one.source.part, axis: one.source.axis, ...(one.plus ? { plus: one.plus } : {}) } : undefined
|
|
543
|
+
const said = { ...(vector ? { vector } : {}), ...(record?.mimic ? { follows: figureMimicTermsOf(record.mimic)! } : {}), ...(travel ? { travel } : {}) }
|
|
544
|
+
/*
|
|
545
|
+
An axis after the first is said as its record writes it -- the joint frame's axis, the control by its id -- the frame
|
|
546
|
+
the form edits it in (ADR-0159). The first is read from its node, `<part>.motion`, as before.
|
|
547
|
+
*/
|
|
548
|
+
if (k > 0) {
|
|
549
|
+
if (!record) return undefined
|
|
550
|
+
const input = m.inputs.find(i => i.id === motionStateIdOf(record) && i.role === 'state')
|
|
551
|
+
return {
|
|
552
|
+
kind: endless ? 'continuous' : record.type === 'revolute' ? 'turn' : 'slide',
|
|
553
|
+
axis: jointAxisToken(record.axis) ?? '?',
|
|
554
|
+
...(record.mimic || !input ? {} : { control: { id: input.id, unit: input.unit, min: input.min, max: input.max } }),
|
|
555
|
+
...said
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const node = m.nodes.find(n => n.id === `${part}.motion`)
|
|
559
|
+
if (!node) return undefined
|
|
544
560
|
const kind = endless ? 'continuous' : node.op === 'axis-turn@1' ? 'turn' : 'slide'
|
|
545
561
|
const unit = node.args.slice(0, 3).map(ref => m.constants.find(c => c.id === ref)?.value)
|
|
546
562
|
const axis = AXES[unit.findIndex(v => v !== undefined && v !== 0)] ?? '?'
|
|
@@ -559,9 +575,7 @@ export function motionOf(asset: FigureAsset, part: string): Motion | undefined {
|
|
|
559
575
|
}
|
|
560
576
|
return undefined
|
|
561
577
|
}
|
|
562
|
-
|
|
563
|
-
const travel = one?.source && 'part' in one.source ? { part: one.source.part, axis: one.source.axis, ...(one.plus ? { plus: one.plus } : {}) } : undefined
|
|
564
|
-
return { kind, axis, ...(vector ? { vector } : {}), control: control(quantity), ...(record?.mimic ? { follows: figureMimicTermsOf(record.mimic)! } : {}), ...(travel ? { travel } : {}) }
|
|
578
|
+
return { kind, axis, control: control(quantity), ...said }
|
|
565
579
|
}
|
|
566
580
|
|
|
567
581
|
/** What the motion form holds: how the part moves, and either its own control or the joint it moves with. */
|
|
@@ -596,10 +610,16 @@ export interface MotionForm {
|
|
|
596
610
|
* The motion form filled from a joint as it is, so a motion can be changed where it is -- its range, the length it
|
|
597
611
|
* travels by, what it follows -- rather than taken away and made again. Taking it away is refused while another joint
|
|
598
612
|
* follows it, which left a leader's stroke fixed for good (the telescopic conveyor, 2026-09-25). The form is the
|
|
599
|
-
* joint's
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
613
|
+
* joint's axis `k` (ADR-0159), the first unless said; the other axes are kept as they are when it is written
|
|
614
|
+
* (`motionJoint`). An axis past the last is a new one: a turn about the first of X, Y, Z the joint's axes do not use.
|
|
615
|
+
*/
|
|
616
|
+
export function motionFormOf(joint: FigureJoint, k = 0): MotionForm {
|
|
617
|
+
const axes = figureMotionsOf(joint)
|
|
618
|
+
const a = axes[k]
|
|
619
|
+
if (!a && k > 0) {
|
|
620
|
+
const used = new Set(axes.map(x => jointAxisToken(x.axis)))
|
|
621
|
+
return { kind: 'turn', axis: AXES.find(x => !used.has(x)) ?? 'x', id: '', min: -90, max: 90, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 }
|
|
622
|
+
}
|
|
603
623
|
const kind: MotionForm['kind'] = a?.type === 'prismatic' ? 'slide' : a?.lowerLimit === undefined && a?.upperLimit === undefined && !a?.mimic ? 'continuous' : 'turn'
|
|
604
624
|
const one = a?.travel && 'source' in a.travel ? a.travel : undefined
|
|
605
625
|
const travel = one?.source && 'part' in one.source ? `${one.source.part}:${one.source.axis}` : ''
|
|
@@ -629,9 +649,10 @@ export function motionFormOf(joint: FigureJoint): MotionForm {
|
|
|
629
649
|
* The axes a part's joint can move with (URDF's mimic): one of another part's that moves the same way — a slide follows
|
|
630
650
|
* a slide, a turn a turn — and has a control of its own, since a follower follows the axis that is driven (ADR-0159).
|
|
631
651
|
*/
|
|
632
|
-
export function leadersFor(asset: FigureAsset, part: string, kind: MotionForm['kind']): Array<FigureNamedMotion & { part: string }> {
|
|
652
|
+
export function leadersFor(asset: FigureAsset, part: string, kind: MotionForm['kind'], self?: string): Array<FigureNamedMotion & { part: string }> {
|
|
653
|
+
/* Another axis of the same joint may lead (a shoulder's second axis following its first); never the axis itself. */
|
|
633
654
|
return figureAxesOf(asset.joints)
|
|
634
|
-
.filter(({ joint, motion }) => joint.body1.part !== part && !motion.mimic && (motion.type === 'prismatic') === (kind === 'slide'))
|
|
655
|
+
.filter(({ joint, motion }) => (self === undefined ? joint.body1.part !== part : motion.id !== self) && !motion.mimic && (motion.type === 'prismatic') === (kind === 'slide'))
|
|
635
656
|
.map(({ joint, motion }) => ({ ...motion, part: joint.body1.part }))
|
|
636
657
|
}
|
|
637
658
|
|
|
@@ -640,27 +661,45 @@ export function leadersFor(asset: FigureAsset, part: string, kind: MotionForm['k
|
|
|
640
661
|
* a turn without end, as in USD) and a control; a follower gets the leader, the multiplier and the offset, and no
|
|
641
662
|
* control or limits of its own — its range is the leader's times the multiplier.
|
|
642
663
|
*/
|
|
643
|
-
export function motionJoint(joint: FigureJoint, m: MotionForm, part: string): FigureJoint {
|
|
664
|
+
export function motionJoint(joint: FigureJoint, m: MotionForm, part: string, k = 0): FigureJoint {
|
|
644
665
|
/*
|
|
645
666
|
Where it is fastened -- its row and its frame's own words too -- is the kernel's to keep (`figureJointWithMotion`,
|
|
646
|
-
ADR-0140 D6). The form writes
|
|
667
|
+
ADR-0140 D6). The form writes axis `k` under the name it had, or adds it past the last; the others stay as they are
|
|
668
|
+
(ADR-0159).
|
|
647
669
|
*/
|
|
648
|
-
const was = joint.motion?.[
|
|
670
|
+
const was = joint.motion?.[k]
|
|
649
671
|
const moving = {
|
|
650
672
|
...(was?.id !== undefined ? { id: was.id } : {}),
|
|
651
673
|
type: m.kind === 'slide' ? ('prismatic' as const) : ('revolute' as const),
|
|
652
674
|
axis: (m.axisVector ?? m.axis.toUpperCase()) as FigureJointMotion['axis'],
|
|
653
675
|
...(was?.through ? { through: was.through } : {})
|
|
654
676
|
}
|
|
655
|
-
const
|
|
677
|
+
const axes = joint.motion ?? []
|
|
678
|
+
const given = (motion: Omit<FigureJointMotion, 'type' | 'axis'>) => figureJointWithMotion(joint, [...axes.slice(0, k), { ...moving, ...motion }, ...axes.slice(k + 1)])
|
|
679
|
+
const control = m.id || controlIdFor(part, m.kind, k)
|
|
656
680
|
if (m.follow) return given({ mimic: { motion: m.follow, multiplier: m.multiplier, ...(m.offset ? { offset: m.offset } : {}) } })
|
|
657
681
|
if (m.followSum) return given({ mimic: { sum: m.followSum.map(t => ({ motion: t.joint, multiplier: t.multiplier })), ...(m.offset ? { offset: m.offset } : {}) } })
|
|
658
|
-
if (m.kind === 'slide' && m.travelSum && !m.travel) return given({ travel: m.travelSum, state: { id:
|
|
682
|
+
if (m.kind === 'slide' && m.travelSum && !m.travel) return given({ travel: m.travelSum, state: { id: control } })
|
|
659
683
|
if (m.kind === 'slide' && m.travel) {
|
|
660
684
|
const [of, axis] = m.travel.split(':')
|
|
661
|
-
return given({ travel: { source: { of: 'part', part: of!, axis: axis as Axis }, ...(m.travelPlus ? { plus: m.travelPlus } : {}) }, state: { id:
|
|
685
|
+
return given({ travel: { source: { of: 'part', part: of!, axis: axis as Axis }, ...(m.travelPlus ? { plus: m.travelPlus } : {}) }, state: { id: control } })
|
|
662
686
|
}
|
|
663
|
-
return given({ ...(m.kind === 'continuous' ? {} : { lowerLimit: m.min, upperLimit: m.max }), state: { id:
|
|
687
|
+
return given({ ...(m.kind === 'continuous' ? {} : { lowerLimit: m.min, upperLimit: m.max }), state: { id: control } })
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** The control an axis gets when the author names none: `<part>-<kind>`, and `<part>-<kind>-<n>` for the n-th axis. */
|
|
691
|
+
export function controlIdFor(part: string, kind: MotionForm['kind'], k: number): string {
|
|
692
|
+
return k === 0 ? `${part}-${kind}` : `${part}-${kind}-${k + 1}`
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The joint without its axis `k`. The axes after it keep the names they went by: one named by its place
|
|
697
|
+
* (`<joint>.<k>`, figure-model `motionIdOf`) is given that name before it moves up, so its control is not renamed under
|
|
698
|
+
* a board that writes to it.
|
|
699
|
+
*/
|
|
700
|
+
export function withoutAxis(joint: FigureJoint, k: number): FigureJoint {
|
|
701
|
+
const axes = joint.motion ?? []
|
|
702
|
+
return figureJointWithMotion(joint, [...axes.slice(0, k), ...axes.slice(k + 1).map((a, i) => (a.id !== undefined ? a : { ...a, id: motionIdOf(joint, k + 1 + i) }))])
|
|
664
703
|
}
|
|
665
704
|
|
|
666
705
|
/**
|
|
@@ -677,7 +716,7 @@ export function motionFormRefusalOf(m: MotionForm, follow: string): 'multiplier'
|
|
|
677
716
|
|
|
678
717
|
/** The select's value for what a dimension follows now. */
|
|
679
718
|
/**
|
|
680
|
-
* What a measure that is not its own follows, in the words of the choice that set it --
|
|
719
|
+
* What a measure that is not its own follows, in the words of the choice that set it -- 「피규어 X 크기에 비례」, 「bed의 X」 --
|
|
681
720
|
* where the row once said only 「다른 규칙에 연결됨」 (user 2026-10-16: which rule?).
|
|
682
721
|
*/
|
|
683
722
|
export function followWordsOf(follow: Follow, say: (key: string, params?: Record<string, string>) => string): string {
|
package/client/modeller/live.ts
CHANGED
|
@@ -90,6 +90,39 @@ export function scrubbed(start: number, pixels: number, step: number): number {
|
|
|
90
90
|
return Number(value.toFixed(digits))
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* A label dragged sideways scrubs a value, as vector tools do: each move gives the value the pointer has come to, the
|
|
95
|
+
* release gives the last one, and a drag that ends where it began -- or that the system took away -- gives it back. The
|
|
96
|
+
* label holds the pointer for the drag. Every field that scrubs goes through here: the part's own numbers and the size
|
|
97
|
+
* editor's rows.
|
|
98
|
+
*/
|
|
99
|
+
export function scrubLabel(
|
|
100
|
+
down: PointerEvent,
|
|
101
|
+
from: { value: number; unit: string },
|
|
102
|
+
to: { move: (value: number) => void; release: (value: number) => void; cancel: () => void }
|
|
103
|
+
): void {
|
|
104
|
+
if (down.button !== 0) return
|
|
105
|
+
const label = down.currentTarget as HTMLElement
|
|
106
|
+
down.preventDefault()
|
|
107
|
+
label.setPointerCapture(down.pointerId)
|
|
108
|
+
let value = from.value
|
|
109
|
+
const move = (e: PointerEvent) => {
|
|
110
|
+
value = scrubbed(from.value, e.clientX - down.clientX, scrubStep(from.unit, from.value, e.shiftKey))
|
|
111
|
+
to.move(value)
|
|
112
|
+
}
|
|
113
|
+
const up = (e: PointerEvent) => {
|
|
114
|
+
label.removeEventListener('pointermove', move)
|
|
115
|
+
label.removeEventListener('pointerup', up)
|
|
116
|
+
label.removeEventListener('pointercancel', up)
|
|
117
|
+
/* A drag the system took away is not the author's value. */
|
|
118
|
+
if (e.type === 'pointercancel' || value === from.value) to.cancel()
|
|
119
|
+
else to.release(value)
|
|
120
|
+
}
|
|
121
|
+
label.addEventListener('pointermove', move)
|
|
122
|
+
label.addEventListener('pointerup', up)
|
|
123
|
+
label.addEventListener('pointercancel', up)
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
/**
|
|
94
127
|
* The bodies to draw while a value is in hand, over every input of the version as the session holds it now: the pose the
|
|
95
128
|
* author has already set with the other controls, and the design values. Drawing over the asset's saved defaults put
|