@things-factory/figure-ui 10.1.265 → 10.1.266

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.
Files changed (35) hide show
  1. package/client/modeller/align.ts +13 -1
  2. package/client/modeller/clips.ts +90 -1
  3. package/client/modeller/editor-styles.ts +36 -0
  4. package/client/modeller/editor.ts +53 -6
  5. package/client/modeller/figure-clips.ts +82 -34
  6. package/client/modeller/joint-drag.ts +32 -2
  7. package/dist-client/modeller/align.d.ts +5 -0
  8. package/dist-client/modeller/align.js +10 -1
  9. package/dist-client/modeller/align.js.map +1 -1
  10. package/dist-client/modeller/clips.d.ts +56 -4
  11. package/dist-client/modeller/clips.js +89 -0
  12. package/dist-client/modeller/clips.js.map +1 -1
  13. package/dist-client/modeller/editor-styles.js +36 -0
  14. package/dist-client/modeller/editor-styles.js.map +1 -1
  15. package/dist-client/modeller/editor.d.ts +7 -0
  16. package/dist-client/modeller/editor.js +58 -5
  17. package/dist-client/modeller/editor.js.map +1 -1
  18. package/dist-client/modeller/figure-clips.d.ts +13 -1
  19. package/dist-client/modeller/figure-clips.js +87 -34
  20. package/dist-client/modeller/figure-clips.js.map +1 -1
  21. package/dist-client/modeller/joint-drag.d.ts +21 -2
  22. package/dist-client/modeller/joint-drag.js +20 -1
  23. package/dist-client/modeller/joint-drag.js.map +1 -1
  24. package/dist-client/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +6 -6
  26. package/test/align.test.ts +18 -0
  27. package/test/clips.test.ts +83 -0
  28. package/test/joint-drag.test.ts +49 -0
  29. package/test/joint-frames.test.ts +3 -2
  30. package/translations/en.json +22 -2
  31. package/translations/es.json +22 -2
  32. package/translations/ja.json +22 -2
  33. package/translations/ko.json +22 -2
  34. package/translations/ms.json +22 -2
  35. package/translations/zh.json +22 -2
@@ -1 +1 @@
1
- {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureHangingChainOf, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigurePartEdit, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * snap to the grid; and whether a mirrored copy is offered.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(revision)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy\n * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.\n */\nexport function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z'): boolean {\n const box = boxOf(revision, part)\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/** How a mirrored copy is made: with what hangs from the part, under which names, turning as a mirror or as the original. */\nexport interface MirrorForm {\n axis: 'x' | 'z'\n chain: boolean\n /** The beginning swapped in the copies' names (\\`fl\\` → \\`fr\\`); empty, the copies take \\`-copy\\`. */\n from: string\n to: string\n keepMotion: boolean\n}\n\n/* The beginning a mirror across each axis swaps, by the figure's faces: X is its left and right, Z its front and back. */\nconst SWAPS: Record<'x' | 'z', Record<string, string>> = { x: { l: 'r', r: 'l' }, z: { f: 'b', b: 'f' } }\n\n/**\n * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them\n * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\\`fl-hip\\` across Z\n * is \\`bl-\\`, across X \\`fr-\\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing\n * moving asks nothing.\n */\nexport function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x' | 'z'): { hanging: string[]; moves: boolean; asks: boolean; form: MirrorForm } {\n const chain = figureHangingChainOf(asset, part)\n const moves = (asset.joints ?? []).some(j => chain.includes(j.body1.part) && !!j.motion?.length)\n const head = /^([a-zA-Z]{1,3})-/.exec(part)?.[1] ?? ''\n const swapped = [...head].map(c => SWAPS[axis][c.toLowerCase()] ?? c).join('')\n const named = head && swapped !== head && chain.every(p => p.startsWith(`${head}-`))\n const hanging = chain.slice(1)\n return {\n hanging,\n moves,\n asks: hanging.length > 0 || moves,\n form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }\n }\n}\n\n/** The one edit the form comes to. */\nexport function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit {\n return {\n kind: 'mirror-part',\n target: part,\n axis: form.axis,\n ...(form.chain ? { chain: true } : {}),\n ...(form.from && form.to && form.from !== form.to ? { prefix: { from: form.from, to: form.to } } : {}),\n ...(form.keepMotion ? { keepMotion: true } : {})\n }\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
1
+ {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureHangingChainOf, figureMotionsOf, figureOccupiedBoxesOf, figureWritersOf, motionStateIdOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigurePartEdit, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * snap to the grid; and whether a mirrored copy is offered.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(revision)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy\n * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.\n */\nexport function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z'): boolean {\n const box = boxOf(revision, part)\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/** How a mirrored copy is made: with what hangs from the part, under which names, turning as a mirror or as the original. */\nexport interface MirrorForm {\n axis: 'x' | 'z'\n chain: boolean\n /** The beginning swapped in the copies' names (\\`fl\\` → \\`fr\\`); empty, the copies take \\`-copy\\`. */\n from: string\n to: string\n keepMotion: boolean\n}\n\n/* The beginning a mirror across each axis swaps, by the figure's faces: X is its left and right, Z its front and back. */\nconst SWAPS: Record<'x' | 'z', Record<string, string>> = { x: { l: 'r', r: 'l' }, z: { f: 'b', b: 'f' } }\n\n/**\n * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them\n * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\\`fl-hip\\` across Z\n * is \\`bl-\\`, across X \\`fr-\\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing\n * moving asks nothing.\n */\nexport function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x' | 'z'): { hanging: string[]; moves: boolean; asks: boolean; form: MirrorForm } {\n const chain = figureHangingChainOf(asset, part)\n const moves = (asset.joints ?? []).some(j => chain.includes(j.body1.part) && !!j.motion?.length)\n const head = /^([a-zA-Z]{1,3})-/.exec(part)?.[1] ?? ''\n const swapped = [...head].map(c => SWAPS[axis][c.toLowerCase()] ?? c).join('')\n const named = head && swapped !== head && chain.every(p => p.startsWith(`${head}-`))\n const hanging = chain.slice(1)\n return {\n hanging,\n moves,\n asks: hanging.length > 0 || moves,\n form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }\n }\n}\n\n/**\n * The clips whose tracks come with a mirrored copy (figure-model's mirror-part, review 2026-11-09): those that play a\n * control of the part, or of what hangs from it when the chain is taken. Said in the form before the copy is made.\n */\nexport function mirroredClipsOf(asset: FigureAsset, part: string, chain: boolean): string[] {\n const parts = chain ? figureHangingChainOf(asset, part) : [part]\n const controls = new Set(\n (asset.joints ?? []).filter(j => parts.includes(j.body1.part)).flatMap(j => figureMotionsOf(j).filter(m => !m.mimic).map(m => motionStateIdOf(m)))\n )\n return [...new Set((asset.drivers ?? []).filter(d => controls.has(d.state)).map(d => d.clip))]\n}\n\n/** The one edit the form comes to. */\nexport function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit {\n return {\n kind: 'mirror-part',\n target: part,\n axis: form.axis,\n ...(form.chain ? { chain: true } : {}),\n ...(form.from && form.to && form.from !== form.to ? { prefix: { from: form.from, to: form.to } } : {}),\n ...(form.keepMotion ? { keepMotion: true } : {})\n }\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
@@ -10,7 +10,7 @@ export interface ClipTrack {
10
10
  min: number;
11
11
  max: number;
12
12
  keys: ClipKey[];
13
- interpolation: 'linear' | 'step';
13
+ interpolation: 'linear' | 'step' | 'smooth';
14
14
  loop: 'wrap' | 'hold';
15
15
  accumulates: boolean;
16
16
  }
@@ -67,6 +67,11 @@ export declare function withoutKey(keys: readonly ClipKey[], index: number): Cli
67
67
  export declare function freeClipNameOf(asset: FigureAsset, clip: string): string;
68
68
  /** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */
69
69
  export declare const copyTargetsOf: (asset: FigureAsset, clip: string) => string[];
70
+ /**
71
+ * Where a smooth track may be moved round its loop to, in seconds later, when `seconds` is not one of them: figure-model
72
+ * moves a smooth track only so that its new start is one of its keys (`driver-shift-off-key`). Null when it may go there.
73
+ */
74
+ export declare function shiftOffKeyOf(track: ClipTrack, duration: number, seconds: number): number[] | null;
70
75
  /** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */
71
76
  export declare const shiftable: (track: ClipTrack) => boolean;
72
77
  /**
@@ -77,9 +82,12 @@ export declare function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: C
77
82
  kind: "set-driver";
78
83
  state: string;
79
84
  clip: string;
80
- keys: ClipKey[];
85
+ keys: {
86
+ seconds: number;
87
+ value: number;
88
+ }[];
81
89
  loop: "hold" | "wrap";
82
- interpolation: "step" | "linear";
90
+ interpolation: "linear" | "step" | "smooth";
83
91
  accumulates: boolean;
84
92
  };
85
93
  /** The edit that writes a track back. */
@@ -89,5 +97,49 @@ export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<P
89
97
  clip: string;
90
98
  keys: ClipKey[];
91
99
  loop: "hold" | "wrap";
92
- interpolation: "step" | "linear";
100
+ interpolation: "linear" | "step" | "smooth";
101
+ };
102
+ /**
103
+ * A new track's keys. By default it holds the pose the figure stands in now, from start to end: a track added to a pose
104
+ * made with care does not throw it to the control's ends (review 2026-11-09). `range` is the preset that swings the
105
+ * control over its whole range and back, so the loop closes where it began.
106
+ */
107
+ export declare function newTrackKeys(input: {
108
+ min?: number;
109
+ max?: number;
110
+ }, current: number | undefined, duration: number, how?: 'hold' | 'range'): ClipKey[];
111
+ /** What a new clip on a control joins, so that it may play it: nothing, a group already there, or a group made with the clip that plays it now. */
112
+ export type TrackJoin = {
113
+ kind: 'free';
114
+ } | {
115
+ kind: 'group';
116
+ group: string;
117
+ label: string;
118
+ } | {
119
+ kind: 'new-group';
120
+ group: string;
121
+ label: string;
122
+ clip: string;
123
+ };
124
+ /**
125
+ * The edits that make a new clip `clip` with one track on `state` (ADR-0051 §3.2: only clips of one motion group play
126
+ * one control). A control no clip plays is taken as it is. A control a group's clips play puts the new clip in that
127
+ * group. A control one clip in no group plays makes a group of that clip and the new one -- the person adds 「열기」 to a
128
+ * door that has 「닫기」, and the two become one group, one playing at a time, without having to design the group first.
129
+ * All of it one step of the history.
130
+ */
131
+ export declare function newClipEdits(asset: FigureAsset, state: string, clip: string, duration: number, keys: ClipKey[]): {
132
+ edits: unknown[];
133
+ join: TrackJoin;
134
+ };
135
+ /**
136
+ * 「이 자세 기억하기」: the pose the figure stands in now, kept in `clip` at `seconds` in one step (review 2026-11-09:
137
+ * a six-axis pose was six tracks keyed one by one). A track whose value there differs gets a key; a control the clip
138
+ * does not play yet and that moved since `base` -- the pose the clip was last shown or kept in -- gets a track that
139
+ * starts from where it was and reaches the pose at that moment (from the start when the moment is 0). A moved control
140
+ * another clip outside this clip's group plays is not taken; it is named in `skipped`.
141
+ */
142
+ export declare function rememberedPoseEdits(asset: FigureAsset, clip: string, seconds: number, values: Record<string, number>, base: Record<string, number>): {
143
+ edits: unknown[];
144
+ skipped: string[];
93
145
  };
@@ -108,6 +108,18 @@ export function freeClipNameOf(asset, clip) {
108
108
  export const copyTargetsOf = (asset, clip) => [
109
109
  ...new Set([...(asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state), ...undrivenOf(asset, clip)])
110
110
  ];
111
+ /**
112
+ * Where a smooth track may be moved round its loop to, in seconds later, when `seconds` is not one of them: figure-model
113
+ * moves a smooth track only so that its new start is one of its keys (`driver-shift-off-key`). Null when it may go there.
114
+ */
115
+ export function shiftOffKeyOf(track, duration, seconds) {
116
+ if (track.interpolation !== 'smooth' || !seconds)
117
+ return null;
118
+ const D = duration;
119
+ const phase = ((seconds % D) + D) % D;
120
+ const fits = [...new Set(track.keys.map(k => round((D - k.seconds) % D)))].sort((a, b) => a - b);
121
+ return fits.some(s => Math.abs(s - phase) < 1e-6) ? null : fits;
122
+ }
111
123
  /** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */
112
124
  export const shiftable = (track) => track.loop === 'wrap' && !track.accumulates;
113
125
  /**
@@ -135,4 +147,81 @@ export const trackEdit = (clip, track, patch = {}) => ({
135
147
  loop: patch.loop ?? track.loop,
136
148
  interpolation: patch.interpolation ?? track.interpolation
137
149
  });
150
+ /** Whether two control values are one: the figure's numbers carry float noise past a millionth. */
151
+ const same = (a, b) => Math.abs(a - b) < 1e-6;
152
+ /**
153
+ * A new track's keys. By default it holds the pose the figure stands in now, from start to end: a track added to a pose
154
+ * made with care does not throw it to the control's ends (review 2026-11-09). `range` is the preset that swings the
155
+ * control over its whole range and back, so the loop closes where it began.
156
+ */
157
+ export function newTrackKeys(input, current, duration, how = 'hold') {
158
+ const min = input.min ?? 0;
159
+ const max = input.max ?? min;
160
+ if (how === 'range')
161
+ return [{ seconds: 0, value: min }, { seconds: round(duration / 2), value: max }, { seconds: duration, value: min }];
162
+ const value = current ?? min;
163
+ return [{ seconds: 0, value }, { seconds: duration, value }];
164
+ }
165
+ /**
166
+ * The edits that make a new clip `clip` with one track on `state` (ADR-0051 §3.2: only clips of one motion group play
167
+ * one control). A control no clip plays is taken as it is. A control a group's clips play puts the new clip in that
168
+ * group. A control one clip in no group plays makes a group of that clip and the new one -- the person adds 「열기」 to a
169
+ * door that has 「닫기」, and the two become one group, one playing at a time, without having to design the group first.
170
+ * All of it one step of the history.
171
+ */
172
+ export function newClipEdits(asset, state, clip, duration, keys) {
173
+ const track = { kind: 'set-driver', state, clip, duration, keys };
174
+ const writers = [...new Set((asset.drivers ?? []).filter(d => d.state === state).map(d => d.clip))];
175
+ if (!writers.length)
176
+ return { edits: [track], join: { kind: 'free' } };
177
+ const group = (asset.motionGroups ?? []).find(g => writers.some(c => g.clips.includes(c)));
178
+ if (group)
179
+ return { edits: [{ ...track, group: group.id }], join: { kind: 'group', group: group.id, label: group.label } };
180
+ const id = freeGroupIdOf(asset);
181
+ const label = asset.stateInputs?.[state]?.label ?? state;
182
+ const other = writers[0];
183
+ return {
184
+ edits: [{ kind: 'set-motion-group', group: { id, label, clips: [other], defaultClip: other, transitionSeconds: 0.2 } }, { ...track, group: id }],
185
+ join: { kind: 'new-group', group: id, label, clip: other }
186
+ };
187
+ }
188
+ /**
189
+ * 「이 자세 기억하기」: the pose the figure stands in now, kept in `clip` at `seconds` in one step (review 2026-11-09:
190
+ * a six-axis pose was six tracks keyed one by one). A track whose value there differs gets a key; a control the clip
191
+ * does not play yet and that moved since `base` -- the pose the clip was last shown or kept in -- gets a track that
192
+ * starts from where it was and reaches the pose at that moment (from the start when the moment is 0). A moved control
193
+ * another clip outside this clip's group plays is not taken; it is named in `skipped`.
194
+ */
195
+ export function rememberedPoseEdits(asset, clip, seconds, values, base) {
196
+ const found = clipsOf(asset).find(c => c.name === clip);
197
+ if (!found)
198
+ return { edits: [], skipped: [] };
199
+ const at = round(Math.min(Math.max(seconds, 0), found.duration));
200
+ const there = new Map(poseAt(asset, clip, at).map(c => [c.input, c.value]));
201
+ const edits = [];
202
+ for (const track of found.tracks) {
203
+ const now = values[track.state];
204
+ if (now === undefined || same(now, there.get(track.state) ?? now))
205
+ continue;
206
+ edits.push(trackEdit(found, track, { keys: withKey(track.keys, { seconds: at, value: now }) }));
207
+ }
208
+ const free = new Set(undrivenOf(asset, clip));
209
+ const skipped = [];
210
+ const played = new Set(found.tracks.map(t => t.state));
211
+ for (const input of asset.document.model.inputs) {
212
+ if (input.role !== 'state' || played.has(input.id))
213
+ continue;
214
+ const now = values[input.id];
215
+ const was = base[input.id];
216
+ if (now === undefined || was === undefined || same(now, was))
217
+ continue;
218
+ if (!free.has(input.id)) {
219
+ skipped.push(input.id);
220
+ continue;
221
+ }
222
+ const keys = at === 0 ? [{ seconds: 0, value: now }, { seconds: found.duration, value: now }] : [{ seconds: 0, value: was }, { seconds: at, value: now }];
223
+ edits.push({ kind: 'set-driver', state: input.id, clip, keys });
224
+ }
225
+ return { edits, skipped };
226
+ }
138
227
  //# sourceMappingURL=clips.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"clips.js","sourceRoot":"","sources":["../../client/modeller/clips.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AA4B7E,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;AAExD,MAAM,UAAU,OAAO,CAAC,KAAkB;IACxC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACvE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAA;IACrC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;QACzF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACf,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,KAAK;YACrD,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,EAAE;YACvB,GAAG,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;YACpB,GAAG,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;YACpB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACnF,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;SAC3B,CAAC,CAAA;QACF,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAkB,EAAE,IAAI,GAAG,EAAE,EAAE,KAAc;IACtE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAU,CAAC,CAAC,CAAC,CAAA;IACtG,MAAM,IAAI,GAAG,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAC5D,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAC9B,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAA;IAC/H,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AACnG,CAAC;AAWD,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,KAAkB;IAC7C,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;IACnF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;IACrD,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9F,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AACzB,CAAC;AAED,wHAAwH;AACxH,MAAM,UAAU,aAAa,CAAC,KAAkB;IAC9C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAA;IACzC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,OAAO,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAAE,CAAC,EAAE,CAAA;IACpC,OAAO,UAAU,CAAC,EAAE,CAAA;AACtB,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAgB,EAAE,QAAwC,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1F,IAAI,EAAE,kBAA2B;IACjC,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,iBAAiB,EAAE;CAC1N,CAAC,CAAA;AAEF,mGAAmG;AACnG,MAAM,UAAU,MAAM,CAAC,KAAkB,EAAE,IAAY,EAAE,OAAe;IACtE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3E,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;SACzB,MAAM,CAAC,CAAC,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;SAC5C,GAAG,CAAC,CAAC,CAAC,EAAE;QACP,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAE,CAAA;QAChC,OAAO,EAAE,IAAI,EAAE,WAAoB,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAA;IAChJ,CAAC,CAAC,CAAA;AACN,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,OAAO,CAAC,IAAwB,EAAE,GAAY;IAC5D,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAA;AACpG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAwB,EAAE,KAAa,EAAE,KAAuB,EAAE,QAAgB;IAC/G,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,EAAE,CAAA;IACvF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/E,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAClD,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC5D,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/E,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAA;AAC3F,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,UAAU,CAAC,IAAwB,EAAE,KAAa;IAChE,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAChD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAA;AAC3C,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAAC,KAAkB,EAAE,IAAY;IAC7D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAC7D,MAAM,IAAI,GAAG,GAAG,IAAI,OAAO,CAAA;IAC3B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACjC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC;QAAE,CAAC,EAAE,CAAA;IACrC,OAAO,GAAG,IAAI,IAAI,CAAC,EAAE,CAAA;AACvB,CAAC;AAED,wGAAwG;AACxG,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAkB,EAAE,IAAY,EAAY,EAAE,CAAC;IAC3E,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;CAClH,CAAA;AAED,6GAA6G;AAC7G,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAA;AAE1F;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAkB,EAAE,IAAU,EAAE,KAAgB,EAAE,MAAc,EAAE,OAAe;IAC/G,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAE,CAAA;IAChG,OAAO;QACL,IAAI,EAAE,YAAqB;QAC3B,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI;QACrE,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CAAA;AACH,CAAC;AAED,yCAAyC;AACzC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAU,EAAE,KAAgB,EAAE,QAAqE,EAAE,EAAE,EAAE,CAAC,CAAC;IACnI,IAAI,EAAE,YAAqB;IAC3B,KAAK,EAAE,KAAK,CAAC,KAAK;IAClB,IAAI,EAAE,IAAI,CAAC,IAAI;IACf,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI;IAC9B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI;IAC9B,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,aAAa;CAC1D,CAAC,CAAA","sourcesContent":["import { driverValue, figureShiftedDriverKeys } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureDriver, FigureInputChange } from '@hatiolab/figure-model'\n\n/*\n * The figure's clips: controls played by keys over time, grouped into clips that each run on one\n * clock. Kept apart from the element so the reading is tested without a browser.\n */\nexport interface ClipKey {\n seconds: number\n value: number\n}\nexport interface ClipTrack {\n state: string\n label: string\n unit: string\n min: number\n max: number\n keys: ClipKey[]\n interpolation: 'linear' | 'step'\n loop: 'wrap' | 'hold'\n accumulates: boolean\n}\nexport interface Clip {\n name: string\n duration: number\n tracks: ClipTrack[]\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\nexport function clipsOf(asset: FigureAsset): Clip[] {\n const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]))\n const clips = new Map<string, Clip>()\n for (const d of asset.drivers ?? []) {\n const input = inputs.get(d.state)\n const clip = clips.get(d.clip) ?? { name: d.clip, duration: d.time.duration, tracks: [] }\n clip.tracks.push({\n state: d.state,\n label: asset.stateInputs?.[d.state]?.label ?? d.state,\n unit: input?.unit ?? '',\n min: input?.min ?? 0,\n max: input?.max ?? 0,\n keys: d.keys.map(k => ({ seconds: round(k.at * d.time.duration), value: k.value })),\n interpolation: d.interpolation,\n loop: d.loop,\n accumulates: d.accumulates\n })\n clips.set(d.clip, clip)\n }\n return [...clips.values()]\n}\n\n/**\n * The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays\n * outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip\n * plays. Without a clip, the controls no clip plays at all.\n */\nexport function undrivenOf(asset: FigureAsset, clip = '', group?: string): string[] {\n const groupOf = new Map((asset.motionGroups ?? []).flatMap(g => g.clips.map(c => [c, g.id] as const)))\n const mine = group ?? (clip ? groupOf.get(clip) : undefined)\n const taken = (state: string) =>\n (asset.drivers ?? []).some(d => d.state === state && (d.clip === clip || mine === undefined || groupOf.get(d.clip) !== mine))\n return asset.document.model.inputs.filter(i => i.role === 'state' && !taken(i.id)).map(i => i.id)\n}\n\n/** A motion group as the panel shows it: its setting, and the clips it holds in the clips' order. */\nexport interface ClipGroup {\n id: string\n label: string\n clips: string[]\n defaultClip: string\n transitionSeconds: number\n}\n\n/** The figure's motion groups, and the clips in none -- the ones a group can take. */\nexport function clipGroupsOf(asset: FigureAsset): { groups: ClipGroup[]; free: string[] } {\n const groups = (asset.motionGroups ?? []).map(g => ({ ...g, clips: [...g.clips] }))\n const grouped = new Set(groups.flatMap(g => g.clips))\n const free = [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c))\n return { groups, free }\n}\n\n/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */\nexport function freeGroupIdOf(asset: FigureAsset): string {\n const taken = new Set((asset.motionGroups ?? []).map(g => g.id))\n if (!taken.has('motion')) return 'motion'\n let n = 2\n while (taken.has(`motion-${n}`)) n++\n return `motion-${n}`\n}\n\n/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */\nexport const groupEdit = (group: ClipGroup, patch: Partial<Omit<ClipGroup, 'id'>> = {}) => ({\n kind: 'set-motion-group' as const,\n group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }\n})\n\n/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */\nexport function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {\n const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))\n return (asset.drivers ?? [])\n .filter((d: FigureDriver) => d.clip === clip)\n .map(d => {\n const unit = units.get(d.state)!\n return { kind: 'set-state' as const, input: d.state, unit, value: driverValue(d, seconds, unit === 'deg' || unit === 'rad' ? unit : 'other') }\n })\n}\n\n/** A key added at a moment, replacing one already there; the keys stay in time order. */\nexport function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[] {\n return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds)\n}\n\n/**\n * A key typed where it stands: its moment, its value, or both. The first key stays at 0 (a track starts there); a moment\n * outside the clip, or on another key's, is refused (null) rather than taking that key's place unseen.\n */\nexport function withKeyChanged(keys: readonly ClipKey[], index: number, patch: Partial<ClipKey>, duration: number): ClipKey[] | null {\n const was = keys[index]\n if (!was) return null\n const next = { seconds: patch.seconds ?? was.seconds, value: patch.value ?? was.value }\n if (!Number.isFinite(next.seconds) || !Number.isFinite(next.value)) return null\n if (index === 0 && next.seconds !== 0) return null\n if (next.seconds < 0 || next.seconds > duration) return null\n if (keys.some((k, i) => i !== index && k.seconds === next.seconds)) return null\n return keys.map((k, i) => (i === index ? next : k)).sort((a, b) => a.seconds - b.seconds)\n}\n\n/** A track's keys without one; a track keeps at least its two ends, as the format asks. */\nexport function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null {\n if (keys.length <= 2 || index === 0) return null\n return keys.filter((_, i) => i !== index)\n}\n\n/** A name no clip has, for a copy: `walk-copy`, then `walk-copy-2`, … */\nexport function freeClipNameOf(asset: FigureAsset, clip: string): string {\n const taken = new Set((asset.drivers ?? []).map(d => d.clip))\n const base = `${clip}-copy`\n if (!taken.has(base)) return base\n let n = 2\n while (taken.has(`${base}-${n}`)) n++\n return `${base}-${n}`\n}\n\n/** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */\nexport const copyTargetsOf = (asset: FigureAsset, clip: string): string[] => [\n ...new Set([...(asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state), ...undrivenOf(asset, clip)])\n]\n\n/** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */\nexport const shiftable = (track: ClipTrack) => track.loop === 'wrap' && !track.accumulates\n\n/**\n * A track copied onto `target` -- itself, or another control -- played `seconds` later round its loop (robot dog\n * 2026-11-03: a leg's swing half a period after another's). One edit; a track the clip plays on `target` is replaced.\n */\nexport function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: ClipTrack, target: string, seconds: number) {\n const driver = (asset.drivers ?? []).find(d => d.clip === clip.name && d.state === track.state)!\n return {\n kind: 'set-driver' as const,\n state: target,\n clip: clip.name,\n keys: seconds ? figureShiftedDriverKeys(driver, seconds) : track.keys,\n loop: track.loop,\n interpolation: track.interpolation,\n accumulates: track.accumulates\n }\n}\n\n/** The edit that writes a track back. */\nexport const trackEdit = (clip: Clip, track: ClipTrack, patch: Partial<Pick<ClipTrack, 'keys' | 'loop' | 'interpolation'>> = {}) => ({\n kind: 'set-driver' as const,\n state: track.state,\n clip: clip.name,\n keys: patch.keys ?? track.keys,\n loop: patch.loop ?? track.loop,\n interpolation: patch.interpolation ?? track.interpolation\n})\n"]}
1
+ {"version":3,"file":"clips.js","sourceRoot":"","sources":["../../client/modeller/clips.ts"],"names":[],"mappings":"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","sourcesContent":["import { driverValue, figureShiftedDriverKeys } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureDriver, FigureInputChange } from '@hatiolab/figure-model'\n\n/*\n * The figure's clips: controls played by keys over time, grouped into clips that each run on one\n * clock. Kept apart from the element so the reading is tested without a browser.\n */\nexport interface ClipKey {\n seconds: number\n value: number\n}\nexport interface ClipTrack {\n state: string\n label: string\n unit: string\n min: number\n max: number\n keys: ClipKey[]\n interpolation: 'linear' | 'step' | 'smooth'\n loop: 'wrap' | 'hold'\n accumulates: boolean\n}\nexport interface Clip {\n name: string\n duration: number\n tracks: ClipTrack[]\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\nexport function clipsOf(asset: FigureAsset): Clip[] {\n const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]))\n const clips = new Map<string, Clip>()\n for (const d of asset.drivers ?? []) {\n const input = inputs.get(d.state)\n const clip = clips.get(d.clip) ?? { name: d.clip, duration: d.time.duration, tracks: [] }\n clip.tracks.push({\n state: d.state,\n label: asset.stateInputs?.[d.state]?.label ?? d.state,\n unit: input?.unit ?? '',\n min: input?.min ?? 0,\n max: input?.max ?? 0,\n keys: d.keys.map(k => ({ seconds: round(k.at * d.time.duration), value: k.value })),\n interpolation: d.interpolation,\n loop: d.loop,\n accumulates: d.accumulates\n })\n clips.set(d.clip, clip)\n }\n return [...clips.values()]\n}\n\n/**\n * The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays\n * outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip\n * plays. Without a clip, the controls no clip plays at all.\n */\nexport function undrivenOf(asset: FigureAsset, clip = '', group?: string): string[] {\n const groupOf = new Map((asset.motionGroups ?? []).flatMap(g => g.clips.map(c => [c, g.id] as const)))\n const mine = group ?? (clip ? groupOf.get(clip) : undefined)\n const taken = (state: string) =>\n (asset.drivers ?? []).some(d => d.state === state && (d.clip === clip || mine === undefined || groupOf.get(d.clip) !== mine))\n return asset.document.model.inputs.filter(i => i.role === 'state' && !taken(i.id)).map(i => i.id)\n}\n\n/** A motion group as the panel shows it: its setting, and the clips it holds in the clips' order. */\nexport interface ClipGroup {\n id: string\n label: string\n clips: string[]\n defaultClip: string\n transitionSeconds: number\n}\n\n/** The figure's motion groups, and the clips in none -- the ones a group can take. */\nexport function clipGroupsOf(asset: FigureAsset): { groups: ClipGroup[]; free: string[] } {\n const groups = (asset.motionGroups ?? []).map(g => ({ ...g, clips: [...g.clips] }))\n const grouped = new Set(groups.flatMap(g => g.clips))\n const free = [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c))\n return { groups, free }\n}\n\n/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */\nexport function freeGroupIdOf(asset: FigureAsset): string {\n const taken = new Set((asset.motionGroups ?? []).map(g => g.id))\n if (!taken.has('motion')) return 'motion'\n let n = 2\n while (taken.has(`motion-${n}`)) n++\n return `motion-${n}`\n}\n\n/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */\nexport const groupEdit = (group: ClipGroup, patch: Partial<Omit<ClipGroup, 'id'>> = {}) => ({\n kind: 'set-motion-group' as const,\n group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }\n})\n\n/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */\nexport function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {\n const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))\n return (asset.drivers ?? [])\n .filter((d: FigureDriver) => d.clip === clip)\n .map(d => {\n const unit = units.get(d.state)!\n return { kind: 'set-state' as const, input: d.state, unit, value: driverValue(d, seconds, unit === 'deg' || unit === 'rad' ? unit : 'other') }\n })\n}\n\n/** A key added at a moment, replacing one already there; the keys stay in time order. */\nexport function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[] {\n return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds)\n}\n\n/**\n * A key typed where it stands: its moment, its value, or both. The first key stays at 0 (a track starts there); a moment\n * outside the clip, or on another key's, is refused (null) rather than taking that key's place unseen.\n */\nexport function withKeyChanged(keys: readonly ClipKey[], index: number, patch: Partial<ClipKey>, duration: number): ClipKey[] | null {\n const was = keys[index]\n if (!was) return null\n const next = { seconds: patch.seconds ?? was.seconds, value: patch.value ?? was.value }\n if (!Number.isFinite(next.seconds) || !Number.isFinite(next.value)) return null\n if (index === 0 && next.seconds !== 0) return null\n if (next.seconds < 0 || next.seconds > duration) return null\n if (keys.some((k, i) => i !== index && k.seconds === next.seconds)) return null\n return keys.map((k, i) => (i === index ? next : k)).sort((a, b) => a.seconds - b.seconds)\n}\n\n/** A track's keys without one; a track keeps at least its two ends, as the format asks. */\nexport function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null {\n if (keys.length <= 2 || index === 0) return null\n return keys.filter((_, i) => i !== index)\n}\n\n/** A name no clip has, for a copy: `walk-copy`, then `walk-copy-2`, … */\nexport function freeClipNameOf(asset: FigureAsset, clip: string): string {\n const taken = new Set((asset.drivers ?? []).map(d => d.clip))\n const base = `${clip}-copy`\n if (!taken.has(base)) return base\n let n = 2\n while (taken.has(`${base}-${n}`)) n++\n return `${base}-${n}`\n}\n\n/** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */\nexport const copyTargetsOf = (asset: FigureAsset, clip: string): string[] => [\n ...new Set([...(asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state), ...undrivenOf(asset, clip)])\n]\n\n/**\n * Where a smooth track may be moved round its loop to, in seconds later, when `seconds` is not one of them: figure-model\n * moves a smooth track only so that its new start is one of its keys (`driver-shift-off-key`). Null when it may go there.\n */\nexport function shiftOffKeyOf(track: ClipTrack, duration: number, seconds: number): number[] | null {\n if (track.interpolation !== 'smooth' || !seconds) return null\n const D = duration\n const phase = ((seconds % D) + D) % D\n const fits = [...new Set(track.keys.map(k => round((D - k.seconds) % D)))].sort((a, b) => a - b)\n return fits.some(s => Math.abs(s - phase) < 1e-6) ? null : fits\n}\n\n/** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */\nexport const shiftable = (track: ClipTrack) => track.loop === 'wrap' && !track.accumulates\n\n/**\n * A track copied onto `target` -- itself, or another control -- played `seconds` later round its loop (robot dog\n * 2026-11-03: a leg's swing half a period after another's). One edit; a track the clip plays on `target` is replaced.\n */\nexport function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: ClipTrack, target: string, seconds: number) {\n const driver = (asset.drivers ?? []).find(d => d.clip === clip.name && d.state === track.state)!\n return {\n kind: 'set-driver' as const,\n state: target,\n clip: clip.name,\n keys: seconds ? figureShiftedDriverKeys(driver, seconds) : track.keys,\n loop: track.loop,\n interpolation: track.interpolation,\n accumulates: track.accumulates\n }\n}\n\n/** The edit that writes a track back. */\nexport const trackEdit = (clip: Clip, track: ClipTrack, patch: Partial<Pick<ClipTrack, 'keys' | 'loop' | 'interpolation'>> = {}) => ({\n kind: 'set-driver' as const,\n state: track.state,\n clip: clip.name,\n keys: patch.keys ?? track.keys,\n loop: patch.loop ?? track.loop,\n interpolation: patch.interpolation ?? track.interpolation\n})\n\n/** Whether two control values are one: the figure's numbers carry float noise past a millionth. */\nconst same = (a: number, b: number) => Math.abs(a - b) < 1e-6\n\n/**\n * A new track's keys. By default it holds the pose the figure stands in now, from start to end: a track added to a pose\n * made with care does not throw it to the control's ends (review 2026-11-09). `range` is the preset that swings the\n * control over its whole range and back, so the loop closes where it began.\n */\nexport function newTrackKeys(input: { min?: number; max?: number }, current: number | undefined, duration: number, how: 'hold' | 'range' = 'hold'): ClipKey[] {\n const min = input.min ?? 0\n const max = input.max ?? min\n if (how === 'range') return [{ seconds: 0, value: min }, { seconds: round(duration / 2), value: max }, { seconds: duration, value: min }]\n const value = current ?? min\n return [{ seconds: 0, value }, { seconds: duration, value }]\n}\n\n/** What a new clip on a control joins, so that it may play it: nothing, a group already there, or a group made with the clip that plays it now. */\nexport type TrackJoin = { kind: 'free' } | { kind: 'group'; group: string; label: string } | { kind: 'new-group'; group: string; label: string; clip: string }\n\n/**\n * The edits that make a new clip `clip` with one track on `state` (ADR-0051 §3.2: only clips of one motion group play\n * one control). A control no clip plays is taken as it is. A control a group's clips play puts the new clip in that\n * group. A control one clip in no group plays makes a group of that clip and the new one -- the person adds 「열기」 to a\n * door that has 「닫기」, and the two become one group, one playing at a time, without having to design the group first.\n * All of it one step of the history.\n */\nexport function newClipEdits(asset: FigureAsset, state: string, clip: string, duration: number, keys: ClipKey[]): { edits: unknown[]; join: TrackJoin } {\n const track = { kind: 'set-driver' as const, state, clip, duration, keys }\n const writers = [...new Set((asset.drivers ?? []).filter(d => d.state === state).map(d => d.clip))]\n if (!writers.length) return { edits: [track], join: { kind: 'free' } }\n const group = (asset.motionGroups ?? []).find(g => writers.some(c => g.clips.includes(c)))\n if (group) return { edits: [{ ...track, group: group.id }], join: { kind: 'group', group: group.id, label: group.label } }\n const id = freeGroupIdOf(asset)\n const label = asset.stateInputs?.[state]?.label ?? state\n const other = writers[0]!\n return {\n edits: [{ kind: 'set-motion-group' as const, group: { id, label, clips: [other], defaultClip: other, transitionSeconds: 0.2 } }, { ...track, group: id }],\n join: { kind: 'new-group', group: id, label, clip: other }\n }\n}\n\n/**\n * 「이 자세 기억하기」: the pose the figure stands in now, kept in `clip` at `seconds` in one step (review 2026-11-09:\n * a six-axis pose was six tracks keyed one by one). A track whose value there differs gets a key; a control the clip\n * does not play yet and that moved since `base` -- the pose the clip was last shown or kept in -- gets a track that\n * starts from where it was and reaches the pose at that moment (from the start when the moment is 0). A moved control\n * another clip outside this clip's group plays is not taken; it is named in `skipped`.\n */\nexport function rememberedPoseEdits(asset: FigureAsset, clip: string, seconds: number, values: Record<string, number>, base: Record<string, number>): { edits: unknown[]; skipped: string[] } {\n const found = clipsOf(asset).find(c => c.name === clip)\n if (!found) return { edits: [], skipped: [] }\n const at = round(Math.min(Math.max(seconds, 0), found.duration))\n const there = new Map(poseAt(asset, clip, at).map(c => [c.input, c.value]))\n const edits: unknown[] = []\n for (const track of found.tracks) {\n const now = values[track.state]\n if (now === undefined || same(now, there.get(track.state) ?? now)) continue\n edits.push(trackEdit(found, track, { keys: withKey(track.keys, { seconds: at, value: now }) }))\n }\n const free = new Set(undrivenOf(asset, clip))\n const skipped: string[] = []\n const played = new Set(found.tracks.map(t => t.state))\n for (const input of asset.document.model.inputs) {\n if (input.role !== 'state' || played.has(input.id)) continue\n const now = values[input.id]\n const was = base[input.id]\n if (now === undefined || was === undefined || same(now, was)) continue\n if (!free.has(input.id)) {\n skipped.push(input.id)\n continue\n }\n const keys = at === 0 ? [{ seconds: 0, value: now }, { seconds: found.duration, value: now }] : [{ seconds: 0, value: was }, { seconds: at, value: now }]\n edits.push({ kind: 'set-driver' as const, state: input.id, clip, keys })\n }\n return { edits, skipped }\n}\n"]}
@@ -132,6 +132,42 @@ export const EditorStyles = css `
132
132
  min-height: 0;
133
133
  }
134
134
  /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */
135
+ /* What a drag does on the joint of the part chosen: one line under the board, its buttons quiet text (review 2026-11-09). */
136
+ p[joint-handle] {
137
+ position: absolute;
138
+ bottom: 12px;
139
+ left: 50%;
140
+ z-index: 5;
141
+ transform: translateX(-50%);
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ align-items: center;
145
+ justify-content: center;
146
+ gap: 2px 8px;
147
+ /* As wide as its words: at left 50% the line would otherwise wrap at half the board. */
148
+ width: max-content;
149
+ max-width: calc(100% - 24px);
150
+ box-sizing: border-box;
151
+ margin: 0;
152
+ padding: 4px 6px 4px 12px;
153
+ border-radius: 16px;
154
+ background: var(--md-sys-color-surface-container-high);
155
+ color: var(--md-sys-color-on-surface-variant);
156
+ font-size: var(--ui-t-small, 11.5px);
157
+ }
158
+ p[joint-handle] button {
159
+ padding: 2px 6px;
160
+ border: none;
161
+ border-radius: 4px;
162
+ background: none;
163
+ color: var(--md-sys-color-primary);
164
+ font: inherit;
165
+ font-weight: 600;
166
+ cursor: pointer;
167
+ }
168
+ p[joint-handle] button:hover {
169
+ background: var(--md-sys-color-surface-container-highest);
170
+ }
135
171
  p[same-measure] {
136
172
  position: absolute;
137
173
  bottom: 12px;
@@ -1 +1 @@
1
- {"version":3,"file":"editor-styles.js","sourceRoot":"","sources":["../../client/modeller/editor-styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoc9B,CAAA","sourcesContent":["import { css } from 'lit'\n\n/** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */\nexport const EditorStyles = css`\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */\n container-type: inline-size;\n /* The app's one face (its typography theme's --theme-font) at the body size: a bare sans-serif here put a second\n face under everything the editor draws, beside the parts list and the shell in the app's own (2026-10-16). */\n font-family: var(--theme-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n }\n /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and\n fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */\n ox-canvas-bar button[mode] {\n /* The bar's height, beside its 32px icons and the save (sweep 2026-10-27 U25). */\n height: 32px;\n border: 1px solid transparent;\n padding: 0 10px;\n border-radius: 999em;\n border-color: transparent;\n background: none;\n }\n ox-canvas-bar button[mode][aria-pressed='true'] {\n border-color: var(--md-sys-color-primary);\n }\n /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */\n ox-canvas-bar .gizmo-tools {\n display: inline-flex;\n gap: 2px;\n padding-left: 4px;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ox-canvas-bar button[tool] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n ox-canvas-bar button[tool]:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n ox-canvas-bar button[tool][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n ox-canvas-bar button[tool]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n /*\n Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they\n showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.\n */\n ox-canvas-bar button[tool] md-icon {\n --md-icon-size: 20px;\n }\n ox-canvas-bar button[tool]:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n /* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */\n div[middle][hidden] {\n display: none;\n }\n /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */\n figure-preview[hidden] {\n display: none;\n }\n ox-canvas-bar .status {\n padding: 0 8px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):\n round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,\n a promise to whoever draws the figure, is outlined.\n */\n ox-canvas-bar button[save],\n ox-canvas-bar button[publish] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 14px 0 10px;\n border-radius: 999em;\n font-weight: 600;\n }\n ox-canvas-bar button[save] md-icon,\n ox-canvas-bar button[publish] md-icon {\n --md-icon-size: 18px;\n }\n ox-canvas-bar button[save] {\n border: 1px solid transparent;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n }\n /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under\n 860px and one column with the middle first under 560px are the frame's, as are the dividers. */\n ox-three-pane {\n flex: 1;\n min-height: 0;\n }\n figure-parts {\n min-height: 0;\n }\n figure-canvas {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n div[middle] {\n position: relative;\n display: flex;\n min-width: 0;\n min-height: 0;\n }\n /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */\n p[same-measure] {\n position: absolute;\n bottom: 12px;\n left: 50%;\n z-index: 5;\n transform: translateX(-50%);\n margin: 0;\n padding: 4px 10px;\n border-radius: 999em;\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-small, 11.5px);\n pointer-events: none;\n white-space: nowrap;\n }\n /* A new figure's first question, over the empty canvas. */\n div[start] {\n position: absolute;\n inset: 50% auto auto 50%;\n z-index: 10;\n color: var(--md-sys-color-on-surface);\n transform: translate(-50%, -50%);\n width: min(440px, calc(100% - 32px));\n box-sizing: border-box;\n padding: 20px;\n border-radius: 12px;\n background: var(--md-sys-color-surface, #fff);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);\n }\n div[start] button[start-button] {\n width: auto;\n margin: 0;\n font-size: var(--ui-t-body);\n height: 36px;\n padding: 0 16px;\n border: 1px solid var(--md-sys-color-outline);\n border-radius: 999em;\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface, #fff);\n cursor: pointer;\n }\n div[start] button[start-button]:hover:not(:disabled) {\n background: var(--md-sys-color-primary-container);\n }\n div[start] button[start-button][pressed] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: var(--md-sys-color-primary);\n }\n div[start] button[start-button]:disabled {\n opacity: 0.5;\n cursor: default;\n }\n div[start] h2 {\n margin: 0 0 12px;\n font-size: var(--ui-t-title);\n }\n div[start-paths] {\n display: grid;\n gap: 14px;\n }\n div[start-paths] h3 {\n margin: 0 0 6px;\n font-size: var(--ui-t-body);\n }\n div[start-shapes] {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n div[start] small {\n display: block;\n margin-top: 4px;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[start] button[start-more] {\n display: block;\n text-align: left;\n margin: 4px 0 0;\n padding: 0;\n font: inherit;\n font-size: var(--ui-t-small);\n color: var(--md-sys-color-primary);\n background: none;\n border: none;\n cursor: pointer;\n }\n /* No rule under the last way to start: it closed nothing (sweep 2026-10-27 U23). */\n div[start-paths] > section:last-of-type {\n border-bottom: none;\n }\n /* The side: tabs over one panel at a time, each using the whole height. */\n /*\n The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body\n for the bar, and the fields the editor drew itself -- the part's name -- stood a step larger than every other field.\n */\n aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n font-size: var(--ui-t-small, 11.5px);\n background: var(--md-sys-color-surface-container-lowest);\n }\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n min-width: 0;\n height: 48px;\n padding: 0;\n border: none;\n border-radius: 0;\n border-bottom: 2px solid transparent;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n button[tab]:hover:not([on]) {\n background: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n button[tab] span[tab-label] {\n max-width: 100%;\n overflow: hidden;\n font-size: var(--ui-t-micro, 11px);\n line-height: 1.2;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n /* A range's two ends read as one: smallest – largest. */\n div[pair][range] {\n align-items: center;\n }\n div[pair][range] > span[dash] {\n flex: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */\n div[panel] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0;\n }\n div[panel][flush] {\n padding: 0;\n }\n /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */\n [off] {\n display: none !important;\n }\n /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */\n div[group-fields] {\n display: grid;\n gap: 6px;\n }\n /* The folded 「더 보기」: its title is the fold's handle, at the title's size. */\n section[more] > details > summary {\n padding: 0;\n }\n section[more] > details > summary > h3 {\n margin: 0;\n }\n div[more-body] {\n display: grid;\n gap: 16px;\n margin-top: 12px;\n }\n /*\n The view settings and the release history, opened from the bar over the right of the work: neither is about the\n part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in\n the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).\n */\n :host {\n position: relative;\n }\n div[side-overlay] {\n position: absolute;\n top: var(--figure-bar-height, 44px);\n right: 8px;\n z-index: 5;\n display: flex;\n flex-direction: column;\n width: min(360px, calc(100% - 16px));\n max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);\n background: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);\n }\n div[overlay-head] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[overlay-head] h3 {\n margin: 0;\n }\n div[overlay-head] button[quiet] {\n margin-left: 0;\n }\n [overlay-body] {\n min-height: 0;\n overflow: auto;\n }\n /* Which part the panel speaks of. Above z-index 1: a chip's label (md-filter-chip) sits at 1 and, coming later, was\n drawn over it when the panel scrolled (sweep 2026-10-27 U2). */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n gap: 4px;\n padding: 10px 12px;\n background: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */\n div[subject-summary] {\n display: grid;\n gap: 2px;\n }\n div[subject-summary] b {\n font-size: var(--ui-t-body, 13px);\n font-weight: 600;\n }\n div[subject-summary] b span {\n font-weight: 400;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[subject-summary] small {\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one\n height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도\n 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).\n */\n button {\n font: inherit;\n box-sizing: border-box;\n color: inherit;\n min-width: 0;\n cursor: pointer;\n }\n /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */\n ox-canvas-bar button[release-blocked] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n padding: 0 14px 0 10px;\n border: none;\n /* Round as save and release beside it: one shape for the bar's end. */\n border-radius: 999em;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n }\n ox-canvas-bar button[release-blocked] md-icon {\n --md-icon-size: 18px;\n }\n div[release-blockers] {\n position: absolute;\n top: 46px;\n right: 8px;\n z-index: 6;\n width: min(340px, calc(100% - 16px));\n padding: 8px 10px 10px;\n box-sizing: border-box;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);\n }\n ox-canvas-bar button[publish] {\n border: 1px solid var(--md-sys-color-outline);\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface);\n }\n ox-canvas-bar button[publish][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n .hint {\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n /* A mirror asked about: under its row, the panel's own field rows (what comes with it, its names, how it turns). */\n div[mirror-form] {\n display: grid;\n gap: 6px;\n margin: 4px 0 8px;\n }\n`\n"]}
1
+ {"version":3,"file":"editor-styles.js","sourceRoot":"","sources":["../../client/modeller/editor-styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwe9B,CAAA","sourcesContent":["import { css } from 'lit'\n\n/** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */\nexport const EditorStyles = css`\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */\n container-type: inline-size;\n /* The app's one face (its typography theme's --theme-font) at the body size: a bare sans-serif here put a second\n face under everything the editor draws, beside the parts list and the shell in the app's own (2026-10-16). */\n font-family: var(--theme-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n }\n /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and\n fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */\n ox-canvas-bar button[mode] {\n /* The bar's height, beside its 32px icons and the save (sweep 2026-10-27 U25). */\n height: 32px;\n border: 1px solid transparent;\n padding: 0 10px;\n border-radius: 999em;\n border-color: transparent;\n background: none;\n }\n ox-canvas-bar button[mode][aria-pressed='true'] {\n border-color: var(--md-sys-color-primary);\n }\n /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */\n ox-canvas-bar .gizmo-tools {\n display: inline-flex;\n gap: 2px;\n padding-left: 4px;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ox-canvas-bar button[tool] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n ox-canvas-bar button[tool]:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n ox-canvas-bar button[tool][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n ox-canvas-bar button[tool]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n /*\n Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they\n showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.\n */\n ox-canvas-bar button[tool] md-icon {\n --md-icon-size: 20px;\n }\n ox-canvas-bar button[tool]:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n /* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */\n div[middle][hidden] {\n display: none;\n }\n /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */\n figure-preview[hidden] {\n display: none;\n }\n ox-canvas-bar .status {\n padding: 0 8px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):\n round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,\n a promise to whoever draws the figure, is outlined.\n */\n ox-canvas-bar button[save],\n ox-canvas-bar button[publish] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 14px 0 10px;\n border-radius: 999em;\n font-weight: 600;\n }\n ox-canvas-bar button[save] md-icon,\n ox-canvas-bar button[publish] md-icon {\n --md-icon-size: 18px;\n }\n ox-canvas-bar button[save] {\n border: 1px solid transparent;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n }\n /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under\n 860px and one column with the middle first under 560px are the frame's, as are the dividers. */\n ox-three-pane {\n flex: 1;\n min-height: 0;\n }\n figure-parts {\n min-height: 0;\n }\n figure-canvas {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n div[middle] {\n position: relative;\n display: flex;\n min-width: 0;\n min-height: 0;\n }\n /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */\n /* What a drag does on the joint of the part chosen: one line under the board, its buttons quiet text (review 2026-11-09). */\n p[joint-handle] {\n position: absolute;\n bottom: 12px;\n left: 50%;\n z-index: 5;\n transform: translateX(-50%);\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: center;\n gap: 2px 8px;\n /* As wide as its words: at left 50% the line would otherwise wrap at half the board. */\n width: max-content;\n max-width: calc(100% - 24px);\n box-sizing: border-box;\n margin: 0;\n padding: 4px 6px 4px 12px;\n border-radius: 16px;\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-small, 11.5px);\n }\n p[joint-handle] button {\n padding: 2px 6px;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--md-sys-color-primary);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n }\n p[joint-handle] button:hover {\n background: var(--md-sys-color-surface-container-highest);\n }\n p[same-measure] {\n position: absolute;\n bottom: 12px;\n left: 50%;\n z-index: 5;\n transform: translateX(-50%);\n margin: 0;\n padding: 4px 10px;\n border-radius: 999em;\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-small, 11.5px);\n pointer-events: none;\n white-space: nowrap;\n }\n /* A new figure's first question, over the empty canvas. */\n div[start] {\n position: absolute;\n inset: 50% auto auto 50%;\n z-index: 10;\n color: var(--md-sys-color-on-surface);\n transform: translate(-50%, -50%);\n width: min(440px, calc(100% - 32px));\n box-sizing: border-box;\n padding: 20px;\n border-radius: 12px;\n background: var(--md-sys-color-surface, #fff);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);\n }\n div[start] button[start-button] {\n width: auto;\n margin: 0;\n font-size: var(--ui-t-body);\n height: 36px;\n padding: 0 16px;\n border: 1px solid var(--md-sys-color-outline);\n border-radius: 999em;\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface, #fff);\n cursor: pointer;\n }\n div[start] button[start-button]:hover:not(:disabled) {\n background: var(--md-sys-color-primary-container);\n }\n div[start] button[start-button][pressed] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: var(--md-sys-color-primary);\n }\n div[start] button[start-button]:disabled {\n opacity: 0.5;\n cursor: default;\n }\n div[start] h2 {\n margin: 0 0 12px;\n font-size: var(--ui-t-title);\n }\n div[start-paths] {\n display: grid;\n gap: 14px;\n }\n div[start-paths] h3 {\n margin: 0 0 6px;\n font-size: var(--ui-t-body);\n }\n div[start-shapes] {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n div[start] small {\n display: block;\n margin-top: 4px;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[start] button[start-more] {\n display: block;\n text-align: left;\n margin: 4px 0 0;\n padding: 0;\n font: inherit;\n font-size: var(--ui-t-small);\n color: var(--md-sys-color-primary);\n background: none;\n border: none;\n cursor: pointer;\n }\n /* No rule under the last way to start: it closed nothing (sweep 2026-10-27 U23). */\n div[start-paths] > section:last-of-type {\n border-bottom: none;\n }\n /* The side: tabs over one panel at a time, each using the whole height. */\n /*\n The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body\n for the bar, and the fields the editor drew itself -- the part's name -- stood a step larger than every other field.\n */\n aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n font-size: var(--ui-t-small, 11.5px);\n background: var(--md-sys-color-surface-container-lowest);\n }\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n min-width: 0;\n height: 48px;\n padding: 0;\n border: none;\n border-radius: 0;\n border-bottom: 2px solid transparent;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n button[tab]:hover:not([on]) {\n background: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n button[tab] span[tab-label] {\n max-width: 100%;\n overflow: hidden;\n font-size: var(--ui-t-micro, 11px);\n line-height: 1.2;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n /* A range's two ends read as one: smallest – largest. */\n div[pair][range] {\n align-items: center;\n }\n div[pair][range] > span[dash] {\n flex: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */\n div[panel] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0;\n }\n div[panel][flush] {\n padding: 0;\n }\n /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */\n [off] {\n display: none !important;\n }\n /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */\n div[group-fields] {\n display: grid;\n gap: 6px;\n }\n /* The folded 「더 보기」: its title is the fold's handle, at the title's size. */\n section[more] > details > summary {\n padding: 0;\n }\n section[more] > details > summary > h3 {\n margin: 0;\n }\n div[more-body] {\n display: grid;\n gap: 16px;\n margin-top: 12px;\n }\n /*\n The view settings and the release history, opened from the bar over the right of the work: neither is about the\n part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in\n the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).\n */\n :host {\n position: relative;\n }\n div[side-overlay] {\n position: absolute;\n top: var(--figure-bar-height, 44px);\n right: 8px;\n z-index: 5;\n display: flex;\n flex-direction: column;\n width: min(360px, calc(100% - 16px));\n max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);\n background: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);\n }\n div[overlay-head] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[overlay-head] h3 {\n margin: 0;\n }\n div[overlay-head] button[quiet] {\n margin-left: 0;\n }\n [overlay-body] {\n min-height: 0;\n overflow: auto;\n }\n /* Which part the panel speaks of. Above z-index 1: a chip's label (md-filter-chip) sits at 1 and, coming later, was\n drawn over it when the panel scrolled (sweep 2026-10-27 U2). */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n gap: 4px;\n padding: 10px 12px;\n background: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */\n div[subject-summary] {\n display: grid;\n gap: 2px;\n }\n div[subject-summary] b {\n font-size: var(--ui-t-body, 13px);\n font-weight: 600;\n }\n div[subject-summary] b span {\n font-weight: 400;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[subject-summary] small {\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one\n height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도\n 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).\n */\n button {\n font: inherit;\n box-sizing: border-box;\n color: inherit;\n min-width: 0;\n cursor: pointer;\n }\n /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */\n ox-canvas-bar button[release-blocked] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n padding: 0 14px 0 10px;\n border: none;\n /* Round as save and release beside it: one shape for the bar's end. */\n border-radius: 999em;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n }\n ox-canvas-bar button[release-blocked] md-icon {\n --md-icon-size: 18px;\n }\n div[release-blockers] {\n position: absolute;\n top: 46px;\n right: 8px;\n z-index: 6;\n width: min(340px, calc(100% - 16px));\n padding: 8px 10px 10px;\n box-sizing: border-box;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);\n }\n ox-canvas-bar button[publish] {\n border: 1px solid var(--md-sys-color-outline);\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface);\n }\n ox-canvas-bar button[publish][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n .hint {\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n /* A mirror asked about: under its row, the panel's own field rows (what comes with it, its names, how it turns). */\n div[mirror-form] {\n display: grid;\n gap: 6px;\n margin: 4px 0 8px;\n }\n`\n"]}
@@ -94,6 +94,8 @@ export declare class FigureEditor extends LitElement {
94
94
  private middle;
95
95
  /** The gizmo shown on the selected part, as the tools in the bar and W · E · R choose it; none once the part changes. */
96
96
  private gizmoMode?;
97
+ /** 「관절 위치 편집」: while on, a drag on a part that hangs from a joint that moves moves where the joint sits, not its pose. Off on each new selection. */
98
+ private jointPlacing;
97
99
  /** A line saying what a command did besides what was asked; gone at the next command. */
98
100
  private notice;
99
101
  /**
@@ -144,6 +146,11 @@ export declare class FigureEditor extends LitElement {
144
146
  * gone or another figure opened has nothing to go back to: the editor closes.
145
147
  */
146
148
  private leaveSectionEditor;
149
+ /**
150
+ * On a part that hangs from a joint that moves, what a drag does, before it is dragged (review 2026-11-09): the handle
151
+ * that poses it, or why none does and where it is moved from -- and 「관절 위치 편집」 to move where the joint sits.
152
+ */
153
+ private renderJointHandle;
147
154
  /** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
148
155
  private renderSameMeasure;
149
156
  /** The scene toggles: the mode already shown takes the gizmo away. */