@things-factory/figure-ui 10.1.244 → 10.1.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/graphql/index.ts +15 -0
- package/client/modeller/align.ts +51 -13
- package/client/modeller/clips.ts +51 -1
- package/client/modeller/data-interface.ts +16 -0
- package/client/modeller/editor-styles.ts +6 -0
- package/client/modeller/editor.ts +112 -25
- package/client/modeller/fasten-form.ts +8 -1
- package/client/modeller/fasten-geometry.ts +24 -2
- package/client/modeller/figure-clips.ts +137 -4
- package/client/modeller/figure-fasten-dialog.ts +1 -0
- package/client/modeller/figure-fasten-drawing.ts +11 -5
- package/client/modeller/figure-links.ts +25 -0
- package/client/modeller/figure-material-panel.ts +54 -4
- package/client/modeller/figure-parts.ts +7 -2
- package/client/modeller/figure-pipe-panel.ts +215 -0
- package/client/modeller/figure-preview.ts +125 -4
- package/client/modeller/figure-section-editor.ts +36 -3
- package/client/modeller/links.ts +24 -2
- package/client/modeller/material-edit.ts +9 -0
- package/client/modeller/maturity.ts +15 -4
- package/client/modeller/palette.ts +9 -0
- package/client/modeller/pipe-path.ts +21 -0
- package/client/modeller/refusals.ts +66 -1
- package/client/modeller/section-controls.ts +2 -1
- package/client/modeller/section-drawing.ts +20 -1
- package/client/modeller/starting-shape.ts +8 -5
- package/client/pages/figure-list-page.ts +15 -5
- package/dist-client/graphql/index.d.ts +2 -0
- package/dist-client/graphql/index.js +15 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/align.d.ts +28 -7
- package/dist-client/modeller/align.js +37 -12
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +24 -0
- package/dist-client/modeller/clips.js +53 -1
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +6 -0
- package/dist-client/modeller/data-interface.js +17 -0
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +6 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +10 -1
- package/dist-client/modeller/editor.js +117 -26
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +2 -0
- package/dist-client/modeller/fasten-form.js +5 -1
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +8 -1
- package/dist-client/modeller/fasten-geometry.js +22 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +137 -4
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +1 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +3 -1
- package/dist-client/modeller/figure-fasten-drawing.js +13 -5
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +5 -0
- package/dist-client/modeller/figure-links.js +22 -0
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +2 -0
- package/dist-client/modeller/figure-material-panel.js +53 -4
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +5 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-pipe-panel.d.ts +29 -0
- package/dist-client/modeller/figure-pipe-panel.js +221 -0
- package/dist-client/modeller/figure-pipe-panel.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +8 -0
- package/dist-client/modeller/figure-preview.js +128 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +5 -0
- package/dist-client/modeller/figure-section-editor.js +33 -3
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -0
- package/dist-client/modeller/links.js +17 -2
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +8 -0
- package/dist-client/modeller/material-edit.js +8 -0
- package/dist-client/modeller/material-edit.js.map +1 -1
- package/dist-client/modeller/maturity.js +16 -5
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/palette.js +9 -0
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/pipe-path.d.ts +19 -0
- package/dist-client/modeller/pipe-path.js +14 -0
- package/dist-client/modeller/pipe-path.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +12 -1
- package/dist-client/modeller/refusals.js +67 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.js +2 -1
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +5 -0
- package/dist-client/modeller/section-drawing.js +19 -1
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/starting-shape.d.ts +9 -2
- package/dist-client/modeller/starting-shape.js +8 -3
- package/dist-client/modeller/starting-shape.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -0
- package/dist-client/pages/figure-list-page.js +12 -3
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/align.test.ts +28 -6
- package/test/browser/editing-page.js +21 -1
- package/test/browser/editing.check.mjs +124 -0
- package/test/browser/links.check.mjs +49 -8
- package/test/browser/modeller.check.mjs +27 -1
- package/test/browser/part-ids.check.mjs +60 -3
- package/test/browser/parts.check.mjs +2 -2
- package/test/browser/places-mesh.check.mjs +14 -8
- package/test/browser/screen.check.mjs +54 -1
- package/test/browser/section-page.js +7 -1
- package/test/browser/section.check.mjs +81 -0
- package/test/browser/side-panel-sweep.check.mjs +13 -0
- package/test/clips.test.ts +30 -1
- package/test/data-motion.test.ts +14 -1
- package/test/fasten-geometry.test.ts +13 -2
- package/test/joint-frames.test.ts +12 -2
- package/test/links.test.ts +24 -0
- package/test/material.test.ts +15 -1
- package/test/maturity.test.ts +19 -1
- package/test/palette-icons.test.ts +2 -1
- package/test/pipe-panel.test.ts +33 -0
- package/test/refusals.test.ts +29 -0
- package/test/revision.test.ts +1 -1
- package/test/section-editor.test.ts +17 -1
- package/translations/en.json +86 -3
- package/translations/es.json +86 -3
- package/translations/ja.json +86 -3
- package/translations/ko.json +86 -3
- package/translations/ms.json +86 -3
- package/translations/zh.json +86 -3
|
@@ -9,8 +9,10 @@ import type { FigureAsset, FigureDataRow, FigureDataValue, FigureGraphStateInput
|
|
|
9
9
|
import { create, figureLegendSourcesChanged, registerFigure, unregisterFigure } from '@hatiolab/things-scene'
|
|
10
10
|
|
|
11
11
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
12
|
+
import { parametersForClip } from '@things-factory/figure-viewer/dist-client/detail-motion.js'
|
|
13
|
+
import { controlsOf } from '@things-factory/figure-viewer/dist-client/controls.js'
|
|
12
14
|
import { dialShape } from './angle-dial.js'
|
|
13
|
-
import { domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
|
|
15
|
+
import { dataValueAt, domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
|
|
14
16
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
15
17
|
import { authoredSizeOf, bodyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
16
18
|
import { previewSizeWordsOf } from './preview-size.js'
|
|
@@ -43,10 +45,16 @@ interface SceneHandle {
|
|
|
43
45
|
root?: {
|
|
44
46
|
set?: (patch: Record<string, unknown>) => void
|
|
45
47
|
fitCameraToModel?: () => void
|
|
48
|
+
/** The scene's camera presets: the perspective, or a square-on view from one side. */
|
|
49
|
+
setCameraView?: (view: PreviewView) => void
|
|
46
50
|
components?: { set?: (property: string, value: unknown) => void }[]
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
/* Where the preview looks from: the edit canvas's presets, which the preview's still scene does not draw (robot dog 2026-11-03). */
|
|
55
|
+
type PreviewView = 'perspective' | 'front' | 'right' | 'top'
|
|
56
|
+
const PREVIEW_VIEWS: readonly PreviewView[] = ['perspective', 'front', 'right', 'top']
|
|
57
|
+
|
|
50
58
|
@customElement('figure-preview')
|
|
51
59
|
export class FigurePreview extends localize(i18next)(LitElement) {
|
|
52
60
|
static styles = css`
|
|
@@ -148,6 +156,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
148
156
|
cursor: pointer;
|
|
149
157
|
opacity: 0.7;
|
|
150
158
|
}
|
|
159
|
+
div[legend] button[view] {
|
|
160
|
+
padding: 0 6px;
|
|
161
|
+
font: inherit;
|
|
162
|
+
color: inherit;
|
|
163
|
+
background: none;
|
|
164
|
+
border: none;
|
|
165
|
+
border-radius: 4px;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
opacity: 0.7;
|
|
168
|
+
}
|
|
169
|
+
div[legend] span[views] {
|
|
170
|
+
display: flex;
|
|
171
|
+
gap: 2px;
|
|
172
|
+
padding-left: 6px;
|
|
173
|
+
border-left: 1px solid rgba(255, 255, 255, 0.22);
|
|
174
|
+
}
|
|
175
|
+
div[legend] button[view][aria-pressed='true'],
|
|
151
176
|
div[legend] button[way][aria-pressed='true'] {
|
|
152
177
|
opacity: 1;
|
|
153
178
|
background-color: rgba(255, 255, 255, 0.18);
|
|
@@ -198,7 +223,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
198
223
|
div[clips] {
|
|
199
224
|
margin-left: auto;
|
|
200
225
|
display: grid;
|
|
201
|
-
grid-template-columns: auto 20px 96px
|
|
226
|
+
grid-template-columns: auto 20px 96px 44px;
|
|
202
227
|
gap: 6px 8px;
|
|
203
228
|
align-items: center;
|
|
204
229
|
padding: 8px 10px;
|
|
@@ -241,6 +266,20 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
241
266
|
fill: none;
|
|
242
267
|
stroke: rgba(255, 255, 255, 0.35);
|
|
243
268
|
}
|
|
269
|
+
/* Folded, the controls are one button: what they covered -- a leg, say -- can be seen (robot dog 2026-11-03). */
|
|
270
|
+
div[clips] button[fold-controls] {
|
|
271
|
+
justify-self: end;
|
|
272
|
+
padding: 0 4px;
|
|
273
|
+
font: inherit;
|
|
274
|
+
color: inherit;
|
|
275
|
+
background: none;
|
|
276
|
+
border: none;
|
|
277
|
+
cursor: pointer;
|
|
278
|
+
opacity: 0.75;
|
|
279
|
+
}
|
|
280
|
+
div[clips] button[fold-controls]:hover {
|
|
281
|
+
opacity: 1;
|
|
282
|
+
}
|
|
244
283
|
div[clips] button[pose] {
|
|
245
284
|
align-self: stretch;
|
|
246
285
|
padding: 4px 8px;
|
|
@@ -277,6 +316,17 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
277
316
|
grid-column: 3 / -1;
|
|
278
317
|
width: auto;
|
|
279
318
|
}
|
|
319
|
+
/* A control's value, typed where the slider cannot land on it (a step of the range's hundredth, robot dog 2026-11-03). */
|
|
320
|
+
div[clips] input[typed] {
|
|
321
|
+
width: 100%;
|
|
322
|
+
padding: 0 3px;
|
|
323
|
+
text-align: right;
|
|
324
|
+
font-variant-numeric: tabular-nums;
|
|
325
|
+
}
|
|
326
|
+
div[clips] label[sampled] output {
|
|
327
|
+
grid-column: 3 / -1;
|
|
328
|
+
text-align: left;
|
|
329
|
+
}
|
|
280
330
|
div[clips] output {
|
|
281
331
|
font-variant-numeric: tabular-nums;
|
|
282
332
|
text-align: right;
|
|
@@ -335,6 +385,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
335
385
|
@state() private setupPart = ''
|
|
336
386
|
/** One figure stretched by sliders, or the eight ways of growing side by side. */
|
|
337
387
|
@state() private way: 'one' | 'eight' = 'one'
|
|
388
|
+
/** Where the camera looks from; a new scene starts in perspective. */
|
|
389
|
+
@state() private looking: PreviewView = 'perspective'
|
|
390
|
+
/** The posing controls folded to their one button. */
|
|
391
|
+
@state() private controlsFolded = false
|
|
338
392
|
/** The size the one figure is tried at; the authored size when the figure stands up. */
|
|
339
393
|
@state() private trying?: { x: number; y: number; z: number }
|
|
340
394
|
private board = { width: 0, height: 0 }
|
|
@@ -376,6 +430,11 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
376
430
|
${(['one', 'eight'] as const).map(
|
|
377
431
|
way => html`<button way=${way} aria-pressed=${String(this.way === way)} @click=${() => this.chooseWay(way)}>${i18next.t(`figure.label.preview-way-${way}`)}</button>`
|
|
378
432
|
)}
|
|
433
|
+
<span views>
|
|
434
|
+
${PREVIEW_VIEWS.map(
|
|
435
|
+
view => html`<button view=${view} aria-pressed=${String(this.looking === view)} @click=${() => this.lookFrom(view)}>${i18next.t(`figure.label.preview-view-${view}`)}</button>`
|
|
436
|
+
)}
|
|
437
|
+
</span>
|
|
379
438
|
${this.way === 'one' ? nothing : PREVIEW_WAYS.map(way => {
|
|
380
439
|
const clamps = clampsByWay[way.name] ?? []
|
|
381
440
|
return html`<span>
|
|
@@ -397,6 +456,11 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
397
456
|
${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset ? valueRowsOf(asset).length : 0) ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
|
|
398
457
|
}
|
|
399
458
|
|
|
459
|
+
private lookFrom(view: PreviewView) {
|
|
460
|
+
this.scene?.root?.setCameraView?.(view)
|
|
461
|
+
this.looking = view
|
|
462
|
+
}
|
|
463
|
+
|
|
400
464
|
/** The one figure's size: a slider per axis the figure lets change, and what it is at the size tried. */
|
|
401
465
|
private renderSizes(asset: FigureAsset, size: { x: number; y: number; z: number }) {
|
|
402
466
|
const say = (key: string, params?: Record<string, string>) => i18next.t(key, params)
|
|
@@ -485,16 +549,28 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
485
549
|
}
|
|
486
550
|
|
|
487
551
|
private renderDrivers(asset: FigureAsset, controls: FigureGraphStateInput[], clips: string[]) {
|
|
552
|
+
const steps = new Map(controlsOf(asset).map(c => [c.id, c.step]))
|
|
488
553
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
|
|
554
|
+
const fold = html`<button fold-controls aria-expanded=${String(!this.controlsFolded)} @click=${() => (this.controlsFolded = !this.controlsFolded)}>
|
|
555
|
+
${i18next.t(this.controlsFolded ? 'figure.button.controls-unfold' : 'figure.button.controls-fold')}
|
|
556
|
+
</button>`
|
|
557
|
+
if (this.controlsFolded) return html`<div clips folded>${fold}</div>`
|
|
489
558
|
return html`<div clips>
|
|
559
|
+
${fold}
|
|
490
560
|
${changed.length
|
|
491
561
|
? html`<button pose title=${i18next.t('figure.text.pose-as-default-explained')} @click=${() => this.poseAsDefault()}>
|
|
492
562
|
${i18next.t('figure.button.pose-as-default')}
|
|
493
563
|
</button>`
|
|
494
564
|
: nothing}
|
|
565
|
+
${Object.keys(this.giving).length
|
|
566
|
+
? html`<button clear-tried title=${i18next.t('figure.text.clear-tried-explained')} @click=${() => this.clearTried()}>
|
|
567
|
+
${i18next.t('figure.button.clear-tried')}
|
|
568
|
+
</button>`
|
|
569
|
+
: nothing}
|
|
495
570
|
${controls.map(control => {
|
|
496
571
|
const value = this.giving[control.id] ?? asset.stateDefaults[control.id] ?? control.min
|
|
497
|
-
|
|
572
|
+
/* The step the editor's controls take (\`controlsOf\`): a degree for an angle, a hundredth of the range otherwise. */
|
|
573
|
+
const step = steps.get(control.id) ?? (control.max - control.min) / 100
|
|
498
574
|
const label = asset.stateInputs?.[control.id]?.label ?? control.id
|
|
499
575
|
return html`<label>
|
|
500
576
|
<span>${label} <span drive>${unitWord(control.unit)}</span></span>
|
|
@@ -507,7 +583,19 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
507
583
|
.value=${String(value)}
|
|
508
584
|
@input=${(e: Event) => this.give(control.id, Number((e.target as HTMLInputElement).value))}
|
|
509
585
|
/>
|
|
510
|
-
<
|
|
586
|
+
<input
|
|
587
|
+
typed
|
|
588
|
+
type="number"
|
|
589
|
+
step="any"
|
|
590
|
+
min=${control.min}
|
|
591
|
+
max=${control.max}
|
|
592
|
+
aria-label=${label}
|
|
593
|
+
.value=${String(Math.round(value * 100) / 100)}
|
|
594
|
+
@change=${(e: Event) => {
|
|
595
|
+
const typed = (e.target as HTMLInputElement).valueAsNumber
|
|
596
|
+
if (Number.isFinite(typed)) this.give(control.id, Math.min(Math.max(typed, control.min), control.max))
|
|
597
|
+
}}
|
|
598
|
+
/>
|
|
511
599
|
</label>`
|
|
512
600
|
})}
|
|
513
601
|
${clips.map(clip => {
|
|
@@ -552,6 +640,16 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
552
640
|
private renderDataValue(row: FigureDataRow) {
|
|
553
641
|
const node = row.node
|
|
554
642
|
const label = row.label ?? row.path
|
|
643
|
+
/*
|
|
644
|
+
The data tab's sample, when it gives the row, is what the figure shows: a value given here is not read. Said, and
|
|
645
|
+
the field not offered, rather than one that changes nothing (robot dog 2026-11-03).
|
|
646
|
+
*/
|
|
647
|
+
const sampled = this.sample === null || this.sample === undefined ? undefined : dataValueAt(this.sample, row.path)
|
|
648
|
+
if (sampled !== undefined)
|
|
649
|
+
return html`<label sampled title=${i18next.t('figure.text.data-value-from-sample')}>
|
|
650
|
+
<span>${label} <span drive>${i18next.t('figure.label.data-value-from-sample')}</span></span>
|
|
651
|
+
<output>${typeof sampled === 'object' ? JSON.stringify(sampled) : String(sampled)}</output>
|
|
652
|
+
</label>`
|
|
555
653
|
const fallback = 'default' in node ? node.default : undefined
|
|
556
654
|
const value = this.valuing[row.path] ?? fallback
|
|
557
655
|
if (node.kind === 'number') {
|
|
@@ -591,6 +689,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
591
689
|
|
|
592
690
|
private move(group: string, playing: { clip: string; speed: number }) {
|
|
593
691
|
this.moving = { ...this.moving, [group]: playing }
|
|
692
|
+
this.release(playing.clip)
|
|
594
693
|
this.tellInstances('motion', Object.fromEntries(Object.entries(this.moving).map(([id, m]) => [id, { ...m, paused: false }])))
|
|
595
694
|
}
|
|
596
695
|
|
|
@@ -607,9 +706,30 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
607
706
|
|
|
608
707
|
private drive(clip: string, value: number) {
|
|
609
708
|
this.driving = { ...this.driving, [clip]: value }
|
|
709
|
+
if (value) this.release(clip)
|
|
610
710
|
this.tellInstances('clips', this.driving)
|
|
611
711
|
}
|
|
612
712
|
|
|
713
|
+
/*
|
|
714
|
+
A value given here wins over the clips for its control (things-scene's \`parameters\`), so a value left from trying held
|
|
715
|
+
the control still while a clip played (robot dog, 2026-11-03). A clip played lets go of the controls it drives, as
|
|
716
|
+
the detail view does (\`parametersForClip\`).
|
|
717
|
+
*/
|
|
718
|
+
private release(clip: string) {
|
|
719
|
+
const asset = this.asset
|
|
720
|
+
if (!asset) return
|
|
721
|
+
const next = parametersForClip(asset, this.giving, clip)
|
|
722
|
+
if (Object.keys(next).length === Object.keys(this.giving).length) return
|
|
723
|
+
this.giving = next
|
|
724
|
+
this.tellInstances('parameters', this.giving)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Every value tried here let go: the figure stands in its default pose, and its clips move it again. Nothing is saved. */
|
|
728
|
+
private clearTried() {
|
|
729
|
+
this.giving = {}
|
|
730
|
+
this.tellInstances('parameters', this.giving)
|
|
731
|
+
}
|
|
732
|
+
|
|
613
733
|
private give(id: string, value: number) {
|
|
614
734
|
this.giving = { ...this.giving, [id]: value }
|
|
615
735
|
this.tellInstances('parameters', this.giving)
|
|
@@ -757,6 +877,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
757
877
|
mode: 0
|
|
758
878
|
}) as SceneHandle
|
|
759
879
|
this.standing = signature
|
|
880
|
+
this.looking = 'perspective'
|
|
760
881
|
this.scene.resize?.()
|
|
761
882
|
this.scene.fit?.('ratio')
|
|
762
883
|
this.scene.root?.fitCameraToModel?.()
|
|
@@ -164,13 +164,16 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
164
164
|
min-width: 0;
|
|
165
165
|
font: inherit;
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
/* A drawn section's end to one point, a rectangle's top another size: a choice over the whole row. */
|
|
168
|
+
label[apex],
|
|
169
|
+
label[top] {
|
|
168
170
|
display: flex;
|
|
169
171
|
align-items: center;
|
|
170
172
|
gap: 6px;
|
|
171
173
|
grid-column: 1 / -1;
|
|
172
174
|
}
|
|
173
|
-
label[apex] small
|
|
175
|
+
label[apex] small,
|
|
176
|
+
label[top] small {
|
|
174
177
|
flex-basis: 100%;
|
|
175
178
|
}
|
|
176
179
|
ol[points] {
|
|
@@ -286,7 +289,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
286
289
|
if (!this.narrow) this.show = 'outline'
|
|
287
290
|
})
|
|
288
291
|
|
|
289
|
-
@state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null, cavity: null }
|
|
292
|
+
@state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null, top: null, cavity: null }
|
|
290
293
|
/** Walls one per side in the inside's drawing; on when they open differing. */
|
|
291
294
|
@state() private perSide = false
|
|
292
295
|
@state() private selected: string | null = null
|
|
@@ -739,6 +742,35 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
739
742
|
: nothing}`
|
|
740
743
|
}
|
|
741
744
|
|
|
745
|
+
/**
|
|
746
|
+
* A rectangle's top another size (ADR-0100 §16.1: a tapered box -- a thigh's cover, a hopper's mouth): on, its width
|
|
747
|
+
* and depth there and one corner radius; off, its top the section again. A box with an inside is not tapered.
|
|
748
|
+
*/
|
|
749
|
+
private topFields() {
|
|
750
|
+
const [h, v] = SCREEN[this.axis]
|
|
751
|
+
const top = this.working.top
|
|
752
|
+
const set = (patch: Partial<NonNullable<SectionWorking['top']>>) => this.change({ ...this.working, top: { ...top!, ...patch } })
|
|
753
|
+
return html`<label top>
|
|
754
|
+
<input
|
|
755
|
+
type="checkbox"
|
|
756
|
+
.checked=${live(!!top)}
|
|
757
|
+
?disabled=${!!this.working.cavity}
|
|
758
|
+
@change=${(ev: Event) => {
|
|
759
|
+
const on = (ev.target as HTMLInputElement).checked
|
|
760
|
+
this.change({ ...this.working, top: on ? (this.opened.top ?? { across: { ...this.working.across }, corner: 0 }) : null })
|
|
761
|
+
}}
|
|
762
|
+
/>
|
|
763
|
+
<span>${i18next.t('figure.label.section-editor-top')}</span>
|
|
764
|
+
${this.working.cavity ? html`<small>${i18next.t('figure.text.section-editor-top-no-inside')}</small>` : nothing}
|
|
765
|
+
</label>
|
|
766
|
+
${top
|
|
767
|
+
? html`${([h, v] as const).map(axis =>
|
|
768
|
+
this.field(i18next.t('figure.label.section-editor-top-across', { axis: axis.toUpperCase() }), top.across[axis] ?? 0, value => set({ across: { ...top.across, [axis]: value } }), false, "")
|
|
769
|
+
)}
|
|
770
|
+
${this.field(i18next.t('figure.label.section-editor-top-corner'), top.corner, value => set({ corner: value }), false, "")}`
|
|
771
|
+
: nothing}`
|
|
772
|
+
}
|
|
773
|
+
|
|
742
774
|
/** The inside, as the section panel shows it, on the working section: on or off, what it makes, its drawing. */
|
|
743
775
|
private cavityBlock(invalid: boolean) {
|
|
744
776
|
const w = this.working
|
|
@@ -821,6 +853,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
821
853
|
this.noteWords(axis)
|
|
822
854
|
)
|
|
823
855
|
)}
|
|
856
|
+
${this.kind === 'rectangle' ? this.topFields() : nothing}
|
|
824
857
|
</div>
|
|
825
858
|
${this.kind === 'polygon'
|
|
826
859
|
? html`<ol points aria-label=${i18next.t('figure.label.section-editor-points')}>
|
package/client/modeller/links.ts
CHANGED
|
@@ -607,6 +607,12 @@ export interface MotionForm {
|
|
|
607
607
|
*/
|
|
608
608
|
followSum?: Array<{ joint: string; multiplier: number }>
|
|
609
609
|
travelSum?: Extract<NonNullable<FigureJointMotion['travel']>, { sum: unknown }>
|
|
610
|
+
/**
|
|
611
|
+
* The point a turn passes (ADR-0159 \`through\`), in millimetres from the joint frame's origin along its axes: the
|
|
612
|
+
* axis moves there, the part does not. A distance that follows a length is said, not edited, and written back.
|
|
613
|
+
*/
|
|
614
|
+
through?: Partial<Record<Axis, number>>
|
|
615
|
+
throughFollows?: NonNullable<FigureJointMotion['through']>
|
|
610
616
|
}
|
|
611
617
|
|
|
612
618
|
/**
|
|
@@ -644,10 +650,26 @@ export function motionFormOf(joint: FigureJoint, k = 0): MotionForm {
|
|
|
644
650
|
travel,
|
|
645
651
|
travelPlus: one?.plus ?? 0,
|
|
646
652
|
...(terms && !single ? { followSum: terms.sum } : {}),
|
|
647
|
-
...(travelSum ? { travelSum } : {})
|
|
653
|
+
...(travelSum ? { travelSum } : {}),
|
|
654
|
+
...throughFormOf(a?.through)
|
|
648
655
|
}
|
|
649
656
|
}
|
|
650
657
|
|
|
658
|
+
/** The form's two halves of an axis's point: the millimetres typed, and the distances that follow a length, kept. */
|
|
659
|
+
function throughFormOf(through: FigureJointMotion['through']): Pick<MotionForm, 'through' | 'throughFollows'> {
|
|
660
|
+
if (!through) return {}
|
|
661
|
+
const typed = Object.fromEntries(Object.entries(through).filter(([, d]) => typeof d === 'number')) as Partial<Record<Axis, number>>
|
|
662
|
+
const follows = Object.fromEntries(Object.entries(through).filter(([, d]) => typeof d !== 'number'))
|
|
663
|
+
return { ...(Object.keys(typed).length ? { through: typed } : {}), ...(Object.keys(follows).length ? { throughFollows: follows } : {}) }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** The axis's point as the record keeps it: the kept distances, the typed ones over them, 0 left out. */
|
|
667
|
+
function throughOf(m: MotionForm): FigureJointMotion['through'] {
|
|
668
|
+
const typed = Object.fromEntries(Object.entries(m.through ?? {}).filter(([, d]) => d))
|
|
669
|
+
const through = { ...(m.throughFollows ?? {}), ...typed }
|
|
670
|
+
return Object.keys(through).length ? through : undefined
|
|
671
|
+
}
|
|
672
|
+
|
|
651
673
|
/**
|
|
652
674
|
* The axes a part's joint can move with (URDF's mimic): one of another part's that moves the same way — a slide follows
|
|
653
675
|
* a slide, a turn a turn — and has a control of its own, since a follower follows the axis that is driven (ADR-0159).
|
|
@@ -675,7 +697,7 @@ export function motionJoint(joint: FigureJoint, m: MotionForm, part: string, k =
|
|
|
675
697
|
...(was?.id !== undefined ? { id: was.id } : {}),
|
|
676
698
|
type: m.kind === 'slide' ? ('prismatic' as const) : ('revolute' as const),
|
|
677
699
|
axis: (m.axisVector ?? m.axis.toUpperCase()) as FigureJointMotion['axis'],
|
|
678
|
-
...(
|
|
700
|
+
...(throughOf(m) ? { through: throughOf(m) } : {})
|
|
679
701
|
}
|
|
680
702
|
const axes = joint.motion ?? []
|
|
681
703
|
const given = (motion: Omit<FigureJointMotion, 'type' | 'axis'>) => figureJointWithMotion(joint, [...axes.slice(0, k), { ...moving, ...motion }, ...axes.slice(k + 1)])
|
|
@@ -10,6 +10,15 @@ export function offeredPaletteOf(app: Record<string, string>, asset: Record<stri
|
|
|
10
10
|
return { ...app, ...asset }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A colour of the part's own, picked by its value (#ffd400) rather than a palette role: named by that value, so the same
|
|
15
|
+
* colour picked twice is one entry of the asset's palette. A role follows the board's theme; this colour does not.
|
|
16
|
+
*/
|
|
17
|
+
export function ownColourOf(hex: string): { token: string; colour: string } {
|
|
18
|
+
const colour = hex.toLowerCase()
|
|
19
|
+
return { token: `colour.${colour.slice(1)}`, colour }
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/** The glow at an intensity: 0 is no glow at all; a new glow takes the part's own colour until another is picked. */
|
|
14
23
|
export function glowOf(material: FigureMaterial, intensity: number): Partial<FigureMaterial> {
|
|
15
24
|
return { emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined }
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
figureGradeOf,
|
|
10
10
|
figureMaterialKeyOf,
|
|
11
11
|
figureScoreOf,
|
|
12
|
+
figurePartBudgetOf,
|
|
12
13
|
figureWorldBoxesOf,
|
|
13
14
|
figureWritersOf
|
|
14
15
|
} from '@hatiolab/figure-model'
|
|
@@ -38,9 +39,11 @@ import type { FigureCapacity } from './scene-budget.js'
|
|
|
38
39
|
*
|
|
39
40
|
* ## Budgets does not have
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
-
* - off-palette surfaces: the material has no surface field, so there is nothing to count
|
|
43
|
-
*
|
|
42
|
+
* One budget is not measured here, and is not shown as zero:
|
|
43
|
+
* - off-palette surfaces: the material has no surface field, so there is nothing to count.
|
|
44
|
+
*
|
|
45
|
+
* The part count against the detail level's limit is measured once the figure declares a level (the level returned,
|
|
46
|
+
* user 2026-09-24, ADR-0087); a figure without one says so.
|
|
44
47
|
*/
|
|
45
48
|
|
|
46
49
|
/*
|
|
@@ -135,7 +138,7 @@ export function figureReportOf(asset: FigureAsset): FigureReport | undefined {
|
|
|
135
138
|
reading: String(reuse.saved),
|
|
136
139
|
because: 'figure.text.maturity-reuse-because'
|
|
137
140
|
},
|
|
138
|
-
|
|
141
|
+
detail(asset),
|
|
139
142
|
{ key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' },
|
|
140
143
|
readable(drawn, writers)
|
|
141
144
|
]
|
|
@@ -158,6 +161,14 @@ export function figureReportOf(asset: FigureAsset): FigureReport | undefined {
|
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
/** The parts used of what the declared detail level allows (`figurePartBudgetOf`); unmeasured when no level is declared. */
|
|
165
|
+
function detail(asset: FigureAsset): FigureMaturityAxis {
|
|
166
|
+
const budget = figurePartBudgetOf(asset)
|
|
167
|
+
if (asset.detailLevel === 'UNLIMITED') return { key: 'detail', blocked: 'figure.text.maturity-blocked-unlimited' }
|
|
168
|
+
if (!budget) return { key: 'detail', blocked: 'figure.text.maturity-blocked-no-detail-level' }
|
|
169
|
+
return { key: 'detail', ratio: Math.min(1, budget.used / budget.limit), reading: `${budget.used}/${budget.limit}`, because: 'figure.text.maturity-detail-because' }
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
/**
|
|
162
173
|
* The key things-scene shares a body's geometry by (`figure-geometry.ts` `keyOf`): the operation, what is not in its
|
|
163
174
|
* numbers (the axis, the kind of section, which ends of an inside are open), its numbers to a micrometre, and segments
|
|
@@ -43,6 +43,15 @@ export function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimit
|
|
|
43
43
|
],
|
|
44
44
|
apex: { x: 0, z: 0 }
|
|
45
45
|
}
|
|
46
|
+
case 'pipe':
|
|
47
|
+
/* An elbow: up, a quarter bend toward +X, along. Its section and pieces are changed in its own panel. */
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
position: { x: 0, y: 0, z: 0 },
|
|
51
|
+
kind: 'pipe',
|
|
52
|
+
dimensions: { radius: 25, wall: 3 },
|
|
53
|
+
path: [{ line: 200 }, { bend: { radius: 100, angle: 90, toward: 90 } }, { line: 200 }]
|
|
54
|
+
}
|
|
46
55
|
case 'plate':
|
|
47
56
|
return { ...standing(10), kind: 'box', dimensions: { width: 600, height: 10, depth: 400 } }
|
|
48
57
|
case 'wall':
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { figureSweepTurnOf, type FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A pipe's path as its panel says it (ADR-0100 §16.2): its pieces as set-pipe takes them, and each bend's direction as
|
|
5
|
+
* the figure's axis it turns to there. Kept apart from the element so the reading is tested without a browser.
|
|
6
|
+
*/
|
|
7
|
+
/** The four quarter directions a bend is offered toward: across the section, from its first axis to its second. */
|
|
8
|
+
export const QUARTERS = [0, 90, 180, 270]
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Which way bend `piece` turns were it `toward` degrees, as the screen says it -- `−X` -- or nothing between the axes:
|
|
12
|
+
* figure-model's reading (`figureSweepTurnOf`), the one a model revising the pipe is told too.
|
|
13
|
+
*/
|
|
14
|
+
export const bendWordOf = (sweep: NonNullable<FigureEvaluatedShape['sweep']>, piece: number, toward: number): string =>
|
|
15
|
+
(figureSweepTurnOf(sweep, piece, toward) ?? '').replace('-', '−')
|
|
16
|
+
|
|
17
|
+
/** A path as `set-pipe` takes it, from the evaluated one. */
|
|
18
|
+
export type PanelPiece = { line: number } | { bend: { radius: number; angle: number; toward: number } }
|
|
19
|
+
export const piecesOf = (sweep: NonNullable<FigureEvaluatedShape['sweep']>): PanelPiece[] =>
|
|
20
|
+
sweep.path.map(p => (p.kind === 'line' ? { line: p.length } : { bend: { radius: p.radius, angle: p.angle, toward: p.toward } }))
|
|
21
|
+
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* English is for logs and readers of the kernel, and a screen that read it went quiet whenever a sentence changed or a
|
|
8
8
|
* new one came under the same code (ADR-0054). A refusal with no details the screen knows is said with its code.
|
|
9
9
|
*/
|
|
10
|
-
import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureClipLoss, type FigureRefusalDetails, type FigureSizeRangeFit } from '@hatiolab/figure-model'
|
|
10
|
+
import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureAsset, type FigureClipLoss, type FigureRefusalDetails, type FigureSizeRangeFit } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
type Say = (key: string, params: Record<string, string>) => string
|
|
13
13
|
type Reason = FigureRefusalDetails['reason']
|
|
@@ -48,6 +48,25 @@ export function sizeRangeFitsOf(fits: readonly FigureSizeRangeFit[], say: Say):
|
|
|
48
48
|
.join(' ')
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Default poses an edit moved because their range no longer held them (a joint's range narrowed or turned the other
|
|
53
|
+
* way, 2026-11-03): each control, from what to what, in one line. A default the author set inside the range is not
|
|
54
|
+
* said; nothing moves in silence (a default that changed unseen, proposal 17).
|
|
55
|
+
*/
|
|
56
|
+
export function defaultsMovedOf(before: FigureAsset | undefined, after: FigureAsset | undefined, say: Say): string {
|
|
57
|
+
if (!before || !after) return ''
|
|
58
|
+
return after.document.model.inputs
|
|
59
|
+
.filter(i => i.role === 'state')
|
|
60
|
+
.flatMap(i => {
|
|
61
|
+
const from = before.stateDefaults[i.id],
|
|
62
|
+
to = after.stateDefaults[i.id]
|
|
63
|
+
if (from === undefined || to === undefined || from === to) return []
|
|
64
|
+
if ((i.min === undefined || from >= i.min) && (i.max === undefined || from <= i.max)) return []
|
|
65
|
+
return [say('figure.text.default-moved-to-end', { control: after.stateInputs?.[i.id]?.label ?? i.id, from: numberOf(from), to: numberOf(to) })]
|
|
66
|
+
})
|
|
67
|
+
.join(' ')
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
/**
|
|
52
71
|
* A placement refused because a part is fastened to the mounting plane in a way the new plane does not take (standing
|
|
53
72
|
* on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
|
|
@@ -99,6 +118,36 @@ export function placedRefusalOf(error: unknown, part: string, say: Say): string
|
|
|
99
118
|
return null
|
|
100
119
|
}
|
|
101
120
|
|
|
121
|
+
/** A part the edit cannot move because a joint or a fastening places it, named by the part the kernel gave. */
|
|
122
|
+
function placedByRefusalOf(error: unknown, say: Say): string | null {
|
|
123
|
+
const part = (detailsOf(error, 'placed-by-joint') ?? detailsOf(error, 'placed-by-fastening'))?.part
|
|
124
|
+
return part === undefined ? null : placedRefusalOf(error, part, say)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A mirrored copy not made (figure-model's mirror-part): why, for the part it was asked of. */
|
|
128
|
+
const MIRROR_REFUSALS = {
|
|
129
|
+
'mirror-on-plane': 'figure.text.mirror-would-overlap',
|
|
130
|
+
'mirror-not-symmetric': 'figure.text.mirror-not-symmetric',
|
|
131
|
+
'mirror-turned-parent': 'figure.text.mirror-turned-parent',
|
|
132
|
+
'mirror-surface': 'figure.text.mirror-surface',
|
|
133
|
+
'mirror-unplaced': 'figure.text.mirror-unplaced',
|
|
134
|
+
'mirror-unmeasured': 'figure.text.mirror-unplaced',
|
|
135
|
+
'mirror-keep-needs-zero': 'figure.text.mirror-keep-needs-zero'
|
|
136
|
+
} as const
|
|
137
|
+
function mirrorRefusalOf(error: unknown, say: Say): string | null {
|
|
138
|
+
for (const [reason, key] of Object.entries(MIRROR_REFUSALS)) {
|
|
139
|
+
const found = detailsOf(error, reason as keyof typeof MIRROR_REFUSALS)
|
|
140
|
+
if (found) return say(key, { part: found.part })
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A value the edit names that the figure does not declare: the value, by its id (the refusal's path). */
|
|
146
|
+
function unknownInputRefusalOf(error: unknown, say: Say): string | null {
|
|
147
|
+
if (codeOf(error) !== 'UNKNOWN_INPUT') return null
|
|
148
|
+
return say('figure.text.unknown-input', { input: (error as { path?: string })?.path ?? '' })
|
|
149
|
+
}
|
|
150
|
+
|
|
102
151
|
/** A part name refused: not a name, already used, or another part is named under the part. The rule is the kernel's. */
|
|
103
152
|
export function partNameRefusalOf(error: unknown, say: Say): string | null {
|
|
104
153
|
/* A new part named as one already is: the same sentence as a part renamed so. */
|
|
@@ -222,6 +271,18 @@ export function sessionRefusalOf(error: unknown, say: Say): string | null {
|
|
|
222
271
|
return key ? say(key, {}) : null
|
|
223
272
|
}
|
|
224
273
|
|
|
274
|
+
/**
|
|
275
|
+
* A pipe refused (ADR-0100 §16.2): a bend tighter than its section -- with the least radius it takes --, a wall that
|
|
276
|
+
* leaves no inside, a path through itself. Nothing for any other refusal.
|
|
277
|
+
*/
|
|
278
|
+
export function pipeRefusalOf(error: unknown, say: Say): string | null {
|
|
279
|
+
const tight = detailsOf(error, 'sweep-bend-tight')
|
|
280
|
+
if (tight) return say('figure.text.pipe-bend-tight', { piece: String(tight.piece + 1), least: String(Math.ceil(tight.least)) })
|
|
281
|
+
if (detailsOf(error, 'sweep-wall')) return say('figure.text.pipe-wall-too-thick', {})
|
|
282
|
+
if (detailsOf(error, 'sweep-self-crossing')) return say('figure.text.pipe-self-crossing', {})
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
|
|
225
286
|
export function editRefusalOf(error: unknown, say: Say): string {
|
|
226
287
|
return (
|
|
227
288
|
sessionRefusalOf(error, say) ??
|
|
@@ -230,9 +291,13 @@ export function editRefusalOf(error: unknown, say: Say): string {
|
|
|
230
291
|
repeatPitchRefusalOf(error, say) ??
|
|
231
292
|
rowLimitRefusalOf(error, say) ??
|
|
232
293
|
placementRefusalOf(error, say) ??
|
|
294
|
+
placedByRefusalOf(error, say) ??
|
|
295
|
+
mirrorRefusalOf(error, say) ??
|
|
296
|
+
unknownInputRefusalOf(error, say) ??
|
|
233
297
|
partNameRefusalOf(error, say) ??
|
|
234
298
|
documentLimitRefusalOf(error, say) ??
|
|
235
299
|
sectionRefusalOf(error, say) ??
|
|
300
|
+
pipeRefusalOf(error, say) ??
|
|
236
301
|
slotLayoutRefusalOf(error, say) ??
|
|
237
302
|
dataMotionRefusalOf(error, say) ??
|
|
238
303
|
say('figure.text.edit-refused', { code: codeOf(error) ?? '' })
|
|
@@ -53,7 +53,8 @@ export function cavityRequestOf(shape: FigureEvaluatedShape, patch: Partial<Figu
|
|
|
53
53
|
|
|
54
54
|
/** The most a rectangle's corners can reach: half its narrower side (the kernel's CORNER_TOO_LARGE says where). */
|
|
55
55
|
export function cornerLimitOf(shape: FigureEvaluatedShape): number | undefined {
|
|
56
|
-
|
|
56
|
+
/* A tapered box's bottom: its corners are its first section's (ADR-0100 §16.1). */
|
|
57
|
+
const e = shape.extrusion ?? (shape.loft?.to ? shape.loft : undefined)
|
|
57
58
|
if (e?.section.kind !== 'rectangle') return undefined
|
|
58
59
|
const [u, v] = FIGURE_SECTION_PLANE[e.axis]
|
|
59
60
|
const far = 2 * Math.max(...acrossOf(e))
|
|
@@ -106,6 +106,8 @@ export interface SectionWorking {
|
|
|
106
106
|
length: number
|
|
107
107
|
/** A drawn section's end drawn to one point (a pyramid), across the plane axes; null for a push. */
|
|
108
108
|
apex: Partial<Record<Axis, number>> | null
|
|
109
|
+
/** A rectangle's top another size across and one corner radius: a tapered box (ADR-0100 §16.1); null for a push. */
|
|
110
|
+
top: { across: Partial<Record<Axis, number>>; corner: number } | null
|
|
109
111
|
/** A pushed rectangle's or ellipse's inside, every side's wall written out; null for none or a drawn section. */
|
|
110
112
|
cavity: { wall: Record<string, number>; start: FigureCavityRequest['start']; end: FigureCavityRequest['end'] } | null
|
|
111
113
|
}
|
|
@@ -115,7 +117,7 @@ export function drawnOf(shape: FigureEvaluatedShape):
|
|
|
115
117
|
| { axis: Axis; section: FigureSection; start: number; end: number; apex: Partial<Record<Axis, number>> | null }
|
|
116
118
|
| undefined {
|
|
117
119
|
if (shape.extrusion) return { ...shape.extrusion, apex: null }
|
|
118
|
-
if (shape.loft) return { ...shape.loft, apex: shape.loft.apex }
|
|
120
|
+
if (shape.loft) return { ...shape.loft, apex: shape.loft.apex ?? null }
|
|
119
121
|
return undefined
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -143,10 +145,19 @@ export function workingOf(shape: FigureEvaluatedShape): SectionWorking {
|
|
|
143
145
|
across,
|
|
144
146
|
length: e.end - e.start,
|
|
145
147
|
apex: e.apex ? { ...e.apex } : null,
|
|
148
|
+
top: topOf(shape),
|
|
146
149
|
cavity: c ? { wall: { ...c.wall }, start: c.start, end: c.end } : null
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
/** A tapered box's top as the editor holds it: its size across and its one corner radius. */
|
|
154
|
+
function topOf(shape: FigureEvaluatedShape): SectionWorking['top'] {
|
|
155
|
+
const t = shape.loft?.to
|
|
156
|
+
if (t?.kind !== 'rectangle') return null
|
|
157
|
+
const [u, v] = FIGURE_SECTION_PLANE[shape.loft!.axis]
|
|
158
|
+
return { across: { [u]: t.size[u]!, [v]: t.size[v]! }, corner: t.corner?.[u] ?? 0 }
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
/**
|
|
151
162
|
* The part as the working state would make it: its section, its length from the same start, its inside -- what the
|
|
152
163
|
* inside's drawing reads, so its walls are judged against the widths being typed and not the ones the editor opened with.
|
|
@@ -182,6 +193,12 @@ export function workingIssuesOf(
|
|
|
182
193
|
const words = issues.map(i => issueWords(i, w.points, e.axis))
|
|
183
194
|
if (!(Number.isFinite(w.length) && w.length > 0)) words.push({ key: 'figure.text.section-issue-length', params: {} })
|
|
184
195
|
if (w.apex && !Object.values(w.apex).every(n => Number.isFinite(n))) words.push({ key: 'figure.text.section-issue-number', params: {} })
|
|
196
|
+
if (w.top) {
|
|
197
|
+
/* The top is a section of its own, checked by the same rule (ADR-0100 §16.1). */
|
|
198
|
+
const [u, v] = FIGURE_SECTION_PLANE[e.axis]
|
|
199
|
+
const top: FigureSection = { kind: 'rectangle', size: { [u]: w.top.across[u]!, [v]: w.top.across[v]! }, ...(w.top.corner > 0 ? { corner: { [u]: w.top.corner, [v]: w.top.corner } } : {}) }
|
|
200
|
+
if (checkFigureSection(top, e.axis).length || !(w.top.corner >= 0)) words.push({ key: 'figure.text.section-issue-top', params: {} })
|
|
201
|
+
}
|
|
185
202
|
if (!issues.length && words.length === 0 && w.cavity) {
|
|
186
203
|
const made = figureExtrusionFeaturesOf({ axis: e.axis, section, start: e.start, end: e.start + w.length }, [{ id: 'inside', ...w.cavity }])
|
|
187
204
|
if ('refused' in made) {
|
|
@@ -205,6 +222,7 @@ export function sectionActionOf(part: string, kind: FigureSection['kind'] | unde
|
|
|
205
222
|
const across = Object.fromEntries(Object.entries(w.across).filter(([k, v]) => changed(v, opened.across[k as Axis])))
|
|
206
223
|
const apexMoved = !w.apex !== !opened.apex || (!!w.apex && Object.entries(w.apex).some(([k, v]) => changed(v, opened.apex![k as Axis])))
|
|
207
224
|
const cavityMoved = JSON.stringify(cavityKeyOf(w)) !== JSON.stringify(cavityKeyOf(opened))
|
|
225
|
+
const topMoved = JSON.stringify(w.top) !== JSON.stringify(opened.top)
|
|
208
226
|
return {
|
|
209
227
|
kind: 'set-section',
|
|
210
228
|
part,
|
|
@@ -212,6 +230,7 @@ export function sectionActionOf(part: string, kind: FigureSection['kind'] | unde
|
|
|
212
230
|
...(Object.keys(across).length ? { across } : {}),
|
|
213
231
|
...(changed(w.length, opened.length) ? { length: w.length } : {}),
|
|
214
232
|
...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {}),
|
|
233
|
+
...(topMoved ? { top: w.top ? { across: { ...w.top.across }, corner: w.top.corner } : null } : {}),
|
|
215
234
|
...(cavityMoved ? { cavity: w.cavity ? { wall: { ...w.cavity.wall }, start: w.cavity.start, end: w.cavity.end } : null } : {})
|
|
216
235
|
}
|
|
217
236
|
}
|