@things-factory/figure-ui 10.1.82 → 10.1.84
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/command-preview.ts +4 -2
- package/client/modeller/drawn-shapes.ts +29 -0
- package/client/modeller/editor.ts +11 -29
- package/client/modeller/figure-links.ts +47 -101
- package/client/modeller/figure-material-panel.ts +2 -3
- package/client/modeller/figure-parts.ts +9 -11
- package/client/modeller/figure-section-panel.ts +175 -0
- package/client/modeller/gizmo.ts +26 -16
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +24 -24
- package/client/modeller/maturity.ts +12 -6
- package/client/modeller/motion-ends.ts +2 -1
- package/client/modeller/refusals.ts +20 -0
- package/client/modeller/renderer.ts +3 -2
- package/client/modeller/section-controls.ts +34 -0
- package/client/modeller/starting-shape.ts +34 -0
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview.js +4 -2
- package/dist-client/modeller/command-preview.js.map +1 -1
- package/dist-client/modeller/drawn-shapes.d.ts +44 -0
- package/dist-client/modeller/drawn-shapes.js +12 -0
- package/dist-client/modeller/drawn-shapes.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +1 -6
- package/dist-client/modeller/editor.js +10 -29
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +2 -2
- package/dist-client/modeller/figure-links.js +45 -96
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.js +2 -3
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +3 -2
- package/dist-client/modeller/figure-parts.js +6 -9
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.d.ts +28 -0
- package/dist-client/modeller/figure-section-panel.js +172 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -0
- package/dist-client/modeller/gizmo.d.ts +3 -3
- package/dist-client/modeller/gizmo.js +24 -11
- package/dist-client/modeller/gizmo.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 +4 -1
- package/dist-client/modeller/links.js +23 -25
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/maturity.d.ts +3 -2
- package/dist-client/modeller/maturity.js +9 -6
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/motion-ends.js +2 -1
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +0 -5
- package/dist-client/modeller/refusals.js +25 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/renderer.d.ts +3 -4
- package/dist-client/modeller/renderer.js +3 -2
- package/dist-client/modeller/renderer.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +11 -0
- package/dist-client/modeller/section-controls.js +27 -0
- package/dist-client/modeller/section-controls.js.map +1 -0
- package/dist-client/modeller/starting-shape.d.ts +32 -0
- package/dist-client/modeller/starting-shape.js +37 -0
- package/dist-client/modeller/starting-shape.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/acceptance-page.js +8 -0
- package/test/command-preview.test.ts +3 -2
- package/test/fixtures/joint-column.json +110 -15
- package/test/fixtures/joint-roller.json +66 -9
- package/test/fixtures/robot-arm-6dof.json +203 -27
- package/test/joint-fan.test.ts +10 -7
- package/test/links.test.ts +6 -5
- package/test/material.test.ts +7 -2
- package/test/renderer.test.ts +2 -1
- package/test/resize-with-placement.test.ts +15 -14
- package/test/scene-canvas.test.ts +2 -1
- package/test/section-panel.test.ts +93 -0
- package/translations/en.json +21 -2
- package/translations/ja.json +20 -1
- package/translations/ko.json +22 -3
- package/translations/ms.json +20 -1
- package/translations/zh.json +20 -1
- package/dist-client/tsconfig.localkernel.tsbuildinfo +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FigureBodyMove, FigureClipLoss, FigureFaceArea, FigureRowReading } from '@hatiolab/figure-model'
|
|
2
2
|
|
|
3
3
|
import type { FigureRenderPart } from './renderer.js'
|
|
4
|
+
import { boxShape, discShape } from './drawn-shapes.js'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
* A command seen before it is done, as the modeller shows it (ADR-0097 §5.5, designer 2026-09-27): the bodies it would
|
|
@@ -53,9 +54,10 @@ export function faceHighlightOf(face: FigureFaceArea | undefined): FigureRenderP
|
|
|
53
54
|
/* Its own x · y · z are the face's two sides and its normal, as the kernel gives them. */
|
|
54
55
|
const r = [0, 1, 2].map(i => [face.u[i]!, face.normal[i]!, face.v[i]!])
|
|
55
56
|
const t = [0, 1, 2].map(i => face.centre[i]! + face.normal[i]! * (LIFT + THICKNESS / 2))
|
|
57
|
+
/* A plane touching a curved side is a line (Q3): drawn as a narrow strip, never a plate. */
|
|
56
58
|
const shape: FigureRenderPart['shape'] = face.round
|
|
57
|
-
?
|
|
58
|
-
:
|
|
59
|
+
? discShape(face.size.u / 2, THICKNESS)
|
|
60
|
+
: boxShape(Math.max(face.size.u, THICKNESS), THICKNESS, Math.max(face.size.v, THICKNESS))
|
|
59
61
|
/* Solid: a translucent plate over a light face was hardly seen (2026-09-27, :3391); it covers the face it names. */
|
|
60
62
|
return { id: TARGET_FACE, shape, pose: { r, t }, material: { token: TARGET_FACE } }
|
|
61
63
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes the modeller draws of its own -- a lit face, a joint's fan, a slide's line -- written as the shapes figure-model
|
|
3
|
+
* evaluates (ADR-0100): a section pushed along Y, centred. things-scene's one builder draws them as it draws a part.
|
|
4
|
+
*/
|
|
5
|
+
export interface DrawnShape {
|
|
6
|
+
provider: 'extrude'
|
|
7
|
+
dimensions: Record<string, number>
|
|
8
|
+
extrusion: { axis: 'y'; section: DrawnSection; start: number; end: number }
|
|
9
|
+
}
|
|
10
|
+
type DrawnSection =
|
|
11
|
+
| { kind: 'rectangle'; size: { x: number; z: number } }
|
|
12
|
+
| { kind: 'ellipse'; radius: { x: number; z: number } }
|
|
13
|
+
| { kind: 'polygon'; points: Array<{ id: string; x: number; z: number }> }
|
|
14
|
+
|
|
15
|
+
const centred = (section: DrawnSection, height: number): DrawnShape => ({
|
|
16
|
+
provider: 'extrude',
|
|
17
|
+
dimensions: { length: height },
|
|
18
|
+
extrusion: { axis: 'y', section, start: -height / 2, end: height / 2 }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/** A box: width along X, height along Y, depth along Z. */
|
|
22
|
+
export const boxShape = (width: number, height: number, depth: number): DrawnShape => centred({ kind: 'rectangle', size: { x: width, z: depth } }, height)
|
|
23
|
+
|
|
24
|
+
/** A disc of a radius, `height` thick along Y. */
|
|
25
|
+
export const discShape = (radius: number, height: number): DrawnShape => centred({ kind: 'ellipse', radius: { x: radius, z: radius } }, height)
|
|
26
|
+
|
|
27
|
+
/** A section in X-Z, `height` thick along Y. */
|
|
28
|
+
export const sectionShape = (points: ReadonlyArray<{ x: number; z: number }>, height: number): DrawnShape =>
|
|
29
|
+
centred({ kind: 'polygon', points: points.map((p, i) => ({ id: `p${i + 1}`, x: p.x, z: p.z })) }, height)
|
|
@@ -6,16 +6,17 @@ import { notify } from '@operato/layout'
|
|
|
6
6
|
import { OxPrompt } from '@operato/popup'
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
8
8
|
import { live } from 'lit/directives/live.js'
|
|
9
|
-
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor,
|
|
9
|
+
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, parseFigureAsset, figureSizeEditChanges, figureScaleAnchorOf, figurePlaneAlignChanges, type FigurePlaneContact, type FigureClipLoss, figurePartFaceOf, type FigureFaceArea, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model'
|
|
10
10
|
import type { RecipeTemplate } from './recipes.js'
|
|
11
11
|
import './figure-recipe-catalog.js'
|
|
12
|
-
import type { FigureAsset, FigureAuthoringAction, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue,
|
|
12
|
+
import type { FigureAsset, FigureAuthoringAction, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement } from '@hatiolab/figure-model'
|
|
13
13
|
import type { FigureProductSession } from './product-session.js'
|
|
14
14
|
import { captureFigureThumbnail } from './thumbnail.js'
|
|
15
15
|
import { drawableFigureParts } from './renderer.js'
|
|
16
16
|
import './canvas.js'
|
|
17
17
|
import './figure-parts.js'
|
|
18
18
|
import './figure-material-panel.js'
|
|
19
|
+
import './figure-section-panel.js'
|
|
19
20
|
import { faceHighlightOf, ghostPartsOf, previewWordsOf, rowWordsOf, type HeldPreview } from './command-preview.js'
|
|
20
21
|
import { motionEndsOf, type MotionEnds } from './motion-ends.js'
|
|
21
22
|
import './figure-links.js'
|
|
@@ -39,7 +40,7 @@ import { nudgeOf, placementOf, type ScaleAnchor } from './stage-model.js'
|
|
|
39
40
|
/** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
|
|
40
41
|
type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
|
|
41
42
|
import { roomRefitRowsOf } from './occupancy.js'
|
|
42
|
-
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf,
|
|
43
|
+
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './refusals.js'
|
|
43
44
|
import type { FigureRenderPart } from './renderer.js'
|
|
44
45
|
import type { FigureBudgetLine } from './maturity.js'
|
|
45
46
|
import { fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
@@ -47,6 +48,7 @@ import { jointFanOf } from './joint-fan.js'
|
|
|
47
48
|
import { jointDragOf } from './joint-drag.js'
|
|
48
49
|
import type { FirstSave } from './first-save.js'
|
|
49
50
|
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE, scrubbed, scrubStep, type LiveValues } from './live.js'
|
|
51
|
+
import { startingShapeKindOfBody } from './starting-shape.js'
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -1089,7 +1091,6 @@ export class FigureEditor extends LitElement {
|
|
|
1089
1091
|
.motionEnds=${section === 'motion' ? this.motionEnds() : null}
|
|
1090
1092
|
@go-fasten=${() => this.goFasten()}
|
|
1091
1093
|
@preview-command=${(e: CustomEvent) => this.previewCommand(e.detail)}
|
|
1092
|
-
@resize-with-placement=${(e: CustomEvent) => this.resizeWithPlacement(e.detail.request)}
|
|
1093
1094
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1094
1095
|
></figure-links>`
|
|
1095
1096
|
}
|
|
@@ -1137,30 +1138,6 @@ export class FigureEditor extends LitElement {
|
|
|
1137
1138
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1138
1139
|
></figure-clips>`
|
|
1139
1140
|
}
|
|
1140
|
-
/**
|
|
1141
|
-
* "Follow the size and keep where the figure is placed" (ADR-0097): figure-model plans it from the figure as it is now,
|
|
1142
|
-
* and the whole plan is one step of the history. A refusal with a way on -- move the part onto the plane, replace how
|
|
1143
|
-
* its position is worked out -- asks in the app's prompt and plans again with that choice; any other says why.
|
|
1144
|
-
*/
|
|
1145
|
-
private resizeWithPlacement(request: FigureResizeRequest) {
|
|
1146
|
-
this.retry = undefined
|
|
1147
|
-
this.notice = ''
|
|
1148
|
-
const plan = planFigureResize(parseFigureAsset(this.session!.source()), request)
|
|
1149
|
-
const refusal = plan.refusal
|
|
1150
|
-
if (refusal) {
|
|
1151
|
-
const choice = refusal.choice
|
|
1152
|
-
/* The gap is chosen on the screen beside the button, with its distance; only replacing a position rule is asked here. */
|
|
1153
|
-
this.retry = choice === 'replacePosition' ? { label: i18next.t('figure.button.resize-choice-replace-position'), run: () => this.resizeWithPlacement({ ...request, replacePosition: true }) } : undefined
|
|
1154
|
-
this.error = resizeRefusalOf(plan, request.part, (key, params) => i18next.t(key, params))
|
|
1155
|
-
return
|
|
1156
|
-
}
|
|
1157
|
-
this.run(() => this.session!.applyRecipe(plan.steps))
|
|
1158
|
-
/*
|
|
1159
|
-
The declared room was measured from the part's old numbers and is not changed: said as a fact, not as a claim that
|
|
1160
|
-
the part leaves it -- that is measured where the room is declared again (designer 2026-09-27).
|
|
1161
|
-
*/
|
|
1162
|
-
if (!this.error && plan.roomReads.length) this.notice = i18next.t('figure.text.resize-room-unchanged')
|
|
1163
|
-
}
|
|
1164
1141
|
/** One command, one step of the history. A refusal a person can meet here is said in their words. */
|
|
1165
1142
|
private linkCommand(action: FigureAuthoringAction) {
|
|
1166
1143
|
this.retry = undefined
|
|
@@ -1367,7 +1344,7 @@ export class FigureEditor extends LitElement {
|
|
|
1367
1344
|
}
|
|
1368
1345
|
rows.set(id, {
|
|
1369
1346
|
id,
|
|
1370
|
-
|
|
1347
|
+
kind: startingShapeKindOfBody((g as { shape?: never }).shape) ?? '',
|
|
1371
1348
|
hidden: this.hiddenParts.has(id),
|
|
1372
1349
|
invisible: (g as { visible?: boolean }).visible === false,
|
|
1373
1350
|
...(id !== g.id ? { copies: 1 } : {}),
|
|
@@ -2037,6 +2014,11 @@ export class FigureEditor extends LitElement {
|
|
|
2037
2014
|
${this.renderGroups(this.withSizeRow(groupFields(partFields, this.selected), held), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable), held)}
|
|
2038
2015
|
${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
|
|
2039
2016
|
${this.renderLinks(unavailable, 'shape')}
|
|
2017
|
+
<figure-section-panel
|
|
2018
|
+
.body=${{ ...part, id: this.selected }}
|
|
2019
|
+
?locked=${unavailable}
|
|
2020
|
+
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
2021
|
+
></figure-section-panel>
|
|
2040
2022
|
<figure-material-panel
|
|
2041
2023
|
.body=${{ ...part, id: this.selected }}
|
|
2042
2024
|
.palette=${view.palette ?? {}}
|
|
@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from 'lit'
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
|
-
import { FIGURE_FACES
|
|
7
|
+
import { FIGURE_FACES } from '@hatiolab/figure-model'
|
|
8
8
|
import type { FigureAsset, FigureAuthoringAction, FigureDimensionSource, FigureFace } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -42,7 +42,6 @@ import {
|
|
|
42
42
|
fixedJointOf,
|
|
43
43
|
rowOf,
|
|
44
44
|
positionsWorkedOut,
|
|
45
|
-
resizeAxesOf,
|
|
46
45
|
fieldWordsOf,
|
|
47
46
|
measureNow,
|
|
48
47
|
type Alignment,
|
|
@@ -109,10 +108,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
109
108
|
div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
|
|
110
109
|
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
111
110
|
button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
|
|
112
|
-
/*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
fieldset[resize-pending] small { opacity: 0.7; }
|
|
111
|
+
/* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
|
|
112
|
+
details[resize-choice] { margin: 12px 0; padding: 0; border: 0; }
|
|
113
|
+
details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
|
|
116
114
|
div[resize-held] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
|
|
117
115
|
div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
|
|
118
116
|
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
@@ -170,8 +168,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
170
168
|
private fastenCandidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
171
169
|
private rowCandidate?: FigureAuthoringAction
|
|
172
170
|
|
|
173
|
-
/**
|
|
174
|
-
@state() private
|
|
171
|
+
/** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
|
|
172
|
+
@state() private choosingFastening = false
|
|
175
173
|
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
176
174
|
@state() private spotlit = false
|
|
177
175
|
@state() private fasten = freshFasten()
|
|
@@ -198,6 +196,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
198
196
|
this.place = freshPlace()
|
|
199
197
|
this.row = freshRow()
|
|
200
198
|
this.editingMotion = false
|
|
199
|
+
this.choosingFastening = false
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
|
|
@@ -317,41 +316,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
317
316
|
}
|
|
318
317
|
|
|
319
318
|
private renderDimensions(asset: FigureAsset) {
|
|
320
|
-
/*
|
|
321
|
-
The kernel's plan for "follow the size and keep where the figure is placed" (ADR-0097), with the directions the
|
|
322
|
-
author ticked below -- a pending choice, drawn apart from the rows above, which only say what each direction does
|
|
323
|
-
now (designer 2026-09-27: the immediate and the pending are not mixed in one look).
|
|
324
|
-
*/
|
|
325
319
|
const editable = followableAxes(asset, this.partId)
|
|
326
|
-
const choice = resizeAxesOf(asset, this.partId, this.pendingAxes[this.partId])
|
|
327
|
-
const axes = choice.axes
|
|
328
|
-
const plan = planFigureResize(asset, { part: this.partId, axes })
|
|
329
|
-
const mode = plan.mode
|
|
330
320
|
const round = editable.length === 2
|
|
331
321
|
const word = (axis: Axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
|
|
332
|
-
const scope =
|
|
333
|
-
axes.length && axes.length === editable.length
|
|
334
|
-
? i18next.t(`figure.text.resize-scope-all-${editable.length}`)
|
|
335
|
-
: i18next.t('figure.text.resize-scope-only', { axes: axes.map(word).join(' · ') })
|
|
336
|
-
const ask = (gap?: 'keep' | 'plane') =>
|
|
337
|
-
this.dispatchEvent(new CustomEvent('resize-with-placement', { detail: { request: { part: this.partId, axes, ...(gap ? { gap } : {}) } }, bubbles: true, composed: true }))
|
|
338
|
-
const gapChoice = plan.refusal?.code === 'GAP_CHOICE' ? plan.refusal : undefined
|
|
339
|
-
/* Brought onto the plane, where it moves -- said on the button before it is pressed. */
|
|
340
|
-
const onto = gapChoice ? planFigureResize(asset, { part: this.partId, axes, gap: 'plane' }).moves[0] : undefined
|
|
341
|
-
const place = i18next.t(`figure.label.resize-place-${mode}`)
|
|
342
|
-
const tick = (axis: Axis, on: boolean) => {
|
|
343
|
-
const next = on ? [...new Set([...axes, axis])] : axes.filter(one => one !== axis)
|
|
344
|
-
this.pendingAxes = { ...this.pendingAxes, [this.partId]: next }
|
|
345
|
-
}
|
|
346
322
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
|
|
347
|
-
/* One step for the whole part (figure-model follow-size): the size is declared from the parts if there is none. */
|
|
348
323
|
const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance')
|
|
349
|
-
const whole = followsAll
|
|
350
|
-
? nothing
|
|
351
|
-
: html`<button act follow-size ?disabled=${this.locked} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
352
|
-
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
353
|
-
</button>`
|
|
354
|
-
const held = plan.refusal?.code === 'HELD'
|
|
355
324
|
return html`<p hint>${i18next.t('figure.text.resize-intent')}</p>
|
|
356
325
|
${asset.sizing === 'uniform' ? html`<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
|
|
357
326
|
<div resize-now>${editable.map(axis => {
|
|
@@ -366,46 +335,17 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
366
335
|
${following
|
|
367
336
|
? html`<button link resize-unfollow=${axis} ?disabled=${this.locked}
|
|
368
337
|
@click=${() => {
|
|
369
|
-
/* Set back to fixed: the author's choice for this part from now on, not a blank to fill with "all". */
|
|
370
|
-
this.pendingAxes = { ...this.pendingAxes, [this.partId]: axes.filter(one => one !== axis) }
|
|
371
338
|
this.follow(asset, axis, 'own', now)
|
|
372
339
|
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
373
340
|
: nothing}
|
|
374
341
|
</div>`
|
|
375
342
|
})}</div>
|
|
376
|
-
${
|
|
377
|
-
?
|
|
378
|
-
: html`<
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const following = now.of === 'instance' && now.axis === axis
|
|
383
|
-
const custom = now.of !== 'own' && !following
|
|
384
|
-
return html`<label>
|
|
385
|
-
<input type="checkbox" resize-tick=${axis} .checked=${axes.includes(axis)} ?disabled=${following || custom}
|
|
386
|
-
@change=${(e: Event) => tick(axis, (e.target as HTMLInputElement).checked)} />
|
|
387
|
-
${word(axis)}${following ? html` <small>${i18next.t('figure.text.resize-already-following')}</small>` : nothing}
|
|
388
|
-
</label>`
|
|
389
|
-
})}
|
|
390
|
-
<p hint resize-basis>${i18next.t(`figure.text.resize-basis-${choice.basis}`)}</p>
|
|
391
|
-
<p hint>${i18next.t('figure.text.placement-intent-' + mode)}</p>
|
|
392
|
-
${gapChoice
|
|
393
|
-
? html`<p hint gap-choice>${i18next.t(gapChoice.mm! > 0 ? 'figure.text.resize-gap-off' : 'figure.text.resize-gap-through', { place, mm: String(Math.abs(Math.round(gapChoice.mm! * 10) / 10)) })}</p>
|
|
394
|
-
<button act resize-keep-gap @click=${() => ask('keep')}>
|
|
395
|
-
${i18next.t(gapChoice.mm! > 0 ? 'figure.button.resize-keep-gap' : 'figure.button.resize-keep-through', { scope, mm: String(Math.abs(Math.round(gapChoice.mm! * 10) / 10)) })}
|
|
396
|
-
</button>
|
|
397
|
-
<button act resize-onto-plane @click=${() => ask('plane')}>
|
|
398
|
-
${i18next.t('figure.button.resize-onto-plane-' + mode, { scope })}
|
|
399
|
-
${onto ? html`<small>${i18next.t('figure.text.resize-move', { axis: onto.axis.toUpperCase(), mm: String(Math.round((onto.to - onto.from) * 10) / 10) })}</small>` : nothing}
|
|
400
|
-
</button>`
|
|
401
|
-
: html`<button act resize-with-placement ?disabled=${choice.basis === 'none' || plan.refusal?.code === 'UNIFORM_POLICY'} @click=${() => ask()}>
|
|
402
|
-
${i18next.t('figure.button.resize-scoped-' + mode, { scope })}
|
|
403
|
-
</button>`}
|
|
404
|
-
${!plan.refusal && plan.replaces.length
|
|
405
|
-
? html`<p hint resize-replaces>${i18next.t('figure.text.resize-replaces', { fields: [...new Set(plan.replaces.map(id => fieldWordsOf(id, key => i18next.t(key))))].join(' · '), place })}</p>`
|
|
406
|
-
: nothing}
|
|
407
|
-
</fieldset>`}
|
|
408
|
-
<details><summary>${i18next.t('figure.label.resize-advanced')}</summary>${whole}${followableAxes(asset, this.partId).map(axis => {
|
|
343
|
+
${followsAll
|
|
344
|
+
? html`<p hint>${i18next.t('figure.text.resize-all-following')}</p>`
|
|
345
|
+
: html`<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
346
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
347
|
+
</button>`}
|
|
348
|
+
<details resize-choice><summary>${i18next.t('figure.label.resize-advanced')}</summary><div>${followableAxes(asset, this.partId).map(axis => {
|
|
409
349
|
const now = followOf(asset, this.partId, axis)
|
|
410
350
|
const value = choiceOf(now)
|
|
411
351
|
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
@@ -445,7 +385,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
445
385
|
</label>`
|
|
446
386
|
: nothing}
|
|
447
387
|
</div>`
|
|
448
|
-
})}</details>`
|
|
388
|
+
})}</div></details>`
|
|
449
389
|
}
|
|
450
390
|
|
|
451
391
|
/**
|
|
@@ -631,6 +571,14 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
631
571
|
target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
|
|
632
572
|
})
|
|
633
573
|
: i18next.t('figure.text.fasten-new')
|
|
574
|
+
/* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
|
|
575
|
+
if (!current && !this.choosingFastening && !f.target)
|
|
576
|
+
return html`
|
|
577
|
+
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
578
|
+
<button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
|
|
579
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
580
|
+
</button>
|
|
581
|
+
`
|
|
634
582
|
return html`
|
|
635
583
|
<p fasten-mode ?editing=${!!current}>${editing}</p>
|
|
636
584
|
${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
@@ -651,34 +599,32 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
651
599
|
</div>
|
|
652
600
|
${this.renderDetachWords()}`
|
|
653
601
|
: nothing}
|
|
654
|
-
<div form>
|
|
655
|
-
<span>${i18next.t('figure.label.my-face')}</span>
|
|
656
|
-
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as FigureFace | 'centre' })}>
|
|
657
|
-
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
658
|
-
</select>
|
|
659
|
-
</div>
|
|
660
|
-
<label toggle>
|
|
661
|
-
<input type="checkbox" .checked=${f.toPlane} @change=${(e: Event) => this.aimAt(asset, { ...f, toPlane: (e.target as HTMLInputElement).checked })} />
|
|
662
|
-
${i18next.t('figure.label.stands-on-plane')}
|
|
663
|
-
</label>
|
|
664
602
|
${f.toPlane
|
|
665
|
-
?
|
|
603
|
+
? html`<p hint>${i18next.t('figure.text.fasten-edit-on-plane')}</p>`
|
|
666
604
|
: html`<div form>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
605
|
+
<span>${i18next.t('figure.label.target-part')}</span>
|
|
606
|
+
<select @change=${(e: Event) => this.aimAt(asset, { ...f, target: (e.target as HTMLSelectElement).value })}>
|
|
607
|
+
<option value="" ?selected=${!f.target}>${i18next.t('figure.label.target-part-placeholder')}</option>
|
|
608
|
+
${targets.map(one =>
|
|
609
|
+
loops(one)
|
|
610
|
+
? html`<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
611
|
+
: html`<option value=${one} ?selected=${one === f.target}>${one}</option>`
|
|
612
|
+
)}
|
|
613
|
+
</select>
|
|
614
|
+
</div>
|
|
615
|
+
${f.target
|
|
616
|
+
? html`<div form>
|
|
617
|
+
<span>${i18next.t('figure.label.my-face')}</span>
|
|
618
|
+
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as FigureFace | 'centre' })}>
|
|
619
|
+
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
620
|
+
</select>
|
|
621
|
+
<span>${i18next.t('figure.label.target-face')}</span>
|
|
622
|
+
<select @change=${(e: Event) => this.aimAt(asset, { ...f, targetFace: (e.target as HTMLSelectElement).value as FigureFace })}>
|
|
623
|
+
${theirFaces.map(face => html`<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
624
|
+
</select>
|
|
625
|
+
</div>`
|
|
626
|
+
: html`<p hint>${i18next.t('figure.text.fasten-target-first')}</p>`}`}
|
|
627
|
+
${(f.toPlane || f.target) ? html`<details fasten-detail>
|
|
682
628
|
<summary>${i18next.t('figure.label.fasten-detail')} · <span fasten-summary>${detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')}</span></summary>
|
|
683
629
|
${f.toPlane
|
|
684
630
|
? nothing
|
|
@@ -700,7 +646,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
700
646
|
${action && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
|
|
701
647
|
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
702
648
|
<md-icon>link</md-icon>${i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')}
|
|
703
|
-
</button
|
|
649
|
+
</button>` : nothing}
|
|
704
650
|
`
|
|
705
651
|
}
|
|
706
652
|
|
|
@@ -6,6 +6,7 @@ import { MATERIAL_PRESETS, SEGMENT_PRESETS, figureTrianglesOf } from '@hatiolab/
|
|
|
6
6
|
import { activePalette } from '@hatiolab/things-scene'
|
|
7
7
|
import type { FigureAssetGeometry, FigureMaterial, FigurePartEdit } from '@hatiolab/figure-model'
|
|
8
8
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
9
|
+
import { isRoundBody } from './starting-shape.js'
|
|
9
10
|
|
|
10
11
|
/** The purpose each palette role is for, by its token. */
|
|
11
12
|
const PURPOSE: Record<string, string> = {
|
|
@@ -23,7 +24,6 @@ const PURPOSE: Record<string, string> = {
|
|
|
23
24
|
export const purposeOf = (token?: string) => i18next.t(`figure.label.palette-purpose-${PURPOSE[token ?? ''] ?? 'registered'}`)
|
|
24
25
|
|
|
25
26
|
/** Bodies with a rim — the ones `set-segments` accepts. */
|
|
26
|
-
const ROUND = new Set(['cylinder', 'ellipticCylinder', 'sphere', 'frustum'])
|
|
27
27
|
const DEFAULT_SEGMENTS = 12
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -230,8 +230,7 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
private renderSegments(part: FigureAssetGeometry & { id: string }) {
|
|
233
|
-
|
|
234
|
-
if (!ROUND.has(provider)) return nothing
|
|
233
|
+
if (!isRoundBody((part as { shape?: never }).shape)) return nothing
|
|
235
234
|
const current = part.segments ?? DEFAULT_SEGMENTS
|
|
236
235
|
return html`
|
|
237
236
|
<div field>
|
|
@@ -13,7 +13,7 @@ import { partLabelOf } from './fields.js'
|
|
|
13
13
|
*
|
|
14
14
|
* 팔레트는 목록 위에, 눈 단추는 늘 자리를 차지하고, 조작은 고른 줄에서만 보인다.
|
|
15
15
|
*
|
|
16
|
-
* 부품의 도형은 평가가 준 `
|
|
16
|
+
* 부품의 도형은 평가가 준 형상에서 읽은 시작 모양 이름(`kind`)으로 보인다. 「숨김」은 여기서도 **작업대의 것**이다 — 저장되지 않고, 자산의 `visibility`(상태에 따라
|
|
17
17
|
* 보이고 안 보이는 것)와는 다른 것이다. 그것은 속성 패널의 몫이다.
|
|
18
18
|
*
|
|
19
19
|
* 이 컴포넌트는 정본을 만지지 않는다. 무엇을 하겠다는 것만 알린다.
|
|
@@ -22,7 +22,8 @@ import { partLabelOf } from './fields.js'
|
|
|
22
22
|
/** 한 줄 — 평가에서 이 패널이 읽는 것만. */
|
|
23
23
|
export interface FigurePartRow {
|
|
24
24
|
id: string
|
|
25
|
-
|
|
25
|
+
/** 시작 모양 이름(`startingShapeKindOfBody`): 상자 · 원기둥 … 아이콘을 고른다. */
|
|
26
|
+
kind: string
|
|
26
27
|
/** 작업대에서 잠깐 가린 것. 저장되지 않는다. */
|
|
27
28
|
hidden?: boolean
|
|
28
29
|
/** 자산이 지금 상태에서 보이지 않는다고 한 것. 저장되는 것이고, 여기서는 표시만 한다. */
|
|
@@ -36,17 +37,14 @@ export interface FigurePartRow {
|
|
|
36
37
|
fastenedTo?: string
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
/** 시작 모양마다의 아이콘. Material Symbols 이름이다. */
|
|
41
|
+
const KIND_ICON: Record<string, string> = {
|
|
42
|
+
cube: 'deployed_code',
|
|
42
43
|
cylinder: 'database',
|
|
43
|
-
ellipticCylinder: 'database',
|
|
44
44
|
frustum: 'change_history',
|
|
45
45
|
sphere: 'circle',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
hollowBox: 'check_box_outline_blank',
|
|
49
|
-
hollowBoxXZ: 'check_box_outline_blank',
|
|
46
|
+
'rounded-box': 'crop_square',
|
|
47
|
+
'hollow-box': 'check_box_outline_blank',
|
|
50
48
|
polygon: 'pentagon',
|
|
51
49
|
portal: 'door_front'
|
|
52
50
|
}
|
|
@@ -575,7 +573,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
575
573
|
>
|
|
576
574
|
<md-icon>${off ? 'visibility_off' : 'visibility'}</md-icon>
|
|
577
575
|
</button>
|
|
578
|
-
<md-icon>${
|
|
576
|
+
<md-icon>${KIND_ICON[part.kind] ?? 'deployed_code'}</md-icon>
|
|
579
577
|
<span label title=${part.id}
|
|
580
578
|
>${partLabelOf(part.id)}${part.copies ? html` <small copies>× ${part.copies}</small>` : nothing}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}${part.fastenedTo
|
|
581
579
|
? html`<small fastened-to title=${i18next.t('figure.label.fastened-to')}>${part.fastenedTo}</small>`
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import { live } from 'lit/directives/live.js'
|
|
4
|
+
|
|
5
|
+
import { i18next, localize } from '@operato/i18n'
|
|
6
|
+
import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
7
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
8
|
+
import { cavityRequestOf, defaultWallOf, mm, sameOf } from './section-controls.js'
|
|
9
|
+
|
|
10
|
+
type Axis = 'x' | 'y' | 'z'
|
|
11
|
+
type End = 'start' | 'end'
|
|
12
|
+
|
|
13
|
+
/** The face each end of a push is, by the axis it runs along: its start is the low side. */
|
|
14
|
+
const END_FACE: Record<Axis, Record<End, string>> = {
|
|
15
|
+
x: { start: 'left', end: 'right' },
|
|
16
|
+
y: { start: 'bottom', end: 'top' },
|
|
17
|
+
z: { start: 'back', end: 'front' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 단면 — a pushed part's rounded corners and its inside (ADR-0100 stage 1).
|
|
22
|
+
*
|
|
23
|
+
* Only what the part's section has: corners for a rectangle, an inside for a rectangle or an ellipse. A drawn section
|
|
24
|
+
* and a turned body show nothing here. A radius or a wall entered sets every corner or every wall to it; the kernel
|
|
25
|
+
* holds per-axis values, and when they differ the field says so and shows none of them.
|
|
26
|
+
*
|
|
27
|
+
* Every change leaves as one `command`; the editor runs it through the session, one step of the undo history.
|
|
28
|
+
*/
|
|
29
|
+
@customElement('figure-section-panel')
|
|
30
|
+
export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
31
|
+
static styles = [SidePanelStyles, css`
|
|
32
|
+
:host {
|
|
33
|
+
display: block;
|
|
34
|
+
}
|
|
35
|
+
div[row] {
|
|
36
|
+
display: flex;
|
|
37
|
+
gap: 6px;
|
|
38
|
+
align-items: center;
|
|
39
|
+
}
|
|
40
|
+
div[row] > * {
|
|
41
|
+
flex: 1;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
}
|
|
44
|
+
div[row] > label {
|
|
45
|
+
flex: 0 0 auto;
|
|
46
|
+
}
|
|
47
|
+
`]
|
|
48
|
+
|
|
49
|
+
@property({ attribute: false }) body?: { id: string; shape: FigureEvaluatedShape }
|
|
50
|
+
@property({ type: Boolean }) locked = false
|
|
51
|
+
|
|
52
|
+
render() {
|
|
53
|
+
const body = this.body
|
|
54
|
+
const extrusion = body?.shape?.provider === 'extrude' ? body.shape.extrusion : undefined
|
|
55
|
+
if (!body || !extrusion) return nothing
|
|
56
|
+
const kind = extrusion.section.kind
|
|
57
|
+
if (kind !== 'rectangle' && kind !== 'ellipse') return nothing
|
|
58
|
+
return html`<section>
|
|
59
|
+
<h3>${i18next.t('figure.label.group-section')}</h3>
|
|
60
|
+
${kind === 'rectangle' ? this.renderCorner(body) : nothing} ${this.renderCavity(body, extrusion.axis)}
|
|
61
|
+
</section>`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private renderCorner(body: { id: string; shape: FigureEvaluatedShape }) {
|
|
65
|
+
const d = body.shape.dimensions
|
|
66
|
+
const reaches = Object.keys(d)
|
|
67
|
+
.filter(k => k.startsWith('corner.'))
|
|
68
|
+
.map(k => d[k])
|
|
69
|
+
const one = reaches.length ? sameOf(reaches) : 0
|
|
70
|
+
return html`<div field>
|
|
71
|
+
<label>${i18next.t('figure.label.section-corner')}</label>
|
|
72
|
+
<input
|
|
73
|
+
type="number"
|
|
74
|
+
min="0"
|
|
75
|
+
step="any"
|
|
76
|
+
.value=${live(one === undefined ? '' : String(mm(one)))}
|
|
77
|
+
placeholder=${one === undefined ? i18next.t('figure.text.section-corner-differs') : ''}
|
|
78
|
+
?disabled=${this.locked}
|
|
79
|
+
@change=${(e: Event) => {
|
|
80
|
+
const input = e.target as HTMLInputElement
|
|
81
|
+
const value = Number(input.value)
|
|
82
|
+
if (input.value === '' || !Number.isFinite(value) || value < 0) {
|
|
83
|
+
input.value = one === undefined ? '' : String(mm(one))
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
/* 0 is a sharp corner: the rounding is taken away rather than kept at nothing. */
|
|
87
|
+
this.command({ kind: 'set-section-corner', part: body.id, corner: value > 0 ? value : null })
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
</div>`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private renderCavity(body: { id: string; shape: FigureEvaluatedShape }, axis: Axis) {
|
|
94
|
+
const cavity = body.shape.cavity
|
|
95
|
+
const request = (patch: Partial<FigureCavityRequest>) => cavityRequestOf(body.shape, patch)
|
|
96
|
+
return html`<div field>
|
|
97
|
+
<label>
|
|
98
|
+
<input
|
|
99
|
+
type="checkbox"
|
|
100
|
+
.checked=${live(!!cavity)}
|
|
101
|
+
?disabled=${this.locked}
|
|
102
|
+
@change=${(e: Event) => {
|
|
103
|
+
const on = (e.target as HTMLInputElement).checked
|
|
104
|
+
;(e.target as HTMLInputElement).checked = !!cavity
|
|
105
|
+
this.command({ kind: 'set-cavity', part: body.id, cavity: on ? request({}) : null })
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
${i18next.t('figure.label.section-cavity')}
|
|
109
|
+
</label>
|
|
110
|
+
</div>
|
|
111
|
+
${cavity
|
|
112
|
+
? html`<div field>
|
|
113
|
+
<label>${i18next.t('figure.label.section-wall')}</label>
|
|
114
|
+
${this.thickness(sameOf(Object.values(cavity.wall)), value => this.command({ kind: 'set-cavity', part: body.id, cavity: request({ wall: value }) }), 'figure.text.section-wall-differs')}
|
|
115
|
+
</div>
|
|
116
|
+
${(['start', 'end'] as const).map(end => this.renderEnd(body.id, axis, end, cavity[end], request))}`
|
|
117
|
+
: nothing}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private renderEnd(
|
|
121
|
+
part: string,
|
|
122
|
+
axis: Axis,
|
|
123
|
+
end: End,
|
|
124
|
+
now: 'open' | { closed: number },
|
|
125
|
+
request: (patch: Partial<FigureCavityRequest>) => FigureCavityRequest
|
|
126
|
+
) {
|
|
127
|
+
const closed = now !== 'open'
|
|
128
|
+
const set = (value: 'open' | { closed: number }) => this.command({ kind: 'set-cavity', part, cavity: request({ [end]: value }) })
|
|
129
|
+
return html`<div field>
|
|
130
|
+
<label>${i18next.t('figure.label.face-' + END_FACE[axis][end])}</label>
|
|
131
|
+
<div row>
|
|
132
|
+
<select
|
|
133
|
+
.value=${live(closed ? 'closed' : 'open')}
|
|
134
|
+
?disabled=${this.locked}
|
|
135
|
+
@change=${(e: Event) => {
|
|
136
|
+
const select = e.target as HTMLSelectElement
|
|
137
|
+
const want = select.value
|
|
138
|
+
select.value = closed ? 'closed' : 'open'
|
|
139
|
+
set(want === 'open' ? 'open' : { closed: defaultWallOf(this.body!.shape) })
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<option value="closed">${i18next.t('figure.label.section-end-closed')}</option>
|
|
143
|
+
<option value="open">${i18next.t('figure.label.section-end-open')}</option>
|
|
144
|
+
</select>
|
|
145
|
+
${closed ? this.thickness(now.closed, value => set({ closed: value })) : html`<span></span>`}
|
|
146
|
+
</div>
|
|
147
|
+
</div>`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A thickness field: a positive number, committed on change; anything else puts the old value back. */
|
|
151
|
+
private thickness(now: number | undefined, commit: (value: number) => void, differs?: string) {
|
|
152
|
+
const shown = now === undefined ? '' : String(mm(now))
|
|
153
|
+
return html`<input
|
|
154
|
+
type="number"
|
|
155
|
+
min="0"
|
|
156
|
+
step="any"
|
|
157
|
+
.value=${live(shown)}
|
|
158
|
+
placeholder=${now === undefined && differs ? i18next.t(differs) : ''}
|
|
159
|
+
?disabled=${this.locked}
|
|
160
|
+
@change=${(e: Event) => {
|
|
161
|
+
const input = e.target as HTMLInputElement
|
|
162
|
+
const value = Number(input.value)
|
|
163
|
+
if (input.value === '' || !Number.isFinite(value) || !(value > 0)) {
|
|
164
|
+
input.value = shown
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
commit(value)
|
|
168
|
+
}}
|
|
169
|
+
/>`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private command(action: FigureAuthoringAction) {
|
|
173
|
+
this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))
|
|
174
|
+
}
|
|
175
|
+
}
|