@things-factory/figure-ui 10.1.91 → 10.1.93

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 (56) hide show
  1. package/client/modeller/editor.ts +61 -90
  2. package/client/modeller/fasten-geometry.ts +152 -0
  3. package/client/modeller/fields.ts +8 -1
  4. package/client/modeller/figure-cavity-drawing.ts +259 -0
  5. package/client/modeller/figure-fasten-drawing.ts +406 -0
  6. package/client/modeller/figure-links.ts +71 -35
  7. package/client/modeller/figure-section-close-choice.ts +52 -0
  8. package/client/modeller/figure-section-editor.ts +58 -3
  9. package/client/modeller/figure-section-panel.ts +5 -0
  10. package/client/modeller/section-controls.ts +19 -0
  11. package/client/modeller/side-panel-styles.ts +4 -11
  12. package/dist-client/modeller/editor.d.ts +10 -4
  13. package/dist-client/modeller/editor.js +59 -88
  14. package/dist-client/modeller/editor.js.map +1 -1
  15. package/dist-client/modeller/fasten-geometry.d.ts +60 -0
  16. package/dist-client/modeller/fasten-geometry.js +138 -0
  17. package/dist-client/modeller/fasten-geometry.js.map +1 -0
  18. package/dist-client/modeller/fields.js +8 -1
  19. package/dist-client/modeller/fields.js.map +1 -1
  20. package/dist-client/modeller/figure-cavity-drawing.d.ts +48 -0
  21. package/dist-client/modeller/figure-cavity-drawing.js +253 -0
  22. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -0
  23. package/dist-client/modeller/figure-fasten-drawing.d.ts +74 -0
  24. package/dist-client/modeller/figure-fasten-drawing.js +452 -0
  25. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -0
  26. package/dist-client/modeller/figure-links.d.ts +7 -0
  27. package/dist-client/modeller/figure-links.js +70 -33
  28. package/dist-client/modeller/figure-links.js.map +1 -1
  29. package/dist-client/modeller/figure-section-close-choice.d.ts +17 -0
  30. package/dist-client/modeller/figure-section-close-choice.js +58 -0
  31. package/dist-client/modeller/figure-section-close-choice.js.map +1 -0
  32. package/dist-client/modeller/figure-section-editor.d.ts +11 -1
  33. package/dist-client/modeller/figure-section-editor.js +57 -3
  34. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  35. package/dist-client/modeller/figure-section-panel.d.ts +1 -0
  36. package/dist-client/modeller/figure-section-panel.js +5 -0
  37. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  38. package/dist-client/modeller/section-controls.d.ts +10 -0
  39. package/dist-client/modeller/section-controls.js +17 -0
  40. package/dist-client/modeller/section-controls.js.map +1 -1
  41. package/dist-client/modeller/side-panel-styles.js +4 -11
  42. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  43. package/dist-client/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -2
  45. package/test/browser/acceptance-page.js +10 -0
  46. package/test/cavity-drawing.test.ts +69 -0
  47. package/test/fasten-drawing.test.ts +45 -0
  48. package/test/fasten-geometry.test.ts +79 -0
  49. package/test/layout.test.ts +19 -4
  50. package/test/links.test.ts +3 -1
  51. package/test/section-editor.test.ts +31 -8
  52. package/translations/en.json +29 -15
  53. package/translations/ja.json +25 -11
  54. package/translations/ko.json +29 -15
  55. package/translations/ms.json +25 -11
  56. package/translations/zh.json +25 -11
@@ -0,0 +1,60 @@
1
+ import type { FigureAsset, FigureFace } from '@hatiolab/figure-model';
2
+ import { type Alignment } from './links.js';
3
+ type Axis = 'x' | 'y' | 'z';
4
+ export type FastenMode = 'meet' | 'flush' | 'centre';
5
+ export type FastenTarget = {
6
+ part: string;
7
+ face: string;
8
+ } | 'plane';
9
+ /**
10
+ * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its
11
+ * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.
12
+ * Undefined when the part has no such face.
13
+ */
14
+ export declare function myFaceFor(asset: FigureAsset, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined;
15
+ /** Which way the fastening is, read back from the part's face and facing. */
16
+ export declare const modeOf: (face: string, facing: "meet" | "flush") => FastenMode;
17
+ /**
18
+ * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each -- none
19
+ * for a plane with no declared size -- the part's footprint on each, and whether its edges can be lined up with.
20
+ */
21
+ export interface FaceView {
22
+ axes: [Axis, Axis];
23
+ face: Partial<Record<Axis, number>>;
24
+ part: Record<Axis, number>;
25
+ edges: Record<Axis, boolean>;
26
+ }
27
+ export declare function faceViewOf(asset: FigureAsset, part: string, target: FastenTarget): FaceView | undefined;
28
+ /** Where the part's centre is from the face's centre along one axis, for the way it is aligned there. */
29
+ export declare function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number;
30
+ /**
31
+ * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:
32
+ * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when
33
+ * within `catchMm`; anywhere else a distance off centre, whole millimetres.
34
+ */
35
+ export declare function alignmentAt(d: number, face: number | undefined, part: number, edges: boolean, catchMm: number): {
36
+ how: Alignment;
37
+ mm?: number;
38
+ caught?: 'centre' | 'inside' | 'outside';
39
+ };
40
+ /** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
41
+ export declare function overhangOf(view: FaceView, at: Record<Axis, number>): {
42
+ touching: boolean;
43
+ sides: Array<{
44
+ axis: Axis;
45
+ side: 'low' | 'high';
46
+ mm: number;
47
+ }>;
48
+ };
49
+ /** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */
50
+ export declare const turnsInPlace: (asset: FigureAsset, part: string) => boolean;
51
+ /**
52
+ * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up
53
+ * for Y, which rises, so a front face is seen as it stands and not upside down.
54
+ */
55
+ export declare function screenOf(axes: readonly [Axis, Axis]): {
56
+ h: Axis;
57
+ v: Axis;
58
+ sign: Record<Axis, 1 | -1>;
59
+ };
60
+ export {};
@@ -0,0 +1,138 @@
1
+ import { figurePartFaceOf } from '@hatiolab/figure-model';
2
+ import { boxOf } from './align.js';
3
+ import { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf } from './links.js';
4
+ const AXES = ['x', 'y', 'z'];
5
+ const OPPOSITE = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' };
6
+ const NORMAL = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] };
7
+ /** The world direction a part's face looks along now, turns and all; by its name when the face's outline is not read. */
8
+ function directionOf(asset, part, face) {
9
+ const area = (() => {
10
+ try {
11
+ return figurePartFaceOf(asset, part, face);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ })();
17
+ if (area) {
18
+ const k = area.normal.findIndex(v => Math.abs(v) > 0.5);
19
+ if (k >= 0)
20
+ return [AXES[k], area.normal[k] > 0 ? 1 : -1];
21
+ }
22
+ return face in NORMAL ? NORMAL[face] : undefined;
23
+ }
24
+ /**
25
+ * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its
26
+ * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.
27
+ * Undefined when the part has no such face.
28
+ */
29
+ export function myFaceFor(asset, part, target, mode) {
30
+ if (mode === 'centre')
31
+ return target === 'plane' ? undefined : 'centre';
32
+ const want = target === 'plane'
33
+ ? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]
34
+ : (() => {
35
+ const d = directionOf(asset, target.part, target.face);
36
+ return d && [d[0], (mode === 'meet' ? -d[1] : d[1])];
37
+ })();
38
+ if (!want)
39
+ return undefined;
40
+ const mine = facesOf(asset, part).filter((f) => f in NORMAL);
41
+ const hit = mine.find(f => {
42
+ const d = directionOf(asset, part, f);
43
+ return d && d[0] === want[0] && d[1] === want[1];
44
+ });
45
+ return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face] : undefined);
46
+ }
47
+ /** Which way the fastening is, read back from the part's face and facing. */
48
+ export const modeOf = (face, facing) => (face === 'centre' ? 'centre' : facing);
49
+ export function faceViewOf(asset, part, target) {
50
+ const across = alignAxesOf(asset, part, target);
51
+ if (across.length !== 2)
52
+ return undefined;
53
+ const box = boxOf(asset, part);
54
+ if (!box)
55
+ return undefined;
56
+ const axes = [across[0].axis, across[1].axis];
57
+ const face = {};
58
+ if (target === 'plane') {
59
+ for (const a of axes)
60
+ if (hasInstanceSize(asset, a))
61
+ face[a] = asset.designInputs[`size.${a}`];
62
+ }
63
+ else {
64
+ const theirs = boxOf(asset, target.part);
65
+ const normal = faceAxisOf(asset, target.part, target.face);
66
+ for (const a of axes)
67
+ if (theirs && a !== normal)
68
+ face[a] = theirs.extent[a];
69
+ }
70
+ return {
71
+ axes,
72
+ face,
73
+ part: { x: box.extent.x, y: box.extent.y, z: box.extent.z },
74
+ edges: Object.fromEntries(across.map(o => [o.axis, o.edges]))
75
+ };
76
+ }
77
+ /** Where the part's centre is from the face's centre along one axis, for the way it is aligned there. */
78
+ export function offsetOf(how, mm, face, part) {
79
+ if (how === 'min' && face !== undefined)
80
+ return -(face - part) / 2;
81
+ if (how === 'max' && face !== undefined)
82
+ return (face - part) / 2;
83
+ if (how === 'mm' || how === 'share')
84
+ return mm ?? 0;
85
+ return 0;
86
+ }
87
+ /**
88
+ * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:
89
+ * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when
90
+ * within `catchMm`; anywhere else a distance off centre, whole millimetres.
91
+ */
92
+ export function alignmentAt(d, face, part, edges, catchMm) {
93
+ if (Math.abs(d) <= catchMm)
94
+ return { how: 'centre', caught: 'centre' };
95
+ if (face !== undefined) {
96
+ const inside = (face - part) / 2, outside = (face + part) / 2;
97
+ for (const [at, how] of [
98
+ [-inside, 'min'],
99
+ [inside, 'max']
100
+ ])
101
+ if (Math.abs(d - at) <= catchMm && inside > 0)
102
+ return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' };
103
+ for (const at of [-outside, outside])
104
+ if (Math.abs(d - at) <= catchMm)
105
+ return { how: 'mm', mm: Math.round(at), caught: 'outside' };
106
+ }
107
+ return { how: 'mm', mm: Math.round(d) };
108
+ }
109
+ /** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
110
+ export function overhangOf(view, at) {
111
+ const sides = [];
112
+ let touching = true;
113
+ for (const a of view.axes) {
114
+ const face = view.face[a];
115
+ if (face === undefined)
116
+ continue;
117
+ const lo = at[a] - view.part[a] / 2, hi = at[a] + view.part[a] / 2;
118
+ if (lo >= face / 2 || hi <= -face / 2)
119
+ touching = false;
120
+ if (-face / 2 - lo > 0.5)
121
+ sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) });
122
+ if (hi - face / 2 > 0.5)
123
+ sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) });
124
+ }
125
+ return { touching, sides };
126
+ }
127
+ /** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */
128
+ export const turnsInPlace = (asset, part) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.body1.face === 'centre'));
129
+ /**
130
+ * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up
131
+ * for Y, which rises, so a front face is seen as it stands and not upside down.
132
+ */
133
+ export function screenOf(axes) {
134
+ const h = axes.includes('x') ? 'x' : 'z';
135
+ const v = axes.find(a => a !== h);
136
+ return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } };
137
+ }
138
+ //# sourceMappingURL=fasten-geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figurePartFaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFace } from '@hatiolab/figure-model'\n\nimport { boxOf } from './align.js'\nimport { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf, type Alignment } from './links.js'\n\n/*\n * What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part\n * lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part's\n * own face follows from those -- the kernel takes no other pairing (figure-model `attach`: meet is the facing face,\n * flush the same one, a face along another axis would need a turn).\n */\n\ntype Axis = 'x' | 'y' | 'z'\nconst AXES: Axis[] = ['x', 'y', 'z']\nexport type FastenMode = 'meet' | 'flush' | 'centre'\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\nconst OPPOSITE: Record<FigureFace, FigureFace> = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' }\nconst NORMAL: Record<FigureFace, [Axis, 1 | -1]> = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] }\n\n/** The world direction a part's face looks along now, turns and all; by its name when the face's outline is not read. */\nfunction directionOf(asset: FigureAsset, part: string, face: string): [Axis, 1 | -1] | undefined {\n const area = (() => {\n try {\n return figurePartFaceOf(asset, part, face)\n } catch {\n return undefined\n }\n })()\n if (area) {\n const k = area.normal.findIndex(v => Math.abs(v) > 0.5)\n if (k >= 0) return [AXES[k]!, area.normal[k]! > 0 ? 1 : -1]\n }\n return face in NORMAL ? NORMAL[face as FigureFace] : undefined\n}\n\n/**\n * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its\n * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.\n * Undefined when the part has no such face.\n */\nexport function myFaceFor(asset: FigureAsset, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined {\n if (mode === 'centre') return target === 'plane' ? undefined : 'centre'\n const want: [Axis, 1 | -1] | undefined =\n target === 'plane'\n ? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]\n : (() => {\n const d = directionOf(asset, target.part, target.face)\n return d && [d[0], (mode === 'meet' ? -d[1] : d[1]) as 1 | -1]\n })()\n if (!want) return undefined\n const mine = facesOf(asset, part).filter((f): f is FigureFace => f in NORMAL)\n const hit = mine.find(f => {\n const d = directionOf(asset, part, f)\n return d && d[0] === want[0] && d[1] === want[1]\n })\n return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face as FigureFace] : undefined)\n}\n\n/** Which way the fastening is, read back from the part's face and facing. */\nexport const modeOf = (face: string, facing: 'meet' | 'flush'): FastenMode => (face === 'centre' ? 'centre' : facing)\n\n/**\n * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each -- none\n * for a plane with no declared size -- the part's footprint on each, and whether its edges can be lined up with.\n */\nexport interface FaceView {\n axes: [Axis, Axis]\n face: Partial<Record<Axis, number>>\n part: Record<Axis, number>\n edges: Record<Axis, boolean>\n}\n\nexport function faceViewOf(asset: FigureAsset, part: string, target: FastenTarget): FaceView | undefined {\n const across = alignAxesOf(asset, part, target)\n if (across.length !== 2) return undefined\n const box = boxOf(asset, part)\n if (!box) return undefined\n const axes = [across[0]!.axis, across[1]!.axis] as [Axis, Axis]\n const face: Partial<Record<Axis, number>> = {}\n if (target === 'plane') {\n for (const a of axes) if (hasInstanceSize(asset, a)) face[a] = asset.designInputs[`size.${a}`]!\n } else {\n const theirs = boxOf(asset, target.part)\n const normal = faceAxisOf(asset, target.part, target.face)\n for (const a of axes) if (theirs && a !== normal) face[a] = theirs.extent[a]\n }\n return {\n axes,\n face,\n part: { x: box.extent.x, y: box.extent.y, z: box.extent.z },\n edges: Object.fromEntries(across.map(o => [o.axis, o.edges])) as Record<Axis, boolean>\n }\n}\n\n/** Where the part's centre is from the face's centre along one axis, for the way it is aligned there. */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n if (how === 'min' && face !== undefined) return -(face - part) / 2\n if (how === 'max' && face !== undefined) return (face - part) / 2\n if (how === 'mm' || how === 'share') return mm ?? 0\n return 0\n}\n\n/**\n * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:\n * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when\n * within `catchMm`; anywhere else a distance off centre, whole millimetres.\n */\nexport function alignmentAt(d: number, face: number | undefined, part: number, edges: boolean, catchMm: number): { how: Alignment; mm?: number; caught?: 'centre' | 'inside' | 'outside' } {\n if (Math.abs(d) <= catchMm) return { how: 'centre', caught: 'centre' }\n if (face !== undefined) {\n const inside = (face - part) / 2,\n outside = (face + part) / 2\n for (const [at, how] of [\n [-inside, 'min'],\n [inside, 'max']\n ] as const)\n if (Math.abs(d - at) <= catchMm && inside > 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }\n for (const at of [-outside, outside]) if (Math.abs(d - at) <= catchMm) return { how: 'mm', mm: Math.round(at), caught: 'outside' }\n }\n return { how: 'mm', mm: Math.round(d) }\n}\n\n/** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> = []\n let touching = true\n for (const a of view.axes) {\n const face = view.face[a]\n if (face === undefined) continue\n const lo = at[a] - view.part[a] / 2,\n hi = at[a] + view.part[a] / 2\n if (lo >= face / 2 || hi <= -face / 2) touching = false\n if (-face / 2 - lo > 0.5) sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) })\n if (hi - face / 2 > 0.5) sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) })\n }\n return { touching, sides }\n}\n\n/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */\nexport const turnsInPlace = (asset: FigureAsset, part: string) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.body1.face === 'centre'))\n\n/**\n * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up\n * for Y, which rises, so a front face is seen as it stands and not upside down.\n */\nexport function screenOf(axes: readonly [Axis, Axis]): { h: Axis; v: Axis; sign: Record<Axis, 1 | -1> } {\n const h = axes.includes('x') ? 'x' : 'z'\n const v = axes.find(a => a !== h)!\n return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } as Record<Axis, 1 | -1> }\n}\n"]}
@@ -27,7 +27,14 @@ export function groupFields(fields, part) {
27
27
  AXES.forEach((axis, i) => byId.has(ids[i]) && (cells[axis] = byId.get(ids[i])));
28
28
  vectors.push({ kind: 'vector', name, cells });
29
29
  }
30
- const singles = own.filter(f => !used.has(f.id)).map(field => ({ kind: 'single', name: nameOf(field.id.slice(prefix.length)), field }));
30
+ /*
31
+ A drawn section's points and a pyramid's apex are edited in the section editor, where a move is checked as it is made
32
+ and the points stand in the outline's order (user 2026-10-01): not here, one field each, in the order they were made.
33
+ */
34
+ const drawn = (id) => /^(point\.[^.]+|apex)\.[xyz]$/.test(id.slice(prefix.length));
35
+ const singles = own
36
+ .filter(f => !used.has(f.id) && !drawn(f.id))
37
+ .map(field => ({ kind: 'single', name: nameOf(field.id.slice(prefix.length)), field }));
31
38
  return { vectors, singles };
32
39
  }
33
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"fields.js","sourceRoot":"","sources":["../../client/modeller/fields.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The values a part or the figure is edited by, grouped: position, rotation and size\n * as one row of three cells each (X · Y · Z read together), and the shape's own measures one to a line under their\n * own title. Kept apart from the editor so the grouping is tested without a browser.\n */\nexport interface FieldLike {\n id: string\n unit: string\n role?: string\n value: number\n}\n\nexport type Axis3 = 'x' | 'y' | 'z'\nconst AXES: readonly Axis3[] = ['x', 'y', 'z']\n\nexport type FieldGroup =\n /* A cell is missing where a rule sets that axis instead of a number: a floor part's height position (ADR-0097). */\n | { kind: 'vector'; name: 'position' | 'rotation' | 'size'; cells: Partial<Record<Axis3, FieldLike>> }\n | { kind: 'single'; name: string; field: FieldLike }\n\n/** The name a single value goes by: its id without the part, `dimension.` and corner numbers read as words. */\nfunction nameOf(rest: string): string {\n return rest.replace(/^dimension\\./, '')\n}\n\n/**\n * The part's own values (`<part>.…`), or with no part the figure's (`size.x` …). Position, rotation and size come\n * first, each as one row when any of its axes is there -- an axis a rule sets shows as held, not as a stray single\n * further down; the rest follow in the order they are declared.\n */\nexport function groupFields(fields: readonly FieldLike[], part?: string): { vectors: FieldGroup[]; singles: FieldGroup[] } {\n /*\n A part is always placed and turned, so its position and turn rows stand even when a rule sets every axis (a\n fastened part): the screen shows the numbers the rule gives. Its size row stands only when it has a field of its own.\n */\n const prefix = part ? `${part}.` : ''\n const own = fields.filter(f => (part ? f.id.startsWith(prefix) : !f.id.includes('.') || /^size\\.[xyz]$/.test(f.id)))\n const byId = new Map(own.map(f => [f.id, f]))\n const used = new Set<string>()\n const vectors: FieldGroup[] = []\n for (const name of ['position', 'rotation', 'size'] as const) {\n const ids = AXES.map(axis => `${prefix}${name}.${axis}`)\n if (!ids.some(id => byId.has(id)) && !(part && name !== 'size')) continue\n ids.forEach(id => used.add(id))\n const cells: Partial<Record<Axis3, FieldLike>> = {}\n AXES.forEach((axis, i) => byId.has(ids[i]!) && (cells[axis] = byId.get(ids[i]!)!))\n vectors.push({ kind: 'vector', name, cells })\n }\n const singles: FieldGroup[] = own.filter(f => !used.has(f.id)).map(field => ({ kind: 'single', name: nameOf(field.id.slice(prefix.length)), field }))\n return { vectors, singles }\n}\n\n/**\n * The part a copy copies: the member at the end of its path. A row repeats its part and what is fastened to it\n * (ADR-0093, 2026-09-26), so the copies of one row are of several parts -- a roller's and the mark on it.\n */\nconst memberOf = (path: unknown[]): string => {\n const last = [...path].reverse().find((step): step is string => typeof step === 'string')!\n return last.replace(/\\.(placed|value)$/, '').replace(/\\.repeat$/, '')\n}\n\n/**\n * The name a person reads for a body. A repeated body's id is its path through the repeats (`[\"roller.repeat\",0,\n * \"roller.placed\"]`), which the list showed as it was; it reads as the part it copies and its place counted from 1 --\n * `roller 1`, and `mark 1` for the mark the first roller carries. Any other id is its own name.\n */\nexport function partLabelOf(id: string): string {\n const path = pathOf(id)\n if (!path) return id\n const places = path.filter((step): step is number => typeof step === 'number').map(n => n + 1)\n return places.length ? `${memberOf(path)} ${places.join('·')}` : id\n}\n\n/**\n * The repeat a body is a copy of: its name, how many copies it makes, and which copy this is (counted from 1). Nothing\n * for a body that is not a copy.\n */\nexport function repeatOf(id: string, geometry: readonly unknown[] | undefined): { name: string; count: number; index: number } | null {\n const path = pathOf(id)\n const paths = (geometry ?? []).map(g => pathOf((g as { id?: string }).id ?? '')).filter((p): p is unknown[] => !!p)\n if (!path) {\n /* The part itself, by its name: how many copies of it the row makes now. */\n const copies = paths.filter(p => memberOf(p) === id).length\n return copies ? { name: id, count: copies, index: 0 } : null\n }\n const name = memberOf(path)\n const count = paths.filter(p => p[0] === path[0] && memberOf(p) === name).length\n const index = path.find((step): step is number => typeof step === 'number')\n return { name, count, index: (index ?? 0) + 1 }\n}\n\nfunction pathOf(id: string): unknown[] | null {\n if (!id.startsWith('[')) return null\n try {\n const path = JSON.parse(id)\n return Array.isArray(path) && typeof path[0] === 'string' ? path : null\n } catch {\n return null\n }\n}\n\n/**\n * The authored part a body belongs to: a repeat's copy belongs to its repeat (`roller`), any other body is its own. The\n * editor selects, lists, hides and names parts by this -- what the author made, not every body the kernel draws\n * (user, 2026-09-24: the conveyor's sixteen rollers were sixteen rows).\n */\nexport function ownerOf(id: string): string {\n const path = pathOf(id)\n return path ? memberOf(path) : id\n}\n\n"]}
1
+ {"version":3,"file":"fields.js","sourceRoot":"","sources":["../../client/modeller/fields.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The values a part or the figure is edited by, grouped: position, rotation and size\n * as one row of three cells each (X · Y · Z read together), and the shape's own measures one to a line under their\n * own title. Kept apart from the editor so the grouping is tested without a browser.\n */\nexport interface FieldLike {\n id: string\n unit: string\n role?: string\n value: number\n}\n\nexport type Axis3 = 'x' | 'y' | 'z'\nconst AXES: readonly Axis3[] = ['x', 'y', 'z']\n\nexport type FieldGroup =\n /* A cell is missing where a rule sets that axis instead of a number: a floor part's height position (ADR-0097). */\n | { kind: 'vector'; name: 'position' | 'rotation' | 'size'; cells: Partial<Record<Axis3, FieldLike>> }\n | { kind: 'single'; name: string; field: FieldLike }\n\n/** The name a single value goes by: its id without the part, `dimension.` and corner numbers read as words. */\nfunction nameOf(rest: string): string {\n return rest.replace(/^dimension\\./, '')\n}\n\n/**\n * The part's own values (`<part>.…`), or with no part the figure's (`size.x` …). Position, rotation and size come\n * first, each as one row when any of its axes is there -- an axis a rule sets shows as held, not as a stray single\n * further down; the rest follow in the order they are declared.\n */\nexport function groupFields(fields: readonly FieldLike[], part?: string): { vectors: FieldGroup[]; singles: FieldGroup[] } {\n /*\n A part is always placed and turned, so its position and turn rows stand even when a rule sets every axis (a\n fastened part): the screen shows the numbers the rule gives. Its size row stands only when it has a field of its own.\n */\n const prefix = part ? `${part}.` : ''\n const own = fields.filter(f => (part ? f.id.startsWith(prefix) : !f.id.includes('.') || /^size\\.[xyz]$/.test(f.id)))\n const byId = new Map(own.map(f => [f.id, f]))\n const used = new Set<string>()\n const vectors: FieldGroup[] = []\n for (const name of ['position', 'rotation', 'size'] as const) {\n const ids = AXES.map(axis => `${prefix}${name}.${axis}`)\n if (!ids.some(id => byId.has(id)) && !(part && name !== 'size')) continue\n ids.forEach(id => used.add(id))\n const cells: Partial<Record<Axis3, FieldLike>> = {}\n AXES.forEach((axis, i) => byId.has(ids[i]!) && (cells[axis] = byId.get(ids[i]!)!))\n vectors.push({ kind: 'vector', name, cells })\n }\n /*\n A drawn section's points and a pyramid's apex are edited in the section editor, where a move is checked as it is made\n and the points stand in the outline's order (user 2026-10-01): not here, one field each, in the order they were made.\n */\n const drawn = (id: string) => /^(point\\.[^.]+|apex)\\.[xyz]$/.test(id.slice(prefix.length))\n const singles: FieldGroup[] = own\n .filter(f => !used.has(f.id) && !drawn(f.id))\n .map(field => ({ kind: 'single', name: nameOf(field.id.slice(prefix.length)), field }))\n return { vectors, singles }\n}\n\n/**\n * The part a copy copies: the member at the end of its path. A row repeats its part and what is fastened to it\n * (ADR-0093, 2026-09-26), so the copies of one row are of several parts -- a roller's and the mark on it.\n */\nconst memberOf = (path: unknown[]): string => {\n const last = [...path].reverse().find((step): step is string => typeof step === 'string')!\n return last.replace(/\\.(placed|value)$/, '').replace(/\\.repeat$/, '')\n}\n\n/**\n * The name a person reads for a body. A repeated body's id is its path through the repeats (`[\"roller.repeat\",0,\n * \"roller.placed\"]`), which the list showed as it was; it reads as the part it copies and its place counted from 1 --\n * `roller 1`, and `mark 1` for the mark the first roller carries. Any other id is its own name.\n */\nexport function partLabelOf(id: string): string {\n const path = pathOf(id)\n if (!path) return id\n const places = path.filter((step): step is number => typeof step === 'number').map(n => n + 1)\n return places.length ? `${memberOf(path)} ${places.join('·')}` : id\n}\n\n/**\n * The repeat a body is a copy of: its name, how many copies it makes, and which copy this is (counted from 1). Nothing\n * for a body that is not a copy.\n */\nexport function repeatOf(id: string, geometry: readonly unknown[] | undefined): { name: string; count: number; index: number } | null {\n const path = pathOf(id)\n const paths = (geometry ?? []).map(g => pathOf((g as { id?: string }).id ?? '')).filter((p): p is unknown[] => !!p)\n if (!path) {\n /* The part itself, by its name: how many copies of it the row makes now. */\n const copies = paths.filter(p => memberOf(p) === id).length\n return copies ? { name: id, count: copies, index: 0 } : null\n }\n const name = memberOf(path)\n const count = paths.filter(p => p[0] === path[0] && memberOf(p) === name).length\n const index = path.find((step): step is number => typeof step === 'number')\n return { name, count, index: (index ?? 0) + 1 }\n}\n\nfunction pathOf(id: string): unknown[] | null {\n if (!id.startsWith('[')) return null\n try {\n const path = JSON.parse(id)\n return Array.isArray(path) && typeof path[0] === 'string' ? path : null\n } catch {\n return null\n }\n}\n\n/**\n * The authored part a body belongs to: a repeat's copy belongs to its repeat (`roller`), any other body is its own. The\n * editor selects, lists, hides and names parts by this -- what the author made, not every body the kernel draws\n * (user, 2026-09-24: the conveyor's sixteen rollers were sixteen rows).\n */\nexport function ownerOf(id: string): string {\n const path = pathOf(id)\n return path ? memberOf(path) : id\n}\n\n"]}
@@ -0,0 +1,48 @@
1
+ import { LitElement, nothing } from 'lit';
2
+ import type { FigureEvaluatedShape } from '@hatiolab/figure-model';
3
+ type Body = {
4
+ id: string;
5
+ shape: FigureEvaluatedShape;
6
+ };
7
+ declare const FigureCavityDrawing_base: (new (...args: any[]) => LitElement) & typeof LitElement;
8
+ /**
9
+ * 속 공간 그림 — the inside of a pushed part drawn, and edited by drawing (user 2026-10-01: 「이해와 편집이 직관적이지 않다」).
10
+ *
11
+ * Seen along the push, the section and its inside: a handle in the middle of each wall is dragged to its thickness,
12
+ * every wall together unless 면마다 벽 is on. Cut along the push, the walls and the two ends: an end pressed opens or
13
+ * closes it, a closed end's inner line is dragged to its thickness. While dragging, the figure behind shows what the
14
+ * inside makes (`command-live`); letting go sends one `set-cavity`. What the kernel would refuse is drawn in the error
15
+ * colour and said, by the kernel's own check, and is not sent.
16
+ */
17
+ export declare class FigureCavityDrawing extends FigureCavityDrawing_base {
18
+ static styles: import("lit").CSSResult;
19
+ body?: Body;
20
+ locked: boolean;
21
+ /** Walls one per side: a handle moves its own wall only. */
22
+ perSide: boolean;
23
+ /** What is being dragged, and the inside it makes; undefined when nothing is. */
24
+ private working?;
25
+ private refusal;
26
+ private dragging;
27
+ private pressedAt;
28
+ private get now();
29
+ private request;
30
+ private action;
31
+ private judge;
32
+ /** The working inside while dragging: drawn, judged, and previewed behind when the kernel would take it. */
33
+ private hold;
34
+ private release;
35
+ private emit;
36
+ private point;
37
+ render(): import("lit-html").TemplateResult<1> | typeof nothing;
38
+ /** Seen along the push: the section, its inside and a handle on each wall. */
39
+ private top;
40
+ /** Cut along the push: the walls across the first plane axis, and the two ends -- pressed to open or close. */
41
+ private side;
42
+ private grab;
43
+ private drag;
44
+ private dragEnd;
45
+ /** A press on an end that did not move is a toggle; a drag was a thickness. */
46
+ private releaseEnd;
47
+ }
48
+ export {};
@@ -0,0 +1,253 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { css, html, LitElement, nothing, svg } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { i18next, localize } from '@operato/i18n';
5
+ import { figureCavitySectionOf } from '@hatiolab/figure-model';
6
+ import { cavityRefusalOf, cavityWorkingOf, END_FACE, endToggled, mm, SIDE_FACE, wallDragged } from './section-controls.js';
7
+ /** How the section is laid on the screen, by the axis it is pushed along: across to the right, then down. */
8
+ const SCREEN = { y: ['x', 'z'], x: ['z', 'y'], z: ['x', 'y'] };
9
+ /**
10
+ * 속 공간 그림 — the inside of a pushed part drawn, and edited by drawing (user 2026-10-01: 「이해와 편집이 직관적이지 않다」).
11
+ *
12
+ * Seen along the push, the section and its inside: a handle in the middle of each wall is dragged to its thickness,
13
+ * every wall together unless 면마다 벽 is on. Cut along the push, the walls and the two ends: an end pressed opens or
14
+ * closes it, a closed end's inner line is dragged to its thickness. While dragging, the figure behind shows what the
15
+ * inside makes (`command-live`); letting go sends one `set-cavity`. What the kernel would refuse is drawn in the error
16
+ * colour and said, by the kernel's own check, and is not sent.
17
+ */
18
+ let FigureCavityDrawing = class FigureCavityDrawing extends localize(i18next)(LitElement) {
19
+ constructor() {
20
+ super(...arguments);
21
+ this.locked = false;
22
+ /** Walls one per side: a handle moves its own wall only. */
23
+ this.perSide = false;
24
+ this.refusal = '';
25
+ this.dragging = null;
26
+ this.pressedAt = 0;
27
+ }
28
+ static { this.styles = css `
29
+ :host {
30
+ display: grid;
31
+ gap: 6px;
32
+ }
33
+ svg {
34
+ width: 100%;
35
+ display: block;
36
+ touch-action: none;
37
+ user-select: none;
38
+ }
39
+ svg text {
40
+ font: inherit;
41
+ }
42
+ small[refusal] {
43
+ color: var(--md-sys-color-error);
44
+ }
45
+ `; }
46
+ get now() {
47
+ return this.working ?? (this.body ? cavityWorkingOf(this.body.shape) : undefined);
48
+ }
49
+ request(w) {
50
+ return { wall: { ...w.wall }, start: w.start, end: w.end };
51
+ }
52
+ action(w) {
53
+ return { kind: 'set-cavity', part: this.body.id, cavity: this.request(w) };
54
+ }
55
+ judge(w) {
56
+ const ask = cavityRefusalOf(this.body.shape, this.request(w));
57
+ if (!ask)
58
+ return '';
59
+ if (ask.code === 'wall-too-thick')
60
+ return i18next.t('figure.text.section-wall-over', { max: String(ask.max) });
61
+ if (ask.code === 'ends-too-thick')
62
+ return i18next.t('figure.text.section-ends-over', { max: String(ask.max) });
63
+ return i18next.t('figure.text.section-not-positive');
64
+ }
65
+ /** The working inside while dragging: drawn, judged, and previewed behind when the kernel would take it. */
66
+ hold(w) {
67
+ this.working = w;
68
+ this.refusal = this.judge(w);
69
+ this.emit('command-live', this.refusal ? null : this.action(w));
70
+ }
71
+ release() {
72
+ const w = this.working;
73
+ this.dragging = null;
74
+ this.working = undefined;
75
+ if (!w)
76
+ return;
77
+ /* Refused: nothing is sent, and the reason stays said until the next drag -- letting go must not look like taking it. */
78
+ if (this.refusal) {
79
+ this.emit('command-live', null);
80
+ return;
81
+ }
82
+ this.emit('command', this.action(w));
83
+ }
84
+ emit(name, action) {
85
+ this.dispatchEvent(new CustomEvent(name, { detail: { action }, bubbles: true, composed: true }));
86
+ }
87
+ point(ev, el) {
88
+ const pt = el.createSVGPoint();
89
+ pt.x = ev.clientX;
90
+ pt.y = ev.clientY;
91
+ return pt.matrixTransform(el.getScreenCTM().inverse());
92
+ }
93
+ render() {
94
+ const e = this.body?.shape.extrusion;
95
+ const w = this.now;
96
+ if (!e || !w || (e.section.kind !== 'rectangle' && e.section.kind !== 'ellipse'))
97
+ return nothing;
98
+ return html `${this.top(e, w)}${this.side(e, w)}
99
+ ${this.refusal ? html `<small refusal role="alert">${this.refusal}</small>` : nothing}`;
100
+ }
101
+ /** Seen along the push: the section, its inside and a handle on each wall. */
102
+ top(e, w) {
103
+ const [h, v] = SCREEN[e.axis];
104
+ const s = e.section;
105
+ const across = (a) => (s.kind === 'rectangle' ? s.size[a] : 2 * s.radius[a]);
106
+ const W = 300, H = 170, pad = 26;
107
+ const k = Math.min((W - 2 * pad) / across(h), (H - 2 * pad) / across(v));
108
+ const ox = W / 2, oy = H / 2;
109
+ const inside = figureCavitySectionOf(s, e.axis, w.wall);
110
+ const bad = !!this.refusal || !('section' in inside);
111
+ const shape = (sec, c, attrs) => {
112
+ const cx = ox + (c[h] ?? 0) * k, cy = oy + (c[v] ?? 0) * k;
113
+ if (sec.kind === 'ellipse')
114
+ return svg `<ellipse cx=${cx} cy=${cy} rx=${sec.radius[h] * k} ry=${sec.radius[v] * k} style=${attrs} />`;
115
+ const r = sec;
116
+ return svg `<rect x=${cx - (r.size[h] * k) / 2} y=${cy - (r.size[v] * k) / 2} width=${r.size[h] * k} height=${r.size[v] * k}
117
+ rx=${(r.corner?.[h] ?? 0) * k} ry=${(r.corner?.[v] ?? 0) * k} style=${attrs} />`;
118
+ };
119
+ const half = { [h]: (across(h) * k) / 2, [v]: (across(v) * k) / 2 };
120
+ /* Each wall's handle sits on the inside's edge, half way along it; the value is the wall's own. */
121
+ const handles = [`${h}-`, `${h}+`, `${v}-`, `${v}+`].map(side => {
122
+ const a = side[0], low = side[1] === '-';
123
+ const edge = (low ? -1 : 1) * (half[a] - w.wall[side] * k);
124
+ const x = a === h ? ox + edge : ox, y = a === v ? oy + edge : oy;
125
+ /* The number stands in the wall itself, half way through it; a wall too thin for it has it just outside. */
126
+ const band = w.wall[side] * k;
127
+ const mid = (low ? -1 : 1) * (half[a] - (band < 16 ? -9 : band / 2));
128
+ const tx = a === h ? ox + mid : x + 14, ty = a === v ? oy + mid + 4 : y - 10;
129
+ return svg `<g wall=${side} style=${`cursor:${a === h ? 'ew-resize' : 'ns-resize'}`}
130
+ @pointerdown=${(ev) => this.grab(ev, { kind: 'wall', side })}>
131
+ <rect x=${x - 6} y=${y - 6} width="12" height="12" rx="2" style="fill:var(--md-sys-color-surface);stroke:var(--md-sys-color-primary);stroke-width:1.5" />
132
+ <text x=${tx} y=${ty} font-size="11" text-anchor=${a === h ? 'middle' : 'start'} style="fill:var(--md-sys-color-on-surface)">${mm(w.wall[side])}</text>
133
+ <title>${i18next.t('figure.label.face-' + SIDE_FACE[side])} ${i18next.t('figure.label.section-wall')}</title>
134
+ </g>`;
135
+ });
136
+ return svg `<svg viewBox=${`0 0 ${W} ${H}`} role="img" aria-label=${i18next.t('figure.label.section-cavity-top')}
137
+ @pointermove=${(ev) => this.drag(ev, e, k, ox, oy, half)}
138
+ @pointerup=${() => this.release()} @pointercancel=${() => this.release()}>
139
+ ${shape(s, {}, 'fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:1.5')}
140
+ ${'section' in inside
141
+ ? shape(inside.section, inside.centre, `fill:var(--md-sys-color-surface);stroke:${bad ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-primary)'};stroke-width:1.2;stroke-dasharray:4 3`)
142
+ : nothing}
143
+ ${this.locked ? nothing : handles}
144
+ </svg>`;
145
+ }
146
+ /** Cut along the push: the walls across the first plane axis, and the two ends -- pressed to open or close. */
147
+ side(e, w) {
148
+ const [h] = SCREEN[e.axis];
149
+ const s = e.section;
150
+ const width = s.kind === 'rectangle' ? s.size[h] : 2 * s.radius[h];
151
+ const length = e.end - e.start;
152
+ const W = 300, H = 170, padX = 40, padY = 26;
153
+ const k = Math.min((W - 2 * padX) / width, (H - 2 * padY) / length);
154
+ const x0 = W / 2 - (width * k) / 2, x1 = W / 2 + (width * k) / 2, yTop = H / 2 - (length * k) / 2, yBot = H / 2 + (length * k) / 2;
155
+ const wl = w.wall[`${h}-`] * k, wr = w.wall[`${h}+`] * k;
156
+ const thick = (end) => (w[end] === 'open' ? 0 : w[end].closed * k);
157
+ /* The push's start is drawn at the bottom: a tote stands on its floor. */
158
+ const floor = thick('start'), lid = thick('end');
159
+ const fill = 'fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:1.2';
160
+ const endZone = (end, y, t) => {
161
+ const open = w[end] === 'open';
162
+ const words = open
163
+ ? i18next.t('figure.label.section-end-open')
164
+ : `${i18next.t('figure.label.section-end-closed')} ${mm(w[end].closed)}`;
165
+ const label = `${i18next.t('figure.label.face-' + END_FACE(e.axis, end))}: ${words}`;
166
+ const inner = end === 'start' ? y - t : y + t;
167
+ return svg `<g end=${end} style=${`cursor:${open ? 'pointer' : 'ns-resize'}`}
168
+ @pointerdown=${(ev) => this.grab(ev, { kind: 'end', end })}>
169
+ <rect x=${x0 + wl} y=${Math.min(y, inner) - 8} width=${x1 - x0 - wl - wr} height=${Math.abs(t) + 16} style="fill:transparent" />
170
+ ${open ? svg `<line x1=${x0 + wl} y1=${y} x2=${x1 - wr} y2=${y} style="stroke:var(--md-sys-color-outline);stroke-dasharray:3 3" />` : nothing}
171
+ <text x=${W / 2} y=${end === 'start' ? yBot + 16 : yTop - 8} font-size="11" text-anchor="middle" style="fill:var(--md-sys-color-primary)">${label}</text>
172
+ <title>${label}</title>
173
+ </g>`;
174
+ };
175
+ return svg `<svg viewBox=${`0 0 ${W} ${H + 8}`} role="img" aria-label=${i18next.t('figure.label.section-cavity-side')}
176
+ @pointermove=${(ev) => this.dragEnd(ev, k, yTop, yBot)}
177
+ @pointerup=${(ev) => this.releaseEnd(ev)} @pointercancel=${() => this.release()}>
178
+ <rect x=${x0} y=${yTop} width=${wl} height=${yBot - yTop} style=${fill} />
179
+ <rect x=${x1 - wr} y=${yTop} width=${wr} height=${yBot - yTop} style=${fill} />
180
+ ${floor ? svg `<rect x=${x0 + wl} y=${yBot - floor} width=${x1 - x0 - wl - wr} height=${floor} style=${fill} />` : nothing}
181
+ ${lid ? svg `<rect x=${x0 + wl} y=${yTop} width=${x1 - x0 - wl - wr} height=${lid} style=${fill} />` : nothing}
182
+ ${this.locked ? nothing : [endZone('start', yBot, floor), endZone('end', yTop, lid)]}
183
+ </svg>`;
184
+ }
185
+ grab(ev, what) {
186
+ if (this.locked)
187
+ return;
188
+ ev.stopPropagation();
189
+ ev.currentTarget.closest('svg').setPointerCapture(ev.pointerId);
190
+ this.dragging = what;
191
+ this.pressedAt = ev.clientY;
192
+ this.refusal = '';
193
+ }
194
+ drag(ev, e, k, ox, oy, half) {
195
+ const d = this.dragging;
196
+ if (d?.kind !== 'wall')
197
+ return;
198
+ const [h] = SCREEN[e.axis];
199
+ const p = this.point(ev, ev.currentTarget);
200
+ const a = d.side[0], low = d.side[1] === '-';
201
+ const at = a === h ? p.x - ox : p.y - oy;
202
+ const value = (half[a] - (low ? -at : at)) / k;
203
+ this.hold({ ...this.now, wall: wallDragged(this.now.wall, d.side, value, this.perSide) });
204
+ }
205
+ dragEnd(ev, k, yTop, yBot) {
206
+ const d = this.dragging;
207
+ if (d?.kind !== 'end' || this.now[d.end] === 'open')
208
+ return;
209
+ if (Math.abs(ev.clientY - this.pressedAt) < 3 && !this.working)
210
+ return;
211
+ const p = this.point(ev, ev.currentTarget);
212
+ const t = d.end === 'start' ? (yBot - p.y) / k : (p.y - yTop) / k;
213
+ this.hold({ ...this.now, [d.end]: { closed: Math.max(1, Math.round(t)) } });
214
+ }
215
+ /** A press on an end that did not move is a toggle; a drag was a thickness. */
216
+ releaseEnd(ev) {
217
+ const d = this.dragging;
218
+ if (d?.kind === 'end' && !this.working && Math.abs(ev.clientY - this.pressedAt) < 3) {
219
+ this.dragging = null;
220
+ const next = { ...this.now, [d.end]: endToggled(this.body.shape, this.now[d.end]) };
221
+ this.refusal = this.judge(next);
222
+ if (!this.refusal)
223
+ this.emit('command', this.action(next));
224
+ return;
225
+ }
226
+ this.release();
227
+ }
228
+ };
229
+ __decorate([
230
+ property({ attribute: false }),
231
+ __metadata("design:type", Object)
232
+ ], FigureCavityDrawing.prototype, "body", void 0);
233
+ __decorate([
234
+ property({ type: Boolean }),
235
+ __metadata("design:type", Object)
236
+ ], FigureCavityDrawing.prototype, "locked", void 0);
237
+ __decorate([
238
+ property({ type: Boolean }),
239
+ __metadata("design:type", Object)
240
+ ], FigureCavityDrawing.prototype, "perSide", void 0);
241
+ __decorate([
242
+ state(),
243
+ __metadata("design:type", void 0)
244
+ ], FigureCavityDrawing.prototype, "working", void 0);
245
+ __decorate([
246
+ state(),
247
+ __metadata("design:type", Object)
248
+ ], FigureCavityDrawing.prototype, "refusal", void 0);
249
+ FigureCavityDrawing = __decorate([
250
+ customElement('figure-cavity-drawing')
251
+ ], FigureCavityDrawing);
252
+ export { FigureCavityDrawing };
253
+ //# sourceMappingURL=figure-cavity-drawing.js.map