@things-factory/figure-ui 10.1.83 → 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 +8 -1
- 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/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 -0
- package/dist-client/modeller/editor.js +8 -1
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +1 -1
- package/dist-client/modeller/figure-clips.d.ts +1 -1
- package/dist-client/modeller/figure-controls.d.ts +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +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-properties.d.ts +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/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 +4 -4
- package/test/material.test.ts +7 -2
- package/test/renderer.test.ts +2 -1
- package/test/resize-with-placement.test.ts +3 -3
- package/test/scene-canvas.test.ts +2 -1
- package/test/section-panel.test.ts +93 -0
- package/translations/en.json +13 -0
- package/translations/ja.json +13 -0
- package/translations/ko.json +13 -0
- package/translations/ms.json +13 -0
- package/translations/zh.json +13 -0
- 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)
|
|
@@ -16,6 +16,7 @@ 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'
|
|
@@ -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
|
/**
|
|
@@ -1342,7 +1344,7 @@ export class FigureEditor extends LitElement {
|
|
|
1342
1344
|
}
|
|
1343
1345
|
rows.set(id, {
|
|
1344
1346
|
id,
|
|
1345
|
-
|
|
1347
|
+
kind: startingShapeKindOfBody((g as { shape?: never }).shape) ?? '',
|
|
1346
1348
|
hidden: this.hiddenParts.has(id),
|
|
1347
1349
|
invisible: (g as { visible?: boolean }).visible === false,
|
|
1348
1350
|
...(id !== g.id ? { copies: 1 } : {}),
|
|
@@ -2012,6 +2014,11 @@ export class FigureEditor extends LitElement {
|
|
|
2012
2014
|
${this.renderGroups(this.withSizeRow(groupFields(partFields, this.selected), held), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable), held)}
|
|
2013
2015
|
${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
|
|
2014
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>
|
|
2015
2022
|
<figure-material-panel
|
|
2016
2023
|
.body=${{ ...part, id: this.selected }}
|
|
2017
2024
|
.palette=${view.palette ?? {}}
|
|
@@ -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
|
+
}
|
package/client/modeller/gizmo.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { figureAttachmentsOf } from '@hatiolab/figure-model'
|
|
1
|
+
import { figureAttachmentsOf, figureShapeProviderOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureAsset, FigureInputChange } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* What the gizmo may write for a part: move · turn · size, on a asset.
|
|
6
6
|
*
|
|
7
7
|
* A drag can only be written back where the part is placed by its own numbers. A part the create commands made is:
|
|
8
|
-
* its pose is `rigid@1` into the asset frame from `<part>.position.*` and `<part>.rotation.*`, and
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* its pose is `rigid@1` into the asset frame from `<part>.position.*` and `<part>.rotation.*`, and its shape takes its
|
|
9
|
+
* measures from inputs of its own — `<part>.size.*` from `addFigureBox`, `<part>.dimension.*` from the editor's shape
|
|
10
|
+
* palette — at the arguments the shape itself names (figure-model's provider). A part fastened to another, following an instance size or placed by
|
|
11
11
|
* expressions has no such numbers; the gizmo does not offer what it could not write back, rather
|
|
12
12
|
* than move something the next evaluation would put back.
|
|
13
13
|
*/
|
|
@@ -29,9 +29,6 @@ export interface GizmoReach {
|
|
|
29
29
|
round?: { across: string[]; along: string[] }
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Shapes whose first three arguments are the extent along X, Y and Z: a door frame's too (its width, height, depth). */
|
|
33
|
-
const BOXES = new Set(['box-shape@1', 'rounded-box@1', 'portal-profile@2', 'hollow-box@2'])
|
|
34
|
-
|
|
35
32
|
interface Node {
|
|
36
33
|
id: string
|
|
37
34
|
op: string
|
|
@@ -65,15 +62,28 @@ export function gizmoReachOf(asset: AssetLike, part: string): GizmoReach {
|
|
|
65
62
|
const posed = direct || reseated
|
|
66
63
|
const shape = nodes.find(node => node.id === `${part}.shape`)
|
|
67
64
|
const mine = (ids: string[]) => ids.every(id => id.startsWith(`${part}.`) && design.has(id))
|
|
68
|
-
|
|
69
|
-
shape
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
/*
|
|
66
|
+
Which argument sizes the part along X, Y and Z: the shape says (figure-model's provider, ADR-0100) -- never its first
|
|
67
|
+
three arguments. A round section written from one radius, a sphere and a taper keep their shape: their radii take one
|
|
68
|
+
factor together.
|
|
69
|
+
*/
|
|
70
|
+
const provider = shape ? figureShapeProviderOf(shape as never) : undefined
|
|
71
|
+
const edit = provider?.edit
|
|
72
|
+
const refOf = (axis: 'x' | 'y' | 'z') => {
|
|
73
|
+
const e = edit?.[axis]
|
|
74
|
+
return e && !('refused' in e) ? shape!.args[e.arg]! : undefined
|
|
75
|
+
}
|
|
76
|
+
const measures = AXES.map(refOf)
|
|
77
|
+
const shared = !!edit && AXES.some(axis => { const e = edit[axis]; return !('refused' in e) && !!(e.sameAs || e.with?.length) })
|
|
78
|
+
let round: GizmoReach['round']
|
|
79
|
+
if (shared && edit) {
|
|
80
|
+
const withs = AXES.flatMap(axis => { const e = edit[axis]; return !('refused' in e) ? (e.with ?? []).map(i => shape!.args[i]!) : [] })
|
|
81
|
+
const sphere = !!provider && Object.keys(provider.faces).length === 0
|
|
82
|
+
const across = [...new Set([refOf('x')!, ...withs])]
|
|
83
|
+
const along = sphere ? [] : [refOf('y')!]
|
|
84
|
+
if ([...across, ...along].every(id => id && mine([id]))) round = { across: sphere ? [...new Set([...across, refOf('y')!])] : across, along }
|
|
85
|
+
}
|
|
86
|
+
const extent = !round && measures.every(Boolean) ? (measures as string[]) : []
|
|
77
87
|
const sizes =
|
|
78
88
|
extent.length === 3 && extent.every(id => id.startsWith(`${part}.`) && design.has(id)) ? (extent as [string, string, string]) : undefined
|
|
79
89
|
/*
|
|
@@ -2,6 +2,7 @@ import { compileFigureAsset, compileFigureGraph, figureWorldBoxesOf, figureWrite
|
|
|
2
2
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
import type { FigureRenderPart } from './renderer.js'
|
|
5
|
+
import { sectionShape } from './drawn-shapes.js'
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
* Where the selected part turns, and how far -- a translucent fan on the editing board. An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and
|
|
@@ -249,14 +250,14 @@ export function jointFanOf(asset: FigureAsset, part: string, values: Record<stri
|
|
|
249
250
|
const r = [0, 1, 2].map(i => [first[i]!, axisNow[i]!, zAxis[i]!])
|
|
250
251
|
const local = (p: number[]) => {
|
|
251
252
|
const v = sub(p, pivot)
|
|
252
|
-
return { x: dot(v, first),
|
|
253
|
+
return { x: dot(v, first), z: dot(v, zAxis) }
|
|
253
254
|
}
|
|
254
255
|
const full = max - min >= 359.999
|
|
255
|
-
const profile = [...(full ? [] : [{ x: 0,
|
|
256
|
+
const profile = [...(full ? [] : [{ x: 0, z: 0 }]), ...traced.map(local)].map(p => ({ x: Math.round(p.x * 1000) / 1000, z: Math.round(p.z * 1000) / 1000 }))
|
|
256
257
|
|
|
257
258
|
return {
|
|
258
259
|
id: JOINT_FAN,
|
|
259
|
-
shape:
|
|
260
|
+
shape: sectionShape(profile, THICKNESS),
|
|
260
261
|
pose: { t: pivot, r },
|
|
261
262
|
material: { token: '__joint-fan', transparent: true },
|
|
262
263
|
/** Which control turns it: the joint the fan shows. */
|
package/client/modeller/links.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, compileFigureGraph, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureFollow, FigureSizeRange } from '@hatiolab/figure-model'
|
|
3
3
|
import type { FigureAsset, FigureBindingPlace, FigureFace, FigureJoint } from '@hatiolab/figure-model'
|
|
4
|
+
import { startingShapeKindOf } from './starting-shape.js'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
* What a part is tied to — the readers behind the parts' size response, fastening and motion sections. Kept apart from the element so they can be tested without a browser.
|
|
@@ -62,7 +63,7 @@ export function canFollow(asset: FigureAsset, part: string): boolean {
|
|
|
62
63
|
*/
|
|
63
64
|
export function followableAxes(asset: FigureAsset, part: string): Axis[] {
|
|
64
65
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
65
|
-
return shape ? figureFollowableAxesOf(shape
|
|
66
|
+
return shape ? figureFollowableAxesOf(shape as never) : []
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
@@ -87,27 +88,14 @@ export function resizeAxesOf(asset: FigureAsset, part: string, pending?: readonl
|
|
|
87
88
|
return { axes: editable, basis: editable.length ? 'suggested' : 'none' }
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
const KIND_OF_SHAPE: Record<string, string> = {
|
|
91
|
-
'box-shape@1': 'cube',
|
|
92
|
-
'rounded-box@1': 'rounded-box',
|
|
93
|
-
'rounded-box@2': 'rounded-box',
|
|
94
|
-
'hollow-box@1': 'hollow-box',
|
|
95
|
-
'hollow-box@2': 'hollow-box',
|
|
96
|
-
'portal-profile@2': 'portal',
|
|
97
|
-
'cylinder-shape@1': 'cylinder',
|
|
98
|
-
'cylinder-shape@2': 'cylinder',
|
|
99
|
-
'sphere-shape@1': 'sphere',
|
|
100
|
-
'frustum-shape@1': 'frustum',
|
|
101
|
-
'polygon-shape@1': 'polygon'
|
|
102
|
-
}
|
|
103
|
-
|
|
104
91
|
/**
|
|
105
92
|
* The selected part in three short lines, for the head of its panel (designer 2026-09-27, B): what it is, how its
|
|
106
93
|
* size answers the figure's, and what holds it -- 「box · 상자 / 너비는 도형을 따라감 / 아랫면을 바닥에 유지 중」.
|
|
107
94
|
*/
|
|
108
95
|
export function partSummaryOf(asset: FigureAsset, part: string, say: (key: string, params?: Record<string, string>) => string): { kind: string; size: string; relation: string } {
|
|
109
96
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
110
|
-
const
|
|
97
|
+
const named = startingShapeKindOf(shape as never)
|
|
98
|
+
const kind = named ? say(`figure.label.primitive-${named}`) : ''
|
|
111
99
|
const editable = followableAxes(asset, part)
|
|
112
100
|
const round = editable.length === 2
|
|
113
101
|
const word = (axis: Axis) => say(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
|
|
@@ -146,7 +134,10 @@ export function fieldWordsOf(id: string, say: (key: string) => string): string {
|
|
|
146
134
|
/** What a part's own measure on an axis is now (a box's side, a cylinder's radius or length), at the design as it stands. */
|
|
147
135
|
export function dimensionNow(asset: FigureAsset, part: string, axis: Axis): number | undefined {
|
|
148
136
|
const m = modelOf(asset)
|
|
149
|
-
|
|
137
|
+
/* The argument the shape names for this axis (figure-model's provider), not its place among the arguments. */
|
|
138
|
+
const shape = m.nodes.find(n => n.id === `${part}.shape`)
|
|
139
|
+
const edit = shape && figureShapeProviderOf(shape as never)?.edit[axis]
|
|
140
|
+
const ref = edit && !('refused' in edit) ? shape!.args[edit.arg] : undefined
|
|
150
141
|
if (!ref) return undefined
|
|
151
142
|
try {
|
|
152
143
|
const values = compileFigureGraph(asset.document.model as never).evaluate({ ...asset.designInputs, ...asset.stateDefaults }).values as Record<string, number>
|
|
@@ -163,7 +154,7 @@ export function dimensionNow(asset: FigureAsset, part: string, axis: Axis): numb
|
|
|
163
154
|
*/
|
|
164
155
|
export function measureNow(asset: FigureAsset, part: string, axis: Axis): number | undefined {
|
|
165
156
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
166
|
-
const edit = shape && figureShapeProviderOf(shape
|
|
157
|
+
const edit = shape && figureShapeProviderOf(shape as never)?.edit[axis]
|
|
167
158
|
if (!shape || !edit || 'refused' in edit) return undefined
|
|
168
159
|
try {
|
|
169
160
|
const values = compileFigureGraph(asset.document.model as never).evaluate({ ...asset.designInputs, ...asset.stateDefaults }).values as Record<string, number>
|
|
@@ -180,13 +171,20 @@ export function measureNow(asset: FigureAsset, part: string, axis: Axis): number
|
|
|
180
171
|
*/
|
|
181
172
|
export function facesOf(asset: FigureAsset, part: string): string[] {
|
|
182
173
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
183
|
-
return Object.keys((shape && figureShapeFaces(shape
|
|
174
|
+
return inScreenOrder(Object.keys((shape && figureShapeFaces(shape as never)) ?? {}))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Faces in the order the screen lists them: the six outer ones left to front, then the rest as the shape gives them. */
|
|
178
|
+
const SCREEN_ORDER = ['left', 'right', 'bottom', 'top', 'back', 'front']
|
|
179
|
+
function inScreenOrder(names: string[]): string[] {
|
|
180
|
+
const rank = (n: string) => (SCREEN_ORDER.includes(n) ? SCREEN_ORDER.indexOf(n) : SCREEN_ORDER.length)
|
|
181
|
+
return names.map((n, i) => [n, i] as const).sort((a, b) => rank(a[0]) - rank(b[0]) || a[1] - b[1]).map(([n]) => n)
|
|
184
182
|
}
|
|
185
183
|
|
|
186
184
|
/** The axis a face of a part faces along, from the same definitions. */
|
|
187
185
|
export function faceAxisOf(asset: FigureAsset, part: string, face: string): Axis | undefined {
|
|
188
186
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
189
|
-
return (shape && figureShapeFaces(shape
|
|
187
|
+
return (shape && figureShapeFaces(shape as never)?.[face]?.axis) as Axis | undefined
|
|
190
188
|
}
|
|
191
189
|
|
|
192
190
|
/** Whether `attach` can take this part: any part whose shape has a flat face. */
|
|
@@ -556,12 +554,14 @@ export function choiceOf(follow: Follow): string {
|
|
|
556
554
|
}
|
|
557
555
|
}
|
|
558
556
|
|
|
559
|
-
/**
|
|
557
|
+
/**
|
|
558
|
+
* The faces a place can sit on for this part: its flat faces, but not a plane that only touches a curved side along a
|
|
559
|
+
* line (a cylinder's side, Q3) -- nothing is held on a line. A sphere has none.
|
|
560
|
+
*/
|
|
560
561
|
export function placeFacesOf(asset: FigureAsset, part: string): FigureFace[] {
|
|
561
562
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return []
|
|
563
|
+
const faces = (shape && figureShapeFaces(shape as never)) ?? {}
|
|
564
|
+
return inScreenOrder(Object.entries(faces).filter(([, f]) => !(f as { contact?: string }).contact).map(([name]) => name)) as FigureFace[]
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
/** The places things are held that sit on this part. */
|
|
@@ -154,8 +154,9 @@ export function figureReportOf(asset: FigureAsset): FigureReport | undefined {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* The key things-scene shares a body's geometry by (`figure-geometry.ts` `keyOf`):
|
|
158
|
-
*
|
|
157
|
+
* The key things-scene shares a body's geometry by (`figure-geometry.ts` `keyOf`): the operation, what is not in its
|
|
158
|
+
* numbers (the axis, the kind of section, which ends of an inside are open), its numbers to a micrometre, and segments
|
|
159
|
+
* (12 when unset).
|
|
159
160
|
*/
|
|
160
161
|
export function shapeKeyOf(body: FigureAssetGeometry | FigureGeometry): string {
|
|
161
162
|
if (!('shape' in body)) {
|
|
@@ -169,9 +170,12 @@ export function shapeKeyOf(body: FigureAssetGeometry | FigureGeometry): string {
|
|
|
169
170
|
.sort()
|
|
170
171
|
.map(k => `${k}=${round(shape.dimensions[k]!)}`)
|
|
171
172
|
.join(',')
|
|
172
|
-
const
|
|
173
|
+
const e = shape.extrusion,
|
|
174
|
+
c = shape.cavity,
|
|
175
|
+
r = shape.revolution
|
|
176
|
+
const meaning = e ? `${e.axis}:${e.section.kind}:${c ? `${c.start === 'open' ? 'o' : 'c'}${c.end === 'open' ? 'o' : 'c'}` : '-'}` : r ? `${r.axis}:${r.profile.kind}` : ''
|
|
173
177
|
const segments = (body as { segments?: number }).segments ?? 12
|
|
174
|
-
return `${shape.provider}|${dims}|s${segments}
|
|
178
|
+
return `${shape.provider}|${meaning}|${dims}|s${segments}`
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
/**
|
|
@@ -202,7 +206,9 @@ function consistency(asset: FigureAsset, distinctMaterials: number): FigureMatur
|
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
/** Seen from above, these do not fill their footprint box, by provider name. */
|
|
205
|
-
|
|
209
|
+
/** Round seen from above: a circle or an ellipse pushed along Y, anything turned about Y. */
|
|
210
|
+
const roundFootprint = (shape: { extrusion?: { axis: string; section: { kind: string } }; revolution?: { axis: string } }) =>
|
|
211
|
+
(shape.extrusion?.axis === 'y' && shape.extrusion.section.kind === 'ellipse') || shape.revolution?.axis === 'y'
|
|
206
212
|
|
|
207
213
|
/**
|
|
208
214
|
* Can it be told apart from above — baseline 1.
|
|
@@ -276,7 +282,7 @@ function topViewRegions(
|
|
|
276
282
|
const x1 = Math.min(GRID, Math.ceil((box.centre.x + box.extent.x / 2 - frame.x0) * sx))
|
|
277
283
|
const y0 = Math.max(0, Math.floor((box.centre.z - box.extent.z / 2 - frame.z0) * sz))
|
|
278
284
|
const y1 = Math.min(GRID, Math.ceil((box.centre.z + box.extent.z / 2 - frame.z0) * sz))
|
|
279
|
-
const round = 'shape' in body &&
|
|
285
|
+
const round = 'shape' in body && roundFootprint(body.shape as never)
|
|
280
286
|
|
|
281
287
|
for (let y = y0; y < y1; y++) {
|
|
282
288
|
for (let x = x0; x < x1; x++) {
|
|
@@ -3,6 +3,7 @@ import type { FigureAsset } from '@hatiolab/figure-model'
|
|
|
3
3
|
|
|
4
4
|
import { PREVIEW, TARGET_FACE } from './command-preview.js'
|
|
5
5
|
import type { FigureRenderPart } from './renderer.js'
|
|
6
|
+
import { boxShape } from './drawn-shapes.js'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
* Where a part's motion takes it (ADR-0097 §5.5, designer 2026-09-27): the bodies its control moves, drawn translucent at
|
|
@@ -73,7 +74,7 @@ export function motionEndsOf(asset: FigureAsset, part: string, values: Record<st
|
|
|
73
74
|
const y = [z[1]! * x[2]! - z[2]! * x[1]!, z[2]! * x[0]! - z[0]! * x[2]!, z[0]! * x[1]! - z[1]! * x[0]!]
|
|
74
75
|
ghost.push({
|
|
75
76
|
id: `${TARGET_FACE}:slide`,
|
|
76
|
-
shape:
|
|
77
|
+
shape: boxShape(length, THICKNESS, THICKNESS),
|
|
77
78
|
pose: { r: [0, 1, 2].map(i => [x[i]!, y[i]!, z[i]!]), t: [0, 1, 2].map(i => (from[i]! + to[i]!) / 2) },
|
|
78
79
|
material: { token: TARGET_FACE }
|
|
79
80
|
})
|
|
@@ -117,10 +117,30 @@ export function recipeRefusalOf(error: unknown, say: Say): string | null {
|
|
|
117
117
|
* duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」, 2026-09-27). The figure's node limit
|
|
118
118
|
* is said with its number.
|
|
119
119
|
*/
|
|
120
|
+
/**
|
|
121
|
+
* A corner or an inside the part cannot take (ADR-0100 stage 1): what was too large or too thick, or what holds on
|
|
122
|
+
* to what the edit would take away. The kernel names the problem in its message; the screen names it in words.
|
|
123
|
+
*/
|
|
124
|
+
function sectionRefusalOf(code: string | undefined, message: string, say: Say): string | null {
|
|
125
|
+
if (code === 'FEATURE_MAPPING_REQUIRED') {
|
|
126
|
+
/* 「takes away inside-bottom (joint j1); top (feature f2)」: the faces, each in the author's words. */
|
|
127
|
+
const faces = [...message.matchAll(/([\w-]+) \([^)]*\)/g)].map(([, face]) => say('figure.label.face-' + face, {}))
|
|
128
|
+
return say('figure.text.section-face-in-use', { faces: faces.join(', ') })
|
|
129
|
+
}
|
|
130
|
+
if (code === 'DIMENSION_LINKED') return say('figure.text.section-dimension-linked', {})
|
|
131
|
+
if (code !== 'GEOMETRY_DOMAIN') return null
|
|
132
|
+
if (/CORNER_TOO_LARGE/.test(message)) return say('figure.text.section-corner-too-large', {})
|
|
133
|
+
if (/NO_ROOM_INSIDE/.test(message)) return say('figure.text.section-wall-too-thick', {})
|
|
134
|
+
if (/CAVITY_NO_LENGTH/.test(message)) return say('figure.text.section-ends-too-thick', {})
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
export function editRefusalOf(error: unknown, say: Say): string {
|
|
121
139
|
const code = (error as { code?: string })?.code
|
|
122
140
|
const message = error instanceof Error ? error.message : String(error)
|
|
123
141
|
if (code === 'DOCUMENT_LIMIT') return say('figure.text.edit-refused-node-limit', { max: /maximum (\d+) nodes/.exec(message)?.[1] ?? '' })
|
|
142
|
+
const section = sectionRefusalOf(code, message, say)
|
|
143
|
+
if (section) return section
|
|
124
144
|
return say('figure.text.edit-refused', { code: code ?? '' })
|
|
125
145
|
}
|
|
126
146
|
|