@struq/skill 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -100
- package/package.json +5 -3
- package/skills/struq/SKILL.md +168 -165
- package/skills/struq/blocks/CATALOG.md +49 -46
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +2 -1
- package/skills/struq/blocks/backgrounds/background-facets.tsx +5 -3
- package/skills/struq/blocks/backgrounds/background-silk.tsx +6 -2
- package/skills/struq/blocks/catalog.json +50 -5
- package/skills/struq/blocks/film.json +11 -7
- package/skills/struq/blocks/footers/footer-contact.tsx +8 -78
- package/skills/struq/blocks/headers/header-bar.tsx +144 -29
- package/skills/struq/blocks/headers/menu-curtain.tsx +36 -19
- package/skills/struq/blocks/heroes/hero-poster.tsx +36 -10
- package/skills/struq/blocks/kit/HeroCanvas.tsx +6 -2
- package/skills/struq/blocks/kit/SplitHeadline.tsx +2 -1
- package/skills/struq/blocks/kit/accent.ts +29 -3
- package/skills/struq/blocks/kit/blocks.css +530 -6
- package/skills/struq/blocks/kit/feel.ts +183 -0
- package/skills/struq/blocks/kit/gsap.ts +9 -6
- package/skills/struq/blocks/kit/middle/PlaceHours.tsx +254 -0
- package/skills/struq/blocks/kit/middle/ProcessStrip.tsx +428 -0
- package/skills/struq/blocks/kit/middle/ServicesList.tsx +496 -0
- package/skills/struq/blocks/kit/middle/entrance.ts +98 -0
- package/skills/struq/blocks/kit/middle/hours.ts +154 -0
- package/skills/struq/blocks/kit/middle/index.ts +14 -0
- package/skills/struq/blocks/kit/motion/HoverLink.tsx +33 -0
- package/skills/struq/blocks/kit/motion/ImageCurtain.tsx +79 -0
- package/skills/struq/blocks/kit/motion/MaskWipe.tsx +91 -0
- package/skills/struq/blocks/kit/motion/Pressable.tsx +58 -0
- package/skills/struq/blocks/kit/motion/RevealText.tsx +103 -0
- package/skills/struq/blocks/kit/motion/Rise.tsx +77 -0
- package/skills/struq/blocks/kit/motion/ScrubLine.tsx +108 -0
- package/skills/struq/blocks/kit/motion/index.ts +11 -0
- package/skills/struq/blocks/kit/quiet/Close.tsx +178 -52
- package/skills/struq/blocks/kit/quiet/Statement.tsx +3 -2
- package/skills/struq/blocks/kit/subject/AppScreen.tsx +388 -0
- package/skills/struq/blocks/kit/subject/Document.tsx +742 -0
- package/skills/struq/blocks/kit/subject/Mark.tsx +44 -0
- package/skills/struq/blocks/kit/subject/PatternPaper.tsx +410 -0
- package/skills/struq/blocks/kit/subject/SectionDrawing.tsx +349 -0
- package/skills/struq/blocks/kit/subject/Ticket.tsx +354 -0
- package/skills/struq/blocks/kit/subject/build.ts +66 -0
- package/skills/struq/blocks/kit/subject/draw.ts +196 -0
- package/skills/struq/blocks/kit/subject/drawings.ts +373 -0
- package/skills/struq/blocks/kit/subject/format.ts +122 -0
- package/skills/struq/blocks/kit/subject/index.ts +11 -0
- package/skills/struq/blocks/kit/subject/patterns.ts +204 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +75 -22
- package/skills/struq/blocks/showpieces/hero-opener.tsx +55 -7
- package/skills/struq/blocks/showpieces/image-to-fullbleed.tsx +64 -6
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +56 -3
- package/skills/struq/blocks/showpieces/panel-wipe.tsx +93 -11
- package/skills/struq/blocks/showpieces/wordmark-finale.tsx +38 -5
- package/skills/struq/canon/canon.json +1 -1
- package/skills/struq/references/art-direction.md +51 -0
- package/skills/struq/scripts/blocks.mjs +56 -20
- package/skills/struq/scripts/canon.mjs +36 -7
- package/skills/struq/scripts/check.mjs +417 -11
- package/skills/struq/scripts/draw.mjs +49 -0
- package/skills/struq/scripts/film-check.mjs +28 -4
- package/skills/struq/scripts/gate.mjs +13 -3
- package/skills/struq/scripts/hand.mjs +222 -0
- package/skills/struq/scripts/images.mjs +12 -3
- package/skills/struq/scripts/look.mjs +1184 -0
- package/skills/struq/scripts/stamp.mjs +47 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// components/struq/kit/subject/drawings.ts (Struq recipe)
|
|
2
|
+
// The section drawings SectionDrawing ships, in millimetres (y down), each drawn from how the real thing is made and
|
|
3
|
+
// checked against it:
|
|
4
|
+
// - compression-fitting: a 15 mm straight coupling cut along its axis. Copper pipe (15 OD, 0.7 wall, drawn solid as
|
|
5
|
+
// thin sections are) pushed into the body's socket up to the pipe stop; the olive (a barrel ring, both ends
|
|
6
|
+
// tapered) sits on the pipe between the nut's inner cone and the body's seat at the socket mouth; the nut is
|
|
7
|
+
// threaded onto the body's male thread (thread crest thick, root thin); the hex in the middle. Upper and lower
|
|
8
|
+
// halves of one part hatched alike, touching parts hatched the other way, the olive at a third angle.
|
|
9
|
+
// - mug: a thrown mug in half section, the profile on the left of the centre line, the outside on the right. Wall 5,
|
|
10
|
+
// rim 4 and rounded, base 4.5 over a recessed underside, a 6 mm foot ring with a bevel, the inner corner filleted;
|
|
11
|
+
// the handle pulled and joined at two attachments on the outside, with a revolved section of the handle drawn on
|
|
12
|
+
// it.
|
|
13
|
+
// - bowl: a footed bowl in half section: wall about 5, rim rounded, foot ring 6 wide and 5 deep, base 6.
|
|
14
|
+
// - door-jamb: a horizontal section through an internal door frame in a stud wall: plasterboard on both faces, the
|
|
15
|
+
// stud (structural timber, crossed), mineral wool between studs, packers, the jamb lining flush with both faces,
|
|
16
|
+
// the planted stop, the door leaf closed against it 3 mm off the jamb, an architrave on each face set back 6 mm
|
|
17
|
+
// from the lining's edge (the reveal).
|
|
18
|
+
// - wall: a cavity wall in horizontal section, outside to inside: facing brick 102, a 50 clear cavity, 100 mineral
|
|
19
|
+
// wool batts (partial fill), 100 concrete block inner leaf, 13 plaster; a stainless wall tie with its drip across
|
|
20
|
+
// the cavity. Break lines where the strip is cut.
|
|
21
|
+
// Parts carry a material (SectionDrawing hatches it to the standard pattern), the callouts a key into `names`.
|
|
22
|
+
import type { Pt } from "./draw";
|
|
23
|
+
|
|
24
|
+
export type Material =
|
|
25
|
+
| "steel"
|
|
26
|
+
| "brass"
|
|
27
|
+
| "copper"
|
|
28
|
+
| "wood"
|
|
29
|
+
| "timber"
|
|
30
|
+
| "ceramic"
|
|
31
|
+
| "insulation"
|
|
32
|
+
| "concrete"
|
|
33
|
+
| "block"
|
|
34
|
+
| "brick"
|
|
35
|
+
| "plaster"
|
|
36
|
+
| "solid"
|
|
37
|
+
| "void";
|
|
38
|
+
|
|
39
|
+
export type DrawingPart = {
|
|
40
|
+
/** The cut face to hatch, an SVG path in drawing units. */
|
|
41
|
+
d: string;
|
|
42
|
+
material: Material;
|
|
43
|
+
/** Hatch angle in degrees (45 by default). Touching parts take different angles. */
|
|
44
|
+
angle?: number;
|
|
45
|
+
/** The edge to draw at outline weight; the face's own outline when left out (a strip's break ends stay thin). */
|
|
46
|
+
stroke?: string;
|
|
47
|
+
/** For "timber": the box the cross is drawn corner to corner in. */
|
|
48
|
+
box?: [number, number, number, number];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type DrawingLine = { d: string; kind: "visible" | "thin" | "center" | "hidden" };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A dimension: the distance between a and b measured along x or y, its line at `at` (the y of an x dimension, the x
|
|
55
|
+
* of a y dimension). `prefix` for a diameter ("Ø"). `text` overrides the measured value.
|
|
56
|
+
*/
|
|
57
|
+
export type DrawingDim = { a: Pt; b: Pt; axis: "x" | "y"; at: number; prefix?: string; text?: string };
|
|
58
|
+
|
|
59
|
+
/** A numbered callout: the leader's dot on the part (`at`), the balloon (`to`), the key of its name. */
|
|
60
|
+
export type DrawingCallout = { key: string; at: Pt; to: Pt };
|
|
61
|
+
|
|
62
|
+
export type Drawing = {
|
|
63
|
+
/** x, y, width, height of the drawing area, in drawing units. */
|
|
64
|
+
view: [number, number, number, number];
|
|
65
|
+
parts: DrawingPart[];
|
|
66
|
+
lines?: DrawingLine[];
|
|
67
|
+
dims?: DrawingDim[];
|
|
68
|
+
callouts?: DrawingCallout[];
|
|
69
|
+
/** The callouts' names, by key. */
|
|
70
|
+
names?: Record<string, string>;
|
|
71
|
+
/** The drawing's default title. */
|
|
72
|
+
title?: string;
|
|
73
|
+
/** The unit the dimensions are in. */
|
|
74
|
+
unit?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type DrawingName = "compression-fitting" | "mug" | "bowl" | "door-jamb" | "wall";
|
|
78
|
+
|
|
79
|
+
const f = (n: number) => Math.round(n * 100) / 100;
|
|
80
|
+
const poly = (pts: readonly Pt[], close = true) =>
|
|
81
|
+
pts.map((p, i) => `${i === 0 ? "M" : "L"}${f(p[0])} ${f(p[1])}`).join(" ") + (close ? " Z" : "");
|
|
82
|
+
const rect = (x: number, y: number, w: number, h: number) => poly([[x, y], [x + w, y], [x + w, y + h], [x, y + h]]);
|
|
83
|
+
/** An axially symmetric part's half, given as (x, r) with r up from the axis, mirrored to the other side. */
|
|
84
|
+
const upper = (pts: readonly Pt[]) => pts.map(([x, r]) => [x, -r] as Pt);
|
|
85
|
+
const lower = (pts: readonly Pt[]) => pts.map(([x, r]) => [x, r] as Pt);
|
|
86
|
+
const flipX = (pts: readonly Pt[]) => pts.map(([x, y]) => [-x, y] as Pt);
|
|
87
|
+
|
|
88
|
+
/** A break line across a cut member: a straight run with one sharp zig in the middle. */
|
|
89
|
+
function breakLine(a: Pt, b: Pt, size = 3): string {
|
|
90
|
+
const mx = (a[0] + b[0]) / 2;
|
|
91
|
+
const my = (a[1] + b[1]) / 2;
|
|
92
|
+
const dx = b[0] - a[0];
|
|
93
|
+
const dy = b[1] - a[1];
|
|
94
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
95
|
+
const ux = dx / len;
|
|
96
|
+
const uy = dy / len;
|
|
97
|
+
const nx = -uy;
|
|
98
|
+
const ny = ux;
|
|
99
|
+
const ext = size * 1.2;
|
|
100
|
+
return poly(
|
|
101
|
+
[
|
|
102
|
+
[a[0] - ux * ext, a[1] - uy * ext],
|
|
103
|
+
[mx - ux * size, my - uy * size],
|
|
104
|
+
[mx - ux * size * 0.3 + nx * size, my - uy * size * 0.3 + ny * size],
|
|
105
|
+
[mx + ux * size * 0.3 - nx * size, my + uy * size * 0.3 - ny * size],
|
|
106
|
+
[mx + ux * size, my + uy * size],
|
|
107
|
+
[b[0] + ux * ext, b[1] + uy * ext],
|
|
108
|
+
],
|
|
109
|
+
false,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The batt insulation symbol: a coil whose loops span the band across its width and run along its length, the way
|
|
115
|
+
* mineral wool is drawn in section.
|
|
116
|
+
*/
|
|
117
|
+
function coil(x0: number, y0: number, x1: number, y1: number, along: "x" | "y"): string {
|
|
118
|
+
const width = along === "y" ? x1 - x0 : y1 - y0;
|
|
119
|
+
const length = along === "y" ? y1 - y0 : x1 - x0;
|
|
120
|
+
const pitch = width * 0.24;
|
|
121
|
+
const k = pitch / (2 * Math.PI);
|
|
122
|
+
const r = pitch * 0.42;
|
|
123
|
+
const R = width / 2 - width * 0.06;
|
|
124
|
+
const turns = Math.floor((length - 2 * r) / pitch);
|
|
125
|
+
const used = turns * pitch;
|
|
126
|
+
const start = (length - used) / 2;
|
|
127
|
+
const pts: Pt[] = [];
|
|
128
|
+
const steps = turns * 36;
|
|
129
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
130
|
+
const t = (i / 36) * 2 * Math.PI;
|
|
131
|
+
const across = R * Math.cos(t);
|
|
132
|
+
const run = start + k * t - r * Math.sin(t);
|
|
133
|
+
pts.push(along === "y" ? [x0 + width / 2 + across, y0 + run] : [x0 + run, y0 + width / 2 + across]);
|
|
134
|
+
}
|
|
135
|
+
return poly(pts, false);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function compressionFitting(): Drawing {
|
|
139
|
+
// One side of the coupling as (x, r); the right side is its mirror.
|
|
140
|
+
const nut: Pt[] = [
|
|
141
|
+
[-34, 7.7], [-31, 7.7], [-31, 8.1], [-26, 9.1], [-26, 9.6], [-24, 9.6], [-24, 10.5], [-9, 10.5],
|
|
142
|
+
[-9, 12.4], [-9.8, 13.2], [-33.2, 13.2], [-34, 12.4],
|
|
143
|
+
];
|
|
144
|
+
const olive: Pt[] = [[-30, 7.5], [-30, 8.3], [-26, 9.1], [-22, 8.3], [-22, 7.5]];
|
|
145
|
+
const pipe: Pt[] = [[-48, 6.8], [-3, 6.8], [-3, 7.5], [-48, 7.5]];
|
|
146
|
+
// The body across both sides: socket, seat and thread on the left, mirrored on the right.
|
|
147
|
+
const bodyLeft: Pt[] = [
|
|
148
|
+
[-3, 6.8], [-3, 7.6], [-20, 7.6], [-24, 9.0], [-24, 10.5], [-7, 10.5], [-7, 12.0], [-6.2, 12.8],
|
|
149
|
+
];
|
|
150
|
+
const body: Pt[] = [...bodyLeft, ...flipX(bodyLeft).reverse()];
|
|
151
|
+
const half = (pts: Pt[], side: "upper" | "lower") => (side === "upper" ? upper(pts) : lower(pts));
|
|
152
|
+
const parts: DrawingPart[] = [];
|
|
153
|
+
for (const side of ["upper", "lower"] as const) {
|
|
154
|
+
parts.push({ d: poly(half(body, side)), material: "brass", angle: 45 });
|
|
155
|
+
for (const mirror of [false, true]) {
|
|
156
|
+
const m = (pts: Pt[]) => half(mirror ? flipX(pts) : pts, side);
|
|
157
|
+
parts.push({ d: poly(m(nut)), material: "brass", angle: -45 });
|
|
158
|
+
parts.push({ d: poly(m(olive)), material: "copper", angle: 75 });
|
|
159
|
+
parts.push({ d: poly(m(pipe)), material: "solid" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const lines: DrawingLine[] = [{ d: poly([[-56, 0], [56, 0]], false), kind: "center" }];
|
|
163
|
+
// The thread's root on the body (thin), each side, each half.
|
|
164
|
+
for (const s of [-1, 1]) for (const x of [-1, 1]) lines.push({ d: poly([[x * 24, s * 9.6], [x * 7, s * 9.6]], false), kind: "thin" });
|
|
165
|
+
// The pipe cut short at both ends.
|
|
166
|
+
for (const x of [-48, 48]) lines.push({ d: breakLine([x, -9.5], [x, 9.5], 2.2), kind: "thin" });
|
|
167
|
+
return {
|
|
168
|
+
view: [-60, -31, 120, 55],
|
|
169
|
+
parts,
|
|
170
|
+
lines,
|
|
171
|
+
dims: [
|
|
172
|
+
{ a: [-40, -7.5], b: [-40, 7.5], axis: "y", at: -40, prefix: "Ø" },
|
|
173
|
+
{ a: [-34, 13.2], b: [34, 13.2], axis: "x", at: 21 },
|
|
174
|
+
],
|
|
175
|
+
callouts: [
|
|
176
|
+
{ key: "pipe", at: [-43, -7.15], to: [-47, -24] },
|
|
177
|
+
{ key: "olive", at: [-26.5, -8.6], to: [-34, -24] },
|
|
178
|
+
{ key: "nut", at: [-17, -12], to: [-20, -24] },
|
|
179
|
+
{ key: "body", at: [0, -12.2], to: [0, -24] },
|
|
180
|
+
],
|
|
181
|
+
names: { pipe: "Copper pipe, 15 mm", olive: "Olive", nut: "Nut", body: "Body" },
|
|
182
|
+
title: "Compression coupling, 15 mm",
|
|
183
|
+
unit: "mm",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mug(): Drawing {
|
|
188
|
+
const section =
|
|
189
|
+
"M0 -7 L-30 -7 C-33.3 -7 -36 -9.7 -36 -13 L-36 -80 L-37 -93 C-37 -95.6 -41 -95.6 -41 -93 L-41 -5 L-39 0 L-33 0 L-31.5 -2.5 L0 -2.5 Z";
|
|
190
|
+
const lines: DrawingLine[] = [
|
|
191
|
+
{ d: "M0 0 L39 0 L41 -5 L41 -18", kind: "visible" },
|
|
192
|
+
{ d: "M41 -30 L41 -68", kind: "visible" },
|
|
193
|
+
{ d: "M41 -80 L41 -93 C41 -94.6 40 -95 39 -95 L0 -95", kind: "visible" },
|
|
194
|
+
{ d: "M0 -5 L41 -5", kind: "visible" },
|
|
195
|
+
// The handle: outer and inner edges, joined at the two attachments.
|
|
196
|
+
{ d: "M41 -80 C60 -82 68 -66 67 -50 C66 -34 58 -20 41 -18", kind: "visible" },
|
|
197
|
+
{ d: "M41 -30 C52 -31 56 -40 56 -50 C56 -60 52 -68 41 -68", kind: "visible" },
|
|
198
|
+
{ d: "M0 -103 L0 6", kind: "center" },
|
|
199
|
+
];
|
|
200
|
+
return {
|
|
201
|
+
view: [-80, -116, 186, 130],
|
|
202
|
+
parts: [
|
|
203
|
+
{ d: section, material: "ceramic" },
|
|
204
|
+
// The revolved section of the handle, laid on it.
|
|
205
|
+
{ d: "M57.7 -50 C57.7 -53.3 65.3 -53.3 65.3 -50 C65.3 -46.7 57.7 -46.7 57.7 -50 Z", material: "ceramic", angle: -45 },
|
|
206
|
+
],
|
|
207
|
+
lines,
|
|
208
|
+
dims: [
|
|
209
|
+
{ a: [-41, -93], b: [41, -93], axis: "x", at: -106, prefix: "Ø" },
|
|
210
|
+
{ a: [41, 0], b: [39, -95], axis: "y", at: 82 },
|
|
211
|
+
],
|
|
212
|
+
callouts: [
|
|
213
|
+
{ key: "rim", at: [-39, -94.2], to: [-62, -100] },
|
|
214
|
+
{ key: "wall", at: [-38.5, -60], to: [-62, -64] },
|
|
215
|
+
{ key: "base", at: [-16, -4.8], to: [-62, -28] },
|
|
216
|
+
{ key: "foot", at: [-36, -0.8], to: [-62, -6] },
|
|
217
|
+
{ key: "handle", at: [61.5, -75], to: [66, -96] },
|
|
218
|
+
],
|
|
219
|
+
names: {
|
|
220
|
+
rim: "Rim, rounded, 4 thick",
|
|
221
|
+
wall: "Wall, 5 thick",
|
|
222
|
+
base: "Base over a recessed underside",
|
|
223
|
+
foot: "Foot ring with bevel",
|
|
224
|
+
handle: "Pulled handle, two attachments",
|
|
225
|
+
},
|
|
226
|
+
title: "Mug, half section",
|
|
227
|
+
unit: "mm",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function bowl(): Drawing {
|
|
232
|
+
const section =
|
|
233
|
+
"M0 -11 L-30 -11 C-52 -12 -71 -30 -75 -60 C-75.4 -62.9 -79.6 -62.9 -80 -60 C-77 -30 -58 -9 -37 -7 L-36 0 L-30 0 L-29 -5 L0 -5 Z";
|
|
234
|
+
return {
|
|
235
|
+
view: [-112, -84, 228, 98],
|
|
236
|
+
parts: [{ d: section, material: "ceramic" }],
|
|
237
|
+
lines: [
|
|
238
|
+
{ d: "M0 0 L36 0 L37 -7 C58 -9 77 -30 80 -60 C79.8 -61.4 78.9 -62 77.5 -62 L0 -62", kind: "visible" },
|
|
239
|
+
{ d: "M0 -7 L37 -7", kind: "visible" },
|
|
240
|
+
{ d: "M0 -72 L0 6", kind: "center" },
|
|
241
|
+
],
|
|
242
|
+
dims: [
|
|
243
|
+
{ a: [-80, -60], b: [80, -60], axis: "x", at: -74, prefix: "Ø" },
|
|
244
|
+
{ a: [36, 0], b: [77.5, -62], axis: "y", at: 94 },
|
|
245
|
+
],
|
|
246
|
+
callouts: [
|
|
247
|
+
{ key: "rim", at: [-77.5, -61.8], to: [-98, -50] },
|
|
248
|
+
{ key: "wall", at: [-66, -30], to: [-98, -28] },
|
|
249
|
+
{ key: "foot", at: [-33, -0.8], to: [-98, -6] },
|
|
250
|
+
{ key: "base", at: [-12, -8], to: [-50, 8] },
|
|
251
|
+
],
|
|
252
|
+
names: { rim: "Rim, rounded", wall: "Wall, about 5 thick", foot: "Foot ring, 6 wide", base: "Base, 6 thick" },
|
|
253
|
+
title: "Bowl, half section",
|
|
254
|
+
unit: "mm",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function doorJamb(): Drawing {
|
|
259
|
+
const architrave = (face: 1 | -1): string => {
|
|
260
|
+
// Section of a 70 x 18 architrave: a small round at the opening's edge, a chamfer on the outer edge.
|
|
261
|
+
const y0 = face === 1 ? 0 : 114;
|
|
262
|
+
const s = face === 1 ? -1 : 1;
|
|
263
|
+
return `M22 ${y0} L22 ${y0 + s * 15} Q22 ${y0 + s * 18} 19 ${y0 + s * 18} L-42 ${y0 + s * 18} L-48 ${y0 + s * 12} L-48 ${y0} Z`;
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
view: [-196, -60, 382, 238],
|
|
267
|
+
parts: [
|
|
268
|
+
{ d: rect(-150, 0, 142, 12.5), material: "plaster", stroke: poly([[-150, 0], [-8, 0], [-8, 12.5], [-150, 12.5]], false) },
|
|
269
|
+
{ d: rect(-150, 101.5, 142, 12.5), material: "plaster", stroke: poly([[-150, 101.5], [-8, 101.5], [-8, 114], [-150, 114]], false) },
|
|
270
|
+
{ d: rect(-46, 12.5, 38, 89), material: "timber", box: [-46, 12.5, 38, 89] },
|
|
271
|
+
{ d: rect(-150, 12.5, 104, 89), material: "void", stroke: poly([[-150, 12.5], [-46, 12.5]], false) },
|
|
272
|
+
{ d: rect(-8, 22, 8, 70), material: "wood", angle: 90 },
|
|
273
|
+
{ d: rect(0, 0, 28, 114), material: "wood", angle: 90 },
|
|
274
|
+
{ d: rect(28, 42, 12, 38), material: "wood", angle: 0 },
|
|
275
|
+
{ d: rect(31, 2, 119, 40), material: "wood", angle: 0, stroke: poly([[150, 2], [31, 2], [31, 42], [150, 42]], false) },
|
|
276
|
+
{ d: architrave(1), material: "wood", angle: 0 },
|
|
277
|
+
{ d: architrave(-1), material: "wood", angle: 0 },
|
|
278
|
+
],
|
|
279
|
+
lines: [
|
|
280
|
+
{ d: coil(-150, 12.5, -46, 101.5, "x"), kind: "thin" },
|
|
281
|
+
{ d: breakLine([-150, -4], [-150, 118], 4), kind: "thin" },
|
|
282
|
+
{ d: breakLine([150, -2], [150, 46], 3), kind: "thin" },
|
|
283
|
+
],
|
|
284
|
+
dims: [
|
|
285
|
+
{ a: [-150, 0], b: [-150, 114], axis: "y", at: -172 },
|
|
286
|
+
{ a: [-48, -18], b: [22, -18], axis: "x", at: -40 },
|
|
287
|
+
{ a: [150, 2], b: [150, 42], axis: "y", at: 168 },
|
|
288
|
+
],
|
|
289
|
+
callouts: [
|
|
290
|
+
{ key: "architrave", at: [-20, -10], to: [-120, -38] },
|
|
291
|
+
{ key: "jamb", at: [14, 60], to: [88, 104] },
|
|
292
|
+
{ key: "stop", at: [34, 61], to: [112, 104] },
|
|
293
|
+
{ key: "leaf", at: [90, 22], to: [136, 104] },
|
|
294
|
+
{ key: "stud", at: [-27, 57], to: [-60, 154] },
|
|
295
|
+
{ key: "plasterboard", at: [-100, 107.5], to: [-110, 154] },
|
|
296
|
+
{ key: "insulation", at: [-120, 60], to: [-150, 154] },
|
|
297
|
+
{ key: "packer", at: [-4, 40], to: [-20, 154] },
|
|
298
|
+
],
|
|
299
|
+
names: {
|
|
300
|
+
architrave: "Architrave, 70 x 18",
|
|
301
|
+
jamb: "Jamb lining, 28",
|
|
302
|
+
stop: "Door stop, 38 x 12",
|
|
303
|
+
leaf: "Door leaf, 40",
|
|
304
|
+
stud: "Stud, 38 x 89",
|
|
305
|
+
plasterboard: "Plasterboard, 12.5",
|
|
306
|
+
insulation: "Mineral wool",
|
|
307
|
+
packer: "Packer",
|
|
308
|
+
},
|
|
309
|
+
title: "Door jamb, horizontal section",
|
|
310
|
+
unit: "mm",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function wall(): Drawing {
|
|
315
|
+
const H = 180;
|
|
316
|
+
const layer = (x: number, w: number, material: Material, angle?: number): DrawingPart => ({
|
|
317
|
+
d: rect(x, 0, w, H),
|
|
318
|
+
material,
|
|
319
|
+
angle,
|
|
320
|
+
stroke: `${poly([[x, 0], [x, H]], false)} ${poly([[x + w, 0], [x + w, H]], false)}`,
|
|
321
|
+
});
|
|
322
|
+
const tie = poly([[52, 88.5], [122, 88.5], [127, 95], [132, 88.5], [302, 88.5], [302, 91.5], [132, 91.5], [127, 98], [122, 91.5], [52, 91.5]]);
|
|
323
|
+
return {
|
|
324
|
+
view: [-28, -78, 420, 330],
|
|
325
|
+
parts: [
|
|
326
|
+
layer(0, 102, "brick", 45),
|
|
327
|
+
layer(102, 50, "void"),
|
|
328
|
+
{ d: rect(152, 0, 100, H), material: "void", stroke: `${poly([[152, 0], [152, H]], false)}` },
|
|
329
|
+
layer(252, 100, "block"),
|
|
330
|
+
layer(352, 13, "plaster"),
|
|
331
|
+
{ d: tie, material: "solid" },
|
|
332
|
+
],
|
|
333
|
+
lines: [
|
|
334
|
+
{ d: coil(152, 0, 252, H, "y"), kind: "thin" },
|
|
335
|
+
{ d: breakLine([-6, 0], [371, 0], 5), kind: "thin" },
|
|
336
|
+
{ d: breakLine([-6, H], [371, H], 5), kind: "thin" },
|
|
337
|
+
],
|
|
338
|
+
dims: [
|
|
339
|
+
{ a: [0, 0], b: [102, 0], axis: "x", at: -26 },
|
|
340
|
+
{ a: [102, 0], b: [152, 0], axis: "x", at: -26 },
|
|
341
|
+
{ a: [152, 0], b: [252, 0], axis: "x", at: -26 },
|
|
342
|
+
{ a: [252, 0], b: [352, 0], axis: "x", at: -26 },
|
|
343
|
+
{ a: [352, 0], b: [365, 0], axis: "x", at: -26 },
|
|
344
|
+
{ a: [0, 0], b: [365, 0], axis: "x", at: -56 },
|
|
345
|
+
],
|
|
346
|
+
callouts: [
|
|
347
|
+
{ key: "brick", at: [51, 140], to: [30, 224] },
|
|
348
|
+
{ key: "cavity", at: [127, 140], to: [95, 224] },
|
|
349
|
+
{ key: "insulation", at: [202, 140], to: [160, 224] },
|
|
350
|
+
{ key: "tie", at: [80, 90], to: [225, 224] },
|
|
351
|
+
{ key: "block", at: [302, 140], to: [290, 224] },
|
|
352
|
+
{ key: "plaster", at: [358.5, 140], to: [355, 224] },
|
|
353
|
+
],
|
|
354
|
+
names: {
|
|
355
|
+
brick: "Facing brick, 102",
|
|
356
|
+
cavity: "Clear cavity, 50",
|
|
357
|
+
insulation: "Mineral wool batts, 100",
|
|
358
|
+
tie: "Stainless wall tie with drip",
|
|
359
|
+
block: "Concrete block, 100",
|
|
360
|
+
plaster: "Plaster, 13",
|
|
361
|
+
},
|
|
362
|
+
title: "Cavity wall, horizontal section",
|
|
363
|
+
unit: "mm",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export const DRAWINGS: Record<DrawingName, () => Drawing> = {
|
|
368
|
+
"compression-fitting": compressionFitting,
|
|
369
|
+
mug,
|
|
370
|
+
bowl,
|
|
371
|
+
"door-jamb": doorJamb,
|
|
372
|
+
wall,
|
|
373
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// components/struq/kit/subject/format.ts (Struq recipe)
|
|
2
|
+
// The subject pieces' shared handwriting: money and dates through Intl in the client's locale, initials for a drawn
|
|
3
|
+
// monogram, a seed from a name, and the figure classes every amount carries. Pure functions, no React.
|
|
4
|
+
//
|
|
5
|
+
// money(1250, "nl-NL", "EUR") -> "€ 1.250,00"
|
|
6
|
+
// amount(1250, "nl-NL", "EUR") -> "1.250,00" (the line items' figure, no symbol)
|
|
7
|
+
// dateText("2026-11-15", "en-GB") -> "15 November 2026"
|
|
8
|
+
// initials("Atelier Oude Delft") -> "AO"
|
|
9
|
+
|
|
10
|
+
/** Every amount and figure: tabular lining figures, never broken across lines (blocks.css, the subject section). */
|
|
11
|
+
export const FIGURES = "subject-figures";
|
|
12
|
+
|
|
13
|
+
/** How many fraction digits a currency shows in a locale (EUR 2, JPY 0). */
|
|
14
|
+
export function currencyDigits(locale: string, currency: string): number {
|
|
15
|
+
try {
|
|
16
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
17
|
+
} catch {
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Rounded to the currency's own digits, so a total adds up to the cents it shows. */
|
|
23
|
+
export function round(value: number, digits: number): number {
|
|
24
|
+
const f = 10 ** digits;
|
|
25
|
+
return Math.round((value + Number.EPSILON) * f) / f;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The amount with its currency symbol, set the locale's way. */
|
|
29
|
+
export function money(value: number, locale: string, currency: string): string {
|
|
30
|
+
try {
|
|
31
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return `${currency} ${value.toFixed(2)}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The amount alone, with the currency's digits and the locale's separators: the figure a line item column holds. */
|
|
38
|
+
export function amount(value: number, locale: string, currency: string): string {
|
|
39
|
+
const digits = currencyDigits(locale, currency);
|
|
40
|
+
try {
|
|
41
|
+
return new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return value.toFixed(digits);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A plain number in the locale (a quantity, a rate). */
|
|
48
|
+
export function number(value: number, locale: string, maxDigits = 2): string {
|
|
49
|
+
try {
|
|
50
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: maxDigits }).format(value);
|
|
51
|
+
} catch {
|
|
52
|
+
return String(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An ISO date (2026-11-15) in the locale. Read and written in UTC, so the server and the browser print the same day
|
|
58
|
+
* whatever their time zones. A string that is no ISO date comes back as it went in.
|
|
59
|
+
*/
|
|
60
|
+
export function dateText(iso: string, locale: string, style: "long" | "medium" | "short" = "long"): string {
|
|
61
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
|
|
62
|
+
if (!m) return iso;
|
|
63
|
+
const date = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
64
|
+
try {
|
|
65
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: style, timeZone: "UTC" }).format(date);
|
|
66
|
+
} catch {
|
|
67
|
+
return iso;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Day and month parts of an ISO date, for a ticket's large date figures. */
|
|
72
|
+
export function dateParts(iso: string, locale: string): { day: string; month: string; year: string; weekday: string } | null {
|
|
73
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
|
|
74
|
+
if (!m) return null;
|
|
75
|
+
const date = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
76
|
+
const part = (options: Intl.DateTimeFormatOptions) => {
|
|
77
|
+
try {
|
|
78
|
+
return new Intl.DateTimeFormat(locale, { ...options, timeZone: "UTC" }).format(date);
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return { day: part({ day: "numeric" }), month: part({ month: "short" }), year: m[1], weekday: part({ weekday: "short" }) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Two letters from a name: the first letters of its first two words, or the first two of a single word. */
|
|
87
|
+
export function initials(name: string): string {
|
|
88
|
+
const words = name
|
|
89
|
+
.replace(/[^\p{L}\p{N}\s&-]/gu, "")
|
|
90
|
+
.split(/[\s-]+/)
|
|
91
|
+
.filter((word) => /\p{L}/u.test(word));
|
|
92
|
+
if (words.length === 0) return "";
|
|
93
|
+
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
|
|
94
|
+
return (words[0][0] + words[1][0]).toUpperCase();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The letters of a name, spaces and punctuation out, upper case: what a ticket's pattern is set from. */
|
|
98
|
+
export function lettersOf(name: string): string[] {
|
|
99
|
+
return Array.from(name.toUpperCase().replace(/[^\p{L}\p{N}]/gu, ""));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A small stable number from a text (FNV-1a), so a pattern drawn from a name is the same on every render. */
|
|
103
|
+
export function seed(text: string): number {
|
|
104
|
+
let h = 0x811c9dc5;
|
|
105
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
106
|
+
h ^= text.charCodeAt(i);
|
|
107
|
+
h = Math.imul(h, 0x01000193);
|
|
108
|
+
}
|
|
109
|
+
return h >>> 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A figure stays with its unit and a "No." with its figure: "500 mg", "15 mm", "Nr. 4" never part at a line end. */
|
|
113
|
+
export function keepUnits(text: string): string {
|
|
114
|
+
return text
|
|
115
|
+
.replace(/(\d)\s+(?=[\p{L}%°€£$]{1,4}(?![\p{L}]))/gu, "$1 ")
|
|
116
|
+
.replace(/(\b(?:No|Nr|Rx)\.?)\s+(?=\d)/g, "$1 ");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The language of a locale, for the default labels ("nl-NL" -> "nl"). */
|
|
120
|
+
export function languageOf(locale: string): string {
|
|
121
|
+
return locale.toLowerCase().split(/[-_]/)[0] ?? "en";
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// components/struq/kit/subject/index.ts (Struq recipe)
|
|
2
|
+
// The subject pieces: what the client actually makes, drawn in code at a studio's level so a builder fills data and
|
|
3
|
+
// never draws it: a typeset trade document, a screen of their own software, event tickets and posters, a sewing
|
|
4
|
+
// pattern piece, a technical section drawing. Each is a <figure data-subject> to place inside a section's columns,
|
|
5
|
+
// in the page's palette and faces, built by the scroll and finished without scripts.
|
|
6
|
+
export { AppScreen } from "./AppScreen";
|
|
7
|
+
export { Document } from "./Document";
|
|
8
|
+
export { Mark } from "./Mark";
|
|
9
|
+
export { PatternPaper } from "./PatternPaper";
|
|
10
|
+
export { SectionDrawing } from "./SectionDrawing";
|
|
11
|
+
export { Ticket } from "./Ticket";
|