@things-factory/figure-ui 10.1.128 → 10.1.141
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/align.ts +24 -18
- package/client/modeller/editor.ts +165 -253
- package/client/modeller/fasten-geometry.ts +12 -9
- package/client/modeller/figure-facts.ts +12 -14
- package/client/modeller/figure-links.ts +16 -12
- package/client/modeller/figure-preview.ts +13 -4
- package/client/modeller/figure-properties.ts +6 -5
- package/client/modeller/figure-verdict.ts +14 -8
- package/client/modeller/joint-fan.ts +15 -17
- package/client/modeller/links.ts +17 -23
- package/client/modeller/live.ts +6 -3
- package/client/modeller/motion-ends.ts +6 -7
- package/client/modeller/occupancy.ts +2 -3
- package/client/modeller/product-session.ts +15 -0
- package/client/modeller/revision.ts +66 -0
- package/client/pages/figure-modeller-page.ts +5 -18
- package/dist-client/modeller/align.d.ts +12 -7
- package/dist-client/modeller/align.js +21 -17
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +8 -34
- package/dist-client/modeller/editor.js +175 -250
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +11 -9
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-facts.d.ts +9 -1
- package/dist-client/modeller/figure-facts.js +11 -15
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -5
- package/dist-client/modeller/figure-links.js +14 -13
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +5 -2
- package/dist-client/modeller/figure-preview.js +13 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +3 -2
- package/dist-client/modeller/figure-properties.js +6 -6
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +4 -2
- package/dist-client/modeller/figure-verdict.js +12 -9
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/joint-fan.d.ts +8 -4
- package/dist-client/modeller/joint-fan.js +15 -16
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -5
- package/dist-client/modeller/links.js +17 -24
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +3 -2
- package/dist-client/modeller/live.js +7 -3
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +2 -2
- package/dist-client/modeller/motion-ends.js +5 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +2 -2
- package/dist-client/modeller/occupancy.js +2 -3
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +44 -39
- package/dist-client/modeller/product-session.js +17 -0
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/revision.d.ts +37 -0
- package/dist-client/modeller/revision.js +39 -0
- package/dist-client/modeller/revision.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +0 -6
- package/dist-client/pages/figure-modeller-page.js +6 -14
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +14 -12
- package/test/browser/modeller-page.js +52 -4
- package/test/browser/modeller-revision.check.mjs +144 -0
- package/test/command-preview.test.ts +9 -8
- package/test/fasten-drawing.test.ts +1 -1
- package/test/fasten-geometry.test.ts +10 -9
- package/test/fields.test.ts +1 -1
- package/test/first-minute.test.ts +1 -1
- package/test/gizmo.test.ts +1 -1
- package/test/joint-fan.test.ts +30 -14
- package/test/joint-frames.test.ts +9 -8
- package/test/links.test.ts +13 -12
- package/test/live.test.ts +3 -3
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +6 -5
- package/test/release-panel.test.ts +3 -3
- package/test/resize-with-placement.test.ts +8 -6
- package/test/revision-of.ts +22 -0
- package/test/revision.test.ts +172 -0
- package/test/section-editor.test.ts +4 -3
- package/test/section-panel.test.ts +5 -4
- package/test/verdict.test.ts +1 -1
- package/client/modeller/evaluated.ts +0 -47
- package/dist-client/modeller/evaluated.d.ts +0 -12
- package/dist-client/modeller/evaluated.js +0 -42
- package/dist-client/modeller/evaluated.js.map +0 -1
- package/dist-client/tsconfig.scratch.tsbuildinfo +0 -1
- package/test/evaluated.test.ts +0 -40
- package/test/mode-switch.test.ts +0 -43
|
@@ -6,7 +6,7 @@ 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, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, 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
12
|
import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement, FigureFaceMappingNeed, FigureFaceRemap } from '@hatiolab/figure-model'
|
|
@@ -32,7 +32,7 @@ import { keyActionOf } from './keys.js'
|
|
|
32
32
|
import { groupFields, ownerOf, partLabelOf, repeatOf, type FieldLike, type FieldGroup } from './fields.js'
|
|
33
33
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
34
34
|
import { ScrollbarStyles } from '@operato/styles'
|
|
35
|
-
import { alignChanges,
|
|
35
|
+
import { alignChanges, canMirror, mirrorChanges, type AlignTool } from './align.js'
|
|
36
36
|
import type { FigurePaletteKind, FigurePartRow } from './figure-parts.js'
|
|
37
37
|
import { paletteRecipe } from './palette.js'
|
|
38
38
|
import { apexOver, drawnOf } from './section-drawing.js'
|
|
@@ -48,7 +48,7 @@ import type { FigureRenderPart } from './renderer.js'
|
|
|
48
48
|
import type { FigureBudgetLine } from './maturity.js'
|
|
49
49
|
import { endPlaceKeyOf, fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
50
50
|
import { jointFanOf } from './joint-fan.js'
|
|
51
|
-
import {
|
|
51
|
+
import { derivedHeld, derivedOf, type FigureRevision } from './revision.js'
|
|
52
52
|
import { jointDragOf } from './joint-drag.js'
|
|
53
53
|
import type { FirstSave } from './first-save.js'
|
|
54
54
|
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE, scrubbed, scrubStep, type LiveValues } from './live.js'
|
|
@@ -75,18 +75,11 @@ function foldSaid(): boolean {
|
|
|
75
75
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
76
76
|
/** The words a panel is given when there are none: one array, so it does not read as a change. */
|
|
77
77
|
const NO_WORDS: string[] = []
|
|
78
|
+
const NO_OVERHANG = { words: NO_WORDS, note: NO_WORDS }
|
|
78
79
|
|
|
79
80
|
/** How long a command's candidate rests before its preview is worked out. */
|
|
80
81
|
const PREVIEW_SETTLE_MS = 150
|
|
81
82
|
|
|
82
|
-
function deepFreeze<T>(value: T): T {
|
|
83
|
-
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
84
|
-
Object.freeze(value)
|
|
85
|
-
for (const inner of Object.values(value)) deepFreeze(inner)
|
|
86
|
-
}
|
|
87
|
-
return value
|
|
88
|
-
}
|
|
89
|
-
|
|
90
83
|
@customElement('figure-editor')
|
|
91
84
|
export class FigureEditor extends LitElement {
|
|
92
85
|
/* The operato scrollbar, as every scrolling panel of the app has it -- the side panel scrolled with the browser's own. */
|
|
@@ -494,7 +487,7 @@ export class FigureEditor extends LitElement {
|
|
|
494
487
|
aria-pressed=${String(this.gizmoMode === tool.mode)}
|
|
495
488
|
?disabled=${!tool.can}
|
|
496
489
|
aria-label=${i18next.t('figure.button.gizmo-' + tool.mode)}
|
|
497
|
-
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t(tool.mode === 'scale' && this.sizeDragUnsure(this.asset()
|
|
490
|
+
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t(tool.mode === 'scale' && this.sizeDragUnsure(this.asset()!, this.selected) ? 'figure.text.gizmo-size-turned-fastening' : 'figure.text.gizmo-not-own', { part: partLabelOf(this.selected), extreme: this.keptExtremeWord() })}`}
|
|
498
491
|
@click=${() => this.useGizmo(tool.mode)}
|
|
499
492
|
>
|
|
500
493
|
<md-icon>${tool.icon}</md-icon>
|
|
@@ -691,7 +684,7 @@ export class FigureEditor extends LitElement {
|
|
|
691
684
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
692
685
|
private useGizmo(mode: 'translate' | 'rotate' | 'scale') {
|
|
693
686
|
if (mode === 'scale' && this.selected && this.gizmoMode !== 'scale') {
|
|
694
|
-
const asset = this.asset()
|
|
687
|
+
const asset = this.asset()
|
|
695
688
|
if (asset && this.sizeDragUnsure(asset, this.selected)) {
|
|
696
689
|
this.error = i18next.t('figure.text.gizmo-size-turned-fastening', { part: partLabelOf(this.selected), extreme: this.keptExtremeWord() })
|
|
697
690
|
return
|
|
@@ -723,31 +716,34 @@ export class FigureEditor extends LitElement {
|
|
|
723
716
|
this.live = NOTHING_LIVE
|
|
724
717
|
}
|
|
725
718
|
}
|
|
719
|
+
/** The bodies drawn with the value in hand, or null when nothing is in hand (or the figure cannot take it). */
|
|
720
|
+
private liveParts(shown: FigureRevision | undefined): FigureRenderPart[] | null {
|
|
721
|
+
return shown ? liveOverSession(shown, this.live) : null
|
|
722
|
+
}
|
|
726
723
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
727
|
-
private drawnParts(geometry: Parameters<typeof drawableFigureParts>[0] | undefined,
|
|
728
|
-
const parts =
|
|
724
|
+
private drawnParts(geometry: Parameters<typeof drawableFigureParts>[0] | undefined, live: FigureRenderPart[] | null) {
|
|
725
|
+
const parts = live ?? drawableFigureParts(geometry ?? [])
|
|
729
726
|
return parts.filter(body => !this.hiddenParts.has(ownerOf((body as { id: string }).id)))
|
|
730
727
|
}
|
|
731
728
|
/**
|
|
732
729
|
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
733
730
|
* fan follows the arm while a control is dragged. Its size comes from what the joint turns (`joint-fan`).
|
|
734
731
|
*/
|
|
735
|
-
private fanOf(
|
|
736
|
-
if (!
|
|
732
|
+
private fanOf(shown: FigureRevision | undefined, live: FigureRenderPart[] | null) {
|
|
733
|
+
if (!shown || !this.selected || this.hiddenParts.has(this.selected)) return null
|
|
737
734
|
try {
|
|
738
|
-
return jointFanOf(
|
|
735
|
+
return jointFanOf(shown, this.selected, live ? { values: this.live.values, geometry: live } : undefined)
|
|
739
736
|
} catch {
|
|
740
737
|
return null
|
|
741
738
|
}
|
|
742
739
|
}
|
|
743
740
|
/**
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* saved angle while one slider moved
|
|
741
|
+
* The figure on screen at its version: what every panel and helper reads (`revision.ts`). Every input as the session
|
|
742
|
+
* holds it now is its `inputs` -- the pose set with the controls included: drawing a value in hand over the saved
|
|
743
|
+
* defaults put every other joint back to its saved angle while one slider moved (user, 2026-09-24).
|
|
747
744
|
*/
|
|
748
|
-
private
|
|
749
|
-
|
|
750
|
-
return view ? Object.fromEntries(view.fields.map(field => [field.id, field.value as number])) : {}
|
|
745
|
+
private shown(): FigureRevision | undefined {
|
|
746
|
+
return this.session?.revisionNow()
|
|
751
747
|
}
|
|
752
748
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
753
749
|
private hold(values: Record<string, number>, edit?: FigurePartEdit) {
|
|
@@ -865,7 +861,7 @@ export class FigureEditor extends LitElement {
|
|
|
865
861
|
*/
|
|
866
862
|
private insertRecipe(recipe: FigureRecipe, values: Record<string, FigureRecipeValue>) {
|
|
867
863
|
try {
|
|
868
|
-
const asset = this.
|
|
864
|
+
const asset = this.shown()!.asset
|
|
869
865
|
const checked = parseFigureRecipe(recipe)
|
|
870
866
|
const id = figureRecipeIdFor(asset, checked)
|
|
871
867
|
const steps = resolveFigureRecipe(checked, { id, ...(checked.target === 'part' ? { target: this.selected } : {}), values })
|
|
@@ -881,7 +877,7 @@ export class FigureEditor extends LitElement {
|
|
|
881
877
|
/** The design values of the asset being edited. */
|
|
882
878
|
private designValues(): Record<string, number> {
|
|
883
879
|
try {
|
|
884
|
-
return this.
|
|
880
|
+
return this.shown()?.asset.designInputs ?? {}
|
|
885
881
|
} catch {
|
|
886
882
|
return {}
|
|
887
883
|
}
|
|
@@ -892,7 +888,7 @@ export class FigureEditor extends LitElement {
|
|
|
892
888
|
/* A repeat is moved and sized through its row, not by a gizmo on one copy. */
|
|
893
889
|
if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id)) return none
|
|
894
890
|
try {
|
|
895
|
-
const asset = this.
|
|
891
|
+
const asset = this.shown()!.asset
|
|
896
892
|
const reach = gizmoReachOf(asset, id)
|
|
897
893
|
/*
|
|
898
894
|
A size drag shows the face it keeps and the commit keeps the same (figureScaleAnchorOf). Where figure-model cannot
|
|
@@ -917,9 +913,10 @@ export class FigureEditor extends LitElement {
|
|
|
917
913
|
private transformed(id: string, result: GizmoResult) {
|
|
918
914
|
const view = this.session!.read()
|
|
919
915
|
/* A part hanging from a joint: the drag edits the joint -- its value, or where it sits (ADR-0093). */
|
|
920
|
-
const
|
|
916
|
+
const shown = this.shown()!
|
|
917
|
+
const asset = shown.asset
|
|
921
918
|
const drawn = view.geometry.find(g => g.id === id) as unknown as FigureRenderPart | undefined
|
|
922
|
-
const drag = drawn ? jointDragOf(asset, ownerOf(id), drawn, result,
|
|
919
|
+
const drag = drawn ? jointDragOf(asset, ownerOf(id), drawn, result, shown.inputs) : null
|
|
923
920
|
if (drag) {
|
|
924
921
|
if (drag.kind === 'value') this.run(() => this.session!.apply([{ kind: 'set-state', input: drag.input, unit: drag.unit, value: drag.value }]))
|
|
925
922
|
else if (drag.kind === 'joint') this.run(() => this.session!.command({ kind: 'set-joint', joint: drag.joint }))
|
|
@@ -994,8 +991,7 @@ export class FigureEditor extends LitElement {
|
|
|
994
991
|
/** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
|
|
995
992
|
private align(tool: AlignTool) {
|
|
996
993
|
this.run(() => {
|
|
997
|
-
const
|
|
998
|
-
const changes = alignChanges(asset, this.selected, this.reachOf(this.selected), tool)
|
|
994
|
+
const changes = alignChanges(this.shown()!, this.selected, this.reachOf(this.selected), tool)
|
|
999
995
|
if (changes.length) this.session!.apply(changes)
|
|
1000
996
|
})
|
|
1001
997
|
}
|
|
@@ -1007,7 +1003,7 @@ export class FigureEditor extends LitElement {
|
|
|
1007
1003
|
this.session!.edit({ kind: 'duplicate-part', target: id })
|
|
1008
1004
|
const copy = this.session!.read().geometry.find(g => !before.has(g.id))?.id
|
|
1009
1005
|
if (!copy) return
|
|
1010
|
-
this.session!.apply(mirrorChanges(this.
|
|
1006
|
+
this.session!.apply(mirrorChanges(this.shown()!, id, copy, axis))
|
|
1011
1007
|
this.selected = copy
|
|
1012
1008
|
})
|
|
1013
1009
|
}
|
|
@@ -1019,33 +1015,29 @@ export class FigureEditor extends LitElement {
|
|
|
1019
1015
|
*/
|
|
1020
1016
|
/* What a tilted part's typed size keeps, by the figure's placement: not the floor's lowest point everywhere (designer 2026-09-27). */
|
|
1021
1017
|
private keptExtremeWord(): string {
|
|
1022
|
-
const placement = (this.asset()
|
|
1018
|
+
const placement = (this.asset())?.placement ?? 'floor'
|
|
1023
1019
|
return i18next.t(`figure.label.kept-extreme-${placement}`)
|
|
1024
1020
|
}
|
|
1025
|
-
/* How the selected part would meet the plane,
|
|
1026
|
-
private
|
|
1027
|
-
|
|
1028
|
-
if (this.planeContact?.source !== source || this.planeContact.part !== part) {
|
|
1029
|
-
let contact: FigurePlaneContact | undefined
|
|
1021
|
+
/* How the selected part would meet the plane, worked out once per version and part. */
|
|
1022
|
+
private planeContactOf(shown: FigureRevision, part: string): FigurePlaneContact | undefined {
|
|
1023
|
+
return derivedOf(shown, `plane-contact|${part}`, () => {
|
|
1030
1024
|
try {
|
|
1031
|
-
|
|
1025
|
+
return figurePlaneAlignChanges(shown.asset, part).contact
|
|
1032
1026
|
} catch {
|
|
1033
|
-
|
|
1027
|
+
return undefined
|
|
1034
1028
|
}
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
return this.planeContact.contact
|
|
1029
|
+
})
|
|
1038
1030
|
}
|
|
1039
|
-
private renderPlaneAlign(
|
|
1040
|
-
const placement = asset.placement ?? 'floor'
|
|
1031
|
+
private renderPlaneAlign(shown: FigureRevision, unavailable: boolean) {
|
|
1032
|
+
const placement = shown.asset.placement ?? 'floor'
|
|
1041
1033
|
/* A tilted part touches the plane at a point or an edge, a ball at its lowest; the tooltip says so rather than promise a face (designer 2026-09-27). */
|
|
1042
|
-
const contact = this.planeContactOf(
|
|
1034
|
+
const contact = this.planeContactOf(shown, this.selected)
|
|
1043
1035
|
const told = contact === 'tilted' || contact === 'extreme' ? `-${contact}` : ''
|
|
1044
1036
|
return html`<button plane-align=${placement} ?disabled=${unavailable} title=${i18next.t(`figure.text.align-to-${placement}${told}`)}
|
|
1045
1037
|
@click=${() => this.alignToPlane()}>${i18next.t(`figure.button.align-to-${placement}`)}</button>`
|
|
1046
1038
|
}
|
|
1047
1039
|
private alignToPlane() {
|
|
1048
|
-
const asset = this.
|
|
1040
|
+
const asset = this.shown()!.asset
|
|
1049
1041
|
const { changes, contact, refusal } = figurePlaneAlignChanges(asset, this.selected)
|
|
1050
1042
|
if (refusal) {
|
|
1051
1043
|
this.error = i18next.t(`figure.text.align-refused-${refusal.toLowerCase().replace(/_/g, '-')}`, { part: partLabelOf(this.selected) })
|
|
@@ -1056,26 +1048,21 @@ export class FigureEditor extends LitElement {
|
|
|
1056
1048
|
}
|
|
1057
1049
|
private renderTools(unavailable: boolean) {
|
|
1058
1050
|
const reach = this.reachOf(this.selected)
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
try {
|
|
1062
|
-
asset = this.plainAsset()
|
|
1063
|
-
} catch {
|
|
1064
|
-
return null
|
|
1065
|
-
}
|
|
1051
|
+
const shown = this.shown()
|
|
1052
|
+
if (!this.selected || !reach.translate || !shown) return null
|
|
1066
1053
|
/*
|
|
1067
1054
|
* In words, two rows by what they do (user 2026-10-01: 「무엇을 하는 것인지 모르겠다」): moving the part to the figure's
|
|
1068
1055
|
* centre lines, onto its plane or onto the grid; and making a mirrored copy, which makes a part and moves nothing.
|
|
1069
1056
|
*/
|
|
1070
1057
|
const tool = (name: AlignTool, label: string, title: string) =>
|
|
1071
1058
|
html`<button ?disabled=${unavailable} title=${i18next.t(title)} @click=${() => this.align(name)}>${i18next.t(label)}</button>`
|
|
1072
|
-
const mirrors = (['x', 'z'] as const).map(axis => ({ axis, able: canMirror(
|
|
1059
|
+
const mirrors = (['x', 'z'] as const).map(axis => ({ axis, able: canMirror(shown, this.selected, axis, reach) }))
|
|
1073
1060
|
return html`<div vector-row>
|
|
1074
1061
|
<label>${i18next.t('figure.label.tools')}</label>
|
|
1075
1062
|
<div tools>
|
|
1076
1063
|
${tool('centre-x', 'figure.button.center-x', 'figure.text.center-on-x')}
|
|
1077
1064
|
${tool('centre-z', 'figure.button.center-z', 'figure.text.center-on-z')}
|
|
1078
|
-
${this.renderPlaneAlign(
|
|
1065
|
+
${this.renderPlaneAlign(shown, unavailable)}
|
|
1079
1066
|
${tool('snap', 'figure.button.snap', 'figure.text.snap-explained')}
|
|
1080
1067
|
</div>
|
|
1081
1068
|
</div>
|
|
@@ -1131,20 +1118,6 @@ export class FigureEditor extends LitElement {
|
|
|
1131
1118
|
What a panel is handed is the same object while it says the same thing: a new array each render reads as a change to
|
|
1132
1119
|
the panel, which draws itself again for nothing (sixteen times for one edit).
|
|
1133
1120
|
*/
|
|
1134
|
-
private dimsSaid?: { key: string; dims: [number, number, number] | undefined }
|
|
1135
|
-
private dimsOf(asset: FigureAsset): [number, number, number] | undefined {
|
|
1136
|
-
const view = this.session?.read()
|
|
1137
|
-
const key = `${view?.context}|${view?.revision}|${this.selected}`
|
|
1138
|
-
if (this.dimsSaid?.key === key) return this.dimsSaid.dims
|
|
1139
|
-
let dims: [number, number, number] | undefined
|
|
1140
|
-
try {
|
|
1141
|
-
dims = boxOf(asset, this.selected, this.currentInputs())?.dims
|
|
1142
|
-
} catch {
|
|
1143
|
-
dims = undefined
|
|
1144
|
-
}
|
|
1145
|
-
this.dimsSaid = { key, dims }
|
|
1146
|
-
return dims
|
|
1147
|
-
}
|
|
1148
1121
|
private previewWordsSaid?: { preview: unknown; part: string; language: string; words: string[] }
|
|
1149
1122
|
private previewWordsNow(): string[] {
|
|
1150
1123
|
const preview = this.currentPreview()
|
|
@@ -1155,24 +1128,18 @@ export class FigureEditor extends LitElement {
|
|
|
1155
1128
|
return words
|
|
1156
1129
|
}
|
|
1157
1130
|
private renderLinks(unavailable: boolean, section: 'shape' | 'places' | 'motion') {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
asset = this.plainAsset()
|
|
1161
|
-
} catch {
|
|
1162
|
-
return null
|
|
1163
|
-
}
|
|
1164
|
-
const dims = this.dimsOf(asset)
|
|
1131
|
+
const shown = this.shown()
|
|
1132
|
+
if (!shown) return null
|
|
1165
1133
|
return html`<figure-links
|
|
1166
|
-
.
|
|
1134
|
+
.revision=${shown}
|
|
1167
1135
|
.section=${section}
|
|
1168
1136
|
.partId=${this.selected}
|
|
1169
|
-
.dims=${dims}
|
|
1170
1137
|
?locked=${unavailable}
|
|
1171
1138
|
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
1172
1139
|
.previewWords=${this.previewWordsNow()}
|
|
1173
|
-
.detachWords=${this.detachWords(
|
|
1174
|
-
.overhangWords=${section === 'shape' ? this.
|
|
1175
|
-
.overhangNote=${section === 'shape' ? this.
|
|
1140
|
+
.detachWords=${this.detachWords(shown)}
|
|
1141
|
+
.overhangWords=${section === 'shape' ? this.overhangOf(shown).words : NO_WORDS}
|
|
1142
|
+
.overhangNote=${section === 'shape' ? this.overhangOf(shown).note : NO_WORDS}
|
|
1176
1143
|
.motionEnds=${section === 'motion' ? this.motionEnds() : null}
|
|
1177
1144
|
@go-fasten=${() => this.goFasten()}
|
|
1178
1145
|
@preview-command=${(e: CustomEvent) => this.previewCommand(e.detail)}
|
|
@@ -1199,12 +1166,8 @@ export class FigureEditor extends LitElement {
|
|
|
1199
1166
|
private lastAnchors: Record<string, ScaleAnchor> = {}
|
|
1200
1167
|
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
|
|
1201
1168
|
private renderControls(view: { fields: Array<{ id: string; role?: string; value: number }> }, unavailable: boolean) {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
asset = this.plainAsset()
|
|
1205
|
-
} catch {
|
|
1206
|
-
return null
|
|
1207
|
-
}
|
|
1169
|
+
const asset = this.shown()?.asset
|
|
1170
|
+
if (!asset) return null
|
|
1208
1171
|
const values = Object.fromEntries(view.fields.filter(f => f.role === 'state').map(f => [f.id, f.value]))
|
|
1209
1172
|
return html`<figure-controls
|
|
1210
1173
|
.asset=${asset}
|
|
@@ -1286,7 +1249,7 @@ export class FigureEditor extends LitElement {
|
|
|
1286
1249
|
/* A size typed in keeps the face the figure's placement keeps -- figure-model's one rule, as the gizmo and "follow the size" do. */
|
|
1287
1250
|
const part = this.selected
|
|
1288
1251
|
const change: FigureInputChange = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }
|
|
1289
|
-
this.run(() => this.session!.apply(field.role !== 'state' && part ? figureSizeEditChanges(this.
|
|
1252
|
+
this.run(() => this.session!.apply(field.role !== 'state' && part ? figureSizeEditChanges(this.shown()!.asset, part, [change]) : [change]))
|
|
1290
1253
|
// Rejected edits must also restore the displayed numeric value.
|
|
1291
1254
|
input.value = String(this.session!.read().fields.find(i => i.id === field.id)!.value)
|
|
1292
1255
|
}
|
|
@@ -1331,14 +1294,12 @@ export class FigureEditor extends LitElement {
|
|
|
1331
1294
|
label.addEventListener('pointerup', up)
|
|
1332
1295
|
label.addEventListener('pointercancel', up)
|
|
1333
1296
|
}
|
|
1334
|
-
/** The measured range, kept for the revision it was measured at: measuring evaluates the figure at its edges. */
|
|
1335
|
-
private range?: { at: string; of: ReturnType<typeof instanceRangeOf>; toDeclare?: ReturnType<typeof instanceRangeToDeclare> }
|
|
1336
1297
|
/** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
|
|
1337
|
-
private renderInstanceRange(
|
|
1338
|
-
if (!
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1298
|
+
private renderInstanceRange(shown: FigureRevision | undefined, unavailable: boolean) {
|
|
1299
|
+
if (!shown) return nothing
|
|
1300
|
+
const asset = shown.asset
|
|
1301
|
+
/* Measured once per version: measuring evaluates the figure at its edges. */
|
|
1302
|
+
const measured = derivedOf(shown, 'instance-range', () => {
|
|
1342
1303
|
let of: ReturnType<typeof instanceRangeOf>
|
|
1343
1304
|
let toDeclare: ReturnType<typeof instanceRangeToDeclare>
|
|
1344
1305
|
try {
|
|
@@ -1351,11 +1312,11 @@ export class FigureEditor extends LitElement {
|
|
|
1351
1312
|
} catch {
|
|
1352
1313
|
toDeclare = undefined
|
|
1353
1314
|
}
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1356
|
-
const range =
|
|
1315
|
+
return { of, toDeclare }
|
|
1316
|
+
})
|
|
1317
|
+
const range = measured.of
|
|
1357
1318
|
/* No size yet: the figure is declared its size from its parts, so a board can resize it and parts can follow it. */
|
|
1358
|
-
const toDeclare =
|
|
1319
|
+
const toDeclare = measured.toDeclare
|
|
1359
1320
|
if (!range && toDeclare)
|
|
1360
1321
|
return html`<section>
|
|
1361
1322
|
<h3>${i18next.t('figure.label.instance-range')}</h3>
|
|
@@ -1472,9 +1433,10 @@ export class FigureEditor extends LitElement {
|
|
|
1472
1433
|
* The pitch of a row that fills its length is edited as a number (`set-repeat` makes it the part's own the first
|
|
1473
1434
|
* time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
|
|
1474
1435
|
*/
|
|
1475
|
-
private renderRepeat(
|
|
1436
|
+
private renderRepeat(shown: FigureRevision | undefined, geometry: readonly { id: string }[], unavailable: boolean) {
|
|
1476
1437
|
const repeat = repeatOf(this.selected, geometry)
|
|
1477
|
-
|
|
1438
|
+
const asset = shown?.asset
|
|
1439
|
+
if (!shown || !asset || !repeat || repeat.index) return nothing
|
|
1478
1440
|
/* The row is set on its part; what hangs from that part is copied with it and has no row of its own. */
|
|
1479
1441
|
if (!asset.document.model.nodes.some(n => n.id === `${this.selected}.repeat`)) return nothing
|
|
1480
1442
|
const model = asset.document.model as unknown as {
|
|
@@ -1486,7 +1448,7 @@ export class FigureEditor extends LitElement {
|
|
|
1486
1448
|
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?'
|
|
1487
1449
|
const own = `${this.selected}.repeat.pitch`
|
|
1488
1450
|
/* The pitch as it stands: the part's own number, a constant, or what a pitch that follows a length comes to. */
|
|
1489
|
-
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(
|
|
1451
|
+
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(shown, layout?.args[4])
|
|
1490
1452
|
const fits = layout?.op === 'fit-pitch@1'
|
|
1491
1453
|
/* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
|
|
1492
1454
|
const lengthRef = layout?.args[0] ?? ''
|
|
@@ -1509,7 +1471,7 @@ export class FigureEditor extends LitElement {
|
|
|
1509
1471
|
@change=${(e: Event) => this.setPitch(field, e)}
|
|
1510
1472
|
/>
|
|
1511
1473
|
</div>
|
|
1512
|
-
<ul row-reading>${rowWordsOf(this.rowNow(
|
|
1474
|
+
<ul row-reading>${rowWordsOf(this.rowNow(shown), (key, params) => i18next.t(key, params)).map(line => html`<li>${line}</li>`)}</ul>
|
|
1513
1475
|
<p hint>
|
|
1514
1476
|
${fits && length !== undefined && longest !== undefined && limit !== undefined
|
|
1515
1477
|
? i18next.t('figure.text.repeat-what-pitch-gives', {
|
|
@@ -1529,12 +1491,14 @@ export class FigureEditor extends LitElement {
|
|
|
1529
1491
|
</section>`
|
|
1530
1492
|
}
|
|
1531
1493
|
/** The selected row as it is drawn now, read by figure-model: nothing when it cannot be read. */
|
|
1532
|
-
private rowNow(
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1494
|
+
private rowNow(shown: FigureRevision): FigureRowReading | undefined {
|
|
1495
|
+
return derivedOf(shown, `row|${this.selected}`, () => {
|
|
1496
|
+
try {
|
|
1497
|
+
return figureRowOf(shown.asset, this.selected, shown.inputs)
|
|
1498
|
+
} catch {
|
|
1499
|
+
return undefined
|
|
1500
|
+
}
|
|
1501
|
+
})
|
|
1538
1502
|
}
|
|
1539
1503
|
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
1540
1504
|
private pitchInputOf(asset: FigureAsset, part: string): string | undefined {
|
|
@@ -1620,7 +1584,7 @@ export class FigureEditor extends LitElement {
|
|
|
1620
1584
|
const pitch = input.valueAsNumber
|
|
1621
1585
|
if (!Number.isFinite(pitch) || pitch === field.value) return
|
|
1622
1586
|
/* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
|
|
1623
|
-
const from = this.pitchInputOf(this.
|
|
1587
|
+
const from = this.pitchInputOf(this.shown()!.asset, this.selected)
|
|
1624
1588
|
if (from) {
|
|
1625
1589
|
this.run(() => this.session!.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]))
|
|
1626
1590
|
input.value = String(this.session!.read().fields.find(i => i.id === from)?.value ?? field.value)
|
|
@@ -1686,13 +1650,14 @@ export class FigureEditor extends LitElement {
|
|
|
1686
1650
|
let face: FigureFaceArea | undefined
|
|
1687
1651
|
if (candidate.face) {
|
|
1688
1652
|
try {
|
|
1689
|
-
|
|
1653
|
+
const shown = this.shown()!
|
|
1654
|
+
face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs)
|
|
1690
1655
|
} catch {
|
|
1691
1656
|
face = undefined
|
|
1692
1657
|
}
|
|
1693
1658
|
}
|
|
1694
1659
|
/* A row is read on the figure the command would make, by figure-model, as drawn. */
|
|
1695
|
-
const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.
|
|
1660
|
+
const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.shown()!.inputs) : undefined
|
|
1696
1661
|
this.preview = {
|
|
1697
1662
|
...held,
|
|
1698
1663
|
context: seen.context,
|
|
@@ -1723,104 +1688,91 @@ export class FigureEditor extends LitElement {
|
|
|
1723
1688
|
it stays where it stands now, what stays fastened to it, which clips would go. "It stays where it is" is said only
|
|
1724
1689
|
when the copy shows it does.
|
|
1725
1690
|
*/
|
|
1726
|
-
private detachSaid?: { key: string; words: string[] }
|
|
1727
1691
|
private detachTimer?: ReturnType<typeof setTimeout>
|
|
1728
|
-
private detachAsked?: string
|
|
1692
|
+
private detachAsked?: { shown: FigureRevision; part: string }
|
|
1729
1693
|
/**
|
|
1730
1694
|
* Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
|
|
1731
1695
|
* edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
|
|
1732
1696
|
*/
|
|
1733
|
-
private detachWords(
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
const
|
|
1738
|
-
if (
|
|
1739
|
-
if (this.detachAsked !==
|
|
1740
|
-
this.detachAsked =
|
|
1697
|
+
private detachWords(shown: FigureRevision): string[] {
|
|
1698
|
+
const part = this.selected
|
|
1699
|
+
if (!this.session || !shown.asset.joints?.some(j => j.body1.part === part)) return NO_WORDS
|
|
1700
|
+
const key = `detach|${part}`
|
|
1701
|
+
const said = derivedHeld<string[]>(shown, key)
|
|
1702
|
+
if (said) return said
|
|
1703
|
+
if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {
|
|
1704
|
+
this.detachAsked = { shown, part }
|
|
1741
1705
|
clearTimeout(this.detachTimer)
|
|
1742
1706
|
this.detachTimer = setTimeout(() => {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
this.computeDetachWords(asset, key)
|
|
1707
|
+
if (this.shown() !== shown || this.selected !== part) return
|
|
1708
|
+
derivedOf(shown, key, () => this.computeDetachWords(shown, part))
|
|
1746
1709
|
this.requestUpdate()
|
|
1747
1710
|
}, PREVIEW_SETTLE_MS)
|
|
1748
1711
|
}
|
|
1749
1712
|
return NO_WORDS
|
|
1750
1713
|
}
|
|
1751
|
-
private computeDetachWords(
|
|
1714
|
+
private computeDetachWords(shown: FigureRevision, part: string): string[] {
|
|
1752
1715
|
const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)
|
|
1753
1716
|
let words: string[]
|
|
1754
1717
|
try {
|
|
1755
|
-
const seen = this.session!.previewCommand({ kind: 'detach', part
|
|
1756
|
-
const own = seen.moves.some(m => m.body ===
|
|
1718
|
+
const seen = this.session!.previewCommand({ kind: 'detach', part })
|
|
1719
|
+
const own = seen.moves.some(m => m.body === part)
|
|
1757
1720
|
words = own
|
|
1758
|
-
? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key
|
|
1721
|
+
? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]
|
|
1759
1722
|
: /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []
|
|
1760
|
-
const children = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part ===
|
|
1723
|
+
const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part))
|
|
1761
1724
|
if (children.length) words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }))
|
|
1762
1725
|
if (seen.clipsWouldGo.length) words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }))
|
|
1763
1726
|
} catch (error) {
|
|
1764
1727
|
words = [say('figure.text.detach-refused', { reason: this.said(error) })]
|
|
1765
1728
|
}
|
|
1766
|
-
this.detachSaid = { key, words }
|
|
1767
1729
|
return words
|
|
1768
1730
|
}
|
|
1769
1731
|
/*
|
|
1770
1732
|
Past the edge of the face a part is fastened to (ADR-0097 §5.5, Q3): the selected part's own, and the parts fastened
|
|
1771
|
-
to it -- said apart from the room, nothing moved. Read once per
|
|
1733
|
+
to it -- said apart from the room, nothing moved. Read once per version, part and language. The note says it is wider
|
|
1734
|
+
than its face: a fact of the relation, said in its details, not as a warning (designer 2026-09-27).
|
|
1772
1735
|
*/
|
|
1773
|
-
private
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
if (
|
|
1736
|
+
private overhangOf(shown: FigureRevision): { words: string[]; note: string[] } {
|
|
1737
|
+
const part = this.selected
|
|
1738
|
+
if (!part) return NO_OVERHANG
|
|
1739
|
+
return derivedOf(shown, `overhang|${part}|${i18next.language}`, () => {
|
|
1740
|
+
const asset = shown.asset
|
|
1741
|
+
const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)
|
|
1742
|
+
const where = (o: FigureFaceOverhang) => ({ on: partLabelOf(o.on.part), face: say(`figure.label.face-${o.on.face}`) })
|
|
1743
|
+
const sides = (o: FigureFaceOverhang) => o.sides.map(s => say(`figure.text.overhang-side-${s.side}`, { axis: s.axis.toUpperCase(), mm: String(s.mm) })).join(', ')
|
|
1744
|
+
const words: string[] = []
|
|
1745
|
+
const note: string[] = []
|
|
1746
|
+
try {
|
|
1747
|
+
const own = figureFaceOverhangOf(asset, part, shown.inputs)
|
|
1748
|
+
if (own?.state === 'larger') {
|
|
1749
|
+
note.push(say('figure.text.overhang-larger', { ...where(own), sides: sides(own) }))
|
|
1750
|
+
if (own.lopsided.length) note.push(say('figure.text.overhang-lopsided', { axes: own.lopsided.map(a => a.toUpperCase()).join(', ') }))
|
|
1751
|
+
}
|
|
1752
|
+
if (own?.state === 'out') words.push(say('figure.text.overhang-out', { ...where(own), sides: sides(own) }))
|
|
1753
|
+
else if (own?.state === 'possible') words.push(say('figure.text.overhang-possible', where(own)))
|
|
1754
|
+
else if (own?.state === 'unknown') words.push(say('figure.text.overhang-unknown', where(own)))
|
|
1755
|
+
const held = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => figureFaceOverhangOf(asset, j.body1.part, shown.inputs))
|
|
1756
|
+
const past = held.filter((o): o is FigureFaceOverhang => o?.state === 'out' || o?.state === 'possible')
|
|
1757
|
+
if (past.length) words.push(say('figure.text.overhang-held', { parts: past.map(o => partLabelOf(o.part)).join(', ') }))
|
|
1758
|
+
} catch {
|
|
1759
|
+
/* Not read: nothing said rather than a guess. */
|
|
1795
1760
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
else if (own?.state === 'unknown') words.push(say('figure.text.overhang-unknown', where(own)))
|
|
1799
|
-
const held = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === this.selected).map(j => figureFaceOverhangOf(asset, j.body1.part, inputs))
|
|
1800
|
-
const past = held.filter((o): o is FigureFaceOverhang => o?.state === 'out' || o?.state === 'possible')
|
|
1801
|
-
if (past.length) words.push(say('figure.text.overhang-held', { parts: past.map(o => partLabelOf(o.part)).join(', ') }))
|
|
1802
|
-
} catch {
|
|
1803
|
-
/* Not read: nothing said rather than a guess. */
|
|
1804
|
-
}
|
|
1805
|
-
this.overhangSaid = { key, words, note }
|
|
1806
|
-
return words
|
|
1761
|
+
return { words, note }
|
|
1762
|
+
})
|
|
1807
1763
|
}
|
|
1808
|
-
/** The selected part's motion at both ends of its range, made once per
|
|
1809
|
-
private motionEndsSaid?: { key: string; ends: MotionEnds | null }
|
|
1764
|
+
/** The selected part's motion at both ends of its range, made once per version and part. */
|
|
1810
1765
|
private motionEnds(): MotionEnds | null {
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
let ends: MotionEnds | null = null
|
|
1766
|
+
const shown = this.shown()
|
|
1767
|
+
const part = this.selected
|
|
1768
|
+
if (!shown || !part) return null
|
|
1769
|
+
return derivedOf(shown, `motion-ends|${part}`, () => {
|
|
1816
1770
|
try {
|
|
1817
|
-
|
|
1771
|
+
return motionEndsOf(shown, part)
|
|
1818
1772
|
} catch {
|
|
1819
|
-
|
|
1773
|
+
return null
|
|
1820
1774
|
}
|
|
1821
|
-
|
|
1822
|
-
}
|
|
1823
|
-
return this.motionEndsSaid.ends
|
|
1775
|
+
})
|
|
1824
1776
|
}
|
|
1825
1777
|
/** "Fasten it first", from the motion tab: to the properties tab, the fastening form open on this part. */
|
|
1826
1778
|
private async goFasten() {
|
|
@@ -1853,50 +1805,8 @@ export class FigureEditor extends LitElement {
|
|
|
1853
1805
|
this.faceMapping = undefined
|
|
1854
1806
|
if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history')) this.tab = 'properties'
|
|
1855
1807
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
* figure is most of what a selection costs. Read, never changed -- what changes a figure goes through the session.
|
|
1859
|
-
*/
|
|
1860
|
-
/**
|
|
1861
|
-
* The session has evaluated this figure at this version, with these inputs: the screen's questions of the text it just
|
|
1862
|
-
* parsed are answered from that, not by evaluating the figure again (each is tens of milliseconds on a real figure).
|
|
1863
|
-
*/
|
|
1864
|
-
private seeded(asset: FigureAsset): FigureAsset {
|
|
1865
|
-
const view = this.session?.read()
|
|
1866
|
-
if (view?.geometry.length) {
|
|
1867
|
-
const values = this.currentInputs()
|
|
1868
|
-
seedEvaluatedGeometry(asset, values, view.geometry as never)
|
|
1869
|
-
seedFigureEvaluation(asset, values, view.geometry)
|
|
1870
|
-
}
|
|
1871
|
-
return asset
|
|
1872
|
-
}
|
|
1873
|
-
private parsed?: { text: string; asset: FigureAsset }
|
|
1874
|
-
private parsedAsset(): FigureAsset {
|
|
1875
|
-
const text = this.session!.source()
|
|
1876
|
-
if (this.parsed?.text !== text) {
|
|
1877
|
-
/* The session's text is what its workspace exported, compiled then: it is read here, not compiled again. */
|
|
1878
|
-
const plain = this.plainAsset()
|
|
1879
|
-
const upgraded = upgradeFigurePlacement(plain)
|
|
1880
|
-
this.parsed = { text, asset: upgraded === plain ? plain : this.seeded(deepFreeze(upgraded)) }
|
|
1881
|
-
}
|
|
1882
|
-
return this.parsed.asset
|
|
1883
|
-
}
|
|
1884
|
-
private plain?: { text: string; asset: FigureAsset }
|
|
1885
|
-
/**
|
|
1886
|
-
* The figure as plain data, parsed once per text and frozen: the render, the gizmo's reach and every tool read it, and
|
|
1887
|
-
* a copy for each of them cost more than the selection they serve. A change goes through the session, never through this.
|
|
1888
|
-
*/
|
|
1889
|
-
private plainAsset(): FigureAsset {
|
|
1890
|
-
const text = this.session!.source()
|
|
1891
|
-
if (this.plain?.text !== text) this.plain = { text, asset: this.seeded(deepFreeze(JSON.parse(text))) }
|
|
1892
|
-
return this.plain.asset
|
|
1893
|
-
}
|
|
1894
|
-
private asset(): unknown {
|
|
1895
|
-
try {
|
|
1896
|
-
return this.plainAsset()
|
|
1897
|
-
} catch {
|
|
1898
|
-
return undefined
|
|
1899
|
-
}
|
|
1808
|
+
private asset(): FigureAsset | undefined {
|
|
1809
|
+
return this.shown()?.asset
|
|
1900
1810
|
}
|
|
1901
1811
|
/** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
|
|
1902
1812
|
private renderTab(tab: SideTab, icon: string, label: string, badge?: number) {
|
|
@@ -1963,25 +1873,26 @@ export class FigureEditor extends LitElement {
|
|
|
1963
1873
|
* Why a drawn push's end cannot be drawn to one point now: the kernel's own refusal of that command on a copy (what is
|
|
1964
1874
|
* fastened to the end face), said as the face. Asked once per figure revision and part.
|
|
1965
1875
|
*/
|
|
1966
|
-
private pointBlocked?: { key: string; words: string }
|
|
1967
1876
|
private pointBlockedOf(drawn: NonNullable<ReturnType<typeof drawnOf>>): string {
|
|
1968
|
-
|
|
1969
|
-
const
|
|
1970
|
-
if (
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1877
|
+
const shown = this.shown()
|
|
1878
|
+
const session = this.session
|
|
1879
|
+
if (drawn.apex || drawn.section.kind !== 'polygon' || !session || !shown) return ''
|
|
1880
|
+
const part = this.selected
|
|
1881
|
+
const apex = apexOver(drawn.section.points, drawn.axis)
|
|
1882
|
+
return derivedOf(shown, `point-blocked|${part}|${i18next.language}`, () => {
|
|
1883
|
+
try {
|
|
1884
|
+
session.previewCommand({ kind: 'set-section', part, apex })
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
if ((error as { code?: string }).code === 'FEATURE_MAPPING_REQUIRED')
|
|
1887
|
+
return i18next.t('figure.text.section-editor-apex-blocked', { face: i18next.t('figure.label.face-' + END_FACE(drawn.axis, 'end')) })
|
|
1888
|
+
}
|
|
1889
|
+
return ''
|
|
1890
|
+
})
|
|
1980
1891
|
}
|
|
1981
1892
|
private renderStart() {
|
|
1982
1893
|
const shapes: FigurePaletteKind[] = ['box', 'cylinder', 'plate']
|
|
1983
1894
|
/* Where it is authored from, chosen before any part (ADR-0097): the figure's own setting, no room made up for it. */
|
|
1984
|
-
const placement = ((this.asset()
|
|
1895
|
+
const placement = ((this.asset())?.placement ?? 'floor') as FigurePlacement
|
|
1985
1896
|
return html`<div start role="region" aria-label=${i18next.t('figure.text.start-question')}>
|
|
1986
1897
|
<h2>${i18next.t('figure.text.start-question')}</h2>
|
|
1987
1898
|
<div start-paths>
|
|
@@ -2028,7 +1939,7 @@ export class FigureEditor extends LitElement {
|
|
|
2028
1939
|
let summary: { kind: string; size: string } | undefined
|
|
2029
1940
|
try {
|
|
2030
1941
|
summary = this.selected
|
|
2031
|
-
? partSummaryOf(this.
|
|
1942
|
+
? partSummaryOf(this.shown()!.asset, ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
|
|
2032
1943
|
: undefined
|
|
2033
1944
|
} catch {
|
|
2034
1945
|
summary = undefined
|
|
@@ -2063,9 +1974,11 @@ export class FigureEditor extends LitElement {
|
|
|
2063
1974
|
A part's field only the declared room still reads -- its old height position once the floor holds it -- moves
|
|
2064
1975
|
nothing; it is shown apart as what it is, not as a position (ADR-0097, designer 2026-09-27).
|
|
2065
1976
|
*/
|
|
1977
|
+
const shown = this.shown()
|
|
1978
|
+
const asset = shown?.asset
|
|
2066
1979
|
let roomOnly: string[] = []
|
|
2067
1980
|
try {
|
|
2068
|
-
roomOnly = this.selected ? figureRoomOnlyInputsOf(
|
|
1981
|
+
roomOnly = this.selected && asset ? figureRoomOnlyInputsOf(asset, this.selected) : []
|
|
2069
1982
|
} catch {
|
|
2070
1983
|
roomOnly = []
|
|
2071
1984
|
}
|
|
@@ -2075,14 +1988,13 @@ export class FigureEditor extends LitElement {
|
|
|
2075
1988
|
try {
|
|
2076
1989
|
const body = view.geometry.find(g => g.id === this.selected) as unknown as FigureRenderPart | undefined
|
|
2077
1990
|
const placed = body ? placementOf(body) : undefined
|
|
2078
|
-
|
|
2079
|
-
if (placed && parsed)
|
|
1991
|
+
if (placed && shown && this.selected)
|
|
2080
1992
|
held = {
|
|
2081
1993
|
position: placed.position,
|
|
2082
1994
|
rotation: placed.rotation,
|
|
2083
|
-
size: (['x', 'y', 'z'] as const).map(axis => measureNow(
|
|
1995
|
+
size: (['x', 'y', 'z'] as const).map(axis => measureNow(shown, ownerOf(this.selected), axis)),
|
|
2084
1996
|
/* Only a size a rule sets: a palette part's own numbers are its shape's fields, not a rule's (found in the browser). */
|
|
2085
|
-
sizeRuled: (['x', 'y', 'z'] as const).every(axis => followOf(
|
|
1997
|
+
sizeRuled: (['x', 'y', 'z'] as const).every(axis => followOf(shown.asset, ownerOf(this.selected), axis).of !== 'own')
|
|
2086
1998
|
}
|
|
2087
1999
|
} catch {
|
|
2088
2000
|
held = undefined
|
|
@@ -2095,8 +2007,8 @@ export class FigureEditor extends LitElement {
|
|
|
2095
2007
|
: view.dirty || !view.id
|
|
2096
2008
|
? 'figure.text.unsaved-changes'
|
|
2097
2009
|
: 'figure.text.saved'
|
|
2098
|
-
const asset = this.asset()
|
|
2099
2010
|
const tab = this.tab
|
|
2011
|
+
const live = this.liveParts(shown)
|
|
2100
2012
|
return html`<ox-canvas-bar
|
|
2101
2013
|
history
|
|
2102
2014
|
fit
|
|
@@ -2137,8 +2049,8 @@ export class FigureEditor extends LitElement {
|
|
|
2137
2049
|
slot="start"
|
|
2138
2050
|
.budget=${this.budget}
|
|
2139
2051
|
@show-maturity=${() => (this.tab = 'verdict')}
|
|
2140
|
-
.parts=${this.partRows(view.geometry, asset
|
|
2141
|
-
.partBudget=${this.partBudgetOf(asset
|
|
2052
|
+
.parts=${this.partRows(view.geometry, asset)}
|
|
2053
|
+
.partBudget=${this.partBudgetOf(asset)}
|
|
2142
2054
|
@set-detail-level=${(e: CustomEvent) => this.linkCommand({ kind: 'set-detail-level', level: e.detail.level })}
|
|
2143
2055
|
.selected=${this.selected}
|
|
2144
2056
|
.modelName=${view.name ?? ''}
|
|
@@ -2160,7 +2072,7 @@ export class FigureEditor extends LitElement {
|
|
|
2160
2072
|
? html`<figure-preview
|
|
2161
2073
|
?hidden=${this.middle !== 'preview'}
|
|
2162
2074
|
.active=${this.middle === 'preview'}
|
|
2163
|
-
.
|
|
2075
|
+
.revision=${shown}
|
|
2164
2076
|
.context=${view.context ?? ''}
|
|
2165
2077
|
.view=${this.view}
|
|
2166
2078
|
.selected=${this.selected}
|
|
@@ -2179,9 +2091,9 @@ export class FigureEditor extends LitElement {
|
|
|
2179
2091
|
${this.sectionEditing && this.sectionEditing === this.selected && !unavailable ? this.renderSectionEditor(view.geometry) : nothing}
|
|
2180
2092
|
<figure-canvas
|
|
2181
2093
|
.view=${this.view}
|
|
2182
|
-
.parts=${this.drawnParts(view.geometry,
|
|
2183
|
-
.anchors=${this.anchorsOf(asset
|
|
2184
|
-
.fan=${this.fanOf(
|
|
2094
|
+
.parts=${this.drawnParts(view.geometry, live)}
|
|
2095
|
+
.anchors=${this.anchorsOf(asset)}
|
|
2096
|
+
.fan=${this.fanOf(shown, live)}
|
|
2185
2097
|
.ghost=${this.previewShown().ghost}
|
|
2186
2098
|
.highlight=${this.previewShown().highlight}
|
|
2187
2099
|
.palette=${view.palette ?? {}}
|
|
@@ -2221,7 +2133,7 @@ export class FigureEditor extends LitElement {
|
|
|
2221
2133
|
</div>
|
|
2222
2134
|
${this.renderRepeatNote(view.geometry)}
|
|
2223
2135
|
</section>
|
|
2224
|
-
${this.renderRepeat(
|
|
2136
|
+
${this.renderRepeat(shown, view.geometry, unavailable)}
|
|
2225
2137
|
${this.renderGroups(this.withSizeRow(groupFields(partFields, this.selected), held), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable), held)}
|
|
2226
2138
|
${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
|
|
2227
2139
|
${this.renderLinks(unavailable, 'shape')}
|
|
@@ -2252,7 +2164,7 @@ export class FigureEditor extends LitElement {
|
|
|
2252
2164
|
</section>`
|
|
2253
2165
|
: html`${this.renderSubject()}<slot name="figure"></slot>
|
|
2254
2166
|
${this.renderGroups(groupFields(common), 'figure.label.group-figure-size', 'figure.label.design-inputs', unavailable)}
|
|
2255
|
-
${this.renderInstanceRange(
|
|
2167
|
+
${this.renderInstanceRange(shown, unavailable)}`}
|
|
2256
2168
|
</div>
|
|
2257
2169
|
<div panel ?off=${tab !== 'capability'}>
|
|
2258
2170
|
${this.renderSubject()}
|
|
@@ -2354,7 +2266,7 @@ export class FigureEditor extends LitElement {
|
|
|
2354
2266
|
* something else. A size that follows something is said at the panel's head, so not again here.
|
|
2355
2267
|
*/
|
|
2356
2268
|
private renderHeldAxes(vectors: FieldGroup[]) {
|
|
2357
|
-
const fastened = !!(this.asset()
|
|
2269
|
+
const fastened = !!(this.asset())?.joints?.some(j => j.body1.part === ownerOf(this.selected))
|
|
2358
2270
|
const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && (['x', 'y', 'z'] as const).some(axis => !g.cells[axis]))
|
|
2359
2271
|
if (!held.length) return nothing
|
|
2360
2272
|
const placed = held.every(g => g.name === 'position' || g.name === 'rotation')
|
|
@@ -2381,7 +2293,7 @@ export class FigureEditor extends LitElement {
|
|
|
2381
2293
|
private async askRoomRefit() {
|
|
2382
2294
|
let refit
|
|
2383
2295
|
try {
|
|
2384
|
-
refit = figureRoomRefitOf(this.
|
|
2296
|
+
refit = figureRoomRefitOf(this.shown()!.asset)
|
|
2385
2297
|
} catch (error) {
|
|
2386
2298
|
this.error = this.said(error)
|
|
2387
2299
|
return
|