@things-factory/figure-ui 10.1.205 → 10.1.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/editor-styles.ts +16 -0
- package/client/modeller/editor.ts +53 -137
- package/client/modeller/fasten-form.ts +50 -11
- package/client/modeller/figure-facts.ts +1 -1
- package/client/modeller/figure-fasten-dialog.ts +3 -2
- package/client/modeller/figure-history-panel.ts +1 -29
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +10 -12
- package/client/modeller/figure-verdict.ts +2 -17
- package/client/modeller/gizmo.ts +10 -0
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +32 -41
- package/client/modeller/maturity.ts +11 -27
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +20 -33
- package/client/pages/figure-modeller-page.ts +7 -22
- package/dist-client/modeller/editor-styles.js +16 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +8 -11
- package/dist-client/modeller/editor.js +57 -138
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +16 -7
- package/dist-client/modeller/fasten-form.js +45 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-facts.js +1 -1
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +3 -3
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +0 -16
- package/dist-client/modeller/figure-history-panel.js +1 -27
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +8 -11
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +0 -5
- package/dist-client/modeller/figure-verdict.js +2 -21
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +6 -0
- package/dist-client/modeller/gizmo.js +11 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +16 -22
- package/dist-client/modeller/links.js +26 -39
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/maturity.d.ts +2 -4
- package/dist-client/modeller/maturity.js +12 -26
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +7 -17
- package/dist-client/modeller/refusals.js +17 -34
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +1 -3
- package/dist-client/pages/figure-modeller-page.js +7 -23
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/browser/acceptance-page.js +4 -4
- package/test/browser/detail-page.js +1 -3
- package/test/browser/detail-tabs-page.js +1 -2
- package/test/browser/links.check.mjs +32 -50
- package/test/browser/modeller.check.mjs +46 -19
- package/test/browser/parts-page.js +2 -2
- package/test/browser/parts.check.mjs +51 -12
- package/test/browser/product-page.check.mjs +6 -9
- package/test/browser/product-page.js +1 -2
- package/test/browser/screen-page.js +2 -6
- package/test/browser/screen.check.mjs +8 -65
- package/test/browser/section.check.mjs +27 -0
- package/test/fasten-form.test.ts +42 -2
- package/test/first-minute.test.ts +2 -3
- package/test/fixtures/joint-column.json +1 -66
- package/test/fixtures/joint-roller.json +1 -66
- package/test/fixtures/robot-arm-6dof.json +2 -67
- package/test/gizmo.test.ts +12 -1
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +39 -34
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +9 -12
- package/test/refusals.test.ts +20 -42
- package/test/resize-with-placement.test.ts +13 -15
- package/test/revision.test.ts +1 -2
- package/test/verdict.test.ts +1 -15
- package/translations/en.json +8 -32
- package/translations/es.json +8 -32
- package/translations/ja.json +8 -32
- package/translations/ko.json +8 -32
- package/translations/ms.json +8 -32
- package/translations/zh.json +8 -32
- package/client/modeller/occupancy.ts +0 -51
- package/dist-client/modeller/occupancy.d.ts +0 -31
- package/dist-client/modeller/occupancy.js +0 -36
- package/dist-client/modeller/occupancy.js.map +0 -1
- package/test/release-panel.test.ts +0 -53
|
@@ -131,6 +131,22 @@ export const EditorStyles = css`
|
|
|
131
131
|
min-width: 0;
|
|
132
132
|
min-height: 0;
|
|
133
133
|
}
|
|
134
|
+
/* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */
|
|
135
|
+
p[same-measure] {
|
|
136
|
+
position: absolute;
|
|
137
|
+
bottom: 12px;
|
|
138
|
+
left: 50%;
|
|
139
|
+
z-index: 5;
|
|
140
|
+
transform: translateX(-50%);
|
|
141
|
+
margin: 0;
|
|
142
|
+
padding: 4px 10px;
|
|
143
|
+
border-radius: 999em;
|
|
144
|
+
background: var(--md-sys-color-surface-container-high);
|
|
145
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
146
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
}
|
|
134
150
|
/* A new figure's first question, over the empty canvas. */
|
|
135
151
|
div[start] {
|
|
136
152
|
position: absolute;
|
|
@@ -6,7 +6,7 @@ import { openPopup } from '@operato/layout'
|
|
|
6
6
|
import { OxPrompt } from '@operato/popup'
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
8
8
|
import { live } from 'lit/directives/live.js'
|
|
9
|
-
import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang
|
|
9
|
+
import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang } from '@hatiolab/figure-model'
|
|
10
10
|
import type { RecipeTemplate } from './recipes.js'
|
|
11
11
|
import './figure-recipe-catalog.js'
|
|
12
12
|
import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement, FigureFaceMappingNeed, FigureFaceRemap } from '@hatiolab/figure-model'
|
|
@@ -30,13 +30,13 @@ import './figure-preview.js'
|
|
|
30
30
|
import './figure-settings.js'
|
|
31
31
|
import { DEFAULT_VIEW, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
32
32
|
import './figure-capability-panel.js'
|
|
33
|
-
import { gizmoChanges, gizmoReachOf, sizeKeepOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
33
|
+
import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf, sizeKeepOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
34
34
|
import { keptBySizeOf, partsAtFrame, SIZE_KEEPS_WORDS, startSizeDrag, throughPlaneOf, type SizeDrag, type SizeDragFrame } from './size-drag.js'
|
|
35
35
|
import type { KeyAction } from './keys.js'
|
|
36
36
|
import { ModellerKeys } from './modeller-keys.js'
|
|
37
37
|
import { PaneFold } from './pane-fold.js'
|
|
38
38
|
import { PanelMemory } from './panel-memory.js'
|
|
39
|
-
import { RefusalTeller
|
|
39
|
+
import { RefusalTeller } from './refusal-teller.js'
|
|
40
40
|
import { groupFields, ownerOf, partLabelOf, repeatOf, type FieldLike, type FieldGroup } from './fields.js'
|
|
41
41
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
42
42
|
import { EditorStyles } from './editor-styles.js'
|
|
@@ -57,14 +57,13 @@ import { nudgeOf, placementOf } from './stage-model.js'
|
|
|
57
57
|
|
|
58
58
|
/** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
|
|
59
59
|
type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
|
|
60
|
-
import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js'
|
|
61
60
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
62
61
|
import type { FigureGateFinding } from '../types.js'
|
|
63
|
-
import { editRefusalOf,
|
|
62
|
+
import { editRefusalOf, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
|
|
64
63
|
import type { FigureRenderPart } from './renderer.js'
|
|
65
64
|
import type { FigureBudgetLine } from './maturity.js'
|
|
66
65
|
import { HelpToggles } from './help.js'
|
|
67
|
-
import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf,
|
|
66
|
+
import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
68
67
|
import { jointFanOf } from './joint-fan.js'
|
|
69
68
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
70
69
|
import { jointDragOf } from './joint-drag.js'
|
|
@@ -135,8 +134,6 @@ export class FigureEditor extends LitElement {
|
|
|
135
134
|
* edit was made (`from`).
|
|
136
135
|
*/
|
|
137
136
|
@state() private faceMapping?: { action: FigureAuthoringAction; need: FigureFaceMappingNeed; from: 'section-editor' | 'panel' }
|
|
138
|
-
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
139
|
-
@state() private retry?: Retry
|
|
140
137
|
@state() private tab: SideTab = 'properties'
|
|
141
138
|
/** The view settings or the release history, open over the work from the bar. */
|
|
142
139
|
@state() private overlay?: BarOverlay
|
|
@@ -268,6 +265,14 @@ export class FigureEditor extends LitElement {
|
|
|
268
265
|
selected: () => this.selected,
|
|
269
266
|
act: action => this.keyAction(action)
|
|
270
267
|
})
|
|
268
|
+
/** The part Ctrl+C took: pasted as the kernel's duplicate. Not kept past the editor, nor written anywhere. */
|
|
269
|
+
private copied = ''
|
|
270
|
+
/** A copied part still in the figure is duplicated -- a part of its own, named by figure-model; one gone, nothing. */
|
|
271
|
+
private pasteCopied() {
|
|
272
|
+
const asset = this.session?.revisionNow()?.asset
|
|
273
|
+
if (!this.copied || !asset?.document.model.nodes.some(n => n.op === 'place@1' && n.id === this.copied)) return
|
|
274
|
+
this.duplicatePart(this.copied)
|
|
275
|
+
}
|
|
271
276
|
private keyAction(action: KeyAction) {
|
|
272
277
|
const view = this.session!.read()
|
|
273
278
|
const canvas = this.renderRoot.querySelector<FigureCanvas>('figure-canvas')
|
|
@@ -275,6 +280,9 @@ export class FigureEditor extends LitElement {
|
|
|
275
280
|
else if (action === 'undo') view.canUndo && this.run(() => this.session!.undo())
|
|
276
281
|
else if (action === 'redo') view.canRedo && this.run(() => this.session!.redo())
|
|
277
282
|
else if (action === 'duplicate') this.duplicatePart(this.selected)
|
|
283
|
+
else if (action === 'copy') this.copied = this.selected
|
|
284
|
+
else if (action === 'paste') this.pasteCopied()
|
|
285
|
+
else if (action === 'hold') return
|
|
278
286
|
else if (action === 'remove') this.removePart(this.selected)
|
|
279
287
|
else this.selected = ''
|
|
280
288
|
}
|
|
@@ -283,9 +291,7 @@ export class FigureEditor extends LitElement {
|
|
|
283
291
|
session: () => this.session,
|
|
284
292
|
error: () => this.error,
|
|
285
293
|
notice: () => this.notice,
|
|
286
|
-
|
|
287
|
-
refuse: message => (this.error = message),
|
|
288
|
-
command: action => this.linkCommand(action)
|
|
294
|
+
refuse: message => (this.error = message)
|
|
289
295
|
})
|
|
290
296
|
private panelMemory = new PanelMemory(() => this.renderRoot)
|
|
291
297
|
/** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
|
|
@@ -331,6 +337,14 @@ export class FigureEditor extends LitElement {
|
|
|
331
337
|
if (closed && this.selected === editing) this.selected = next
|
|
332
338
|
})
|
|
333
339
|
}
|
|
340
|
+
/** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
|
|
341
|
+
private renderSameMeasure(unavailable: boolean) {
|
|
342
|
+
if (unavailable || this.gizmoMode !== 'scale' || !this.selected) return nothing
|
|
343
|
+
const axes = sameMeasureAxesOf(this.reachOf(this.selected))
|
|
344
|
+
return axes
|
|
345
|
+
? html`<p same-measure role="status">${i18next.t('figure.text.gizmo-same-measure', { axes: axes.map(a => a.toUpperCase()).join(' · ') })}</p>`
|
|
346
|
+
: nothing
|
|
347
|
+
}
|
|
334
348
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
335
349
|
private useGizmo(mode: 'translate' | 'rotate' | 'scale') {
|
|
336
350
|
/*
|
|
@@ -412,6 +426,9 @@ export class FigureEditor extends LitElement {
|
|
|
412
426
|
}
|
|
413
427
|
|
|
414
428
|
private changed() {
|
|
429
|
+
/* What the step did to the size ends the author typed, said with whatever else it said. */
|
|
430
|
+
const fits = this.session?.takeSizeRangeFits() ?? []
|
|
431
|
+
if (fits.length) this.notice = [this.notice, sizeRangeFitsOf(fits, (key, params) => i18next.t(key, params))].filter(Boolean).join(' ')
|
|
415
432
|
this.requestUpdate()
|
|
416
433
|
this.dispatchEvent(new CustomEvent('changed', { bubbles: true, composed: true }))
|
|
417
434
|
}
|
|
@@ -506,27 +523,16 @@ export class FigureEditor extends LitElement {
|
|
|
506
523
|
private movePart(id: string, to: number) {
|
|
507
524
|
this.run(() => this.session!.edit({ kind: 'move-part', target: id, to }))
|
|
508
525
|
}
|
|
509
|
-
private removePart(id: string
|
|
510
|
-
this.retry = undefined
|
|
526
|
+
private removePart(id: string) {
|
|
511
527
|
try {
|
|
512
|
-
const view = this.session!.edit({ kind: 'remove-part', target: id
|
|
528
|
+
const view = this.session!.edit({ kind: 'remove-part', target: id }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
|
|
513
529
|
this.error = ''
|
|
514
530
|
/* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
|
|
515
531
|
this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? ''
|
|
516
532
|
if (this.selected === id) this.selected = ''
|
|
517
533
|
this.changed()
|
|
518
534
|
} catch (error) {
|
|
519
|
-
if (this.refusals.askClipLoss(error, () => this.removePart(id
|
|
520
|
-
/*
|
|
521
|
-
A part in the declared space goes, and the space is fitted to the parts left, in the same click: every space
|
|
522
|
-
this product declares is fitted to the parts (「부품에 맞추기」, the AI's fit-room -- no padding), so fitting it
|
|
523
|
-
again keeps what it says, and there is nothing to ask (user, 2026-09-27: 「이 확인이 꼭 필요한가 ?」).
|
|
524
|
-
*/
|
|
525
|
-
if (!refitRoom && inDeclaredRoom(error)) {
|
|
526
|
-
this.removePart(id, true)
|
|
527
|
-
if (!this.error) this.notice = [this.notice, i18next.t('figure.text.removal-space-refitted')].filter(Boolean).join(' ')
|
|
528
|
-
return
|
|
529
|
-
}
|
|
535
|
+
if (this.refusals.askClipLoss(error, () => this.removePart(id))) return
|
|
530
536
|
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error)
|
|
531
537
|
}
|
|
532
538
|
}
|
|
@@ -965,10 +971,8 @@ export class FigureEditor extends LitElement {
|
|
|
965
971
|
private linkCommand(action: FigureAuthoringAction, from: 'section-editor' | 'panel' = 'panel'): boolean {
|
|
966
972
|
/* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
|
|
967
973
|
this.holdAction(null)
|
|
968
|
-
this.retry = undefined
|
|
969
974
|
this.notice = ''
|
|
970
975
|
const hadSize = this.hasSize()
|
|
971
|
-
const hadSpace = !!this.shown()?.asset.occupancy
|
|
972
976
|
try {
|
|
973
977
|
this.session!.command(action)
|
|
974
978
|
this.error = ''
|
|
@@ -976,9 +980,7 @@ export class FigureEditor extends LitElement {
|
|
|
976
980
|
/* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
|
|
977
981
|
if (action.kind === 'follow-size' && !hadSize) {
|
|
978
982
|
const size = this.designValues()
|
|
979
|
-
|
|
980
|
-
const spaceToo = !hadSpace && !!this.shown()?.asset.occupancy
|
|
981
|
-
this.notice = i18next.t(spaceToo ? 'figure.text.size-and-space-declared-to-follow' : 'figure.text.size-declared-to-follow', {
|
|
983
|
+
this.notice = i18next.t('figure.text.size-declared-to-follow', {
|
|
982
984
|
x: Math.round(size['size.x'] ?? 0).toLocaleString(),
|
|
983
985
|
y: Math.round(size['size.y'] ?? 0).toLocaleString(),
|
|
984
986
|
z: Math.round(size['size.z'] ?? 0).toLocaleString()
|
|
@@ -996,12 +998,6 @@ export class FigureEditor extends LitElement {
|
|
|
996
998
|
this.error = ''
|
|
997
999
|
return false
|
|
998
1000
|
}
|
|
999
|
-
const narrowing = narrowingRetryOf(action, error, (key, params) => i18next.t(key, params))
|
|
1000
|
-
if (narrowing) {
|
|
1001
|
-
this.error = narrowing.message
|
|
1002
|
-
this.retry = { label: narrowing.label, action: narrowing.retry as FigureAuthoringAction }
|
|
1003
|
-
return false
|
|
1004
|
-
}
|
|
1005
1001
|
this.error =
|
|
1006
1002
|
code === 'DIMENSION_CYCLE'
|
|
1007
1003
|
? i18next.t('figure.text.follow-would-loop')
|
|
@@ -1086,21 +1082,21 @@ export class FigureEditor extends LitElement {
|
|
|
1086
1082
|
label.addEventListener('pointerup', up)
|
|
1087
1083
|
label.addEventListener('pointercancel', up)
|
|
1088
1084
|
}
|
|
1089
|
-
/** The sizes a board may give the figure, and the
|
|
1085
|
+
/** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
|
|
1090
1086
|
private renderInstanceRange(shown: FigureRevision | undefined, unavailable: boolean) {
|
|
1091
1087
|
if (!shown) return nothing
|
|
1092
1088
|
const asset = shown.asset
|
|
1093
|
-
/*
|
|
1089
|
+
/* Computed once per version: figure-model evaluates the figure at its edges. */
|
|
1094
1090
|
const measured = derivedOf(shown, 'instance-range', () => {
|
|
1095
1091
|
let of: ReturnType<typeof instanceRangeOf>
|
|
1096
|
-
let toDeclare: ReturnType<typeof
|
|
1092
|
+
let toDeclare: ReturnType<typeof instanceSizeToDeclare>
|
|
1097
1093
|
try {
|
|
1098
1094
|
of = instanceRangeOf(asset)
|
|
1099
1095
|
} catch {
|
|
1100
1096
|
of = undefined
|
|
1101
1097
|
}
|
|
1102
1098
|
try {
|
|
1103
|
-
toDeclare = of ? undefined :
|
|
1099
|
+
toDeclare = of ? undefined : instanceSizeToDeclare(asset)
|
|
1104
1100
|
} catch {
|
|
1105
1101
|
toDeclare = undefined
|
|
1106
1102
|
}
|
|
@@ -1121,13 +1117,17 @@ export class FigureEditor extends LitElement {
|
|
|
1121
1117
|
return html`<section board-size>
|
|
1122
1118
|
${title('figure.text.board-size-fixed-explained')}
|
|
1123
1119
|
<p fixed-size>${i18next.t('figure.text.board-size-fixed', { x: mm(toDeclare.at.x), y: mm(toDeclare.at.y), z: mm(toDeclare.at.z) })}</p>
|
|
1124
|
-
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size',
|
|
1120
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', at: toDeclare.at })}>
|
|
1125
1121
|
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
|
|
1126
1122
|
</button>
|
|
1127
1123
|
</section>`
|
|
1128
1124
|
if (!range) return nothing
|
|
1129
|
-
const differs = rangeDiffers(range.declared, range.measured)
|
|
1130
1125
|
const followers = sizeFollowersOf(asset)
|
|
1126
|
+
/*
|
|
1127
|
+
An end the author typed shows its number; an open end is empty, with what the parts allow written faintly in it
|
|
1128
|
+
(ADR-0087 decision 6). Typing narrows that end, emptying the field opens it again.
|
|
1129
|
+
*/
|
|
1130
|
+
const shownEnd = (r: (typeof range)['x'], bound: 'min' | 'max') => (bound === 'max' && !Number.isFinite(r.max) ? '∞' : mm(r[bound]))
|
|
1131
1131
|
return html`<section board-size>
|
|
1132
1132
|
${title('figure.text.board-size-range-explained')}
|
|
1133
1133
|
${(['x', 'y', 'z'] as const).map(
|
|
@@ -1139,11 +1139,12 @@ export class FigureEditor extends LitElement {
|
|
|
1139
1139
|
aria-label=${`size.${axis}.${bound}`}
|
|
1140
1140
|
type="number"
|
|
1141
1141
|
step="any"
|
|
1142
|
-
.value=${live(String(Math.round(range
|
|
1142
|
+
.value=${live(range[axis].narrowed[bound] ? String(Math.round(range[axis][bound])) : '')}
|
|
1143
|
+
placeholder=${shownEnd(range[axis], bound)}
|
|
1143
1144
|
?disabled=${unavailable}
|
|
1144
1145
|
@change=${(e: Event) => {
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1146
|
+
const typed = (e.target as HTMLInputElement).valueAsNumber
|
|
1147
|
+
this.linkCommand({ kind: 'declare-instance-size', range: rangeWith(range, axis, bound, typed), replace: true })
|
|
1147
1148
|
}}
|
|
1148
1149
|
/>`
|
|
1149
1150
|
)}
|
|
@@ -1157,19 +1158,6 @@ export class FigureEditor extends LitElement {
|
|
|
1157
1158
|
})
|
|
1158
1159
|
: i18next.t('figure.text.board-size-no-followers')}
|
|
1159
1160
|
</p>
|
|
1160
|
-
${differs
|
|
1161
|
-
? html`<p hint>
|
|
1162
|
-
${i18next.t('figure.text.instance-range-can-widen', {
|
|
1163
|
-
ranges: (['x', 'y', 'z'] as const)
|
|
1164
|
-
.filter(axis => rangeDiffers(range.declared, { ...range.declared, [axis]: range.measured[axis] }))
|
|
1165
|
-
.map(axis => `${axis.toUpperCase()} ${mm(range.measured[axis].min)} – ${mm(range.measured[axis].max)}`)
|
|
1166
|
-
.join(' · ')
|
|
1167
|
-
})}
|
|
1168
|
-
</p>
|
|
1169
|
-
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })}>
|
|
1170
|
-
<md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
|
|
1171
|
-
</button>`
|
|
1172
|
-
: nothing}
|
|
1173
1161
|
${this.renderSizingRule(asset, unavailable)}
|
|
1174
1162
|
</section>`
|
|
1175
1163
|
}
|
|
@@ -1325,7 +1313,7 @@ export class FigureEditor extends LitElement {
|
|
|
1325
1313
|
this.publishing?.part === part
|
|
1326
1314
|
? this.publishing
|
|
1327
1315
|
: listed && input
|
|
1328
|
-
? { part, label: listed.label, min: input.min
|
|
1316
|
+
? /* A published input has both ends (figure-model checkPublicInput). */ { part, label: listed.label, min: input.min!, max: input.max! }
|
|
1329
1317
|
: undefined
|
|
1330
1318
|
const open = !!form
|
|
1331
1319
|
const edit = (key: 'label' | 'min' | 'max') => (e: Event) => {
|
|
@@ -1417,19 +1405,13 @@ export class FigureEditor extends LitElement {
|
|
|
1417
1405
|
if (state === 'released') return i18next.t('figure.text.release-state-released', { version: String(version) })
|
|
1418
1406
|
return version > 0 ? i18next.t('figure.text.release-state-draft-over', { version: String(version) }) : i18next.t('figure.text.release-state-draft-new')
|
|
1419
1407
|
}
|
|
1420
|
-
/** The findings that block release, under the bar beside
|
|
1421
|
-
private renderReleaseBlockers(blockers: FigureGateFinding[], asset: FigureAsset | undefined
|
|
1408
|
+
/** The findings that block release, under the bar beside 「발행」. */
|
|
1409
|
+
private renderReleaseBlockers(blockers: FigureGateFinding[], asset: FigureAsset | undefined) {
|
|
1422
1410
|
const parts = (asset?.appearance ?? []).map(look => look.target)
|
|
1423
|
-
const way = (code: string) =>
|
|
1424
|
-
(code === 'occupancy-undeclared' || code === 'part-outside-occupancy') && asset && parts.length
|
|
1425
|
-
? html`<button act ?disabled=${unavailable} @click=${() => this.linkCommand(occupancyFromParts(asset))}>
|
|
1426
|
-
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
1427
|
-
</button>`
|
|
1428
|
-
: nothing
|
|
1429
1411
|
return html`<div release-blockers role="dialog" aria-label=${i18next.t('figure.text.must-fix-to-release')} @keydown=${(e: KeyboardEvent) => e.key === 'Escape' && (this.blockersOpen = false)}>
|
|
1430
1412
|
<ul gate>
|
|
1431
1413
|
<li head>${i18next.t('figure.text.must-fix-to-release')}</li>
|
|
1432
|
-
${blockers.map(finding => renderGateFinding(finding, true,
|
|
1414
|
+
${blockers.map(finding => renderGateFinding(finding, true, nothing, parts))}
|
|
1433
1415
|
</ul>
|
|
1434
1416
|
</div>`
|
|
1435
1417
|
}
|
|
@@ -1758,19 +1740,9 @@ export class FigureEditor extends LitElement {
|
|
|
1758
1740
|
/* A repeat's own row values (its pitch) have their own section, not the shape's. */
|
|
1759
1741
|
const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.') && !i.id.startsWith(this.selected + '.repeat.'))
|
|
1760
1742
|
const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(ownerOf(g.id) + '.')))
|
|
1761
|
-
/*
|
|
1762
|
-
A part's field only the declared room still reads -- its old height position once the floor holds it -- moves
|
|
1763
|
-
nothing; it is shown apart as what it is, not as a position (ADR-0097, designer 2026-09-27).
|
|
1764
|
-
*/
|
|
1765
1743
|
const shown = this.shown()
|
|
1766
1744
|
const asset = shown?.asset
|
|
1767
|
-
|
|
1768
|
-
try {
|
|
1769
|
-
roomOnly = this.selected && asset ? figureRoomOnlyInputsOf(asset, this.selected) : []
|
|
1770
|
-
} catch {
|
|
1771
|
-
roomOnly = []
|
|
1772
|
-
}
|
|
1773
|
-
const partFields = own.filter(i => !roomOnly.includes(i.id))
|
|
1745
|
+
const partFields = own
|
|
1774
1746
|
/* The numbers a rule gives the axes that have no field of their own -- shown read-only, never as a blank. */
|
|
1775
1747
|
let held: HeldValues | undefined
|
|
1776
1748
|
try {
|
|
@@ -1787,7 +1759,6 @@ export class FigureEditor extends LitElement {
|
|
|
1787
1759
|
} catch {
|
|
1788
1760
|
held = undefined
|
|
1789
1761
|
}
|
|
1790
|
-
const roomFields = own.filter(i => roomOnly.includes(i.id))
|
|
1791
1762
|
/*
|
|
1792
1763
|
Only what is under way is said. Saved or not is the save button's: filled and pressable when there is something
|
|
1793
1764
|
to save, dimmed when not -- the saved and unsaved words beside it said the same thing again in the primary
|
|
@@ -1840,7 +1811,7 @@ export class FigureEditor extends LitElement {
|
|
|
1840
1811
|
</button>
|
|
1841
1812
|
<button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}><md-icon>save</md-icon>${i18next.t('figure.button.save')}</button>
|
|
1842
1813
|
</ox-canvas-bar>
|
|
1843
|
-
${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset
|
|
1814
|
+
${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset) : nothing}
|
|
1844
1815
|
${this.catalogOpen
|
|
1845
1816
|
? html`<figure-recipe-catalog
|
|
1846
1817
|
.templates=${this.templates}
|
|
@@ -1921,7 +1892,7 @@ export class FigureEditor extends LitElement {
|
|
|
1921
1892
|
@render-error=${(e: CustomEvent<Error>) => {
|
|
1922
1893
|
this.error = e.detail.message
|
|
1923
1894
|
}}
|
|
1924
|
-
></figure-canvas
|
|
1895
|
+
></figure-canvas>${this.renderSameMeasure(unavailable)}</div>
|
|
1925
1896
|
<aside slot="end">
|
|
1926
1897
|
<div tabs>
|
|
1927
1898
|
${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
|
|
@@ -1950,7 +1921,6 @@ export class FigureEditor extends LitElement {
|
|
|
1950
1921
|
${this.renderRepeatNote(view.geometry)}
|
|
1951
1922
|
</section>
|
|
1952
1923
|
${this.renderPartGroups(shown, view.geometry, partFields, unavailable, held)}
|
|
1953
|
-
${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
|
|
1954
1924
|
<figure-material-panel
|
|
1955
1925
|
.body=${{ ...part, id: this.selected }}
|
|
1956
1926
|
.palette=${view.palette ?? {}}
|
|
@@ -2096,60 +2066,6 @@ export class FigureEditor extends LitElement {
|
|
|
2096
2066
|
if (fastened && placed) return html`<p hint held-axes>${i18next.t('figure.text.held-by-fastening')}</p>`
|
|
2097
2067
|
return html`<p hint held-axes>${i18next.t('figure.text.held-worked-out', { group: held.map(g => i18next.t('figure.label.group-' + g.name)).join(' · ') })}</p>`
|
|
2098
2068
|
}
|
|
2099
|
-
/**
|
|
2100
|
-
* A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
|
|
2101
|
-
* typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.
|
|
2102
|
-
*/
|
|
2103
|
-
private renderRoomOnly(fields: FieldLike[], unavailable: boolean) {
|
|
2104
|
-
return html`<section room-only>
|
|
2105
|
-
<h3>${i18next.t('figure.label.room-only-inputs')}</h3>
|
|
2106
|
-
<p hint>${i18next.t('figure.text.room-only-inputs')}</p>
|
|
2107
|
-
<div form>
|
|
2108
|
-
${fields.map(
|
|
2109
|
-
f => html`<span>${fieldWordsOf(f.id, key => i18next.t(key))} (${f.unit})</span><input aria-label=${f.id} type="number" .value=${String(f.value)} disabled />`
|
|
2110
|
-
)}
|
|
2111
|
-
</div>
|
|
2112
|
-
<button act room-refit ?disabled=${unavailable} @click=${() => this.askRoomRefit()}><md-icon>fit_screen</md-icon>${i18next.t('figure.button.room-refit')}</button>
|
|
2113
|
-
</section>`
|
|
2114
|
-
}
|
|
2115
|
-
/** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
|
|
2116
|
-
private async askRoomRefit() {
|
|
2117
|
-
let refit
|
|
2118
|
-
try {
|
|
2119
|
-
refit = figureRoomRefitOf(this.shown()!.asset)
|
|
2120
|
-
} catch (error) {
|
|
2121
|
-
this.error = this.said(error)
|
|
2122
|
-
return
|
|
2123
|
-
}
|
|
2124
|
-
if (!refit) return
|
|
2125
|
-
const rows = roomRefitRowsOf(refit)
|
|
2126
|
-
const outside = (list: typeof refit.outside.declared) =>
|
|
2127
|
-
list.length
|
|
2128
|
-
? list.map(o => i18next.t('figure.text.room-part-outside', { part: partLabelOf(o.part), axis: o.axis.toUpperCase(), mm: String(o.mm) })).join(' · ')
|
|
2129
|
-
: i18next.t('figure.text.room-nothing-outside')
|
|
2130
|
-
const agreed = await OxPrompt.open({
|
|
2131
|
-
type: 'question',
|
|
2132
|
-
title: i18next.t('figure.label.room-refit'),
|
|
2133
|
-
template: html`<table room-refit>
|
|
2134
|
-
<tr><th></th><th>${i18next.t('figure.label.room-declared')}</th><th>${i18next.t('figure.label.room-proposed')}</th></tr>
|
|
2135
|
-
${rows.map(
|
|
2136
|
-
r => html`<tr>
|
|
2137
|
-
<td>${i18next.t(`figure.label.room-at-${r.at}`)} ${r.axis.toUpperCase()}</td>
|
|
2138
|
-
<td>${r.declared}</td>
|
|
2139
|
-
<td>${r.same ? i18next.t('figure.text.room-same') : r.proposed}</td>
|
|
2140
|
-
</tr>`
|
|
2141
|
-
)}
|
|
2142
|
-
</table>
|
|
2143
|
-
<p>${i18next.t('figure.label.room-declared')}: ${outside(refit.outside.declared)}</p>
|
|
2144
|
-
<p>${i18next.t('figure.label.room-proposed')}: ${outside(refit.outside.proposed)}</p>
|
|
2145
|
-
${refit.drops.length
|
|
2146
|
-
? html`<p>${i18next.t('figure.text.room-drops', { fields: [...new Set(refit.drops.map(id => `${partLabelOf(id.split('.')[0]!)} ${fieldWordsOf(id, key => i18next.t(key))}`))].join(' · ') })}</p>`
|
|
2147
|
-
: nothing}`,
|
|
2148
|
-
confirmButton: { text: i18next.t('figure.button.room-refit-confirm') },
|
|
2149
|
-
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
2150
|
-
})
|
|
2151
|
-
if (agreed) this.linkCommand(refit.step)
|
|
2152
|
-
}
|
|
2153
2069
|
private renderSingle(name: string, field: FieldLike, unavailable: boolean) {
|
|
2154
2070
|
const words = name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }))
|
|
2155
2071
|
return html`<span ?scrub=${!unavailable} @pointerdown=${(e: PointerEvent) => this.scrub(field as never, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FIGURE_FACES, figureFastenTargetAt, figurePlaneFacingOf, figureSurfaceKeptPlaceOf } from '@hatiolab/figure-model'
|
|
1
|
+
import { FIGURE_FACES, figureFastenKeptGapOf, figureFastenTargetAt, figurePlaneFacingOf, figureSurfaceKeptPlaceOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureAuthoringAction, FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
import { faceViewOf, type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js'
|
|
@@ -38,6 +38,12 @@ export interface FastenForm {
|
|
|
38
38
|
gap: number
|
|
39
39
|
/** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */
|
|
40
40
|
gapKept: unknown
|
|
41
|
+
/**
|
|
42
|
+
* Where the gap came from (ADR-0140 stage 4, 「현재 상태로 붙이기」): `kept` -- the distance the part stands at, worked
|
|
43
|
+
* out again whenever where or how it is fastened changes, so fastening does not move it; `typed` -- the author's
|
|
44
|
+
* number, left as it is; `pressed` -- 0, the part put where the canvas was pressed.
|
|
45
|
+
*/
|
|
46
|
+
gapFrom: 'kept' | 'typed' | 'pressed'
|
|
41
47
|
align: Partial<Record<Axis, Alignment>>
|
|
42
48
|
alignMm: Partial<Record<Axis, number>>
|
|
43
49
|
alignInput: Partial<Record<Axis, string>>
|
|
@@ -52,6 +58,7 @@ export const freshFasten = (): FastenForm => ({
|
|
|
52
58
|
facing: 'meet',
|
|
53
59
|
gap: 0,
|
|
54
60
|
gapKept: undefined,
|
|
61
|
+
gapFrom: 'kept',
|
|
55
62
|
align: {},
|
|
56
63
|
alignMm: {},
|
|
57
64
|
alignInput: {}
|
|
@@ -60,7 +67,9 @@ export const freshFasten = (): FastenForm => ({
|
|
|
60
67
|
/** The part's own fastening as a form, or a fresh one: a window opens on what the part has (walk 1, 2026-09-26). */
|
|
61
68
|
export function fastenFormOf(revision: FigureRevision, part: string): FastenForm {
|
|
62
69
|
const own = fasteningOf(revision, part)
|
|
63
|
-
|
|
70
|
+
if (!own || own.atOrigin) return freshFasten()
|
|
71
|
+
/* A gap the record works out is the author's; a number is where the part stands, so aiming again keeps it there. */
|
|
72
|
+
return { ...freshFasten(), ...own, gapFrom: own.gapKept !== undefined ? 'typed' : 'kept' } as FastenForm
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */
|
|
@@ -133,11 +142,11 @@ export function fastenChanged(
|
|
|
133
142
|
change: FastenChange
|
|
134
143
|
): FastenForm {
|
|
135
144
|
const f = form
|
|
136
|
-
if ('targetFace' in change) return aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace })
|
|
137
|
-
if ('onSurface' in change) return aimFasten(revision, part, f, { surface: true })
|
|
138
|
-
if ('surface' in change) return { ...f, surface: change.surface }
|
|
139
|
-
if ('mode' in change) return { ...f, facing: change.mode }
|
|
140
|
-
if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined }
|
|
145
|
+
if ('targetFace' in change) return keptGapOf(revision, part, aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace }))
|
|
146
|
+
if ('onSurface' in change) return keptGapOf(revision, part, aimFasten(revision, part, f, { surface: true }))
|
|
147
|
+
if ('surface' in change) return keptGapOf(revision, part, { ...f, surface: change.surface })
|
|
148
|
+
if ('mode' in change) return keptGapOf(revision, part, { ...f, facing: change.mode })
|
|
149
|
+
if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined, gapFrom: 'typed' }
|
|
141
150
|
return {
|
|
142
151
|
...f,
|
|
143
152
|
align: { ...f.align, [change.axis]: change.how },
|
|
@@ -145,6 +154,29 @@ export function fastenChanged(
|
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The form with the gap that keeps the part where it stands (ADR-0140 stage 4, user 2026-10-20 「현재 상태로 붙이기」):
|
|
159
|
+
* figure-model's `figureFastenKeptGapOf` for what it is aimed at and how it meets it. A gap the author typed, or the 0 a
|
|
160
|
+
* canvas press puts, is left as it is. Nothing aimed at yet, the form is as it was.
|
|
161
|
+
*/
|
|
162
|
+
export function keptGapOf(revision: FigureRevision, part: string, form: FastenForm): FastenForm {
|
|
163
|
+
if (form.gapFrom !== 'kept') return form
|
|
164
|
+
const asset = revision.asset
|
|
165
|
+
const theirFaces = form.target ? facesOf(asset, form.target) : FACES
|
|
166
|
+
const targetFace = (theirFaces.includes(form.targetFace) ? form.targetFace : theirFaces[0]) as FigureFace
|
|
167
|
+
const planeWay = figurePlaneFacingOf(asset)
|
|
168
|
+
const mode: FastenMode = form.toPlane ? (mountingPlaneOf(asset).facings.includes(form.facing) ? form.facing : (planeWay ?? 'meet')) : form.facing
|
|
169
|
+
const to = form.toPlane
|
|
170
|
+
? ('plane' as const)
|
|
171
|
+
: form.target && form.surface && surfaceOf(asset, form.target)
|
|
172
|
+
? { part: form.target, surface: form.surface }
|
|
173
|
+
: form.target
|
|
174
|
+
? { part: form.target, face: targetFace }
|
|
175
|
+
: undefined
|
|
176
|
+
if (!to) return form
|
|
177
|
+
return { ...form, gap: figureFastenKeptGapOf(asset, part, to, mode, { ...revision.inputs }), gapKept: undefined }
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
/** What a form comes to: the face aimed at, the way, the drawing of the face and the command. */
|
|
149
181
|
export interface FastenPlan {
|
|
150
182
|
/** Nothing is aimed at until a part or the plane is chosen. */
|
|
@@ -226,12 +258,16 @@ export function fastenSentenceOf(
|
|
|
226
258
|
return say(`figure.text.fasten-sentence-${plan.mode}`, { to })
|
|
227
259
|
}
|
|
228
260
|
|
|
229
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* What the folded details hold that is not their default, said while folded; nothing for the default. The gap's default
|
|
263
|
+
* is the one it starts at -- the distance the part stands at, or 0 where the canvas was pressed -- so only a number typed
|
|
264
|
+
* is said.
|
|
265
|
+
*/
|
|
230
266
|
export function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {
|
|
231
267
|
return [
|
|
232
268
|
...(form.gapKept !== undefined
|
|
233
269
|
? [say('figure.text.fasten-summary-gap-worked-out')]
|
|
234
|
-
: form.gap
|
|
270
|
+
: form.gapFrom === 'typed' && form.gap
|
|
235
271
|
? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]
|
|
236
272
|
: [])
|
|
237
273
|
]
|
|
@@ -244,6 +280,9 @@ export function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {
|
|
|
244
280
|
* nothing to fasten to is not aimed at; a press on a curve there is no fastening to aims at the part alone. Each says
|
|
245
281
|
* why, by its words' key.
|
|
246
282
|
*/
|
|
283
|
+
/* Put where the canvas was pressed: on the face or the surface there, with no gap (ADR-0140 stage 4). */
|
|
284
|
+
const PRESSED = { gap: 0, gapKept: undefined, gapFrom: 'pressed' as const }
|
|
285
|
+
|
|
247
286
|
export function aimFastenAt(
|
|
248
287
|
revision: FigureRevision,
|
|
249
288
|
part: string,
|
|
@@ -265,8 +304,8 @@ export function aimFastenAt(
|
|
|
265
304
|
/* On the face where it was pressed, as a point of a curved surface is: its middle there. */
|
|
266
305
|
const onFace = aimFasten(revision, part, { ...aimed, surface: undefined }, { targetFace: lands.face as FigureFace })
|
|
267
306
|
const at = lands.at as Partial<Record<Axis, number>>
|
|
268
|
-
return { form: { ...onFace, align: Object.fromEntries(Object.keys(at).map(a => [a, 'mm'])) as FastenForm['align'], alignMm: { ...at }, alignInput: {} }, part: target }
|
|
307
|
+
return { form: { ...onFace, ...PRESSED, align: Object.fromEntries(Object.keys(at).map(a => [a, 'mm'])) as FastenForm['align'], alignMm: { ...at }, alignInput: {} }, part: target }
|
|
269
308
|
}
|
|
270
|
-
if ('surface' in lands) return { form: { ...aimed, toPlane: false, target, surface: lands.surface, align: {}, alignMm: {}, alignInput: {} }, part: target }
|
|
309
|
+
if ('surface' in lands) return { form: { ...aimed, ...PRESSED, toPlane: false, target, surface: lands.surface, align: {}, alignMm: {}, alignInput: {} }, part: target }
|
|
271
310
|
return { form: aimed, note: 'aim-not-here', part: target }
|
|
272
311
|
}
|
|
@@ -27,7 +27,7 @@ export function figureFigureFacts(revision: FigureRevision): FigureFigureFacts {
|
|
|
27
27
|
}
|
|
28
28
|
function figureFactsOf(revision: FigureRevision): FigureFigureFacts {
|
|
29
29
|
const asset = revision.asset as FigureAsset
|
|
30
|
-
const feature =
|
|
30
|
+
const feature = asset.mountingFace?.surface.feature
|
|
31
31
|
const found = feature?.match(/^(.+)\.mount\.(left|right|bottom|top|back|front)\.value$/)
|
|
32
32
|
return {
|
|
33
33
|
name: asset.name,
|
|
@@ -16,6 +16,7 @@ import { turnsInPlace, type FastenMode } from './fasten-geometry.js'
|
|
|
16
16
|
import {
|
|
17
17
|
aimFasten,
|
|
18
18
|
aimFastenAt,
|
|
19
|
+
keptGapOf,
|
|
19
20
|
fastenChanged,
|
|
20
21
|
fastenDetailWordsOf,
|
|
21
22
|
fastenFormOf,
|
|
@@ -260,7 +261,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
260
261
|
step="any"
|
|
261
262
|
.value=${f.gapKept !== undefined ? '' : String(f.gap)}
|
|
262
263
|
placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
|
|
263
|
-
@change=${(e: Event) => this.set({ gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined })}
|
|
264
|
+
@change=${(e: Event) => this.set({ gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined, gapFrom: 'typed' })}
|
|
264
265
|
/>
|
|
265
266
|
</div>
|
|
266
267
|
</details>
|
|
@@ -330,7 +331,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
330
331
|
this.aimNote = ''
|
|
331
332
|
const chosen = this.form.toPlane ? PLANE : this.form.target
|
|
332
333
|
if (part === chosen) return
|
|
333
|
-
this.form = aimFasten(this.revision, this.partId, this.form, part === PLANE ? { plane: true } : { part })
|
|
334
|
+
this.form = keptGapOf(this.revision, this.partId, aimFasten(this.revision, this.partId, this.form, part === PLANE ? { plane: true } : { part }))
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
/**
|
|
@@ -401,35 +401,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
401
401
|
* 준다 — 화면이 문장을 지으면 서버가 거절할 때와 다른 말을 하게 된다.
|
|
402
402
|
*/
|
|
403
403
|
private renderFinding(finding: FigureGateFinding, blocking: boolean) {
|
|
404
|
-
return renderGateFinding(finding, blocking,
|
|
405
|
-
${this.offersFit(finding.code)
|
|
406
|
-
? html`<button fit-base @click=${() => this.fitBase()}>
|
|
407
|
-
${i18next.t('figure.button.fit-base-to-parts')}
|
|
408
|
-
</button>`
|
|
409
|
-
: nothing}
|
|
410
|
-
`)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* 넘친 것을 담는 길 하나를 여기서도 내민다 — **같은 동작이 속성 패널에도 있다**(「부품에 맞추기」).
|
|
415
|
-
*
|
|
416
|
-
* 그 단추를 아는 저작자는 거기서 누른다. 발행이 막혔다는 말을 여기서 읽은 사람은 그 단추가 있는
|
|
417
|
-
* 것을 모르고, 문구는 그것을 가리킬 수 없다 — 문장은 형식이 쓰고 형식은 화면의 단추를 모른다.
|
|
418
|
-
* 그래서 화면이 자기 자리에서 같은 동작을 낸다. 누르면 상자만 커지고 부품은 제자리이며, 저장은
|
|
419
|
-
* 여전히 저작자가 따로 한다.
|
|
420
|
-
*/
|
|
421
|
-
/**
|
|
422
|
-
* 「부품에 맞추기」를 내미는 판정: 공간을 부품에서 다시 선언한다(`declare-occupancy`) — 부품은 그대로 두고 선언만
|
|
423
|
-
* 바꾼다. 공간을 아직 선언하지 않은 도형도 같은 단추로 선언한다: 새로 만든 도형은 전부 그 판정으로 막히고, 화면에
|
|
424
|
-
* 다른 길이 없다. 장착면이 떠 있는 것은 여기 없다 — 부품 하나를 평면에 붙여도 거기 붙지 않은 것은 따라오지 않는다.
|
|
425
|
-
* 판정의 「어떻게」가 할 일을 말한다.
|
|
426
|
-
*/
|
|
427
|
-
private offersFit(code: string): boolean {
|
|
428
|
-
return code === 'part-outside-occupancy' || code === 'occupancy-undeclared'
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private fitBase() {
|
|
432
|
-
this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }))
|
|
404
|
+
return renderGateFinding(finding, blocking, nothing)
|
|
433
405
|
}
|
|
434
406
|
|
|
435
407
|
/** Where one released version downloads as a package file, or undefined before the figure has a full name. */
|
|
@@ -278,7 +278,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
278
278
|
<section size-response>
|
|
279
279
|
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
280
280
|
<p group-question>${i18next.t('figure.label.resize-intent')}</p>
|
|
281
|
-
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.
|
|
281
|
+
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.part-cannot-follow')}</p>`}
|
|
282
282
|
</section>
|
|
283
283
|
${positions.length
|
|
284
284
|
? html`<section position-response>
|
|
@@ -302,7 +302,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
302
302
|
${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
|
|
303
303
|
</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
304
304
|
<slot name="place"></slot>
|
|
305
|
-
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.
|
|
305
|
+
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.part-cannot-fasten')}</p>`}
|
|
306
306
|
</section>
|
|
307
307
|
`
|
|
308
308
|
this.candidate = this.fastenCandidate
|